Skip to content

expose a session handle so a remote agent can be steered #1287

Description

@shoelessness

What problem are you trying to solve?

I'm building a gateway/orchestrator agent on eve — a chat front door that never does the work itself. It classifies an ask, authorizes it, dispatches it to an execution backend, and relays progress back to the humans in the thread. Backends are heterogeneous: Cursor Cloud Agents today, other eve agents alongside them, more later.

Every backend has to satisfy one executor contract:

interface ExecutorAdapter {
  launch(descriptor): Promise<RunRef>;
  steer(runId, message): Promise<{ delivered: boolean }>;   // send another turn
  status(runId): Promise<{ status, url?, ... }>;
  abort(runId): Promise<{ aborted: boolean }>;
}

Cursor Cloud Agents satisfy all four. defineRemoteAgent satisfies only launch — a remote call is one turn in, one result out, and no handle/id allows for follow-ups, so the other three verbs have nowhere to go.

                    ┌─────────────────────────────────┐
                    │   gateway agent  (eve)          │
                    │   classify · authorize ·        │
                    │   dispatch · relay              │
                    └──────┬───────────────────┬──────┘
                           │                   │
              same contract│                   │same contract
                           ▼                   ▼
        ┌──────────────────────────┐  ┌──────────────────────────┐
        │  Cursor Cloud Agent      │  │  eve agent               │
        │  (HTTP API + run ids)    │  │  (defineRemoteAgent)     │
        ├──────────────────────────┤  ├──────────────────────────┤
        │  launch   ✅             │  │  launch   ✅             │
        │  steer    ✅             │  │  steer    ❌  no handle  │
        │  status   ✅             │  │  status   ❌  no handle  │
        │  abort    ✅             │  │  abort    ❌  no handle  │
        └──────────────────────────┘  └──────────────────────────┘
                    the eve-native backend is the one
                    my gateway can't fully address

In the orchestrator code, I have a union whose second variant can't do three of the four verbs:

type ProfileEntry =
  | { kind: "cloud";    adapter: ExecutorAdapter }   // steerable, checkable, abortable
  | { kind: "subagent"; tool: string };              // fire, wait, done

Everything downstream special-cases the second kind, including prose telling the model to complete the call itself because authored code can't.

Why this isn't the background-tasks story

#635 and #1084 give me detach and lifecycle — parent turn freed, status/cancel, completion notification. Both hit me too. Neither gives me a worker that stays a conversation: a task completes once, and I need to send turn 2 and turn 3 into the same worker with its context intact.

The distinguishing verb is steer, and it's the one users exercise. From a live multiplayer thread:

  1. Someone dispatches work to a remote agent.
  2. Two minutes later a different person adds a constraint.
  3. It has to reach the running worker, attributed, without restarting it.
  4. A third person asks how it's going and gets the worker's own narration back.

tasks/update isn't that, and cancel-and-relaunch discards accumulated context while doubling cost and side effects.

Proposed solution

A remote hop opens a durable session instead of consuming a turn:

 TODAY   parent turn ├──────── blocked ────────┤ ends
                         └─► remote: one turn in, one result out, gone

 WANTED  parent turn ├──┤ ends immediately
                        └─► worker session ──────────────────────────►
                                ▲        ▲          ▲          ▲
                             send()   status()   stream()   cancel()
                                │
                        turn 2, from a *different* human, mid-flight
// pseudo-code approach; likely more complex in practicum
const worker = await remoteWorker.open({ message: initialAsk });
await worker.send("From a teammate in the thread: also cover the EU case.");
const state = await worker.status();
for await (const e of worker.stream()) { /* relay to the thread */ }
await worker.cancel();

That's roughly POST /eve/v1/session/<id> plus the stream endpoint — machinery that already exists for external clients. The gap is that defineRemoteAgent doesn't expose it to a calling agent, so agent-to-agent is the one consumer that can't use eve's own session API.

Priority: a durable handle that outlives the parent turn, send() for further turns, readable status/progress, cancel. Identity continuity across the hop (#604) is what lets the worker's tools authorize as the right principal.

If #635 grows a session-shaped handle rather than a job-shaped one, this becomes a comment there instead — happy to consolidate.

Related: #635, #1084, #661, #604, #362.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions