Skip to main content

Async HandleId Pattern

The problem

Some tool calls take 5–60 seconds: video rendering, batch jobs, slow third-party APIs, large file downloads. If the tool blocks synchronously, the agent loop blocks too. That’s bad:
  • The user sees a frozen UI.
  • Streaming providers may close the connection on timeout.
  • The agent can’t do anything else while it waits — it can’t even tell the user “this will take a minute”.
The fix is to return a handle immediately, run the real work in the background, and let the agent poll for the result later.

defineAsyncTool

When the LLM calls renderVideo({ script: "..." }), the tool returns immediately:
Meanwhile, the real execute() is running in a fire-and-forget background promise. When it finishes, the result is cached on RunContext.sessionState["__asyncHandles"] keyed by the handle.

Config

createPollResultTool()

Add this once to your agent’s tool list. The LLM calls it to retrieve results.

pollResult parameters

pollResult return values

Result is always JSON-stringified for the LLM to parse.

waitMs semantics

pollResult(handle, waitMs: 10000) polls every 100ms for up to 10 seconds. If the result arrives mid-poll, it returns immediately with status: "done". If the deadline expires with the work still pending, it returns status: "pending" (the LLM should call again). Capped at 30000ms to prevent the LLM from blocking forever.

Complete example

Internal storage

Handles live on RunContext.sessionState["__asyncHandles"] as a Map<string, HandleEntry>. Each entry tracks:
The map is cleared with RunContext. For multi-run handle persistence, serialize sessionState between runs via your session manager.

Composition with other patterns

Async + Memory Pointers

When the async result is itself huge:
When pollResult returns status: "done", result: <huge text>, the auto-pointer converter wraps that result in an art: pointer. The agent then calls getArtifact(pointer) if it needs the bytes.

Async + BullMQ background queue

For work that needs to survive process restart, push the real execution to BullMQ via @agentium/queue and store the BullMQ job ID as the handle:
The handle now indirectly references a durable BullMQ job, so even if the agent process restarts, the work continues.

When to use

  • API calls > 5 seconds
  • Video / audio / image generation
  • Large data downloads
  • Anything that benefits from the LLM doing something else while waiting

When NOT to use

  • Sub-second tools — the handle overhead isn’t worth it
  • Tools whose result the LLM needs to reason about immediately
  • Tools called inside a tight workflow loop where everything is sequential anyway

See also