@arnilo/prism 0.0.96 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/CHANGELOG.md +290 -2
  2. package/README.md +17 -3
  3. package/dist/agent-definitions.js +2 -3
  4. package/dist/agent-event-source.d.ts +11 -0
  5. package/dist/agent-event-source.js +512 -0
  6. package/dist/agent-loops.d.ts +5 -0
  7. package/dist/agent-loops.js +99 -14
  8. package/dist/agent-run-lifecycle.d.ts +5 -2
  9. package/dist/agent-run-lifecycle.js +18 -2
  10. package/dist/agent-run-state.d.ts +27 -1
  11. package/dist/agent-run-state.js +113 -7
  12. package/dist/agents.d.ts +3 -1
  13. package/dist/agents.js +1255 -129
  14. package/dist/artifacts.d.ts +132 -0
  15. package/dist/artifacts.js +44 -0
  16. package/dist/cache-helpers.js +18 -9
  17. package/dist/checkpoints.d.ts +4 -0
  18. package/dist/checkpoints.js +17 -9
  19. package/dist/cli-init.js +3 -7
  20. package/dist/cli-runner.d.ts +2 -6
  21. package/dist/cli-runner.js +71 -33
  22. package/dist/compaction.js +5 -4
  23. package/dist/config.js +7 -4
  24. package/dist/content.js +26 -24
  25. package/dist/context-budget.d.ts +67 -0
  26. package/dist/context-budget.js +288 -0
  27. package/dist/contracts.d.ts +590 -8
  28. package/dist/contracts.js +142 -1
  29. package/dist/contribution-parsing.js +6 -2
  30. package/dist/contributions.d.ts +2 -0
  31. package/dist/contributions.js +3 -0
  32. package/dist/conversations.d.ts +50 -0
  33. package/dist/conversations.js +98 -0
  34. package/dist/credentials.d.ts +22 -2
  35. package/dist/credentials.js +18 -3
  36. package/dist/devices.d.ts +94 -0
  37. package/dist/devices.js +138 -0
  38. package/dist/event-multiplexer.js +18 -4
  39. package/dist/extensions.d.ts +18 -1
  40. package/dist/extensions.js +79 -6
  41. package/dist/feedback.js +12 -10
  42. package/dist/guardrails.d.ts +1 -1
  43. package/dist/guardrails.js +26 -17
  44. package/dist/identity.d.ts +92 -0
  45. package/dist/identity.js +265 -0
  46. package/dist/index.d.ts +94 -72
  47. package/dist/index.js +48 -36
  48. package/dist/input.d.ts +10 -1
  49. package/dist/input.js +152 -52
  50. package/dist/instruction-injection.d.ts +1 -1
  51. package/dist/middleware.js +9 -1
  52. package/dist/models.d.ts +2 -0
  53. package/dist/models.js +3 -0
  54. package/dist/node/agent-definitions.js +16 -8
  55. package/dist/node/contribution-discovery.d.ts +1 -2
  56. package/dist/node/contribution-discovery.js +3 -3
  57. package/dist/node/session-store-jsonl.js +13 -7
  58. package/dist/node/settings.d.ts +1 -1
  59. package/dist/node/settings.js +1 -1
  60. package/dist/node/system-project-prompts.js +2 -4
  61. package/dist/node/trust.js +1 -1
  62. package/dist/persistence-lifecycle.d.ts +103 -0
  63. package/dist/persistence-lifecycle.js +202 -0
  64. package/dist/provider-events.d.ts +1 -0
  65. package/dist/provider-events.js +6 -1
  66. package/dist/provider-request-policy.js +3 -4
  67. package/dist/providers/media.d.ts +1 -1
  68. package/dist/providers/openai-compatible.d.ts +46 -1
  69. package/dist/providers/openai-compatible.js +123 -53
  70. package/dist/providers/openai-primitives.js +10 -7
  71. package/dist/providers/transport.d.ts +6 -0
  72. package/dist/providers/transport.js +21 -0
  73. package/dist/providers.d.ts +2 -0
  74. package/dist/providers.js +3 -0
  75. package/dist/redaction.d.ts +1 -0
  76. package/dist/redaction.js +26 -9
  77. package/dist/resources.d.ts +2 -2
  78. package/dist/resources.js +2 -2
  79. package/dist/retry.d.ts +5 -0
  80. package/dist/retry.js +8 -1
  81. package/dist/rpc.js +55 -11
  82. package/dist/run-ledger.d.ts +6 -0
  83. package/dist/run-ledger.js +16 -13
  84. package/dist/run-limits.js +49 -10
  85. package/dist/secure-agent.js +8 -2
  86. package/dist/security.js +7 -2
  87. package/dist/session-stores.d.ts +7 -2
  88. package/dist/session-stores.js +195 -21
  89. package/dist/skill-disclosure.d.ts +35 -0
  90. package/dist/skill-disclosure.js +101 -0
  91. package/dist/skill-load.d.ts +25 -0
  92. package/dist/skill-load.js +112 -0
  93. package/dist/structured-output.d.ts +5 -1
  94. package/dist/structured-output.js +20 -2
  95. package/dist/system-prompts.js +7 -2
  96. package/dist/testing/agent-event-source-conformance.d.ts +4 -0
  97. package/dist/testing/agent-event-source-conformance.js +54 -0
  98. package/dist/testing/compaction-conformance.js +5 -1
  99. package/dist/testing/extension-conformance.js +15 -3
  100. package/dist/testing/feedback.d.ts +1 -3
  101. package/dist/testing/feedback.js +1 -1
  102. package/dist/testing/persistence-schema.d.ts +2 -2
  103. package/dist/testing/persistence-schema.js +280 -35
  104. package/dist/testing/provider-conformance.js +3 -3
  105. package/dist/testing/run-ledger-conformance.js +1 -1
  106. package/dist/testing/session-store-conformance.d.ts +6 -0
  107. package/dist/testing/session-store-conformance.js +37 -2
  108. package/dist/testing/tool-conformance.js +30 -5
  109. package/dist/testing/tool-effect-store-conformance.d.ts +9 -0
  110. package/dist/testing/tool-effect-store-conformance.js +85 -0
  111. package/dist/thinking.js +4 -1
  112. package/dist/tool-effects.d.ts +15 -0
  113. package/dist/tool-effects.js +352 -0
  114. package/dist/tool-result-fold.d.ts +40 -0
  115. package/dist/tool-result-fold.js +176 -0
  116. package/dist/tools.d.ts +8 -3
  117. package/dist/tools.js +248 -13
  118. package/docs/0.1.0-readiness.md +215 -0
  119. package/docs/a2a.md +33 -2
  120. package/docs/acp.md +152 -0
  121. package/docs/ag-ui-adoption.md +77 -0
  122. package/docs/ag-ui.md +225 -0
  123. package/docs/agent-events.md +34 -3
  124. package/docs/agent-identity.md +144 -0
  125. package/docs/agent-loops.md +17 -2
  126. package/docs/agent-session-runtime.md +21 -4
  127. package/docs/browser-automation.md +5 -0
  128. package/docs/caveman.md +129 -0
  129. package/docs/cli-rpc.md +3 -6
  130. package/docs/coding-agent-tools.md +229 -25
  131. package/docs/coding-security.md +77 -11
  132. package/docs/compaction-and-retry.md +5 -2
  133. package/docs/compaction-llm.md +20 -1
  134. package/docs/compaction-observational-memory.md +52 -8
  135. package/docs/context-and-skills.md +94 -7
  136. package/docs/contribution-registries.md +1 -0
  137. package/docs/conversations.md +135 -0
  138. package/docs/credential-storage.md +34 -1
  139. package/docs/credentials-and-redaction.md +11 -1
  140. package/docs/database-persistence.md +27 -7
  141. package/docs/device-adapters.md +97 -0
  142. package/docs/enterprise-postgres-state.md +178 -0
  143. package/docs/evaluations.md +14 -1
  144. package/docs/extensions.md +4 -1
  145. package/docs/forge-integration.md +113 -0
  146. package/docs/guardrails.md +16 -2
  147. package/docs/host-security.md +35 -4
  148. package/docs/index.md +69 -37
  149. package/docs/input-and-prompt-assembly.md +8 -7
  150. package/docs/language-intelligence.md +162 -0
  151. package/docs/mcp-tools.md +62 -5
  152. package/docs/middleware-hooks.md +2 -2
  153. package/docs/migration.md +427 -2
  154. package/docs/model-routing.md +111 -0
  155. package/docs/multimodal-content.md +8 -5
  156. package/docs/node-jsonl-session-store.md +1 -1
  157. package/docs/observability.md +2 -0
  158. package/docs/openapi-tools.md +56 -0
  159. package/docs/performance.md +282 -0
  160. package/docs/policy-and-audit.md +171 -0
  161. package/docs/ponytail.md +127 -0
  162. package/docs/postgres-persistence.md +8 -4
  163. package/docs/process-sessions.md +147 -0
  164. package/docs/provider-caching.md +13 -1
  165. package/docs/provider-conformance.md +29 -5
  166. package/docs/provider-packages.md +43 -2
  167. package/docs/provider-request-policies.md +2 -0
  168. package/docs/providers/ai-sdk.md +24 -7
  169. package/docs/providers/alibaba.md +179 -0
  170. package/docs/providers/anthropic.md +93 -0
  171. package/docs/providers/azure.md +74 -0
  172. package/docs/providers/bedrock.md +72 -0
  173. package/docs/providers/google.md +89 -0
  174. package/docs/providers/ollama.md +166 -0
  175. package/docs/providers/openai-compatible.md +31 -2
  176. package/docs/providers/openai.md +24 -5
  177. package/docs/providers/openrouter.md +2 -0
  178. package/docs/providers/vertex.md +71 -0
  179. package/docs/public-contracts.md +68 -4
  180. package/docs/rag.md +41 -12
  181. package/docs/release-and-install.md +362 -208
  182. package/docs/resource-loading.md +3 -0
  183. package/docs/runs-and-usage.md +3 -0
  184. package/docs/server.md +44 -6
  185. package/docs/session-store-conformance.md +2 -0
  186. package/docs/session-stores.md +41 -2
  187. package/docs/sqlite-persistence.md +11 -3
  188. package/docs/structured-output.md +7 -1
  189. package/docs/supervisors.md +8 -0
  190. package/docs/tool-effects.md +95 -0
  191. package/docs/tools.md +5 -0
  192. package/docs/work-artifacts-and-review.md +102 -0
  193. package/docs/work-connectors.md +32 -0
  194. package/docs/work-tools.md +137 -0
  195. package/docs/workflows.md +6 -0
  196. package/docs/working-and-semantic-memory.md +40 -7
  197. package/package.json +30 -7
  198. package/templates/init/providers.json +22 -0
  199. package/docs/review-coverage-2026-07-14.md +0 -260
  200. package/docs/review-coverage-2026-07-15.md +0 -193
  201. package/docs/review-coverage-2026-07-17-provider-validation.md +0 -192
  202. package/docs/review-coverage-2026-07-19-phase-3.md +0 -174
  203. package/docs/review-coverage-2026-07-20-phase-4.md +0 -175
