Skip to content

A2A: detached task execution + persistent TaskStore so orchestrator parks survive disconnects #382

Description

@initializ-mk

Follow-up from agent-orchestrator#53 (durable workflow engine). The orchestrator now dispatches workflow steps as synchronous tasks/send and holds the connection for the whole step — because of how forge executes tasks today, there is no safer option:

  1. tasks/send executes inline in the HTTP request (forge-cli/runtime/runner.go handler → executeTask → blocking executor.Execute). The terminal task only exists in the response.
  2. Consent-gate parks block inside the request (mcp_authgate.go Awaithandle.WaitCtx(r.Context())). If the client disconnects — timeout, orchestrator pod restart, network blip — the request context cancels, the gate unwinds, and the task flips to canceled: the user's pending consent is silently destroyed.
  3. TaskStore is an in-memory map (forge-core/a2a/taskstore.go) — after an agent pod restart, tasks/get for any prior task returns "task not found", so the orchestrator can't distinguish "never ran" from "ran and was lost" and must re-dispatch.
  4. Minor: tasks/get for an unknown id returns JSON-RPC -32602 (invalid params) with a "task not found: <id>" message instead of the A2A -32001 TaskNotFound code — clients have to match on the message string.

Asks (in value order)

  1. Detached execution: tasks/send (or an opt-in variant/param) accepts the task, runs it decoupled from the request lifetime, and returns submitted/working immediately; clients poll tasks/get (or subscribe) to terminal state. Parks then survive client disconnects, and the orchestrator can release its connection + goroutine while a consent gate waits — which is also the machinery the platform's HITL approval work (agent-orchestrator#54) wants underneath.
  2. Durable (or at least TTL'd + restart-tolerant) TaskStore so tasks/get stays answerable across agent restarts for recent tasks. Client-chosen deterministic ids are already in use by the orchestrator (wf-<execution>-<node>-a<attempt>) — per-attempt records accumulate forever in the current map, so eviction is needed regardless.
  3. -32001 TaskNotFound on tasks/get/tasks/cancel for unknown ids (keep the message; fix the code).

Until this lands, the orchestrator's workaround is: no fixed dispatch timeout (the per-step budget is the connection lifetime, default 35 min), a watchdog polling tasks/get to mirror the transient auth-required state, tasks/cancel before any deliberate abandon, and re-dispatch of a fresh attempt when a lost task can't be classified. It works, but every consent park pins a connection + goroutine on both sides for up to the step budget, and forge's 10-minute gate timeout remains the ceiling for how long a consent can wait.

Refs: initializ/agent-orchestrator#53, initializ/agent-orchestrator#54 (HITL approval op, will build on detached execution).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions