Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .changeset/ai-harness-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ Add `@tanstack/ai-harness-cli`: `runCli(harness)` runs a harness from the termin
- `-p "prompt"` runs one prompt for scripts and CI, and `--output ndjson` prints every AG-UI event.
- `--serve` serves the session protocol over HTTP behind a bearer token.
- `--acp`, `--mcp`, and `--dashboard` load `@tanstack/ai-acp`, `@tanstack/ai-mcp`, and `@tanstack/ai-dashboard` only when you use the flag. A missing package, or one without harness support, fails that flag with a clear message.
- `host` and `principal` options let the CLI use your durable host and run as your user.
- `host` and `principal` options let the CLI use your durable host and run as your user. Every mode except `--dashboard` opens its sessions as that principal. On a host with `access`, `--serve` reads the user of each request with `identify`, and `--mcp` does not start, because stdio has no login.
4 changes: 4 additions & 0 deletions .changeset/ai-harness-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ Add `@tanstack/ai-harness`: build an agent harness on TanStack AI. A harness kee

- `defineHarness()` names an agent: its adapter, tools, system prompts, middleware, agents, subagents, routing, plugins, turn hooks, and durability options.
- `createHarnessHost()` opens sessions. A session takes `send` (a prompt, a steer into the running turn, a follow-up, or a message to an agent run with `to`), `continue`, `reset`, `resolve`, `cancel`, and `background` inputs. Each returns an operation with a receipt, and the session streams typed events. `host.isOpen(harness, { threadId })` says whether this host holds that session open.
- `createHarnessHost({ access })` decides who can use each session. `identify({ request })` reads the user of a request, and `authorize({ principal, action, session, run })` decides each action. The principal type comes from what `identify` returns. The host keeps each tenant apart, needs a `principal` on each call, and `host.open` returns a handle that acts only as its principal. A refused action throws `AccessDeniedError`. Without `access`, one user runs the host, with no checks. With `access`, keyed adapters and provider keys read env vars only when `access.envKeys` is `true`.
- `membersOnly()` is a ready `authorize` with the roles owner, member, and viewer. `host.sessions.share` and `host.sessions.unshare` change the members, and `host.sessions.fork` takes `members: 'self' | 'all'`. For apps with their own access tables, `host.revoke` takes the access of a user to a thread. `host.identify` reads the user of a request for your own routes. Each `host.sessions` method takes one object.
- A principal with a `tenantId` keeps its sessions in its tenant: one thread id in two tenants is two sessions, with two index entries and two logs. `logIdOf({ tenantId, logId })` gives the id of a log in the store.
- Every run has a run context with its user, and that user's credentials and provider keys: `ctx.context.run` in tools and middleware (type them with `HarnessRunContext`), `ctx.turn.run` in a turn plugin, and `run` in a command. The `ctx.credentials` and `ctx.keys` of a session plugin have no user, so each call throws. `ctx.agents.start(agent, input, { principal })` starts an agent for a user. The log keeps the sender of every input.
- `definePlugin()` adds tools, prompts, middleware, agents, commands, config options, state, and extension points to a session or to one turn.
- Agents run from code with `session.agents.<name>.run()` and `.start()`. In agent code, `ctx.harness` gives `step.do()` (a finished step returns its stored value when the run runs again) and `agents.start()` (a child that counts against the subagent budget).
- With `stores.log` from `@tanstack/ai-persistence`, a session is durable: inputs, tool results, and steps are in the log, and the next host recovers a turn that a crash stopped. `durableTool()` runs side effects once. `durability.truncatedToolResult` sets the text of a tool call cut at the output limit. Without it, a cut answer ends the turn.
Expand Down
6 changes: 3 additions & 3 deletions .changeset/ai-harness-transport.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

Serve a harness session to clients.