@@ -0,0 +1,176 @@
1
+ import { estimateTextBytes } from "./context-budget.js";
2
+ export const DEFAULT_TOOL_RESULT_FOLD_MIN_AGE_TURNS = 2;
3
+ export const DEFAULT_TOOL_RESULT_FOLD_MIN_BYTES = 4_096;
4
+ export const DEFAULT_TOOL_RESULT_FOLD_MAX_SUMMARY_BYTES = 512;
5
+ export const HARD_TOOL_RESULT_FOLD_MAX_SUMMARY_BYTES = 4_096;
6
+ export const TOOL_RESULT_FOLD_TURN_METADATA_KEY = "prismToolResultTurn";
7
+ /** Run overrides agent; disabled when neither supplies `summarize`. */
8
+ export function resolveToolResultFold(run, agent) {
9
+ const options = run ?? agent;
10
+ if (!options?.summarize)
11
+ return undefined;
12
+ const minAgeTurns = options.minAgeTurns ?? DEFAULT_TOOL_RESULT_FOLD_MIN_AGE_TURNS;
13
+ const minBytes = options.minBytes ?? DEFAULT_TOOL_RESULT_FOLD_MIN_BYTES;
14
+ const maxSummaryBytes = options.maxSummaryBytes ?? DEFAULT_TOOL_RESULT_FOLD_MAX_SUMMARY_BYTES;
15
+ assertPositiveInt(minAgeTurns, "minAgeTurns", 1, 1_024);
16
+ assertPositiveInt(minBytes, "minBytes", 1, 32 * 1024 * 1024);
17
+ assertPositiveInt(maxSummaryBytes, "maxSummaryBytes", 1, HARD_TOOL_RESULT_FOLD_MAX_SUMMARY_BYTES);
18
+ return {
19
+ minAgeTurns,
20
+ minBytes,
21
+ maxSummaryBytes,
22
+ summarize: options.summarize,
23
+ };
24
+ }
25
+ /** Projection-only fold for history tool messages; does not mutate the input array. */
26
+ export async function foldToolResultHistory(history, options, context) {
27
+ if (history.length === 0)
28
+ return history;
29
+ const turns = inferToolResultTurns(history);
30
+ const out = [];
31
+ for (let index = 0; index < history.length; index += 1) {
32
+ const message = history[index];
33
+ const folded = await foldToolResultMessage(message, options, {
34
+ ...context,
35
+ toolResultTurn: turns[index] ?? context.turn,
36
+ });
37
+ out.push(folded);
38
+ }
39
+ return out;
40
+ }
41
+ /** Projection-only fold for in-flight tool results before message conversion. */
42
+ export async function foldToolResults(results, options, context) {
43
+ if (results.length === 0)
44
+ return results;
45
+ const out = [];
46
+ for (const result of results) {
47
+ const folded = await foldToolResultValue(result, options, {
48
+ ...context,
49
+ toolResultTurn: context.turn,
50
+ });
51
+ out.push(folded);
52
+ }
53
+ return out;
54
+ }
55
+ async function foldToolResultMessage(message, options, context) {
56
+ if (message.role !== "tool")
57
+ return message;
58
+ const block = message.content.find((part) => part.type === "tool_result");
59
+ if (!block || block.type !== "tool_result")
60
+ return message;
61
+ const text = toolResultText(block.result, block.error, message.content);
62
+ const folded = await maybeFold({
63
+ options,
64
+ context,
65
+ toolCallId: block.toolCallId,
66
+ toolName: block.name,
67
+ text,
68
+ apply: (summary) => ({
69
+ ...message,
70
+ content: message.content.map((part) => part.type === "tool_result"
71
+ ? {
72
+ ...part,
73
+ result: foldedToolResultHeader(block.name, block.toolCallId, summary),
74
+ error: undefined,
75
+ }
76
+ : part),
77
+ metadata: { ...message.metadata, prismFolded: true },
78
+ }),
79
+ });
80
+ return folded ?? message;
81
+ }
82
+ async function foldToolResultValue(result, options, context) {
83
+ const text = toolResultText(result.value, result.error, result.content);
84
+ const folded = await maybeFold({
85
+ options,
86
+ context,
87
+ toolCallId: result.toolCallId,
88
+ toolName: result.name,
89
+ text,
90
+ apply: (summary) => ({
91
+ ...result,
92
+ value: foldedToolResultHeader(result.name, result.toolCallId, summary),
93
+ error: undefined,
94
+ metadata: { ...result.metadata, prismFolded: true },
95
+ }),
96
+ });
97
+ return folded ?? result;
98
+ }
99
+ async function maybeFold(input) {
100
+ const age = input.context.turn - input.context.toolResultTurn;
101
+ if (age < input.options.minAgeTurns)
102
+ return undefined;
103
+ if (estimateTextBytes(input.text) < input.options.minBytes)
104
+ return undefined;
105
+ throwIfAborted(input.context.signal);
106
+ try {
107
+ const summary = await input.options.summarize({
108
+ sessionId: input.context.sessionId,
109
+ runId: input.context.runId,
110
+ turn: input.context.toolResultTurn,
111
+ toolCallId: input.toolCallId,
112
+ toolName: input.toolName,
113
+ text: input.text,
114
+ });
115
+ return input.apply(capSummaryBytes(String(summary), input.options.maxSummaryBytes));
116
+ }
117
+ catch {
118
+ return undefined;
119
+ }
120
+ }
121
+ export function formatFoldedToolResult(summary) {
122
+ return summary;
123
+ }
124
+ export function foldedToolResultHeader(toolName, toolCallId, summary) {
125
+ return `Tool result ${toolName} [${toolCallId}]: ${summary}`;
126
+ }
127
+ function toolResultText(result, error, extra) {
128
+ const parts = [JSON.stringify(error ?? result ?? null)];
129
+ for (const block of extra ?? []) {
130
+ if (block.type === "text" && "text" in block && typeof block.text === "string")
131
+ parts.push(block.text);
132
+ }
133
+ return parts.join("\n");
134
+ }
135
+ function capSummaryBytes(summary, maxBytes) {
136
+ const bytes = estimateTextBytes(summary);
137
+ if (bytes <= maxBytes)
138
+ return summary;
139
+ const encoded = new TextEncoder().encode(summary);
140
+ const suffix = new TextEncoder().encode("…");
141
+ let end = Math.max(0, maxBytes - suffix.length);
142
+ while (end > 0 && (encoded[end] & 0xc0) === 0x80)
143
+ end--;
144
+ return new TextDecoder().decode(encoded.slice(0, end)) + "…";
145
+ }
146
+ function inferToolResultTurns(history) {
147
+ const turns = new Array(history.length).fill(1);
148
+ let providerTurn = 0;
149
+ let toolTurn = 1;
150
+ for (let index = 0; index < history.length; index += 1) {
151
+ const message = history[index];
152
+ const stamped = readToolResultTurn(message.metadata);
153
+ if (message.role === "assistant") {
154
+ providerTurn += 1;
155
+ toolTurn = providerTurn;
156
+ }
157
+ if (message.role === "tool") {
158
+ turns[index] = stamped ?? toolTurn;
159
+ }
160
+ }
161
+ return turns;
162
+ }
163
+ function readToolResultTurn(metadata) {
164
+ const value = metadata?.[TOOL_RESULT_FOLD_TURN_METADATA_KEY];
165
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
166
+ }
167
+ function assertPositiveInt(value, name, min, max) {
168
+ if (!Number.isSafeInteger(value) || value < min || value > max) {
169
+ throw new TypeError(`toolResultFold.${name} must be a safe integer from ${min} to ${max}`);
170
+ }
171
+ }
172
+ function throwIfAborted(signal) {
173
+ if (signal?.aborted)
174
+ throw signal.reason instanceof Error ? signal.reason : new Error("Tool result fold aborted");
175
+ }
176
+ //# sourceMappingURL=tool-result-fold.js.map
package/dist/tools.d.ts CHANGED
@@ -1,15 +1,15 @@
1
- import type { AgentEvent, ErrorInfo, Guardrails, JsonObject, OwnershipScope, RunLedger, ToolCallContent, ToolDefinition, ToolExecutionContext, ToolRegistry, ToolResult } from "./contracts.js";
2
- import type { RunLimitTracker } from "./run-limits.js";
1
+ import type { AgentEvent, ErrorInfo, Guardrails, JsonObject, OwnershipScope, RunLedger, ToolCallContent, ToolDefinition, ToolExecutionContext, ToolEffectDeclaration, ToolEffectStore, ToolRegistry, ToolResult } from "./contracts.js";
3
2
  import type { MiddlewareRegistry } from "./middleware.js";
