ta
TanStack
OSS
ta
Pipelines
Nx runs
Insights
Compare tasks
Analytics
Sign in
Toggle Sidebar
ai
ai
Pipelines
feat(ai-harness): Tau (#1755) * feat(ai-persistence): session stores for inputs, credentials, a log, leases, work claims, and a session index * test(ai-persistence): name each inbox id in the conformance case * fix(ai-persistence): read the sessions store through the persistence plan * feat(ai-harness): new package with the harness contracts and plugin API * feat(ai-harness): session engine and host, with the session split into modules * feat(ai-harness): durable sessions on the session log * test(ai-harness): unit tests and E2E for the core harness * docs(ai-harness): harness guides, README, and changeset * fix(ai-harness): restore split imports, type the session for hooks, and keep cut-call text consistent * test(ai-harness): keep only the core auth case, and use a type import in the session base * fix(ai-harness): keep the lockfile minimal, sort the e2e dependency, and leave sessionOfTurn to the transport * fix(ai-harness): import ./session/index so the published d.ts files resolve * feat(ai-skills): a harness plugin that makes each skill a slash command (#1756) * feat(ai-skills): skills plugin for a harness, with one command per skill * docs(ai-skills): say that the harness skills plugin does not run on Cloudflare Workers * feat(ai-harness, ai-persistence): read the transcript of a child agent run session.agentTranscript(subagentRunId) gives the saved messages of a child of the thread, for a UI that shows subagents after a reload. It uses the new loadChildTranscript, which walks the parent chain so a nested child loads and a child of another thread does not. * test(ai-harness): a fork does not read the child transcripts of its source * fix(ai-persistence): loadChildTranscript keeps the card of a child with no messages A UI reads how a child ended from the card in metadata.tanstack.subagent. A child that failed before its first message has only its card-only head. * docs(ai-harness): describe the card in session.agentTranscript * docs(ai-harness): show what a child agent wrote after a reload * feat(ai-acp): serve a harness as an ACP v2 agent (#1758) * feat(ai-acp): serve a harness as an ACP v2 agent * fix(ai-acp): give each idle stop reason its own update type * feat(ai-acp): list the sessions of the harness with session/list ACP v2 agents with sessions must answer session/list. The agent reads the top-level sessions of the harness from the host's session index, 50 per page. Each session reports the agent's cwd (new cwd option, default process.cwd()), because the harness keeps no cwd per thread. * fix(ai-acp): make approvals, cancel, and queued prompts work in the agent - Cancel during a permission request ends the turn as cancelled. The next prompt cancels the open interrupts first, because the thread takes no new input while they are open. - Interrupts the editor cannot answer (sign-in, input form, client tool) are cancelled, and the model continues. - A rejected resolve receipt ends the turn with an error. - The prompts of a session run one after another, so a later prompt cannot overtake a permission request or report out of order. - session/prompt echoes a user_message with the returned messageId. - Data that is not base64 fails with invalid params. - Unknown session ids fail with resource not found. - The agent keeps no session objects; host.open returns the live one. * feat(ai-harness, ai-persistence, ai-skills): onEvent and onMedia callbacks, object params, and review fixes (#1779) * feat(ai-harness, ai-persistence): onEvent, onMedia, and object params - session.onEvent(listener) and session.onEvent(type, listener) call listeners as the session publishes events. The typed form narrows the event. Each returns a stop function. operation.onEvent does the same for one operation. - operation.onMedia(listener) gets each media record an operation saves, and a chat turn result has every record in `media`. - Store methods and session/host methods with several plain-data arguments take one object: InboxStore.markApplied/markRejected, CredentialStore.get/set/delete, LogStore.append/subscribe, WorkClaimStore.release, SessionIndexStore.upsertIf, host.sessions.rename/update/fork, session.setConfig/answer/setDelivery/ sendToAgent, ctx.session.setConfig, and credentials.set. * docs(harness, persistence): onEvent, onMedia, object params, and fixes - overview, usage, media: use onEvent and onMedia instead of a for-await loop. The usage sample no longer loops forever. - sessions: a "React to events" section on session.onEvent. - sessions, fork-and-reset, plugins, subagents, store-reference: the object-param call shapes. - Fix wrong statements: parentThreadId, the define*Store list, what withPersistence uses, host.close({ recoverable }), the transient status codes, the recover hook context, and Bedrock in the adapter list. * fix(ai-harness): redact credentials from errors a session publishes credentialsFor records the secret values it hands out, and command and turn errors pass through scrubSecrets before they reach clients. scrubSecrets also matches the JSON-escaped and URL-encoded forms. * fix(ai-harness): harden media keys, MIME types, part kinds, and base64 - Encode the client thread id in the upload blob key, so '../' cannot leave a filesystem blob store's folder. - Accept only a strict type/subtype[; name=value] MIME type, so a data URL cannot be broken by the stored type. - Send the stored media kind, not the client part type. - Base64 with Buffer when present, else per-chunk btoa, so a big file does not build one binary string. * fix(ai-skills): re-arm dead skill folder watchers, and watch without recursive A watcher whose folder went away (a 'rename' of the folder itself, or a folder that no longer exists at a sync) is dropped, so the parent is watched and a re-created folder is seen. Where recursive watch is not available (Linux before Node 19.1), the folder is watched without it instead of being treated as missing. * fix(ai-harness): scrub credentials from SUBAGENT_ERROR chunks of a chat turn * fix(ai-harness): accept MediaRecorder MIME types, refuse line breaks kindOf accepts commas and quoted parameter values (video/webm;codecs=vp8,opus) and refuses CR/LF. dataUrl keeps only parameters with plain values, so a comma or quote cannot break the data URL. * fix(ai-persistence): a log conformance case for concurrent appends, and a working durable memory setup in the JSDoc The memoryLogStore example spread memoryPersistence().stores, which now has messages and inbox, so a durable harness host rejected it. The log suite now checks that of two appends at one position, exactly one wins and the other rejects with LogConflictError. * fix(ai-harness): keep usage buckets as own properties A principal id like __proto__ wrote its counts onto Object.prototype. * fix(ai-harness): resumePending reads past expired claims of other harnesses listExpired has no cursor, so more than limit expired claims of other harnesses starved the host. It now reads with a doubling limit until it has limit claims of its own or the store has no more. * fix(ai-harness): catch failed lease renewals, and put repaired tool results next to their calls A failed runs.update in the renewal timer was an unhandled rejection. repairTranscript appended recovered results after later host messages; they now go right after the tool-call message. * fix(ai-harness): make the session log durable across crashes, closes, and odd stores - A durable tool's appended records land in the same append as its harness.tool.result, so a crash after the result no longer loses them, and an unrelated commit no longer flushes them early. - A failed read in the store subscriber fails the log instead of an unhandled rejection. - A checkpoint is copied on load, so in-place usage folding no longer changes the stored checkpoint and counts twice. - A log goes idle only after the last append landed, and session.close() waits for it, so a quick reopen reads it. - A project or reduce fold that throws skips the record with a warning, live and on reload, instead of a half-applied fold and a stale seq. - Folds read until an empty page, and the landed check ignores key order. - Tool results, starts, and steps are dropped once the call's result is in the transcript, or its agent run settled. * fix(ai-harness): settle operations when a store write throws A store write that threw after an agent run, in a command, or in the early steps of a turn left the operation unsettled and the rejection unhandled. The operation now fails with a RUN_ERROR. An early turn failure also settles its input, so settled() returns. Writes no one waits for (inbox marks, wake follow-ups, group cancels, the close on a log failure) now catch their errors and report a warning. * fix(ai-harness): admit inputs correctly on store failures, close, and replays - A failed input write no longer keeps its id, so a retry runs. - Work inputs that come while the session closes are rejected with 'closed', and a turn queued late in a close ends cancelled. - A duplicate replayed from the log gets the text of its own turn, and the real interrupts when its turn still waits. - A prompt that joined the running turn stays findable, ends cancelled or interrupted with that turn, and a cancel of it is refused. * fix(ai-harness): clean up on cancel, failed open, and reload during close - A cancelled turn rejects its pending ctx.session.ask() questions. - A session that fails to open closes as on a crash: it disposes its plugins, stops the claim timer, and stops the turns recovery started. - A reload that waited for the running turn mounts nothing when the session started to close, and close() waits for a reload in progress. * fix(ai-harness): end harnessText streams with the real outcome - A signal that aborted before the call cancels the inner turn, and the abort listener is removed when the call ends. - The outer stream awaits the inner turn, so a cancelled or failed turn ends with RUN_ERROR, not RUN_FINISHED. - The remote adapter passes the signal to fetch, cancels the body reader on an early end, reads CRLF events, and fails a stream that closes before RUN_FINISHED. * fix(ai-harness): harden plugin state, events, and turn plugin mounts - A plugin with lifetime 'turn' that adds a command or a config option fails at mount with a clear error. The session reads both only from its session plugins, so they never worked. - An async plugin event listener that rejects gets a warning, not an unhandled rejection. - Plugin state and inspect() key by name with no prototype, so a plugin named 'constructor' or '__proto__' is a plain key. - Without a store setIf, plugin state updates of a session run one at a time, so concurrent updates are not lost. * fix(ai-harness): bound settled-operation bookkeeping and keep retrying claims - A session keeps its last 500 settled operations, with their receipts, settlements, and agent chains. Older ones go, so a long-lived session does not grow. A duplicate of an older input falls back to the log or the inbox. - A claim that another host holds no longer stops the claim timer. The timer tries again while the thread has work, so a running turn gets a claim once the other one expires. * fix(ai-harness): keep a turn's text past mid-turn inputs, and cancelled runs cancelled - A replayed duplicate reads its turn's text up to the next turn or reset, not up to an answer, command, or agent run applied during the turn. - An operation that a store write fails after a cancel ends cancelled, with no second terminal event, and an agent run frees its input lease also when its end record fails. * fix(ai-harness): fail, not cancel, an operation that a log failure stopped * fix(ai-harness): redact credentials from background agent errors The session keeps one set of the secrets its credentials accesses and provider keys hand out, so an agent run that reads its own keys gets its RUN_ERROR, SUBAGENT_ERROR, and failure text scrubbed too. * docs(harness): a turn plugin cannot add commands or config * fix(ai-harness): catch async onEvent listeners, and drop operation listeners when it finishes A listener's rejected promise is reported, not left unhandled. An operation's onEvent and onMedia listeners stop after its finished event, and a listener stopped by an earlier one gets nothing more. * fix(ai-harness): follow-ups to the session fixes - answer() keeps the question when its write fails. - A command's questions belong to the command, so cancelling it rejects them. - A closing session starts no agent follow-up run. - Plugin warnings, early turn failures, failed operations, reload errors, and job failure notes are scrubbed of credentials. - hostRecords reads until an empty page. - Agent runs stay listed past the settled window, a forgotten run id stays taken, and turnJoins entries are dropped. * fix(ai-harness): cancel queued agent follow-ups on close, and drop operation listeners on every settle - A closing session cancels an agent chain's queued follow-ups, so close() and their callers no longer wait for runs that never start. - An operation removes its onEvent and onMedia listeners a tick after it settles, on every settle path. An async onMedia listener's rejection is caught. - A failed reset's message is scrubbed of credentials. * fix(ai-harness): cancel queued follow-ups after a log failure, and publish an agent run's finished event before its lease write - A log failure closes the session, and the queued follow-ups of an agent chain now end cancelled then too, so close() does not hang. - An agent run publishes its finished event as it settles, before the awaited lease release, so its operation listeners still get it. - The operation listener cleanup timer does not keep a process alive. * feat(ai-harness): serve sessions over HTTP and WebSocket, with a typed client and OAuth connectors (#1757) * feat(ai-harness): serve sessions over HTTP and WebSocket, with a typed client and OAuth connectors * fix(ai-harness): add back sessionOfTurn for the run route, and the OAuth cases of auth.test.ts * fix(ai-harness): import ./session/index, so the built types resolve HarnessSession * test(ai-harness): startLoopbackReceiver cases from #1555 * feat(ai-harness, ai-mcp): MCP servers in a harness, and a harness as an MCP server (#1763) * feat(ai-harness, ai-mcp): MCP servers in a harness, and a harness as an MCP server Adds @tanstack/ai-mcp/harness (createHarnessMcpServer and the mcp() plugin) and @tanstack/ai-mcp/connector (mcpConnector with OAuth sign-in and RFC 9207 iss). mcp() and mcpConnector ask spec 2025 elicitation/create requests and spec 2026 input rounds as session questions. Split out of #1555. * docs(ai-harness, ai-mcp): import definePlugin in the discoverTools sample * feat(ai-harness): GET agent-transcript and client.agentTranscript A client can read the saved messages of a child agent run, as a local session does with session.agentTranscript. The route opens the session for the principal, so a child of another thread or user stays hidden. * docs(ai-harness): list GET agent-transcript in the handler routes * docs(ai-harness): describe the card in client.agentTranscript * docs(ai-harness): read a child's messages from the browser * feat(ai-harness): createSessionView, one live store for a session in any UI (#1761) * feat(ai-harness): createSessionView, one live store for a session in any UI * fix(ai-harness): import ../session/index in the view * feat(ai-harness): close the gaps between createSessionView and the harness - Actions: followUp, continue, upload, configure, cancel(operationId), cancelInput, setDelivery, reset, revert, unrevert, and ViewAgent.cancel. - Interrupts: generic interrupts get their own list with resolve and cancel. Approvals can send edited arguments or a payload, and find their tool in a child agent. - Subagents: AgentPart.result, and after a reload each child shows its saved messages and how it ended. - A view that opens while a turn runs shows the turn from its start without what the transcript has. The snapshot is read before the transcript. - A retry drops the failed attempt, turnEnd skips a turn that stops for an interrupt, usage, models, and settings are in the state, and media that no message shows stops getting new URLs. * docs(ai-harness): every view action, other interrupts, and subagents after a reload * fix(ai-harness, ai-mcp): adopt object params from #1779 The client, the session view, and its source take one object for setConfig, answer, setDelivery, sendToAgent, renameSession, updateSession, and forkSession, as the session does. A view agent's send takes { mode }, as AgentRun.send does. The protocol, the HTTP handler, the connectors, and the MCP connector call the new store and session shapes. * docs(ai-harness): object params in the transport, view, MCP, and auth pages * test(ai-harness): rename, update, and fork a session through the client * fix(ai-harness): join and resume a run on any instance, and no insecure transport defaults (#1781) * fix(ai-harness): no insecure or single-process transport defaults - `canAccess` is now required on `createHarnessHandler` and `handleHarnessSocket`. Forgetting it used to let every principal into every thread. Allow-all is now an explicit `canAccess: () => true`. - A durable host (`stores.log`) runs behind several instances, so a random per-handler `mediaSecret` makes signed media URLs fail on every other instance. The handler now throws at creation on a durable host without `mediaSecret`. A host without a log store keeps the random default. - `oauthConnector` no longer defaults to `login: 'loopback'`, which opened a node:http listener on 127.0.0.1 inside the server process, where the user's browser can never reach it. `login` is now required. A default of `'device'` was rejected: it needs `oauth.deviceUrl`, which many apps do not have, so a silent default would just move the failure to run time. Requiring the choice is the smallest change that is correct for both a CLI (`'loopback'`) and a server (`'device'`). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(ai-harness): join and resume a run on any instance of a durable host On serverless hosting, several instances serve one URL with no sticky routing, and a connection can be cut at any time. - `POST run` streamed with no `id:` lines, so a cut stream could not resume. On a durable host each event now carries its log cursor as `id` (the same cursors as `GET events`). The ai-client SSE adapters already re-send the request with `Last-Event-ID`: the run id is the input id, so the retry finds the same turn (live, or replayed from the log on another instance) and streams after that cursor, without running it again. The `resume` branch of `POST run` does the same. - `GET run?runId=` only found turns in this process. When the turn is not local, a durable host now looks up its thread in `stores.runs` (the run id is the operation id, and every chat turn has a run record), opens the thread, and streams that operation's `harness.event` records from the shared log, from the start or `Last-Event-ID`, live until `harness.operation.finished`. Agent runs and child runs stay 404. - `GET run?threadId=` also reports a turn another instance runs as `activeRun` (from `runs.findActiveRun`), so a reloaded ChatClient on another instance knows to join it. Without a log store nothing changes: no ids on `POST run`, since a process-local cursor means nothing on another instance. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * docs(ai-harness): mark the session view experimental, and say what a cursor is `createSessionView`, `SessionViewState`, and `SessionViewEvents` become public API on publish. Tag them `@experimental` so their shape can still change in a minor release. The HTTP transport page now says that event cursors are opaque strings: log positions on a durable host, process-local counters without a log store. So a deployment with more than one instance needs `stores.log`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Jack Herrington <jherr@pobox.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(ai-harness): session.onQuestion, onAuthRequired, onMedia, and onUsage Each hook calls a listener for one harness event, with a typed value. An app no longer filters CUSTOM events by name to answer a question, show a sign-in, show a media file, or show a live token count. The mcp, auth, usage, media, and plugins docs use the hooks. * feat(ai-harness): first-party plugins in @tanstack/ai-harness/plugins: permissions, agent profiles, questions, todos, titles, and usage (#1759) * feat(ai-harness): first-party plugins in @tanstack/ai-harness/plugins Add permissions, agents, question, todos, title, modelPicker, projectInstructions, fileCommands, compact, usage, and boundToolOutput under the ./plugins entry. Export the portable permission names from the package root again, so edge-safe plugins can read the rules. * docs(ai-harness): say that a tool with no rule runs in plan mode * fix(ai-harness): adopt object params from #1779 The first-party plugins and their tests now call setConfig and answer with one object. Prompt results in the tests also have media: []. * docs(ai-harness): answer permission questions with onEvent The permissions page listens for the question with session.onEvent instead of a loop over session.events(). The permissions and agents pages call setConfig and answer with one object. * ci: apply automated fixes * docs(ai-harness): answer each open permission question once Tool calls can run in parallel, so two questions can be open at the same time. The sample now answers each open question once, by id, instead of the first one in the list. * docs(ai-harness): answer permission questions with session.onQuestion * fix(ai-harness): safe /compact, cut marker in approvals, and whole-folder globs - /compact fails and keeps the conversation when the summary is empty. - The approval question says when it cut the call arguments. - A **/ glob matches whole folders only, and {a,b} matches one of the parts. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * fix(ai-harness, ai-persistence): pass inputs to the instance that owns a thread, and other pre-release fixes (#1780) * fix(ai-harness): definePlugin refuses a setup() contribution at the top level `const TDefinition` turns off the excess property check, so `definePlugin({ name, adapter })` type-checked and the adapter was never used. A key of the setup() return (adapter, tools, prompts, middleware, ...) at the top level is now a type error and throws. Other extra keys stay the plugin's own API, such as the diff() of snapshots(). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(ai-harness): refuse to write a log that has a record type from a newer harness During a rolling deploy an old host could fold a log with records it skips, then write on top of that wrong state. It now reads the log but fails the write with LogVersionError. Checkpoints list the record types their writer knew, so an old host never starts from a newer fold. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(ai-persistence): store shapes that third-party backends can implement - InboxStore.get/markApplied/markRejected take threadId, so a store does not need a global index of input ids. - WorkClaimStore.get(threadId) tells a host who owns a thread. - The session revision contract allows content-hash ETags (S3, Netlify Blobs), which give the same revision back for the same content. None of these interfaces is released yet. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(ai-harness): pass an input to the host that owns the thread, not over its turn On a durable host, an input on a second host appended to the log and made the running owner fail with LogConflictError, which aborted its turn. Two actors per cold start make this routine on serverless platforms. With stores.workClaims, accept() now checks who owns the thread: - With stores.inbox (now allowed in durable mode), the input waits in the inbox and the receipt is 'queued'. The owner drains the inbox every second while it holds the claim, once more when it goes idle, and when a session opens (the crash sweep). - Without an inbox, the input is refused with 'owned_elsewhere'. A question from ctx.session.ask claims the thread and keeps it busy, so its answer is forwarded to the host that holds the question. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(ai-harness): a host that opens a thread leaves a live owner's inputs alone Between storing an input and writing its run lease, the owner's input looked crashed to another host. Any request that opened the thread there (events, hydrate, a join) recovered and ran it, and the owner then failed with a log conflict. Recovery now skips a thread whose work claim another live host holds. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(e2e): an input on a second host waits for the owner of the thread Two hosts over one set of durable stores. A follow-up on the second host is queued and runs on the owner after its turn; without an inbox it is refused. In both, the owner's turn ends normally. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(ai-harness): a forwarded turn runs under the run id of the host that took it A POST run on a host that does not own the thread forwards the prompt. The owner now runs it as the request's AG-UI run id, so the operation on the forwarding host streams the owner's events from the shared log, and useChat sees the turn on whichever instance it reached. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * docs(ai-persistence): the inbox and work claim store shapes, and content-hash revisions Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * docs(ai-harness): inputs on another instance, rolling deploys, and setup() keys Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(ai-harness): a host that passed an input on runs it if the owner stops The forwarding host reads the inbox every second until its inputs ran. When the owner stops, its claim expires, and the forwarding host claims the thread and runs them, so an input never waits for a sweep. The claim test from #1779 now covers both setups: refused without an inbox, and run once the other claim expires with one. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(ai-harness): run a forwarded input safely when the owner stops - A host that passed a POST run on runs it itself, on the operation its caller holds, when the owner stops. Before, its own run id refused it as a conflict. - An inbox entry is applied only after the owner stored the turn. Before, a failed log write lost a forwarded prompt. - A host that takes a thread from the inbox recovers the stopped owner's inputs first, as an open does. Before, they were never run. * fix(ai-harness): a forwarded resume runs as its run id, and two lint errors - A POST run with a resume that reaches a host that does not own the thread got HTTP 409: the queued receipt had no operationId, and the owner ran the resume under a new id. The inbox entry now keeps the run id, the owner runs the resume as it, and the receipt names it. - watchInbox passed an async function to setInterval, and applyForwarded had a default case where the switch can be exhaustive. Both failed test:oxlint in CI. * fix(ai-harness, ai-persistence): keep a forwarded secret answer out of the inbox A host that does not have a question cannot tell if it is a secret, so it stored the answer in stores.inbox as typed, and the entry stayed there. InboxStore.delete removes an entry. The owner deletes a forwarded answer after it applies it, and logs a secret answer as [secret]. * fix(ai-harness): a forwarded input runs once, keeps its wake mark, and ids stay bounded - The owner passes the inbox input id to cancel, command, answer, and setConfig (an @internal inputId option). An entry that the owner applies again, after a crash or a failed markApplied, is a duplicate. Before, a forwarded command ran again on each read of the inbox. - A forwarded follow-up that wakes the session keeps its wake mark. - The forwarded id set keeps the newest 500 ids, like settled operations. - Docs: a forwarded prompt runs without overrides and ephemeral messages. * fix(ai-harness): refuse a revert on a host that does not own the thread revert and unrevert write a harness.revert record and never went through accept, so a host that did not own the thread wrote to the log during the owner's turn, and the owner's turn failed with a log conflict. They now refuse with busy while another host holds the claim. The revert and unrevert cases of applyForwarded never ran, so they go with agent and background. --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Alem Tuzlak <t.zlak@hotmail.com> * feat(ai-code-mode): codeMode() harness plugin in @tanstack/ai-code-mode/harness (#1769) * feat(ai-harness): first-party plugins in @tanstack/ai-harness/plugins Add permissions, agents, question, todos, title, modelPicker, projectInstructions, fileCommands, compact, usage, and boundToolOutput under the ./plugins entry. Export the portable permission names from the package root again, so edge-safe plugins can read the rules. * docs(ai-harness): say that a tool with no rule runs in plan mode * feat(ai-code-mode): codeMode() harness plugin in @tanstack/ai-code-mode/harness * fix(ai-harness): adopt object params from #1779 The first-party plugins and their tests now call setConfig and answer with one object. Prompt results in the tests also have media: []. * docs(ai-harness): answer permission questions with onEvent The permissions page listens for the question with session.onEvent instead of a loop over session.events(). The permissions and agents pages call setConfig and answer with one object. * ci: apply automated fixes * docs(ai-harness): answer each open permission question once Tool calls can run in parallel, so two questions can be open at the same time. The sample now answers each open question once, by id, instead of the first one in the list. * fix(ai-code-mode): tell the model what execute_typescript returns (#1785) fix(ai-code-mode): describe execute_typescript result shape to the model The tool description and system prompt now say the tool returns { success, result, logs, error }, where result is the program's return value, so models stop guessing at the wrapper. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(ai-code-mode): cover the code mode allow rules and the prompt reset Add tests: a program runs with no question when permissions() asks by default, in default and plan mode. A user rule that asks for execute_typescript still wins. A failed pick drops the prompt of the last turn. The docs list all six isolate drivers and say what happens when two tools get the same safe name. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Jack Herrington <jherr@pobox.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(ai-harness): goal() plugin that keeps working until a judge says the goal is met (#1762) * feat(ai-harness): first-party plugins in @tanstack/ai-harness/plugins Add permissions, agents, question, todos, title, modelPicker, projectInstructions, fileCommands, compact, usage, and boundToolOutput under the ./plugins entry. Export the portable permission names from the package root again, so edge-safe plugins can read the rules. * docs(ai-harness): say that a tool with no rule runs in plan mode * feat(ai-harness): goal() plugin keeps a session working until a judge says the goal is met * fix(ai-harness): adopt object params from #1779 The first-party plugins and their tests now call setConfig and answer with one object. Prompt results in the tests also have media: []. * docs(ai-harness): answer permission questions with onEvent The permissions page listens for the question with session.onEvent instead of a loop over session.events(). The permissions and agents pages call setConfig and answer with one object. * ci: apply automated fixes * docs(ai-harness): answer each open permission question once Tool calls can run in parallel, so two questions can be open at the same time. The sample now answers each open question once, by id, instead of the first one in the list. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * feat(ai-harness): build a harness into a worker artifact, and run it as a worker (#1760) * feat(ai-harness): serve sessions over HTTP and WebSocket, with a typed client and OAuth connectors * feat(ai-harness): build a harness into a worker artifact, and run it as a worker * fix(ai-harness): add back sessionOfTurn for the run route, and the OAuth cases of auth.test.ts * fix(ai-harness): import ./session/index, so the built types resolve HarnessSession * test(ai-harness): the stub worker ignores cancel and reads content parts * test(ai-harness): allow Node modules in the build and worker entries * test(ai-harness): startLoopbackReceiver cases from #1555 * feat(ai-harness, ai-mcp): MCP servers in a harness, and a harness as an MCP server (#1763) * feat(ai-harness, ai-mcp): MCP servers in a harness, and a harness as an MCP server Adds @tanstack/ai-mcp/harness (createHarnessMcpServer and the mcp() plugin) and @tanstack/ai-mcp/connector (mcpConnector with OAuth sign-in and RFC 9207 iss). mcp() and mcpConnector ask spec 2025 elicitation/create requests and spec 2026 input rounds as session questions. Split out of #1555. * docs(ai-harness, ai-mcp): import definePlugin in the discoverTools sample * feat(ai-harness): GET agent-transcript and client.agentTranscript A client can read the saved messages of a child agent run, as a local session does with session.agentTranscript. The route opens the session for the principal, so a child of another thread or user stays hidden. * docs(ai-harness): list GET agent-transcript in the handler routes * docs(ai-harness): describe the card in client.agentTranscript * docs(ai-harness): read a child's messages from the browser * feat(ai-harness): createSessionView, one live store for a session in any UI (#1761) * feat(ai-harness): createSessionView, one live store for a session in any UI * fix(ai-harness): import ../session/index in the view * feat(ai-harness): close the gaps between createSessionView and the harness - Actions: followUp, continue, upload, configure, cancel(operationId), cancelInput, setDelivery, reset, revert, unrevert, and ViewAgent.cancel. - Interrupts: generic interrupts get their own list with resolve and cancel. Approvals can send edited arguments or a payload, and find their tool in a child agent. - Subagents: AgentPart.result, and after a reload each child shows its saved messages and how it ended. - A view that opens while a turn runs shows the turn from its start without what the transcript has. The snapshot is read before the transcript. - A retry drops the failed attempt, turnEnd skips a turn that stops for an interrupt, usage, models, and settings are in the state, and media that no message shows stops getting new URLs. * docs(ai-harness): every view action, other interrupts, and subagents after a reload * fix(ai-harness, ai-mcp): adopt object params from #1779 The client, the session view, and its source take one object for setConfig, answer, setDelivery, sendToAgent, renameSession, updateSession, and forkSession, as the session does. A view agent's send takes { mode }, as AgentRun.send does. The protocol, the HTTP handler, the connectors, and the MCP connector call the new store and session shapes. * docs(ai-harness): object params in the transport, view, MCP, and auth pages * test(ai-harness): rename, update, and fork a session through the client * test(ai-harness): a remote harness response with no body fails the run Since #1779, the remote harnessText fails a stream that closes before RUN_FINISHED. A response with no body is such a stream. * test(ai-harness): pass canAccess in the remote harness tests (#1783) test(ai-harness): pass canAccess to the handler in the remote harness tests The handler requires canAccess (fix/harness-transport-release). Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Jack Herrington <jherr@pobox.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(ai-harness): providerKeys() plugin lets each user connect their own model provider key (#1765) * feat(ai-harness): first-party plugins in @tanstack/ai-harness/plugins Add permissions, agents, question, todos, title, modelPicker, projectInstructions, fileCommands, compact, usage, and boundToolOutput under the ./plugins entry. Export the portable permission names from the package root again, so edge-safe plugins can read the rules. * docs(ai-harness): say that a tool with no rule runs in plan mode * feat(ai-harness): providerKeys() plugin lets each user connect their own model provider key * test(ai-e2e): read the key from the adapter factory, aimock redacts the header * fix(ai-persistence): store a tool result once when a run resumes from a client tool (#1744) * fix(ai-persistence): store a tool result once when a run resumes from a client tool * test(e2e): client tool resume stores each tool result once * fix(ai-persistence): keep a later tool result that reuses a stored toolCallId Count stored tool results per toolCallId and let each posted copy use up one, instead of dropping every posted result whose toolCallId the prefix holds. Check before the id check, so a result posted under its stored id also uses up its count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGyaNPEJb8zBzY2wY5xWdZ * Revert "fix(ai-persistence): keep a later tool result that reuses a stored toolCallId" Provider tool call ids are unique within a thread, so the reuse case does not occur. Back to the plain toolCallId match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGyaNPEJb8zBzY2wY5xWdZ * fix(ai-persistence): replace a tool result matched by toolCallId in place Skip the toolCallId match when the incoming tool result has no toolCallId, so two id-less results are not treated as one. Replace the matched stored row with the incoming copy instead of dropping it, the same as a match by id. Move the merge-rules comment onto mergeStoredMessages and add direct merge tests. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> * fix(openai-base): report Responses cache write tokens in usage (#1774) * fix(openai-base): map Responses input_tokens_details.cache_write_tokens to cacheWriteTokens Closes #1734 * fix(openai-base): keep the cache-write fix to one block; add Responses usage e2e * fix(ai-opencode, ai-sandbox): end the run when a teardown step does not settle (#1773) Bound each OpenCode teardown step to one second and log failures, and close open connections when the host tool bridge closes. Closes #1638 * feat(ai): check tool input after approval edits and middleware, just before it runs (#1750) * feat(ai): check tool input at the last moment, after approval edits and middleware * test(ai): fix the boundary client tool tests * test(ai): the subagent tool input is checked after middleware * test(ai): subagent schema errors also show the received arguments * fix(ai): keep returned client results and run tool hooks once at boundaries - A client result that comes back is kept as is. Checking the raw model input again dropped the real result when middleware had repaired it. - An afterTools boundary reuses the tool phase result, so onBeforeToolCall and the tool results do not run twice. - The boundary drains executeToolCalls through drainToolCallGenerator and passes the resume state; the hooks and resume state come from shared helpers. - The validation error shows the model's own arguments in the tool result only, so middleware-injected values do not reach the model or tracing. - Changeset, middleware docs, and args JSDoc corrected. * feat(ai): check JSON Schema and MCP tool input with @cfworker/json-schema (#1751) * feat(ai): validate JSON Schema tool input with @cfworker/json-schema * chore: keep one @google/genai copy for ai-vertex and ai-gemini * test(ai): the JSON Schema of a subagent tool checks background * fix(ai): keep the page URL out of a JSON Schema tool error A failed $ref included the validator base URI. In a browser that URI is the page, query included. * fix(ai): JSON Schema tool check accepts OpenAPI 3.0 and keeps data intact - Strip `format` only from schema positions, so a `format` key inside `const` or `enum` data still matches. - Convert boolean `exclusiveMinimum`/`exclusiveMaximum` per schema, so they work under any `$schema` and numeric bounds elsewhere still apply. - Support OpenAPI 3.0 `nullable: true`: null joins `type`/`enum`, and a nullable `$ref`/`allOf`/`anyOf`/`oneOf`/`not` schema is wrapped as `if: null, else: schema`. Local `$ref`s into a wrapped schema move with it. - Check an input with no JSON form as null, list at most 10 errors, and drop more wrapper errors (`if`, `$recursiveRef`, `additionalItems`, `unevaluatedItems`). - Read the first error line with `split('\n', 1)`, which type-checks under `noUncheckedIndexedAccess`. * feat(ai): repair common model mistakes in tool input, with chat({ repairToolInput }) (#1752) * feat(ai): repair common model mistakes in tool input before the final check * fix(ai): type the repair order list * test(ai): use an input the repair cannot fix in the subagent schema test * feat(ai): chat({ repairToolInput }) to turn off or replace the input repair false turns the built-in tool input repair off. A function replaces it: it gets { tool, toolCall, input, error } for an input that failed the check, and returns the input to check again, or undefined to send the validation error to the model. * feat(ai, adapters): keep the raw tool argument string for replay (#1753) * feat(ai, adapters): keep the raw tool argument string for replay * refactor(ai): one tool-argument reconcile helper, args sent once finishToolCallArguments replaces the three reconcile copies in ToolCallManager, TextEngine and StreamProcessor. TOOL_CALL_END now sends its arguments once, as metadata.tanstack.args reconciled to the input; in-process input is parsed back from it. Drops jsonInput and isToolInputJsonLossless. A null input no longer replaces the raw args. * fix(ai-bedrock, ai-gemini): replay malformed tool arguments as {} Malformed arguments now stay in the history next to their tool error. The Bedrock and Gemini Interactions history converters send them as {} instead of failing the request. * fix(ai-gemini): send the args snapshot only when no deltas stream The initial step.arguments snapshot went out as a TOOL_CALL_ARGS delta before the streamed deltas, so the deltas did not add up to the final arguments on the client. * refactor(adapters): always send args on TOOL_CALL_END Drop the redundant rawArgs guards and hasArguments flags. Core treats empty args the same as missing args. * test(ai): keep the JSON Schema check tests off the repair path The OpenAPI 3.0 and $ref tests from feat/json-schema-tool-input send inputs the repair now fixes (a null optional field, "3" for an integer). They check the schema alone, with repairToolInput: false. Adds a test that the repair walks a nullable schema, and restores the subagent 'yes' input. --------- Co-authored-by: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> * fix(ai): give held calls a result and keep child interrupts at boundaries - When the only call that waits for approval fails its input check, the calls that the batch gating held back now run, so each call has a result. - An approved client tool is sent to the client while another call waits for approval, as a plain client tool already was. - The validation error tells the model when an approval edit or middleware changed its arguments, so it does not retry the same valid call. - An afterTools boundary interrupt keeps the subagent interrupts of the tool phase. - Changeset and server tools docs: a bad approval edit is rejected when the run resumes. --------- Co-authored-by: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> * fix(ai-harness): adopt object params from #1779 The first-party plugins and their tests now call setConfig and answer with one object. Prompt results in the tests also have media: []. * docs(ai-harness): answer permission questions with onEvent The permissions page listens for the question with session.onEvent instead of a loop over session.events(). The permissions and agents pages call setConfig and answer with one object. * ci: apply automated fixes * fix(ai-harness, ai-e2e): adopt object params from #1779 providerKeys saves the key with credentials.set({ id, credential }). The tests and the E2E route use the object shapes of the credential store and session.answer. Prompt results in the tests have media: []. A new test checks that a key that /connect saved is scrubbed from the errors that the session publishes. * docs(ai-harness): answer the /connect question with onEvent The provider keys page answers the key question with session.onEvent instead of a loop over session.events(), and calls session.answer with one object. * docs(ai-harness): answer each open permission question once Tool calls can run in parallel, so two questions can be open at the same time. The sample now answers each open question once, by id, instead of the first one in the list. * fix(ai, ai-persistence): close subagent budget and continue edges (#1786) fix(ai): close subagent budget and continue edges A background start that throws gives the maxCalls slot back, and maxConcurrent counts that child until done settles. A non-object schema no longer drops sessionId or background, and a top-level anyOf still shows those fields. A suspended child with no stored base keeps its transcript. An exhausted parent deadline says no time is left. * fix(ai-compaction, openai-base): do not save a failed native compact (#1787) * fix(ai-compaction, openai-base): do not save a failed native compact A failed compact must not become the checkpoint or the durable record. The next process calls compact again. A compact echo keeps image files, documents, and audio. * test(e2e): retry native compact when the result has no item A compact result with no item is not stored. The next run on the same thread calls compact again, then uses the fallback strategy. * feat(ai-client): publish observable client snapshots (#1232) * feat(ai-client): publish observable client snapshots Hold chat, generation, video, and realtime UI state in a TanStack Store atom and subscribe from the framework hooks. Snapshot tool input, output, and subagent transcripts are copies. A failed realtime connect keeps the transcript. A throwing subscriber cannot stick isLoading or skip the next listener. * ci: apply automated fixes * fix(ai-react, deps): batch useChat renders and restore one @google/genai copy useChat used useSyncExternalStore, which renders synchronously on every snapshot change. A buffered stream rendered once per chunk and the time-slicing e2e spec saw 6-9 long tasks. Hold the snapshot in state so React batches changes into one render per task, as main did. The lockfile regen moved ai-vertex's @google/genai dev dependency onto a different zod peer than ai-gemini's. Two copies meant the factories test mocked the wrong one. Restore main's entry. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> * fix(ai): report evaluate usage when answer mapping fails (#1742) * fix(ai): report evaluate usage when answer mapping fails - Wrap only mapAnswers in decide() so a mapping failure reports usage (evaluate:usage event and onUsage) before the error is rethrown - A provider answer that is missing or has the wrong type made decide() throw before usage was reported, so billed tokens were lost - The success path is unchanged, and onFinish still runs only on success - Add tests for the mapping failure, the success event order, a throwing onUsage, and an adapter that throws * docs(evaluate): note that usage is reported when answer mapping fails - Say that onUsage and evaluate:usage still get the usage when an answer is missing or has the wrong type - Bump the evaluate page updatedAt * chore(changeset): say which error decide() throws when onUsage fails - A throwing onUsage hook propagates on the mapping-failure path, same as on the success path - The changeset said decide() always throws the original error, which was not true in that case * test(e2e): cover decide() usage reporting when answer mapping fails - Add an aimock mount at /evaluate-usage-on-failure that serves an OpenAI Decisions response with valid usage and a choice outside the question's options - Add the /api/evaluate-usage-on-failure route, which runs decide() through the OpenAI evaluate adapter and returns the error name and message plus the usage seen by the onUsage hook - Add evaluate-usage-on-failure.spec.ts asserting decide() rejects and the onUsage hook received the usage * fix(ai): log the decide() mapping error before onUsage runs A throwing onUsage hook replaces the mapping error, so log it first. Also correct the e2e comments (the choice has no probability, it is not checked against the options) and widen the docs and changeset wording. --------- Co-authored-by: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> * fix(ai): file each model call on its own assistant message (#1776) * fix(ai): file each model call on its own assistant message (#1533) A finished tool-call turn stays closed, and a signature goes to the step that owns it. The next model call no longer lands on the previous assistant message. * fix(e2e): typecheck the call-ownership harness The wire yields events the same way the other harnesses do, and the page label accepts tool-result content. * fix(ai): keep REASONING_MESSAGE_START metadata on the assistant message (#1789) * fix(ai): keep REASONING_MESSAGE_START metadata on the assistant message Fixes #1667 * fix(ai): file reasoning-start metadata on the run's own assistant message * test(ai): give reasoning start events the required role * feat(openai-base, ai-openrouter, ai-groq, ai-utils): send json_object when strict json_schema is rejected (#1775) * feat(ai, openai-base, ai-openrouter, ai-groq): send json_object when strict json_schema is rejected * refactor(openai-base, ai-openrouter, ai-groq, ai-utils): pick json_object in the adapter and send the schema as a system prompt Drop the structuredOutputMode option from chat(). Adapters choose JSON mode from modelOptions or the model catalog, and add the schema as a system prompt because JSON mode does not send it. * fix(openai-base, ai-openrouter): keep JSON mode off tool turns and match :variant catalog ids - supportsCombinedToolsAndSchema() returns false in JSON mode, so the schema prompt goes only on the separate structured-output request. mapOptionsToRequest no longer adds it, so it cannot repeat. - OpenRouter checks the full id (with modelOptions.variant) before the id without its :variant, in one shared helper for the JSON-only and combined sets. - Docs, changeset and skills: plain JSON Schema and streamed replies are not validated; note the openai-base json_object behavior change. - Tests: :free/variant/fallback lookup, combined gate in JSON mode, OpenRouter chat and Responses JSON mode with tools, single schema prompt, Groq prompt. * test(ai-harness): pass thread id to inbox.get in provider-keys test The inbox store's get() now takes { threadId, inputId } after the one-object params migration, but provider-keys.test.ts still called it positionally (inbox.get(receipt.inputId)), failing @tanstack/ai-harness test:types. Update it to the object form like the other inbox.get call sites. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Patrick L <patrick.michel@minubo.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Chris Trześniewski <k.trzesniewski@gmail.com> Co-authored-by: Jack Herrington <jherr@pobox.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> * fix(ai-harness): return done from the subagent start binding SubagentBinding.start is typed to return { subagentRunId, done }, where done settles when the child ends, but the session binding returned only { subagentRunId }. That broke @tanstack/ai-harness build and test:types. Capture the runAgent operation and resolve it as done, the same awaitable the agent group already uses. Also update the todos session-plugins test: chat() now checks a tool's JSON Schema before it runs, so an invalid todo status is a tool error the model sees, not the plugin's own "Each todo needs text and a status." message. The test now expects the input-validation error. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ai-harness-cli): run a harness from the terminal, with optional ACP, MCP, and dashboard modes (#1768) * feat(ai-harness): serve sessions over HTTP and WebSocket, with a typed client and OAuth connectors * feat(ai-harness): createSessionView, one live store for a session in any UI * fix(ai-harness): add back sessionOfTurn for the run route, and the OAuth cases of auth.test.ts * feat(ai-harness-cli): run a harness from the terminal, with optional ACP, MCP, and dashboard modes * fix(ai-harness): import ./session/index, so the built types resolve HarnessSession * fix(ai-harness): import ../session/index in the view * test(ai-harness): startLoopbackReceiver cases from #1555 * docs(examples): harness-cli, a notes assistant in the terminal with an Ink screen (#1770) * docs(examples): harness-cli, a notes assistant in the terminal with an Ink screen * chore(examples): keep the @google/genai resolution of ai-vertex in the lockfile * feat(ai-harness, ai-mcp): MCP servers in a harness, and a harness as an MCP server (#1763) * feat(ai-harness, ai-mcp): MCP servers in a harness, and a harness as an MCP server Adds @tanstack/ai-mcp/harness (createHarnessMcpServer and the mcp() plugin) and @tanstack/ai-mcp/connector (mcpConnector with OAuth sign-in and RFC 9207 iss). mcp() and mcpConnector ask spec 2025 elicitation/create requests and spec 2026 input rounds as session questions. Split out of #1555. * docs(ai-harness, ai-mcp): import definePlugin in the discoverTools sample * feat(ai-harness): GET agent-transcript and client.agentTranscript A client can read the saved messages of a child agent run, as a local session does with session.agentTranscript. The route opens the session for the principal, so a child of another thread or user stays hidden. * docs(ai-harness): list GET agent-transcript in the handler routes * docs(ai-harness): describe the card in client.agentTranscript * docs(ai-harness): read a child's messages from the browser * feat(ai-harness): createSessionView, one live store for a session in any UI (#1761) * feat(ai-harness): createSessionView, one live store for a session in any UI * fix(ai-harness): import ../session/index in the view * feat(ai-harness): close the gaps between createSessionView and the harness - Actions: followUp, continue, upload, configure, cancel(operationId), cancelInput, setDelivery, reset, revert, unrevert, and ViewAgent.cancel. - Interrupts: generic interrupts get their own list with resolve and cancel. Approvals can send edited arguments or a payload, and find their tool in a child agent. - Subagents: AgentPart.result, and after a reload each child shows its saved messages and how it ended. - A view that opens while a turn runs shows the turn from its start without what the transcript has. The snapshot is read before the transcript. - A retry drops the failed attempt, turnEnd skips a turn that stops for an interrupt, usage, models, and settings are in the state, and media that no message shows stops getting new URLs. * docs(ai-harness): every view action, other interrupts, and subagents after a reload * fix(ai-harness, ai-mcp): adopt object params from #1779 The client, the session view, and its source take one object for setConfig, answer, setDelivery, sendToAgent, renameSession, updateSession, and forkSession, as the session does. A view agent's send takes { mode }, as AgentRun.send does. The protocol, the HTTP handler, the connectors, and the MCP connector call the new store and session shapes. * docs(ai-harness): object params in the transport, view, MCP, and auth pages * test(ai-harness): rename, update, and fork a session through the client * fix(ai-harness-cli): adopt object params from #1779 The session source of the CLI passes answer and setConfig through as one object. The /answer and /config commands and the tests call the new session and credential shapes. The printer test state has the view's interrupts and models. * fix(ai-harness-cli): answer view interrupts, and skip old child parts on a reopened thread - The Ink screen asks each item of state.interrupts after the approvals, so a turn that waits for an interrupt can go on. - Line mode marks the saved parts of a child that ended before they loaded, so a reopened thread does not print old tool lines or save old media again. - docs/config.json lists Build your own UI once. * fix(ai-harness-cli): pass canAccess and mediaSecret to the harness handler (#1782) The handler now requires canAccess, and a mediaSecret on a durable host (fix/harness-transport-release). The CLI serves one principal behind its token, so it can use every thread, and the token signs media URLs. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * docs(harness-cli): document the --serve trust model --serve uses one bearer token, one principal, and canAccess: () => true, so any token holder can drive any thread (and its host-side tools) as the CLI's user. Document that trust boundary: keep it on loopback, what --host 0.0.0.0 exposes, and point readers who need per-user auth or thread rules to createHarnessHandler. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Jack Herrington <jherr@pobox.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(ai-harness, ai-persistence, ai-acp, ai-skills): review fix pass on sessions, durability, and adapters Sessions: inputs settle on every path (commands, detached wakes, recovery), principals follow the requester (agent starts, wakes, steer with no turn), admitted inputs leave on reject/settle, close() waits for recovery, restart notes no longer end a transcript a cut turn continues, prompt() refuses a session waiting on interrupts up front, JSON Schema command inputs run unchecked like core tool inputs, Standard Schema inputs are validated. Durability: lease renewal checks owner and status, crashed runs without a first lease stamp are found, fork closes the source session it opened and keeps the index entry, resumePending survives one bad thread, emit survives one bad listener, durable tool results are cloned, operation finish/fail wake a waiting feed, resource scope dispose no longer waits on in-flight opens. LeaseStore gets defineLeaseStore, memoryLeaseStore, and conformance cases. InboxStatus 'expired' removed (never produced). Agents: harnessContext() replaced by ctx.harness through a new SubagentBinding.extend hook in core; SubagentContextExtension is augmentable. harnessText: per-call thread id, principal option, session closed in finally, interrupt finish reason, SSE trailing buffer flushed, outer tools forwarded. ACP: TOOL_CALL_ARGS forwarded as rawInput; known() checks the harness name. Skills: watcher follows the nearest existing ancestor folder. * refactor(ai-harness): discriminate Receipt, InputSettlement, MediaRecord, and AgentRun by status A rejected receipt always carries its reason, a failed settlement always carries its error, generated media always carries the run that made it, and an agent run always names its agent. The types now say so. SettlementOutcome is the outcome arm on its own, for code that settles several inputs at once. * docs(ai-harness, ai-persistence): match the docs to the fix pass ctx.harness replaces harnessContext(), the subagents sample prints the child transcript, coalescing covers tool call arguments, commitRevert runs on every turn, plugin state without a metadata store lives in memory, durable tool records land with harness.tool.result, InboxStatus has no 'expired', defineLeaseStore and memoryLeaseStore are listed, agents.start takes a principal, and JSON Schema command inputs run unchecked. Changesets for SubagentBinding.extend and the lease store helpers. * feat(ai-harness)!: one session.send() replaces prompt, steer, followUp, and sendToAgent send(message, { mode: 'prompt' | 'steer' | 'followUp' }) always returns an Operation. A steer's operation ends with the turn it joined. send(message, { to: agentOperationId, mode }) routes a message to an agent run and returns an operation that ends with that run; an unknown run rejects with not_running. continue() and AgentRun.send stay. Log records keep their ops, so stored logs replay unchanged. ACP, harnessText, the e2e routes, the README, and the tests use send(). * docs(ai-harness): send() on every harness page, and the plugin session api sends too Every sample sends with session.send, with mode: 'steer' or 'followUp' where the page needs them, and send(message, { to }) for an agent run. The plugin ctx.session.send matches the session API name. * fix(ai-harness, ai-persistence): review follow-ups on the send() push resumePending names its counts in the AggregateError message. memoryLeaseStore is exported. harnessText closes only the per-call session it opened, so a caller's named thread stays open. A bare /command validates {} against an object schema. close() keeps its docs above the followers field. ctx.harness docs say a plain chat() run has none. An applied log input without an operation id, and a rejected one without a reason, are corrupt-log errors instead of empty strings. A failed settle append after a reset or a cut turn is reported, not swallowed. * fix(ai-harness): close only the session a call opened, and report refused wakes host.isOpen(harness, { threadId }) says whether this host holds a session open. harnessText and host.fork close a session only when they opened it, so a thread the caller holds open keeps its running turn, and a per-call thread does not pile up on the host. A background agent's wake that the session refuses (the thread waits for an interrupt) is reported instead of dropped. A command whose result landed but whose settle append failed finishes completed. send(message, { to }) refuses with the input id it was given. resumePending's error carries the threads it did open. Tests for each, plus the JSON Schema command input and the forwarded follower on close. * fix(ai-harness, e2e): lint the view receipt helper, and keep the e2e helper's prompt name The thread-controls spec calls its own HTTP helper, prompt(), not the session API. receiptOf in the view is a block, not a comma expression. * feat(ai, ai-harness, ai-persistence): credential store moves to byok, with server-side keys CredentialStore, Credential, CredentialScope, and defineCredentialStore now live in @tanstack/ai/byok. @tanstack/ai/byok/server adds storedProviderKeys, readCredential, tenantScopeOf, memoryCredentialStore, and envKeyEntry, so a server keeps each user's provider keys without the harness or persistence. Keys are not a persistence store: createHarnessHost and runCli take a `credentials` option, and stores.credentials is gone. A user without a tenant no longer reads the bucket that every scope with neither shares. Docs: a Keep Keys on the Server page, the store rules, and a secure persistence section in the persistence skill. * feat(ai, ai-harness, ai-mcp): out-of-band tool runs, client appends, and a per-run system preamble (#1798) * feat(ai-harness, ai-mcp): out-of-band session.tool() and per-run systemPreamble Two generic host primitives for the harness. - session.tool(name, args?, options?) runs one harness tool with no model turn (e.g. from a schedule or webhook), through the same middleware, input check, and permissions() rules as a model call; the call and its result join the transcript, and a tool that asks for approval stops the turn with an interrupt. A { op: 'tool', name, args?, meta? } client input runs a tool in the new expose.tools list (any other tool is refused with not_exposed); capabilitiesOf marks each tool with `exposed`; createHarnessMcpServer adds a tool_<name> tool per exposed tool. meta is echoed as a tanstack.injection custom event, and a plain-object or absent turn context carries the live threadId/runId so in-band tools can resolve the calling thread. - session.prompt(message, { systemPreamble }) prepends per-run, server-only system prompts ahead of the harness's own (e.g. host-provided memory). A client input cannot set them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(harness): document out-of-band tool runs and systemPreamble - New page "Run a tool without a model" (harness/tool-runs): session.tool, expose.tools, the tanstack.injection meta event, and the client/MCP path. - inputs: a server-only systemPreamble section next to ephemeral notes. - mcp-server: the tool_<name> per-tool entry. - config.json nav entry and addedAt/updatedAt dates. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ai, ai-harness, ai-mcp): typed expose.tools, an append input, and review fixes - expose.tools is typed from the harness tools, and defineHarness throws for a name that is not in tools. session.tool() and the new client.tool() type args from the tool's input schema. - expose.records lets a client add host records with an append input, client.append(), or the MCP append tool. A record of another type, or with a thread field, is refused with not_exposed. - A tool input now answers with its real receipt (its inputId, queued, and conflict), not always accepted. - session.tool() is refused with pending_interrupts while interrupts wait, as a prompt is. - threadId and runId go on the tool execution context, not into the user's context. ToolExecutionContext gets the two optional fields. - An MCP tool input with its own threadId field throws when the server is created, because MCP uses threadId to pick the thread. * docs(harness): typed tool runs, client records, and the MCP append tool * docs(harness): pass the CLI host credentials as a host option The credential store left persistence.stores in c2b505b78, so this snippet failed test:kiira on feat/harness-core too. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Alem Tuzlak <t.zlak@hotmail.com> * feat(ai, ai-persistence): keep approved edits raw until the final check, and bind a client-tool phase to its approval (#1764) * feat(ai): check tool input at the last moment, after approval edits and middleware * test(ai): fix the boundary client tool tests * feat(ai): validate JSON Schema tool input with @cfworker/json-schema * chore: keep one @google/genai copy for ai-vertex and ai-gemini * feat(ai): repair common model mistakes in tool input before the final check * fix(ai): type the repair order list * feat(ai, adapters): keep the raw tool argument string for replay * test(ai): the subagent tool input is checked after middleware * test(ai): subagent schema errors also show the received arguments * test(ai): the JSON Schema of a subagent tool checks background * test(ai): use an input the repair cannot fix in the subagent schema test * feat(ai, ai-persistence): keep approved edits raw until the final check, and bind a client-tool phase to its approval * test(ai-harness): adapt the replay parity test to the tool-input stack * test(ai-harness): expect media in turn results after #1779 * test(ai-harness): use session.send() after the core send() change * fix(ai): keep an approved edit only when its call passed the check - Remove checkCompletedResumedClientTools. Main's final check (#1750) keeps a client result as it is, so this pass never checked the input. It only sent a second TOOL_CALL_RESULT and wrote an unchecked edit. - A null editedArgs is no edit (executeToolCalls uses editedArgs ?? input), so the transcript keeps the model input, not the string "null". - A middleware skip did not pass the check, so its edit is not kept. retainApprovedArgumentEdits takes the phase result and picks the ids. * feat(ai-harness): coding tools in @tanstack/ai-harness/plugins/coding (#1767) * feat(ai-harness): first-party plugins in @tanstack/ai-harness/plugins Add permissions, agents, question, todos, title, modelPicker, projectInstructions, fileCommands, compact, usage, and boundToolOutput under the ./plugins entry. Export the portable permission names from the package root again, so edge-safe plugins can read the rules. * docs(ai-harness): say that a tool with no rule runs in plan mode * feat(ai-harness): coding tools in @tanstack/ai-harness/plugins/coding Add workspaceTools (read, write, edit or patch, list, grep, bash, webfetch, websearch), the WorkspaceBackend with hostBackend, snapshots (/undo, /redo, revert file restore), and formatter in a new Node entry. turndown and @vscode/ripgrep are optional peers. Split out of #1555. * docs(ai-harness): end the terminal loop of the coding samples on exit * fix(ai-harness): adopt object params from #1779 The first-party plugins and their tests now call setConfig and answer with one object. Prompt results in the tests also have media: []. * docs(ai-harness): answer permission questions with onEvent The permissions page listens for the question with session.onEvent instead of a loop over session.events(). The permissions and agents pages call setConfig and answer with one object. * ci: apply automated fixes * fix(ai-harness, ai-e2e): adopt object params from #1779 The coding tool tests and the coding-tools E2E route call session.answer with one object. * docs(ai-harness): print and answer with onEvent in the coding samples The coding agent and coding tools samples print the text with turn.onEvent and answer questions with session.onEvent, instead of a loop over prompt().stream(). They call session.answer with one object. * docs(ai-harness): answer each open permission question once Tool calls can run in parallel, so two questions can be open at the same time. The sample now answers each open question once, by id, instead of the first one in the list. * docs(ai-harness): ask one question at a time in the coding samples Tool calls can run in parallel, so two questions can be open at the same time. The terminal samples now ask each open question once, one at a time. A failed turn prints its error and the loop goes on. * feat(ai-harness, ai-sandbox): codingAgents and sandboxWorkspaceBackend in @tanstack/ai-sandbox/harness (#1771) * feat(ai-sandbox): codingAgents and sandboxWorkspaceBackend in @tanstack/ai-sandbox/harness codingAgents({ sandbox, agents, workspace? }) lets a harness hand tasks to Claude Code, Codex, Grok Build, or any ACP agent in a sandbox, with a saved session per agent and read-only plan mode. sandboxWorkspaceBackend runs the harness coding tools in a sandbox. Split out of #1555. * test(ai-sandbox): an agent without plan options does not run in plan mode * fix(ai-sandbox): adopt object params from #1779 The coding agents test calls session.setConfig with one object. * fix(ai-harness, ai-sandbox, ai-e2e): use session.send() after the core send() change * fix(ai-harness, ai-sandbox): review fixes for the coding tools - A background job keeps the last 10 MiB of its output, not all of it. The host backend decodes UTF-8 on the stream, so a character split over two chunks stays whole. - A Windows network path (\server\share) goes through the boundary check before realpath, which connects to the server. - patch refuses an Add File on a folder before it writes any file. - The formatter checks the project again after a failed check, so one failed readdir does not stop formatting for the session. * perf(ai-harness): faster file reads, edits, and searches in the coding tools - edit_file skips a loose match level that changes neither the file nor the old text. A miss in a 5 MB file takes about 12 ms, not 1 s. - hostBackend.readLines reads one page of a file. It counts lines on the bytes and decodes only the page. read_file uses it when the backend has it, and reads the whole file when it does not. - Without rg, list_files and grep read up to 16 folders or files at a time, and keep the order. - The real path of the root is read once, the permission check reuses the real path of the call, read_file needs no stat when the file is there, and writeFile makes folders only when one is missing. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * feat(ai, ai-persistence, ai-harness, ai-mcp, ai-acp, ai-sandbox, ai-utils): the harness redesign as one PR, with one durable log per session, access, waits, the agent tree, and coding workspaces (#1807) * feat(ai-persistence): LogStore v2 contract and the records API with an epoch * feat(ai-persistence): LogStore ownership with an epoch fence * docs(ai-persistence): LogStore v2 reference * refactor(ai-harness): move the harness to the LogStore v2 call shapes * feat(ai-persistence): LogStore input lane with atomic consumption * feat(ai-persistence): LogStore snapshots, truncate, and delete * feat(ai-persistence): memory log backend shared by store instances, and the kit twin option * docs(ai-persistence): acquire adds a missing owner row and deleteLog resets it; widen the conformance expiry window for networked stores * feat(ai-harness): seal the public API with subpaths, one-object calls, and grouped options (#1809) * chore(ai-acp): pin @agentclientprotocol/sdk to ~1.8.0 * chore: keep the lockfile peer resolution unchanged for the ACP pin * refactor(ai-harness): drop the 'run' lifetime, type schema-less command input as unknown, use core metadata keys * refactor(ai-harness): subpath entry points for server, oauth, media, and Node plugins * refactor(ai-harness): HarnessSession as a type over a private class, whenIdle, and harnessOf * refactor(ai-harness): group host options per concern, and keep project and reduce on the harness * docs(ai-harness): one-object samples, subpath imports, grouped options, and the import guide * refactor(ai-harness): one-object parameters for the host, session, client, view, and helpers * docs(ai-harness): client and view samples in the one-object shapes * refactor(ai-harness): ctx.harness always present, and harness-first host.open and helpers * fix(ai, ai-harness): each context default registration removes only itself, an empty media.secret fails at start, and the docs say the oauth loopback helpers need Node * feat(ai-persistence, ai-harness): identity and access, with tenants, shared sessions, and a run context (#1812) * feat(ai-persistence): session index v2 keyed by tenant and thread, with owner, members, and required compare-and-set * feat(ai-harness): a RunContext for every run, so tools and plugins act as the user who started the run * feat(ai-harness): access types and the membersOnly() policy * feat(ai-harness): tenant-keyed index rows, log ids, and media keys * feat(ai-harness): host access with a principal-bound handle, sharing, revoke, and handlers that fail closed * docs(ai-harness): access and tenants, shared threads on membersOnly, and the run context * feat(ai-mcp, ai-acp, ai-harness-cli): the MCP server, the ACP agent, and the CLI act as a principal * fix(ai-harness): every thread-keyed store write carries the tenant * fix(ai-harness): delete removes the transcript, session plugins see the owner, host.hasAccess, and events wait for a new thread * test(ai-e2e): the protocol route on host access, and a shared-thread access spec * test(ai-harness): delete removes the transcript and closes the session * chore(ai-harness-cli): list @tanstack/ai-mcp as a dev dependency for the /mcp test * feat(ai-harness): session lifecycle with handles, observe, retention, host plugins, and a purging delete (#1813) * fix(ai-harness, ai-mcp): OAuth flows and commands stop on their signal * fix(ai-harness-cli): SIGINT and SIGTERM cancel the turn, then stop, in every mode * fix(ai-harness): one retention policy bounds every per-session collection, the feed, and the log tail * feat(ai-harness, ai-mcp, ai-acp, ai-harness-cli): reference-counted session handles with idle close and maxOpen * docs(ai-harness): lifecycle and retention, handles, observe, signals, and OAuth cancel * feat(ai-harness, ai-mcp, ai-acp): host.observe, a read-only session view, and read routes on it * test(ai-e2e): delete purges media and messages, and reads work after idle close * feat(ai-harness, ai-mcp): host-lifetime plugins, MCP stdio servers once per host, and delete that purges a thread * fix(ai-harness): definePlugin infers its definition only from the argument * fix(ai-harness): key host plugins by the harness definition, not its name * test(ai-mcp): open two sessions of one harness definition to share a stdio server * test(ai-harness, ai-harness-cli): give polled checks 5 s, for slower CI runs * feat(ai-persistence, ai-harness): the session log is the session, with ownership, an input lane, workers, schedules, and snapshots (#1814) * feat(ai-harness): thread-state log records and their fold * feat(ai-persistence, ai-harness): one persistence shape, every host has a log * feat(ai-harness): settings, config, plugin state, interrupted turns, overrides, and agent results live in the log * feat(ai-harness): one ownership epoch per log, renewed once per host, with abort on loss * feat(ai-harness): runs and interrupts live in the session log * feat(ai-persistence, ai-harness): inputs from any machine through the log input lane, host.submit, and no inbox, work claims, or leases * feat(ai-harness): createHarnessWorker, session.schedule, and step.sleep on the log * test(e2e): a worker on another host runs a submitted input and a schedule * feat(ai-harness): one log per session tree, session.logState(), and a wake when a busy session closes * feat(ai-harness): snapshots in the log store by size or time, retention logRecords, and the harness.gap event * fix(ai-harness): export LogOwnedElsewhereError, and say that the log keeps ephemeral messages with the turn * docs(ai-harness, ai-persistence): durable sessions, one log per tree, workers, snapshots, and the changesets for the log as the session * ci: apply automated fixes * fix(ai-harness): check answers and cancels from submit and schedule against their run, a schedule against its maker, and keep a same-host takeover and a lost log's inputs working * test(ai-harness): give polled checks 5 s, for slower CI coverage runs * test(e2e): read the data lines of POST run streams that now carry an id line per event * feat(ai, ai-harness, ai-mcp): one redaction sink, the final tool result in the log, secret answers by reference, and single-flight OAuth refresh (#1815) * feat(ai): CredentialStore compare-and-set pair, readCredentialEntry, and one scrubSecrets * feat(ai-harness): one redactor per session for log records, feed events, plugin state, notes, and the model context * feat(ai-harness): the log keeps the final tool result the model saw, and errors and step results leave the session scrubbed * feat(ai-harness): a secret answer sent to another host goes through the credential store as a one-time reference * test(e2e): a secret answer sent to another host never shows in the transcript, the events, the log, or the model input * feat(ai-harness, ai-mcp): OAuth refresh runs once per host, saves with compare-and-set to the scope it came from, and keeps the client registration * docs(ai, ai-harness, ai-mcp): redaction, secret answers across hosts, the final tool result, OAuth refresh, and the CredentialStore compare-and-set pair * feat(ai, ai-harness, ai-mcp, ai-acp, ai-harness-cli): durable waits in the log, at the boundary, in background runs, and on every transport (#1817) * feat(ai-harness): the Wait type and waits in the log, with expiry, waitFor, settled({ inputId, signal }), and operator answers to human waits * wip(ai-harness, ai-mcp): PR 7 boundary, coding-approvals, live-waits, background-waits (stopped mid-way, not checked) * feat(ai-harness, ai-mcp): waits at the boundary (permissions, the question tool, step.ask), live MCP and sign-in waits, and background runs that suspend and resume * feat(ai, ai-harness, ai-mcp, ai-acp, ai-harness-cli): agent runs pause on boundary approvals through SubagentBinding.interrupts, and every transport uses one wait shape * test(ai-mcp): the media result has a waits list * test(e2e): an approval asked on one server is answered on another, and an unanswered wait expires * docs(ai, ai-harness, ai-mcp, ai-acp, ai-harness-cli): wait for a person, step.ask, approvals at the boundary, background waits, and waits on every transport * fix(ai-harness): a session that does not own the log shows the waits and interrupts other hosts write * fix(ai-harness): lint errors in agent waits, step.ask, and permission resources * test(ai-harness, ai-code-mode): approvals are turn waits answered with resolve, and the device code carries expiresAt * test(e2e): in acceptEdits mode the write asks before it runs, with its formatter, and a reject writes nothing * feat(ai-harness): the agent tree, with typed agent actions, limits and budgets that hold, run inheritance, shared state, durable groups, and harnessAgent as a child run (#1818) * feat(ai-harness): typed subject-first agent actions start, run, and group, with grouped options, for plugins and agent code * feat(ai-harness): the run tree in the log, with RunNode for turns, agents, tools, and commands, and agentRuns read from it * feat(ai-harness): agent limits and budgets that hold, with queued starts, per-run usage, timeouts, attempts, and a budget code * feat(ai-harness): background runs inherit turn plugins, child signals follow their parent, tool-mode children get ctx.harness and share the turn budget, and ctx.harness.state shares state by key * feat(ai-harness): durable agent groups with a concurrency gate and keyed children, and routed agents that take steers, late joins, and turn hooks * docs(ai-harness): agents from code, groups, limits, budgets, the run tree, shared state, and routed agents * feat(ai-harness): harnessAgent runs as a child session in the root log, for the parent's user and keys, with usage, budget, waits, and events in the parent run * docs(ai-harness): the harnessAgent child run, as built * test(e2e): agent groups with a concurrency gate, a run budget, shared state, and a harnessAgent child run * test(ai-harness): give polled checks and a takeover deadline room for slower CI coverage runs * feat(ai-harness): handoffs between agents and to a person, the active agent in the log, and host.transfer between harnesses (#1819) * wip(ai-harness): PR 9 transfer (stopped mid-way, not checked) * feat(ai-harness): host.transfer moves a conversation to another harness with an envelope, links both threads, and is safe to retry; fork stays in its harness * feat(ai-harness): the active agent lives in the log and answers when the router picks nothing; routing.strategy 'handoff' becomes routing.after 'main' * feat(ai-harness): ctx.harness.handoff, a handoff tool, and one handoff path for code, tool, and router, with the target answering in the same turn * feat(ai-harness): hand a turn to a person through a human wait, operator replies with as: 'operator', and run.handoff for the main model's tools * feat(ai-harness): handing a turn to a person is opt-in with routing.humans, and an operator reply shows live in the view * test(e2e): handoffs to an agent and to a person, and a transfer between harnesses that is safe to retry * docs(ai-harness): hand off a conversation to an agent, to a person, or to another harness * test(e2e): the handoff route reads view text by message role * feat(ai-harness, ai-sandbox, ai-sandbox-local-process): coding tools for many threads, with workspaces per run, locks, bounded jobs, and moving threads between hosts (#1820) * fix(ai-harness, ai-sandbox): coding commands get an env allowlist, one workspace per thread, and real paths checked under the lock * wip: unfinished node work, saved before a shutdown (not reviewed) * fix(ai-harness): shared file locks with an unchanged check, and snapshots per thread and per run * fix(ai-harness, ai-sandbox, ai-sandbox-local-process): bounded background jobs with a ring buffer, a kill that waits for the spawn, workspace close, and an env allowlist in the local sandbox * perf(ai-harness, ai-sandbox): size caps for webfetch and read_file, range reads in the sandbox, one grep listing, and coding events for jobs, snapshots, and the formatter * docs(ai-harness, ai-sandbox, ai-sandbox-local-process): run the coding tools for many threads, with workspaces per thread, jobs, moving threads between hosts, and snapshots in the log * feat(ai-harness): ownership.hostId pins threads that use a local folder to their host, with E2E specs for env, roots, moving a sandbox thread, stranded, and pinning * chore(ai-harness): allow the ps binary in knip, and give polled checks room for slower CI coverage runs * feat(ai, ai-harness): protocol v2 with one wire encoding, healthy streams, run tags, lifecycle events, usage by source, and tracing (#1821) * feat(ai-harness, ai): protocol v2 frames with error codes and request ids, observability types, and pull-based SSE and socket senders * docs(ai-harness): first draft of the harness protocol page * feat(ai-harness): one wire encoding on every transport with hello, version, request ids, and heartbeats; hooks and status from other hosts; a gap event for evicted feeds * test(e2e): the access spec skips the hello frame of the session stream * feat(ai-harness): a grouped stream option for the heartbeat, the slow-consumer close, and the SSE and socket buffer limits * feat(ai-harness): run tags on every event frame, ctx.model.chat for plugins, and usage by source * feat(ai-harness): lifecycle events, counted router and transfer model calls, tracing with span links, and onOperation reports * docs(ai-harness): the harness protocol page, keeping the stream open, stopping a turn from useChat, and the changesets for reporting and observability * docs(ai-harness): run tags and the gap reload in the custom UI page * test(e2e): the wire, the gap, the run tree in one stream, lifecycle events, tracing, remote status, cancel from another host, and an unsupported version * test(ai-harness): give polled checks room for slower CI coverage runs * feat(ai-mcp, ai-acp, ai-code-mode, ai-skills, ai-harness-cli, ai-harness): integrations on the new harness, with MCP reconnects, ACP order, a skills watcher per host, and a CLI that cancels at once (#1822) * feat(ai-mcp): mcp() reconnects with backoff, lists tools again on list_changed, and takes needsApproval and toolName per server * fix(ai-acp, ai-code-mode, ai-harness): ACP keeps prompt order and releases idle sessions, code mode never moves approval tools, harnessText acts for the caller * perf(ai-skills): one folder watcher per host with a cache of the skills * feat(ai-harness-cli): cancel and steer act at once in a terminal, loadOptional reports real errors, and --dashboard goes away * chore(changeset): the integration changes for mcp, acp, code mode, skills, and harnessText * perf(ai-harness, ai-harness-cli): cheaper plugin, log, media, view, client, and host-event paths, no idle store calls, and context prompts after the history (#1823) * perf(ai-harness): cache plugin lists and permission patterns, read instruction files async, parse SSE as a stream with one abort listener and capped backoff, and give each control input an id * perf(ai-harness): stream media, cache base64 parts across turns, cap open-ended ranges, and add a byte-capped event ring * perf(ai-harness, ai-harness-cli): the view and the printer only redo what changed, children load on demand with view.loadAgent, operation.stream reads from its start * perf(ai-harness): ring buffer log tail, joins and rebuilt operations read from the run start, a per-tenant host event bus, no MESSAGES_SNAPSHOT in the log, and no idle store calls * perf(ai-harness): plugin prompts with placement 'context' go after the history, so the cached prompt prefix stays valid * test(ai-harness): one threadId in the expected gap frame * test(e2e): the first-party fixtures match the context prompt that now comes after the user message * fix(ai, ai-harness): a guard tier for tool calls, complete plugin removal, media and body limits, signing keys, and turn and config fixes (#1824) * fix(ai-harness): a settled operation keeps its status, a media signer with key rotation, and agents() rule docs that match the code * fix(ai-harness): every plugin contribution has an owner, ctx.on cleans up, inline media goes to the media store, and capabilities report the socket only when mounted * docs(ai-harness): say when capabilities report the WebSocket * feat(ai): onGuardToolCall middleware hook that runs on the final arguments of every tool call * test(e2e): onGuardToolCall denies on the final args after a transformArgs * fix(ai-harness): tool turns take no steers, a turn keeps its config, removing a plugin removes all of it, body and frame limits, signing keys, and a warning on tool name clashes * fix(ai-harness): the media signer's key check covers an empty secret * fix(ai-harness-cli): serve signs media URLs with a key derived from the token, so a short token meets the 32-byte key rule * feat(ai-harness, ai-mcp, ai-acp, ai-code-mode, ai-harness-cli): the final review of the harness, with its fixes, one object per call, and the known limits (#1825) * fix(ai-harness, ai-mcp, ai-acp, ai-code-mode): final review fixes for durability, access, plugins, transports, integrations, and coding tools * fix(ai-acp): one agent app serves one connection, with editor capabilities read from that connection, and no private SDK field * fix(ai-harness): a thread delete ends it in first-party plugins, the bash spill is scrubbed, removed schedules go at once, agent run receipts resolve, and session.append takes one object with an optional inputId * refactor(ai-harness, ai-mcp, ai-harness-cli): session, client, and view methods take one object, the client and view use putMedia and client.sessions, and internal fields leave the public types * docs(ai-harness, ai-mcp, ai-acp, ai-code-mode): the final review changes, the fixed durable-tools and server-tools text, and a known limits section * fix(ai-harness): revoke and unshare reject when work does not stop, thread delete reaches child logs and known harnesses, plugins declare kept records, and the review fixes - host.revoke and host.sessions.unshare reject when the work stop fails (the access is gone already). unshare ends the reads before it stops the work. - createHarnessHost takes harnesses, so a delete on a new host ends the thread in its plugins. An unknown harness warns. A child thread ends in the log of its root. - A plugin declares keepRecords. The core fold reads them from the plugins of the harness that folds the log, and counts per type. snapshots() keeps its 500 steps this way. - revert and unrevert open the workspace of the thread from the log when no turn opened it on this host. - The harness.revert and harness.job records keep the principal. - append with no inputId goes through accept(), so its id is real. - A formatter config below the root, or a write under node_modules, asks again. - The base64 cache takes the largest encodedBytes. The client takes idleTimeoutMs. - An events() read listens for a revoke from the call, not the first pull. * docs(ai-harness): the harnesses option, keepRecords, idleTimeoutMs, a revoke that rejects, and one known limit less * feat(ai-harness): ctx.session.keptRecords reads the kept records of a plugin, and session.revert reads them the same way * fix(ai-harness): an always answer saves no rule for an @unsaved: action * refactor(ai-harness): mounted plugins keep each contribution list with its owner, not in parallel owner arrays * fix(ai-harness): a plugins remove setting is rejected when a kept turn plugin or a harness middleware needs the removed capability * fix(ai-harness): client media checks every part before it stores any, refuses provider file handles unless media.allowProviderFiles, and the frame limit docs name the server maxPayload * fix(ai-harness): a retried client input with inline media reuses the stored media, so it stores nothing again and is a duplicate, not a conflict * docs(ai-harness): routed root-agent turns take steers and late joins, only session.tool() turns do not * fix(ai-harness-cli): --serve signs media URLs with --media-key or HARNESS_MEDIA_KEY, or a random key, never with the bearer token * fix(ai-harness): keep the usage source in the view, and a host events read that listens from the call * fix(ai-harness): send context prompts as the last system prompt, not as a user message * fix(ai-harness): check the snapshot shape in client.events(), and document its head-first default * feat(ai-harness): an onTurnStart plugin hook, used by projectInstructions and workspaceTools * fix(ai-harness): keep the interrupt messages snapshot in the log, so a rejoin from the log gets it * fix(ai-harness-cli): load the saved child agents in the TUI example, and document view.loadAgent for a ui * fix(ai-mcp): no reconnect timer after close, backoff that survives crash loops, and onDuplicateTool 'error' * fix(ai-harness-cli): tell a missing optional package by its resolve error code, not the message text * fix(ai-harness): check the inner session of harnessText through the outer thread, and pass the run principal on as is * fix(ai-harness): bind the harnessText inner thread to its outer thread before the outer check applies * fix(ai-acp): keep the sessions of the connection known on a host with no session index * refactor(ai-skills): one shared watcher per host that only triggers a sync, with no skill cache * fix(ai-harness): one harnessText inner thread id shape, <outer>/inner/<harness>, on every host * fix(ai-harness): use the real OpenTelemetry types, guard a tracer that throws, and trace a transfer inside it @opentelemetry/api is now an optional peer (type imports, and a lazy load for trace.setSpan when a host has a tracer). The copied span types and the private context key are gone. A tracer or span call that throws is logged and fails no run. The harness.transfer span is part of host.transfer, not a patch after it. * fix(ai-harness): trim the protocol to one version check, put the parent run on RUN_STARTED, and surface harness.error in the client - A requested version other than 2 gets unsupported_version from one equality check. negotiateVersion, SUPPORTED_PROTOCOL_VERSIONS, and the supported field are gone. - harness.event frames and session events no longer carry parentRunId. The AG-UI RUN_STARTED of a child agent run has it. - client.events() and client.hostEvents() connect again after slow_consumer and throw a HarnessProtocolError (code + message) for any other error frame. * fix(ai-harness): no usage event after a run finished, and bySource on old snapshots - A ctx.model.chat call that ends after its run settled (the title plugin) writes the usage record only: the run sends nothing after RUN_FINISHED. - Usage totals from a snapshot written before bySource get an empty map at load, so session.usage().bySource is always an object. * fix(ai-harness, ai-persistence): read takeover and open questions from real state, and clear the runs of a stopped owner - LogStore.acquire returns expired: true when it takes a log from another owner whose ownership expired with no release. harness.ownership.changed says taken_over from that, not from pending inputs. - The host status reads open questions from the folded log only. - The status rule drops the running operations of a stopped owner on taken_over, and the old operation of a resumed input, so a reader host does not show running forever after a takeover. * test(ai-harness): an append is an input, so a non-owner forwards it and the log has its input records - shared-log: check that the two app records land in one store append, or neither, among the input records that the append now writes. - ownership: the append of a host that does not own the log is queued and the owner writes it. A write that is not an input (a schedule) is still refused. - docs: an append on a host that does not own the thread gets a queued receipt. * fix(ai-harness): coding tools review fixes for many threads - The changed-since-read check keeps what each chat run read, so a lead and its tool-mode subagents do not lose each other's edits. - File locks are keyed by the sandbox (or host) and the real path, not by the root string. - A foreground command moves to the background only under maxJobs, and the log gets its process id (exec onStart), so a later host can stop it. - A job that the backend cannot check (killJob throws, or no killJob) stays in the log, and the model hears that it may still run. hostBackend.killJob throws when ps gives no start time. - workspaceRoot(base, ...parts) encodes each part of a workspace folder, so a thread id from a client cannot leave base. - host.unpin({ harness, threadId }) unpins a thread from a host that does not come back. * fix(ai-sandbox, ai-sandbox-local-process): job ids with a start time, one commandEnv, and passEnv - sandboxWorkspaceBackend gives a job id of pid and start time (ps -o lstart), and killJob stops only the process that started then. It rejects when it cannot check a process that runs. - commandEnv and createOutputTail live in @tanstack/ai-sandbox, and local-process imports commandEnv from there. - localProcessSandbox({ passEnv }) gives commands more variables of the host, for a CLI login or a proxy. * docs(ai-harness, ai-sandbox): safe workspace roots, host.unpin, a stable hostId, no isolation claims for host commands, and passEnv * refactor(ai-utils, ai-harness, ai-sandbox, ai-sandbox-local-process): one commandEnv and createOutputTail in @tanstack/ai-utils The three packages import both helpers from @tanstack/ai-utils, a new direct dependency of each. No more copies. commandEnv reads process.env only when there is a process, and createOutputTail needs no Buffer. * feat(ai-persistence, ai-harness): host heartbeats, so a pin lapses only when its host stopped - LogStore.heartbeat({ hostId, ttlMs }). acquire and listOrphaned ignore the affinity of a host with no live heartbeat. The memory store, the conformance suite, and the SQL recipe have it. - A host with ownership.hostId sends a heartbeat each renewMs, also when it owns no log. A live host keeps its pins on idle threads. When it stops, another host reattaches its threads. - Docs for the workspaces, deploy, durable sessions, and store pages. * fix(ai-utils): type the process env of commandEnv, so the React Native smoke typechecks Without process, commandEnv reads {}. The union of process.env and {} cannot be indexed by a string under the React Native smoke tsconfig (TS7053 at command.ts:46), and the Test job stopped there. * fix(ai-vertex): give ai-vertex the same @google/genai copy as ai-gemini again The lockfile moved the @google/genai devDependency of ai-vertex to the zod@4.6.5 peer copy, while ai-gemini keeps the zod@4.3.6 copy. Then the vi.mock('@google/genai') of the vertex tests mocks another file than the one that ai-gemini imports, and tests/factories.test.ts fails. * test(ai-harness): give the live-host pin test a 1 s heartbeat With a 300 ms ttl and a 100 ms heartbeat, a CI run that holds the event loop for 200 ms lets the heartbeat expire, and the pin stops counting. * feat(ai-harness, ai-persistence): an operator role, so the user who waits for a person cannot act as that person membersOnly() gives the operator action only to the role 'operator' by default. membersOnly({ operator: { roles } }) sets the roles that have it. A human wait takes only 'operator', for an answer and for a resolve that ends it, on a handle and through host.submit. * fix(ai-harness): handoff review fixes - The fold ends a handoff to a person when its human wait settles in any way, and refuses a human handoff record with no waitId. - The router writes the move to the main model once when the active agent is gone. - An operator can hand back to a root agent of a turn plugin: the human handoff record keeps the agents of its turn. - routing.handoffs: false gives no model the handoff tool. - A handoff stops only the chat that made it. - A handoff in a part that also stops for interrupts waits with them, so the resolve turn runs the target or pauses for a person. - The view adds no empty message for a handoff to a person or an operator hand-back. - An operator reply keeps its text in the assistant message and its files in a marked user note, which the view shows with the reply. - host.transfer throws for a source thread that does not exist. * fix(ai): ask onShouldContinue after the resumed tools, before the next model call A resume runs the pending tool calls, then went to the model with no loop check. Now the middleware onShouldContinue decides, as after any tool phase. false ends the run with no more model calls. So a harness handoff in a part that also waited for a client tool runs the target in the resolve turn, and the agent that handed off does not answer again. * fix(ai-harness): a harnessAgent child goes on after any resume, and a POST run resume of its wait streams the parent run The parent run now listens for the next turn of the child session, so an answer that comes early, or a child that resumes by itself (an expired wait), no longer leaves the parent run waiting for good. An answer to a child wait returns a receipt that names the parent run, so the POST run handler streams that run to its end. * fix(ai-harness): a plugin start with parent: ctx.context.run is a child of that agent run A top-level agent run that awaited a plugin start could wait for good under maxConcurrent: the start queued behind the run that awaited it. With parent: ctx.context.run, the start is a child of the running agent run, as ctx.harness.agents starts it, and takes no maxConcurrent slot. * fix(ai-harness): child ids of a resumable run in their own namespaces, so keyed children never collide and an outliving child keeps its steps Keyed and positional children now get separate ids (start:k:<key> vs start:n:<n>, group:k:<key> vs group:n:<n>), after a '/' that the settle cleanup prefix agent:<parentInputId>: cannot match. So a background child that outlives its parent keeps its steps, tool results, and kept answers. A known child of another agent than the one asked for now throws. * fix(ai-harness): a resumed agent run carries the usage of the run before, so its budget holds across a restart The log fold now keeps the usage sum of a run whose input is still applied, and moves it to the next run of that input when it applies. So maxTokens and maxCost count every attempt of a resume: true run. * fix(ai-harness): onModelError of an agent run retries only the failed model call The option was asked for any failure of the run, and 'retry' ran the whole agent again from its first message, with no bound: a tool side effect ran twice, and a permanent error looped for good. Now a chat middleware asks the option only for a model call that fails before it streams, waits a short backoff, and calls that model again. Core calls one model call at most 3 times. * fix(ai-harness): host.open reserves only real harnessAgent child and harnessText inner thread ids host.open refused any thread id that contained /agent/ or /inner/. One helper now refuses such an id only when the part before the marker is a thread of the index, so an app id like org/agent/notes opens. * docs(ai-harness): document only the stable fields of harness.run.node, and mark its bookkeeping fields internal * chore(ai-harness): import the backoff wait under its own name, so it shadows no wait * refactor(ai-harness): one admission gate for maxConcurrent slots, group slots, and tree budget calls The session maxConcurrent slots, the slots of each group, and the tree budget calls that ran when a queued child started were three separate mechanisms. A small Gate now holds the slots of both the session and a group, with FIFO waiters, drop on abort, and an admit hook that takes the tree budget call as a start enters. admitStart replaces reserveWhenReady. No behavior change. * feat(ai-harness): a plugin start in a tool call of an agent run is a child of that run, with parent: null to opt out ctx.agents of a plugin now finds the agent run whose tool call runs (when it is the only operation of the session in a tool call), and starts the agent as its child, so it takes no maxConcurrent slot and the run can await it. parent: { runId } still names the run, and parent: null starts a top-level run that takes a slot. * fix(ai-harness): refuse every /inner/ thread id from app code host.open, host.submit, host.fork, and host.transfer now refuse any thread id with /inner/, also when the thread before it does not exist yet, so no app call can take the id of a harnessText inner session first. Only the throughOuterThread open gets one. The narrow /agent/ rule stays. * fix(ai-harness): a stop of the parent run cancels its harnessAgent child again The change that lets a child go on after any resume dropped the abort listener of the parent run. A stopped run then left its child running, and a run stopped while its child waited never ended. * test(ai-harness): skip four flaky CI tests, and no backoff after the last onModelError retry * feat(ai): export validateJsonSchema and the Standard Schema types * fix(ai-harness): recovery fails an interrupted agent run whose waits never reached the log, so it does not wait forever * fix(ai-harness, ai-mcp, ai-acp, ai-harness-cli): one wait view, and answer and resolve check every wait they end - snapshot().pendingWaits is the one wait view. pendingInterrupts and pendingQuestions are gone. A PendingWait has its interrupt when a chat run stopped for it, turn: true for a wait of the stopped turn, and the secret and url of a question. The HTTP connect payload, the view, MCP, ACP, the CLI, and the e2e apps read it. - The answer to a logged wait is checked with validateJsonSchema of core (cfworker), so pattern and every other keyword count. The hand-written JSON Schema validator and the copied Standard Schema types are gone. - resolve takes only waits with a pending interrupt (use_answer for a step.ask question or a handoff wait), and only the waits of one run (mixed_runs). host.submit checks a resolve against the run of its wait. - A second answer to a wait that an answer ends now is refused. - The MCP resolve tool finds a background-run wait in the log and checks every decision, so it answers a wait that another server opened. * test(e2e): read only this test's request from the aimock journal in the ArkType wire spec Other specs run at the same time against the same aimock. The spec read entries[0], which was a request of a parallel spec, so it got no tool. It now finds its request by the x-test-id header, and it no longer clears the journal that the other specs read. * fix(ai-harness): no dropped-handle warning after host.close() host.close() closes every session, but it kept the hold records. When the garbage collector later dropped a handle that was not closed, the host warned. The warning came at a random time, also while vitest closed a test file, which fails the run with 'Closing rpc while onUserConsoleLog was pending'. The host now gives no warning after close(). A test in client-reads also closes the handle that it opens. * feat(ai): add lastChatMiddleware to SubagentBinding, and the secret_answer credential type - binding.lastChatMiddleware runs after the own middleware of each ctx.chat call of an agent run, so a host middleware sees the final model context and tool results. Nested children get it after their own. - Credential has a secret_answer type with an expiresAt, so the harness keeps a secret answer apart from the user credentials. * fix(ai-harness): keep secrets out of step values, agent middleware, and secret answer rows - step.do throws when its value holds a secret of the session, on the first run and on a replay, so the log never keeps a secret and a tool never gets a silent '[redacted]' value. - The redact, tool result scrub, and tool result recorder middleware of an agent run go in binding.lastChatMiddleware, after the agent's own ctx.chat middleware. The log keeps the result the model got, and an agent middleware that sets the messages again cannot send a secret to the model. - Secret answers are credentials of the type secret_answer with an expiresAt 24 hours later. credentials.list hides that type. An expired ref is refused. - The redactor is a closure. Its walk covers Map and Set, and add() takes each string field of a secret answer with fields. - The duplicate check compares an input without the secrets of the session, as the log keeps it, so a retry with a secret in it is a duplicate. * fix(ai-mcp): ask for a sign-in when the user signs out while the SDK refreshes a token The SDK's new tokens are not saved when credentials.refresh finds no credential, so saveTokens throws AuthRequiredError instead of dropping them with no sign. * test(e2e): run the harness secret answer route against aimock The route uses the OpenAI adapter against aimock with a fixture, and the spec reads the model calls of each host from the aimock journal. * docs(ai-harness): narrow credentials on oauth, so the secret_answer kind type-checks * fix(ai-persistence): keep the harness of the owner or the first offer for a log * fix(ai-harness): check the harness in host.submit, and throw when a child cannot find its root * fix(ai-harness): prune settled state from the thread fold, and change its maps in place * fix(ai-harness): take only JSON values from step.do * fix(ai-harness): back off a failing log in the worker, free the slot of a long sleep, and need media on a shared host * test(ai-harness): skip a build-stub test that times out under load * chore(changeset): note the review fixes of the log session * fix(ai-harness): need media only on a host with ownership, and warn once on another host --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * fix(ai-harness): refuse an open while a thread purges, and a delete of a thread with no entry on a host with access * fix(ai-harness): throw on each call of a closed handle, and keep the quiet reopen only for a live handle * fix(ai-harness): share a host plugin setup only for the same plugin object * refactor(ai-harness): build the live and the observed snapshot with one builder * fix(ai-harness-cli): resolve at once on a second signal with your own host, also when the turn ignores the cancel * chore(changeset): note the review fixes of the session lifecycle --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * fix(ai-harness): run the rest of an answered turn as the user who sent it When the owner answers the approval of a member's turn, the resumed turn used the credentials and provider key of the owner. It now runs as the sender of the stopped turn, also after a restart. The log still keeps the answerer as the actor of the resolve. * fix(ai-harness): keep the keys of one user out of the shared provider-keys state The plugin state showed 'env' for a key that a host with access never reads, and /connect wrote the status of one user into the state that every member reads. The state now holds only the env vars that the session reads, and /keys checks the keys of the user who runs it. * fix(ai-harness): explicit access rules, one owner record, and fail-closed child and media checks - access takes { identify, authorize, listFilter?, members?, envKeys? }. membersOnly() returns authorize, listFilter, and members, so spread it into access. The host no longer sniffs a marker on authorize. - The owner is kept once: the member with the role owner. SessionIndexEntry has no ownerId. A share cannot change the role of the owner. - host.open of a child thread checks read on its root first, and a child whose root is missing is refused, so a stranger gets 403 and never the parent thread id. - A signed media URL proves only that the handler made it. The handler runs identify for it and checks read for that user in the tenant of the URL. The fabricated principal is gone. - A revoke of a user in GET host-events drops every kept read decision, so the child threads of the revoked thread stop too. A deleted child is decided through its parent. - createHarnessMcpServer uses requestUsers from the server entry, the same fail-closed rule as the HTTP handler and the socket. - Tests for the delete refusal over HTTP, a revoked handle that cancels its own run, and a member turn that the owner approves. * docs(ai-harness): explicit access rules, members-only ownership, and signed media that needs a user * feat(ai-harness): access.anonymous gives a guest for a signed media URL with no login The handler calls it only on the signed media route, after the signature checks out, and only when identify finds no user. authorize then decides read for the guest, in the tenant of the URL. Without it, the request gets 401. The guest reaches no other route. * refactor(ai-harness)!: nest the access rules in access.policy access is now { identify, policy, anonymous?, envKeys? }, and a policy is { authorize, listFilter?, members? }. membersOnly() returns a policy, so a host passes policy: membersOnly(). A custom policy is policy: { authorize }. No spread of the rules into access. Callers, tests, e2e routes, docs, and the changeset follow. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * fix(ai,ai-harness): optional ctx.harness, an error for a mixed log fold, and an allow-listed HarnessSession - Remove registerSubagentContextDefaults, its process-global Set, and the harness module side effect that filled it. ctx.harness is now optional on SubagentContextExtension. Every session path still passes it through binding.extend. ai-harness goes back to "sideEffects": false. - A harness with another durability.project or durability.reduce than the one that folds an open log now throws when it opens that log, in place of a silent first-opener-wins fold. - HarnessSession lists its public members (an allow-list), and SessionImpl implements it. takeOver, scrubError, loggedRun, and scheduleRun leave the public type. - The POST run route calls resolveForRun and sendForRun directly, and throws for a session of another package copy. - Fix the expose.commands JSDoc to client.command({ name }). --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * fix(ai-persistence)!: owner-only pins and snapshots, a delete tombstone, and a plain memoryLogState - setAffinity, putSnapshot, and truncate need an acquired log at the current epoch. UNOWNED_EPOCH calls reject with OwnershipLostError. Plain unowned appends still work. - deleteLog leaves a tombstone: every fenced write to the log fails, also with UNOWNED_EPOCH, until the next acquire. A late saveThread cannot bring a purged transcript back. An offer still works. - memoryLogState() replaces memoryLogBackend(). memoryLogStore({ state }) takes the plain shared state. The frozen handle, the WeakMap, and the TypeError guard are gone. - Conformance: acquire clears the wake time, renew after release fails, the twin must have a new log store instance, owner-only pins, snapshots, and truncates, and the delete tombstone. - The SQL recipe in store-reference.md matches the contract: acquire with expired, owner-only fences, the tombstone row, and the first-offer harness in listOrphaned. * fix(ai-persistence,ai-harness)!: offer to a deleted log returns 'deleted' - OfferResult gets 'deleted'. An offer to a log with a delete tombstone stores nothing and returns 'deleted', until a new acquire clears the tombstone. Contract JSDoc, memory store, a conformance case, and the SQL recipe (a deleted column on log_owner) follow it. - The harness surfaces it: host.submit and a session input that goes to the lane get a rejected receipt with the reason 'deleted', an awaited operation rejects with InputRejectedError ("The thread was deleted."), and a transfer record offer throws. * docs(ai-harness): bump updatedAt of the inputs page * test(ai-harness): check that no sign-in deadline starts, not the process-wide timer count, so loopbackLogin's onUrl test is not flaky in CI * refactor(ai-harness, ai-harness-cli, ai-mcp): remove what the harness redesign replaced, with the docs, skills, and changesets of the end state (#1829) * chore(ai-acp): pin @agentclientprotocol/sdk to ~1.8.0 * chore: keep the lockfile peer resolution unchanged for the ACP pin * refactor(ai-harness): drop the 'run' lifetime, type schema-less command input as unknown, use core metadata keys * refactor(ai-harness): subpath entry points for server, oauth, media, and Node plugins * refactor(ai-harness): HarnessSession as a type over a private class, whenIdle, and harnessOf * refactor(ai-harness): group host options per concern, and keep project and reduce on the harness * docs(ai-harness): one-object samples, subpath imports, grouped options, and the import guide * refactor(ai-harness): one-object parameters for the host, session, client, view, and helpers * docs(ai-harness): client and view samples in the one-object shapes * refactor(ai-harness): ctx.harness always present, and harness-first host.open and helpers * feat(ai-persistence): session index v2 keyed by tenant and thread, with owner, members, and required compare-and-set * feat(ai-harness): a RunContext for every run, so tools and plugins act as the user who started the run * feat(ai-harness): access types and the membersOnly() policy * feat(ai-harness): tenant-keyed index rows, log ids, and media keys * feat(ai-harness): host access with a principal-bound handle, sharing, revoke, and handlers that fail closed * docs(ai-harness): access and tenants, shared threads on membersOnly, and the run context * feat(ai-mcp, ai-acp, ai-harness-cli): the MCP server, the ACP agent, and the CLI act as a principal * fix(ai-harness): every thread-keyed store write carries the tenant * fix(ai-harness): delete removes the transcript, session plugins see the owner, host.hasAccess, and events wait for a new thread * test(ai-e2e): the protocol route on host access, and a shared-thread access spec * test(ai-harness): delete removes the transcript and closes the session * chore(ai-harness-cli): list @tanstack/ai-mcp as a dev dependency for the /mcp test * fix(ai-harness, ai-mcp): OAuth flows and commands stop on their signal * fix(ai-harness-cli): SIGINT and SIGTERM cancel the turn, then stop, in every mode * fix(ai-harness): one retention policy bounds every per-session collection, the feed, and the log tail * feat(ai-harness, ai-mcp, ai-acp, ai-harness-cli): reference-counted session handles with idle close and maxOpen * docs(ai-harness): lifecycle and retention, handles, observe, signals, and OAuth cancel * feat(ai-harness, ai-mcp, ai-acp): host.observe, a read-only session view, and read routes on it * test(ai-e2e): delete purges media and messages, and reads work after idle close * feat(ai-harness, ai-mcp): host-lifetime plugins, MCP stdio servers once per host, and delete that purges a thread * fix(ai-harness): definePlugin infers its definition only from the argument * feat(ai-harness): thread-state log records and their fold * fix(ai-harness): key host plugins by the harness definition, not its name * feat(ai-persistence, ai-harness): one persistence shape, every host has a log * test(ai-mcp): open two sessions of one harness definition to share a stdio server * test(ai-harness, ai-harness-cli): give polled checks 5 s, for slower CI runs * feat(ai-harness): settings, config, plugin state, interrupted turns, overrides, and agent results live in the log * feat(ai-harness): one ownership epoch per log, renewed once per host, with abort on loss * feat(ai-mcp): mcp() reconnects with backoff, lists tools again on list_changed, and takes needsApproval and toolName per server * fix(ai-harness, ai-sandbox): coding commands get an env allowlist, one workspace per thread, and real paths checked under the lock * feat(ai-harness): runs and interrupts live in the session log * fix(ai-acp, ai-code-mode, ai-harness): ACP keeps prompt order and releases idle sessions, code mode never moves approval tools, harnessText acts for the caller * wip: unfinished node work, saved before a shutdown (not reviewed) * fix(ai-harness): shared file locks with an unchanged check, and snapshots per thread and per run * fix(ai-harness, ai-sandbox, ai-sandbox-local-process): bounded background jobs with a ring buffer, a kill that waits for the spawn, workspace close, and an env allowlist in the local sandbox * perf(ai-skills): one folder watcher per host with a cache of the skills * feat(ai-harness-cli): cancel and steer act at once in a terminal, loadOptional reports real errors, and --dashboard goes away * feat(ai-persistence, ai-harness): inputs from any machine through the log input lane, host.submit, and no inbox, work claims, or leases * fix(ai-harness): a settled operation keeps its status, a media signer with key rotation, and agents() rule docs that match the code * perf(ai-harness, ai-sandbox): size caps for webfetch and read_file, range reads in the sandbox, one grep listing, and coding events for jobs, snapshots, and the formatter * fix(ai-harness): every plugin contribution has an owner, ctx.on cleans up, inline media goes to the media store, and capabilities report the socket only when mounted * perf(ai-harness): cache plugin lists and permission patterns, read instruction files async, parse SSE as a stream with one abort listener and capped backoff, and give each control input an id * docs(ai-harness): say when capabilities report the WebSocket * feat(ai-harness): createHarnessWorker, session.schedule, and step.sleep on the log * feat(ai): onGuardToolCall middleware hook that runs on the final arguments of every tool call * test(e2e): onGuardToolCall denies on the final args after a transformArgs * perf(ai-harness): stream media, cache base64 parts across turns, cap open-ended ranges, and add a byte-capped event ring * test(e2e): a worker on another host runs a submitted input and a schedule * feat(ai): CredentialStore compare-and-set pair, readCredentialEntry, and one scrubSecrets * feat(ai-harness): one log per session tree, session.logState(), and a wake when a busy session closes * perf(ai-harness, ai-harness-cli): the view and the printer only redo what changed, children load on demand with view.loadAgent, operation.stream reads from its start * feat(ai-harness): snapshots in the log store by size or time, retention logRecords, and the harness.gap event * fix(ai-harness): export LogOwnedElsewhereError, and say that the log keeps ephemeral messages with the turn * feat(ai-harness, ai): protocol v2 frames with error codes and request ids, observability types, and pull-based SSE and socket senders * docs(ai-harness, ai-persistence): durable sessions, one log per tree, workers, snapshots, and the changesets for the log as the session * ci: apply automated fixes * docs(ai-harness): first draft of the harness protocol page * feat(ai-harness): one redactor per session for log records, feed events, plugin state, notes, and the model context * fix(ai-harness): check answers and cancels from submit and schedule against their run, a schedule against its maker, and keep a same-host takeover and a lost log's inputs working * test(ai-harness): give polled checks 5 s, for slower CI coverage runs * test(e2e): read the data lines of POST run streams that now carry an id line per event * feat(ai-harness): the log keeps the final tool result the model saw, and errors and step results leave the session scrubbed * fix(ai-harness): tool turns take no steers, a turn keeps its config, removing a plugin removes all of it, body and frame limits, signing keys, and a warning on tool name clashes * feat(ai-harness): a secret answer sent to another host goes through the credential store as a one-time reference * fix(ai, ai-harness): each context default registration removes only itself, an empty media.secret fails at start, and the docs say the oauth loopback helpers need Node * fix(ai-harness): the media signer's key check covers an empty secret * test(e2e): a secret answer sent to another host never shows in the transcript, the events, the log, or the model input * feat(ai-harness, ai-mcp): OAuth refresh runs once per host, saves with compare-and-set to the scope it came from, and keeps the client registration * perf(ai-harness): ring buffer log tail, joins and rebuilt operations read from the run start, a per-tenant host event bus, no MESSAGES_SNAPSHOT in the log, and no idle store calls * perf(ai-harness): plugin prompts with placement 'context' go after the history, so the cached prompt prefix stays valid * docs(ai, ai-harness, ai-mcp): redaction, secret answers across hosts, the final tool result, OAuth refresh, and the CredentialStore compare-and-set pair * feat(ai-harness): the Wait type and waits in the log, with expiry, waitFor, settled({ inputId, signal }), and operator answers to human waits * feat(ai-harness): one wire encoding on every transport with hello, version, request ids, and heartbeats; hooks and status from other hosts; a gap event for evicted feeds * test(e2e): the access spec skips the hello frame of the session stream * feat(ai-harness): typed subject-first agent actions start, run, and group, with grouped options, for plugins and agent code * wip(ai-harness, ai-mcp): PR 7 boundary, coding-approvals, live-waits, background-waits (stopped mid-way, not checked) * wip(ai-harness): PR 9 transfer (stopped mid-way, not checked) * feat(ai-harness): host.transfer moves a conversation to another harness with an envelope, links both threads, and is safe to retry; fork stays in its harness * feat(ai-harness): a grouped stream option for the heartbeat, the slow-consumer close, and the SSE and socket buffer limits * feat(ai-harness): the active agent lives in the log and answers when the router picks nothing; routing.strategy 'handoff' becomes routing.after 'main' * feat(ai-harness, ai-mcp): waits at the boundary (permissions, the question tool, step.ask), live MCP and sign-in waits, and background runs that suspend and resume * feat(ai-harness): ctx.harness.handoff, a handoff tool, and one handoff path for code, tool, and router, with the target answering in the same turn * feat(ai-harness): hand a turn to a person through a human wait, operator replies with as: 'operator', and run.handoff for the main model's tools * feat(ai, ai-harness, ai-mcp, ai-acp, ai-harness-cli): agent runs pause on boundary approvals through SubagentBinding.interrupts, and every transport uses one wait shape * test(ai-mcp): the media result has a waits list * feat(ai-harness): handing a turn to a person is opt-in with routing.humans, and an operator reply shows live in the view * test(e2e): an approval asked on one server is answered on another, and an unanswered wait expires * test(e2e): handoffs to an agent and to a person, and a transfer between harnesses that is safe to retry * docs(ai-harness): hand off a conversation to an agent, to a person, or to another harness * docs(ai, ai-harness, ai-mcp, ai-acp, ai-harness-cli): wait for a person, step.ask, approvals at the boundary, background waits, and waits on every transport * fix(ai-harness): a session that does not own the log shows the waits and interrupts other hosts write * fix(ai-harness): lint errors in agent waits, step.ask, and permission resources * feat(ai-harness): the run tree in the log, with RunNode for turns, agents, tools, and commands, and agentRuns read from it * feat(ai-harness): agent limits and budgets that hold, with queued starts, per-run usage, timeouts, attempts, and a budget code * test(ai-harness, ai-code-mode): approvals are turn waits answered with resolve, and the device code carries expiresAt * test(e2e): in acceptEdits mode the write asks before it runs, with its formatter, and a reject writes nothing * feat(ai-harness): background runs inherit turn plugins, child signals follow their parent, tool-mode children get ctx.harness and share the turn budget, and ctx.harness.state shares state by key * feat(ai-harness): durable agent groups with a concurrency gate and keyed children, and routed agents that take steers, late joins, and turn hooks * docs(ai-harness, ai-sandbox, ai-sandbox-local-process): run the coding tools for many threads, with workspaces per thread, jobs, moving threads between hosts, and snapshots in the log * feat(ai-harness): ownership.hostId pins threads that use a local folder to their host, with E2E specs for env, roots, moving a sandbox thread, stranded, and pinning * docs(ai-harness): agents from code, groups, limits, budgets, the run tree, shared state, and routed agents * feat(ai-harness): harnessAgent runs as a child session in the root log, for the parent's user and keys, with usage, budget, waits, and events in the parent run * docs(ai-harness): the harnessAgent child run, as built * test(e2e): agent groups with a concurrency gate, a run budget, shared state, and a harnessAgent child run * test(ai-harness): give polled checks and a takeover deadline room for slower CI coverage runs * test(e2e): the handoff route reads view text by message role * feat(ai-harness): run tags on every event frame, ctx.model.chat for plugins, and usage by source * chore(ai-harness): allow the ps binary in knip, and give polled checks room for slower CI coverage runs * feat(ai-harness): lifecycle events, counted router and transfer model calls, tracing with span links, and onOperation reports * test(ai-harness): one threadId in the expected gap frame * docs(ai-harness): the harness protocol page, keeping the stream open, stopping a turn from useChat, and the changesets for reporting and observability * docs(ai-harness): run tags and the gap reload in the custom UI page * test(e2e): the wire, the gap, the run tree in one stream, lifecycle events, tracing, remote status, cancel from another host, and an unsupported version * chore(changeset): the integration changes for mcp, acp, code mode, skills, and harnessText * test(e2e): the first-party fixtures match the context prompt that now comes after the user message * fix(ai-harness-cli): serve signs media URLs with a key derived from the token, so a short token meets the 32-byte key rule * fix(ai-harness, ai-mcp, ai-acp, ai-code-mode): final review fixes for durability, access, plugins, transports, integrations, and coding tools * test(ai-harness): give polled checks room for slower CI coverage runs * fix(ai-acp): one agent app serves one connection, with editor capabilities read from that connection, and no private SDK field * fix(ai-harness): a thread delete ends it in first-party plugins, the bash spill is scrubbed, removed schedules go at once, agent run receipts resolve, and session.append takes one object with an optional inputId * refactor(ai-harness, ai-mcp, ai-harness-cli): session, client, and view methods take one object, the client and view use putMedia and client.sessions, and internal fields leave the public types * docs(ai-harness, ai-mcp, ai-acp, ai-code-mode): the final review changes, the fixed durable-tools and server-tools text, and a known limits section * refactor(ai-harness, ai-harness-cli, ai-mcp): remove what the redesign replaced - The purge no longer deletes the old usage and revert metadata rows. The log keeps both, and deleteLog removes them. - The snapshot parser drops its branches for older snapshot shapes. A snapshot with a missing field is not used, so the fold starts again. InputState.seq and LogState.transcriptSeq are required. - A root turn tracks its running tools only with harness.tool.started. It no longer also writes pendingTools to the run checkpoint. - SessionSnapshot.pendingWaits is required, so the "?? []" fallbacks go. - HarnessSession hides scrubError, takeOver, scheduleRun, and loggedRun. - Remove the unused traceparentOf and formatOnWrite, the --dashboard test, and old lease and metadata wording. leases.test.ts is now recovery.test.ts. - Docs, skills, and changesets describe the end state: snapshots in the log, not a metadata checkpoint. Changesets that told the history of unreleased code are removed or folded into the main ones. * docs(ai-harness): read pendingWaits with no fallback in the code samples * test(ai-harness): name projectFormatters in its describe, and drop old lease and run store wording * test(ai-harness): a snapshot with usage totals that have no bySource is not used, and skip a load-flaky revert test * chore(changeset): fold the LogStore heartbeat changeset into the session stores one, and drop history wording * test(ai-harness): run the tests that were skipped as flaky again, now that the CI-flaky oauth test is fixed --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * test(ai-harness): a second host's plugin state does not stop the owner's turn (from #1810) * docs(ai-harness): match the code on who answers a human wait, send to another host's run, and stores.log * feat(ai-harness): client.sessions.share and unshare, and export the HarnessErrorCode type client.sessions gets share({ threadId, userId, role }) and unshare({ threadId, userId }). Both use the POST sessions ops that the handler already has, and resolve to the changed entry. A missing thread throws, as rename does. HarnessErrorCode, the type of HarnessProtocolError.code, comes from the main, /client, and /server entries. * feat(ai-harness): add a handoff note to the transcript for router and code handoffs The target of a router or code handoff now gets a handoff tool call and its result in the transcript, before it answers. With routing.handoffs false, it gets one assistant note. The ids come from the turn input, so a run of the turn on the next host keeps each note once. ai-persistence: a routed answer that waits for an approval keeps its own message on resume when a finished tool call comes before it. * feat(ai-harness): session.jobs() lists the background jobs that did not end A harness.job start now has at and label (the command or the tool name). The fold keeps the backend id, who moved the call, when it started, and the label, and it still folds the old records and snapshots. session.jobs() and jobs() of a host.observe view read the fold of the thread and its child threads. JobInfo.running is true for a job that this host runs. * feat(ai-harness): accept onModelError as data that the log keeps An agent start takes onModelError: { retries: 1 | 2, transientOnly? }. The run node keeps it, so a host that takes the run over or resumes it retries too. The function form stays, live only, with its warning. * feat(ai-harness, ai, ai-persistence): an agent tool waits with the turn An agent that the main model calls as a tool can now wait. Its approvals, questions, sign-ins, app interrupts, client tools, and step.ask questions are waits of the turn. After the resolve, on any host, the agent goes on from where it stopped. - The turn keeps the agent cards of a turn with agents, as a client does, and keeps only the messages of the turn in the record. - PendingWait gets agent and subagentRunId. - ctx.harness.step of an agent tool is durable. step.ask stops the agent, and the answer is a step in the log. - Core: an agent run that throws an input request stops as suspended. - Persistence: the parent run does not check the answers of a child. - durability.agentToolWaits: false keeps the old refusal. * feat(ai-harness): every host and host.observe see the waits of a harnessAgent child The child writes its waits to its own thread in the log of the root. The snapshot of a root session and of a host.observe view now reads them from the fold of each child thread whose run did not end. The live children of the owner win over the log, so the owner lists each wait once. An answer on another host is offered to the owner, which gives it to the child. * feat(ai-harness): resume background agents of the model after a host stop A child that the model starts with background: true now starts as a run with resume: true. The next host continues it with the same subagentRunId, so the id that the model got stays valid. durability.resumeBackgroundAgents: false fails it as before. * docs(ai-harness): an agent tool waits with the turn - subagents: a new section, "When an agent tool waits", with step.ask in agent code and the known limits. - permissions, waits, durable-tools: an agent tool asks and waits as the turn does. - test: an agent that a router picks waits too. * feat(ai-harness): start a queued top-level agent start on the next host Recovery no longer refuses a top-level agent start that waited for a maxConcurrent slot when its host stopped. The next host starts it with the run id of its queued node, so the receipt stays valid, and it waits for a slot again in log order. A start whose cancel reached the log settles aborted. * fix… * fix(ai-harness-cli): give the ui view every session method attachingSource() forwarded 12 of the 21 SessionViewSource members, so in a custom ui view loadAgent() did nothing, putMedia() rejected, and updateThreadSettings, reset, revert, unrevert, continueWithoutMessage, cancelInput, and changeInputDelivery showed an error notice. Forward every member. Required<SessionViewSource> makes a missing member a type error. * fix(ai-harness): read session view approvals in permissions() A session view answers an approval with the core resolution { approved, payload?, editedArgs? }. permissions() knew only true/false, a string, or { answer, message }, so the schema refused the resume and the turn failed, and readApproval() read a yes as a reject and dropped the reject message. The approval schema and readApproval() now accept the view shape: a yes is the answer its payload names (or once), a no rejects with the payload as the message. A yes with editedArgs rejects: this approval cannot apply edited arguments, and the model's original call is not what the user approved. * test(e2e): answer a harness approval with the session view shape The approval scenario of harness-waits also runs with the answer that a session view sends, { approved: true, payload: 'always' }, and checks that deploy runs. * feat(ai): pass a debug option to the model calls of child agents SubagentBinding gets debug?: DebugOption. The bound activities of a child use it when a call sets no debug of its own, and nested children inherit it, so a host can send the logs of child agents to its own logger. * feat(ai-harness): send harness warnings and errors to a host logger createHarnessHost({ logger }) takes a Logger from @tanstack/ai (default ConsoleLogger). Every console.warn and console.error of the harness goes through it, plugins get it as ctx.logger, and the model calls of a session send their errors to it. A logger that throws or rejects cannot break the harness. * feat(ai-harness): PermissionCallCapability for tool calls that a plugin runs permissions() provides PermissionCallCapability: the same decision as for a direct tool call (allow, deny, or ask with the approval question and schema), and settle() reads the answer and saves an 'always' rule. The guard and the turn boundary share this decision. * feat(ai-harness): child agents log through the host logger The subagent binding of a session gets the errors-only debug option with the host logger, so a failed model call of a child agent goes to the host logger and not to the console. * feat(ai-harness): FileChanged events with a unified diff from the coding tools write_file, edit_file, and patch send a FileChanged plugin event after the write hooks: the path, the kind of change, a git-style unified patch, and the counts. A new unifiedDiff() in utils.ts makes the patch (Myers with a cap, no dependency). Binary files, files over 1 MiB, patches over 256 KiB, .env files, and files that the permission rules protect get no patch. * feat(ai-sandbox-docker): copy a localSource folder into the container dockerSandbox().create() with a localSource(dir) workspace now copies the folder into the work dir: the files that git lists (tracked, and untracked that .gitignore keeps, with their uncommitted changes) in one tar stream through putArchive, and the current branch as a git bundle, so git status in the container matches the host. A git worktree works. The folder must be a full (not shallow) git repository, and the image needs git. A failed copy removes the container. Fork, restore, and resume do not copy. * feat(ai-code-mode): code mode runs the coding tools under the permission rules codeMode({ include }) can now move a tool that has permission checks, like the harness coding tools. Each call inside a program gets the same decision as a direct call through PermissionCallCapability: allow runs, deny throws in the program, ask waits for the user (a step.ask question with the approval schema) and applies the answer. Each checked call is a step.do, so a host stop while a program waits does not run finished calls again. The program timeout defaults to 10 minutes when a checked tool moves. The new directCalls option ('remove' | 'keep', also a session setting) keeps a moved tool in the model's tool list too. * feat(ai-computer-use, ai-harness): computerUse() harness plugin in @tanstack/ai-computer-use/harness (#1844) * feat(ai-computer-use, ai-harness): computerUse() harness plugin Screenshots go to the media store of the session. The model gets only the newest screenshots. OpenAI safety checks ask through permissions(). Plugins can store media with ctx.session.putMedia. * docs(harness): give your agent a computer * feat(ai-computer-use): plan mode refuses the computer tool * feat(ai-mcp, ai-code-mode): plugin warnings go to the host logger mcp() warns about duplicate tool names, and codeMode() about tools that include cannot move, through ctx.logger instead of console.warn. * docs(harness, sandbox): copy a local folder into a Docker sandbox A new subsection in Run the tools in a sandbox shows localSource() with dockerSandbox(), and the Docker provider docs describe the copy. * test(e2e): a code mode program edits a file after an approval A code-mode-edit scenario: a scripted isolate program reads notes.txt with no question, then its edit_file call waits for the user's approval, and runs after the answer once. * feat(ai-harness): snapshots() sends FileChanged for the files of bash At the end of each model step, snapshots() sends FileChanged for the changed files that the workspace tools did not report, like the files that bash wrote: one git diff for the step, split per file, with the same caps, and at most 100 events for each step. * test(e2e): FileChanged for a write_file edit and a bash write A coding-diffs scenario collects the FileChanged events of a turn: write_file updates a.txt (patch -one +two), and snapshots() reports b.txt that bash created (+hi). * docs(harness): show live diffs, and answer permissions() approvals from a view custom-ui gets a Show live diffs section with view.on(FileChanged), and the approve and reject options for a permissions() approval. snapshots and coding-workspaces describe the FileChanged events of snapshots(). The FileChange comment names which limits the tools and snapshots() apply. * docs(harness): run the coding tools in a code mode program code-mode gets Run the coding tools in a program (setup, allow/deny/ask, the approval question, time limits, host stops) and Keep direct calls (directCalls). permissions documents PermissionCallCapability, and coding-tools and waits name code mode where a call can ask while it runs. * fix(ai-code-mode): write the aroundCalls hook type inline The build of @tanstack/ai-code-mode-snippets failed with TS2883: the inferred type of createCodeModeWithSnippetsConfig named CodeModeCallHook, which the package index does not export. The aroundCalls option now writes the hook type out, and CodeModeCallHook is derived from it. * feat(ai-harness, ai-skills): host plugins get the host logger HostPluginContext gets logger, the host logger, so a lifetime 'host' plugin can warn without a session. skills() sends its shared-folder warnings there instead of console.warn. * docs(harness): send the harness warnings to your own logger host-options gets Send the warnings to your own logger (a file logger for a terminal UI, pino for a server), plugins gets Write warnings to the host logger, and mcp names the host logger for the duplicate-tool warning. The logger example in host.ts sets pino's errorKey. docs/config.json dates the pages that this work changed. * fix: one @google/genai for ai-vertex and ai-gemini in the lockfile After the merge of main, ai-vertex resolved @google/genai with the zod@4.6.5 peer and ai-gemini with zod@4.3.6, so pnpm made two copies. The vi.mock of @google/genai in the ai-vertex tests then missed the copy that ai-gemini uses, and 2 factory tests failed. ai-vertex now uses the zod@4.3.6 variant, as on main; the two unused variant entries are gone. pnpm install --frozen-lockfile accepts the lockfile. * fix(ai-harness): link a permissions() approval in the view to its tool call A permissions() approval is a generic interrupt: its tool call id is only in its payload. The view looked the call up by interrupt.toolCallId, found none, and showed the question text as the tool, with no args. The view now takes the tool call id from the payload, so the approval has the tool name, the args, and the toolCallId of its call. * refactor(ai-harness, ai-client): drop fallbacks for the now-required threadId and runId threadId is required on TextOptions and runContext ids are required in 1.0. harnessText, artifactText, and the tool-turn adapter read threadId as is. The connect wrapper and buildRunAgentInputBody stop guarding runContext ids, so requireSyntheticId, generateRunId, and the catch around the synthetic RUN_ERROR go. * chore: release ai-harness and ai-harness-cli as 1.0.0 with the rest * fix(ai-harness): view.ready settles when the session closes during its first load A closed session handle throws at once from describe(), snapshot(), and transcript(). The first load called them inside Promise.resolve(...), so the throw escaped the .catch, and view.ready rejected with no handler. promiseOf() turns the throw into a rejection: the view shows the error as a notice, and ready resolves, as for any other failed read. * fix(ai-sandbox-docker): resume gives null only for a missing container resume() turned every inspect error into null, so a stopped Docker looked like a deleted container, and the harness said the workspace is gone. Now only a 404 is null. Any other error goes to the caller. * fix(ai-harness): undo, redo, and revert send FileChanged for the files they put back snapshots() restored files with git for /undo, /redo, revert, and unrevert, and sent no FileChanged. So a live diff, or a sandbox that mirrors the files to the user's folder, never saw the restore. Each restore now takes a snapshot after it, and sends one FileChanged per changed file, built by the same code as the end-of-step events (sendChanges). /undo and /redo take a snapshot before the restore too, so a user edit to another file sends no event. .env files stay out. The docs, the changeset, and the coding-tools E2E route and spec cover it. * fix(ai-harness): keep the user message of a turn that stops before its first model call A turn counts as started when its applied record is in the log, but the first chat() call saves the user message. A recoverable close or a crash in between left no message in the transcript, and recovery ran the turn again with sentMessage only, so the model got messages: []. A turn that had added a note was also settled as completed, because its note looked like an answer. The user message now has a fixed id, inputMessageId(inputId), also for a held message. Recovery checks if the transcript has the message. If not, the turn adds it, once. A note no longer counts as an answer before the message is saved. * test(ai-harness): give the inner-failure harnessText test a threadId threadId is required since 9d999ebdc, and the as never cast hid that the test did not pass one. host.open refused the thread "undefined/inner/..." before the turn ran. * fix(ai-harness): a forwarded command ends with the owner's result or error (#1872) * fix(ai-harness): a forwarded command ends with the owner's result or error A command that reaches a host that does not own the thread now waits for the owner to settle it. It resolves with the owner's result, fails when the command throws, and rejects with InputRejectedError when the owner refuses it (unknown_command, invalid_input). Before, it completed with undefined. Closes #1796 * fix(ai): resume a nested subagent that waits, under a child loader A child resumes from its stored pending messages. The store keeps a nested child in its own thread, so those messages had no card for it, and the nested child ran again from the start. Keep the nested cards from the client's copy on the stored message of their parent tool call. Also update the harnessText test for the required threadId: one session per thread. * test: drop the nested-card stopgap, skip its test until #1878, add a twin to the chat-only conformance The stopgap in spawn.ts copied nested subagent cards from the client, which trusts the client's messages for a nested child. #1878 fixes the nested resume from the store. Until then, skip the subagent-waits test that needs it. The chat-only conformance test from main had no twin. On this branch memoryPersistence has a log store, so the log-across-instances cases need one. * fix(ai-persistence): check a durable resume after the whole onConfig chain A tool that a middleware adds in onConfig can ask for approval, but the resolve failed with "Tool <name> is unavailable for interrupt". withPersistence checked the resume in its own onConfig, before later middleware added tools. Now chat() runs the check after the onConfig chain, against the final tools. * fix(ai-harness): run the harness middleware on ctx.model.chat calls A plugin model call (title(), a judge, the router) ran only the usage counter, so app middleware for cost, traces or limits never saw it. The call now runs the harness middleware too, with the run in ctx.context.run. * fix(ai-compaction): add a middleware option to conversationSummarizer The summary call ran only its own usage count, so app middleware for cost or traces never saw it. The new middleware option runs after that count. * fix(ai-compaction): drop a background summary that another model prepared A summary prepared on a model A turn was applied on a model B turn. The ready summary now keeps its model, and a call on another model drops it as stale. * fix(ai-harness): run the continuation of a recovered resolve A host that stopped in the model call after a resolve left its interrupts used up. The host that recovered the thread then settled the resolve completed, but it never called the model, so the turn had no answer. Now a used-up resume with no final answer runs again like a stopped chat turn: the transcript is repaired, a cut answer can continue, and the model gets the tool result. * fix(ai-harness): keep the text that a cancelled turn streamed A cancel cut the answer, and the transcript kept none of it. The next model call then got two user messages in a row. Now the text that the model streamed before the cancel goes in the transcript as an assistant message. An answer that is saved already is not added again. * fix(ai-harness): end the text message and the run of a cancelled turn A cancelled turn sent RUN_STARTED and TEXT_MESSAGE_START, but no TEXT_MESSAGE_END and no run end. A client saw an open message and an open run until harness.operation.finished. Now a stopped turn ends its open text message. A cancel then sends RUN_FINISHED with outcome cancelled, as an agent run does. A turn past its time limit sends RUN_ERROR with code timeout. * fix(ai-harness): show idle in host.events() after a turn that a steer joined A steer that joined a turn ends a tick after that turn. The host read the snapshot at harness.operation.finished, saw the steer as still active, and kept the status running. Now the snapshot leaves out a joined steer whose turn ended. * fix(ai-anthropic): move outputSchema keywords that Anthropic rejects into the description * fix(ai-anthropic): report the 1-hour part of a cache write as cacheWrite1hTokens * fix(ai-anthropic): warn once when chat({ reasoning }) is ignored for a model missing from the reasoning table * fix(ai-anthropic): also move an outputSchema not into the description * fix(openai-base): drop msg_ and fc_ item ids on replay of a message with no reasoning item * fix(ai-cloudflare): keep the reply when the usage trailer has no choices in any SSE form * fix(ai): put the model text on a schema failure, and give a non-JSON answer the parse-failed code * fix(ai-client): send addToolResult through the generic interrupt when the tool schemas differ * fix(ai-persistence): check "not due yet" on a live log so slow stores pass the log ownership cases * feat(ai-sandbox): let each sandbox file call take an abort signal SandboxFs methods take an optional last argument { signal }. bootstrapWorkspace gives its signal to its file calls. E2B and local process give the signal to the native call. The other providers check it before the call. * feat(ai-harness): add a beforeWrite workspace hook that can refuse a write write_file, edit_file and patch run the beforeWrite hooks before they change a file. A hook that returns { refuse } fails the call, and no file changes. A patch asks for each file before its first write. * fix(ai-harness): the duplicate-input check ignores the key order of the stored input * fix(ai-harness): a steer that can't join another sender's turn gets a queued receipt for its own operation * feat(ai-harness): harness.input.accepted carries the input's user message * feat(ai-harness): TurnInfo names the model and the attempt, and a snapshot has the saved state of a turn plugin * feat(ai-harness): TurnInfo.sender names who sent the input, also the user who resolved * fix(ai-harness): the log keeps the name of a sender, not only its id and tenant * fix(ai-harness): the run hydration carries the pending waits, so a reload during a step.ask question keeps it * feat(ai-harness): a host.observe view reads how an input ended with settled() * docs(ai-harness): idle in a snapshot means no turn runs on this host, not that the thread is free * feat(ai-harness): export the abort reasons SHUTDOWN_REASON, RUN_CANCEL_REASON, and BUDGET_REASON * feat(ai-harness, ai-client): export HARNESS_EVENTS from ./client, and the chat hydrate types from ai-client * feat(ai-harness): subagents.background false keeps the background field off the agent tools * feat(ai-harness): media.urlTtlMs sets how long a signed media URL works * chore: changeset for the fifth findings list fixes * fix(ai, ai-persistence): resume a waiting nested subagent from the store (#1879) * fix(ai, ai-persistence): resume a waiting nested subagent from the store A nested child that waits on an interrupt ran again from its prompt under withPersistence. The store keeps a nested child in its own transcript, so the stored pending of the outer child had no card for it. Core now rebuilds each nested card from the store and takes only the id from the client card. LoadChild takes an optional parentSubagentRunId, and returns interruptIds and parentToolCallId. Closes #1878 * test(ai-harness): run the nested wait test again (#1878) * feat(ai-harness): onModelError can retry the turn on another adapter * feat(ai-harness): middleware reads who made a model call from ctx.context.run.source * feat(ai-harness): a plugin model pick with override: true wins over the stored model * feat(ai-harness): acceptInput checks, changes, or refuses each input before the log keeps it * feat(ai): let a server tool read run capabilities ToolExecutionContext gets optional get and getOptional, the same accessors as the middleware context. chat() fills them from the run's capabilities, so a server tool can call ctx.get(SandboxCapability) under withSandbox. Harness turns keep them on the tool context. * feat(ai-sandbox): pass the tool call signal to the workspace file calls WorkspaceBackend file methods (readFile, writeFile, stat, realpath, readdir, remove, readLines) take an optional { signal }. The workspace tools give each file call the abort signal of the tool call. sandboxWorkspaceBackend passes it to the sandbox file system and to the commands of the call. hostBackend uses it for readFile, writeFile and readLines. * feat(ai-compaction): allow maxTokens, contextWindow and atTokens as functions of the model Each limit takes a number or a function ({ model }) => number. The function runs once for each model call, and the checkpoint key holds the value for that call. A plain number works as before. The check that atTokens is below maxTokens runs at creation only when both are numbers. * test(ai-harness): readers keep the full transcript after a compaction * fix(ai-harness): keep the full transcript for readers after a compaction The session log fold now keeps two lists: the model context, which addRecordsToModelContext changes, and the transcript that readers see. A record that adds messages at the end (a signal) goes to both lists. A record that rewrites earlier messages (a compaction) changes only the model context. - session.transcript(), host.observe, GET transcript, the chat hydration, agentTranscript and logMessageStore read the transcript. - replaceTranscript, revert and logMessageStore saves write in transcript positions. - appliedAt, turnText and recovery use transcript positions, so a compaction in a turn does not move them. - Snapshots keep the transcript when it differs (version 6). Version 5 snapshots still load. * feat(ai-persistence): archive a thread and hide it from lists Add archived to SessionIndexEntry and withArchived to the list options. Lists leave archived entries out by default. host.sessions.archive and unarchive set and clear the flag with the configure action, and the handler and client.sessions expose both. A delete still finds archived child threads. * feat(ai): check a streamed outputSchema value against the schema chat({ outputSchema, stream: true }) now runs the same Standard Schema check as the Promise path. A value that fails it is not sent as structured-output.complete. The stream ends with RUN_ERROR structured-output-validation-failed, and the model text is on metadata.tanstack.rawText (parse failures get it too). A plain JSON Schema is still not checked. * feat(ai-harness): outputSchema on session.send session.send(message, { outputSchema }) passes the schema to the turn's chat() call. The result gets a typed structured value. A failed check fails the turn and settles the input with the chat() code and rawText. The schema is per send and in memory only: HTTP does not take it, a joined steer ignores it, and a recovered turn runs without it and warns. * feat(ai-harness): erase the messages of one user from a thread host.sessions.eraseUser replaces each message of a user with a [removed] placeholder that keeps its id and role, and with replies: 'remove' also the answers to it. It logs a harness.erase record, writes a snapshot at the head, and truncates the records before it, so no new LogStore method is needed. It deletes the media of those messages, needs the new erase access action, and refuses while the thread is busy. Joined steers now keep the id of their input, so the erase finds their sender. * feat(ai-harness): export HarnessRecord and document an encrypting LogStore Export the union of the records that the harness logs, and show a LogStore wrapper that encrypts the records and the snapshot, with the rule that it must give back on read exactly what it got on write. * docs(ai-harness): say that an erase drops a compaction summary * chore: changeset for the fifth findings list asks * Revert "fix(openai-base): drop msg_ and fc_ item ids on replay of a message with no reasoning item" This reverts commit addd7b9a6f61872fcd30b24c2032c44311b07d88. * fix(openai-base): drop replay item ids only when the message lost its reasoning item The first B13 fix dropped msg_ and fc_ ids from every message with no reasoning item. That broke a same-model replay of an answer whose model call sent no reasoning (#1726). The Responses adapter now marks a message whose model call sent a reasoning item (metadata.tanstack.reasoningItem), and a replay drops the ids only when that mark is set and the reasoning item is missing. * chore: correct the B13 changeset line * docs(ai-harness): drop duplicate imports in shared snippet groups * fix: repair the build and tests after the model sync The sync added eleven_text_to_sound_v3, which the ElevenLabs SDK types do not list, and removed models that tests and docs used. * chore: release ai-harness and ai-harness-cli as 0.1.0 * docs(ai-harness): mark the harness as experimental; fix the e2e model id after the sync --------- Co-authored-by: Jack Herrington <jherr@pobox.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Patrick L <patrick.michel@minubo.com> Co-authored-by: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Co-authored-by: Chris Trześniewski <k.trzesniewski@gmail.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
nx affected --targets=test:sherif,test:knip,test:docs,test:kiira,test:maintainer,test:ai-review,test:oxlint,test:lib,test:types,test:build,build
⌘K
feat(ai-harness): Tau (#1755)
Succeeded
in
3m 8s
Started
Linux
4 CPU cores
read
access token used
Copy command
nx affected --targets=test:sherif,test:knip,test:docs,test:kiira,test:maintainer,test:ai-review,test:oxlint,test:lib,test:types,test:build,build
1793
e4ce1b1
GitHub
Open on GitHub
Tasks
Resource Usage
ta
TanStack
OSS