Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
183 commits
Select commit Hold shift + click to select a range
50299e0
chore(ai-acp): pin @agentclientprotocol/sdk to ~1.8.0
AlemTuzlak Oct 16, 2026
f617668
chore: keep the lockfile peer resolution unchanged for the ACP pin
AlemTuzlak Oct 16, 2026
27c7da0
refactor(ai-harness): drop the 'run' lifetime, type schema-less comma…
AlemTuzlak Oct 16, 2026
d03a973
refactor(ai-harness): subpath entry points for server, oauth, media, …
AlemTuzlak Oct 16, 2026
bb9768a
refactor(ai-harness): HarnessSession as a type over a private class, …
AlemTuzlak Oct 16, 2026
4f52e54
refactor(ai-harness): group host options per concern, and keep projec…
AlemTuzlak Oct 16, 2026
8d7f179
docs(ai-harness): one-object samples, subpath imports, grouped option…
AlemTuzlak Oct 16, 2026
ca411d0
refactor(ai-harness): one-object parameters for the host, session, cl…
AlemTuzlak Oct 16, 2026
99f991f
docs(ai-harness): client and view samples in the one-object shapes
AlemTuzlak Oct 16, 2026
12a3fe2
refactor(ai-harness): ctx.harness always present, and harness-first h…
AlemTuzlak Oct 16, 2026
263f285
feat(ai-persistence): session index v2 keyed by tenant and thread, wi…
AlemTuzlak Oct 17, 2026
6a65b6e
feat(ai-harness): a RunContext for every run, so tools and plugins ac…
AlemTuzlak Oct 17, 2026
432d342
feat(ai-harness): access types and the membersOnly() policy
AlemTuzlak Oct 17, 2026
ecc5aa7
feat(ai-harness): tenant-keyed index rows, log ids, and media keys
AlemTuzlak Oct 17, 2026
3efb02e
feat(ai-harness): host access with a principal-bound handle, sharing,…
AlemTuzlak Oct 17, 2026
42faa9d
docs(ai-harness): access and tenants, shared threads on membersOnly, …
AlemTuzlak Oct 17, 2026
70e1c5e
feat(ai-mcp, ai-acp, ai-harness-cli): the MCP server, the ACP agent, …
AlemTuzlak Oct 17, 2026
cc5f5ea
fix(ai-harness): every thread-keyed store write carries the tenant
AlemTuzlak Oct 17, 2026
799a373
fix(ai-harness): delete removes the transcript, session plugins see t…
AlemTuzlak Oct 17, 2026
f134b9d
test(ai-e2e): the protocol route on host access, and a shared-thread …
AlemTuzlak Oct 17, 2026
668ee7c
test(ai-harness): delete removes the transcript and closes the session
AlemTuzlak Oct 17, 2026
1e02b85
chore(ai-harness-cli): list @tanstack/ai-mcp as a dev dependency for …
AlemTuzlak Oct 17, 2026
d3e5cf7
fix(ai-harness, ai-mcp): OAuth flows and commands stop on their signal
AlemTuzlak Oct 17, 2026
bfbf9be
fix(ai-harness-cli): SIGINT and SIGTERM cancel the turn, then stop, i…
AlemTuzlak Oct 17, 2026
712ca71
fix(ai-harness): one retention policy bounds every per-session collec…
AlemTuzlak Oct 17, 2026
a43132a
Merge branch 'feat/harness-access' into feat/harness-lifecycle
AlemTuzlak Oct 17, 2026
277eeb9
feat(ai-harness, ai-mcp, ai-acp, ai-harness-cli): reference-counted s…
AlemTuzlak Oct 17, 2026
5c1b958
docs(ai-harness): lifecycle and retention, handles, observe, signals,…
AlemTuzlak Oct 17, 2026
4faa687
feat(ai-harness, ai-mcp, ai-acp): host.observe, a read-only session v…
AlemTuzlak Oct 17, 2026
dc51be6
test(ai-e2e): delete purges media and messages, and reads work after …
AlemTuzlak Oct 17, 2026
3edade7
feat(ai-harness, ai-mcp): host-lifetime plugins, MCP stdio servers on…
AlemTuzlak Oct 17, 2026
6510a12
fix(ai-harness): definePlugin infers its definition only from the arg…
AlemTuzlak Oct 17, 2026
b2126e5
feat(ai-harness): thread-state log records and their fold
AlemTuzlak Oct 17, 2026
974b61b
fix(ai-harness): key host plugins by the harness definition, not its …
AlemTuzlak Oct 17, 2026
a60085f
feat(ai-persistence, ai-harness): one persistence shape, every host h…
AlemTuzlak Oct 17, 2026
869b4a5
Merge branch 'feat/harness-lifecycle' into feat/harness-log-session
AlemTuzlak Oct 17, 2026
657ee91
test(ai-mcp): open two sessions of one harness definition to share a …
AlemTuzlak Oct 17, 2026
ce1293c
test(ai-harness, ai-harness-cli): give polled checks 5 s, for slower …
AlemTuzlak Oct 17, 2026
c4cafd3
feat(ai-harness): settings, config, plugin state, interrupted turns, …
AlemTuzlak Oct 17, 2026
96ce736
Merge branch 'feat/harness-lifecycle' into feat/harness-log-session
AlemTuzlak Oct 17, 2026
1a0962a
feat(ai-harness): one ownership epoch per log, renewed once per host,…
AlemTuzlak Oct 17, 2026
bc7b856
feat(ai-mcp): mcp() reconnects with backoff, lists tools again on lis…
AlemTuzlak Oct 17, 2026
0c635c6
fix(ai-harness, ai-sandbox): coding commands get an env allowlist, on…
AlemTuzlak Oct 17, 2026
babeff6
feat(ai-harness): runs and interrupts live in the session log
AlemTuzlak Oct 17, 2026
693450a
fix(ai-acp, ai-code-mode, ai-harness): ACP keeps prompt order and rel…
AlemTuzlak Oct 17, 2026
5e10199
wip: unfinished node work, saved before a shutdown (not reviewed)
AlemTuzlak Oct 17, 2026
76af6ac
fix(ai-harness): shared file locks with an unchanged check, and snaps…
AlemTuzlak Oct 17, 2026
dd05481
fix(ai-harness, ai-sandbox, ai-sandbox-local-process): bounded backgr…
AlemTuzlak Oct 17, 2026
e7d1555
perf(ai-skills): one folder watcher per host with a cache of the skills
AlemTuzlak Oct 17, 2026
7a48444
feat(ai-harness-cli): cancel and steer act at once in a terminal, loa…
AlemTuzlak Oct 17, 2026
07470f5
feat(ai-persistence, ai-harness): inputs from any machine through the…
AlemTuzlak Oct 17, 2026
7d946f1
fix(ai-harness): a settled operation keeps its status, a media signer…
AlemTuzlak Oct 17, 2026
8a5f44f
perf(ai-harness, ai-sandbox): size caps for webfetch and read_file, r…
AlemTuzlak Oct 17, 2026
3868e4f
fix(ai-harness): every plugin contribution has an owner, ctx.on clean…
AlemTuzlak Oct 17, 2026
c0b86ac
perf(ai-harness): cache plugin lists and permission patterns, read in…
AlemTuzlak Oct 17, 2026
5ce3e64
docs(ai-harness): say when capabilities report the WebSocket
AlemTuzlak Oct 17, 2026
0430d43
feat(ai-harness): createHarnessWorker, session.schedule, and step.sle…
AlemTuzlak Oct 17, 2026
08faf9a
feat(ai): onGuardToolCall middleware hook that runs on the final argu…
AlemTuzlak Oct 17, 2026
d6026ce
test(e2e): onGuardToolCall denies on the final args after a transform…
AlemTuzlak Oct 17, 2026
c41c30a
perf(ai-harness): stream media, cache base64 parts across turns, cap …
AlemTuzlak Oct 17, 2026
c249782
test(e2e): a worker on another host runs a submitted input and a sche…
AlemTuzlak Oct 17, 2026
526a787
feat(ai): CredentialStore compare-and-set pair, readCredentialEntry, …
AlemTuzlak Oct 17, 2026
8e9797c
feat(ai-harness): one log per session tree, session.logState(), and a…
AlemTuzlak Oct 17, 2026
76918b1
perf(ai-harness, ai-harness-cli): the view and the printer only redo …
AlemTuzlak Oct 17, 2026
6d96156
feat(ai-harness): snapshots in the log store by size or time, retenti…
AlemTuzlak Oct 17, 2026
1d2300f
fix(ai-harness): export LogOwnedElsewhereError, and say that the log …
AlemTuzlak Oct 17, 2026
f4d1120
feat(ai-harness, ai): protocol v2 frames with error codes and request…
AlemTuzlak Oct 17, 2026
28b0800
docs(ai-harness, ai-persistence): durable sessions, one log per tree,…
AlemTuzlak Oct 17, 2026
a28f10c
ci: apply automated fixes
autofix-ci[bot] Oct 17, 2026
27ed0b3
docs(ai-harness): first draft of the harness protocol page
AlemTuzlak Oct 17, 2026
1b18536
Merge branch 'feat/harness-log-session' into feat/harness-redaction
AlemTuzlak Oct 17, 2026
d02278c
Merge branch 'feat/harness-log-session' into feat/harness-point-bugs
AlemTuzlak Oct 17, 2026
b4af9dc
feat(ai-harness): one redactor per session for log records, feed even…
AlemTuzlak Oct 17, 2026
606fa8e
fix(ai-harness): check answers and cancels from submit and schedule a…
AlemTuzlak Oct 17, 2026
1dc6224
test(ai-harness): give polled checks 5 s, for slower CI coverage runs
AlemTuzlak Oct 17, 2026
f75cced
Merge remote-tracking branch 'origin/feat/harness-log-session' into f…
AlemTuzlak Oct 17, 2026
4c3d101
test(e2e): read the data lines of POST run streams that now carry an …
AlemTuzlak Oct 17, 2026
36e8134
feat(ai-harness): the log keeps the final tool result the model saw, …
AlemTuzlak Oct 17, 2026
ad83aff
Merge branch 'feat/harness-log-session' into feat/harness-redaction
AlemTuzlak Oct 17, 2026
42c8014
fix(ai-harness): tool turns take no steers, a turn keeps its config, …
AlemTuzlak Oct 17, 2026
4a43804
Merge branch 'feat/harness-log-session' into feat/harness-point-bugs
AlemTuzlak Oct 17, 2026
be73839
Merge branch 'feat/harness-log-session' into feat/harness-perf
AlemTuzlak Oct 17, 2026
7c13e0f
feat(ai-harness): a secret answer sent to another host goes through t…
AlemTuzlak Oct 17, 2026
8528cfd
fix(ai, ai-harness): each context default registration removes only i…
AlemTuzlak Oct 17, 2026
b88d140
Merge branch 'feat/harness-log-store-v2' into feat/harness-api-shape
AlemTuzlak Oct 17, 2026
cfa1721
Merge branch 'feat/harness-api-shape' into feat/harness-access
AlemTuzlak Oct 17, 2026
2123a0f
Merge branch 'feat/harness-access' into feat/harness-lifecycle
AlemTuzlak Oct 17, 2026
d73bc86
Merge branch 'feat/harness-lifecycle' into feat/harness-log-session
AlemTuzlak Oct 17, 2026
8afc555
Merge branch 'feat/harness-log-session' into feat/harness-point-bugs
AlemTuzlak Oct 17, 2026
07f9377
fix(ai-harness): the media signer's key check covers an empty secret
AlemTuzlak Oct 17, 2026
9ca6284
test(e2e): a secret answer sent to another host never shows in the tr…
AlemTuzlak Oct 17, 2026
bc35162
feat(ai-harness, ai-mcp): OAuth refresh runs once per host, saves wit…
AlemTuzlak Oct 17, 2026
742b4c8
perf(ai-harness): ring buffer log tail, joins and rebuilt operations …
AlemTuzlak Oct 17, 2026
abe24ae
perf(ai-harness): plugin prompts with placement 'context' go after th…
AlemTuzlak Oct 17, 2026
cfea3ab
Merge branch 'feat/harness-log-session' into feat/harness-reporting
AlemTuzlak Oct 17, 2026
36f40df
docs(ai, ai-harness, ai-mcp): redaction, secret answers across hosts,…
AlemTuzlak Oct 17, 2026
a7cc5c1
Merge branch 'feat/harness-log-session' into feat/harness-redaction
AlemTuzlak Oct 17, 2026
68d5e7b
feat(ai-harness): the Wait type and waits in the log, with expiry, wa…
AlemTuzlak Oct 17, 2026
ca7dc37
Merge branch 'feat/harness-redaction' into feat/harness-waits
AlemTuzlak Oct 17, 2026
2051764
feat(ai-harness): one wire encoding on every transport with hello, ve…
AlemTuzlak Oct 17, 2026
171b1d5
test(e2e): the access spec skips the hello frame of the session stream
AlemTuzlak Oct 17, 2026
754f6b8
feat(ai-harness): typed subject-first agent actions start, run, and g…
AlemTuzlak Oct 17, 2026
1b4ce5f
wip(ai-harness, ai-mcp): PR 7 boundary, coding-approvals, live-waits,…
AlemTuzlak Oct 17, 2026
6e131ab
wip(ai-harness): PR 9 transfer (stopped mid-way, not checked)
AlemTuzlak Oct 17, 2026
d6f98fa
feat(ai-harness): host.transfer moves a conversation to another harne…
AlemTuzlak Oct 18, 2026
04b667b
feat(ai-harness): a grouped stream option for the heartbeat, the slow…
AlemTuzlak Oct 18, 2026
cb5f621
feat(ai-harness): the active agent lives in the log and answers when …
AlemTuzlak Oct 18, 2026
82b844f
feat(ai-harness, ai-mcp): waits at the boundary (permissions, the que…
AlemTuzlak Oct 18, 2026
17deaf5
feat(ai-harness): ctx.harness.handoff, a handoff tool, and one handof…
AlemTuzlak Oct 18, 2026
d1f482b
feat(ai-harness): hand a turn to a person through a human wait, opera…
AlemTuzlak Oct 18, 2026
b6395ba
feat(ai, ai-harness, ai-mcp, ai-acp, ai-harness-cli): agent runs paus…
AlemTuzlak Oct 18, 2026
0c44fa1
test(ai-mcp): the media result has a waits list
AlemTuzlak Oct 18, 2026
8b8e3b8
feat(ai-harness): handing a turn to a person is opt-in with routing.h…
AlemTuzlak Oct 18, 2026
2183533
Merge branch 'feat/harness-waits' into feat/harness-agent-tree
AlemTuzlak Oct 18, 2026
6e3bda5
test(e2e): an approval asked on one server is answered on another, an…
AlemTuzlak Oct 18, 2026
dec6601
test(e2e): handoffs to an agent and to a person, and a transfer betwe…
AlemTuzlak Oct 18, 2026
b44ab64
docs(ai-harness): hand off a conversation to an agent, to a person, o…
AlemTuzlak Oct 18, 2026
c9fb394
docs(ai, ai-harness, ai-mcp, ai-acp, ai-harness-cli): wait for a pers…
AlemTuzlak Oct 18, 2026
00e250b
fix(ai-harness): a session that does not own the log shows the waits …
AlemTuzlak Oct 18, 2026
6a442bd
fix(ai-harness): lint errors in agent waits, step.ask, and permission…
AlemTuzlak Oct 18, 2026
9fe6d91
feat(ai-harness): the run tree in the log, with RunNode for turns, ag…
AlemTuzlak Oct 18, 2026
6bab888
Merge branch 'feat/harness-waits' into feat/harness-agent-tree
AlemTuzlak Oct 18, 2026
48f9f03
Merge branch 'feat/harness-handoffs' into feat/harness-integrations
AlemTuzlak Oct 18, 2026
29f01de
feat(ai-harness): agent limits and budgets that hold, with queued sta…
AlemTuzlak Oct 18, 2026
0ed113a
test(ai-harness, ai-code-mode): approvals are turn waits answered wit…
AlemTuzlak Oct 18, 2026
eb3279e
test(e2e): in acceptEdits mode the write asks before it runs, with it…
AlemTuzlak Oct 18, 2026
32bd09c
feat(ai-harness): background runs inherit turn plugins, child signals…
AlemTuzlak Oct 18, 2026
bbc3750
Merge feat/harness-handoffs into feat/harness-coding-workspace, with …
AlemTuzlak Oct 18, 2026
1562321
feat(ai-harness): durable agent groups with a concurrency gate and ke…
AlemTuzlak Oct 18, 2026
1dacc99
docs(ai-harness, ai-sandbox, ai-sandbox-local-process): run the codin…
AlemTuzlak Oct 18, 2026
f73c34e
feat(ai-harness): ownership.hostId pins threads that use a local fold…
AlemTuzlak Oct 18, 2026
ee58e44
docs(ai-harness): agents from code, groups, limits, budgets, the run …
AlemTuzlak Oct 18, 2026
1723ce1
feat(ai-harness): harnessAgent runs as a child session in the root lo…
AlemTuzlak Oct 18, 2026
45e4f97
docs(ai-harness): the harnessAgent child run, as built
AlemTuzlak Oct 18, 2026
5e7a8fd
Merge branch 'feat/harness-waits' into feat/harness-agent-tree
AlemTuzlak Oct 18, 2026
d75e0ce
test(e2e): agent groups with a concurrency gate, a run budget, shared…
AlemTuzlak Oct 18, 2026
2d48b0f
Merge branch 'feat/harness-agent-tree' into feat/harness-handoffs
AlemTuzlak Oct 18, 2026
dc5aede
test(ai-harness): give polled checks and a takeover deadline room for…
AlemTuzlak Oct 18, 2026
955730b
Merge branch 'feat/harness-agent-tree' into feat/harness-handoffs
AlemTuzlak Oct 18, 2026
da677f0
Merge branch 'feat/harness-handoffs' into feat/harness-reporting
AlemTuzlak Oct 18, 2026
5d33d32
test(e2e): the handoff route reads view text by message role
AlemTuzlak Oct 18, 2026
a15ac85
Merge branch 'feat/harness-handoffs' into feat/harness-coding-workspace
AlemTuzlak Oct 18, 2026
809977b
Merge branch 'feat/harness-handoffs' into feat/harness-coding-workspace
AlemTuzlak Oct 18, 2026
d3d9d8d
feat(ai-harness): run tags on every event frame, ctx.model.chat for p…
AlemTuzlak Oct 18, 2026
22d1caf
Merge branch 'feat/harness-coding-workspace' into feat/harness-reporting
AlemTuzlak Oct 18, 2026
8942571
Merge branch 'feat/harness-reporting' into feat/harness-point-bugs
AlemTuzlak Oct 18, 2026
27fc7df
Merge branch 'feat/harness-reporting' into feat/harness-perf
AlemTuzlak Oct 18, 2026
be54212
chore(ai-harness): allow the ps binary in knip, and give polled check…
AlemTuzlak Oct 18, 2026
f07d16c
feat(ai-harness): lifecycle events, counted router and transfer model…
AlemTuzlak Oct 18, 2026
e59918d
Merge branch 'feat/harness-coding-workspace' into feat/harness-reporting
AlemTuzlak Oct 18, 2026
e9b055c
Merge branch 'feat/harness-reporting' into feat/harness-integrations
AlemTuzlak Oct 18, 2026
69057b2
Merge branch 'feat/harness-integrations' into feat/harness-perf
AlemTuzlak Oct 18, 2026
0635c94
test(ai-harness): one threadId in the expected gap frame
AlemTuzlak Oct 18, 2026
2ab325f
docs(ai-harness): the harness protocol page, keeping the stream open,…
AlemTuzlak Oct 18, 2026
4169f3d
docs(ai-harness): run tags and the gap reload in the custom UI page
AlemTuzlak Oct 18, 2026
8bf66f3
test(e2e): the wire, the gap, the run tree in one stream, lifecycle e…
AlemTuzlak Oct 18, 2026
f1d5347
Merge branch 'feat/harness-perf' into feat/harness-point-bugs
AlemTuzlak Oct 18, 2026
b0a4a90
Merge branch 'feat/harness-reporting' into feat/harness-integrations
AlemTuzlak Oct 18, 2026
9cd7efc
chore(changeset): the integration changes for mcp, acp, code mode, sk…
AlemTuzlak Oct 18, 2026
cbb2254
Merge branch 'feat/harness-integrations' into feat/harness-perf
AlemTuzlak Oct 18, 2026
5fadbfd
Merge branch 'feat/harness-perf' into feat/harness-point-bugs
AlemTuzlak Oct 18, 2026
9265b90
test(e2e): the first-party fixtures match the context prompt that now…
AlemTuzlak Oct 18, 2026
012afa8
Merge branch 'feat/harness-perf' into feat/harness-point-bugs
AlemTuzlak Oct 18, 2026
0515997
fix(ai-harness-cli): serve signs media URLs with a key derived from t…
AlemTuzlak Oct 18, 2026
cb1598c
fix(ai-harness, ai-mcp, ai-acp, ai-code-mode): final review fixes for…
AlemTuzlak Oct 18, 2026
824b03f
Merge branch 'feat/harness-point-bugs' into feat/harness-final-review
AlemTuzlak Oct 18, 2026
b742c17
test(ai-harness): give polled checks room for slower CI coverage runs
AlemTuzlak Oct 18, 2026
cebad01
Merge branch 'feat/harness-reporting' into feat/harness-integrations
AlemTuzlak Oct 18, 2026
2d03d1f
Merge branch 'feat/harness-integrations' into feat/harness-perf
AlemTuzlak Oct 18, 2026
a609b81
Merge branch 'feat/harness-perf' into feat/harness-point-bugs
AlemTuzlak Oct 18, 2026
d8d2be4
fix(ai-acp): one agent app serves one connection, with editor capabil…
AlemTuzlak Oct 18, 2026
1e4bf7a
fix(ai-harness): a thread delete ends it in first-party plugins, the …
AlemTuzlak Oct 18, 2026
9f91021
refactor(ai-harness, ai-mcp, ai-harness-cli): session, client, and vi…
AlemTuzlak Oct 18, 2026
e6e64b4
Merge branch 'feat/harness-point-bugs' into feat/harness-final-review
AlemTuzlak Oct 18, 2026
08e3beb
docs(ai-harness, ai-mcp, ai-acp, ai-code-mode): the final review chan…
AlemTuzlak Oct 18, 2026
7b31219
refactor(ai-harness, ai-harness-cli, ai-mcp): remove what the redesig…
AlemTuzlak Oct 19, 2026
68308a4
docs(ai-harness): read pendingWaits with no fallback in the code samples
AlemTuzlak Oct 19, 2026
48290c6
Merge branch 'feat/harness-log-store-v2' into feat/harness-cleanup
AlemTuzlak Oct 20, 2026
d322e2c
test(ai-harness): name projectFormatters in its describe, and drop ol…
AlemTuzlak Oct 20, 2026
ef2dd9a
test(ai-harness): a snapshot with usage totals that have no bySource …
AlemTuzlak Oct 20, 2026
b5085d9
chore(changeset): fold the LogStore heartbeat changeset into the sess…
AlemTuzlak Oct 20, 2026
42a4b36
Merge remote-tracking branch 'origin/feat/harness-log-store-v2' into …
AlemTuzlak Oct 20, 2026
afa54cf
test(ai-harness): run the tests that were skipped as flaky again, now…
AlemTuzlak Oct 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions .changeset/ai-byok-credential-cas.md

This file was deleted.

6 changes: 3 additions & 3 deletions .changeset/ai-harness-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
Add `@tanstack/ai-harness-cli`: `runCli(harness)` runs a harness from the terminal.

- Line mode reads one message or `/command` per line, hides a secret answer while the user types, and sends `@path` files. In a terminal, `/cancel` and a message typed while a turn runs act at once: the message joins the running turn. When a turn waits, each next line answers one wait: `y` or `n` for an approval (`once`, `always`, or `reject` for an approval of `permissions()`), and an answer read with the schema for a question. Pass `ui` to show your own screen on a session view instead.
- `-p "prompt"` runs one prompt for scripts and CI, and `--output ndjson` prints every AG-UI event. `-p` exits with code 3 (`EXIT.needsAction`) at the first wait of the turn: an approval, a question, or a sign-in. The exit code for this case changed from 2 to 3. A script that checks for exit code 2 must check for 3.
- `--serve` serves the session protocol over HTTP behind a bearer token.
- `--acp` and `--mcp` load `@tanstack/ai-acp` and `@tanstack/ai-mcp` only when you use the flag. A missing package, or one without harness support, fails that flag with a clear message. Any other error while the package loads is reported, not hidden. The `--dashboard` flag was removed.
- `-p "prompt"` runs one prompt for scripts and CI, and `--output ndjson` prints every AG-UI event. `-p` exits with code 3 (`EXIT.needsAction`) at the first wait of the turn: an approval, a question, or a sign-in.
- `--serve` serves the session protocol over HTTP behind a bearer token. It signs media URLs with its own key, never with the token: `--media-key` or `HARNESS_MEDIA_KEY` (at least 32 bytes), or a new random key at each start. Without a set key, signed media URLs stop working after a restart. On a host without `access`, it also passes `openAccess`: the `principal` of `runCli`, or `true` without one. So each request with the token runs as the user of the CLI.
- `--acp` and `--mcp` load `@tanstack/ai-acp` and `@tanstack/ai-mcp` only when you use the flag. A missing package, or one without harness support, fails that flag with a clear message. Any other error while the package loads is reported, not hidden.
- In every mode, the first SIGINT or SIGTERM cancels the running turn. A second one, or one while no turn runs, stops the CLI, and `runCli` resolves to 130 (SIGINT) or 143 (SIGTERM). The CLI closes a host that it made with `recoverable: true`, or releases its session handle on your own host and does not wait for a turn that ignores the cancel. The `signals` option takes another signal source, for example an `EventEmitter` in a test.
- `host` and `principal` options let the CLI use your durable host and run as your user. Every mode opens its sessions as that principal. On a host with `access`, `--serve` reads the user of each request with `identify`, and `--mcp` does not start, because stdio has no login.
6 changes: 3 additions & 3 deletions .changeset/ai-harness-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Add `@tanstack/ai-harness`: build an agent harness on TanStack AI. A harness kee
- `createHarnessHost()` opens sessions. Each `host.open` gives its own handle to the live session of the thread. `session.close()`, or `await using`, releases the handle. A dropped handle is released by the garbage collector, with one warning. After `close()`, each call on the handle throws. When the session closes under a handle that is not released (its log failed, or a delete ran), the next call on the handle opens it again.
- `lifecycle` on `createHarnessHost()` sets how sessions live. A session that no handle holds closes after it was idle for `idleCloseMs` (default 60 000). `maxOpen` (default 500) first closes the least recently used idle sessions. A session is not idle while a background job of this host runs. `retention` (`settledOperations`, `settledAgentRuns`, `eventBytes`) bounds the settled operations, agent runs, receipts, events, and log tail that each session keeps in memory. The log keeps the `MESSAGES_SNAPSHOT` that core sends when a run pauses at an interrupt, so a rejoin from the log rebuilds the paused run. `host.close({ recoverable })` stops the work of every session and refuses later opens. With `recoverable: true`, the host gives each log that has work back with a wake, so a worker takes it at once.
- `host.observe(harness, { threadId, principal })` reads a thread with no session: its events, transcript, snapshot, media, usage, and entry. It sets up no plugins, writes nothing, and never takes the log, so a read works while another host owns the thread. Its snapshot follows the same rules as `session.snapshot()`, without what only an open session knows (a running turn, operations, queues). `view.logState()` reads the `durability.reduce` fold.
- `host.sessions.delete` closes the sessions of a thread and of its child threads. Then it deletes the log of the session tree (with the settings, config, plugin state, interrupted turn, runs, interrupts, usage, revert data, and the inputs that wait), the index entries, the uploaded media, the generated artifacts, and the log checkpoint. The first-party plugins also end the thread: `workspaceTools()` closes an owned sandbox with `'closed'`, and `snapshots()` removes the shadow repository. This needs the index entry, and the harness on that host: a host that opened it, or one with it in the `harnesses` option of `createHarnessHost()`. Else the host warns. A child thread is ended in the log of its root. While the delete runs, an open of the thread throws. With `access`, a thread with no index entry throws `AccessDeniedError`.
- `host.sessions.delete` closes the sessions of a thread and of its child threads. Then it deletes the log of the session tree (with the settings, config, plugin state, interrupted turn, runs, interrupts, usage, revert data, and the inputs that wait), the index entries, the uploaded media, and the generated artifacts. The first-party plugins also end the thread: `workspaceTools()` closes an owned sandbox with `'closed'`, and `snapshots()` removes the shadow repository. This needs the index entry, and the harness on that host: a host that opened it, or one with it in the `harnesses` option of `createHarnessHost()`. Else the host warns. A child thread is ended in the log of its root. While the delete runs, an open of the thread throws. With `access`, a thread with no index entry throws `AccessDeniedError`.
- A session takes `send` (a prompt, a steer into the running turn, a follow-up, or a message to an agent run with `to`), `continue`, `reset`, `resolve`, `cancel`, and `background` inputs. Each returns an operation with a receipt, and the session streams typed events. `host.isOpen(harness, { threadId })` says whether this host holds that session open.
- The session methods take one object: `cancelInput({ inputId, principal? })`, `revert({ messageId, principal? })`, `background({ toolCallId?, principal? })`, `getMedia({ id })`, `mediaUrl({ id })`, and `agentTranscript({ subagentRunId })`.
- `session.append({ records, inputId?, principal? })` returns a `Receipt`. A second append with the same `inputId` writes nothing. Without `inputId`, the append gets a new input id, and the log keeps it as an input. The `harness.revert` record keeps the principal of `revert`, and the `harness.job` record keeps the principal of `background`.
Expand All @@ -31,15 +31,15 @@ Add `@tanstack/ai-harness`: build an agent harness on TanStack AI. A harness kee
- `ctx.harness.state({ key, initial })` gives the agents of a session a shared state by key, with `get` and `update`. The log keeps it, and a fork copies it. `STATE_SNAPSHOT` has `{ plugins, shared }`.
- A routed agent takes steers at its next model call, and late joins in the same turn, and `turn.beforeFinish` and `turn.onModelError` run for it.
- Every host keeps each session tree in one log, `stores.log` from `@tanstack/ai-persistence`. `memoryPersistence()` has a memory log, and a host with no `persistence` uses it. The log holds the inputs, events, transcripts, runs, interrupts, settings, config, plugin state, agent results, schedules, and tool steps, and the host folds them when a thread opens. A root thread and its child threads (agent runs and subagents) write to the log of the root. `session.logState()` reads the `durability.reduce` fold of the whole tree. A harness with another `durability.project` or `durability.reduce` than the one that folds an open log throws when it opens that log. With a log store that survives a restart, the next host recovers a turn that a crash stopped.
- One host owns a log at a time: the first write takes it, one timer per host renews it, and a host that loses it stops its work on it. A read never takes the log. An input that reaches another host waits in the input lane of the log, and the owner runs it (receipt `queued`). `ownership: { ttlMs, renewMs }` sets the timing. `renewMs` must be less than `ttlMs`, else `createHarnessHost` throws a `RangeError`. With only `ttlMs`, `renewMs` is a third of it.
- One host owns a log at a time: the first write takes it, one timer per host renews it, and a host that loses it stops its work on it. A host gives a log back when it has no work. Another host recovers the work of a log only after its ownership expires. A read never takes the log. An input that reaches another host waits in the input lane of the log, and the owner runs it (receipt `queued`) with its JSON overrides (`model`, `modelOptions`, `reasoning`, and `promptCache`). An input to a thread that was deleted is refused: the receipt is `rejected` with the reason `deleted`, and an awaited operation rejects with `InputRejectedError` ("The thread was deleted."). `ownership: { ttlMs, renewMs }` sets the timing. `renewMs` must be less than `ttlMs`, else `createHarnessHost` throws a `RangeError`. With only `ttlMs`, `renewMs` is a third of it.
- When the owner refuses an offered input (busy, or an id that the log has with another payload), `session.settled()` on the host that offered it rejects with `InputRejectedError`. The log gets a `harness.offer.rejected` record. An offer that comes while the owner appends is not missed.
- A host that does not own a thread opens it while the owner runs a background job. Settings, config values, and the interrupted turn follow the changes of other hosts. `configure` merges into the latest stored settings.
- The owner writes a snapshot of the folded log to the log store after `log.snapshot.afterBytes` of new records (default 1 000 000) or `log.snapshot.afterMs` (default 300 000), and a new host opens from it. With `lifecycle.retention.logRecords: 'since-snapshot'` (default `'all'`), the owner truncates the records before each snapshot. A reader whose cursor is in the dropped part gets one `harness.gap` event (`EventGap`), then the stream goes on. With `'since-snapshot'`, a thread whose snapshot the host cannot use (a changed `reduce` version or `project`) cannot open, and rejects with `LogTruncatedError`. `session.revert()` works with `'since-snapshot'`: the snapshot keeps the last 500 file steps. A plugin sets which of its records the snapshot keeps with `keepRecords: { [type]: count }` on its definition, and reads them, oldest first, with `ctx.session.keptRecords(type)`. The fold keeps the last 100 ended runs, run nodes, interrupts, and agent results, and drops the turn overrides of a settled input, so a long thread does not grow its fold and its snapshot with each turn.
- `host.submit` stores an input for a thread and runs nothing. `session.schedule({ at, input, scheduleId, principal })` runs an input later, and `session.cancelSchedule({ scheduleId })` stops it. `createHarnessWorker({ host, harnesses, concurrency, signal, pollMs })` runs the threads that have work and no live host, and returns `{ stop }`. `host.resumePending` does one sweep. A swept thread that fails to open goes back with a wake in 30 s. A worker skips a thread that fails to open for a time that doubles after each failure in a row, up to 5 minutes, so the other threads still run. `host.submit` throws for a thread of another harness, and a later offer never changes the harness of a log.
- `durableTool()` keeps the result of each step. A crash can cut a step, and then it runs again, so `step.do` passes `{ idempotencyKey, epoch }` to `fn`. Give the key to your system, so it refuses the second side effect. `step.ask({ name, message, schema })` asks the user: the log keeps the question, any host can take the answer, and a run again after a stop returns the logged answer. A `step.do` value must be JSON: a `Date`, a `Map`, or a class instance is a type error. `step.sleep({ name, ms })` waits in the process, and the log keeps the wake time, so a worker continues the call after a stop. A sleep longer than 30 s on a thread that only a worker runs gives the thread back with the wake, so it keeps no worker slot. `durability.truncatedToolResult` sets the text of a tool call cut at the output limit. Without it, a cut answer ends the turn.
- `turn.onModelError`, `retryTransientErrors()`, `turn.beforeFinish`, and `turn.canJoin` control how a turn retries, continues, and ends.
- Every approval, question, sign-in, client tool, and human takeover that a run waits for is a `Wait` in the session log (`waitId`, `kind`, `runId`, `schema`, `expiresAt`, `message`). `WaitKind` is `approval`, `question`, `sign-in`, `client-tool`, or `human`. `waitKind`, `isApproval`, `waitOfInterrupt`, and `waitOfQuestion` come from the root and from `./view`. `snapshot().pendingWaits` lists the open waits: each one with its `interrupt` when a chat run stopped for it, and `turn: true` for a wait of the stopped turn. It is the one wait view of a snapshot. and `session.waitFor({ runId, signal })` resolves with the next one. A wait ends `answered`, `cancelled`, or `expired`. A wait with `expiresAt` expires on time. Only `sign-in` has a default limit: 15 minutes.
- A wait that only the log holds keeps no host busy: the session goes idle, and an answer on any host continues the run. `resolve` answers the waits with an `interrupt`: of a stopped turn, or of one background agent run per call. `answer` answers the waits with no `interrupt` (a `step.ask` question, an MCP question, the `human` wait of a handoff), checks the answer against the JSON Schema of the wait, and keeps a secret out of the log. A `resolve` of such a wait is refused with `use_answer`, and a second answer to a wait that one answer ends now is refused. `host.submit` checks an answer against the wait that it answers.
- A wait that only the log holds keeps no host busy: the session goes idle, and an answer on any host continues the run. `resolve` answers the waits with an `interrupt`: of a stopped turn, or of one background agent run per call. `answer` answers the waits with no `interrupt` (a `step.ask` question, an MCP question, the `human` wait of a handoff), checks the answer against the JSON Schema of the wait, and keeps a secret out of the log. A `resolve` of such a wait is refused with `use_answer`, and a second answer to a wait that one answer ends is refused. `host.submit` checks an answer against the wait that it answers.
- A run waits for a person only at fixed points: `permissions()` before a tool call, the `question` tool, `step.ask` in a durable tool, a command (`CommandContext.step`), or an agent run, and the live waits of MCP elicitation and OAuth sign-in. `ctx.session.ask` throws anywhere else, with an error that names `step.ask`, the `question` tool, and `PermissionResources`. A command that called `step.ask` runs again from the top after a host stop. `Question` takes `expiresAt` and `signal`.
- A background agent run stops with status `waiting` on a wait, for example an approval of `permissions()`, and continues after the answer, also after a restart. An agent that the model calls as a tool inside a turn cannot wait: a call of it that needs an approval is refused.
- The `operator` access action answers a `human` wait of any run, also with a resolve. A `human` wait takes only `operator`. `membersOnly()` gives it only to the role `operator`, so the user who waits for a person cannot act as that person. `membersOnly({ operator: { roles } })` sets the roles that have it. `authorize` gets the `wait` (`waitId`, `kind`) of an `answer` or `operator` action.
Expand Down
5 changes: 0 additions & 5 deletions .changeset/ai-persistence-host-heartbeat.md

This file was deleted.

7 changes: 7 additions & 0 deletions .changeset/byok-credential-store.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,12 @@ BYOK keys can now live on the server. `@tanstack/ai/byok` exports the `Credentia
- `tenantScopeOf(scope)`: the tenant scope of a user scope, or `undefined` when the scope has no user or no tenant.
- `memoryCredentialStore()`: a store in memory, for development and tests.
- `envKeyEntry(provider)`: the first `provider.env` name that is set, with its value.
- `readCredentialEntry(store, scope, id)`: reads a credential like `readCredential`. It also tells you which scope had the credential (the user or the tenant), and gives its revision when the store has `getVersioned`.

`CredentialStore` has two optional methods for compare-and-set writes, `getVersioned` and `setIf`, with the same shape as the pair on `MetadataStore`. A store without them still works. `memoryCredentialStore` has both.

Keep the store in your own database or in a secrets manager, and encrypt the secrets at rest. An `oauth` credential can keep the OAuth `client` it was issued to. `list` never returns secret values.

`scrubSecrets` from `@tanstack/ai/byok` now also removes the JSON-escaped and URL-encoded forms of each secret. It skips a secret that is shorter than 6 characters, so a short value does not remove common words.

`Credential` has a new type, `secret_answer` (`{ value, expiresAt }`). The harness keeps a secret answer to a question there for a short time and deletes it once read. Leave this type out of a list of connected accounts. `memoryCredentialStore().list` returns its `expiresAt`.
Loading
Loading