@arnilo/prism 0.0.96 → 0.1.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.
- package/CHANGELOG.md +285 -2
- package/README.md +17 -3
- package/dist/agent-definitions.js +2 -3
- package/dist/agent-event-source.d.ts +11 -0
- package/dist/agent-event-source.js +512 -0
- package/dist/agent-loops.d.ts +5 -0
- package/dist/agent-loops.js +99 -14
- package/dist/agent-run-lifecycle.d.ts +5 -2
- package/dist/agent-run-lifecycle.js +18 -2
- package/dist/agent-run-state.d.ts +27 -1
- package/dist/agent-run-state.js +113 -7
- package/dist/agents.d.ts +3 -1
- package/dist/agents.js +1255 -129
- package/dist/artifacts.d.ts +132 -0
- package/dist/artifacts.js +44 -0
- package/dist/cache-helpers.js +18 -9
- package/dist/checkpoints.d.ts +4 -0
- package/dist/checkpoints.js +17 -9
- package/dist/cli-init.js +3 -7
- package/dist/cli-runner.d.ts +2 -6
- package/dist/cli-runner.js +71 -33
- package/dist/compaction.js +5 -4
- package/dist/config.js +7 -4
- package/dist/content.js +26 -24
- package/dist/context-budget.d.ts +67 -0
- package/dist/context-budget.js +288 -0
- package/dist/contracts.d.ts +590 -8
- package/dist/contracts.js +142 -1
- package/dist/contribution-parsing.js +6 -2
- package/dist/contributions.d.ts +2 -0
- package/dist/contributions.js +3 -0
- package/dist/conversations.d.ts +50 -0
- package/dist/conversations.js +98 -0
- package/dist/credentials.d.ts +22 -2
- package/dist/credentials.js +18 -3
- package/dist/devices.d.ts +94 -0
- package/dist/devices.js +138 -0
- package/dist/event-multiplexer.js +18 -4
- package/dist/extensions.d.ts +18 -1
- package/dist/extensions.js +79 -6
- package/dist/feedback.js +12 -10
- package/dist/guardrails.d.ts +1 -1
- package/dist/guardrails.js +26 -17
- package/dist/identity.d.ts +92 -0
- package/dist/identity.js +265 -0
- package/dist/index.d.ts +94 -72
- package/dist/index.js +48 -36
- package/dist/input.d.ts +10 -1
- package/dist/input.js +152 -52
- package/dist/instruction-injection.d.ts +1 -1
- package/dist/middleware.js +9 -1
- package/dist/models.d.ts +2 -0
- package/dist/models.js +3 -0
- package/dist/node/agent-definitions.js +16 -8
- package/dist/node/contribution-discovery.d.ts +1 -2
- package/dist/node/contribution-discovery.js +3 -3
- package/dist/node/session-store-jsonl.js +13 -7
- package/dist/node/settings.d.ts +1 -1
- package/dist/node/settings.js +1 -1
- package/dist/node/system-project-prompts.js +2 -4
- package/dist/node/trust.js +1 -1
- package/dist/persistence-lifecycle.d.ts +103 -0
- package/dist/persistence-lifecycle.js +202 -0
- package/dist/provider-events.d.ts +1 -0
- package/dist/provider-events.js +6 -1
- package/dist/provider-request-policy.js +3 -4
- package/dist/providers/media.d.ts +1 -1
- package/dist/providers/openai-compatible.d.ts +46 -1
- package/dist/providers/openai-compatible.js +123 -53
- package/dist/providers/openai-primitives.js +10 -7
- package/dist/providers/transport.d.ts +6 -0
- package/dist/providers/transport.js +21 -0
- package/dist/providers.d.ts +2 -0
- package/dist/providers.js +3 -0
- package/dist/redaction.d.ts +1 -0
- package/dist/redaction.js +26 -9
- package/dist/resources.d.ts +2 -2
- package/dist/resources.js +2 -2
- package/dist/retry.d.ts +5 -0
- package/dist/retry.js +8 -1
- package/dist/rpc.js +55 -11
- package/dist/run-ledger.d.ts +6 -0
- package/dist/run-ledger.js +16 -13
- package/dist/run-limits.js +49 -10
- package/dist/secure-agent.js +8 -2
- package/dist/security.js +7 -2
- package/dist/session-stores.d.ts +7 -2
- package/dist/session-stores.js +195 -21
- package/dist/skill-disclosure.d.ts +35 -0
- package/dist/skill-disclosure.js +101 -0
- package/dist/skill-load.d.ts +25 -0
- package/dist/skill-load.js +112 -0
- package/dist/structured-output.d.ts +5 -1
- package/dist/structured-output.js +20 -2
- package/dist/system-prompts.js +7 -2
- package/dist/testing/agent-event-source-conformance.d.ts +4 -0
- package/dist/testing/agent-event-source-conformance.js +54 -0
- package/dist/testing/compaction-conformance.js +5 -1
- package/dist/testing/extension-conformance.js +15 -3
- package/dist/testing/feedback.d.ts +1 -3
- package/dist/testing/feedback.js +1 -1
- package/dist/testing/persistence-schema.d.ts +2 -2
- package/dist/testing/persistence-schema.js +280 -35
- package/dist/testing/provider-conformance.js +3 -3
- package/dist/testing/run-ledger-conformance.js +1 -1
- package/dist/testing/session-store-conformance.d.ts +6 -0
- package/dist/testing/session-store-conformance.js +37 -2
- package/dist/testing/tool-conformance.js +30 -5
- package/dist/testing/tool-effect-store-conformance.d.ts +9 -0
- package/dist/testing/tool-effect-store-conformance.js +85 -0
- package/dist/thinking.js +4 -1
- package/dist/tool-effects.d.ts +15 -0
- package/dist/tool-effects.js +352 -0
- package/dist/tool-result-fold.d.ts +40 -0
- package/dist/tool-result-fold.js +176 -0
- package/dist/tools.d.ts +8 -3
- package/dist/tools.js +248 -13
- package/docs/0.1.0-readiness.md +202 -0
- package/docs/a2a.md +33 -2
- package/docs/acp.md +126 -0
- package/docs/ag-ui-adoption.md +77 -0
- package/docs/ag-ui.md +225 -0
- package/docs/agent-events.md +34 -3
- package/docs/agent-identity.md +144 -0
- package/docs/agent-loops.md +17 -2
- package/docs/agent-session-runtime.md +21 -4
- package/docs/browser-automation.md +5 -0
- package/docs/caveman.md +129 -0
- package/docs/cli-rpc.md +3 -6
- package/docs/coding-agent-tools.md +229 -25
- package/docs/coding-security.md +77 -11
- package/docs/compaction-and-retry.md +5 -2
- package/docs/compaction-llm.md +20 -1
- package/docs/compaction-observational-memory.md +52 -8
- package/docs/context-and-skills.md +94 -7
- package/docs/contribution-registries.md +1 -0
- package/docs/conversations.md +135 -0
- package/docs/credential-storage.md +34 -1
- package/docs/credentials-and-redaction.md +11 -1
- package/docs/database-persistence.md +27 -7
- package/docs/device-adapters.md +97 -0
- package/docs/enterprise-postgres-state.md +178 -0
- package/docs/evaluations.md +14 -1
- package/docs/extensions.md +4 -1
- package/docs/forge-integration.md +113 -0
- package/docs/guardrails.md +16 -2
- package/docs/host-security.md +35 -4
- package/docs/index.md +69 -37
- package/docs/input-and-prompt-assembly.md +8 -7
- package/docs/language-intelligence.md +162 -0
- package/docs/mcp-tools.md +62 -5
- package/docs/middleware-hooks.md +2 -2
- package/docs/migration.md +423 -2
- package/docs/model-routing.md +111 -0
- package/docs/multimodal-content.md +8 -5
- package/docs/node-jsonl-session-store.md +1 -1
- package/docs/observability.md +2 -0
- package/docs/openapi-tools.md +56 -0
- package/docs/performance.md +282 -0
- package/docs/policy-and-audit.md +171 -0
- package/docs/ponytail.md +127 -0
- package/docs/postgres-persistence.md +8 -4
- package/docs/process-sessions.md +147 -0
- package/docs/provider-caching.md +13 -1
- package/docs/provider-conformance.md +29 -5
- package/docs/provider-packages.md +43 -2
- package/docs/provider-request-policies.md +2 -0
- package/docs/providers/ai-sdk.md +24 -7
- package/docs/providers/alibaba.md +179 -0
- package/docs/providers/anthropic.md +93 -0
- package/docs/providers/azure.md +74 -0
- package/docs/providers/bedrock.md +72 -0
- package/docs/providers/google.md +89 -0
- package/docs/providers/ollama.md +166 -0
- package/docs/providers/openai-compatible.md +31 -2
- package/docs/providers/openai.md +24 -5
- package/docs/providers/openrouter.md +2 -0
- package/docs/providers/vertex.md +71 -0
- package/docs/public-contracts.md +61 -4
- package/docs/rag.md +41 -12
- package/docs/release-and-install.md +323 -206
- package/docs/resource-loading.md +3 -0
- package/docs/runs-and-usage.md +3 -0
- package/docs/server.md +44 -6
- package/docs/session-store-conformance.md +2 -0
- package/docs/session-stores.md +41 -2
- package/docs/sqlite-persistence.md +11 -3
- package/docs/structured-output.md +7 -1
- package/docs/supervisors.md +8 -0
- package/docs/tool-effects.md +95 -0
- package/docs/tools.md +5 -0
- package/docs/work-artifacts-and-review.md +102 -0
- package/docs/work-connectors.md +32 -0
- package/docs/work-tools.md +137 -0
- package/docs/workflows.md +6 -0
- package/docs/working-and-semantic-memory.md +40 -7
- package/package.json +30 -8
- package/templates/init/providers.json +22 -0
- package/docs/review-coverage-2026-07-14.md +0 -260
- package/docs/review-coverage-2026-07-15.md +0 -193
- package/docs/review-coverage-2026-07-17-provider-validation.md +0 -192
- package/docs/review-coverage-2026-07-19-phase-3.md +0 -174
- 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) =>
|
|
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
|
|
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
|
|
90
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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?.({
|
|
157
|
-
|
|
158
|
-
|
|
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?.({
|
|
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);
|