@arnilo/prism 0.6.0 → 0.8.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 (178) hide show
  1. package/CHANGELOG.md +79 -5
  2. package/README.md +12 -11
  3. package/dist/agent-approval.d.ts +4 -0
  4. package/dist/agent-approval.js +5 -1
  5. package/dist/agent-definitions.js +1 -0
  6. package/dist/agent-run-lifecycle.js +39 -4
  7. package/dist/agent-run-state.d.ts +18 -0
  8. package/dist/agent-run-state.js +39 -9
  9. package/dist/agent-session/helpers.js +6 -1
  10. package/dist/agent-session/session/assemble.js +159 -7
  11. package/dist/agent-session/session/persist.d.ts +16 -0
  12. package/dist/agent-session/session/persist.js +64 -4
  13. package/dist/agent-session/session/provider-round.d.ts +3 -3
  14. package/dist/agent-session/session/provider-round.js +12 -6
  15. package/dist/agent-session/session/tool-round.js +5 -1
  16. package/dist/agent-session/session/types.d.ts +22 -1
  17. package/dist/agent-session/session.d.ts +16 -0
  18. package/dist/agent-session/session.js +42 -3
  19. package/dist/artifacts.d.ts +39 -1
  20. package/dist/artifacts.js +73 -0
  21. package/dist/attention-compiler.d.ts +121 -0
  22. package/dist/attention-compiler.js +479 -0
  23. package/dist/checkpoints.js +7 -11
  24. package/dist/cli-init.js +20 -6
  25. package/dist/context-budget.d.ts +20 -1
  26. package/dist/context-budget.js +10 -1
  27. package/dist/contracts-core/agent.d.ts +7 -0
  28. package/dist/contracts-core/attention.d.ts +66 -0
  29. package/dist/contracts-core/attention.js +2 -0
  30. package/dist/contracts-core/compaction.d.ts +59 -0
  31. package/dist/contracts-core/compaction.js +77 -1
  32. package/dist/contracts-core/content.d.ts +5 -0
  33. package/dist/contracts-core/loop.d.ts +42 -0
  34. package/dist/contracts-core/provider.d.ts +4 -0
  35. package/dist/contracts-core/run-limits.d.ts +2 -0
  36. package/dist/contracts-core.d.ts +1 -0
  37. package/dist/contracts-core.js +1 -0
  38. package/dist/contracts-protocol.d.ts +44 -3
  39. package/dist/contracts-run-state.d.ts +32 -5
  40. package/dist/evidence-grounding.d.ts +29 -0
  41. package/dist/evidence-grounding.js +162 -0
  42. package/dist/host-composition.d.ts +91 -0
  43. package/dist/host-composition.js +279 -0
  44. package/dist/index.d.ts +13 -6
  45. package/dist/index.js +7 -4
  46. package/dist/input.d.ts +13 -1
  47. package/dist/input.js +40 -1
  48. package/dist/provider-events.d.ts +3 -1
  49. package/dist/provider-events.js +2 -2
  50. package/dist/providers/transport.d.ts +3 -1
  51. package/dist/providers/transport.js +36 -0
  52. package/dist/redaction.js +18 -2
  53. package/dist/run-bundle.d.ts +89 -0
  54. package/dist/run-bundle.js +149 -0
  55. package/dist/secure-agent.d.ts +2 -0
  56. package/dist/secure-agent.js +6 -1
  57. package/dist/testing/state-concurrency-conformance.js +5 -12
  58. package/dist/tool-result-fold.d.ts +12 -0
  59. package/dist/tool-result-fold.js +13 -6
  60. package/dist/tools.d.ts +10 -0
  61. package/dist/tools.js +41 -0
  62. package/docs/acp-agent.md +42 -11
  63. package/docs/acp.md +2 -1
  64. package/docs/ag-ui.md +10 -3
  65. package/docs/agent-definitions.md +9 -1
  66. package/docs/agent-events.md +4 -1
  67. package/docs/agent-loops.md +33 -0
  68. package/docs/agent-session-runtime.md +8 -7
  69. package/docs/attention-compiler.md +272 -0
  70. package/docs/cli-rpc.md +4 -2
  71. package/docs/coding-agent-tools.md +1 -1
  72. package/docs/coding-security.md +6 -3
  73. package/docs/coding-tools.md +0 -1
  74. package/docs/coding-workspaces.md +22 -0
  75. package/docs/compaction-and-retry.md +36 -4
  76. package/docs/compaction-observational-memory.md +63 -10
  77. package/docs/connected-apps.md +116 -0
  78. package/docs/context-and-skills.md +17 -2
  79. package/docs/conversations.md +1 -1
  80. package/docs/core.md +1 -1
  81. package/docs/dev-inspector.md +4 -0
  82. package/docs/device-adapters.md +1 -0
  83. package/docs/diagrams.md +6 -6
  84. package/docs/document-reader.md +18 -10
  85. package/docs/documents.md +40 -11
  86. package/docs/durable-runs.md +87 -0
  87. package/docs/enterprise-postgres-state.md +6 -2
  88. package/docs/evaluations.md +168 -4
  89. package/docs/execution-timeline.md +186 -0
  90. package/docs/guardrails.md +33 -0
  91. package/docs/history/0.7.0-primitive-review.md +254 -0
  92. package/docs/history/079-messaging-primitive-review.md +391 -0
  93. package/docs/history/080-messaging-followon-primitive-review.md +234 -0
  94. package/docs/history/081-connected-apps-primitive-review.md +74 -0
  95. package/docs/history/083-prism-work-primitive-review.md +84 -0
  96. package/docs/history/084-primitive-review.md +96 -0
  97. package/docs/history/085-honesty-and-cut-primitive-review.md +91 -0
  98. package/docs/history/README.md +5 -0
  99. package/docs/history/migration-0.0.md +2 -2
  100. package/docs/history/release-handoffs.md +75 -1
  101. package/docs/host-compositions.md +149 -0
  102. package/docs/host-security.md +2 -2
  103. package/docs/hosted-sandboxes.md +94 -0
  104. package/docs/index.md +82 -45
  105. package/docs/input-and-prompt-assembly.md +1 -0
  106. package/docs/knowledge-sync.md +84 -0
  107. package/docs/language-intelligence.md +1 -1
  108. package/docs/live-testing.md +8 -3
  109. package/docs/mcp-tools.md +3 -1
  110. package/docs/memory-fabric.md +416 -0
  111. package/docs/messaging-channel-operations.md +166 -0
  112. package/docs/messaging-channels.md +150 -0
  113. package/docs/migrate-to-0.5.md +1 -1
  114. package/docs/migrate-to-0.6.md +1 -0
  115. package/docs/migrate-to-0.7.md +345 -0
  116. package/docs/migrate-to-0.8.md +124 -0
  117. package/docs/migration.md +43 -1
  118. package/docs/model-registry.md +12 -2
  119. package/docs/model-routing.md +79 -4
  120. package/docs/multi-agent-patterns.md +20 -6
  121. package/docs/observability.md +52 -1
  122. package/docs/openapi-tools.md +1 -1
  123. package/docs/operations.md +14 -4
  124. package/docs/options-index.md +47 -3
  125. package/docs/peer-dependencies.md +12 -10
  126. package/docs/postgres-persistence.md +1 -1
  127. package/docs/process-sessions.md +3 -1
  128. package/docs/prompt-registry.md +1 -1
  129. package/docs/provider-caching.md +4 -2
  130. package/docs/provider-conformance.md +1 -1
  131. package/docs/provider-layer.md +2 -2
  132. package/docs/provider-packages.md +22 -22
  133. package/docs/providers/bedrock.md +71 -7
  134. package/docs/providers/neuralwatt.md +5 -1
  135. package/docs/providers/openai.md +1 -1
  136. package/docs/rag.md +24 -8
  137. package/docs/realtime-voice.md +87 -0
  138. package/docs/release-and-install.md +53 -45
  139. package/docs/run-bundle.md +92 -0
  140. package/docs/runs-and-usage.md +17 -2
  141. package/docs/server.md +7 -3
  142. package/docs/sheets.md +9 -9
  143. package/docs/signal-channel.md +112 -0
  144. package/docs/speech.md +7 -1
  145. package/docs/sqlite-persistence.md +1 -1
  146. package/docs/supervisors.md +33 -5
  147. package/docs/telegram-channel.md +157 -0
  148. package/docs/testing.md +2 -2
  149. package/docs/thinking-and-reasoning.md +3 -1
  150. package/docs/tools.md +6 -5
  151. package/docs/web-tools.md +2 -1
  152. package/docs/wiki.md +1 -1
  153. package/docs/work-artifacts-and-review.md +14 -4
  154. package/docs/work-connectors.md +12 -10
  155. package/docs/work-sandbox.md +115 -0
  156. package/docs/work-tools.md +50 -18
  157. package/docs/workflows.md +69 -1
  158. package/docs/working-and-semantic-memory.md +25 -14
  159. package/package.json +5 -3
  160. package/templates/README.md +2 -0
  161. package/templates/business-worker/README.md.tmpl +19 -0
  162. package/templates/business-worker/env.example.tmpl +1 -0
  163. package/templates/business-worker/gitignore.tmpl +11 -0
  164. package/templates/business-worker/manifest.json +12 -0
  165. package/templates/business-worker/package.json.tmpl +23 -0
  166. package/templates/business-worker/src/agent.ts.tmpl +92 -0
  167. package/templates/business-worker/src/index.ts.tmpl +13 -0
  168. package/templates/business-worker/src/tests/agent.test.ts.tmpl +77 -0
  169. package/templates/business-worker/tsconfig.json.tmpl +15 -0
  170. package/templates/personal-assistant/README.md.tmpl +18 -0
  171. package/templates/personal-assistant/env.example.tmpl +1 -0
  172. package/templates/personal-assistant/gitignore.tmpl +11 -0
  173. package/templates/personal-assistant/manifest.json +11 -0
  174. package/templates/personal-assistant/package.json.tmpl +23 -0
  175. package/templates/personal-assistant/src/agent.ts.tmpl +65 -0
  176. package/templates/personal-assistant/src/index.ts.tmpl +13 -0
  177. package/templates/personal-assistant/src/tests/agent.test.ts.tmpl +28 -0
  178. package/templates/personal-assistant/tsconfig.json.tmpl +15 -0