- `createHarnessHandler()` from `@tanstack/ai-harness/server` serves a session over HTTP. `POST run` is standard AG-UI, so `useChat` with `fetchServerSentEvents` works on a harness, with approvals, client tools, and a reload that joins the running turn. `POST control`, `GET events`, `GET transcript`, `GET agent-transcript` (the messages of a child agent run), `GET snapshot`, `GET describe`, `sessions`, and the media routes cover the rest.
- `handleHarnessSocket()`, from the same entry, serves the same session over a WebSocket.
- `createHarnessHandler()` from `@tanstack/ai-harness/server` serves a session over HTTP, as the user that the host `access` reads from each request. `POST run` is standard AG-UI, so `useChat` with `fetchServerSentEvents` works on a harness, with approvals, client tools, and a reload that joins the running turn. `POST control`, `GET events`, `GET transcript`, `GET agent-transcript` (the messages of a child agent run), `GET snapshot`, and `GET describe` cover one session. `sessions` (with the `share` and `unshare` ops), `GET host-events`, and the media routes cover the rest. A refused action answers 403. A read of a thread that does not exist answers 404 and starts no session.
- `handleHarnessSocket()`, from the same entry, serves the same session over a WebSocket. It takes the upgrade `request` and reads the user from it.
- `expose` on `defineHarness()` names the agents, settings, config keys, and commands that a client can use. Any other input of those kinds gets `rejected` with `not_exposed`.
- `createHarnessClient()` from `@tanstack/ai-harness/client` is a typed, browser-safe client. It sends inputs, reads the session, and streams events that resume from a cursor.
- `oauthConnector()` and the OAuth helpers from `@tanstack/ai-harness/oauth` (`loopbackLogin`, `deviceLogin`, `exchangeCode`, `refreshCredential`, and more) keep a credential per sender. `token({ wait: true })` stops the turn with an `auth_required` interrupt until the user signs in.
- `oauthConnector()` and the OAuth helpers from `@tanstack/ai-harness/oauth` (`loopbackLogin`, `deviceLogin`, `exchangeCode`, `refreshCredential`, and more) keep a credential per user. A tool calls `token({ run: ctx.context.run })` for the token of the user of its run. `token({ run, wait: true })` stops the turn with an `auth_required` interrupt until the user signs in.
1 change: 1 addition & 0 deletions .changeset/ai-mcp-harness.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
Use MCP in a TanStack AI harness.

- `@tanstack/ai-mcp/harness` adds `createHarnessMcpServer`. It serves a harness as an MCP server, so Claude Code, Claude Desktop, Cursor, or another agent can use it. The tools are `chat`, `steer`, `cancel`, `approve`, `reject`, `resolve`, `answer`, and `status`, plus `agent_<name>` for each exposed agent and `command_<name>` for each exposed command. Every tool takes an optional `threadId`. With `approvals: 'ask'` (the default), the client asks the user about each approval when it supports elicitation. Otherwise the approvals come back in the result. `approvals: 'auto'` approves every tool call. `resolve` answers every kind of interrupt in one call.
- On a host with `access`, `createHarnessMcpServer` reads the user of each HTTP request with `identify`. The host checks each tool call of that user. A host without `access` needs `openAccess: true` (no user) or `openAccess: { principal }` (one local user), else the server throws at the start.
- `@tanstack/ai-mcp/harness` also adds the `mcp()` harness plugin. It connects stdio and HTTP servers by name, gives the model their tools as `<server>_<tool>`, and adds a `/mcp` status command. `oauth: true` signs in to a server with `/connect <name>`.
- `@tanstack/ai-mcp/connector` adds `mcpConnector`: one OAuth MCP server as a harness plugin, with `needsApproval`, `prefix`, `scopes`, and `clientName`. The sign-in passes the `iss` of the callback on (RFC 9207).
- `mcp()` and `mcpConnector` answer the `elicitation/create` requests of MCP spec 2025 servers, and the input requests of spec 2026-07-28 servers, as session questions. One tool call can ask up to 5 times.
Expand Down
2 changes: 1 addition & 1 deletion .changeset/harness-acp-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
'@tanstack/ai-acp': minor
---

`@tanstack/ai-acp/agent` adds `createAcpAgent` and `serveAcp`, which serve a harness as an ACP v2 agent (experimental, like the draft protocol). Tool approvals become permission requests, cancel cancels the running turn, and image, audio, and embedded file blocks go to the media store of the session. `session/list` lists the sessions of the harness from the session index of the host. `@tanstack/ai-acp` now uses `@agentclientprotocol/sdk` 1.8, pinned to `~1.8.0` because the agent uses its experimental v2 entry point. The existing ACP client adapters keep working.
`@tanstack/ai-acp/agent` adds `createAcpAgent` and `serveAcp`, which serve a harness as an ACP v2 agent (experimental, like the draft protocol). Tool approvals become permission requests, cancel cancels the running turn, and image, audio, and embedded file blocks go to the media store of the session. `session/list` lists the sessions of the harness from the session index of the host. The `principal` option is the user of the editor: each session opens as that user, and the list shows the sessions of that user. It is required on a host with `access`. `@tanstack/ai-acp` now uses `@agentclientprotocol/sdk` 1.8, pinned to `~1.8.0` because the agent uses its experimental v2 entry point. The existing ACP client adapters keep working.
2 changes: 1 addition & 1 deletion .changeset/harness-cli-handler-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
'@tanstack/ai-harness-cli': patch
---