4
3
  import { type SecretRedactor } from "./redaction.js";
5
4
  import { type DuplicateRegistrationOptions } from "./registry-options.js";
5
+ import type { RunLimitTracker } from "./run-limits.js";
6
6
  import { type PermissionPolicy, type TrustPolicy } from "./security.js";
7
7
  export interface ToolFilter {
8
8
  readonly allow?: readonly string[];
9
9
  readonly deny?: readonly string[];
10
10
  }
11
11
  export type ToolFilterInput = ToolFilter | readonly ToolFilter[];
12
- export type ToolValidator = (tool: ToolDefinition, args: JsonObject, context: ToolExecutionContext) => void | string | ErrorInfo | Promise<void | string | ErrorInfo>;
12
+ export type ToolValidator = (tool: ToolDefinition, args: JsonObject, context: ToolExecutionContext) => undefined | string | ErrorInfo | Promise<undefined | string | ErrorInfo>;
13
13
  export interface ToolArgumentValidationError {
14
14
  readonly path?: string;
15
15
  readonly message: string;
@@ -42,7 +42,11 @@ export interface DispatchToolCallOptions {
42
42
  readonly trust?: TrustPolicy;
43
43
  readonly redactor?: SecretRedactor;
44
44
  readonly ledger?: RunLedger;
45
+ /** Optional shared recovery store. Only declared optional/required effects use it. */
46
+ readonly effectStore?: ToolEffectStore;
45
47
  readonly ownership?: OwnershipScope;
48
+ /** Host-verified identity; asserted active before tool side effects when present. */
49
+ readonly identity?: import("./identity.js").AgentIdentity;
46
50
  /** Tool stages run after middleware normalization and before side effects/exposure. */
47
51
  readonly guardrails?: Guardrails;
48
52
  /** Shared run tracker; direct hosts may supply one for their call scope. */
@@ -53,3 +57,4 @@ export interface ToolRegistryOptions extends DuplicateRegistrationOptions {
53
57
  export declare function createToolRegistry(tools?: readonly ToolDefinition[], options?: ToolRegistryOptions): ToolRegistry;
54
58
  export declare function filterTools(tools: readonly ToolDefinition[], filter?: ToolFilterInput): readonly ToolDefinition[];
55
59
  export declare function dispatchToolCall(options: DispatchToolCallOptions): Promise<ToolResult>;
60
+ export declare function resolveToolEffectDeclaration(tool: ToolDefinition, args: JsonObject, context: ToolExecutionContext): ToolEffectDeclaration | undefined;
package/dist/tools.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { isJsonObject } from "./config.js";
2
- import { createId } from "./ids.js";
3
2
  import { GuardrailError, runGuardrails } from "./guardrails.js";
3
+ import { assertIdentityActive, assertIdentityMatchesOwnership, ownershipFromIdentity } from "./identity.js";
4
+ import { createId } from "./ids.js";
4
5
  import { errorToErrorInfo, redactRunLedgerRecord, redactSecrets } from "./redaction.js";
5
6
  import { assertCanRegister } from "./registry-options.js";
6
7
  import { assertPermission, assertTrusted } from "./security.js";
8
+ import { deriveToolEffectKey, toolEffectArgumentsHash, ToolEffectError } from "./tool-effects.js";
7
9
  /** Wrap a schema adapter as the existing `ToolValidator` seam used by dispatch and the agent runtime. */
8
10
  export function createToolParameterValidator(validator, options = {}) {
9
11
  const missingSchema = options.missingSchema ?? "allow";
@@ -51,7 +53,9 @@ export function createToolRegistry(tools = [], options = {}) {
51
53
  export function filterTools(tools, filter) {
52
54
  const filters = Array.isArray(filter) ? filter : filter ? [filter] : [];
53
55
  const denied = new Set(filters.flatMap((item) => item.deny ?? []));
54
- const allows = filters.map((item) => item.allow?.length ? new Set(item.allow) : undefined).filter((item) => Boolean(item));
56
+ const allows = filters
57
+ .map((item) => (item.allow?.length ? new Set(item.allow) : undefined))
58
+ .filter((item) => Boolean(item));
55
59
  return tools.filter((tool) => !denied.has(tool.name) && allows.every((allow) => allow.has(tool.name)));
56
60
  }
57
61
  function toolExecutionMetadata(startedAt, status) {
@@ -86,9 +90,11 @@ export async function dispatchToolCall(options) {
86
90
  const postcheck = await checkCall(mediatedCall, options, startedAt);
87
91
  if (postcheck)
88
92
  return postcheck;
89
- const context = {
90
- ...options.context,
93
+ const { idempotencyKey: _untrustedKey, ...baseContext } = options.context;
94
+ let context = {
95
+ ...baseContext,
91
96
  toolCallId: mediatedCall.id,
97
+ identity: options.identity ?? options.context.identity,
92
98
  progress: async (progress, metadata) => {
93
99
  await options.context.progress?.(progress, metadata);
94
100
  await options.emit?.({
@@ -108,8 +114,22 @@ export async function dispatchToolCall(options) {
108
114
  },
109
115
  };
110
116
  try {
111
- await assertTrusted(options.trust, { kind: "tool", target: mediatedCall.name, capability: "execute", metadata: options.context.metadata });
112
- await assertPermission(options.permission, { kind: "tool", action: "execute", target: mediatedCall.name, metadata: options.context.metadata });
117
+ if (context.identity) {
118
+ assertIdentityActive(context.identity);
119
+ assertIdentityMatchesOwnership(context.identity, options.ownership);
120
+ }
121
+ await assertTrusted(options.trust, {
122
+ kind: "tool",
123
+ target: mediatedCall.name,
124
+ capability: "execute",
125
+ metadata: options.context.metadata,
126
+ });
127
+ await assertPermission(options.permission, {
128
+ kind: "tool",
129
+ action: "execute",
130
+ target: mediatedCall.name,
131
+ metadata: options.context.metadata,
132
+ });
113
133
  }
114
134
  catch (error) {
115
135
  return blocked(mediatedCall, context, "permission_denied", errorToErrorInfo(error, secrets), options, startedAt);
@@ -117,17 +137,38 @@ export async function dispatchToolCall(options) {
117
137
  const validation = await options.validate?.(tool, mediatedCall.arguments, context);
118
138
  if (validation)
119
139
  return blocked(mediatedCall, context, "validation_failed", toErrorInfo(validation, secrets), options, startedAt);
140
+ let effect;
141
+ try {
142
+ const prepared = await prepareToolEffect(tool, mediatedCall, context, options);
143
+ if (prepared.result)
144
+ return prepared.result;
145
+ context = prepared.context;
146
+ effect = prepared.effect;
147
+ }
148
+ catch (error) {
149
+ return blocked(mediatedCall, context, "execution_denied", errorToErrorInfo(error, secrets), options, startedAt);
150
+ }
120
151
  try {
121
152
  await options.beforeExecute?.(mediatedCall, tool, context);
122
153
  }
123
154
  catch (error) {
124
- if (error?.code === "ERR_PRISM_AGENT_RUN_SUSPENDED")
155
+ await failBeforeEffect(effect, isSuspended(error) ? "failed_retryable" : "failed_terminal");
156
+ // Loop-state contract errors (snapshot capture) are terminal run errors, not tool errors.
157
+ if (isSuspended(error) || isLoopStateError(error) || isDelegationSuspended(error))
125
158
  throw error;
126
159
  return blocked(mediatedCall, context, "execution_denied", errorToErrorInfo(error, secrets), options, startedAt);
127
160
  }
128
- await options.emit?.({ type: "tool_execution_started", sessionId: context.sessionId, runId: context.runId, call: mediatedCall });
129
- await appendToolCallRecord(options, "started", mediatedCall, startedAt, {});
161
+ let completedResult;
162
+ let dispatchAttempted = false;
130
163
  try {
164
+ await options.emit?.({ type: "tool_execution_started", sessionId: context.sessionId, runId: context.runId, call: mediatedCall });
165
+ await appendToolCallRecord(options, "started", mediatedCall, startedAt, {});
166
+ if (effect) {
167
+ dispatchAttempted = true;
168
+ const record = await effect.store.markDispatched(transition(effect));
169
+ effect.expectedVersion = record.version;
170
+ effect.dispatched = true;
171
+ }
131
172
  const raw = await tool.execute(mediatedCall.arguments, context);
132
173
  const mediatedResult = await (options.middleware?.run("tool_result", raw) ?? raw);
133
174
  const outputGuards = await runGuardrails({
@@ -148,27 +189,221 @@ export async function dispatchToolCall(options) {
148
189
  if (outputGuards.terminal) {
149
190
  if (outputGuards.terminal.action !== "block")
150
191
  throw new GuardrailError(outputGuards.terminal);
192
+ if (effect)
193
+ return finishUnknownEffect(effect, mediatedCall, context, options, startedAt);
151
194
  return blocked(mediatedCall, context, "guardrail_blocked", { message: "Tool result blocked by guardrail" }, options, startedAt);
152
195
  }
196
+ if (effect && mediatedResult.error)
197
+ return finishUnknownEffect(effect, mediatedCall, context, options, startedAt);
153
198
  const result = options.redactor?.redact(mediatedResult) ?? mediatedResult;
199
+ if (effect) {
200
+ try {
201
+ const record = await effect.store.complete({ ...transition(effect), result });
202
+ effect.expectedVersion = record.version;
203
+ effect.completed = true;
204
+ completedResult = record.result ?? result;
205
+ }
206
+ catch {
207
+ return finishUnknownEffect(effect, mediatedCall, context, options, startedAt);
208
+ }
209
+ }
210
+ completedResult ??= result;
154
211
  const finishedAt = new Date().toISOString();
155
212
  const metadata = toolExecutionMetadata(startedAt, "finished");
156
- await options.emit?.({ type: "tool_execution_finished", sessionId: context.sessionId, runId: context.runId, result, metadata });
157
- await appendToolCallRecord(options, "finished", mediatedCall, startedAt, { finishedAt, result });
158
- return result;
213
+ await options.emit?.({
214
+ type: "tool_execution_finished",
215
+ sessionId: context.sessionId,
216
+ runId: context.runId,
217
+ result: completedResult,
218
+ metadata,
219
+ });
220
+ await appendToolCallRecord(options, "finished", mediatedCall, startedAt, { finishedAt, result: completedResult });
221
+ return completedResult;
159
222
  }
160
223
  catch (error) {
224
+ if (completedResult)
225
+ return completedResult;
226
+ // Nested-run suspensions must propagate to the run loop, never become tool errors.
227
+ if (isDelegationSuspended(error)) {
228
+ if (effect && (effect.dispatched || dispatchAttempted))
229
+ await unknownEffectResult(effect, mediatedCall);
230
+ else
231
+ await failBeforeEffect(effect, "failed_retryable");
232
+ throw error;
233
+ }
234
+ if (effect && (effect.dispatched || dispatchAttempted))
235
+ return finishUnknownEffect(effect, mediatedCall, context, options, startedAt);
236
+ await failBeforeEffect(effect, "failed_terminal");
161
237
  if (error instanceof GuardrailError)
162
238
  throw error;
163
239
  const info = errorToErrorInfo(error, secrets);
164
240
  const result = { toolCallId: mediatedCall.id, name: mediatedCall.name, error: info };
165
241
  const finishedAt = new Date().toISOString();
166
242
  const metadata = toolExecutionMetadata(startedAt, "error");
167
- await options.emit?.({ type: "tool_execution_error", sessionId: context.sessionId, runId: context.runId, call: mediatedCall, error: info, metadata });
243
+ await options.emit?.({
244
+ type: "tool_execution_error",
245
+ sessionId: context.sessionId,
246
+ runId: context.runId,
247
+ call: mediatedCall,
248
+ error: info,
249
+ metadata,
250
+ });
168
251
  await appendToolCallRecord(options, "error", mediatedCall, startedAt, { finishedAt, result });
169
252
  return result;
170
253
  }
171
254
  }
255
+ async function prepareToolEffect(tool, call, context, options) {
256
+ const identity = context.identity;
257
+ const declaration = resolveToolEffectDeclaration(tool, call.arguments, context);
258
+ if (!declaration || declaration.kind === "none" || declaration.idempotency === "none")
259
+ return { context };
260
+ if (!identity && declaration.idempotency === "unsupported")
261
+ return { context };
262
+ if (!identity)
263
+ throw new ToolEffectError("ERR_PRISM_TOOL_EFFECT_CONFLICT", "verified identity is required for a durable tool effect");
264
+ const ownership = ownershipFromIdentity(identity);
265
+ const argumentsHash = toolEffectArgumentsHash(call.arguments);
266
+ const base = {
267
+ identity,
268
+ ownership,
269
+ sessionId: context.sessionId,
270
+ runId: context.runId,
271
+ toolCallId: call.id,
272
+ toolName: call.name,
273
+ argumentsHash,
274
+ };
275
+ const key = { ...base, key: deriveToolEffectKey(base), signal: context.signal };
276
+ const keyedContext = { ...context, idempotencyKey: key.key };
277
+ if (declaration.idempotency === "tool_managed" || declaration.idempotency === "unsupported")
278
+ return { context: keyedContext };
279
+ const store = options.effectStore;
280
+ if (!store) {
281
+ if (declaration.idempotency === "required")
282
+ throw new ToolEffectError("ERR_PRISM_TOOL_EFFECT_REQUIRED", "durable tool effect store is required");
283
+ return { context: keyedContext };
284
+ }
285
+ let begun;
286
+ try {
287
+ begun = await store.begin(key);
288
+ }
289
+ catch (error) {
290
+ if (error instanceof ToolEffectError)
291
+ throw error;
292
+ throw new ToolEffectError("ERR_PRISM_TOOL_EFFECT_UNKNOWN", "tool effect claim outcome is unknown");
293
+ }
294
+ if (begun.outcome === "existing")
295
+ return { context: keyedContext, result: replayEffectResult(begun.record) };
296
+ if (!begun.record.claimToken)
297
+ throw new ToolEffectError("ERR_PRISM_TOOL_EFFECT_UNKNOWN", "tool effect claim outcome is unknown");
298
+ return {
299
+ context: keyedContext,
300
+ effect: {
301
+ store,
302
+ key,
303
+ claimToken: begun.record.claimToken,
304
+ expectedVersion: begun.record.version,
305
+ dispatched: false,
306
+ completed: false,
307
+ },
308
+ };
309
+ }
310
+ export function resolveToolEffectDeclaration(tool, args, context) {
311
+ const classifierContext = Object.freeze({
312
+ sessionId: context.sessionId,
313
+ runId: context.runId,
314
+ toolCallId: context.toolCallId,
315
+ signal: context.signal,
316
+ metadata: context.metadata,
317
+ });
318
+ const declaration = typeof tool.effect === "function" ? tool.effect(args, classifierContext) : tool.effect;
319
+ if (!declaration)
320
+ return undefined;
321
+ if (!["none", "local_mutation", "external_mutation"].includes(declaration.kind) ||
322
+ !["none", "optional", "required", "tool_managed", "unsupported"].includes(declaration.idempotency) ||
323
+ (declaration.kind === "none" && declaration.idempotency !== "none")) {
324
+ throw new ToolEffectError("ERR_PRISM_TOOL_EFFECT_LIMIT", "tool effect declaration is invalid");
325
+ }
326
+ return declaration;
327
+ }
328
+ function replayEffectResult(record) {
329
+ if (record.status === "completed") {
330
+ if (record.result)
331
+ return record.result;
332
+ throw new ToolEffectError("ERR_PRISM_TOOL_EFFECT_COMPLETED", "tool effect already completed without replayable result");
333
+ }
334
+ if (record.status === "dispatched" || record.status === "unknown") {
335
+ throw new ToolEffectError("ERR_PRISM_TOOL_EFFECT_UNKNOWN", "tool effect outcome requires reconciliation");
336
+ }
337
+ throw new ToolEffectError("ERR_PRISM_TOOL_EFFECT_CONFLICT", "tool effect is not dispatchable");
338
+ }
339
+ function transition(effect) {
340
+ return { ...effect.key, claimToken: effect.claimToken, expectedVersion: effect.expectedVersion };
341
+ }
342
+ async function failBeforeEffect(effect, status) {
343
+ if (!effect || effect.dispatched || effect.completed)
344
+ return;
345
+ try {
346
+ await effect.store.fail({
347
+ ...transition(effect),
348
+ status,
349
+ failure: { code: "ERR_PRISM_TOOL_EFFECT_PRE_DISPATCH" },
350
+ });
351
+ }
352
+ catch {
353
+ // No effect was invoked. A stale/failed pre-dispatch transition only delays a later safe retry.
354
+ }
355
+ }
356
+ async function unknownEffectResult(effect, call) {
357
+ try {
358
+ let claim = effect.dispatched
359
+ ? { claimToken: effect.claimToken, version: effect.expectedVersion }
360
+ : undefined;
361
+ if (!claim) {
362
+ const current = await effect.store.get(effect.key);
363
+ if (current?.status === "dispatched" && current.claimToken)
364
+ claim = { claimToken: current.claimToken, version: current.version };
365
+ }
366
+ if (claim) {
367
+ await effect.store.markUnknown({
368
+ ...effect.key,
369
+ claimToken: claim.claimToken,
370
+ expectedVersion: claim.version,
371
+ failure: { code: "ERR_PRISM_TOOL_EFFECT_UNKNOWN" },
372
+ });
373
+ }
374
+ }
375
+ catch {
376
+ // A post-dispatch persistence error is itself ambiguous; never expose or retry it.
377
+ }
378
+ return effectErrorResult(call, "ERR_PRISM_TOOL_EFFECT_UNKNOWN", "tool effect outcome requires reconciliation");
379
+ }
380
+ async function finishUnknownEffect(effect, call, context, options, startedAt) {
381
+ const result = await unknownEffectResult(effect, call);
382
+ const error = result.error;
383
+ const finishedAt = new Date().toISOString();
384
+ const metadata = toolExecutionMetadata(startedAt, "error");
385
+ try {
386
+ await options.emit?.({ type: "tool_execution_error", sessionId: context.sessionId, runId: context.runId, call, error, metadata });
387
+ await appendToolCallRecord(options, "error", call, startedAt, { finishedAt, result });
388
+ }
389
+ catch {
390
+ // The effect is already ambiguous; exposure/ledger failures cannot make it safe to retry.
391
+ }
392
+ return result;
393
+ }
394
+ function effectErrorResult(call, code, message) {
395
+ const error = new ToolEffectError(code, message);
396
+ return { toolCallId: call.id, name: call.name, error: errorToErrorInfo(error) };
397
+ }
398
+ function isSuspended(error) {
399
+ return error?.code === "ERR_PRISM_AGENT_RUN_SUSPENDED";
400
+ }
401
+ function isLoopStateError(error) {
402
+ return typeof error?.code === "string" && error.code.startsWith("ERR_PRISM_LOOP_");
403
+ }
404
+ function isDelegationSuspended(error) {
405
+ return error?.code === "ERR_PRISM_DELEGATION_SUSPENDED";
406
+ }
172
407
  async function checkCall(call, options, startedAt) {
173
408
  const context = options.context;
174
409
  const tool = options.registry.get(call.name);