@@ -58,7 +58,7 @@ async function foldToolResultMessage(message, options, context) {
58
58
  const block = message.content.find((part) => part.type === "tool_result");
59
59
  if (block?.type !== "tool_result")
60
60
  return message;
61
- const text = toolResultText(block.result, block.error, message.content);
61
+ const text = toolResultFoldText(block.result, block.error, message.content);
62
62
  const folded = await maybeFold({
63
63
  options,
64
64
  context,
@@ -80,7 +80,7 @@ async function foldToolResultMessage(message, options, context) {
80
80
  return folded ?? message;
81
81
  }
82
82
  async function foldToolResultValue(result, options, context) {
83
- const text = toolResultText(result.value, result.error, result.content);
83
+ const text = toolResultFoldText(result.value, result.error, result.content);
84
84
  const folded = await maybeFold({
85
85
  options,
86
86
  context,
@@ -112,7 +112,7 @@ async function maybeFold(input) {
112
112
  toolName: input.toolName,
113
113
  text: input.text,
114
114
  });
115
- return input.apply(capSummaryBytes(String(summary), input.options.maxSummaryBytes));
115
+ return input.apply(capToolResultSummary(String(summary), input.options.maxSummaryBytes));
116
116
  }
117
117
  catch {
118
118
  return undefined;
@@ -124,7 +124,9 @@ export function formatFoldedToolResult(summary) {
124
124
  export function foldedToolResultHeader(toolName, toolCallId, summary) {
125
125
  return `Tool result ${toolName} [${toolCallId}]: ${summary}`;
126
126
  }
127
- function toolResultText(result, error, extra) {
127
+ /** Text the fold summarizes for one tool result: the payload JSON, then any text blocks.
128
+ * Shared with the attention compiler so both hash/summarize the same bytes. */
129
+ export function toolResultFoldText(result, error, extra) {
128
130
  const parts = [JSON.stringify(error ?? result ?? null)];
129
131
  for (const block of extra ?? []) {
130
132
  if (block.type === "text" && "text" in block && typeof block.text === "string")
@@ -132,7 +134,9 @@ function toolResultText(result, error, extra) {
132
134
  }
133
135
  return parts.join("\n");
134
136
  }
135
- function capSummaryBytes(summary, maxBytes) {
137
+ /** UTF-8 cap that never splits a multi-byte character. Shared with the attention
138
+ * compiler's host-summarize path so both cap host text identically. */
139
+ export function capToolResultSummary(summary, maxBytes) {
136
140
  const bytes = estimateTextBytes(summary);
137
141
  if (bytes <= maxBytes)
138
142
  return summary;
@@ -143,7 +147,10 @@ function capSummaryBytes(summary, maxBytes) {
143
147
  end--;
144
148
  return `${new TextDecoder().decode(encoded.slice(0, end))}…`;
145
149
  }
146
- function inferToolResultTurns(history) {
150
+ /** Provider turn per history index: assistant messages advance the turn, tool rows use
151
+ * their `prismToolResultTurn` stamp when present and the last assistant turn otherwise.
152
+ * Shared with the attention compiler so age gates match the fold exactly. */
153
+ export function inferToolResultTurns(history) {
147
154
  const turns = new Array(history.length).fill(1);
148
155
  let providerTurn = 0;
149
156
  let toolTurn = 1;
package/dist/tools.d.ts CHANGED
@@ -56,5 +56,15 @@ export interface ToolRegistryOptions extends DuplicateRegistrationOptions {
56
56
  }
57
57
  export declare function createToolRegistry(tools?: readonly ToolDefinition[], options?: ToolRegistryOptions): ToolRegistry;
58
58
  export declare function filterTools(tools: readonly ToolDefinition[], filter?: ToolFilterInput): readonly ToolDefinition[];
59
+ /** Cap matches tool-search index; run allow-lists never exceed the disclosed set. */
60
+ export declare const HARD_RUN_TOOL_NAMES = 1024;
61
+ /**
62
+ * Per-run allow-list. Omitted grant → unchanged list. Checkpointed grant cannot widen.
63
+ * Fresh unknown names fail closed; resume drops names the current registry no longer has.
64
+ */
65
+ export declare function selectRunTools(listed: readonly ToolDefinition[], requested: readonly string[] | undefined, checkpoint?: readonly string[]): {
66
+ readonly tools: readonly ToolDefinition[];
67
+ readonly grant: readonly string[] | undefined;
68
+ };
59
69
  export declare function dispatchToolCall(options: DispatchToolCallOptions): Promise<ToolResult>;
60
70
  export declare function resolveToolEffectDeclaration(tool: ToolDefinition, args: JsonObject, context: ToolExecutionContext): ToolEffectDeclaration | undefined;
package/dist/tools.js CHANGED
@@ -58,6 +58,47 @@ export function filterTools(tools, filter) {
58
58
  .filter((item) => Boolean(item));
59
59
  return tools.filter((tool) => !denied.has(tool.name) && allows.every((allow) => allow.has(tool.name)));
60
60
  }
61
+ /** Cap matches tool-search index; run allow-lists never exceed the disclosed set. */
62
+ export const HARD_RUN_TOOL_NAMES = 1024;
63
+ const MAX_RUN_TOOL_NAME_CHARS = 256;
64
+ function assertRunToolNames(names) {
65
+ if (names.length > HARD_RUN_TOOL_NAMES) {
66
+ throw new TypeError(`RunOptions.toolNames exceeds ${HARD_RUN_TOOL_NAMES} entries`);
67
+ }
68
+ const out = [];
69
+ const seen = new Set();
70
+ for (const name of names) {
71
+ if (typeof name !== "string" || name.length === 0 || name.length > MAX_RUN_TOOL_NAME_CHARS) {
72
+ throw new TypeError(`RunOptions.toolNames entries must be non-empty strings of at most ${MAX_RUN_TOOL_NAME_CHARS} characters`);
73
+ }
74
+ if (!seen.has(name)) {
75
+ seen.add(name);
76
+ out.push(name);
77
+ }
78
+ }
79
+ return out;
80
+ }
81
+ /**
82
+ * Per-run allow-list. Omitted grant → unchanged list. Checkpointed grant cannot widen.
83
+ * Fresh unknown names fail closed; resume drops names the current registry no longer has.
84
+ */
85
+ export function selectRunTools(listed, requested, checkpoint) {
86
+ const req = requested === undefined ? undefined : assertRunToolNames(requested);
87
+ const grant = checkpoint === undefined ? req : req === undefined ? checkpoint : req.filter((name) => checkpoint.includes(name));
88
+ if (grant === undefined)
89
+ return { tools: listed, grant };
90
+ if (grant.length === 0)
91
+ return { tools: [], grant };
92
+ const available = new Set(listed.map((tool) => tool.name));
93
+ if (checkpoint === undefined) {
94
+ for (const name of grant) {
95
+ if (!available.has(name))
96
+ throw new TypeError(`Unknown run tool: ${name}`);
97
+ }
98
+ return { tools: filterTools(listed, { allow: grant }), grant };
99
+ }
100
+ return { tools: filterTools(listed, { allow: grant.filter((name) => available.has(name)) }), grant };
101
+ }
61
102
  function toolExecutionMetadata(startedAt, status) {
62
103
  return { durationMs: Math.max(0, Date.now() - Date.parse(startedAt)), status };
63
104
  }
package/docs/acp-agent.md CHANGED
@@ -24,22 +24,36 @@ The config file is the trust boundary: unknown keys are rejected (a typo cannot
24
24
  | --- | --- | --- |
25
25
  | `userId` | yes | Ownership user id for every session (single-local-user `authorize`). |
26
26
  | `cwd` | yes | Workspace root the coding tools are bound to (must be an existing directory). Sessions always operate on this root — a client-supplied `cwd` never moves the tools. |
27
- | `sessionStore` | no | `{ "type": "sqlite", "path": ".prism/sessions.db" }` or `{ "type": "memory" }` (default). SQLite persists sessions, runs, checkpoints, and leases (`createSqlitePersistence`). |
28
- | `mcp.allow` | no | MCP allow-list. http/sse servers must have a `url` starting with an allow entry; stdio servers require the marker `"stdio"`. The UNSTABLE `acp` transport is never approved. |
27
+ | `model` | yes* | Model selection `{ "provider": "<name>", "model": "<id>" }`. Supported providers: `openai`, `anthropic`, `google`, `deepseek`, `openrouter`, `ollama`, `xai`, `zai`, `alibaba`, `kimi`, `clinepass`, `commandcode`, `neuralwatt`, `opencode-go`, `hyper`, and `mock`. *Required unless `provider` is passed programmatically. |
28
+ | `credentialRef` | yes* | Reference (env var name or host secret identifier) used to resolve API credentials. *Required when using a non-mock provider without an injected provider instance. |
29
+ | `sessionStore` | no | `{ "type": "sqlite", "path": ".prism/sessions.db" }` or `{ "type": "memory" }` (default). SQLite persists sessions, runs, checkpoints, and leases (`createSqlitePersistence`), and supports session agent reconstruction across restarts. |
30
+ | `mcp.allow` | no | MCP allow-list. http/sse servers must match an allow origin or path-segment subtree; stdio servers require the marker `"stdio"`. The UNSTABLE `acp` transport is never approved. |
29
31
  | `modes` | no | Mode table `{ "modes": [{ "id", "name", "description?" }], "defaultModeId"? }`; ids unique, `defaultModeId` must name a mode. |
30
32
  | `configOptions` | no | `{ "options": [{ "type": "boolean" \| "select", "id", "name", "defaultValue", ... }] }`; ids unique. Select options are advertised/settable per the B3 gate (see [acp.md](acp.md)). |
31
33
  | `limits` | no | AG-UI/ACP caps passthrough (`AgUiLimitOptions`). |
32
34
 
33
- Example:
35
+ Real-provider example:
34
36
 
35
37
  ```json
36
38
  {
37
39
  "userId": "local",
38
40
  "cwd": ".",
41
+ "model": { "provider": "openai", "model": "gpt-4o" },
42
+ "credentialRef": "OPENAI_API_KEY",
39
43
  "sessionStore": { "type": "sqlite", "path": ".prism/sessions.db" },
40
44
  "mcp": { "allow": ["https://mcp.example.com"] },
41
45
  "modes": { "modes": [{ "id": "edit", "name": "Edit" }], "defaultModeId": "edit" },
42
- "configOptions": [{ "type": "boolean", "id": "verbose", "name": "Verbose", "defaultValue": false }]
46
+ "configOptions": { "options": [{ "type": "boolean", "id": "verbose", "name": "Verbose", "defaultValue": false }] }
47
+ }
48
+ ```
49
+
50
+ Explicit offline mock mode example:
51
+
52
+ ```json
53
+ {
54
+ "userId": "local",
55
+ "cwd": ".",
56
+ "model": { "provider": "mock", "model": "mock" }
43
57
  }
44
58
  ```
45
59
 
@@ -48,31 +62,48 @@ Example:
48
62
  The binary is pure wiring (~200 lines) — no protocol code lives here. It builds:
49
63
 
50
64
  - `authorize` — single local user; every inbound call is scoped by session id.
51
- - `sessionFactory` — real Prism sessions over `createAgent` with the nine coding tools (`createCodingTools(config.cwd)`), durable `runState` (`interruptBeforeTool`, checkpoints), ownership-scoped to `userId`.
52
- - `lifecycle` — `createAgentRunLifecycle` over the same checkpoint store, so approvals suspend/resume durably.
53
- - `mcp` — allow-list `select` gate with http/sse transports.
65
+ - `sessionFactory` — real Prism sessions over `createAgent` with the nine coding tools (`createCodingTools(config.cwd)`), durable `runState` (`interruptBeforeTool`, checkpoints), ownership-scoped to `userId`. When the client advertises filesystem capabilities, a per-session agent with client-backed buffer tools is constructed and bound to the session id.
66
+ - `lifecycle` — `createAgentRunLifecycle` over the same checkpoint store. Durable SQLite checkpoints enable interrupted runs and approval state to be reconstructed across restarts with the selected model and provider intact.
67
+ - `mcp` — allow-list `select` gate with origin- and path-segment subtree checking for http/sse transports.
54
68
  - `modes` / `configOptions` — from config.
55
- - Provider — **mock by default** (full lifecycle, no tokens). Wire a real provider programmatically:
69
+ - Provider — **fail closed before startup**. Unlike earlier releases where missing configuration silently defaulted to mock mode (Trap C), Prism 0.7.0 requires either an explicit `model` in config or an injected provider. For real providers, credentials are resolved lazily on demand via dynamic import of `@arnilo/prism-providers/<adapter>`. Offline mock mode must be explicitly specified (`model: { provider: "mock", model: "mock" }`).
70
+
71
+ Programmatic usage:
56
72
 
57
73
  ```ts
58
74
  import { createSpawnableAgent, loadConfig } from "@arnilo/prism-acp-agent";
59
- import { createOpenAIResponsesProvider } from "@arnilo/prism-providers/openai";
60
75
 
76
+ // Driven by config with optional custom credential resolver
61
77
  const agent = createSpawnableAgent({
62
78
  config: loadConfig("prism-acp-agent.json"),
63
- provider: createOpenAIResponsesProvider({ apiKey: process.env.OPENAI_API_KEY }),
79
+ credentialResolver: (ref) => process.env[ref],
80
+ });
81
+
82
+ // Or programmatic provider override (must match config.model.provider)
83
+ const customAgent = createSpawnableAgent({
84
+ config: loadConfig("prism-acp-agent.json"),
85
+ provider: customProvider,
64
86
  });
65
87
  ```
66
88
 
67
89
  ## Library surface
68
90
 
69
91
  - `loadConfig(path)` / `parseConfig(text, baseDir)` — read + validate; throw `ConfigError` (code `PRISM_ACP_AGENT_CONFIG`) with a clear message.
70
- - `createSpawnableAgent({ config, provider? })` — build the ACP `AgentApp`.
92
+ - `createSpawnableAgent({ config, provider?, model?, credentialResolver? })` — build the ACP `AgentApp`.
71
93
  - `selectMcpServers(allow, servers)` — the allow-list gate, exported for reuse in custom hosts.
94
+ - `SUPPORTED_PROVIDERS` — list of supported provider adapter identifiers.
72
95
 
73
96
  ## Security posture
74
97
 
75
98
  - Config file = trust boundary: validated shape, no arbitrary code execution.
76
99
  - MCP servers only from the allow-list; the UNSTABLE `acp` transport is never bridged.
100
+ - **Origin and path-segment destination matching**:
101
+ - Exact origin matching normalizes scheme, hostname (punycode IDN), and effective port (e.g. 443 on https). Lookalike hosts (`https://mcp.example.com.attacker.invalid`) are strictly rejected.
102
+ - Path matching enforces exact path or path-segment subtree: `https://mcp.example.com/mcp` admits `/mcp`, `/mcp/`, and `/mcp/sub`, but rejects `/mcp-other` and `/other`.
103
+ - Config entries with credentials (`user:pass@`), query parameters, fragment identifiers, or ambiguous path forms (`%2e%2e`, `%2f`, `%5c`, `..`, backslashes) fail validation.
104
+ - Candidate URLs embedding credentials or ambiguous encoded path forms fail closed at selection.
105
+ - Stdio servers require the explicit `"stdio"` marker; URL entries cannot authorize `stdio` processes, and `"stdio"` cannot authorize remote servers.
77
106
  - Coding tools are bound to `config.cwd` only; session ownership is fixed to `userId`.
78
107
  - Session store paths are resolved against the config directory and fail closed on invalid config.
108
+ - **Credential isolation**: Secret values never enter config persistence, argv flags, stdout protocol streams, events, or model context. Identity carries only the non-secret `credentialRef` name.
109
+ - **Trust model**: The ACP agent config is designed as a single-local-user trust boundary (workstation / editor agent), not a multi-tenant business boundary. Cross-tenant credential sharing or multi-user elevation must not be multiplexed through a single spawnable ACP agent process.
package/docs/acp.md CHANGED
@@ -57,6 +57,7 @@ In-stream `SessionUpdate`s:
57
57
  | `permission_denied` lifecycle | `tool_call_update` status `failed` (never raw args; synthesized id `prism:denied:<approvalId>` when no `toolCallId`) |
58
58
  | `configuration_changed` lifecycle | `config_option_update` with the full current set, per streaming session |
59
59
  | Plan lifecycle (F5, UNSTABLE-gated) | `plan_changed` → `plan_update` with `plan: { type: "items", planId = planPath, entries: [{ content, priority: "medium", status }] }` — the complete entry list per update (client replaces its plan wholesale); `plan_removed` → `plan_removed` with `planId = planPath`. Emitted only when the client advertised `ClientCapabilities.plan`; mapper stays capability-agnostic (gate in the agent wiring). Entries come from `writeCodingPlanFile`'s `onEvent` (parsed via `parseCodingPlanTodos`) or host-emitted through their `CodingLifecycleEmitter`; text passes the shared redactor and byte caps. |
60
+ | Subagent lifecycle | `subagent_started` / `subagent_stopped` → `agent_message_chunk` with only redacted child/delegation ids and terminal status. Wire `observeSupervisorLifecycle()` to emit them; child inputs, outputs, paths, and error text stay absent. |
60
61
  | Session title (F6) | `sessions.title({ sessionId, prompt, signal })` resolves on `session/prompt`; a defined value differing from the last emitted title produces `session_info_update` with `{ sessionUpdate: "session_info_update", title }`. Best-effort: `undefined` or a throw means no title and no update (requests never fail on titles); the host owns title storage. Titles pass the shared redactor and are truncated at `maxTextBytes`/`maxEventBytes`. |
61
62
  | Slash commands (F9) | `commands.list({ sessionId, signal })` on `session/new`/`load`/`resume` produces `available_commands_update` with `{ name, description, input?: { hint } }` (SDK `AvailableCommand`; description is required). Names/descriptions/hints pass the shared redactor and `maxTextBytes`; the list is sliced at `acpCommandsPerUpdate`. Best-effort: a throw or non-array omits the update (session start never fails on commands). |
62
63
  | Session mode/config switch | `current_mode_update` / `config_option_update` |
@@ -115,7 +116,7 @@ const agent = createPrismAcpAgent({
115
116
  - **Client fs/terminal are adapters, not a second implementation.** `AcpClientFilesystem` / `AcpClientTerminals` wrap the client's `fs/*` and `terminal/*` methods behind the Phase 9 `ProcessSession`-flavored interfaces; the agent pre-generates the session id so terminal requests can carry it. `createAcpFilesystemOperations` from `@arnilo/prism-coding-tools/agent` maps that filesystem seam onto the coding tools' `read`/`write`/`edit` operations. This editor-buffer mode is intentionally hybrid: `repo_list`, `repo_search`, `glob`, `delete`, and `move` remain disk-backed unless the host supplies separate operations; binary/image/document handling never falls back to local disk. Host repo operations remain default when the client fs is absent.
116
117
  - **Spawnable ACP coding registry (Task 6).** `@arnilo/prism-acp-agent` wires `createAcpClientFilesystem` and creates a separate coding tool registry per ACP session when the client advertises `fs/read_text_file` or `fs/write_text_file`. That session's `read`/`write`/`edit` operations use editor buffers; without fs advertisement, the existing disk registry is used. `shell`, repository search/list/glob, `delete`, and `move` remain disk-backed in this hybrid mode. Durable approvals resolve the same per-session agent, so one session cannot resume through another session's buffer adapter.
117
118
  - **Modes and config options are a pure host overlay.** The agent stores only a thin per-session registry; `apply`/`onChange` hooks narrow the host's own behavior. Mode switches can narrow or host-authorized widen — never a parallel policy evaluator, never a client-enabled tool.
118
- - **Lifecycle wiring.** Pass your `createCodingLifecycleEmitter()` as `coding.lifecycle`; `file_changed` etc. then flow to streaming sessions. `configuration_changed` broadcasts `config_option_update` (agent-message fallback if the SDK rejects the kind).
119
+ - **Lifecycle wiring.** Pass your `createCodingLifecycleEmitter()` as `coding.lifecycle`; `file_changed` etc. then flow to streaming sessions. Call `observeSupervisorLifecycle(supervisor, { onEvent: lifecycle.emit })` for redacted child start/stop milestones. `configuration_changed` broadcasts `config_option_update` (agent-message fallback if the SDK rejects the kind).
119
120
  - **Stream budgets.** Every lifecycle update counts against the same per-run stream event/byte budget as prompt updates; overflowing closes the update, never the run.
120
121
 
121
122
  ### Persistence and ownership
package/docs/ag-ui.md CHANGED
@@ -31,6 +31,7 @@ npm install @arnilo/prism @arnilo/prism-ag-ui
31
31
  | `authorize` | Rebinds untrusted AG-UI thread/run selectors to host ownership on every request. `false` returns 403. |
32
32
  | `sessionFactory` | Returns an authorized Prism `AgentSession`; it receives only host-approved `AgUiPreparedInput`, never raw client tools/state. |
33
33
  | `input.project` | Opts into full `RunAgentInput`; turns bounded, still-untrusted history, state, context, forwarded props, media, and lineage into host-selected Prism `Message` values. Omit for legacy final-text mode. |
34
+ | `inputPolicy.clientState` (`AgUiInputPolicyOptions`) | `"honor"` (default) passes validated client `state`/`tools` to `input.project`. `"ignore"` validates the same envelope for shape and bounds, then discards both fields, so input comes only from the server session and projector; an unknown value fails at construction with `ERR_PRISM_AG_UI_INPUT` instead of silently honoring the client. |
34
35
  | `input.frontendTools` | Explicitly selects client-side handoffs. Returned names must be request-tool subset; adapter never turns JSON tool declarations into Prism `ToolDefinition`s. |
35
36
  | `mcp` | Optional `createAgUiMcpAdapter({ bridge, select })`; host selects reviewed `bridge.tools`, then `sessionFactory` receives them as `input.serverTools`. Normal Prism dispatch/loop remains sole executor. |
36
37
  | `a2a` | Optional `createAgUiA2AAdapter({ client, select, correlate })`; verified remote A2A task stream replaces this handler's local session only. Host selection/correlation binds each remote task to ownership. |
@@ -44,13 +45,17 @@ npm install @arnilo/prism @arnilo/prism-ag-ui
44
45
 
45
46
  The handler accepts only `POST` JSON validated with official AG-UI `RunAgentInputSchema`. Every aggregate is bounded before a callback runs. With no `input.project`, it preserves compatibility: final text user message only; non-empty state or frontend tools fail before authorization/session lookup. With a projector, all current roles/history, context, state, forwarded props, multimodal parts, parent lineage, and tool-result continuations are available as untrusted input. The projector must apply Prism media URL/SSRF/MIME policy before forwarding media. Start a run with no `resume` and no `?cursor=`; replay supplies `?cursor=`.
46
47
 
48
+ ### Server-authoritative input (`inputPolicy.clientState: "ignore"`)
49
+
50
+ An official browser client posts its own projection: `initialState` becomes the request `state` and `runAgent({ tools })` supplies a tool list. Hosts that keep projection and tools on the server had two options — relay/rewrite every request, or reject state posts and break every browser run. `inputPolicy: { clientState: "ignore" }` is the third: the posted envelope is still schema-validated and bounded (so malformed, oversized, and poisonous payloads fail exactly as before, `400`/`413`), and then client `state` and `tools` are dropped before authorization, `coWorkContext`, `mcp.prepare`, `input.project`, and `defaultAgUiInput` see the input. Input is derived solely from the server session and the host projector, so a browser posting full state gets a normal run whose projection and tool list are the server's own. `AgUiPreparedInput.clientState` reports which policy produced the payload, `frontendTools` stays empty, and `capabilities.tools.clientProvided` is refused under `"ignore"` because the handler never hands client tools to a session. `state`, `context`, and `forwardedProps` reaching the projector under `"honor"` remain untrusted: the projector is still the only authority.
51
+
47
52
  ## Outputs / response / events
48
53
 
49
54
  The handler returns `text/event-stream`, one `data: <AG-UI event>\n\n` frame per output. Mapper lifecycle is ordered: `RUN_*`, `STEP_*`, `TEXT_MESSAGE_*`, and `TOOL_CALL_*` are deterministic Prism mappings. Host projectors may additionally prove and emit `STATE_SNAPSHOT`/`STATE_DELTA`, `MESSAGES_SNAPSHOT`, `ACTIVITY_*`, current `REASONING_*`, `RAW`, and named `CUSTOM` values.
50
55
 
51
- `delegated_agent_step` maps by default to bounded `ACTIVITY_SNAPSHOT` metadata with activity type `prism.delegated_agent_step`; `includeCustomEvents: true` also emits `CUSTOM prism.delegated_agent_step`. The safe payload contains adapter/conversation identifiers, step index/state/kind, duration, token counts, tool/subagent names, and opaque detail references only. Normal assistant text remains `TEXT_MESSAGE_*`; delegated events never duplicate transcript text. Raw event bodies, tool arguments/results, paths, URIs, logs, and hidden thought text remain absent unless a host explicitly supplies a projection. All values revalidate against official `EventSchemas`; deprecated `THINKING_*` and convenience chunk events are not produced. Active message/tool/reasoning/step sequences close before error, interruption, or finish.
56
+ `delegated_agent_step` maps by default to bounded `ACTIVITY_SNAPSHOT` metadata with activity type `prism.delegated_agent_step`; `includeCustomEvents: true` also emits `CUSTOM prism.delegated_agent_step`. The safe payload contains adapter/conversation identifiers, step index/state/kind, duration, token counts, tool/subagent names, and opaque detail references only. Coding hosts can supply `observeSupervisorLifecycle(..., { delegatedAgentStep })` output here for supervisor child activity. Normal assistant text remains `TEXT_MESSAGE_*`; delegated events never duplicate transcript text. Raw event bodies, tool arguments/results, paths, URIs, logs, and hidden thought text remain absent unless a host explicitly supplies a projection. All values revalidate against official `EventSchemas`; deprecated `THINKING_*` and convenience chunk events are not produced. Active message/tool/reasoning/step sequences close before error, interruption, or finish.
52
57
 
53
- A Prism durable `agent_suspended` returns `RUN_FINISHED` with core interrupt id `${runId}:${version}` and a strict `{ decision: "approve" | "deny" }` schema. `projection.interrupt` may attach bounded expiry/metadata or additional host policy interrupts but must retain that core id. Without `interrupts.resume`, one exact entry is required; `cancelled` means deny. An aggregate policy may validate bounded multiple entries, then returns one current-version core decision. Payloads containing `editedArgs`/`args` always deny: Prism does not mutate persisted tool calls. The adapter checks host authorization, selected run, suspended status, and checkpoint version, then calls `AgentRunLifecycle.resumeStream()` once. Claimed/dispatched tools are never replayed.
58
+ A Prism durable `agent_suspended` returns `RUN_FINISHED` with core interrupt id `${runId}:${version}` and a strict `{ decision: "approve" | "deny" }` schema (extended with `editedArgs`/`modifiedArguments`, `approvalId`, and `reason` when `capabilities.humanInTheLoop.approveWithEdits` is enabled). `projection.interrupt` may attach bounded expiry/metadata or additional host policy interrupts but must retain that core id. Without `interrupts.resume`, one exact entry is required; `cancelled` means deny. An aggregate policy may validate bounded multiple entries, then returns one current-version core decision. When `approveWithEdits` is configured, validated client edits map directly to core `RunDecision` with `outcome: "allow_once"` and `modifiedArguments`, executing revalidation against tool schemas under single atomic CAS. When `approveWithEdits` is omitted or false (default), payloads containing `editedArgs`/`args`/`modifiedArguments` safely fail closed and deny. The adapter checks host authorization, selected run, suspended status, and checkpoint version, then calls `AgentRunLifecycle.resumeStream()` once. Claimed/dispatched tools are never replayed.
54
59
 
55
60
  `createPersistenceAgUiReplay()` remains a compatible page adapter. `createAgentEventSourceAgUiReplay()` resolves exact ownership/run once per open, then consumes the shared durable source through terminal or live follow; it never attaches replica-local `session.subscribe()`. Every record must already be redacted. Mapped events carry stable `prismEventId` and bounded opaque `prismCursor`; records with no standard mapping emit `CUSTOM prism.replay_cursor`, so clients can persist progress. Terminal replay never creates a session or reruns a provider/tool.
56
61
 
@@ -84,7 +89,9 @@ Co-work uses bounded, redacted `CUSTOM prism.cowork.*` events through `mapCoWork
84
89
 
85
90
  ## Request/response example
86
91
 
87
- Resume a default single interrupt with `resume: [{ "interruptId": "run-1:4", "status": "resolved", "payload": { "decision": "approve" } }]`. Full history, client tool results, and mutable state need authorized `input.project` selection. This adapter is not a conversation database.
92
+ Resume a default single interrupt with `resume: [{ "interruptId": "run-1:4", "status": "resolved", "payload": { "decision": "approve" } }]`.
93
+ When `approveWithEdits` is advertised and configured, clients can approve with edited arguments: `resume: [{ "interruptId": "run-1:4", "status": "resolved", "payload": { "decision": "approve", "editedArgs": { "path": "approved.txt" } } }]` (or `modifiedArguments`). The edits map directly to core `RunDecision` (`outcome: "allow_once"`, `modifiedArguments`), validated against the tool schema under CAS `expectedVersion`.
94
+ Full history, client tool results, and mutable state need authorized `input.project` selection. This adapter is not a conversation database.
88
95
 
89
96
  ## Implementation example
90
97
 
@@ -30,11 +30,12 @@ Do not use the bundle loader to discover providers — provider/model packages s
30
30
  | `model?` | `ModelConfig` object, or a `"<provider>/<model>"` string resolved through `registries.models`. Optional at authoring time: when omitted, resolution falls back to `context.overrides.model` (host-injected selection); an explicit definition `model` drives registry resolution, and neither present fails closed with `Agent "<name>" has no model`. |
31
31
  | `tools?` | Tool names to activate from the active tool registry / `registries.tools`. Omitted means no active tools unless `activateAllCapabilities: true` is passed for migration. |
32
32
  | `skills?` | Skill names resolved via `resolveActiveSkills()`; omitted means no active skills unless `activateAllCapabilities: true` is passed for migration. `toolNames` enforcement applies at activation. |
33
- | `context?` | Context provider names from `registries.contextProviders`. |
33
+ | `context?` | Context provider names from `registries.contextProviders`. A host registers whichever providers it wants under its own names — for example a memory fabric's provider as `"memory-fabric"`; the definition contract itself carries no provider-specific field (see [Memory fabric](memory-fabric.md)). |
34
34
  | `systemPrompt?` | `SystemPromptConfig` layer (see [System prompts](system-prompts.md)). |
35
35
  | `instructions?` | Base prompt text. |
36
36
  | `loop?` | `AgentLoopStrategy` or `AgentLoopOptions` (see [Agent loops](agent-loops.md)). |
37
37
  | `metadata?` | Free-form metadata. |
38
+ | `attentionCompiler?` | Opt-in attention compiler for the resolved config: `true` for defaults, an `AttentionCompilerOptions` object to tune ratios/depth (see [Attention compiler](attention-compiler.md)). Copied verbatim onto `AgentConfig.attentionCompiler` and nothing else; `RunOptions.attentionCompiler` may later disable or relax it. |
38
39
  | `create?(config?)` | Optional escape hatch. When present, overrides declarative resolution: the helper builds a base `AgentConfig` from the declarative fields, calls `create(config)`, then merges `context.overrides`. |
39
40
 
40
41
  ### `AgentDefinitionResolutionContext` (contract, `@arnilo/prism`)
@@ -116,6 +117,12 @@ instructions: You are a careful coding agent.
116
117
  Prefer minimal diffs. Cite the file you changed.
117
118
  ```
118
119
 
120
+ `context` names are whatever the host registered in `registries.contextProviders` — for example
121
+ `registries.contextProviders.register("memory-fabric", fabric.createContextProvider())` makes
122
+ `context: [memory-fabric]` work, while an unregistered name fails closed at resolution. Nothing about
123
+ the fabric is required by `AgentDefinition`; a host that never registers a provider resolves the same
124
+ definition unchanged.
125
+
119
126
  `discoverAgentBundles({ configRoot })` returns (paths only):
120
127
 
121
128
  ```json
@@ -244,6 +251,7 @@ Use `activateAllCapabilities: true` only while migrating old configs. It intenti
244
251
  - [System prompts](system-prompts.md): `composeSystemPrompt` source ranks and the `AGENT.md` body / `SYSTEM.md` / `AGENTS.md` prompt layering reused by `resolveAgentBundle`.
245
252
  - [Contribution discovery (workspace)](contribution-discovery.md): `discoverContributions` for repo `.agents/` contributions passed as `repoContributions`.
246
253
  - [Context and skills](context-and-skills.md): `resolveActiveSkills` and `RunOptions.activeSkills` activation that consumes discovered skills.
254
+ - [Memory fabric](memory-fabric.md): optional `fabric.createContextProvider()` registered as a `context` name, and `fabric.attach(session)` gating its tools and workers.
247
255
  - [Tools](tools.md): `ToolDefinition` / `(toolNames)` enforcement and host-owned tool execution.
248
256
  - [Agent loops](agent-loops.md): `resolveLoop` and loop strategies passed via `loop` / `context.overrides`.
249
257
  - [Extensions](extensions.md): `registerAgent()` programmatic registration of inert `AgentDefinition` values.
@@ -105,6 +105,8 @@ Agent / turn / message events:
105
105
 
106
106
  Adapters should call `createDelegatedAgentStep({ sessionId, runId, adapterId, externalConversationId, stepIndex, state, kind, usage })` rather than forwarding external JSON. The constructor allow-lists fields and fails closed on malformed or oversized identifiers/counters.
107
107
 
108
+ Coding hosts call `observeSupervisorLifecycle(supervisor, { onEvent, delegatedAgentStep })` to turn supervisor milestones into `subagent_started` / `subagent_stopped` coding lifecycle events. Both carry only redacted `childId`, `delegationId`, and `depth`; stopped events add terminal `AgentRunStatus`. Supplying `delegatedAgentStep` emits the bounded `delegated_agent_step` records AG-UI already maps. Child inputs, outputs, paths, and delegation error text never cross either bridge.
109
+
108
110
  `message_delta.content.type === "tool_call_delta"` carries `{ index, id?, name?, argumentsText? }`. Treat it as a streaming fragment. The runtime reconstructs and persists a final `tool_call` before executing tools. Deltas missing `id`/`name` at stream end fail the provider turn with `ErrorInfo.code: "incomplete_delta"` (typed `ProviderTransportError`); they never throw a bare `Error`. Malformed JSON with id+name present recovers as a blocked tool result (`invalid_json_arguments`) instead.
109
111
 
110
112
  Tool execution events:
@@ -134,6 +136,7 @@ Queue / subscriber / compaction / retry / provider events:
134
136
  | `event_subscriber_overflow` | `sessionId`, `droppedEvents: number`, `maxQueuedEvents: number`, `overflow: "close" \| "drop_oldest" \| "drop_newest"` |
135
137
  | `compaction_started` | `sessionId`, `runId?` |
136
138
  | `compaction_finished` | `sessionId`, `runId?`, `summary: string` |
139
+ | `attention_compiled` | `sessionId`, `runId?`, `used: number`, `usedAfter: number`, `inputCap: number`, `triggerRatio: number`, `droppedThinkingTurns: number`, `stubbedToolResults: number`, `stubbedBytes: number`, `truncated: boolean` — one per mutated turn of the opt-in [attention compiler](attention-compiler.md); counts only, never message text |
137
140
  | `retry_scheduled` | `sessionId`, `runId`, `attempt: number`, `delayMs: number`, `error: ErrorInfo` |
138
141
 
139
142
  Provider turn events (metadata only — see [Observability](observability.md)):
@@ -251,4 +254,4 @@ for await (const event of session.stream("draft", { loop: { strategy: "generate-
251
254
  - [Observability](observability.md): `ProviderTurnMetadata`; optional adapter builds one parented GenAI span tree from metadata-only lifecycle events and ignores message/progress deltas.
252
255
  - [Tools](tools.md): `tool_execution_*` variants.
253
256
  - [Compaction and retry policies](compaction-and-retry.md): `compaction_*` and `retry_scheduled` variants.
254
- - [Frontend interoperability (AG-UI and ACP)](ag-ui.md): optional redacted mapping of this stream; durable replay is ledger-backed and at-least-once, never a live-subscriber substitute. [ACP coding-host interop](acp.md) additionally maps `CodingLifecycleEvent`s from `@arnilo/prism-coding-tools/agent` (`file_changed`, `worktree_changed`, `permission_denied`, `configuration_changed`, `plan_changed`, `plan_removed`; process events reuse `CodingProcessEvent`) into ACP session updates — locations/diff blocks only through projection allow-lists, terminal chunks under `process.outputChunkBytes`, plan updates only to clients that advertised the UNSTABLE `plan` capability.
257
+ - [Frontend interoperability (AG-UI and ACP)](ag-ui.md): optional redacted mapping of this stream; durable replay is ledger-backed and at-least-once, never a live-subscriber substitute. [ACP coding-host interop](acp.md) additionally maps `CodingLifecycleEvent`s from `@arnilo/prism-coding-tools/agent` (`file_changed`, `worktree_changed`, `permission_denied`, `configuration_changed`, `plan_changed`, `plan_removed`, `subagent_started`, `subagent_stopped`; process events reuse `CodingProcessEvent`) into ACP session updates — locations/diff blocks only through projection allow-lists, terminal chunks under `process.outputChunkBytes`, plan updates only to clients that advertised the UNSTABLE `plan` capability.
@@ -130,6 +130,39 @@ The snapshot is stored as `loopState: { name, revision, snapshot }` on the durab
130
130
 
131
131
  A strategy returned by `generateValidateReviseLoop()` is safe to reuse across sequential runs. Its built-in state is scoped to `(sessionId, runId)`; a new non-restored run resets attempts, artifact phase, saved schema, and pending repair messages, while a restored run keeps the checkpointed state. Arbitrary custom strategies are not cloned or reset automatically.
132
132
 
133
+ ## Turn policy
134
+
135
+ `RunOptions.turnPolicy` (`TurnPolicyOptions`) lets a host end a run **cleanly** at a provider-turn boundary — after the previous turn's tool results are persisted, before the next provider request (the same point `checkpointPolicy: "every-turn"` checkpoints at). This is the "stop when the agent has done enough" seam: an investigation that should stop at the first plan paint, a desk that stops after N turns, a policy that stops once a tool budget is spent.
136
+
137
+ ```ts
138
+ await session.run("Investigate the churn spike", {
139
+ turnPolicy: {
140
+ // Clean turn cap: reaching it stops the run instead of failing it.
141
+ maxTurns: 4,
142
+ // Consulted once per boundary; a stop ends the run as `succeeded`.
143
+ stop: (ctx) =>
144
+ ctx.turns >= 1 && ctx.toolCalls >= 1
145
+ ? { action: "stop", reason: "l1-first-plan-paint" }
146
+ : { action: "continue" },
147
+ },
148
+ });
149
+ ```
150
+
151
+ | `TurnBoundaryContext` field | Meaning |
152
+ | --- | --- |
153
+ | `sessionId`, `runId` | Run correlation. |
154
+ | `turn` | 1-based index of the provider turn this boundary precedes. |
155
+ | `turns` | Provider turns already completed (`turn - 1`; `0` at the first boundary). |
156
+ | `toolCalls` | Host tool calls dispatched so far in this run. |
157
+ | `usage` | Run-total usage so far, when the provider reported any. |
158
+ | `metadata` | Run metadata (never prompt text, tool arguments, or results). |
159
+
160
+ A `stop` decision is a **clean terminal outcome**, not an error or a limit breach: the run returns `status: "succeeded"` with `stopReason: "host_policy"` and `stopDetail` (the host's `reason`, ≤256 UTF-8 bytes, redacted). The same pair rides `agent_finished.finishReason`/`stopDetail`, the finish `RunRecord`, and the projected `ExecutionTimeline`. `turnPolicy.maxTurns` is a *clean* cap: it reports `stopReason: "turn_limit"` and, unlike a `limits.maxTurns` breach, never throws `AgentRunLimitError`. A run overlay may only narrow `limits.maxTurns` — widening throws before the first provider turn.
161
+
162
+ A policy stop stays **resumable**: with `runState: { checkpointPolicy: "every-turn" }` the terminal state keeps the run frontier, so `resumeAgentRun(..., { decision: "continue" })` continues from the boundary. Steers queued before the stop are already in the session history and reach the resumed leg exactly once. A `turnPolicy.maxTurns` stop is the exception — resuming it would re-stop on the first boundary. Resumed runs carry no `turnPolicy` (resume options are not run options), so a continued leg runs to its natural end unless the host stops it again.
163
+
164
+ The callback is synchronous and bounded, and is never called when `turnPolicy` is omitted: a run without a policy keeps its exact request stream. A callback that throws or returns a malformed decision fails the run closed with `ERR_PRISM_TURN_POLICY` (the boundary makes no provider call and the checkpoint stays fail-closed); a stopped run is never recorded as failed.
165
+
133
166
  ## Outputs / response / events
134
167
 
135
168
  `AgentLoopStrategy.run(ctx)` returns `Promise<Usage | undefined>` as a fallback for custom loops. Core runtime independently accumulates every usage-bearing provider turn in O(turns), persists scoped turn/run rows, and emits `agent_finished` with the aggregate.
@@ -39,7 +39,7 @@ createAgentSession(config: AgentSessionConfig & { agent: Agent }): AgentSession
39
39
 
40
40
  `AgentConfig.provider` must contain the host-selected provider. Prism does not resolve providers from hidden globals. Alternatively, set `AgentConfig.providerSource: ProviderResolver` (or override per run with `RunOptions.providerSource`, which wins) to resolve the provider from `model.provider` each run; when `AgentConfig.provider` is set it takes first precedence and the resolver is bypassed. See [Provider layer § Provider resolver](provider-layer.md#provider-resolver).
41
41
 
42
- `session.run(input, options)` accepts the existing Prism input shape:
42
+ `session.run(input, options)` accepts the existing Prism input shape. `RunOptions.toolNames` optionally allow-lists registered tool names for that run (omit = full registry; empty = none). See [Tools](tools.md#per-run-tool-scoping).
43
43
 
44
44
  ```ts
45
45
  string | Message | readonly Message[]
@@ -51,7 +51,7 @@ string | Message | readonly Message[]
51
51
 
52
52
  `RunOptions.model` can override the request model for a run. Model overrides append a `model_change` entry. `AgentConfig.inputLayout` selects the default input assembly layout (`"cache_aware"` by default, or opt-in `"legacy"`); `RunOptions.inputLayout` wins for one run. `AgentConfig.thinkingLevel` / `RunOptions.thinkingLevel` (run wins) is the session thinking intent — Prism snaps it onto the request after host `providerOptions`. `AgentConfig.providerOptions`/`RunOptions.providerOptions` supply generic provider request options (session/cache/header/compat/extra hints only — provider-level timeout/retry hints were removed in 0.1.5). Kernel construction always stamps `options.sessionId`/`cacheKey` from `session.id` when missing; `createSessionCachePolicy` is an overlay, not required. Use `RunOptions.signal`/host abort controllers for timeouts and `AgentConfig.retry`/`RunOptions.retry` for retry. `AgentConfig.providerRequestPolicies`/`RunOptions.providerRequestPolicies` run before `AIProvider.generate()` and before `provider_request` middleware. `AgentConfig.systemPrompt` and `RunOptions.systemPrompt` add explicit layered system prompt contributions; `RunOptions.systemPrompt: false` disables configured prompt layers for that run while keeping `AgentConfig.instructions` as the base path. `RunOptions.compaction` can enable auto-compaction for that run or use `false` to disable configured auto-compaction. `RunOptions.retry` can enable provider-turn retry for that run or use `false` to disable configured retry. `RunOptions.metadata` is merged with agent/session metadata for assembly, provider requests, and tool contexts. Run tool-round limits via `RunOptions.limits.maxToolRounds`. `RunOptions.signal` is bridged into the per-run abort signal passed to assembly, providers, tools, auto-compaction, and retry backoff.
53
53
 
54
- `RunOptions.activeSkills` selects named skills from a configured `SkillRegistry`; `RunOptions.skills` replaces a plain `Skill[]` config for one run. When `AgentConfig.skills` is a registry and neither is set, **no skills activate** unless `activateAllSkills: true` (run or agent). `skillsDisclosure` (`"progressive"` default, `"eager"` opt-in; run wins) controls catalog vs full instruction bodies; the session-owned `LoadedSkillSet` is populated by `load_skill` when the host registers `createLoadSkillTool`. `toolResultFold` (off unless the host supplies `summarize`) optionally folds aged large tool results in provider input only. See [Context and skills](context-and-skills.md).
54
+ `RunOptions.activeSkills` selects named skills from a configured `SkillRegistry`; `RunOptions.skills` replaces a plain `Skill[]` config for one run. When `AgentConfig.skills` is a registry and neither is set, **no skills activate** unless `activateAllSkills: true` (run or agent). `skillsDisclosure` (`"progressive"` default, `"eager"` opt-in; run wins) controls catalog vs full instruction bodies; the session-owned `LoadedSkillSet` is populated by `load_skill` when the host registers `createLoadSkillTool`. `toolResultFold` (off unless the host supplies `summarize`) optionally folds aged large tool results in provider input only. `AgentConfig.attentionCompiler` (or `true` for defaults) opts into the per-turn attention compiler; `RunOptions.attentionCompiler: false` disables it for one run and an options object may only relax the agent setting — the session resolves it with the run's model before the first provider turn, and keeps one sticky frontier per session so a stub made once stays applied. See [Context and skills](context-and-skills.md) and [Attention compiler](attention-compiler.md).
55
55
 
56
56
  ## Outputs / response / events
57
57
 
@@ -61,7 +61,7 @@ string | Message | readonly Message[]
61
61
 
62
62
  `session.stream(input, options?)` subscribes first, starts exactly one run, yields only that run's events, and terminates when the run succeeds, fails, or aborts. Early consumer return aborts the owned run and releases the session. `SubscribeOptions.maxQueuedEvents` / `overflow` may be passed alongside `RunOptions`.
63
63
 
64
- `resumeAgentRunStream(agent, ref, resume, options?)` does the same for one existing suspended durable run. It validates checkpoint ownership, revision/fingerprint, and `expectedVersion`, then subscribes before emitting `agent_started` / `agent_resumed` and resumed message/tool/terminal events. `AgentRunResumeStreamOptions` combines existing resume options with `signal`, `maxQueuedEvents`, and `overflow`; early return aborts only resumed execution. It does not replay a claimed/dispatched tool, poll a ledger, or retain a worker. `createAgentRunLifecycle().resumeStream(ref, resume, request?)` adds the same behavior after host agent-capability resolution.
64
+ `resumeAgentRunStream(agent, ref, resume, options?)` does the same for one existing suspended durable run. It validates checkpoint ownership, revision/fingerprint, and `expectedVersion`, then subscribes before emitting `agent_started` / `agent_resumed` and resumed message/tool/terminal events. `AgentRunResumeStreamOptions` combines `AgentRunResumeOptions` (including the optional `onSession` observer seam a supervisor uses to attach a child event pump to the rebuilt session) with `maxQueuedEvents` and `overflow`; early return aborts only resumed execution. Since 0.8.0 (plan 080 Task 3), `AgentRunResumeOptions.signal` is inherited by both entrypoints, so `resumeAgentRun()` aborts a live resumed provider/tool turn the same way `resumeAgentRunStream()` does — checked before each preparation step and threaded into the resumed execution. It does not replay a claimed/dispatched tool, poll a ledger, or retain a worker. `createAgentRunLifecycle().resumeStream(ref, resume, request?)` adds the same behavior after host agent-capability resolution.
65
65
 
66
66
  `session.subscribe(options?)` remains available for hosts that want a long-lived subscriber across runs. Subscribe before `run()` to observe that run's events. The consumer loop and `session.run()` must run concurrently (e.g. start the `for await` consumer, then `await Promise.all([consumer, session.run("Hi")])`): events are only emitted during a live run, so awaiting the subscribe loop before calling `run()` deadlocks. Prefer `session.stream()` when you only need one run's events. `SubscribeOptions.maxQueuedEvents` defaults to `1024` (minimum `1`) and caps events queued while the consumer is not awaiting `next()`. `SubscribeOptions.overflow` defaults to `"close"`; it clears queued payload events, delivers one `event_subscriber_overflow` notice to that subscriber, then closes it. `"drop_oldest"` keeps newest events; `"drop_newest"` ignores new events while full.
67
67
 
@@ -79,7 +79,7 @@ For tool calls, the runtime streams provider `tool_call_delta` fragments as `mes
79
79
 
80
80
  Provider `thinking`/`reasoning` content emitted during a turn is preserved as `thinking` content blocks on the assistant message in session history. On the next turn, provider packages decide how to carry prior reasoning forward. For example, the NeuralWatt provider serializes prior `thinking` blocks under a `reasoning_content` field for reasoning-capable models (gated on `capabilities.reasoning` / `compat.preserve_thinking`, droppable via `compat.clear_thinking`); see [NeuralWatt provider](providers/neuralwatt.md). Non-reasoning providers/models receive no reasoning field, so prior thinking does not leak into providers that do not support it.
81
81
 
82
- `session.compact(options?)` runs the selected compaction strategy, appends one `kind: "compaction"` entry under the current leaf, updates the leaf, emits `compaction_started` and `compaction_finished`, and returns the appended `CompactionResult`. If `AgentConfig.compaction` or `RunOptions.compaction` includes `thresholdEntries`, auto-compaction checks once after input/model-change entries are appended and before provider input assembly; `RunOptions.compaction: false` skips that run's auto-compaction.
82
+ `session.compact(options?)` runs the selected compaction strategy, appends one `kind: "compaction"` entry under the current leaf, updates the leaf, emits `compaction_started` and `compaction_finished`, and returns the appended `CompactionResult`. If `AgentConfig.compaction` or `RunOptions.compaction` includes `thresholdEntries` (entry count) or `trigger` (entry count, input ratio against the compiler's cap, or a host callback), auto-compaction checks once after input/model-change entries are appended and before provider input assembly; `RunOptions.compaction: false` skips that run's auto-compaction. A branch that already ends with a `kind: "compaction"` entry is left alone.
83
83
 
84
84
  `entries()` returns the current branch entries. `checkout(leafId?)` moves the session to an existing leaf and rebuilds history. `fork()` returns a session on the same store/session id at the selected leaf without copying entries. `clone({ id })` copies the current branch to a new session id with new entry ids.
85
85
 
@@ -186,11 +186,12 @@ Set `runState` with a host-owned `CheckpointStore`, stable `definitionRevision`,
186
186
  `resumeAgentRun` accepts exactly one of:
187
187
 
188
188
  - `decision: "approve" | "deny"` — legacy single-approval path. `approve` allows every pending decision once; `deny` terminates the run as `denied`.
189
+ - `decision: "continue"` — crash recovery for a running-state checkpoint written by [`checkpointPolicy: "every-turn"`](durable-runs.md): resumes from the last provider-turn boundary without re-dispatching tools. It requires a running state and never bypasses a gate — a suspended run still needs `approve`/`deny` or a decision batch.
189
190
  - `decisions: readonly RunDecision[]` — one atomic batch. Every entry validates against the recorded pending set (unknown/foreign `approvalId`, duplicates, stale `expectedVersion`, invalid outcomes fail the whole batch closed with `AgentDecisionError` and leave state and version untouched). Outcomes: `allow_once`, `allow_for_run`, `reject_once`, `reject_for_run`. `reject_*` continues the run with a blocked tool result carrying the bounded (2 KB) `reason`. `modifiedArguments` are revalidated (schema, then input guardrails; permission/trust re-run at dispatch) and produce a new arguments hash. `elicitation` payloads are validated against the pending decision's `elicitationSchema` (required keys plus the configured host validator) and resolve the suspended call without executing it. A batch deciding a strict subset persists the decided entries and re-suspends with the remainder pending at the bumped version.
190
191
 
191
- `*_for_run` outcomes append a `StickyDecision` to the durable run state: later calls in the same run matching the scope exactly (all recorded fields) proceed or are blocked without a new suspension, policy still enforced at dispatch. Sticky decisions expire when the run reaches any terminal status. Caps: 32 pending decisions per run (hard 128), 64 sticky decisions (hard 256), 2 KB decision reasons, 16 KB elicitation payloads.
192
+ `*_for_run` outcomes append a `StickyDecision` to the durable run state: later calls in the same run matching the scope exactly (all recorded fields) proceed or are blocked without a new suspension, policy still enforced at dispatch. Sticky decisions expire when the run reaches any terminal status. Caps: 32 pending decisions per run (hard 128), 64 sticky decisions (hard 256), 2 KB decision reasons, 16 KB elicitation payloads. Frontend adapters (such as AG-UI with `capabilities.humanInTheLoop.approveWithEdits`) and the server resume endpoint (`POST .../resume` with `modifiedArguments`) map human edits directly to `RunDecision` entries with `modifiedArguments` under single atomic CAS, revalidating tool parameter schemas and invalidating stale draft approvals.
192
193
 
193
- **Runtime input validation (0.2.0, plan 020 Task 2).** Every public resume entrypoint (`resumeAgentRun`, `resumeAgentRunStream`, `AgentRunLifecycle.resume()`/`resumeStream()`) validates the complete resume input in core before any checkpoint read/write, agent resolution, subscription, or tool execution: a non-null object, positive safe-integer `expectedVersion`, exactly one of `decision`/`decisions`, legacy `decision` exactly `approve`/`deny`, and a non-empty batch ≤ 128 entries whose entries are objects with a bounded non-empty `approvalId`, a whitelisted outcome, an optional string `reason` within the 2 KB limit, and JSON-object `modifiedArguments`/`elicitation` within the 16 KB limit. Unknown legacy decisions (e.g. `"sideways"`) and malformed untyped batches fail closed with `AgentDecisionError` (`ERR_PRISM_DECISION_INVALID`/`..._LIMIT`/`..._DUPLICATE`) under a **no-side-effect guarantee**: zero checkpoint writes/CAS changes, zero tool calls, zero resumed events. This holds for plain-JavaScript and `as any` callers; the server's transport parser is defense in depth, not the security boundary. State-dependent checks (foreign/stale approval ids, scope, schema, policy) still run in the atomic batch resolver.
194
+ **Runtime input validation (0.2.0, plan 020 Task 2).** Every public resume entrypoint (`resumeAgentRun`, `resumeAgentRunStream`, `AgentRunLifecycle.resume()`/`resumeStream()`) validates the complete resume input in core before any checkpoint read/write, agent resolution, subscription, or tool execution: a non-null object, positive safe-integer `expectedVersion`, exactly one of `decision`/`decisions`, legacy `decision` exactly `approve`/`deny`/`continue`, and a non-empty batch ≤ 128 entries whose entries are objects with a bounded non-empty `approvalId`, a whitelisted outcome, an optional string `reason` within the 2 KB limit, and JSON-object `modifiedArguments`/`elicitation` within the 16 KB limit. Unknown legacy decisions (e.g. `"sideways"`) and malformed untyped batches fail closed with `AgentDecisionError` (`ERR_PRISM_DECISION_INVALID`/`..._LIMIT`/`..._DUPLICATE`) under a **no-side-effect guarantee**: zero checkpoint writes/CAS changes, zero tool calls, zero resumed events. This holds for plain-JavaScript and `as any` callers; the server's transport parser is defense in depth, not the security boundary. State-dependent checks (foreign/stale approval ids, scope, schema, policy) still run in the atomic batch resolver.
194
195
 
195
196
  ```ts
196
197
  const result = await session.run("Publish draft", {
@@ -203,7 +204,7 @@ if (result.status === "suspended") {
203
204
  }
204
205
  ```
205
206
 
206
- Resume requires exact checkpoint ownership, version, agent fingerprint, and revision. The fingerprint hashes the agent id/name, `definitionRevision`, model, instructions, system-prompt contributions, skills (name/instructions/tool names), tool definitions (name/parameters/exclusive), guardrail definitions (name/stage/revision), and loop strategy — changing any of them without bumping `definitionRevision` fails resume closed instead of silently continuing with different agent semantics. Prism CAS-claims approval before work, rechecks normal guardrail/permission/validation/limit paths, and marks a pending tool dispatched before its side effect. `createAgentRunLifecycle()` wraps the same core path for server/MCP hosts: adapters pass only authorized ownership, status returns only `{ state, version }`, and `resolveAgent()` supplies current agent/revision. `resumeStream()` uses that same claim path and bounded subscriber, so adapters do not poll or duplicate resume logic. Remote restart requires both checkpoint and session stores to be durable. A crash after that mark is ambiguous and is never replayed automatically; use host tool idempotency keyed by `runId`/`toolCallId` or resolve it manually. Checkpoints contain bounded redacted state plus session/leaf references, never provider objects, callbacks, signals, credentials, or raw secrets. State is bounded at save by `runState.maxStateBytes` (default 256 KB, at most the 1 MB hard cap); load bounds against the 1 MB hard cap only, so state saved with a raised limit stays resumable while oversized records are still rejected. Since 0.1.3 (plan 015 Task 4), durable runs may opt in to session-state persistence with `persistSessionState: true` on both the run and resume options: the loaded-skill **name catalog** (≤64 names, ≤256 chars each) rides the checkpoint and is restored into the resumed session's `LoadedSkillSet`; skill **bodies are never persisted** and re-resolve from the live registry via `load_skill`. Since 0.1.6 (plan 018 closeout `checkpoint-bodies`), `includeSkillBodies: true` on BOTH the run and resume options additionally persists the exact loaded-skill **instructions** (`{name, instructions}` pairs, redacted at the checkpoint boundary like all state, ≤64 bodies / ≤256-char names / ≤262144-byte bodies / ≤1 MiB total) so resume re-renders them registry-independently — no `load_skill` round-trip and no dependence on the registry still serving the same text; `maxStateBytes` (default 256 KB) refuses oversize bodies with a recorded error, never silently truncates. Default off keeps the checkpoint shape byte-identical to 0.1.3. Built-in loop options are durable; custom `AgentLoopStrategy` instances are durable when they declare `snapshot`/`restore` hooks (see [Agent loops § Durable runs](agent-loops.md#durable-runs)) and reject before provider work otherwise.
207
+ Resume requires exact checkpoint ownership, version, agent fingerprint, and revision. A checkpoint load or delete under a non-matching ownership scope reads as absent (`null`), and a save against a foreign-owned record fails as a generic `ERR_PRISM_CHECKPOINT_CONFLICT` (plan 080 Task 3) — a tenant cannot distinguish “another tenant owns this key” from “missing”, and callers that relied on the old `Checkpoint ownership mismatch` throw now see the same miss they would for an unknown key. The fingerprint hashes the agent id/name, `definitionRevision`, model, instructions, system-prompt contributions, skills (name/instructions/tool names), tool definitions (name/parameters/exclusive), guardrail definitions (name/stage/revision), and loop strategy — changing any of them without bumping `definitionRevision` fails resume closed instead of silently continuing with different agent semantics. Prism CAS-claims approval before work, rechecks normal guardrail/permission/validation/limit paths, and marks a pending tool dispatched before its side effect. `createAgentRunLifecycle()` wraps the same core path for server/MCP hosts: adapters pass only authorized ownership, status returns only `{ state, version }`, and `resolveAgent()` supplies current agent/revision. `resumeStream()` uses that same claim path and bounded subscriber, so adapters do not poll or duplicate resume logic. Remote restart requires both checkpoint and session stores to be durable. A crash after that mark is ambiguous and is never replayed automatically; use host tool idempotency keyed by `runId`/`toolCallId` or resolve it manually. Checkpoints contain bounded redacted state plus session/leaf references, never provider objects, callbacks, signals, credentials, or raw secrets. State is bounded at save by `runState.maxStateBytes` (default 256 KB, at most the 1 MB hard cap); load bounds against the 1 MB hard cap only, so state saved with a raised limit stays resumable while oversized records are still rejected. Since 0.1.3 (plan 015 Task 4), durable runs may opt in to session-state persistence with `persistSessionState: true` on both the run and resume options: the loaded-skill **name catalog** (≤64 names, ≤256 chars each) rides the checkpoint and is restored into the resumed session's `LoadedSkillSet`; skill **bodies are never persisted** and re-resolve from the live registry via `load_skill`. Since 0.1.6 (plan 018 closeout `checkpoint-bodies`), `includeSkillBodies: true` on BOTH the run and resume options additionally persists the exact loaded-skill **instructions** (`{name, instructions}` pairs, redacted at the checkpoint boundary like all state, ≤64 bodies / ≤256-char names / ≤262144-byte bodies / ≤1 MiB total) so resume re-renders them registry-independently — no `load_skill` round-trip and no dependence on the registry still serving the same text; `maxStateBytes` (default 256 KB) refuses oversize bodies with a recorded error, never silently truncates. Default off keeps the checkpoint shape byte-identical to 0.1.3. Since 0.7.0 (plan 074 P3), `persistSessionState: true` also carries the opt-in [attention compiler](attention-compiler.md)'s sticky frontier (`sessionState.attentionSticky`: 32-hex thinking keys plus tool-call ids, newest 256 of each, redacted like all state) so a resumed run keeps its thinking strips and tool stubs instead of re-deciding its first turn from the ratio; a malformed frontier is dropped entry by entry and never blocks a resume. Since 0.7.0, `onSession` hands the reconstructed session to a caller-supplied observer before the resumed run starts, so an observer (the supervisor's child-event pump) can subscribe while the run is still live; it is called for every resume outcome, a throw fails closed before any event or tool work, and the session is valid only for the duration of that resume. Built-in loop options are durable; custom `AgentLoopStrategy` instances are durable when they declare `snapshot`/`restore` hooks (see [Agent loops § Durable runs](agent-loops.md#durable-runs)) and reject before provider work otherwise. For mid-run crash recovery (`checkpointPolicy: "every-turn"` plus `decision: "continue"`), see [Durable runs](durable-runs.md).
207
208
 
208
209
  ## Secure composition
209
210