zidane 6.4.4 → 6.5.0

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.
Files changed (99) hide show
  1. package/README.md +1 -0
  2. package/dist/{acp-BLO58K3F.js → acp-B-AOovHz.js} +2 -2
  3. package/dist/{acp-BLO58K3F.js.map → acp-B-AOovHz.js.map} +1 -1
  4. package/dist/acp-cli.js +3 -2
  5. package/dist/acp-cli.js.map +1 -1
  6. package/dist/acp.d.ts +1 -1
  7. package/dist/acp.js +1 -1
  8. package/dist/{agent-8sUadp5M.js → agent-SQ6rxfpT.js} +231 -106
  9. package/dist/agent-SQ6rxfpT.js.map +1 -0
  10. package/dist/agent.d.ts +1 -1
  11. package/dist/agent.js +1 -1
  12. package/dist/chat/pure.d.ts +3 -3
  13. package/dist/chat.d.ts +13 -44
  14. package/dist/chat.d.ts.map +1 -1
  15. package/dist/chat.js +5 -4
  16. package/dist/chat.js.map +1 -1
  17. package/dist/{context-transport-D29WvHcm.js → context-transport-CDBatvQc.js} +3 -3
  18. package/dist/{context-transport-D29WvHcm.js.map → context-transport-CDBatvQc.js.map} +1 -1
  19. package/dist/contexts/daytona.d.ts +1 -1
  20. package/dist/contexts/e2b.d.ts +1 -1
  21. package/dist/eval.d.ts +1 -1
  22. package/dist/extensions.d.ts +2 -2
  23. package/dist/extensions.js +2 -2
  24. package/dist/headless.d.ts +1 -1
  25. package/dist/headless.js +1 -1
  26. package/dist/{index-C-Nd2ZbV.d.ts → index-CWBSrqI6.d.ts} +676 -3
  27. package/dist/index-CWBSrqI6.d.ts.map +1 -0
  28. package/dist/index.d.ts +2 -2
  29. package/dist/index.js +7 -6
  30. package/dist/index.js.map +1 -1
  31. package/dist/{login-CMVVnAHS.js → login-XZDS4K2o.js} +3 -3
  32. package/dist/{login-CMVVnAHS.js.map → login-XZDS4K2o.js.map} +1 -1
  33. package/dist/{mcp-3GXUBwur.js → mcp-D27rP47I.js} +87 -6
  34. package/dist/mcp-D27rP47I.js.map +1 -0
  35. package/dist/mcp.d.ts +2 -2
  36. package/dist/mcp.js +3 -2
  37. package/dist/origin-pinned-fetch-CqG8VZIq.js +159 -0
  38. package/dist/origin-pinned-fetch-CqG8VZIq.js.map +1 -0
  39. package/dist/output/stream-json.d.ts +1 -1
  40. package/dist/output/terminal.d.ts +1 -1
  41. package/dist/plugins-2PMTVcp-.js +1174 -0
  42. package/dist/plugins-2PMTVcp-.js.map +1 -0
  43. package/dist/plugins.d.ts +2 -0
  44. package/dist/plugins.js +2 -0
  45. package/dist/presets.d.ts +1 -1
  46. package/dist/presets.js +1 -1
  47. package/dist/providers/anthropic.d.ts +1 -1
  48. package/dist/providers/arcee.d.ts +1 -1
  49. package/dist/providers/baseten.d.ts +1 -1
  50. package/dist/providers/cerebras.d.ts +1 -1
  51. package/dist/providers/local.d.ts +1 -1
  52. package/dist/providers/openai-compat.d.ts +1 -1
  53. package/dist/providers/openai.d.ts +1 -1
  54. package/dist/providers/openrouter.d.ts +1 -1
  55. package/dist/providers/xai.d.ts +1 -1
  56. package/dist/providers.d.ts +1 -1
  57. package/dist/restate.d.ts +1 -1
  58. package/dist/restate.js +5 -3
  59. package/dist/restate.js.map +1 -1
  60. package/dist/session/sqlite.d.ts +1 -1
  61. package/dist/session.d.ts +1 -1
  62. package/dist/{resolve-CZJxds85.js → skills-Cp9vnBwd.js} +1430 -1006
  63. package/dist/skills-Cp9vnBwd.js.map +1 -0
  64. package/dist/skills.d.ts +1 -1
  65. package/dist/skills.js +1 -2
  66. package/dist/{stdio-BvL8HGYW.js → stdio-O-m9WVkE.js} +2 -2
  67. package/dist/{stdio-BvL8HGYW.js.map → stdio-O-m9WVkE.js.map} +1 -1
  68. package/dist/{stdio-loader-CjfoqBle.js → stdio-loader-BSGQmG4L.js} +2 -2
  69. package/dist/{stdio-loader-CjfoqBle.js.map → stdio-loader-BSGQmG4L.js.map} +1 -1
  70. package/dist/{tool-formatters-DPQjso_C.d.ts → tool-formatters-gFqQbzPY.d.ts} +2 -2
  71. package/dist/{tool-formatters-DPQjso_C.d.ts.map → tool-formatters-gFqQbzPY.d.ts.map} +1 -1
  72. package/dist/tools/fetch-url.d.ts +1 -1
  73. package/dist/tools/web-search.d.ts +1 -1
  74. package/dist/tools.d.ts +1 -1
  75. package/dist/tools.js +1 -1
  76. package/dist/{transcript-anchors-CgDzYin1.js → transcript-anchors-D4IxJifV.js} +42 -5
  77. package/dist/transcript-anchors-D4IxJifV.js.map +1 -0
  78. package/dist/{transcript-anchors-BCUpbNxc.d.ts → transcript-anchors-qCs3wZpu.d.ts} +98 -92
  79. package/dist/transcript-anchors-qCs3wZpu.d.ts.map +1 -0
  80. package/dist/tui.d.ts +38 -4
  81. package/dist/tui.d.ts.map +1 -1
  82. package/dist/tui.js +839 -105
  83. package/dist/tui.js.map +1 -1
  84. package/dist/{turn-operations-Bu2Hkinw.d.ts → turn-operations-M9-jUyDd.d.ts} +3 -3
  85. package/dist/{turn-operations-Bu2Hkinw.d.ts.map → turn-operations-M9-jUyDd.d.ts.map} +1 -1
  86. package/dist/types-BIarq1qE.js.map +1 -1
  87. package/dist/types.d.ts +1 -1
  88. package/docs/PLUGINS.md +340 -0
  89. package/docs/RESTATE.md +69 -3
  90. package/docs/SKILL.md +24 -8
  91. package/package.json +6 -1
  92. package/dist/agent-8sUadp5M.js.map +0 -1
  93. package/dist/index-C-Nd2ZbV.d.ts.map +0 -1
  94. package/dist/mcp-3GXUBwur.js.map +0 -1
  95. package/dist/resolve-CZJxds85.js.map +0 -1
  96. package/dist/skills-Dd6XCi4s.js +0 -420
  97. package/dist/skills-Dd6XCi4s.js.map +0 -1
  98. package/dist/transcript-anchors-BCUpbNxc.d.ts.map +0 -1
  99. package/dist/transcript-anchors-CgDzYin1.js.map +0 -1
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { d as ClassifiedErrorKind, i as AgentContextExceededError, l as CONTEXT_EXCEEDED_MESSAGE_PATTERNS, n as AgentAbortedError, o as AgentProviderError, p as matchesContextExceeded, r as AgentBudgetExceededError, s as AgentToolNotAllowedError, u as ClassifiedError } from "./timeout-DEaqslLO.js";
2
- import { $m as ToolDef, $p as DEFAULT_AGENT_CLOCK, Ah as SessionRun, As as SpawnToolOptions, Bm as ToolResultDocumentContent, C as SuccessfulMutationOutcome, Cm as RunHookMap, Dh as Session, Dm as SessionMessage, Em as SessionHookContext, Gi as AgentHookMap, Gm as ToolResultTextContent, Hm as ToolResultImageContent, Hp as AgentClock, Im as ToolHookContext, Jm as TurnUsage, Jp as ChildRunStats, Kf as OpenAIParams, Ki as AgentHooks, Km as ToolResultVideoContent, Lm as ToolOutcome, Mm as StreamHookContext, Oh as SessionData, Om as SessionTurn, Pm as ThinkingLevel, Qm as ToolContext, Rm as ToolResultAudioContent, Rr as InteractionToolOptions, S as MutationValidationResult, Sf as OpenRouterParams, Sh as SkillResource, Sm as RetryConfig, Th as CreateSessionOptions, Tm as SessionEndStatus, Ts as ChildAgent, Um as ToolResultImageRefContent, Up as AgentRunOptions, Vh as RemoteStoreOptions, Vm as ToolResultDocumentRefContent, Vp as AgentBehavior, Wi as Agent, Wm as ToolResultMetadata, Wp as AgentStats, Xm as toolOutputByteLength, Ym as toolOutputBudgetByteLength, Zm as toolResultToText, _ as statsByModel, _m as PromptVideoPart, af as StreamCallbacks, am as McpToolHookContext, b as MutationValidationHooksOptions, bg as McpConnection, bh as SkillConfig, bm as ResolveContentRef, cf as ToolCall, df as TurnResult, dh as StagedToolState, dm as ProgressNudgeMessage, eh as ToolMap, fh as ToolStateController, fm as PromptAudioPart, gm as PromptTextPart, h as flattenTurns, hh as ToolStateView, hm as PromptPart, if as ProviderCapabilities, im as McpServerConfig, ip as CerebrasParams, jh as SessionStore, js as SpawnToolState, km as SpawnHookContext, lf as ToolResult, lm as OAuthRefreshHookContext, mh as ToolStateStore, mm as PromptImagePart, nh as ReadStateEntry, of as StreamOptions, p as ModelUsage, ph as ToolStateOp, pm as PromptDocumentPart, pp as AnthropicParams, qi as AgentOptions, qm as TurnFinishReason, rf as Provider, rh as ReadStateMap, t as Preset, uf as ToolSpec, um as ProgressNudgeKind, v as MutationValidationBatch, vm as RepeatGuardConfig, vr as ValidationResult, wh as SkillsConfig, wm as SessionContentBlock, x as MutationValidationMutation, xm as ResolveContentRefBlock, y as MutationValidationFailure, ym as RepeatGuardToolMatcher, zm as ToolResultContent } from "./index-C-Nd2ZbV.js";
2
+ import { $h as SkillsConfig, $m as SessionContentBlock, Bf as TurnResult, Bh as StagedToolState, Bm as ProgressNudgeMessage, C as SuccessfulMutationOutcome, Ch as TurnUsage, Cm as ChildRunStats, Dh as ToolContext, Eh as toolResultToText, Ff as StreamOptions, Gi as AgentHookMap, Gm as PromptTextPart, Hh as ToolStateOp, Hm as PromptDocumentPart, Hp as AnthropicParams, Jm as RepeatGuardToolMatcher, Ki as AgentHooks, Km as PromptVideoPart, Lf as ToolCall, Mf as Provider, Mh as ReadStateMap, Nf as ProviderCapabilities, Nm as McpServerConfig, Np as CerebrasParams, Ns as SpawnToolOptions, Oh as ToolDef, Om as DEFAULT_AGENT_CLOCK, Os as ChildAgent, Pf as StreamCallbacks, Pm as McpToolHookContext, Ps as SpawnToolState, Qm as RunHookMap, Rf as ToolResult, Rm as OAuthRefreshHookContext, Rr as InteractionToolOptions, S as MutationValidationResult, Sh as TurnFinishReason, Th as toolOutputByteLength, Uh as ToolStateStore, Um as PromptImagePart, Vh as ToolStateController, Vm as PromptAudioPart, Wh as ToolStateView, Wi as Agent, Wm as PromptPart, Xm as ResolveContentRefBlock, Yg as McpConnection, Yh as SkillConfig, Ym as ResolveContentRef, Zf as OpenRouterParams, Zh as SkillResource, Zm as RetryConfig, _ as statsByModel, _h as ToolResultImageContent, _m as AgentClock, ag as SessionRun, b as MutationValidationHooksOptions, bh as ToolResultTextContent, dh as ToolHookContext, eg as CreateSessionOptions, eh as SessionEndStatus, fh as ToolOutcome, gg as RemoteStoreOptions, gh as ToolResultDocumentRefContent, gm as AgentBehavior, h as flattenTurns, hh as ToolResultDocumentContent, ih as SpawnHookContext, jh as ReadStateEntry, kh as ToolMap, lh as ThinkingLevel, mh as ToolResultContent, ng as Session, nh as SessionMessage, og as SessionStore, p as ModelUsage, ph as ToolResultAudioContent, qi as AgentOptions, qm as RepeatGuardConfig, rg as SessionData, rh as SessionTurn, sh as StreamHookContext, t as Preset, th as SessionHookContext, v as MutationValidationBatch, vh as ToolResultImageRefContent, vm as AgentRunOptions, vr as ValidationResult, wh as toolOutputBudgetByteLength, x as MutationValidationMutation, xh as ToolResultVideoContent, xp as OpenAIParams, y as MutationValidationFailure, yh as ToolResultMetadata, ym as AgentStats, zf as ToolSpec, zm as ProgressNudgeKind } from "./index-CWBSrqI6.js";
3
3
  import { _ as TaskHandle, a as ContextType, c as ExecutionContext, g as TaskExitInfo, h as TaskEntry, l as ExecutionHandle, n as ContextCapabilities, o as DetachedTasksCapability, p as SpawnConfig, r as ContextHardening, s as ExecResult, t as BackgroundTaskStatus, v as TaskStallInfo } from "./types-M2DauK2D.js";
