Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
32dd9f3
feat(ai-mcp, ai-harness, ai-sandbox, ai-memory, ai-code-mode, ai-comp…
AlemTuzlak Nov 2, 2026
7695dca
feat(ai-memory): forward the call signal in the hindsight tools
AlemTuzlak Nov 2, 2026
2c859e4
feat(ai-sandbox): stop a bridged tool when its MCP request closes
AlemTuzlak Nov 2, 2026
9b92ed1
feat(ai-isolate-quickjs, ai-isolate-quickjs-bun, ai-isolate-daytona):…
AlemTuzlak Nov 2, 2026
83db32e
feat(ai-computer-use, ai-computer-playwright, ai-computer-sandbox): o…
AlemTuzlak Nov 2, 2026
f89edbb
feat(ai-memory): forward the recall signal in the hindsight and honch…
AlemTuzlak Nov 2, 2026
2434339
feat(ai-sandbox-e2b, ai-sandbox-docker, ai-sandbox-sprites, ai-sandbo…
AlemTuzlak Nov 2, 2026
ff85bc3
docs: abort signals for isolates, computer actions, Honcho recall, an…
AlemTuzlak Nov 2, 2026
db9b132
feat(ai-sandbox): drop the env.set signal no provider can forward
AlemTuzlak Nov 2, 2026
f5d0694
fix(ai-sandbox-boxd, ai-computer-playwright, ai-isolate-quickjs-bun):…
AlemTuzlak Nov 2, 2026
abf2116
fix(ai-sandbox-boxd): stop waiting when fork creation is aborted
AlemTuzlak Nov 2, 2026
335bb86
fix(ai-sandbox-boxd): stop waiting on snapshot requests when the sign…
AlemTuzlak Nov 2, 2026
87b3a86
fix(ai-sandbox-boxd): do not make an aborted fork() wait for the fork…
AlemTuzlak Nov 2, 2026
5f753a5
fix(ai-sandbox-boxd): stop waiting for a failed fork's delete on abort
AlemTuzlak Nov 2, 2026
a247ffd
fix(isolates): keep context on guest AbortError, report Worker abort
AlemTuzlak Nov 3, 2026
67cde48
docs(ai-sandbox): say which git backends stop on abort
AlemTuzlak Nov 3, 2026
7e70926
Merge origin/main into feat/abort-signal-options-agents
AlemTuzlak Nov 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/abort-memory-hindsight-tools.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-memory': minor
---

The `hindsight_retain`, `hindsight_recall`, and `hindsight_reflect` tools now give the abort signal of the call to the Hindsight client.
5 changes: 5 additions & 0 deletions .changeset/ai-client-abort-signal-agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-client': minor
---

`createMcpAppBridge().callTool(input, { signal })` passes the signal to the call request.
5 changes: 5 additions & 0 deletions .changeset/ai-code-mode-abort-signal-agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-code-mode': minor
---

`IsolateContext.execute(code, options?)` takes an optional `{ signal }`. `execute_typescript` passes the abort signal of the tool call, so a driver can stop the run.
5 changes: 5 additions & 0 deletions .changeset/ai-computer-playwright-action-signal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-computer-playwright': minor
---

`screenshot`, `zoom`, and `navigate` reject when their signal aborts. Playwright takes no signal, so the page call continues in the background. The factory closes the new browser context when `create` aborts during the start URL.
5 changes: 5 additions & 0 deletions .changeset/ai-computer-sandbox-abort-signal-agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-computer-sandbox': minor
---

Pass the `create()` signal to `provider.create()`.
5 changes: 5 additions & 0 deletions .changeset/ai-computer-sandbox-action-signal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-computer-sandbox': minor
---

The driver passes the signal of each action to `sandbox.process.exec`, so an abort stops the xdotool or screenshot command. `createSandboxComputerDriver({ signal })` stops the setup commands, and `sandboxComputer` passes the `create` signal to it.
5 changes: 5 additions & 0 deletions .changeset/ai-computer-use-abort-signal-agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-computer-use': minor
---

`ComputerDriverFactory.create({ threadId, signal })` gets the abort signal of the tool call that needs the machine.
5 changes: 5 additions & 0 deletions .changeset/ai-computer-use-action-signal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-computer-use': minor
---

Each `ComputerDriver` action takes an optional `signal`, for example `move(x, y, { signal })` and `click(x, y, { button, count, signal })`. The computer tools pass the abort signal of the tool call to each action. An abort also stops the `wait` and `hold_key` waits and the wait before the screenshot. `keyUp` and `mouseUp` get no signal, so held keys and buttons are released after an abort.
5 changes: 5 additions & 0 deletions .changeset/ai-harness-abort-signal-agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-harness': minor
---

Each request method of `createHarnessClient` takes an optional last argument `{ signal }`: the control inputs, `snapshot`, `transcript`, `agentTranscript`, `describe`, `putMedia`, `mediaUrl`, `loadMedia`, and `sessions.*`.
5 changes: 5 additions & 0 deletions .changeset/ai-isolate-cloudflare-abort-signal-agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-isolate-cloudflare': minor
---

Forward the `execute()` signal to the worker request.
5 changes: 5 additions & 0 deletions .changeset/ai-isolate-daytona-abort-signal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-isolate-daytona': minor
---

When the `execute()` signal aborts, `execute()` returns an `AbortError` failure and starts no more tool rounds. The Daytona SDK cannot cancel a `codeRun`, so a run that already started continues in the sandbox until its own timeout.
5 changes: 5 additions & 0 deletions .changeset/ai-isolate-e2b-abort-signal-agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-isolate-e2b': minor
---

Kill the sandbox process group when the `execute()` signal aborts. The run returns an `AbortError` failure.
5 changes: 5 additions & 0 deletions .changeset/ai-isolate-node-abort-signal-agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-isolate-node': minor
---

Stop a running script when the `execute()` signal aborts. The run returns an `AbortError` failure.
5 changes: 5 additions & 0 deletions .changeset/ai-isolate-quickjs-abort-signal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-isolate-quickjs': minor
---

Stop a run when the `execute()` signal aborts. The run returns an `AbortError` failure, and the context is disposed, the same as after a timeout.
5 changes: 5 additions & 0 deletions .changeset/ai-isolate-quickjs-bun-abort-signal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-isolate-quickjs-bun': minor
---

Stop a run when the `execute()` signal aborts. The run returns an `AbortError` failure. The context stays usable.
10 changes: 10 additions & 0 deletions .changeset/ai-mcp-abort-signal-agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@tanstack/ai-mcp': minor
---

New optional signals. Old calls work the same.

- `createMCPClient({ signal })` cancels the connect handshake.
- `tools({ signal })`, `resources({ signal })`, `readResource(uri, { signal })`, `resourceTemplates({ signal })`, `prompts({ signal })`, and `getPrompt(name, args, { signal })` send the signal to the MCP SDK.
- The lazy tool list in `callTool` uses the signal of the call.
- The handler from `createMcpAppCallHandler` takes `(req, { signal })` and passes it to the connect, the tool list, and the tool call.
5 changes: 5 additions & 0 deletions .changeset/ai-memory-abort-signal-agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-memory': minor
---

`MemoryAdapter.recall(scope, query, { signal })` and `Embedder.embed(text, { signal })` take an optional signal. `memoryMiddleware` passes the chat run signal. `inMemory()`, `redis()`, and `mem0()` forward it.
5 changes: 5 additions & 0 deletions .changeset/ai-memory-provider-recall-signal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-memory': minor
---

The Hindsight adapter passes the `recall()` signal to `client.recall`. The Honcho adapter stops waiting when the signal aborts. The Honcho SDK takes no signal, so its request continues in the background.
5 changes: 5 additions & 0 deletions .changeset/ai-sandbox-abort-signal-agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-sandbox': minor
---

New optional signals on the sandbox contract: `git.clone({ signal })`, `git.push(dir, { signal })`, `git.pull(dir, { signal })`, `snapshot(label, { signal })`, `fork({ signal })`, and `ports.connect(port, { signal })`. The exec-backed git passes the signal to each git command.
5 changes: 5 additions & 0 deletions .changeset/ai-sandbox-blaxel-abort-signal-agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-sandbox-blaxel': minor
---

Pass the `git.push` and `git.pull` signal to the git command.
5 changes: 5 additions & 0 deletions .changeset/ai-sandbox-boxd-k11-signal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-sandbox-boxd': minor
---

When the `snapshot` signal aborts, `snapshot` stops polling for the capture. When the `fork` signal aborts, `fork` stops waiting for the new machine and deletes it. The boxd SDK takes no signal, so a started capture continues in boxd.
5 changes: 5 additions & 0 deletions .changeset/ai-sandbox-docker-k11-signal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-sandbox-docker': minor
---

`snapshot(label, { signal })`, `fork({ signal })`, and `ports.connect(port, { signal })` pass the signal to dockerode as `abortSignal`, which aborts the Docker API request.
5 changes: 5 additions & 0 deletions .changeset/ai-sandbox-e2b-k11-signal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-sandbox-e2b': minor
---

`snapshot(label, { signal })` and `fork({ signal })` pass the signal to the E2B SDK, which aborts the request.
5 changes: 5 additions & 0 deletions .changeset/ai-sandbox-sprites-k11-signal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-sandbox-sprites': minor
---

`snapshot(label, { signal })` passes the signal to the checkpoint request.
5 changes: 5 additions & 0 deletions .changeset/ai-sandbox-tool-bridge-call-signal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-sandbox': minor
---

`ToolBridgeCore.callTool(name, args, { signal })` takes an optional signal for one call. The tool gets a signal that aborts when this signal or the bridge `signal` aborts. The host MCP bridge passes the request signal, so a closed `tools/call` request stops the bridged tool.
11 changes: 10 additions & 1 deletion docs/code-mode/code-mode-isolates.md
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,10 @@ interface IsolateConfig {
}

interface IsolateContext {
execute<T = unknown>(code: string): Promise<ExecutionResult<T>>
execute<T = unknown>(
code: string,
options?: { signal?: AbortSignal },
): Promise<ExecutionResult<T>>
dispose(): Promise<void>
}

Expand All @@ -399,3 +402,9 @@ interface ExecutionResult<T = unknown> {
error?: NormalizedError
}
```

Code Mode passes the abort signal of the tool call as `options.signal`. When it aborts, stop the run and return a failure. The built-in drivers do this:

- The Node, Cloudflare, E2B, and QuickJS drivers stop the run at once. After an abort, the Node and QuickJS contexts are disposed, the same as after a timeout.
- The QuickJS on Bun driver stops the run between jobs. The context stays usable.
- The Daytona driver returns at once and starts no more tool rounds. The Daytona SDK cannot cancel a `codeRun`, so a run that started continues in the sandbox until its `timeout`.
4 changes: 3 additions & 1 deletion docs/computer-use/custom-driver.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ Coordinates are in pixels, in `display` space. Key names are `KeyboardEvent.key`
| `press(keys)` | Presses one combo, like `['Control', 's']`. |
| `type(text)` | Types text as a user does. |

Each action also takes an optional `signal`, the abort signal of the tool call. It is the last argument, for example `move(x, y, { signal })`. `click` gets it in its options: `click(x, y, { button, count, signal })`. Pass it to your requests, so an action stops when the run stops. `keyUp` and `mouseUp` get no signal, because they release what an aborted action held down.

### Add the optional methods

Four methods are optional:
Expand Down Expand Up @@ -191,7 +193,7 @@ The factory has these members:
| `display` | The screen size of every machine. Each driver from `create` has this `display`. |
| `environment` | The kind of every machine. |
| `capabilities` | `{ zoom, cursorPosition, navigate }`: the optional methods that every driver from `create` has. The tool is built before any machine exists, so the factory tells it. |
| `create({ threadId })` | Makes the driver of one thread. If it throws, the model gets the error. The next model turn calls `create` again. |
| `create({ threadId, signal })` | Makes the driver of one thread. `signal` is the abort signal of the tool call that needs the machine. Pass it to your setup requests. If `create` throws, the model gets the error. The next model turn calls `create` again. |
| `close()` | Optional. Frees what the threads share. `computerUse.close()` calls it after it closes every thread. |

When a thread closes or goes idle, `createComputerUse` calls `close()` on the driver of that thread.
14 changes: 7 additions & 7 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,7 @@
"label": "Manual MCP",
"to": "tools/mcp-manual",
"addedAt": "2026-06-05",
"updatedAt": "2026-10-22"
"updatedAt": "2026-11-02"
},
{
"label": "MCP Type Generation",
Expand Down Expand Up @@ -660,7 +660,7 @@
"label": "MCP Apps",
"to": "mcp/apps",
"addedAt": "2026-06-24",
"updatedAt": "2026-10-22"
"updatedAt": "2026-11-02"
},
{
"label": "Serve Tools over HTTP",
Expand Down Expand Up @@ -1025,7 +1025,7 @@
"label": "Code Mode Isolate Drivers",
"to": "code-mode/code-mode-isolates",
"addedAt": "2026-04-15",
"updatedAt": "2026-10-22"
"updatedAt": "2026-11-02"
},
{
"label": "Lazy Tools",
Expand Down Expand Up @@ -1061,7 +1061,7 @@
"label": "Custom Driver",
"to": "computer-use/custom-driver",
"addedAt": "2026-10-20",
"updatedAt": "2026-10-22"
"updatedAt": "2026-11-02"
}
],
"tab": "guides"
Expand Down Expand Up @@ -1229,7 +1229,7 @@
"label": "Sandbox and extend the coding tools",
"to": "harness/coding-tools-backends",
"addedAt": "2026-10-13",
"updatedAt": "2026-10-22"
"updatedAt": "2026-11-02"
},
{
"label": "Run the coding tools for many threads",
Expand Down Expand Up @@ -1360,7 +1360,7 @@
"label": "Providers",
"to": "sandbox/providers",
"addedAt": "2026-06-29",
"updatedAt": "2026-10-29"
"updatedAt": "2026-11-02"
},
{
"label": "Harnesses",
Expand Down Expand Up @@ -1556,7 +1556,7 @@
"label": "Adapters",
"to": "memory/adapters",
"addedAt": "2026-08-04",
"updatedAt": "2026-10-22"
"updatedAt": "2026-11-02"
},
{
"label": "Custom Adapter",
Expand Down
2 changes: 2 additions & 0 deletions docs/harness/coding-tools-backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ export const sandboxed = defineHarness({

To give the agent the project folder on your machine, [copy it into the container](#copy-your-project-into-the-container).

To stop a slow clone, pass `signal`: `sandbox.git.clone({ url, signal })`. `git.push(dir, { signal })` and `git.pull(dir, { signal })` take it too. When the signal aborts, it stops the git command on every provider except Daytona. Daytona runs git through its own API, which takes no signal, so the git command runs to the end.

3. When you are done, stop the sandbox with `await sandbox.destroy()`.

Every provider works. Get the handle from `create()`, or from `resume()` for a sandbox that runs already. See [Providers](../sandbox/providers). To create a sandbox for each thread, and resume it on another server, see [Run the coding tools for many threads](./coding-workspaces).
Expand Down
13 changes: 13 additions & 0 deletions docs/harness/connect.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,19 @@ Without `from`, `events()` starts at the newest event, so it sends no old events
- To read every event from the start of the session, pass `from: '0'`. Do this when you rebuild a transcript from the events.
- If the snapshot answer has no `cursor` (the URL is not a harness handler), `events()` throws and does not try again.

Each request method of the client takes an optional last argument `{ signal }`. When the signal aborts, the request stops and the call rejects:

```ts group=harness-connect-client
const controller = new AbortController()
setTimeout(() => controller.abort(), 5000)

const snapshot = await client.snapshot({ signal: controller.signal })
await client.send('Summarize my open tickets.', undefined, {
signal: controller.signal,
})
console.log(snapshot.cursor)
```

To upload files and show the media that your agents make, read [Send and show media](./media).

## Send a message to a running agent
Expand Down
3 changes: 2 additions & 1 deletion docs/mcp/apps.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,8 @@ export const Route = createFileRoute('/api/mcp-apps/call')({
POST: async ({ request }) => {
// body: { threadId, serverId, toolName, args?, messageId? }
const body = await request.json()
const result = await handler(body)
// The signal stops the connect and the tool call if the widget leaves.
const result = await handler(body, { signal: request.signal })
return new Response(JSON.stringify(result), {
headers: { 'Content-Type': 'application/json' },
})
Expand Down
6 changes: 3 additions & 3 deletions docs/memory/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ keywords:
- options
---

Each memory adapter implements `recall` and `save`. Swap one for another in `memoryMiddleware`.
Each memory adapter implements `recall` and `save`. Swap one for another in `memoryMiddleware`. `memoryMiddleware` calls `recall(scope, query, { signal })` with the abort signal of the chat run. The built-in adapters pass it to their requests when the backend client can take it.

| Adapter | Import | Use it for | Extra install |
| --- | --- | --- | --- |
Expand Down Expand Up @@ -64,7 +64,7 @@ const memory = inMemory({
```

- **`extract`** returns `ExtractedFact[]` (`{ text, kind?, importance?, metadata? }`), or `undefined` to save nothing extra. Put an LLM-based fact extractor here. The adapter does not depend on a model.
- **`embedder`** embeds the query on `recall` and the stored text on `save`. With no embedder, the score uses word overlap, recency, and importance only.
- **`embedder`** embeds the query on `recall` and the stored text on `save`. With no embedder, the score uses word overlap, recency, and importance only. On `recall`, `embed(text, { signal })` gets the abort signal of the chat run. Pass it to your embedding request, so a stopped run does not wait for it.

## `inMemory()`

Expand Down Expand Up @@ -162,7 +162,7 @@ const memory = mem0({

## `honcho()`

Honcho stores the turn as messages in a session. `recall` returns one written answer about the user, built by Honcho, with no separate fragments.
Honcho stores the turn as messages in a session. `recall` returns one written answer about the user, built by Honcho, with no separate fragments. When the chat run aborts, `recall` stops waiting and returns an empty prompt. The Honcho SDK takes no abort signal, so the Honcho request continues on the server.

| Option | Type | Default | Purpose |
| --- | --- | --- | --- |
Expand Down
21 changes: 21 additions & 0 deletions docs/sandbox/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,27 @@ const text = await handle.fs.read('/workspace/README.md', {

`bootstrapWorkspace(handle, workspace, { signal })` gives its signal to each file call that it makes. A bring-your-own provider must reject a call when the signal is already aborted.

### Stop a snapshot, fork, or port call

`snapshot(label, { signal })`, `fork({ signal })`, and `ports.connect(port, { signal })` take a signal too:

```ts
import { e2bSandbox } from '@tanstack/ai-sandbox-e2b'

const handle = await e2bSandbox().create({})
const controller = new AbortController()

const ref = await handle.snapshot?.('after-setup', {
signal: controller.signal,
})
```

- E2B gives the signal to `snapshot` and `fork`.
- Docker gives the signal to `snapshot`, `fork`, and `ports.connect`.
- Sprites gives the signal to `snapshot`.
- boxd stops waiting for the snapshot or the fork. An aborted fork is deleted.
- The other bundled providers ignore the signal for these calls. Their SDKs take no signal.

## Advanced

Each provider has its own options and limits: idle timers, ports, network rules, non-root users, and snapshot behavior. Read them in [Providers (Advanced)](./providers-advanced) before you go to production.
21 changes: 21 additions & 0 deletions docs/tools/mcp-manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,27 @@ const stream = chat({
controller.abort()
```

Outside a chat run, pass `signal` to a client call yourself. Each of these calls takes it:

```ts
import { createMCPClient } from '@tanstack/ai-mcp'

const controller = new AbortController()
const { signal } = controller

const mcp = await createMCPClient({
transport: { type: 'http', url: process.env.MCP_URL! },
signal, // cancels the connect only
})

const tools = await mcp.tools({ signal })
const resources = await mcp.resources({ signal })
const prompts = await mcp.prompts({ signal })
console.log(tools.length, resources.length, prompts.length)
```

`readResource(uri, { signal })`, `resourceTemplates({ signal })`, and `getPrompt(name, args, { signal })` take it too. The client sends the signal to the MCP SDK, which sends `notifications/cancelled` to the server.

## Full server and client example

This TanStack Start route connects to two MCP servers and streams the response to the browser.
Expand Down
Loading
Loading