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”.
defineAsyncTool
renderVideo({ script: "..." }), the tool returns immediately:
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 onRunContext.sessionState["__asyncHandles"] as a Map<string, HandleEntry>. Each entry tracks:
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: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:
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
- Memory Pointer Pattern — pair with this for large async results
@agentium/queue— durable BullMQ-backed jobs- Tool Loop Detection — catch agents that poll forever