4
4
  import { t as SandboxProvider } from "./sandbox-DLY6uah7.js";
5
5
  export { type Agent, AgentAbortedError, type AgentBehavior, AgentBudgetExceededError, type AgentClock, AgentContextExceededError, type AgentHookMap, type AgentHooks, type AgentOptions, AgentProviderError, type AgentRunOptions, type AgentStats, AgentToolNotAllowedError, type AnthropicParams, type BackgroundTaskStatus, CONTEXT_EXCEEDED_MESSAGE_PATTERNS, type CerebrasParams, type ChildAgent, type ChildRunStats, type ClassifiedError, type ClassifiedErrorKind, type ContextCapabilities, type ContextHardening, type ContextType, type CreateSessionOptions, DEFAULT_AGENT_CLOCK, type DetachedTasksCapability, type ExecResult, type ExecutionContext, type ExecutionHandle, type InteractionToolOptions, type McpConnection, type McpServerConfig, type McpToolHookContext, type ModelUsage, type MutationValidationBatch, type MutationValidationFailure, type MutationValidationHooksOptions, type MutationValidationMutation, type MutationValidationResult, type OAuthRefreshHookContext, type OpenAIParams, type OpenRouterParams, type Preset, type ProgressNudgeKind, type ProgressNudgeMessage, type PromptAudioPart, type PromptDocumentPart, type PromptImagePart, type PromptPart, type PromptTextPart, type PromptVideoPart, type Provider, type ProviderCapabilities, type ReadStateEntry, type ReadStateMap, type RemoteStoreOptions, type RepeatGuardConfig, type RepeatGuardToolMatcher, type ResolveContentRef, type ResolveContentRefBlock, type RetryConfig, type RunHookMap, type SandboxProvider, type Session, type SessionContentBlock, type SessionData, type SessionEndStatus, type SessionHookContext, type SessionMessage, type SessionRun, type SessionStore, type SessionTurn, type SkillConfig, type SkillResource, type SkillsConfig, type SpawnConfig, type SpawnHookContext, type SpawnToolOptions, type SpawnToolState, type StagedToolState, type StreamCallbacks, type StreamHookContext, type StreamOptions, type SuccessfulMutationOutcome, type TaskEntry, type TaskExitInfo, type TaskHandle, type TaskStallInfo, type ThinkingLevel, type ToolCall, type ToolContext, type ToolDef, type ToolHookContext, type ToolMap, type ToolOutcome, type ToolResult, type ToolResultAudioContent, type ToolResultContent, type ToolResultDocumentContent, type ToolResultDocumentRefContent, type ToolResultImageContent, type ToolResultImageRefContent, type ToolResultMetadata, type ToolResultTextContent, type ToolResultVideoContent, type ToolSpec, type ToolStateController, type ToolStateOp, type ToolStateStore, type ToolStateView, type TurnFinishReason, type TurnResult, type TurnUsage, type ValidationResult, flattenTurns, matchesContextExceeded, statsByModel, toolOutputBudgetByteLength, toolOutputByteLength, toolResultToText };
