Repository navigation
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
Conversation
…t_changed, and takes needsApproval and toolName per server
…eases idle sessions, code mode never moves approval tools, harnessText acts for the caller
…dOptional reports real errors, and --dashboard goes away
# Conflicts: # .changeset/ai-harness-cli.md # packages/ai-harness-cli/src/lines.ts # packages/ai-harness-cli/tests/cli.test.ts # packages/ai-harness/src/harness-text.ts # packages/ai-mcp/src/harness-plugin.ts
…ills, and harnessText
🦋 Changeset detectedLatest commit: ccda7e3 The changes in this PR will be included in the next version bump. This PR includes changesets to release 69 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
View your CI Pipeline Execution ↗ for commit 7ab3b63
☁️ Nx Cloud last updated this comment at |
@tanstack/ai
@tanstack/ai-acp
@tanstack/ai-angular
@tanstack/ai-anthropic
@tanstack/ai-bedrock
@tanstack/ai-byteplus
@tanstack/ai-claude-code
@tanstack/ai-client
@tanstack/ai-cloudflare
@tanstack/ai-code-mode
@tanstack/ai-code-mode-snippets
@tanstack/ai-codex
@tanstack/ai-cohere
@tanstack/ai-compaction
@tanstack/ai-devtools-core
@tanstack/ai-durable-stream
@tanstack/ai-elevenlabs
@tanstack/ai-event-client
@tanstack/ai-fal
@tanstack/ai-gemini
@tanstack/ai-grok
@tanstack/ai-grok-build
@tanstack/ai-groq
@tanstack/ai-harness
@tanstack/ai-harness-cli
@tanstack/ai-isolate-cloudflare
@tanstack/ai-isolate-daytona
@tanstack/ai-isolate-e2b
@tanstack/ai-isolate-node
@tanstack/ai-isolate-quickjs
@tanstack/ai-isolate-quickjs-bun
@tanstack/ai-llmgateway
@tanstack/ai-lovable
@tanstack/ai-mcp
@tanstack/ai-memory
@tanstack/ai-mistral
@tanstack/ai-octane
@tanstack/ai-ollama
@tanstack/ai-ollaya
@tanstack/ai-openai
@tanstack/ai-opencode
@tanstack/ai-openrouter
@tanstack/ai-perplexity
@tanstack/ai-persistence
@tanstack/ai-preact
@tanstack/ai-react
@tanstack/ai-react-ui
@tanstack/ai-reactor
@tanstack/ai-remix
@tanstack/ai-sandbox
@tanstack/ai-sandbox-blaxel
@tanstack/ai-sandbox-boxd
@tanstack/ai-sandbox-cloudflare
@tanstack/ai-sandbox-daytona
@tanstack/ai-sandbox-docker
@tanstack/ai-sandbox-e2b
@tanstack/ai-sandbox-local-process
@tanstack/ai-sandbox-railway
@tanstack/ai-sandbox-sprites
@tanstack/ai-sandbox-upstash-box
@tanstack/ai-sandbox-vercel
@tanstack/ai-skills
@tanstack/ai-solid
@tanstack/ai-solid-ui
@tanstack/ai-svelte
@tanstack/ai-typesafe
@tanstack/ai-utils
@tanstack/ai-vercel-gateway
@tanstack/ai-vertex
@tanstack/ai-vue
@tanstack/ai-vue-ui
@tanstack/ai-worldlabs
@tanstack/openai-base
@tanstack/preact-ai-devtools
@tanstack/react-ai-devtools
@tanstack/solid-ai-devtools
@tanstack/svelte-ai-devtools
commit: |
Coverage✅ Coverage held across 30 compared package(s). Each package is measured twice in this job — on this PR and on its merge-base with
|
…lient, 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
…ash loops, and onDuplicateTool 'error'
…rror code, not the message text
…uter thread, and pass the run principal on as is
…d before the outer check applies
… no session index
… sync, with no skill cache
…/<harness>, on every host
…eams, 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.
…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.
… 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.
The integrations now run on the new harness primitives. MCP servers reconnect and refresh their tools, ACP keeps prompt order, code mode never hides an approval, skills share one watcher per host, the CLI cancels at once, and
harnessTextacts for the caller. This is PR 12 of the harness redesign stack for #1755. It targets #1821 (feat/harness-reporting). PRs 12, 13, and 14 are chained (13 on 12, 14 on 13), so each one merges cleanly in order.🎯 Changes
@tanstack/ai-mcp: themcp()plugin.list_changed.needsApprovalandtoolName. When two servers give a tool with the same name,mcp()warns once.@tanstack/ai-acp. A prompt takes its place in the queue before its media is saved, so prompts keep their order. An idle session gives its place back.@tanstack/ai-code-mode.includenever moves a tool into code mode when it needs approval or when the rules deny it or ask about it.@tanstack/ai-skills. One folder watcher per host, with a cache that a change clears, and a fallback message for a skill that does not load.@tanstack/ai-harness-cli./canceland a message typed while a turn runs act at once. Line mode no longer waits for idle.loadOptionalreports real load errors.--dashboardis removed. Waits use exit code 3 (from PR 7).harnessText.RUN_FINISHED, and its SSE reader takes multi-linedata:and CRLF.harnessAgent.The wait mapping that the plan put here for ACP, the MCP server, and the CLI was already built in PR 7 (#1817), so this PR does not repeat it.
Docs:
mcp,acp,code-mode,skills,cli, andconnectunderdocs/harness/. The changesets for@tanstack/ai-mcp,@tanstack/ai-acp,@tanstack/ai-code-mode,@tanstack/ai-skills,@tanstack/ai-harness-cli, and@tanstack/ai-harnessare edited in place.✅ Checklist
pnpm run test:pr, or these tests do not apply to this pull request.docs/for this change, or this change is not user-facing.pnpm changeset), or this PR does not change a published package.🚀 Release Impact
Testing
Commands run:
test:typesofai,ai-harness,ai-mcp,ai-acp,ai-harness-cli,ai-code-mode,ai-skills,ai-sandbox, and the E2E app. All pass.vitest relatedon the changed files, with--maxWorkers=2. Inai-harness: 127 files and 1,568 tests. In the other packages:ai-mcp17 files,ai-acp4,ai-code-mode1,ai-skills1,ai-harness-cli7. All pass.test:oxlintofai-harness,test:knip,pnpm test:docs, andoxfmt --checkon every changed file.Not run locally: E2E and coverage. CI runs them.
Manual test:
pnpm --dir packages/ai-mcp exec vitest run tests/harness-reconnect.test.ts tests/harness-plugin.test.ts.pnpm --dir packages/ai-harness-cli exec vitest run tests/cli.test.ts, and read "line mode in a terminal": a/canceltyped during a turn stops it at once.How this PR makes testing easy: the new and updated tests in each package pin each change:
harness-reconnectfor MCP, the ACP agent tests, the code-mode harness tests, the skills watcher tests, and the CLI line-mode tests.Risk / rollback
Low to medium. Each change is in one integration package. The CLI's
--dashboardis gone, and the harness and CLI are not released yet. To undo, revert this PR.Public API change
Before
After
Review fixes
These fixes come from a stack review. #1823 to #1825 are already squash-merged into this branch. This PR is squash-merged into #1821 next.
host.close(). The backoff resets only after 30 s of a stable connection. New optionmcp({ onDuplicateTool: 'warn' | 'error' }), default'warn'.harnessText: the inner thread is<outer>/inner/<harness>on every host. Access is checked through the outer thread only when the inner entry links to that outer thread. App code cannot open/inner/ids. The run principal is passed on as it is.Checks: the unit tests for the changed files, plus
test:typesandtest:oxlintfor ai-mcp, ai-harness-cli, ai-harness, ai-acp and ai-skills. CI runs the rest.