`--serve` passes `canAccess` and `media: { secret }` to the harness handler, which needs both.
`--serve` passes `media: { secret }` to the harness handler. On a host without `access`, it also passes `openAccess`: the `principal` of `runCli`, or `true` without one. So each request with the token runs as the user of the CLI. On a host with `access`, `identify` reads the user of each request.
4 changes: 2 additions & 2 deletions .changeset/harness-secure-defaults.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@

Make the transport defaults safe for a server with many instances.

- `createHarnessHandler` and `handleHarnessSocket` need `canAccess`. To let every principal use every thread, pass `canAccess: () => true`.
- On a durable host (`stores.log`), `createHarnessHandler` throws without `media: { secret }`, so a media URL that one instance signs works on every instance.
- `createHarnessHandler` and `handleHarnessSocket` read the user of each request with the host `access`. On a host without `access`, pass `openAccess: true` (no user) or `openAccess: { principal }` (one local user), else they throw at the start. So a server never serves every thread to everyone by mistake.
- On a durable host (`stores.log`), `createHarnessHandler` throws without `media: { secret }`, so a media URL that one instance signs works on every instance. A signed URL names its user, and the handler checks that user again when it serves the file.
- `oauthConnector` needs `login`. Use `'device'` on a server, and `'loopback'` only for a harness on the user's own machine, such as a CLI.
4 changes: 2 additions & 2 deletions .changeset/persistence-session-stores.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ Add opt-in session stores. The chat and generation middleware work as before wit
- `log` (`LogStore`): an append-only session log with 14 methods, each with one object argument. Records: `append`, `read`, and `subscribe`. Ownership: `acquire`, `renew`, `release`, `setAffinity`, and `listOrphaned`. The input lane: `offer` and `offered`, consumed in the same write with `append({ consumes })`. Snapshots and lifecycle: `getSnapshot`, `putSnapshot`, `truncate`, and `deleteLog`. Each fenced write carries the epoch from `acquire`, and a write with an old epoch rejects with `OwnershipLostError`. A writer that never calls `acquire` uses `UNOWNED_EPOCH`. `append` rejects with `LogConflictError` when another writer took the position, and `read` rejects with `LogTruncatedError` before the truncation point. `memoryLogStore()` is the reference store. It is not part of `memoryPersistence()`. `memoryLogBackend()` lets two store instances share one log. The conformance kit needs `twin`, a second store set on the same backend, for the `log` cases across instances.
- `leases` (`LeaseStore`, `TurnLease`, `TurnLeaseKey`): turn leases from a system that already has them. `defineLeaseStore()` types a custom one, `memoryLeaseStore()` is the reference store, and the conformance testkit covers it. It is not part of `memoryPersistence()`.
- `workClaims` (`WorkClaimStore`): claims on threads that have pending work, with `claim`, `release`, and `listExpired`.
- `sessions` (`SessionIndexStore`): one index entry per thread, with `upsert`, `get`, `list`, and `delete`. `list` filters by parent thread, owner, title `search`, `harness` name, and exact `metadata` values, and pages with a cursor. The optional `getVersioned` and `upsertIf` give compare-and-set writes.
- `sessions` (`SessionIndexStore`): one index entry per tenant and thread. The key is `{ tenantId?, threadId }` (`SessionIndexKey`), so one thread id in two tenants is two entries. An entry has an `ownerId` and `members` (`SessionMember`, with the role `owner`, `member`, or `viewer`). The store has `get(key)`, `getVersioned(key)`, `upsert(entry)`, `upsertIf({ entry, revision })` for compare-and-set writes, `list`, and `delete(key)`, a hard delete. `list` reads one tenant, filters by member, parent thread, title `search`, `harness` name, and exact `metadata` values, and pages with a cursor.

`defineInboxStore`, `defineLogStore`, `defineWorkClaimStore`, and `defineSessionIndexStore` type an adapter. `memoryPersistence()` now also has `inbox`, `workClaims`, and `sessions`. `runPersistenceConformance` runs the cases of each session store when the store is present, so an existing backend needs no `skip` entry.

With `stores.sessions`, `withPersistence` writes one index entry for each subagent child (`subagent:<runId>`), with the parent thread, the tool call, and the owner and `harness` name of the parent entry.
With `stores.sessions`, `withPersistence` writes one index entry for each subagent child (`subagent:<runId>`), in the tenant of its `tenantId` option. The entry has the parent thread, the tool call, and the `ownerId` and `harness` name of the parent entry. It has no members: access to a child goes through its top-level thread.