@@ -0,0 +1,340 @@
1
+ # Agent Plugins
2
+
3
+ Support for [Agent Plugins](https://agent-plugins.org) 1.0.0 — an open, vendor-neutral package format for reusable agent components. A plugin is a directory with a manifest and its components at fixed locations; any conformant client can load it.
4
+
5
+ ```
6
+ my-plugin/
7
+ ├── plugin.json manifest (required)
8
+ ├── skills/ one directory per Agent Skill
9
+ │ └── deploy/
10
+ │ └── SKILL.md
11
+ └── mcp.json MCP server configuration
12
+ ```
13
+
14
+ ```ts
15
+ import { loadAgentPlugin } from 'zidane/plugins'
16
+
17
+ const plugin = await loadAgentPlugin('./my-plugin')
18
+ if (plugin.ok) {
19
+ console.log(plugin.manifest.name, plugin.skills.length, plugin.mcpServers.length)
20
+ }
21
+ ```
22
+
23
+ Agent Plugins deliberately does **not** redefine either component format — skills are Agent Skills, MCP config is MCP config. So `zidane/plugins` owns only the packaging contract and hands back plain `SkillConfig[]` / `McpServerConfig[]` that every other zidane surface already consumes.
24
+
25
+ ## Why a separate loader
26
+
27
+ zidane already reads both component types, but both existing readers are deliberately *lenient*: `discoverSkills` recurses into nested category directories, and `normalizeMcpServers` infers a transport from the presence of `command` / `url` and accepts `httpUrl` / `sseUrl` / `type: "http"` aliases. That leniency is correct for a file the user wrote, and wrong for a third-party package — guessing on behalf of a stranger's `mcp.json` is how you spawn a command the author never wrote.
28
+
29
+ `zidane/plugins` is the strict layer: closed schemas, immediate-children-only discovery, and a filesystem boundary. It never loosens the existing readers; it wraps them.
30
+
31
+ ## Discovery
32
+
33
+ Plugins are found in four locations, in precedence order — the same project/user pair every other zidane discovery surface uses:
34
+
35
+ 1. `{project}/.agents/plugins/`
36
+ 2. `{project}/.zidane/plugins/`
37
+ 3. `~/.agents/plugins/`
38
+ 4. `~/.zidane/plugins/`
39
+
40
+ Each **immediate child directory** is one plugin. Nothing is searched below that, and a child with no `plugin.json` is silently ignored — dropping a README next to your plugins is not an error.
41
+
42
+ `.agents/plugins/` is the portable location: Agent Plugins is vendor-neutral, so there is no foreign-agent or Ruler import variant the way there is for skills and MCP servers.
43
+
44
+ Identity is the **manifest** name, not the directory name. Two directories called `tools/` shipping different plugins are two plugins; the same plugin vendored at two scopes is one, and the project copy wins.
45
+
46
+ Discovered plugins are enabled by default and gated by `enabledPlugins` in settings. A plugin that fails to load still appears — as a read-only row explaining why — rather than vanishing.
47
+
48
+ ### Extra roots for development
49
+
50
+ `ZIDANE_PLUGIN_DIRS` adds scan roots, mirroring `ZIDANE_EXTENSION_DIRS`:
51
+
52
+ ```sh
53
+ ZIDANE_PLUGIN_DIRS=/repo/plugins zidane
54
+ ```
55
+
56
+ Each entry is a **root** whose immediate children are plugins (`<root>/<name>/plugin.json`), not a single plugin directory. Multiple roots use the platform path delimiter.
57
+
58
+ This exists because all four default roots are dot-directories and `.zidane/` is gitignored — so a repo has nowhere *tracked* to keep example or first-party plugins. Extra roots are appended **after** the four defaults and scoped `project`, so a real project or user plugin always wins a name collision against a dev root.
59
+
60
+ This repo uses it to dogfood both load paths in `npm run tui`:
61
+
62
+ | Plugin | Route | Origin shown |
63
+ | --- | --- | --- |
64
+ | `plugins/hello-plugin` | disk, via `ZIDANE_PLUGIN_DIRS=../plugins` | `[project: plugins/hello-plugin]` |
65
+ | `extensions/plugin-host/vendor/vendored-plugin` | `ctx.loadAgentPlugin` | `[via extension: plugin-host]` |
66
+
67
+ Their skills (`plugin-smoke-test`, `vendored-smoke-test`) double as the manual verification scripts for plugin loading.
68
+
69
+ ### Project-scope trust
70
+
71
+ A project plugin's stdio MCP servers run, the same as those in a project `.zidane/mcps.json` beside them. There is no extra gate, deliberately: holding plugins to a stricter rule than the MCP config sitting next to them would be inconsistent rather than safer, since both are things you or your repo placed on purpose.
72
+
73
+ Stated plainly, the exposure that comes with that: a cloned repo carrying `.agents/plugins/evil/mcp.json` gets its stdio command spawned on the first agent bootstrap. That is today's behavior for project `mcps.json` too. If you don't want it, disable the plugin in settings, or set `enabledPlugins` to an allowlist that omits it.
74
+
75
+ (The `dropStdio` carve-out in `chat/mcps-discovery.ts` is narrower than it looks — it applies to **Ruler** files specifically, because `importRulerConfigs` defaults on and would otherwise execute a repo-committed foreign config unprompted.)
76
+
77
+ ## Loading a plugin
78
+
79
+ ### From an extension
80
+
81
+ The primary entry point. One call registers everything the plugin ships:
82
+
83
+ ```ts
84
+ import { defineExtension } from 'zidane'
85
+
86
+ export default defineExtension({
87
+ name: 'my-toolkit',
88
+ capabilities: ['plugins'],
89
+ async setup(ctx) {
90
+ const plugin = await ctx.loadAgentPlugin('./vendor/deploy-tools')
91
+ if (!plugin.ok)
92
+ ctx.logger.warn('deploy-tools failed to load')
93
+ },
94
+ })
95
+ ```
96
+
97
+ `dir` may be absolute or relative to the extension's own directory, so an extension can vendor plugins inside itself. Inline extensions (no directory) resolve against `ctx.paths.cwd`.
98
+
99
+ Options, when you need them:
100
+
101
+ ```ts
102
+ await ctx.loadAgentPlugin({
103
+ dir: './vendor/deploy-tools',
104
+ skills: true, // register skills/ (default true)
105
+ mcp: true, // register mcp.json servers (default true)
106
+ namePrefix: 'acme', // rename to `acme-<name>` (default none)
107
+ })
108
+ ```
109
+
110
+ Three things worth knowing:
111
+
112
+ - **It never throws for plugin-level problems.** A malformed manifest, an escaping symlink or an invalid skill comes back as `ok: false` / diagnostics and is logged to the extension's logger. One bad package cannot fail your `setup()`.
113
+ - **Await it inside `setup()`.** Registration closes when `setup()` resolves. A load still in flight at that point is dropped with a warning rather than mutating an already-published registry.
114
+ - **Components inherit the extension's scope.** A project-scoped extension's plugin is project-scoped, exactly as `ctx.registerSkill` behaves.
115
+
116
+ ### From the SDK
117
+
118
+ ```ts
119
+ import { loadAgentPlugin } from 'zidane/plugins'
120
+
121
+ const plugin = await loadAgentPlugin(dir, {
122
+ source: 'user', // scope stamped on discovered skills (default 'project')
123
+ skills: true, // discover skills/ (default true)
124
+ mcp: true, // discover mcp.json (default true)
125
+ namePrefix: 'acme', // rename to `acme-<name>` (default none)
126
+ dataRoot: '/custom/parent', // ${PLUGIN_DATA} parent; name is appended
127
+ dataDir: '/exact/dir', // or pin one plugin's directory outright
128
+ })
129
+ ```
130
+
131
+ ## `plugin.json`
132
+
133
+ The manifest is a **closed** schema, validated locally — the spec forbids retrieving a schema while loading. `$schema` and `name` are required; everything else is optional metadata.
134
+
135
+ ```json
136
+ {
137
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
138
+ "name": "deploy-tools",
139
+ "version": "1.2.0",
140
+ "description": "Deployment skills and a release MCP server",
141
+ "author": { "name": "Acme", "email": "dev@acme.example" },
142
+ "homepage": "https://acme.example",
143
+ "repository": "https://github.com/acme/deploy-tools",
144
+ "license": "MIT",
145
+ "keywords": ["deploy", "release"],
146
+ "extensions": { "dev.example": { "anything": true } }
147
+ }
148
+ ```
149
+
150
+ `name` is 1–64 characters: lowercase letters, digits, `-` and `.`, alphanumeric at both ends, no `--` or `..`.
151
+
152
+ Metadata strings are type-checked only. A `license` that isn't valid SPDX, or a `homepage` that isn't a valid URL, does **not** reject the plugin — the spec keeps identity portable and leaves semantic policing to registries.
153
+
154
+ Exactly two violations are non-fatal, per spec:
155
+
156
+ | Violation | Behavior |
157
+ |---|---|
158
+ | Unknown top-level field | Reported, ignored, loading continues |
159
+ | `extensions` is not an object | Reported, ignored, loading continues |
160
+
161
+ Every other schema violation **rejects the whole plugin**, and rejection happens *before* component discovery — a plugin with a broken manifest never has its skills read or its servers spawned.
162
+
163
+ `extensions` member *values* are never validated. The spec says to ignore namespaces the client does not implement without inspecting them, so a client validates only its own namespace at the point of use.
164
+
165
+ ## `skills/`
166
+
167
+ Each immediate child directory containing a `SKILL.md` that resolves to a regular file is one skill. The format is plain [Agent Skills](./SKILL.md) — same parser as `~/.agents/skills`.
168
+
169
+ Nested descendants are **not** searched. `skills/category/nested/SKILL.md` is invisible, even though zidane's own skill discovery would find it. Bundling a non-skill directory under `skills/` is legal and silently ignored.
170
+
171
+ Every discovered skill is stamped with `metadata['zidane.plugin']` naming the plugin that shipped it.
172
+
173
+ ## `mcp.json`
174
+
175
+ ```json
176
+ {
177
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
178
+ "mcpServers": {
179
+ "release": {
180
+ "type": "stdio",
181
+ "command": "uvx",
182
+ "args": ["release-mcp", "--root", "${PLUGIN_ROOT}"],
183
+ "env": { "CACHE_DIR": "${PLUGIN_DATA}/cache" },
184
+ "cwd": "${PLUGIN_ROOT}"
185
+ },
186
+ "registry": {
187
+ "type": "streamable-http",
188
+ "url": "https://mcp.acme.example/v1",
189
+ "headers": { "X-Client": "zidane" }
190
+ }
191
+ }
192
+ }
193
+ ```
194
+
195
+ `$schema` must declare the **same** Agent Plugins version as `plugin.json`.
196
+
197
+ `type` is required and never inferred. Each transport has a closed key set:
198
+
199
+ | `type` | Permitted keys |
200
+ |---|---|
201
+ | `stdio` | `type`, `command`, `args`, `env`, `cwd` |
202
+ | `streamable-http` | `type`, `url`, `headers` |
203
+ | `sse` | `type`, `url`, `headers` |
204
+
205
+ An unknown field, an unknown transport, or a missing required field invalidates **that one entry**; its siblings still load. A malformed document, a version mismatch, or an unknown top-level field disables the MCP component for that plugin only — its skills are unaffected.
206
+
207
+ ### Plugin variables
208
+
209
+ Two placeholders, and only two:
210
+
211
+ | Variable | Value |
212
+ |---|---|
213
+ | `${PLUGIN_ROOT}` | Symlink-resolved plugin root |
214
+ | `${PLUGIN_DATA}` | Dedicated writable directory, persistent across plugin updates |
215
+
216
+ They expand in `args`, `env` values and `cwd` — **nowhere else**. In `command`, `url` and `headers` the text stays literal, so a `${PLUGIN_ROOT}` in `command` is rejected rather than handed to the OS verbatim.
217
+
218
+ Expansion is single-pass and non-recursive: an expanded value containing `${PLUGIN_DATA}` does not expand again. Unknown `${...}` text is left alone — zidane's wider `${VAR}` / `${VAR:-default}` / `${env:VAR}` handling does **not** apply to plugin configs (they carry `preExpanded`, which suppresses it). That is deliberate: it keeps an ambient `PLUGIN_ROOT` environment variable from reaching fields the spec says must stay literal.
219
+
220
+ Both variables are always present in a stdio server's `env`, written last, and a plugin that tries to set either itself has that entry rejected.
221
+
222
+ `${PLUGIN_DATA}` defaults to `<dataDir>/plugin-data/<plugin-name>/` from `resolveStorageDirs` (honouring `ZIDANE_STORAGE_DIR` and XDG). It sits **outside** the plugin root so a plugin update — which replaces the package tree — cannot wipe it. Extension-loaded plugins get it inside the loading extension's storage root instead, so two extensions loading the same plugin don't share state.
223
+
224
+ The directory is created at **spawn** time, not load time. Loading is not consent: a host has to load every plugin it finds just to learn its name (identity lives in the manifest, so it can't filter first), and `enabledPlugins` only applies afterwards — creating on load left a directory behind for every plugin the user had switched off, on every discovery pass. A `cwd` inside `${PLUGIN_DATA}` is created along with it, since spawning into a non-existent directory fails with a bare `ENOENT`; a `cwd` under `${PLUGIN_ROOT}` is not, because that path is package content the author was supposed to ship.
225
+
226
+ Note the honest consequence: `ZIDANE_PREFIX` is *not* read by `resolveStorageDirs` — callers thread `prefix` explicitly, which disk discovery does.
227
+
228
+ ### stdio commands
229
+
230
+ `command` is exactly one executable token — no shell, no argv splitting:
231
+
232
+ - **Bare name** (`uvx`, `node`) → PATH lookup by the OS.
233
+ - **`./`-relative** → resolved inside the plugin root, and it must exist.
234
+ - **Absolute** → rejected. It is neither a PATH lookup nor a package file.
235
+
236
+ `cwd` defaults to the plugin root and must stay inside the root or the data directory, checked both lexically and after symlink resolution.
237
+
238
+ ### Remote servers
239
+
240
+ - HTTPS required unless the host is loopback.
241
+ - No userinfo credentials, no fragment.
242
+ - Configured headers are pinned to the URL's origin: redirects are followed manually and credentials are dropped the moment a hop leaves that origin. `authorization` is always protected, including the header the SDK's OAuth provider injects rather than reading from config.
243
+ - A redirect to non-loopback cleartext is refused outright, not merely stripped.
244
+
245
+ ## The package boundary
246
+
247
+ Every package file must stay inside the symlink-resolved plugin root. Lexical checks are not enough — a `skills` symlink pointing at `~/.ssh` passes every one of them — so each path is resolved through `realpath` and re-tested, and required filesystem kinds are asserted.
248
+
249
+ Failures are isolated at the narrowest level that still holds:
250
+
251
+ | Problem | Consequence |
252
+ |---|---|
253
+ | Invalid manifest, or manifest outside the root | Whole plugin rejected, no components discovered |
254
+ | `skills/` or `mcp.json` invalid, wrong kind, or outside the root | That component type disabled; the other still loads |
255
+ | One skill invalid, unreadable, or outside the root | That skill skipped; siblings load |
256
+ | One `mcp.json` entry invalid | That server skipped; siblings load |
257
+ | Any other escaping path | Access denied for that path |
258
+
259
+ A **missing** fixed location is not an error. `components.skills === 'absent'` means the plugin ships no skills; `'invalid'` means they were rejected.
260
+
261
+ ## Results and diagnostics
262
+
263
+ ```ts
264
+ interface LoadedPlugin {
265
+ ok: boolean // false ⇒ plugin rejected, components never discovered
266
+ dir: string // root as requested, made absolute
267
+ root: string // symlink-resolved root — the boundary
268
+ manifest?: PluginManifest // absent iff !ok
269
+ skills: SkillConfig[]
270
+ mcpServers: McpServerConfig[]
271
+ components: {
272
+ skills: 'absent' | 'loaded' | 'invalid' | 'unsupported'
273
+ mcp: 'absent' | 'loaded' | 'invalid' | 'unsupported'
274
+ }
275
+ diagnostics: PluginDiagnostic[]
276
+ }
277
+ ```
278
+
279
+ Each diagnostic carries a `severity`, a stable machine-readable `code` (`manifest-unknown-field`, `skill-escapes-root`, `mcp-entry-transport-invalid`, …), a message, and optionally the `field` or `path` involved. `severity: 'error'` on a plugin that still returned `ok: true` means a narrow boundary fired, not that the plugin failed.
280
+
281
+ ## Naming and collisions
282
+
283
+ zidane's skill catalog and MCP server names are single flat namespaces, so two plugins each shipping a `deploy` skill or a `github` server would collide. The spec leaves the policy to the client; zidane's default is first-found-wins with a collision diagnostic, plus opt-in `namePrefix` for the cases where you know you're loading several.
284
+
285
+ Server names are validated in the form they actually take in the tool catalog — `mcp_<server>_<tool>` — which means *after* `namePrefix` is applied, since a bad prefix can invalidate a name the plugin got right.
286
+
287
+ Collisions are resolved at the merge, not at load, so a plugin's settings row counts what the **package ships** rather than what ended up live. A plugin server whose name matches one in `mcps.json` is dropped (first-wins, `mcps.json` first), and a plugin skill whose name matches an extension's is shadowed. The MCPs and Skills tabs are the authority on what actually loaded; skill collisions emit `name-collision-shadowed`, server collisions are logged under `ZIDANE_DEBUG`. Pass `namePrefix` when loading plugins that might collide.
288
+
289
+ ### The same plugin from both routes
290
+
291
+ A plugin present *both* on disk and vendored inside an extension is **one** plugin: the disk copy wins, and the extension's copy contributes nothing. One settings row, one checkbox, one set of skills and servers.
292
+
293
+ This has to hold on the contribution side, not just the display side. If both copies stayed live, that single checkbox would govern two of them, and the skills are the sharp end: both sets reach the catalog, where one shadows the other and earns a `name-collision-shadowed` warning about a collision the user never created.
294
+
295
+ The MCP half is the same shape but milder — both configs reach the merge, where a first-wins dedupe by name collapses them, so nothing is spawned twice. What's wrong there is that one checkbox nominally governs two records, and which copy survives is decided by merge order rather than by anything the user chose.
296
+
297
+ Disk wins even when the disk copy is *rejected*, so a broken installed copy doesn't quietly fall back to a vendored one while the row reads `rejected`.
298
+
299
+ ## Settings
300
+
301
+ Plugins live in the **Extensions & Plugins** tab, interleaved alphabetically with extensions rather than in a separate section. They aren't parallel categories — a plugin can be loaded *by* an extension — so each row carries a kind badge and an origin instead:
302
+
303
+ ```
304
+ [✓] git-flow v2.1.0 ext
305
+ Git helpers · [project: .zidane/extensions/git-flow]
306
+ [✓] deploy-tools v1.2.0 plugin
307
+ Deploy skills · [project: .agents/plugins/deploy-tools] · 3 skills, 1 server
308
+ [✓] registry-mcp v0.4.0 plugin
309
+ Registry access · [via extension: my-toolkit] · 1 server
310
+ ✗ broken-thing plugin
311
+ rejected: manifest-name-invalid · [user: ~/.agents/plugins/broken-thing]
312
+ ```
313
+
314
+ The kind badge is a filled pill trailing the title line, so names stay left-aligned instead of being pushed right by a fixed-width column. Extensions get the neutral `border` tier because they're the majority — a saturated pill on nearly every row is noise — and plugins get the `model` accent so they stand out of that field.
315
+
316
+ Both colors derive from `ThemeColors` / `ThemeSurfaces` members every theme must define, rather than from `surfaces.chips` entries: that map is keyed by completion-provider id, and a third-party theme that never opted in would fall back to `chips.default` for both kinds, collapsing the very distinction the badge exists to draw.
317
+
318
+ Errored extensions and rejected plugins lead the list. A rejected plugin has no checkbox — there is nothing to enable — matching how an errored extension renders.
319
+
320
+ Searching matches a plugin's name, description and origin, plus the literal word `plugin`: type `plugin` to narrow to plugins, or an extension's name to find the plugins it loaded.
321
+
322
+ `ctrl+r` on this tab re-scans the plugin directories, so a package you just dropped into `.agents/plugins/` appears without a restart. It refreshes **plugins only** — an extension's `setup()` runs once per process, so extensions genuinely cannot be re-scanned in place, and calling it a generic refresh would over-promise. Extension-loaded plugins likewise only reload with their extension.
323
+
324
+ Toggling writes `enabledPlugins`, which gates the plugin's skills **and** its MCP servers together — for disk plugins and for extension-loaded ones alike. Extension-loaded plugins need a second filter pass to achieve that (`filterRegistryPlugins`), because their contributions are fanned into the loading extension's own sinks and `enabledExtensions` can only express whole-extension on/off.
325
+
326
+ Skills from an *enabled* plugin are force-included into `enabledSkills` when you have an allowlist there — enabling the plugin is the consent, and `enabledPlugins` is where you withhold it.
327
+
328
+ The gate is plugin-level only. A plugin's skills **are** listed in the Skills tab, badged `plugin:<name>`, but they carry no checkbox: the agent build force-includes every enabled plugin's skills, so a per-skill "off" would be persisted, flip the box, and then be overridden on the next build. Rather than ship a control whose off state doesn't hold, the row is read-only and the badge points at the plugin row that owns the switch. Turning off one skill of a plugin means disabling the plugin.
329
+
330
+ Extension-registered skills work the same way, badged `ext:<name>` and gated by `enabledExtensions`.
331
+
332
+ Real per-skill control would mean dropping that force-include and seeding contributed skill names into `enabledSkills` at enable time — a persistence change, not a rendering one.
333
+
334
+ ## Conformance
335
+
336
+ zidane implements Agent Plugins 1.0.0: manifest validation, both component types, both required MCP transports plus legacy HTTP+SSE, plugin variables, and the failure-isolation ladder. Supported versions are listed in `SUPPORTED_AGENT_PLUGINS_VERSIONS`; a plugin targeting anything else is rejected with a diagnostic rather than loaded on a guess.
337
+
338
+ `test/plugins/conformance.test.ts` maps 1:1 to the [published checklist](https://agent-plugins.org/client-implementers/conformance) — one test per line, same order, same headings. Behavioral depth lives in the sibling suites; that file exists so conformance can be verified item by item instead of inferred from three test files. A checklist line with no test there is a claim nobody checked.
339
+
340
+ Not yet implemented: installing a plugin from a git/npm spec (`/plugin install`). Drop a package into one of the discovery directories, load it from an extension, or call `loadAgentPlugin` directly.
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. 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.
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,64 @@ 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**, for two reasons. 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
+ - **A child as its own Restate invocation is not supported.** That's the idiomatic answer for long or genuinely parallel children (own journal, own suspension budget), but it needs a transport for `child:*` hook bubbling and stat rollup across the invocation boundary.
409
+
350
410
  ## Durable tool state
351
411
 
352
412
  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 +456,7 @@ The invariant that makes it replay-safe: **a staged mutation is committed only i
396
456
  - **`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
457
  - **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
458
  - **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.
459
+ - **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
460
  - **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
461
  - **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
462
 
@@ -589,7 +649,13 @@ A runtime with a hard per-invocation CPU/wall cap (Cloudflare's ~5-minute limit)
589
649
 
590
650
  `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
651
 
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 five scenarios through the real ingress: 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. It 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.
652
+ `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 ten scenarios through the real ingress.
653
+
654
+ 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.
655
+
656
+ 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.
657
+
658
+ 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
659
 
594
660
  `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
661
 
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 three optional identity fields (omitted from the per-row "key ctx fields" column above to keep the table readable):
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
- Same three fields land on `ToolContext` (visible to the tool body via `ctx.runId` / `ctx.parentRunId` / `ctx.depth`). The loop centralizes their construction in `buildToolHookBase`, so a new tool-related hook event automatically inherits them.
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` and `shareReadState`
615
+ ### Parent/child boundaries — `persist`, `shareReadState`, `shareHandle`
603
616
 
604
- Two **independent** flags control what crosses the parent/child boundary:
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.4.4",
3
+ "version": "6.5.0",
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",