> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentium.in/llms.txt
> Use this file to discover all available pages before exploring further.

# Async HandleId Pattern

> Long-running tools return a handle synchronously; the agent polls for the real result

# 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`

```typescript theme={null}
import { defineAsyncTool } from "@agentium/core";
import { z } from "zod";

const renderVideo = defineAsyncTool({
  name: "renderVideo",
  description: "Render a 5-second video clip from a script.",
  parameters: z.object({ script: z.string() }),
  ttlSeconds: 600,              // default 600 — drop result after 10 min
  execute: async ({ script }) => {
    // The long-running work. Runs in the background; the LLM doesn't wait.
    const url = await videoServiceClient.render(script);  // takes ~30s
    return url;
  },
});
```

When the LLM calls `renderVideo({ script: "..." })`, the tool returns immediately:

```json theme={null}
{
  "handle": "ah:550e8400-e29b-41d4-a716-446655440000",
  "status": "pending",
  "note": "Call pollResult with this handle (and optionally waitMs to wait until ready) to retrieve the result."
}
```

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

| Field         | Type                                           | Default  | Meaning                                                                                                         |
| ------------- | ---------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------- |
| `name`        | `string`                                       | required | Tool name                                                                                                       |
| `description` | `string`                                       | required | The framework auto-appends `"[Async] Returns a handle..."` to help the model understand the contract            |
| `parameters`  | `z.ZodObject`                                  | required | Zod schema (same as `defineTool`)                                                                               |
| `execute`     | `(args, ctx) => Promise<string \| ToolResult>` | required | The slow work. Runs in the background.                                                                          |
| `ttlSeconds`  | `number`                                       | `600`    | After this many seconds the cached result is dropped. Subsequent `pollResult` calls return `status: "expired"`. |

## `createPollResultTool()`

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

```typescript theme={null}
import { Agent, createPollResultTool, openai } from "@agentium/core";

const agent = new Agent({
  name: "video-bot",
  model: openai("gpt-4o"),
  tools: [renderVideo, createPollResultTool()],
  instructions: `
    When the user wants a video, call renderVideo (returns a handle).
    Then call pollResult(handle, waitMs: 30000) to retrieve the URL.
  `,
});
```

### `pollResult` parameters

```typescript theme={null}
{
  handle: string;      // "ah:..." from a defineAsyncTool call
  waitMs?: number;     // optional, max 30000 — block up to this many ms for completion
}
```

### `pollResult` return values

| status        | When                                       | Other fields                               |
| ------------- | ------------------------------------------ | ------------------------------------------ |
| `"pending"`   | Background work still in flight            | `handle`                                   |
| `"done"`      | Work completed successfully                | `handle`, `result: <execute return value>` |
| `"error"`     | Background work threw                      | `handle`, `error: <message>`               |
| `"expired"`   | Result was older than `ttlSeconds`         | `handle`                                   |
| `"not-found"` | Handle doesn't exist (typo, wrong session) | `handle`                                   |

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

```typescript theme={null}
import { Agent, createPollResultTool, defineAsyncTool, openai } from "@agentium/core";
import { z } from "zod";

const renderVideo = defineAsyncTool({
  name: "renderVideo",
  description: "Render a video clip from a script.",
  parameters: z.object({ script: z.string() }),
  ttlSeconds: 600,
  execute: async ({ script }) => {
    await new Promise((r) => setTimeout(r, 7_000));
    return `https://cdn.example.com/videos/${Date.now()}.mp4`;
  },
});

const agent = new Agent({
  name: "video-bot",
  model: openai("gpt-4o"),
  tools: [renderVideo, createPollResultTool()],
  instructions:
    "When the user requests a video, call renderVideo to start rendering, " +
    "then call pollResult with the returned handle and waitMs: 10000 to retrieve the URL.",
});

const result = await agent.run("Make me a 5-second clip of a sunset over the mountains.");
console.log(result.text); // "Here's your sunset clip: https://cdn.example.com/videos/..."
```

## Internal storage

Handles live on `RunContext.sessionState["__asyncHandles"]` as a `Map<string, HandleEntry>`. Each entry tracks:

```typescript theme={null}
interface HandleEntry {
  status: "pending" | "resolved" | "rejected";
  value?: unknown;        // resolved value
  error?: string;         // rejection message
  startedAt: number;
  ttlMs: number;
}
```

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:

```typescript theme={null}
const downloadDataset = defineAsyncTool({
  name: "downloadDataset",
  parameters: z.object({ name: z.string() }),
  execute: async ({ name }) => {
    return await fetch(`https://datasets.example.com/${name}.csv`).then((r) => r.text());
  },
});
```

When `pollResult` returns `status: "done", result: <huge text>`, the [auto-pointer converter](/features/memory-pointers) 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`](/queue/overview) and store the BullMQ job ID as the handle:

```typescript theme={null}
const reportTool = defineAsyncTool({
  name: "generateReport",
  parameters: z.object({ quarter: z.string() }),
  execute: async ({ quarter }) => {
    const job = await reportQueue.add("render", { quarter });
    return await job.waitUntilFinished(reportEvents);
  },
});
```

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

* [Memory Pointer Pattern](/features/memory-pointers) — pair with this for large async results
* [`@agentium/queue`](/queue/overview) — durable BullMQ-backed jobs
* [Tool Loop Detection](/features/tool-loop-detection) — catch agents that poll forever