`loadChildTranscript(messages, subagentRunId, threadId)` reads the stored messages of a child agent run for a UI. It returns `undefined` for a child of another thread.
44 changes: 26 additions & 18 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -761,7 +761,7 @@
"label": "Store Reference",
"to": "persistence/store-reference",
"addedAt": "2026-08-04",
"updatedAt": "2026-10-16"
"updatedAt": "2026-10-17"
},
{
"label": "How Persistence Works",
Expand Down Expand Up @@ -927,7 +927,7 @@
"label": "Build your first harness",
"to": "harness/overview",
"addedAt": "2026-10-13",
"updatedAt": "2026-10-16"
"updatedAt": "2026-10-17"
},
{
"label": "Send and show media",
Expand All @@ -939,23 +939,30 @@
"label": "Connect clients",
"to": "harness/connect",
"addedAt": "2026-10-13",
"updatedAt": "2026-10-16"
"updatedAt": "2026-10-17"
},
{
"label": "Access and tenants",
"to": "harness/access",
"addedAt": "2026-10-17"
},
{
"label": "Run in the terminal",
"to": "harness/cli",
"addedAt": "2026-10-13"
"addedAt": "2026-10-13",
"updatedAt": "2026-10-17"
},
{
"label": "Deploy a harness",
"to": "harness/deploy",
"addedAt": "2026-10-13"
"addedAt": "2026-10-13",
"updatedAt": "2026-10-17"
},
{
"label": "Send inputs safely",
"to": "harness/inputs",
"addedAt": "2026-10-13",
"updatedAt": "2026-10-16"
"updatedAt": "2026-10-17"
},
{
"label": "Run a tool without a model",
Expand All @@ -972,7 +979,7 @@
"label": "List, rename, and fork sessions",
"to": "harness/sessions",
"addedAt": "2026-10-13",
"updatedAt": "2026-10-16"
"updatedAt": "2026-10-17"
},
{
"label": "Run side effects once",
Expand All @@ -983,7 +990,7 @@
"label": "Build on the session log",
"to": "harness/session-log",
"addedAt": "2026-10-13",
"updatedAt": "2026-10-16"
"updatedAt": "2026-10-17"
},
{
"label": "Control how a turn ends",
Expand All @@ -1005,7 +1012,7 @@
"label": "Fork and reset a thread",
"to": "harness/fork-and-reset",
"addedAt": "2026-10-13",
"updatedAt": "2026-10-16"
"updatedAt": "2026-10-17"
},
{
"label": "Share one log between sessions",
Expand All @@ -1017,13 +1024,13 @@
"label": "Write a plugin",
"to": "harness/plugins",
"addedAt": "2026-10-13",
"updatedAt": "2026-10-16"
"updatedAt": "2026-10-17"
},
{
"label": "Track usage and cost",
"to": "harness/usage",
"addedAt": "2026-10-13",
"updatedAt": "2026-10-16"
"updatedAt": "2026-10-17"
},
{
"label": "Ask before risky tool calls",
Expand Down Expand Up @@ -1070,25 +1077,25 @@
"label": "Auth and connectors",
"to": "harness/auth",
"addedAt": "2026-10-13",
"updatedAt": "2026-10-16"
"updatedAt": "2026-10-17"
},
{
"label": "Share a thread between people",
"to": "harness/shared-threads",
"addedAt": "2026-10-13",
"updatedAt": "2026-10-16"
"updatedAt": "2026-10-17"
},
{
"label": "Connect model providers",
"to": "harness/provider-keys",
"addedAt": "2026-10-13",
"updatedAt": "2026-10-16"
"updatedAt": "2026-10-17"
},
{
"label": "Build your own UI",
"to": "harness/custom-ui",
"addedAt": "2026-10-13",
"updatedAt": "2026-10-16"
"updatedAt": "2026-10-17"
},
{
"label": "Code mode in a harness",
Expand All @@ -1104,7 +1111,7 @@
"label": "Run agents from a harness",
"to": "harness/subagents",
"addedAt": "2026-10-13",
"updatedAt": "2026-10-16"
"updatedAt": "2026-10-17"
},
{
"label": "Add skills",
Expand All @@ -1121,12 +1128,13 @@
"label": "Use from any MCP client",
"to": "harness/mcp-server",
"addedAt": "2026-10-13",
"updatedAt": "2026-10-16"
"updatedAt": "2026-10-17"
},
{
"label": "Use a harness from an editor (ACP)",
"to": "harness/acp",
"addedAt": "2026-10-13"
"addedAt": "2026-10-13",
"updatedAt": "2026-10-17"
}
]
},
Expand Down
Loading
Loading