zidane 6.4.4 → 6.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/acp-cli.js +7 -6
- package/dist/acp-cli.js.map +1 -1
- package/dist/{acp-BLO58K3F.js → acp-o-LQAjFw.js} +4 -5
- package/dist/{acp-BLO58K3F.js.map → acp-o-LQAjFw.js.map} +1 -1
- package/dist/acp.d.ts +1 -1
- package/dist/acp.js +1 -1
- package/dist/{agent-8sUadp5M.js → agent-BVNkC1h4.js} +886 -113
- package/dist/agent-BVNkC1h4.js.map +1 -0
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +1 -2
- package/dist/{anthropic-D3lkw5mv.js → anthropic-Dov2mC5e.js} +3 -3
- package/dist/{anthropic-D3lkw5mv.js.map → anthropic-Dov2mC5e.js.map} +1 -1
- package/dist/{auth-BzYbbdN_.js → auth-B8ZuBcKC.js} +5 -5
- package/dist/{auth-BzYbbdN_.js.map → auth-B8ZuBcKC.js.map} +1 -1
- package/dist/chat/pure.d.ts +3 -3
- package/dist/chat/pure.js +2 -2
- package/dist/chat.d.ts +13 -44
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +8 -7
- package/dist/chat.js.map +1 -1
- package/dist/{edit-utils-EGosADZq.js → context-breakdown-BLF10tOO.js} +154 -2
- package/dist/context-breakdown-BLF10tOO.js.map +1 -0
- package/dist/{context-transport-D29WvHcm.js → context-transport-D-daWOmp.js} +3 -3
- package/dist/{context-transport-D29WvHcm.js.map → context-transport-D-daWOmp.js.map} +1 -1
- package/dist/contexts/daytona.d.ts +1 -1
- package/dist/contexts/e2b.d.ts +1 -1
- package/dist/eval.d.ts +1 -1
- package/dist/eval.js +1 -1
- package/dist/extensions.d.ts +2 -2
- package/dist/extensions.js +2 -2
- package/dist/headless.d.ts +1 -1
- package/dist/headless.js +3 -3
- package/dist/{index-C-Nd2ZbV.d.ts → index-CWBSrqI6.d.ts} +676 -3
- package/dist/index-CWBSrqI6.d.ts.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +13 -13
- package/dist/{login-CMVVnAHS.js → login-CR3fDHXh.js} +3 -3
- package/dist/{login-CMVVnAHS.js.map → login-CR3fDHXh.js.map} +1 -1
- package/dist/{mcp-3GXUBwur.js → mcp-C6IkXNOS.js} +87 -6
- package/dist/mcp-C6IkXNOS.js.map +1 -0
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +3 -2
- package/dist/{messages-C01kKGj0.js → messages-C_H9soi5.js} +2 -3
- package/dist/{messages-C01kKGj0.js.map → messages-C_H9soi5.js.map} +1 -1
- package/dist/{openai-compat-B3c_hzwf.js → openai-compat-CnHeo4Lz.js} +216 -3
- package/dist/openai-compat-CnHeo4Lz.js.map +1 -0
- package/dist/origin-pinned-fetch-CqG8VZIq.js +159 -0
- package/dist/origin-pinned-fetch-CqG8VZIq.js.map +1 -0
- package/dist/output/stream-json.d.ts +1 -1
- package/dist/output/terminal.d.ts +1 -1
- package/dist/plugins-2PMTVcp-.js +1174 -0
- package/dist/plugins-2PMTVcp-.js.map +1 -0
- package/dist/plugins.d.ts +2 -0
- package/dist/plugins.js +2 -0
- package/dist/presets.d.ts +1 -1
- package/dist/presets.js +1 -1
- package/dist/providers/anthropic.d.ts +1 -1
- package/dist/providers/anthropic.js +1 -1
- package/dist/providers/arcee.d.ts +1 -1
- package/dist/providers/arcee.js +1 -1
- package/dist/providers/baseten.d.ts +1 -1
- package/dist/providers/baseten.js +1 -1
- package/dist/providers/cerebras.d.ts +1 -1
- package/dist/providers/cerebras.js +1 -1
- package/dist/providers/local.d.ts +1 -1
- package/dist/providers/local.js +1 -1
- package/dist/providers/openai-compat.d.ts +1 -1
- package/dist/providers/openai-compat.js +1 -1
- package/dist/providers/openai.d.ts +1 -1
- package/dist/providers/openai.js +2 -2
- package/dist/providers/openrouter.d.ts +1 -1
- package/dist/providers/openrouter.js +1 -1
- package/dist/providers/xai.d.ts +1 -1
- package/dist/providers/xai.js +1 -1
- package/dist/{providers-z1vU0b2Q.js → providers-B6mFe7OD.js} +3 -3
- package/dist/{providers-z1vU0b2Q.js.map → providers-B6mFe7OD.js.map} +1 -1
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +4 -4
- package/dist/restate.d.ts +178 -2
- package/dist/restate.d.ts.map +1 -1
- package/dist/restate.js +463 -7
- package/dist/restate.js.map +1 -1
- package/dist/session/sqlite.d.ts +1 -1
- package/dist/{session-cr9VdoEO.js → session-VpLrNaFQ.js} +2 -2
- package/dist/{session-cr9VdoEO.js.map → session-VpLrNaFQ.js.map} +1 -1
- package/dist/session.d.ts +1 -1
- package/dist/session.js +2 -2
- package/dist/{resolve-CZJxds85.js → skills-Cp9vnBwd.js} +1430 -1006
- package/dist/skills-Cp9vnBwd.js.map +1 -0
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +1 -2
- package/dist/{stdio-BvL8HGYW.js → stdio-DzUUkkhS.js} +2 -2
- package/dist/{stdio-BvL8HGYW.js.map → stdio-DzUUkkhS.js.map} +1 -1
- package/dist/{stdio-loader-CjfoqBle.js → stdio-loader-BILyUGmi.js} +2 -2
- package/dist/{stdio-loader-CjfoqBle.js.map → stdio-loader-BILyUGmi.js.map} +1 -1
- package/dist/{tool-formatters-DPQjso_C.d.ts → tool-formatters-gFqQbzPY.d.ts} +2 -2
- package/dist/{tool-formatters-DPQjso_C.d.ts.map → tool-formatters-gFqQbzPY.d.ts.map} +1 -1
- package/dist/tools/fetch-url.d.ts +1 -1
- package/dist/tools/web-search.d.ts +1 -1
- package/dist/tools.d.ts +1 -1
- package/dist/tools.js +1 -2
- package/dist/{transcript-anchors-CgDzYin1.js → transcript-anchors-DEIr0tJC.js} +44 -7
- package/dist/transcript-anchors-DEIr0tJC.js.map +1 -0
- package/dist/{transcript-anchors-BCUpbNxc.d.ts → transcript-anchors-qCs3wZpu.d.ts} +98 -92
- package/dist/transcript-anchors-qCs3wZpu.d.ts.map +1 -0
- package/dist/tui.d.ts +38 -4
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +843 -109
- package/dist/tui.js.map +1 -1
- package/dist/{turn-operations-CAvi8NYt.js → turn-operations-C3IijXMW.js} +2 -2
- package/dist/{turn-operations-CAvi8NYt.js.map → turn-operations-C3IijXMW.js.map} +1 -1
- package/dist/{turn-operations-Bu2Hkinw.d.ts → turn-operations-M9-jUyDd.d.ts} +3 -3
- package/dist/{turn-operations-Bu2Hkinw.d.ts.map → turn-operations-M9-jUyDd.d.ts.map} +1 -1
- package/dist/types-BIarq1qE.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/{xai-BPwMQSuF.js → xai-Du6Wfnlb.js} +2 -2
- package/dist/{xai-BPwMQSuF.js.map → xai-Du6Wfnlb.js.map} +1 -1
- package/docs/PLUGINS.md +340 -0
- package/docs/RESTATE.md +168 -3
- package/docs/SKILL.md +24 -8
- package/package.json +6 -1
- package/dist/agent-8sUadp5M.js.map +0 -1
- package/dist/aliasing-lVFbm81n.js +0 -217
- package/dist/aliasing-lVFbm81n.js.map +0 -1
- package/dist/context-breakdown-Dzo25N45.js +0 -155
- package/dist/context-breakdown-Dzo25N45.js.map +0 -1
- package/dist/edit-utils-EGosADZq.js.map +0 -1
- package/dist/index-C-Nd2ZbV.d.ts.map +0 -1
- package/dist/mcp-3GXUBwur.js.map +0 -1
- package/dist/openai-compat-B3c_hzwf.js.map +0 -1
- package/dist/resolve-CZJxds85.js.map +0 -1
- package/dist/skills-Dd6XCi4s.js +0 -420
- package/dist/skills-Dd6XCi4s.js.map +0 -1
- package/dist/tool-state-DTs-3ZHH.js +0 -653
- package/dist/tool-state-DTs-3ZHH.js.map +0 -1
- package/dist/transcript-anchors-BCUpbNxc.d.ts.map +0 -1
- package/dist/transcript-anchors-CgDzYin1.js.map +0 -1
package/docs/RESTATE.md
CHANGED
|
@@ -196,7 +196,9 @@ On platforms without bidirectional streaming (AWS Lambda, sandbox-reattach runne
|
|
|
196
196
|
|
|
197
197
|
Pass `restateClock(ctx, { coalesce: true })` to collapse most of those round-trips: the clock journals a real anchor once, derives a monotonic logical sequence from it (`anchor`, `anchor+1`, …) without re-journaling, and re-anchors against the real clock only every `reanchorEvery` calls (default 8; tune via `{ coalesce: { reanchorEvery: n } }`). It stays fully replay-deterministic (anchors replay verbatim; the counter is a pure function of call order) and strictly monotonic.
|
|
198
198
|
|
|
199
|
-
The **tradeoff**: timestamps within a re-anchor window are logical — ordered and stable, but clustered near the window's anchor rather than wall-clock-accurate. Run `elapsed` (`endedAt - startedAt`) and per-turn `createdAt` lose precision down to the window granularity; live-only measurements (TTFT, `maxWallMs` budget) are unaffected because the loop reads those from `Date.now()` directly.
|
|
199
|
+
The **tradeoff**: timestamps within a re-anchor window are logical — ordered and stable, but clustered near the window's anchor rather than wall-clock-accurate. Run `elapsed` (`endedAt - startedAt`) and per-turn `createdAt` lose precision down to the window granularity; live-only measurements (TTFT, `maxWallMs` budget) are unaffected because the loop reads those from `Date.now()` directly.
|
|
200
|
+
|
|
201
|
+
Two subagent-specific consequences, both from differencing two coalesced draws: the elapsed figure in the spawn result string the **model reads** (`[sub-agent child-1] Completed in 2 turns (3ms)`) becomes a count of intervening clock draws rather than milliseconds, so a child that ran 40 seconds can report single-digit ms; and `createSpawnTool({ timeoutMs })`'s turn-boundary deadline measures the same logical scale, so it effectively counts draws too and won't fire on wall-clock time. Leave coalescing off if the model needs true durations or the child budget must track real time — hosts can always read the real duration off `ChildRunStats.stats.elapsed` (`spawn:complete` / `AgentStats.children`), which stays live `Date.now()`. Lower `reanchorEvery` ⇒ more accurate timestamps + more suspensions; higher ⇒ fewer suspensions + more clustering. The default (coalescing off) journals every `now()` and keeps wall-accurate timestamps, so opt in per deployment. Provider/tool `ctx.run` calls remain irreducible suspension points — coalescing only addresses the clock.
|
|
200
202
|
|
|
201
203
|
**Gotcha**: when you pair a `restateSessionStore` with a session, the store's own `generateTurnId` runs ahead of `clock.randomUUID()` (the loop short-circuits via `session?.generateTurnId() ?? clock.randomUUID()`). The store derives deterministic IDs from the persisted turn count (`turn_1`, `turn_2`, …) so turn IDs stay stable across replay and across later invocations on the same object key. If you wire a custom store, keep turn ID generation deterministic from persisted state or let the Restate-backed clock provide the fallback UUIDs.
|
|
202
204
|
|
|
@@ -347,6 +349,159 @@ createAgent({
|
|
|
347
349
|
|
|
348
350
|
The scheduler starts concurrency-safe, Restate-wrapped tools in submission order, joins each chunk through `RestatePromise.all()`, and emits tool results back to the model in original call order. Unsafe tools remain barriers. If a safe tool is not wrapped with `restateTool()` / `wrapAgentTools()`, Restate mode runs that chunk sequentially instead of falling back to native promise parallelism. This mode is opt-in so the TUI/headless harness keeps its native sliding-window scheduler.
|
|
349
351
|
|
|
352
|
+
## Subagents
|
|
353
|
+
|
|
354
|
+
`spawn` works under Restate, with one rule: **the spawn tool is never journaled — its child's steps are.**
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
const spawnTool = createSpawnTool({
|
|
358
|
+
persist: true, // child turns + runs land in the parent's session
|
|
359
|
+
shareHandle: true, // no unjournaled per-child sandbox mint
|
|
360
|
+
timeoutMs: 600_000,
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
createAgent({
|
|
364
|
+
provider: restateProvider(myProvider, ctx, { streamCallbacks: 'suppress' }),
|
|
365
|
+
// Pass `spawn` in like any other tool — `wrapAgentTools` leaves it unwrapped.
|
|
366
|
+
tools: wrapAgentTools({ ...tools, spawn: spawnTool }, ctx),
|
|
367
|
+
clock: restateClock(ctx),
|
|
368
|
+
session,
|
|
369
|
+
behavior: restateBehavior({ maxTurns: 8 }),
|
|
370
|
+
})
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### Why the spawn tool is excluded from wrapping
|
|
374
|
+
|
|
375
|
+
A child agent drives the *same* journaled provider (`ctx.provider`) and the *same* wrapped tools (`ctx.tools`) as its parent. If the spawn tool itself were wrapped, the child's first provider call would open a `ctx.run` **inside** the spawn tool's own `ctx.run`. That fails on the retry with `RestateError (570) Found a mismatch between the code paths taken during the previous execution and the paths taken during this execution` — observed against a real server, which is why `test/fixtures/restate-docker` now carries a subagent scenario.
|
|
376
|
+
|
|
377
|
+
`wrapAgentTools` therefore skips any tool declaring `ToolDef.runsNestedAgent` (the built-in `spawn` sets it; host orchestrator tools that call `agent.run` should too). The skip is keyed on that marker, not the tool name, so a host tool that merely happens to be called `spawn` still gets wrapped.
|
|
378
|
+
|
|
379
|
+
What you get instead is better: the child's provider calls and tool calls are journal entries **of the parent's invocation**. A crash mid-subagent replays the child's completed steps from the journal without re-running their bodies; only the unfinished step re-executes. Suspensions inside a child behave the same way.
|
|
380
|
+
|
|
381
|
+
**Escape hatch.** Wrapping a spawn tool explicitly (`restateTool(spawnTool, ctx, …)`) is legal *only* if that child is fully non-durable — a raw provider from a model catalog, unwrapped tools. Then the whole child collapses into ONE journal entry: cheap to replay, but a crash inside the child re-runs the entire child (re-billing every LLM call) and none of its side effects are individually journaled.
|
|
382
|
+
|
|
383
|
+
### What the child inherits
|
|
384
|
+
|
|
385
|
+
| Inherited | How |
|
|
386
|
+
|---|---|
|
|
387
|
+
| `AgentClock` | Automatic. The child stamps `SessionTurn.createdAt`, run start/end, and fallback turn ids from the parent's journaled clock, so a shared session gets byte-identical metadata across attempts. A child on the default clock mints fresh wall-clock values on every replay. |
|
|
388
|
+
| `AgentBehavior` | Automatic, including `toolBatchExecutor` — a child's own concurrency-safe fleets join through `RestatePromise.all()` like the parent's. |
|
|
389
|
+
| Wrapped tools + provider | Automatic (`ctx.tools` / `ctx.provider`) — this is what makes the child's steps journaled at all. |
|
|
390
|
+
| Session | `persist: true`. Child turn ids then come from `restateSessionStore`'s deterministic `(object key, seq)` mint. |
|
|
391
|
+
| Execution handle | `shareHandle: true`. Otherwise every child calls `execution.spawn()` — an unjournaled allocation, so a replayed attempt mints a fresh sandbox and orphans the first. |
|
|
392
|
+
| Durable tool state | **Never.** A child's `ctx.state` is an isolated in-memory map — see the note under Durable tool state below. |
|
|
393
|
+
|
|
394
|
+
### Per-child timeout
|
|
395
|
+
|
|
396
|
+
`createSpawnTool({ timeoutMs })` normally races the child against a `setTimeout`. That is a wall-clock branch: the same child can time out live and not on the replay, and the abort can land mid-stream, tearing a provider call down from inside its own journal entry. When the clock reports `journaled` (as `restateClock` does), the spawn tool leaves the timer off and instead samples the journaled clock at the child's **turn boundaries** — one sample before the run, one per completed turn — aborting the child at a boundary where nothing is in flight. Both mechanisms report `status: 'timeout'`.
|
|
397
|
+
|
|
398
|
+
Deliberately **not** implemented by borrowing `behavior.maxWallMs`, which looks like the natural fit: the loop derives the provider stream watchdogs from the wall budget (`providerWatchdogTimeouts` clamps start / total / idle by it), so a small `timeoutMs` would also shrink the child's TTFT ceiling and abort a live stream inside its `ctx.run`. Against a real server that fails the attempt with `awaitNext already pending` and then `RestateError 570` on the retry. The fixture's deadline scenario exists because that bug shipped for exactly one iteration.
|
|
399
|
+
|
|
400
|
+
Two consequences of the turn-boundary check: a child stuck inside ONE long tool call is not interrupted (use the runtime's own invocation timeout for a hard cut), and the measured elapsed time **spans the crash-to-retry gap**, because the run-start timestamp replays from before the crash. Budget accordingly.
|
|
401
|
+
|
|
402
|
+
### Limits
|
|
403
|
+
|
|
404
|
+
- **Sequential spawns only** with `createSpawnTool` — for parallel children use `restateSpawnTool` (next section), which sidesteps both reasons below by giving each child its own journal. The weaker one: parallel children race on shared session state — the `child-N` label counter and the run-id re-sync both read `session.runs` at mint time — so label / run-id assignment can differ between attempts. The stronger one: children share the parent's *clock instance*, and `restateClock` carries mutable draw state (the `randomUUID` sequence counter, plus the anchor / call counter in `coalesce` mode). Concurrent children interleave those draws, so the same logical turn can draw a different id or timestamp ordinal on a replay — the `JournalMismatchError` class the clock module exists to prevent. `restateBehavior`'s default `maxConcurrentTools: 1` already gives you the safe posture; don't raise it for a turn that can spawn more than once.
|
|
405
|
+
- **Anything that reaches the transcript must come from `ctx.clock`.** The spawn result string is persisted in the parent's turns, and a state write's payload IS compared across attempts — so the elapsed figure in `[sub-agent child-1] Completed in 2 turns (35ms)` is sampled from the clock, not from `stats.elapsed` (which stays live `Date.now()` by design, for measurement accuracy). Getting this wrong fails the invocation at `set state` with `RestateError 570`, and only on an attempt that fails AFTER a child completed — a crash mid-child never produces the string, so the failure hides until a retry happens later in the run. The fixture's retry scenario exists for exactly that reason. The same rule is why the child inherits the clock at all.
|
|
406
|
+
- **The parent's spawn tool call is not itself journaled**, so it re-executes on replay: agent construction, MCP re-use, hook bubbling, and result-string formatting all run again. Keep host `onChildStart` / `spawn:*` handlers idempotent.
|
|
407
|
+
- **Don't set `behavior.maxWallMs` on a Restate-backed agent** (parent or child) unless you mean it: the provider stream watchdogs are clamped by it, so a tight budget aborts an in-flight stream from inside its journal entry rather than stopping cleanly at a turn boundary. Reach for the runtime's own invocation timeout, or `createSpawnTool({ timeoutMs })` for a child.
|
|
408
|
+
|
|
409
|
+
## Parallel subagents
|
|
410
|
+
|
|
411
|
+
`createSpawnTool` runs children in the parent's journal, which is what makes them sequential. `restateSpawnTool` dispatches each child to **its own invocation**, so children fan out through the durable fleet — and a crash in one child retries only that child.
|
|
412
|
+
|
|
413
|
+
```ts
|
|
414
|
+
const spawn = restateSpawnTool({
|
|
415
|
+
// Return the runtime's promise UNTOUCHED — no await, no .then/.catch.
|
|
416
|
+
dispatch: req => ctx.serviceClient(childService).runChild(req),
|
|
417
|
+
// Stop in-flight children when the parent's own loop aborts.
|
|
418
|
+
onAbort: () => ctx.invocation(ctx.request().id).cancel(),
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
createAgent({
|
|
422
|
+
tools: { ...wrapAgentTools(tools, ctx), spawn },
|
|
423
|
+
clock: restateClock(ctx),
|
|
424
|
+
session,
|
|
425
|
+
behavior: restateBehavior({
|
|
426
|
+
maxConcurrentTools: 4, // defaults to 1 — without this there is no fan-out
|
|
427
|
+
toolBatchExecutor: restateToolBatchExecutor({ promises: RestatePromise }),
|
|
428
|
+
}),
|
|
429
|
+
})
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Choosing between the two
|
|
433
|
+
|
|
434
|
+
| | `createSpawnTool` | `restateSpawnTool` |
|
|
435
|
+
|---|---|---|
|
|
436
|
+
| Children per turn | one at a time | parallel (fleet-capped) |
|
|
437
|
+
| Journal | shares the parent's | one per child |
|
|
438
|
+
| Crash in a child | replays the parent frame, re-drives the child | retries only that child |
|
|
439
|
+
| `child:*` events | live (in-process bubbling) | replayed onto the parent bus when the child merges |
|
|
440
|
+
| Host code needed | none | a child handler |
|
|
441
|
+
|
|
442
|
+
### Host obligations
|
|
443
|
+
|
|
444
|
+
- **The child handler must be a `service`**, or an object on a *different* key. A virtual object serializes per key, so children sharing the parent's key queue behind the parent that still holds it — no parallelism, and a deadlock risk.
|
|
445
|
+
- **Raise `maxConcurrentTools` and install the durable batch executor.** The fleet gate is all-or-nothing (`fleet.every(canExecuteDurably)`), so with the default cap of 1 the children serialize silently.
|
|
446
|
+
- **The child runs sessionless** and returns a `ChildRunResult` (`status`, `text`, `turns`, `stats`). The parent stays the single writer: it merges each child's turns under a `SessionRun` labeled in submission order, so the persisted tree matches the in-process tool's.
|
|
447
|
+
- **Enforce depth in the handler** (`req.depth`). The tool always dispatches, because short-circuiting would mean returning a non-durable promise and the fleet rejects those.
|
|
448
|
+
|
|
449
|
+
### What you give up
|
|
450
|
+
|
|
451
|
+
- **`child:*` events fire, but not live** — see the section below.
|
|
452
|
+
- **`spawn:before` is fired without awaiting** — `durableExecute` must return the durable promise synchronously, so listeners on it must not perform journaled I/O.
|
|
453
|
+
- **Merge ordering**: children are merged on `tool-results:after` (with `turn:after` as a backstop for turns that pack no results), so their turns land just *after* the parent's tool-result turn rather than just before it. `createdAt` still orders them correctly for consumers that sort chronologically. If the invocation is torn down before the turn ends at all — a cancellation, say — a child that already settled is never merged and its `spawn:complete` never fires; its work still happened, it just isn't recorded.
|
|
454
|
+
- **The child should report its own `startedAt` / `endedAt`** (measured on its clock, e.g. `restateClock(ctx)`). Those land in the merged `SessionRun`, which is persisted through a journaled state write, so they have to be replay-stable — the parent never samples a clock for them. Omit them and it falls back to the child's first/last turn timestamps, then to the parent's run start.
|
|
455
|
+
- **Per-child timeouts are the callee's job.** Racing the call promise is the forbidden native-combinator pattern; give the child handler its own budget, or use the runtime's `.orTimeout()`.
|
|
456
|
+
- **Cancelling the parent INVOCATION stops the children** — Restate cancels the call tree, so each child halts at its next journal interaction, at any depth. Verified: with two children mid-fan-out, no further work steps started after a `PATCH /invocations/{id}/cancel`. A step already inside a `ctx.run` finishes, since run bodies are not interruptible.
|
|
457
|
+
- **An in-agent `agent.abort()` needs `onAbort`.** On its own, `abort()` unwinds the parent's loop while its invocation keeps running, so nothing tells the children to stop. `onAbort` fires once with the in-flight set; cancel the parent's own invocation from it and the runtime cancels the tree. The cost is that the parent invocation ends too — an in-agent abort becomes "stop everything", so persist any summary BEFORE aborting.
|
|
458
|
+
- **Cancelling individual children is not possible in this tool shape.** The `invocationId` on the runtime's call handle is itself a journal-backed promise, so reading it while the call is in flight registers a second consumer on the SDK's single-consumer progress channel: `awaitNext already pending`, a failed attempt, and a replay on *every* fan-out (measured — 13 spurious attempts across the fixture suite before this was reverted). Per-child cancellation needs a two-phase `send → id → attach` tool, where the id is consumed before the result promise exists.
|
|
459
|
+
|
|
460
|
+
### Child events
|
|
461
|
+
|
|
462
|
+
Hook bubbling can't cross an invocation boundary, so a parallel child's events can't reach the parent's bus *as they happen*. Instead, each child's transcript is **replayed onto the parent's bus when it merges** — so `src/output/terminal.ts`, the GUI engine, and any `child:tool:*` listener render parallel children with no changes. Turn it off with `emitChildEvents: false`.
|
|
463
|
+
|
|
464
|
+
| Fires | From |
|
|
465
|
+
|---|---|
|
|
466
|
+
| `child:stream:text` | assistant text, one chunk per turn |
|
|
467
|
+
| `child:tool:before` | each `tool_call` block, with a run-cumulative `runToolCounts` tally |
|
|
468
|
+
| `child:tool:after` / `child:tool:error` | the matching `tool_result`, split on `isError` |
|
|
469
|
+
| `child:turn:after` | each assistant turn, `usage` straight off the turn |
|
|
470
|
+
| `child:stream:end` | end of each turn |
|
|
471
|
+
|
|
472
|
+
Every event carries `childId` (the reserved label), `depth`, and — on the tool events, which extend `ToolHookContext` — `runId`, `parentRunId`, and `parentCallId` (the spawn call that owns the child). Per-child blocks stay contiguous and follow **submission** order, not settlement order.
|
|
473
|
+
|
|
474
|
+
**Not synthesized**: `tool:gate`, `tool:transform`, `tool:dispatched`, `tool:cancelled`, `background:*`. Those describe decisions taken inside another process; inventing them would be fiction.
|
|
475
|
+
|
|
476
|
+
**They re-fire on a parent replay**, exactly as the in-process tool's `child:tool:*` does — the drain runs live on every attempt. Dedupe by `callId` / `turnId`, or gate on `createReplayTracker`. The fixture pins this rather than asserting it in prose: a post-merge replay emits 24 events for 12 events' worth of work, measured through a file sink that outlives the attempt.
|
|
477
|
+
|
|
478
|
+
#### Nested fan-out
|
|
479
|
+
|
|
480
|
+
A child dispatched to a service runs **sessionless**, so it has nowhere to persist its own children. Left alone, a depth-2 fan-out loses everything below depth 1 — the parent sees the child's `spawn` tool calls and none of the work they did. Hand the subtree up instead:
|
|
481
|
+
|
|
482
|
+
```ts
|
|
483
|
+
const merged: MergedChild[] = []
|
|
484
|
+
const spawn = restateSpawnTool({
|
|
485
|
+
onChildMerged: child => merged.push(child),
|
|
486
|
+
dispatch: req => ctx.serviceClient(childService).runChild(req),
|
|
487
|
+
})
|
|
488
|
+
// … and return it from the handler
|
|
489
|
+
return { status: 'completed', text, turns: agent.turns, stats, children: merged }
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
The merging parent walks that subtree depth-first, **re-labeling** each descendant from its own session counter (every level mints `child-1` locally, so the arriving labels collide), and emits their `child:*` events at the right depth. Verified end to end: parent → one child → two grandchildren lands four runs at depths 0/1/2/2 with labels `child-1` / `child-2` / `child-3`, and the grandchildren's tool events arrive at `depth: 2`.
|
|
493
|
+
|
|
494
|
+
#### If you need it live
|
|
495
|
+
|
|
496
|
+
Publish from the child's own bus inside your handler; the child is an ordinary agent there and its hooks fire normally. `ChildRunRequest` carries `parentRunId`, `parentCallId` and `sessionId` so the child can tag what it publishes and your UI can group it with the parent's own events. Two caveats:
|
|
497
|
+
|
|
498
|
+
- `restateProvider(..., { streamCallbacks: 'suppress' })` no-ops `onText`, so text deltas never fire even inside the child. Drop `suppress` and you get `maxRetryAttempts: 1` instead — otherwise an entry retry re-emits deltas and duplicates text.
|
|
499
|
+
- A child's events re-fire on the *child's* own replay, so the sink must be idempotent.
|
|
500
|
+
|
|
501
|
+
What's deliberately not offered is journaling each child event so the parent can consume it as a journal entry. That is the only live option that stays inside the determinism envelope, and it costs a journal entry per event plus a parent that must stay awake to read them — which defeats the suspension that makes long children cheap. Defensible at coarse milestones, not per token.
|
|
502
|
+
|
|
503
|
+
Every property here is pinned by the fixture against a real server: two children overlapping by ~394ms of their 400ms of work; four children at a cap of two running as two clean waves (max concurrency exactly 2, wall time 1031ms versus 2090ms for the same fan-out at cap 1); a crash inside one child leaving its sibling with exactly one execution; and both children's transcripts arriving on the parent's bus as `child:*` events, in contiguous per-child blocks.
|
|
504
|
+
|
|
350
505
|
## Durable tool state
|
|
351
506
|
|
|
352
507
|
The problem it solves: **a tool cannot keep state in a closure.** A journaled result skips the tool body on replay, so a `consumed = true` written inside `execute` never happens the second time around — while the closure itself was rebuilt from the original input. The tool then re-serves a payload it already delivered.
|
|
@@ -396,7 +551,7 @@ The invariant that makes it replay-safe: **a staged mutation is committed only i
|
|
|
396
551
|
- **`set(key, undefined)` is a deletion.** It is what a read would report anyway, and an undefined value is not JSON-stable across the journal.
|
|
397
552
|
- **Two-phase tools** (`durableStart`) journal their delta on the final entry (`wait.result` / `attach`). A write staged in `start` is lost if the process dies before the wait completes; write state in the final phase when it must be crash-durable.
|
|
398
553
|
- **Keep values small and JSON-serializable.** They cross a journal and land in virtual-object state, which Restate size-caps: flags, ids, counters, cursors — not transcripts.
|
|
399
|
-
- **Not inherited by subagents.** A child you construct yourself can take the same store; `createSpawnTool` has **no** `shareToolState` equivalent to `shareReadState`, so tools running inside a *spawned* subagent get an isolated in-memory map and their writes never reach the parent's store.
|
|
554
|
+
- **Not inherited by subagents.** A child you construct yourself can take the same store; `createSpawnTool` has **no** `shareToolState` equivalent to `shareReadState`, so tools running inside a *spawned* subagent get an isolated in-memory map and their writes never reach the parent's store. This is the one thing a subagent does NOT get under Restate — everything else (journaled clock, wrapped tools/provider, session, handle) is covered in § Subagents. A child's one-shot delivery is therefore not crash-safe; keep such tools in the parent, or hand the child an explicitly constructed agent with the same `toolState` store.
|
|
400
555
|
- **The store is bound to one invocation's `ctx`.** Build it inside the handler, like `restateSessionStore` and `restateProvider`. An agent constructed once at module scope and reused across invocations would commit through a dead context — and never re-hydrate. `toolState` reads like agent-lifetime configuration, which makes this easier to get wrong than the other ctx-bound wrappers.
|
|
401
556
|
- **The stored map is a schema you own.** It is one JSON object under one state key, with no versioning or migration built in. Renaming a key or changing a value's shape has to tolerate the old value still being there from a previous deploy; `restateToolStateStore(ctx, { key })` gives you a clean namespace when a migration is easier than a tolerant read.
|
|
402
557
|
|
|
@@ -589,7 +744,17 @@ A runtime with a hard per-invocation CPU/wall cap (Cloudflare's ~5-minute limit)
|
|
|
589
744
|
|
|
590
745
|
`test/tool-state.test.ts` covers the runtime-agnostic half — snapshot hydration, staging, commit-on-settle, the size guard, the commit-failure hook, the in-memory default, and the loop round-trip.
|
|
591
746
|
|
|
592
|
-
`bun run restate:fixture:test` is the end-to-end suite against a **real** Restate server in Docker, and is deliberately **not** part of CI — run it locally when you touch the adapter. It boots the container, supervises the fixture service, and drives
|
|
747
|
+
`bun run restate:fixture:test` is the end-to-end suite against a **real** Restate server in Docker, and is deliberately **not** part of CI — run it locally when you touch the adapter. It boots the container, supervises the fixture service, and drives nineteen scenarios through the real ingress.
|
|
748
|
+
|
|
749
|
+
Five cover the adapter itself: baseline agent run; crash → replay (the service process is killed mid-invocation and respawned, so the journal, not a mock, decides what re-runs); per-object-key state isolation; commit-after-soft-failure; and an awakeable suspend/resume.
|
|
750
|
+
|
|
751
|
+
Five cover **subagents**, each pinned on an on-disk ledger that has to outlive the process: a crash inside the child; an awakeable park inside the child (suspend → external resume, with the child's `register` phase required to fire exactly once, or the resumed awakeable id would be one nobody resolves); a depth-2 grandchild crash, two un-journaled spawn frames deep; a journaled-deadline trip; and an attempt that fails **without** a process death, so the journal replays against warm module state — that one asserts an unchanged boot count to prove it took the retry path and not the crash path.
|
|
752
|
+
|
|
753
|
+
Eight cover **parallel** subagents (`restateSpawnTool`). Two children as separate invocations, asserted to have *overlapping* work intervals — counting executions can't tell parallel from sequential. A crash inside one of them, where the surviving sibling must show exactly one execution: proof that the children really are journal-isolated. Then the scheduling pair: four children in one turn against a fleet cap of two, where the interval ledger must show a maximum of exactly **two** open at once (3+ means the cap is ignored, 1 means the fleet silently fell back to sequential), and the same fan-out at cap 1 as a control, where nothing may overlap. Measured: 1031ms wall at cap 2 versus 2090ms at cap 1 for identical work.
|
|
754
|
+
|
|
755
|
+
The remaining three cover the failure modes that decide whether this is production-safe. **A parent crash mid-fan-out**: the parent dies while both children are still working, and on replay must re-attach to the running invocations rather than dispatch new ones — exactly one execution per child, even though the parent frame ran twice. **Depth-2 fan-out**: a child that itself spawns two grandchildren, whose intervals must overlap, so parallelism survives nesting. **Cancellation**, twice: once from outside (the parent invocation is cancelled mid-flight and no child may start another work step) and once from inside (`agent.abort()` with `onAbort` wired, where children finish the step in flight and never begin the next). **A post-merge replay**: the fan-out completes and merges, then the handler fails once, so the parent replays *after* the merge — the only scenario that re-executes the merge path, and the one that caught wall-clock timestamps leaking into the merged run records.
|
|
756
|
+
|
|
757
|
+
Both subagent bugs found in review — the `maxWallMs` watchdog clamp and the live `elapsed` inside a persisted string — were caught here and nowhere else. The suite fails if the crash never forced a respawn, so it cannot pass without actually exercising replay. This is the only coverage where replay, suspension, and virtual-object state semantics come from the SDK rather than from our reading of it.
|
|
593
758
|
|
|
594
759
|
`test/start.test.ts` covers the local `bun start --restate` runner helpers: option parsing, deployment registration, ingress invocation timeout wiring, readiness/service probing, runtime-state cleanup, cached completion, changed-input rejection, and `status` / `history` handlers.
|
|
595
760
|
|
package/docs/SKILL.md
CHANGED
|
@@ -262,10 +262,22 @@ interface ToolDef {
|
|
|
262
262
|
) => Promise<string | ToolResultContent[]>
|
|
263
263
|
/** Default `false`. See "Per-tool concurrency" below. */
|
|
264
264
|
isConcurrencySafe?: boolean | ((input: Record<string, unknown>) => boolean)
|
|
265
|
+
/**
|
|
266
|
+
* Set it when `execute` runs a full nested agent loop (calls `agent.run`).
|
|
267
|
+
* The built-in `spawn` does. Inert outside durable execution; under a durable
|
|
268
|
+
* runtime it is load-bearing — `wrapAgentTools` keeps such tools out of its
|
|
269
|
+
* journaling wrapper, because the child's own journaled calls cannot nest
|
|
270
|
+
* inside the outer tool's journal entry. Miss it on a host orchestrator tool
|
|
271
|
+
* and the invocation fails on its first retry. See `docs/RESTATE.md`
|
|
272
|
+
* § Subagents.
|
|
273
|
+
*/
|
|
274
|
+
runsNestedAgent?: boolean
|
|
265
275
|
}
|
|
266
276
|
```
|
|
267
277
|
|
|
268
|
-
`ToolContext` exposes `provider`, `signal`, `execution`, `handle`, `hooks`, `turnId`, `callId`, `runId?`, `parentRunId?`, `session?`, `readState?`, `depth?`, plus preset fields (`name`, `system`, `tools`, `toolAliases`, `mcpServers`, `skills`, `behavior`) so `spawn` can inherit.
|
|
278
|
+
`ToolContext` exposes `provider`, `signal`, `execution`, `handle`, `hooks`, `turnId`, `callId`, `runId?`, `parentRunId?`, `parentCallId?`, `session?`, `readState?`, `clock?`, `depth?`, plus preset fields (`name`, `system`, `tools`, `toolAliases`, `mcpServers`, `skills`, `behavior`) so `spawn` can inherit.
|
|
279
|
+
|
|
280
|
+
`clock` is the run's time / UUID source (`AgentClock`) — prefer it over `Date.now()` / `crypto.randomUUID()` for anything that gets persisted or shown to the model, because a durable host replaces it with a journaled one so those values replay byte-identically. If you write your own durable adapter, set `AgentClock.journaled: true` on the clock you install: consumers read it to avoid deciding on a wall-clock timer inside a journaled region (the `spawn` tool switches its per-child timeout to a turn-boundary check on that signal).
|
|
269
281
|
|
|
270
282
|
Returns `string` (common) or `ToolResultContent[]` (images / mixed content). Routed natively via `tool_result.content` when supported, or via companion user message on plain Chat Completions. Flatten via `toolResultToText(result)`.
|
|
271
283
|
|
|
@@ -372,7 +384,7 @@ For post-edit validation, use `createMutationValidationHooks()` from `zidane`. I
|
|
|
372
384
|
| `session:turns` | Turns persisted | — | `sessionId, turns, count` |
|
|
373
385
|
| `session:save` | Session store write | — | `sessionId` |
|
|
374
386
|
| `session:meta` | Session metadata write | — | `sessionId, key: string, value: unknown` |
|
|
375
|
-
| `spawn:before`, `spawn:complete`, `spawn:error` | Sub-agent lifecycle | — | `id, task, stats?, error?` |
|
|
387
|
+
| `spawn:before`, `spawn:complete`, `spawn:error` | Sub-agent lifecycle | — | `id, task, callId?, stats?, error?` (`callId` = the spawn call's own id, i.e. the child's `parentCallId`) |
|
|
376
388
|
| `session:start`, `session:end` | Run boundaries (always fire) | — | `sessionId, runId` + (`prompt` \| `status, turnRange`) |
|
|
377
389
|
| `agent:abort` | `agent.abort()` called | — | `{}` |
|
|
378
390
|
| `agent:done` | Run finished (all exit paths) | — | `AgentStats` |
|
|
@@ -385,13 +397,14 @@ Mutable ctx fields: `tool:gate` (`block`, `reason`, `result`), `tool:transform`
|
|
|
385
397
|
|
|
386
398
|
### Identity fields on tool hook contexts
|
|
387
399
|
|
|
388
|
-
Every `tool:*` and `mcp:tool:*` hook ctx additionally carries
|
|
400
|
+
Every `tool:*` and `mcp:tool:*` hook ctx additionally carries four optional identity fields (omitted from the per-row "key ctx fields" column above to keep the table readable):
|
|
389
401
|
|
|
390
402
|
- `runId?: string` — the `SessionRun.id` this call belongs to. Lets one listener disambiguate calls across parallel runs / subagent trees without subscribing to `child:tool:*`.
|
|
391
|
-
- `parentRunId?: string` — set when the call's agent is a subagent (mirrors `SessionRun.parentRunId`); absent on top-level runs. Drives UI grouping for subagent-scoped state (todowrite batches, edit groups, etc.) without resolving the run row.
|
|
403
|
+
- `parentRunId?: string` — set when the call's agent is a subagent (mirrors `SessionRun.parentRunId`); absent on top-level runs. Drives UI grouping for subagent-scoped state (todowrite batches, edit groups, etc.) without resolving the run row. Only wired when the spawn shares the parent's session (`persist`) — for grouping that must hold on every subagent, use `parentCallId`.
|
|
404
|
+
- `parentCallId?: string` — the `callId` of the `spawn` call that created this run; absent on top-level runs. Set for every child the built-in `spawn` tool starts (it forwards its own call id; no session required), so hosts mirroring subagent work into a client protocol can use it as the "parent tool use" grouping key — absent at depth 0, a real tool-call id at depth ≥ 1. Two qualifications: it is not implied by `depth >= 1` (a host that spawns by calling `agent.run({ depth })` itself must also pass `AgentRunOptions.parentCallId`), and it resolves against the parent's own `tool:*` stream only at depth 1 — at depth ≥ 2 it names a call in the child's turn, which the top-level listener saw only as `child:tool:*`. Same ancestry semantics as `childId` / `depth` on `child:*` events. The spawn payloads (`spawn:before` / `:complete` / `:error`) carry the same id as `callId`, which is where a host learns the `childId` → call mapping for a child that never dispatches a tool.
|
|
392
405
|
- `depth?: number` — subagent depth, 0 = top-level. Mirrors `ToolContext.depth`.
|
|
393
406
|
|
|
394
|
-
|
|
407
|
+
All four land on `ToolContext` as well (visible to the tool body via `ctx.runId` / `ctx.parentRunId` / `ctx.parentCallId` / `ctx.depth`). The loop centralizes their construction in `buildToolHookBase`, so a new tool-related hook event automatically inherits them.
|
|
395
408
|
|
|
396
409
|
### Child (sub-agent) bubble events
|
|
397
410
|
|
|
@@ -599,14 +612,17 @@ definePreset({
|
|
|
599
612
|
})
|
|
600
613
|
```
|
|
601
614
|
|
|
602
|
-
### Parent/child boundaries — `persist
|
|
615
|
+
### Parent/child boundaries — `persist`, `shareReadState`, `shareHandle`
|
|
603
616
|
|
|
604
|
-
|
|
617
|
+
Three **independent** flags control what crosses the parent/child boundary:
|
|
605
618
|
|
|
606
619
|
| Flag | Default | What it shares |
|
|
607
620
|
|---|---|---|
|
|
608
621
|
| `persist` | `false` | The parent's `Session`. Child turns get appended to the same `session.turns` stream with the child's `runId`; the child's `SessionRun` carries `parentRunId` so the tree is reconstructible. As a side-effect the read-state map (Session-keyed) is also shared. |
|
|
609
622
|
| `shareReadState` | `false` | Only the read-state map. The child gets `AgentOptions.readState` populated with the parent's resolved map but **no session** — `read_file` populations land in the parent's tracking, but child turns stay in-memory. |
|
|
623
|
+
| `shareHandle` | `false` | The parent's `ExecutionHandle`. The child skips its own `execution.spawn()` and never destroys the handle (host owns the lifecycle), so children see the parent's filesystem and background-task namespace instead of an isolated scratch space. Matters most for remote contexts (sandbox / container), where a per-child allocation is real — and under a durable runtime is an unjournaled side effect that orphans a sandbox on every replayed attempt. See `docs/RESTATE.md` § Subagents. |
|
|
624
|
+
|
|
625
|
+
Always inherited (not flags): the parent's `AgentClock`, so a durable host's journaled time/UUID source stamps child metadata too, and `AgentBehavior`, which carries the durable tool-batch scheduler.
|
|
610
626
|
|
|
611
627
|
Use `shareReadState: true` (without `persist`) when you want the parent's `requireReadBeforeEdit` gate to honor reads the child performed, without polluting the parent's session with child turns. The gate's error message now hints at this when the entry is missing — re-read in the calling context OR opt into one of the sharing flags.
|
|
612
628
|
|
|
@@ -918,7 +934,7 @@ createSandboxContext(myProvider) // implement SandboxProvider (E2B, Rivet, …)
|
|
|
918
934
|
| `RunHookMap` | `run({ hooks })` shape. |
|
|
919
935
|
| `AgentBehavior.maxConcurrentTools` | Cap on in-flight tools per turn (default `10`, clamped at `1`). See "Per-tool concurrency" above. |
|
|
920
936
|
| `ToolDef.isConcurrencySafe` | `boolean \| ((input) => boolean)`. Defaults to `false`. See "Per-tool concurrency" above. |
|
|
921
|
-
| `ToolHookContext` / `McpToolHookContext` / `SessionHookContext` / `StreamHookContext` / `SpawnHookContext` | Hook ctx base types. `ToolHookContext` / `McpToolHookContext` carry `runId? / parentRunId? / depth?` identity fields on every `tool:*` / `mcp:tool:*` event — see Identity fields above. |
|
|
937
|
+
| `ToolHookContext` / `McpToolHookContext` / `SessionHookContext` / `StreamHookContext` / `SpawnHookContext` | Hook ctx base types. `ToolHookContext` / `McpToolHookContext` carry `runId? / parentRunId? / parentCallId? / depth?` identity fields on every `tool:*` / `mcp:tool:*` event — see Identity fields above. |
|
|
922
938
|
| `ReadStateMap` / `ReadStateEntry` | Per-file read-tracking map (`Map<canonicalAbsPath, { contentHash, offset, limit, maxBytes, lineNumbers?, mtimeMs }>`). Construct one manually for `AgentOptions.readState` to share across an agent tree; or get the session-bound one via `getReadState(session)` / the lookup-aware `resolveReadStateMap(ctx)`. Canonical keys come from `readStateKey(cwd, path)`. |
|
|
923
939
|
| `ClassifiedError` | `{ kind, providerCode?, message? }` from `Provider.classifyError`. |
|
|
924
940
|
| `AgentContextExceededError` / `AgentProviderError` / `AgentAbortedError` / `AgentToolNotAllowedError` / `AgentBudgetExceededError` | Typed errors. `toTypedError(err)` wraps native errors. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zidane",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.5.1",
|
|
4
4
|
"description": "an agent that goes straight to the goal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -171,6 +171,11 @@
|
|
|
171
171
|
"import": "./dist/skills.js",
|
|
172
172
|
"default": "./dist/skills.js"
|
|
173
173
|
},
|
|
174
|
+
"./plugins": {
|
|
175
|
+
"types": "./dist/plugins.d.ts",
|
|
176
|
+
"import": "./dist/plugins.js",
|
|
177
|
+
"default": "./dist/plugins.js"
|
|
178
|
+
},
|
|
174
179
|
"./extensions": {
|
|
175
180
|
"types": "./dist/extensions.d.ts",
|
|
176
181
|
"import": "./dist/extensions.js",
|