omk-agent-core 0.90.8 → 0.91.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/README.md +97 -1
- package/dist/agent-loop.d.ts +25 -2
- package/dist/agent-loop.d.ts.map +1 -1
- package/dist/agent-loop.js +492 -187
- package/dist/agent-loop.js.map +1 -1
- package/dist/agent.d.ts +29 -7
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +81 -46
- package/dist/agent.js.map +1 -1
- package/dist/builtin-tool-resource-claims.d.ts +19 -0
- package/dist/builtin-tool-resource-claims.d.ts.map +1 -0
- package/dist/builtin-tool-resource-claims.js +200 -0
- package/dist/builtin-tool-resource-claims.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/node-resource-resolver.d.ts +42 -0
- package/dist/node-resource-resolver.d.ts.map +1 -0
- package/dist/node-resource-resolver.js +149 -0
- package/dist/node-resource-resolver.js.map +1 -0
- package/dist/node.d.ts +1 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +2 -0
- package/dist/node.js.map +1 -1
- package/dist/path-segments.d.ts +8 -0
- package/dist/path-segments.d.ts.map +1 -1
- package/dist/path-segments.js +62 -9
- package/dist/path-segments.js.map +1 -1
- package/dist/plain-data.d.ts +7 -0
- package/dist/plain-data.d.ts.map +1 -0
- package/dist/plain-data.js +70 -0
- package/dist/plain-data.js.map +1 -0
- package/dist/tool-dag-scheduler.d.ts +86 -0
- package/dist/tool-dag-scheduler.d.ts.map +1 -0
- package/dist/tool-dag-scheduler.js +171 -0
- package/dist/tool-dag-scheduler.js.map +1 -0
- package/dist/tool-execution-boundary.d.ts +52 -0
- package/dist/tool-execution-boundary.d.ts.map +1 -0
- package/dist/tool-execution-boundary.js +185 -0
- package/dist/tool-execution-boundary.js.map +1 -0
- package/dist/tool-resource-claims.d.ts +31 -0
- package/dist/tool-resource-claims.d.ts.map +1 -0
- package/dist/tool-resource-claims.js +128 -0
- package/dist/tool-resource-claims.js.map +1 -0
- package/dist/tool-timeout.d.ts +96 -0
- package/dist/tool-timeout.d.ts.map +1 -0
- package/dist/tool-timeout.js +173 -0
- package/dist/tool-timeout.js.map +1 -0
- package/dist/tool-transcript-integrity.d.ts +65 -0
- package/dist/tool-transcript-integrity.d.ts.map +1 -0
- package/dist/tool-transcript-integrity.js +223 -0
- package/dist/tool-transcript-integrity.js.map +1 -0
- package/dist/types.d.ts +219 -10
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +50 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-tool execution timeout and cancellation for the agent loop.
|
|
3
|
+
*
|
|
4
|
+
* The agent loop awaits each tool's `execute` promise at a single shared
|
|
5
|
+
* chokepoint. A tool that ignores its `AbortSignal` and never settles would
|
|
6
|
+
* otherwise stall the whole run. This module bounds that risk without process
|
|
7
|
+
* killing: the real execute promise is raced against two terminal causes — a
|
|
8
|
+
* per-call timeout timer and the parent run's abort — so an uncooperative tool
|
|
9
|
+
* still yields an immediate, immutable terminal result.
|
|
10
|
+
*
|
|
11
|
+
* The child `AbortSignal` handed to the tool is best-effort cooperative
|
|
12
|
+
* cancellation only; correctness comes from the race, not from the tool
|
|
13
|
+
* honoring the signal. `AbortSignal.any()` is intentionally not used: the
|
|
14
|
+
* parent-abort and timeout wiring is explicit so timer/listener disposal is
|
|
15
|
+
* idempotent and runs on every outcome.
|
|
16
|
+
*
|
|
17
|
+
* A late settlement of the real promise (after the terminal cause already won)
|
|
18
|
+
* is observed exactly once for audit only. It never emits a second tool result
|
|
19
|
+
* or lifecycle end, never mutates the committed result, and — because the real
|
|
20
|
+
* promise is wrapped so it never rejects — can never surface as an unhandled
|
|
21
|
+
* rejection.
|
|
22
|
+
*/
|
|
23
|
+
import { createAbortedToolResult, createTimeoutToolResult } from "./tool-execution-boundary.js";
|
|
24
|
+
export { createAbortedToolResult, createTimeoutToolResult } from "./tool-execution-boundary.js";
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the effective {@link ToolExecutionPolicy} with the release default
|
|
27
|
+
* `lateSettlement: "audit"`: late settlements are observable audit events
|
|
28
|
+
* unless a caller explicitly opts out with `"ignore"`.
|
|
29
|
+
*/
|
|
30
|
+
export function resolveToolExecutionPolicy(policy) {
|
|
31
|
+
return {
|
|
32
|
+
...(policy?.timeoutMs === undefined ? {} : { timeoutMs: policy.timeoutMs }),
|
|
33
|
+
...(policy?.cancelSiblingsOnFatal === undefined ? {} : { cancelSiblingsOnFatal: policy.cancelSiblingsOnFatal }),
|
|
34
|
+
lateSettlement: policy?.lateSettlement === "ignore" ? "ignore" : "audit",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the effective per-call timeout with strict precedence:
|
|
39
|
+
* per-tool `AgentTool.timeoutMs` > per-name `config.toolTimeouts[name]` >
|
|
40
|
+
* global `config.toolTimeoutMs`.
|
|
41
|
+
*
|
|
42
|
+
* The first level that is *present* (not `undefined`) wins, so a per-tool `0`
|
|
43
|
+
* deliberately disables the timeout even when a global default is set. A
|
|
44
|
+
* resolved value that is absent, non-finite, or non-positive returns `0`, which
|
|
45
|
+
* disables only the timer. Parent cancellation is still raced so an
|
|
46
|
+
* uncooperative tool cannot keep an aborted run open.
|
|
47
|
+
*/
|
|
48
|
+
export function resolveToolTimeoutMs(tool, config, toolName) {
|
|
49
|
+
let chosen;
|
|
50
|
+
if (tool.timeoutMs !== undefined) {
|
|
51
|
+
chosen = tool.timeoutMs;
|
|
52
|
+
}
|
|
53
|
+
else if (config.toolTimeouts?.[toolName] !== undefined) {
|
|
54
|
+
chosen = config.toolTimeouts[toolName];
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
chosen = config.toolTimeoutMs;
|
|
58
|
+
}
|
|
59
|
+
if (typeof chosen !== "number" || !Number.isFinite(chosen) || chosen <= 0) {
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
return chosen;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Race a tool's `execute` promise against a per-call timeout and parent abort.
|
|
66
|
+
*
|
|
67
|
+
* Control flow (single path, no `AbortSignal.any()`):
|
|
68
|
+
* - A timer and a parent-abort listener each resolve one shared "terminal cause"
|
|
69
|
+
* deferred. Whichever fires first wins; resolving is idempotent.
|
|
70
|
+
* - The timer callback resolves the timeout cause *before* aborting the child so
|
|
71
|
+
* a tool that rejects promptly on abort cannot make "aborted" win a timeout.
|
|
72
|
+
* - `Promise.race` picks the real settlement or the terminal cause. Timer and
|
|
73
|
+
* listener disposal is idempotent and runs on every outcome.
|
|
74
|
+
* - On a terminal-cause win, the real promise (wrapped so it never rejects) is
|
|
75
|
+
* observed once for the audit event and the committed result is immutable.
|
|
76
|
+
*/
|
|
77
|
+
export async function runToolCallWithTimeout(options) {
|
|
78
|
+
const { toolCallId, toolName, timeoutMs, signal, start, emitUpdate, emitLateSettlement, toErrorResult } = options;
|
|
79
|
+
const lateSettlementPolicy = resolveToolExecutionPolicy({ lateSettlement: options.lateSettlement }).lateSettlement;
|
|
80
|
+
// Defensive: a parent already aborted before execution starts never runs the
|
|
81
|
+
// tool. prepareToolCall normally catches this earlier.
|
|
82
|
+
if (signal?.aborted) {
|
|
83
|
+
return {
|
|
84
|
+
result: createAbortedToolResult(false),
|
|
85
|
+
isError: true,
|
|
86
|
+
executionStarted: false,
|
|
87
|
+
terminalDisposition: "aborted",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const childController = new AbortController();
|
|
91
|
+
let raceSettled = false;
|
|
92
|
+
let disposed = false;
|
|
93
|
+
let timer;
|
|
94
|
+
let resolveCause;
|
|
95
|
+
const causePromise = new Promise((resolve) => {
|
|
96
|
+
resolveCause = resolve;
|
|
97
|
+
});
|
|
98
|
+
const abortChild = (reason) => {
|
|
99
|
+
if (!childController.signal.aborted) {
|
|
100
|
+
childController.abort(reason);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const onParentAbort = () => {
|
|
104
|
+
// Resolve the terminal cause before aborting the child so the committed
|
|
105
|
+
// disposition stays "aborted" even for a tool that rejects on its signal.
|
|
106
|
+
resolveCause({ kind: "aborted" });
|
|
107
|
+
abortChild(signal?.reason);
|
|
108
|
+
};
|
|
109
|
+
const dispose = () => {
|
|
110
|
+
if (disposed) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
disposed = true;
|
|
114
|
+
if (timer !== undefined) {
|
|
115
|
+
clearTimeout(timer);
|
|
116
|
+
timer = undefined;
|
|
117
|
+
}
|
|
118
|
+
signal?.removeEventListener("abort", onParentAbort);
|
|
119
|
+
};
|
|
120
|
+
if (timeoutMs > 0) {
|
|
121
|
+
timer = setTimeout(() => {
|
|
122
|
+
// Reason ordering: settle the timeout cause first, then signal the child.
|
|
123
|
+
resolveCause({ kind: "timeout" });
|
|
124
|
+
abortChild(new Error(`Tool "${toolName}" timed out after ${timeoutMs}ms`));
|
|
125
|
+
}, timeoutMs);
|
|
126
|
+
}
|
|
127
|
+
signal?.addEventListener("abort", onParentAbort, { once: true });
|
|
128
|
+
// Updates are observation-only: deliver those seen before terminality, but
|
|
129
|
+
// never let listener settlement or failure delay the terminal race.
|
|
130
|
+
const gatedUpdate = (partialResult) => {
|
|
131
|
+
if (raceSettled)
|
|
132
|
+
return;
|
|
133
|
+
void Promise.resolve()
|
|
134
|
+
.then(() => emitUpdate(partialResult))
|
|
135
|
+
.catch(() => undefined);
|
|
136
|
+
};
|
|
137
|
+
// Wrap so a synchronous throw from `start` becomes a rejection and, crucially,
|
|
138
|
+
// so this promise never rejects — both settlements map to a value. That makes
|
|
139
|
+
// the late observer safe from unhandled rejections.
|
|
140
|
+
const realSettled = (async () => start(childController.signal, gatedUpdate))().then((result) => ({ kind: "resolved", result }), (error) => ({ kind: "rejected", error }));
|
|
141
|
+
const raced = await Promise.race([
|
|
142
|
+
realSettled.then((settlement) => ({ from: "real", settlement })),
|
|
143
|
+
causePromise.then((cause) => ({ from: "cause", cause })),
|
|
144
|
+
]);
|
|
145
|
+
raceSettled = true;
|
|
146
|
+
dispose();
|
|
147
|
+
if (raced.from === "real") {
|
|
148
|
+
// The tool settled first: behave exactly like the no-timeout path.
|
|
149
|
+
const settlement = raced.settlement;
|
|
150
|
+
if (settlement.kind === "resolved") {
|
|
151
|
+
return { result: settlement.result, isError: false, executionStarted: true };
|
|
152
|
+
}
|
|
153
|
+
return { result: toErrorResult(settlement.error), isError: true, executionStarted: true };
|
|
154
|
+
}
|
|
155
|
+
// A terminal cause won. Observe the real promise's eventual settlement exactly
|
|
156
|
+
// once for audit only; realSettled never rejects and the chained catch guards
|
|
157
|
+
// against a throwing emit, so no unhandled rejection is possible. The
|
|
158
|
+
// `"ignore"` policy drops the audit event but never the immutable result.
|
|
159
|
+
const disposition = raced.cause.kind;
|
|
160
|
+
if (lateSettlementPolicy === "audit") {
|
|
161
|
+
realSettled
|
|
162
|
+
.then((settlement) => emitLateSettlement({
|
|
163
|
+
toolCallId,
|
|
164
|
+
toolName,
|
|
165
|
+
disposition,
|
|
166
|
+
outcome: settlement.kind === "resolved" ? "resolved" : "rejected",
|
|
167
|
+
}))
|
|
168
|
+
.catch(() => { });
|
|
169
|
+
}
|
|
170
|
+
const result = disposition === "timeout" ? createTimeoutToolResult(toolName, timeoutMs) : createAbortedToolResult(true);
|
|
171
|
+
return { result, isError: true, executionStarted: true, terminalDisposition: disposition };
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=tool-timeout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-timeout.js","sourceRoot":"","sources":["../src/tool-timeout.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Per-tool execution timeout and cancellation for the agent loop.\n *\n * The agent loop awaits each tool's `execute` promise at a single shared\n * chokepoint. A tool that ignores its `AbortSignal` and never settles would\n * otherwise stall the whole run. This module bounds that risk without process\n * killing: the real execute promise is raced against two terminal causes — a\n * per-call timeout timer and the parent run's abort — so an uncooperative tool\n * still yields an immediate, immutable terminal result.\n *\n * The child `AbortSignal` handed to the tool is best-effort cooperative\n * cancellation only; correctness comes from the race, not from the tool\n * honoring the signal. `AbortSignal.any()` is intentionally not used: the\n * parent-abort and timeout wiring is explicit so timer/listener disposal is\n * idempotent and runs on every outcome.\n *\n * A late settlement of the real promise (after the terminal cause already won)\n * is observed exactly once for audit only. It never emits a second tool result\n * or lifecycle end, never mutates the committed result, and — because the real\n * promise is wrapped so it never rejects — can never surface as an unhandled\n * rejection.\n */\n\nimport { createAbortedToolResult, createTimeoutToolResult } from \"./tool-execution-boundary.ts\";\nimport type {\n\tAgentLoopConfig,\n\tAgentTool,\n\tAgentToolResult,\n\tAgentToolUpdateCallback,\n\tToolExecutionPolicy,\n\tToolLateSettlementOutcome,\n\tToolTimeoutDisposition,\n} from \"./types.ts\";\n\nexport type { ToolDispositionEnvelope } from \"./tool-execution-boundary.ts\";\nexport { createAbortedToolResult, createTimeoutToolResult } from \"./tool-execution-boundary.ts\";\n\n/**\n * Resolve the effective {@link ToolExecutionPolicy} with the release default\n * `lateSettlement: \"audit\"`: late settlements are observable audit events\n * unless a caller explicitly opts out with `\"ignore\"`.\n */\nexport function resolveToolExecutionPolicy(policy: Partial<ToolExecutionPolicy> | undefined): ToolExecutionPolicy {\n\treturn {\n\t\t...(policy?.timeoutMs === undefined ? {} : { timeoutMs: policy.timeoutMs }),\n\t\t...(policy?.cancelSiblingsOnFatal === undefined ? {} : { cancelSiblingsOnFatal: policy.cancelSiblingsOnFatal }),\n\t\tlateSettlement: policy?.lateSettlement === \"ignore\" ? \"ignore\" : \"audit\",\n\t};\n}\n\n/** Audit-only description of a real tool promise settling after its terminal cause won. */\nexport interface ToolLateSettlement {\n\ttoolCallId: string;\n\ttoolName: string;\n\t/** The terminal disposition that was already committed when the real promise settled. */\n\tdisposition: ToolTimeoutDisposition;\n\t/** How the real tool promise eventually settled. Disposition-safe metadata only. */\n\toutcome: ToolLateSettlementOutcome;\n}\n\n/**\n * Resolve the effective per-call timeout with strict precedence:\n * per-tool `AgentTool.timeoutMs` > per-name `config.toolTimeouts[name]` >\n * global `config.toolTimeoutMs`.\n *\n * The first level that is *present* (not `undefined`) wins, so a per-tool `0`\n * deliberately disables the timeout even when a global default is set. A\n * resolved value that is absent, non-finite, or non-positive returns `0`, which\n * disables only the timer. Parent cancellation is still raced so an\n * uncooperative tool cannot keep an aborted run open.\n */\nexport function resolveToolTimeoutMs(\n\ttool: Pick<AgentTool<any>, \"timeoutMs\">,\n\tconfig: Pick<AgentLoopConfig, \"toolTimeoutMs\" | \"toolTimeouts\">,\n\ttoolName: string,\n): number {\n\tlet chosen: number | undefined;\n\tif (tool.timeoutMs !== undefined) {\n\t\tchosen = tool.timeoutMs;\n\t} else if (config.toolTimeouts?.[toolName] !== undefined) {\n\t\tchosen = config.toolTimeouts[toolName];\n\t} else {\n\t\tchosen = config.toolTimeoutMs;\n\t}\n\tif (typeof chosen !== \"number\" || !Number.isFinite(chosen) || chosen <= 0) {\n\t\treturn 0;\n\t}\n\treturn chosen;\n}\n\ntype TerminalCause = { kind: ToolTimeoutDisposition };\n\ntype RealSettlement<TDetails> =\n\t| { kind: \"resolved\"; result: AgentToolResult<TDetails> }\n\t| { kind: \"rejected\"; error: unknown };\n\nexport interface RunToolCallWithTimeoutOptions<TDetails = any> {\n\ttoolCallId: string;\n\ttoolName: string;\n\t/** Effective timeout in ms. A non-positive value disables the timer, not parent cancellation. */\n\ttimeoutMs: number;\n\t/** Parent run abort signal, if any. */\n\tsignal: AbortSignal | undefined;\n\t/** Start the real tool, passing the child (best-effort) signal and update sink. */\n\tstart: (childSignal: AbortSignal, onUpdate: AgentToolUpdateCallback<TDetails>) => Promise<AgentToolResult<TDetails>>;\n\t/** Emit a `tool_execution_update` for a partial result observed before terminality. */\n\temitUpdate: (partialResult: AgentToolResult<TDetails>) => Promise<void> | void;\n\t/** Emit the audit-only late-settlement event exactly once, after the terminal cause won. */\n\temitLateSettlement: (settlement: ToolLateSettlement) => Promise<void> | void;\n\t/** Map a thrown/rejected tool error to a normal error result (real completion path). */\n\ttoErrorResult: (error: unknown) => AgentToolResult<any>;\n\t/**\n\t * Late-settlement policy (default `\"audit\"`). `\"ignore\"` drops the audit\n\t * event; the committed terminal result is immutable under both policies.\n\t */\n\tlateSettlement?: ToolExecutionPolicy[\"lateSettlement\"];\n}\n\nexport interface RunToolCallWithTimeoutResult {\n\tresult: AgentToolResult<any>;\n\tisError: boolean;\n\t/** True when the tool's `execute` actually began before the result was committed. */\n\texecutionStarted: boolean;\n\t/** Terminal cause when the runtime (not the tool) committed the result. */\n\tterminalDisposition?: ToolTimeoutDisposition;\n}\n\n/**\n * Race a tool's `execute` promise against a per-call timeout and parent abort.\n *\n * Control flow (single path, no `AbortSignal.any()`):\n * - A timer and a parent-abort listener each resolve one shared \"terminal cause\"\n * deferred. Whichever fires first wins; resolving is idempotent.\n * - The timer callback resolves the timeout cause *before* aborting the child so\n * a tool that rejects promptly on abort cannot make \"aborted\" win a timeout.\n * - `Promise.race` picks the real settlement or the terminal cause. Timer and\n * listener disposal is idempotent and runs on every outcome.\n * - On a terminal-cause win, the real promise (wrapped so it never rejects) is\n * observed once for the audit event and the committed result is immutable.\n */\nexport async function runToolCallWithTimeout<TDetails = any>(\n\toptions: RunToolCallWithTimeoutOptions<TDetails>,\n): Promise<RunToolCallWithTimeoutResult> {\n\tconst { toolCallId, toolName, timeoutMs, signal, start, emitUpdate, emitLateSettlement, toErrorResult } = options;\n\tconst lateSettlementPolicy = resolveToolExecutionPolicy({ lateSettlement: options.lateSettlement }).lateSettlement;\n\n\t// Defensive: a parent already aborted before execution starts never runs the\n\t// tool. prepareToolCall normally catches this earlier.\n\tif (signal?.aborted) {\n\t\treturn {\n\t\t\tresult: createAbortedToolResult(false),\n\t\t\tisError: true,\n\t\t\texecutionStarted: false,\n\t\t\tterminalDisposition: \"aborted\",\n\t\t};\n\t}\n\n\tconst childController = new AbortController();\n\tlet raceSettled = false;\n\tlet disposed = false;\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\n\tlet resolveCause!: (cause: TerminalCause) => void;\n\tconst causePromise = new Promise<TerminalCause>((resolve) => {\n\t\tresolveCause = resolve;\n\t});\n\n\tconst abortChild = (reason: unknown): void => {\n\t\tif (!childController.signal.aborted) {\n\t\t\tchildController.abort(reason);\n\t\t}\n\t};\n\n\tconst onParentAbort = (): void => {\n\t\t// Resolve the terminal cause before aborting the child so the committed\n\t\t// disposition stays \"aborted\" even for a tool that rejects on its signal.\n\t\tresolveCause({ kind: \"aborted\" });\n\t\tabortChild(signal?.reason);\n\t};\n\n\tconst dispose = (): void => {\n\t\tif (disposed) {\n\t\t\treturn;\n\t\t}\n\t\tdisposed = true;\n\t\tif (timer !== undefined) {\n\t\t\tclearTimeout(timer);\n\t\t\ttimer = undefined;\n\t\t}\n\t\tsignal?.removeEventListener(\"abort\", onParentAbort);\n\t};\n\n\tif (timeoutMs > 0) {\n\t\ttimer = setTimeout(() => {\n\t\t\t// Reason ordering: settle the timeout cause first, then signal the child.\n\t\t\tresolveCause({ kind: \"timeout\" });\n\t\t\tabortChild(new Error(`Tool \"${toolName}\" timed out after ${timeoutMs}ms`));\n\t\t}, timeoutMs);\n\t}\n\n\tsignal?.addEventListener(\"abort\", onParentAbort, { once: true });\n\n\t// Updates are observation-only: deliver those seen before terminality, but\n\t// never let listener settlement or failure delay the terminal race.\n\tconst gatedUpdate: AgentToolUpdateCallback<TDetails> = (partialResult) => {\n\t\tif (raceSettled) return;\n\t\tvoid Promise.resolve()\n\t\t\t.then(() => emitUpdate(partialResult))\n\t\t\t.catch(() => undefined);\n\t};\n\n\t// Wrap so a synchronous throw from `start` becomes a rejection and, crucially,\n\t// so this promise never rejects — both settlements map to a value. That makes\n\t// the late observer safe from unhandled rejections.\n\tconst realSettled: Promise<RealSettlement<TDetails>> = (async () =>\n\t\tstart(childController.signal, gatedUpdate))().then(\n\t\t(result) => ({ kind: \"resolved\" as const, result }),\n\t\t(error) => ({ kind: \"rejected\" as const, error }),\n\t);\n\n\tconst raced = await Promise.race<\n\t\t{ from: \"real\"; settlement: RealSettlement<TDetails> } | { from: \"cause\"; cause: TerminalCause }\n\t>([\n\t\trealSettled.then((settlement) => ({ from: \"real\" as const, settlement })),\n\t\tcausePromise.then((cause) => ({ from: \"cause\" as const, cause })),\n\t]);\n\n\traceSettled = true;\n\tdispose();\n\n\tif (raced.from === \"real\") {\n\t\t// The tool settled first: behave exactly like the no-timeout path.\n\t\tconst settlement = raced.settlement;\n\t\tif (settlement.kind === \"resolved\") {\n\t\t\treturn { result: settlement.result, isError: false, executionStarted: true };\n\t\t}\n\t\treturn { result: toErrorResult(settlement.error), isError: true, executionStarted: true };\n\t}\n\n\t// A terminal cause won. Observe the real promise's eventual settlement exactly\n\t// once for audit only; realSettled never rejects and the chained catch guards\n\t// against a throwing emit, so no unhandled rejection is possible. The\n\t// `\"ignore\"` policy drops the audit event but never the immutable result.\n\tconst disposition = raced.cause.kind;\n\tif (lateSettlementPolicy === \"audit\") {\n\t\trealSettled\n\t\t\t.then((settlement) =>\n\t\t\t\temitLateSettlement({\n\t\t\t\t\ttoolCallId,\n\t\t\t\t\ttoolName,\n\t\t\t\t\tdisposition,\n\t\t\t\t\toutcome: settlement.kind === \"resolved\" ? \"resolved\" : \"rejected\",\n\t\t\t\t}),\n\t\t\t)\n\t\t\t.catch(() => {});\n\t}\n\n\tconst result =\n\t\tdisposition === \"timeout\" ? createTimeoutToolResult(toolName, timeoutMs) : createAbortedToolResult(true);\n\treturn { result, isError: true, executionStarted: true, terminalDisposition: disposition };\n}\n"]}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, browser-safe transcript integrity inspector and repair for the tool-call
|
|
3
|
+
* transcripts produced by the agent loop.
|
|
4
|
+
*
|
|
5
|
+
* The inspector detects five classes of structural corruption:
|
|
6
|
+
* - `missing_result`: a tool call with no matching tool result
|
|
7
|
+
* - `duplicate_result`: two or more tool results for the same call id
|
|
8
|
+
* - `orphan_result`: a tool result whose call id was never emitted
|
|
9
|
+
* - `duplicate_call_id`: two or more tool calls sharing an id
|
|
10
|
+
* - `interleaved_non_result`: a non-result message breaks the contiguous run of
|
|
11
|
+
* results that must follow an assistant message's tool calls (including a
|
|
12
|
+
* result that arrives before its call, after a user/custom boundary, or at the
|
|
13
|
+
* start of the transcript)
|
|
14
|
+
*
|
|
15
|
+
* IDs are accounted globally, so an orphan or duplicate is caught regardless of
|
|
16
|
+
* where it appears. Repair is intentionally conservative: it only appends
|
|
17
|
+
* synthetic results for unambiguous missing tail calls. Any duplicate, orphan,
|
|
18
|
+
* interleaving, or mid-transcript gap fails closed.
|
|
19
|
+
*
|
|
20
|
+
* This module uses no platform APIs (no `process`, fs, or timers) so it is safe
|
|
21
|
+
* to run in a browser.
|
|
22
|
+
*/
|
|
23
|
+
import type { ToolResultMessage } from "omk-ai";
|
|
24
|
+
import { type AgentMessage } from "./types.ts";
|
|
25
|
+
export type TranscriptIntegrityIssueKind = "missing_result" | "duplicate_result" | "orphan_result" | "duplicate_call_id" | "interleaved_non_result";
|
|
26
|
+
export interface TranscriptIntegrityIssue {
|
|
27
|
+
readonly kind: TranscriptIntegrityIssueKind;
|
|
28
|
+
readonly toolCallId: string;
|
|
29
|
+
readonly toolName?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface TranscriptIntegrityReport {
|
|
32
|
+
readonly ok: boolean;
|
|
33
|
+
readonly issues: readonly TranscriptIntegrityIssue[];
|
|
34
|
+
}
|
|
35
|
+
/** Thrown by {@link repairTranscriptIntegrity} when a transcript cannot be safely repaired. */
|
|
36
|
+
export declare class TranscriptIntegrityError extends Error {
|
|
37
|
+
readonly report: TranscriptIntegrityReport;
|
|
38
|
+
constructor(message: string, report: TranscriptIntegrityReport);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Inspect a transcript and report every detected integrity issue. Pure: does not
|
|
42
|
+
* mutate the input and uses no platform APIs.
|
|
43
|
+
*
|
|
44
|
+
* IDs are accounted globally first (catching duplicates, orphans, and missing
|
|
45
|
+
* results anywhere in the transcript), then a single left-to-right pass checks
|
|
46
|
+
* the ordering invariant that an assistant message's tool calls must be followed
|
|
47
|
+
* by a contiguous run of their results with no intervening non-result message.
|
|
48
|
+
*/
|
|
49
|
+
export declare function inspectTranscriptIntegrity(messages: readonly AgentMessage[]): TranscriptIntegrityReport;
|
|
50
|
+
/**
|
|
51
|
+
* Create a synthetic terminal tool result. Shared by transcript repair and the
|
|
52
|
+
* agent-loop abort closure so the disposition of an unresolved call is encoded
|
|
53
|
+
* identically everywhere. The `details.omk` envelope marks the artifact as
|
|
54
|
+
* synthetic (`executionStarted: false`): it closes the provider transcript and
|
|
55
|
+
* never claims the tool actually ran.
|
|
56
|
+
*/
|
|
57
|
+
export declare function createSyntheticToolResult(toolCallId: string, toolName: string, reason: string, timestamp?: number, disposition?: "aborted" | "skipped"): ToolResultMessage;
|
|
58
|
+
/**
|
|
59
|
+
* Repair a transcript by appending synthetic results for unambiguous missing
|
|
60
|
+
* tail calls. Fails closed (throws {@link TranscriptIntegrityError}) for any
|
|
61
|
+
* duplicate, orphan, interleaving, or mid-transcript gap. Idempotent: a second
|
|
62
|
+
* call on already-repaired input returns an equal copy with no new messages.
|
|
63
|
+
*/
|
|
64
|
+
export declare function repairTranscriptIntegrity(messages: readonly AgentMessage[], reason?: string): AgentMessage[];
|
|
65
|
+
//# sourceMappingURL=tool-transcript-integrity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-transcript-integrity.d.ts","sourceRoot":"","sources":["../src/tool-transcript-integrity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAA8B,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAE5E,OAAO,EAAE,KAAK,YAAY,EAA4B,MAAM,YAAY,CAAC;AAEzE,MAAM,MAAM,4BAA4B,GACrC,gBAAgB,GAChB,kBAAkB,GAClB,eAAe,GACf,mBAAmB,GACnB,wBAAwB,CAAC;AAE5B,MAAM,WAAW,wBAAwB;IACxC,QAAQ,CAAC,IAAI,EAAE,4BAA4B,CAAC;IAC5C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,yBAAyB;IACzC,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,SAAS,wBAAwB,EAAE,CAAC;CACrD;AAED,+FAA+F;AAC/F,qBAAa,wBAAyB,SAAQ,KAAK;IAClD,QAAQ,CAAC,MAAM,EAAE,yBAAyB,CAAC;IAC3C,YAAY,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,yBAAyB,EAI7D;CACD;AAcD;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,GAAG,yBAAyB,CA8FvG;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACxC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,SAAS,GAAE,MAAmB,EAC9B,WAAW,GAAE,SAAS,GAAG,SAAqB,GAC5C,iBAAiB,CAgBnB;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,YAAY,EAAE,CAgE5G","sourcesContent":["/**\n * Pure, browser-safe transcript integrity inspector and repair for the tool-call\n * transcripts produced by the agent loop.\n *\n * The inspector detects five classes of structural corruption:\n * - `missing_result`: a tool call with no matching tool result\n * - `duplicate_result`: two or more tool results for the same call id\n * - `orphan_result`: a tool result whose call id was never emitted\n * - `duplicate_call_id`: two or more tool calls sharing an id\n * - `interleaved_non_result`: a non-result message breaks the contiguous run of\n * results that must follow an assistant message's tool calls (including a\n * result that arrives before its call, after a user/custom boundary, or at the\n * start of the transcript)\n *\n * IDs are accounted globally, so an orphan or duplicate is caught regardless of\n * where it appears. Repair is intentionally conservative: it only appends\n * synthetic results for unambiguous missing tail calls. Any duplicate, orphan,\n * interleaving, or mid-transcript gap fails closed.\n *\n * This module uses no platform APIs (no `process`, fs, or timers) so it is safe\n * to run in a browser.\n */\n\nimport type { AssistantMessage, ToolCall, ToolResultMessage } from \"omk-ai\";\nimport { createImmutableSnapshot } from \"./plain-data.ts\";\nimport { type AgentMessage, createToolResultEnvelope } from \"./types.ts\";\n\nexport type TranscriptIntegrityIssueKind =\n\t| \"missing_result\"\n\t| \"duplicate_result\"\n\t| \"orphan_result\"\n\t| \"duplicate_call_id\"\n\t| \"interleaved_non_result\";\n\nexport interface TranscriptIntegrityIssue {\n\treadonly kind: TranscriptIntegrityIssueKind;\n\treadonly toolCallId: string;\n\treadonly toolName?: string;\n}\n\nexport interface TranscriptIntegrityReport {\n\treadonly ok: boolean;\n\treadonly issues: readonly TranscriptIntegrityIssue[];\n}\n\n/** Thrown by {@link repairTranscriptIntegrity} when a transcript cannot be safely repaired. */\nexport class TranscriptIntegrityError extends Error {\n\treadonly report: TranscriptIntegrityReport;\n\tconstructor(message: string, report: TranscriptIntegrityReport) {\n\t\tsuper(message);\n\t\tthis.name = \"TranscriptIntegrityError\";\n\t\tthis.report = report;\n\t}\n}\n\nfunction isAssistantMessage(message: AgentMessage): message is AssistantMessage {\n\treturn message.role === \"assistant\";\n}\n\nfunction isToolResultMessage(message: AgentMessage): message is ToolResultMessage {\n\treturn message.role === \"toolResult\";\n}\n\nfunction isToolCallBlock(block: unknown): block is ToolCall {\n\treturn (block as { type?: string } | null)?.type === \"toolCall\";\n}\n\n/**\n * Inspect a transcript and report every detected integrity issue. Pure: does not\n * mutate the input and uses no platform APIs.\n *\n * IDs are accounted globally first (catching duplicates, orphans, and missing\n * results anywhere in the transcript), then a single left-to-right pass checks\n * the ordering invariant that an assistant message's tool calls must be followed\n * by a contiguous run of their results with no intervening non-result message.\n */\nexport function inspectTranscriptIntegrity(messages: readonly AgentMessage[]): TranscriptIntegrityReport {\n\tconst issues: TranscriptIntegrityIssue[] = [];\n\n\tconst callIdCount = new Map<string, number>();\n\tconst resultIdCount = new Map<string, number>();\n\tconst callToolName = new Map<string, string>();\n\n\tfor (const message of messages) {\n\t\tif (isAssistantMessage(message)) {\n\t\t\tfor (const block of message.content) {\n\t\t\t\tif (isToolCallBlock(block)) {\n\t\t\t\t\tcallIdCount.set(block.id, (callIdCount.get(block.id) ?? 0) + 1);\n\t\t\t\t\tif (!callToolName.has(block.id)) {\n\t\t\t\t\t\tcallToolName.set(block.id, block.name);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (isToolResultMessage(message)) {\n\t\t\tresultIdCount.set(message.toolCallId, (resultIdCount.get(message.toolCallId) ?? 0) + 1);\n\t\t}\n\t}\n\n\tfor (const [id, count] of callIdCount) {\n\t\tif (count > 1) {\n\t\t\tissues.push({ kind: \"duplicate_call_id\", toolCallId: id, toolName: callToolName.get(id) });\n\t\t}\n\t}\n\tfor (const [id, count] of resultIdCount) {\n\t\tif (count > 1) {\n\t\t\tissues.push({ kind: \"duplicate_result\", toolCallId: id });\n\t\t}\n\t}\n\tfor (const [id] of resultIdCount) {\n\t\tif (!callIdCount.has(id)) {\n\t\t\tissues.push({ kind: \"orphan_result\", toolCallId: id });\n\t\t}\n\t}\n\tfor (const [id] of callIdCount) {\n\t\tif (!resultIdCount.has(id)) {\n\t\t\tissues.push({ kind: \"missing_result\", toolCallId: id, toolName: callToolName.get(id) });\n\t\t}\n\t}\n\n\tconst pending = new Map<string, string>();\n\tlet resultsRegion = false;\n\n\tfor (const message of messages) {\n\t\tif (isAssistantMessage(message)) {\n\t\t\tconst calls = message.content.filter(isToolCallBlock);\n\t\t\tif (calls.length > 0) {\n\t\t\t\t// A new assistant message with tool calls while previous calls are\n\t\t\t\t// still unresolved means a non-result interrupted the prior region.\n\t\t\t\tif (pending.size > 0) {\n\t\t\t\t\tfor (const [id, name] of pending) {\n\t\t\t\t\t\tissues.push({ kind: \"interleaved_non_result\", toolCallId: id, toolName: name });\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tfor (const call of calls) {\n\t\t\t\t\tpending.set(call.id, call.name);\n\t\t\t\t}\n\t\t\t\tresultsRegion = true;\n\t\t\t} else if (resultsRegion && pending.size > 0) {\n\t\t\t\tfor (const [id, name] of pending) {\n\t\t\t\t\tissues.push({ kind: \"interleaved_non_result\", toolCallId: id, toolName: name });\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t} else if (isToolResultMessage(message)) {\n\t\t\tif (resultsRegion && pending.has(message.toolCallId)) {\n\t\t\t\tpending.delete(message.toolCallId);\n\t\t\t\tif (pending.size === 0) {\n\t\t\t\t\tresultsRegion = false;\n\t\t\t\t}\n\t\t\t} else if (callIdCount.has(message.toolCallId)) {\n\t\t\t\t// Known call id arriving outside its contiguous results region:\n\t\t\t\t// orphan-at-start, after-user, or assistant -> non-result -> result.\n\t\t\t\tissues.push({ kind: \"interleaved_non_result\", toolCallId: message.toolCallId });\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\t// Unknown-id orphans are already reported above; do not double report.\n\t\t} else {\n\t\t\t// user or custom non-result message: breaks an open results region.\n\t\t\tif (resultsRegion && pending.size > 0) {\n\t\t\t\tfor (const [id, name] of pending) {\n\t\t\t\t\tissues.push({ kind: \"interleaved_non_result\", toolCallId: id, toolName: name });\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\tconst frozenIssues = Object.freeze(issues.map((issue) => Object.freeze({ ...issue })));\n\treturn Object.freeze({ ok: frozenIssues.length === 0, issues: frozenIssues });\n}\n\n/**\n * Create a synthetic terminal tool result. Shared by transcript repair and the\n * agent-loop abort closure so the disposition of an unresolved call is encoded\n * identically everywhere. The `details.omk` envelope marks the artifact as\n * synthetic (`executionStarted: false`): it closes the provider transcript and\n * never claims the tool actually ran.\n */\nexport function createSyntheticToolResult(\n\ttoolCallId: string,\n\ttoolName: string,\n\treason: string,\n\ttimestamp: number = Date.now(),\n\tdisposition: \"aborted\" | \"skipped\" = \"aborted\",\n): ToolResultMessage {\n\tconst envelope = createToolResultEnvelope({\n\t\tsynthetic: true,\n\t\tdisposition,\n\t\treason,\n\t\texecutionStarted: false,\n\t});\n\treturn createImmutableSnapshot({\n\t\trole: \"toolResult\",\n\t\ttoolCallId,\n\t\ttoolName,\n\t\tcontent: [{ type: \"text\", text: reason }],\n\t\tdetails: { omk: envelope },\n\t\tisError: true,\n\t\ttimestamp,\n\t});\n}\n\n/**\n * Repair a transcript by appending synthetic results for unambiguous missing\n * tail calls. Fails closed (throws {@link TranscriptIntegrityError}) for any\n * duplicate, orphan, interleaving, or mid-transcript gap. Idempotent: a second\n * call on already-repaired input returns an equal copy with no new messages.\n */\nexport function repairTranscriptIntegrity(messages: readonly AgentMessage[], reason?: string): AgentMessage[] {\n\tconst report = inspectTranscriptIntegrity(messages);\n\n\tfor (const issue of report.issues) {\n\t\tif (issue.kind !== \"missing_result\") {\n\t\t\tthrow new TranscriptIntegrityError(\n\t\t\t\t`Cannot repair transcript: detected ${issue.kind} for tool call ${issue.toolCallId}`,\n\t\t\t\treport,\n\t\t\t);\n\t\t}\n\t}\n\n\tconst missingIds = new Set(report.issues.map((issue) => issue.toolCallId));\n\tif (missingIds.size === 0) {\n\t\treturn messages.slice();\n\t}\n\n\tlet lastAssistantIndex = -1;\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tif (isAssistantMessage(messages[i])) {\n\t\t\tlastAssistantIndex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (lastAssistantIndex === -1) {\n\t\tthrow new TranscriptIntegrityError(\n\t\t\t\"Cannot repair transcript: missing results without an assistant message\",\n\t\t\treport,\n\t\t);\n\t}\n\n\tconst lastAssistant = messages[lastAssistantIndex];\n\tif (!isAssistantMessage(lastAssistant)) {\n\t\tthrow new TranscriptIntegrityError(\"Cannot repair transcript: trailing message is not assistant\", report);\n\t}\n\tconst tailCalls = lastAssistant.content.filter(isToolCallBlock);\n\tconst tailCallIds = new Set(tailCalls.map((call) => call.id));\n\tfor (const id of missingIds) {\n\t\tif (!tailCallIds.has(id)) {\n\t\t\tthrow new TranscriptIntegrityError(\n\t\t\t\t`Cannot repair transcript: missing result for ${id} is not in the trailing assistant message`,\n\t\t\t\treport,\n\t\t\t);\n\t\t}\n\t}\n\n\tfor (let i = lastAssistantIndex + 1; i < messages.length; i++) {\n\t\tconst message = messages[i];\n\t\tif (!isToolResultMessage(message) || !tailCallIds.has(message.toolCallId)) {\n\t\t\tthrow new TranscriptIntegrityError(\n\t\t\t\t\"Cannot repair transcript: non-result message after the trailing assistant message\",\n\t\t\t\treport,\n\t\t\t);\n\t\t}\n\t}\n\n\tconst text = reason ?? \"Tool result missing; synthesized by transcript repair\";\n\tconst repaired = messages.slice();\n\tfor (const call of tailCalls) {\n\t\tif (missingIds.has(call.id)) {\n\t\t\trepaired.push(createSyntheticToolResult(call.id, call.name, text));\n\t\t}\n\t}\n\treturn repaired;\n}\n"]}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, browser-safe transcript integrity inspector and repair for the tool-call
|
|
3
|
+
* transcripts produced by the agent loop.
|
|
4
|
+
*
|
|
5
|
+
* The inspector detects five classes of structural corruption:
|
|
6
|
+
* - `missing_result`: a tool call with no matching tool result
|
|
7
|
+
* - `duplicate_result`: two or more tool results for the same call id
|
|
8
|
+
* - `orphan_result`: a tool result whose call id was never emitted
|
|
9
|
+
* - `duplicate_call_id`: two or more tool calls sharing an id
|
|
10
|
+
* - `interleaved_non_result`: a non-result message breaks the contiguous run of
|
|
11
|
+
* results that must follow an assistant message's tool calls (including a
|
|
12
|
+
* result that arrives before its call, after a user/custom boundary, or at the
|
|
13
|
+
* start of the transcript)
|
|
14
|
+
*
|
|
15
|
+
* IDs are accounted globally, so an orphan or duplicate is caught regardless of
|
|
16
|
+
* where it appears. Repair is intentionally conservative: it only appends
|
|
17
|
+
* synthetic results for unambiguous missing tail calls. Any duplicate, orphan,
|
|
18
|
+
* interleaving, or mid-transcript gap fails closed.
|
|
19
|
+
*
|
|
20
|
+
* This module uses no platform APIs (no `process`, fs, or timers) so it is safe
|
|
21
|
+
* to run in a browser.
|
|
22
|
+
*/
|
|
23
|
+
import { createImmutableSnapshot } from "./plain-data.js";
|
|
24
|
+
import { createToolResultEnvelope } from "./types.js";
|
|
25
|
+
/** Thrown by {@link repairTranscriptIntegrity} when a transcript cannot be safely repaired. */
|
|
26
|
+
export class TranscriptIntegrityError extends Error {
|
|
27
|
+
report;
|
|
28
|
+
constructor(message, report) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = "TranscriptIntegrityError";
|
|
31
|
+
this.report = report;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function isAssistantMessage(message) {
|
|
35
|
+
return message.role === "assistant";
|
|
36
|
+
}
|
|
37
|
+
function isToolResultMessage(message) {
|
|
38
|
+
return message.role === "toolResult";
|
|
39
|
+
}
|
|
40
|
+
function isToolCallBlock(block) {
|
|
41
|
+
return block?.type === "toolCall";
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Inspect a transcript and report every detected integrity issue. Pure: does not
|
|
45
|
+
* mutate the input and uses no platform APIs.
|
|
46
|
+
*
|
|
47
|
+
* IDs are accounted globally first (catching duplicates, orphans, and missing
|
|
48
|
+
* results anywhere in the transcript), then a single left-to-right pass checks
|
|
49
|
+
* the ordering invariant that an assistant message's tool calls must be followed
|
|
50
|
+
* by a contiguous run of their results with no intervening non-result message.
|
|
51
|
+
*/
|
|
52
|
+
export function inspectTranscriptIntegrity(messages) {
|
|
53
|
+
const issues = [];
|
|
54
|
+
const callIdCount = new Map();
|
|
55
|
+
const resultIdCount = new Map();
|
|
56
|
+
const callToolName = new Map();
|
|
57
|
+
for (const message of messages) {
|
|
58
|
+
if (isAssistantMessage(message)) {
|
|
59
|
+
for (const block of message.content) {
|
|
60
|
+
if (isToolCallBlock(block)) {
|
|
61
|
+
callIdCount.set(block.id, (callIdCount.get(block.id) ?? 0) + 1);
|
|
62
|
+
if (!callToolName.has(block.id)) {
|
|
63
|
+
callToolName.set(block.id, block.name);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else if (isToolResultMessage(message)) {
|
|
69
|
+
resultIdCount.set(message.toolCallId, (resultIdCount.get(message.toolCallId) ?? 0) + 1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (const [id, count] of callIdCount) {
|
|
73
|
+
if (count > 1) {
|
|
74
|
+
issues.push({ kind: "duplicate_call_id", toolCallId: id, toolName: callToolName.get(id) });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (const [id, count] of resultIdCount) {
|
|
78
|
+
if (count > 1) {
|
|
79
|
+
issues.push({ kind: "duplicate_result", toolCallId: id });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const [id] of resultIdCount) {
|
|
83
|
+
if (!callIdCount.has(id)) {
|
|
84
|
+
issues.push({ kind: "orphan_result", toolCallId: id });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
for (const [id] of callIdCount) {
|
|
88
|
+
if (!resultIdCount.has(id)) {
|
|
89
|
+
issues.push({ kind: "missing_result", toolCallId: id, toolName: callToolName.get(id) });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const pending = new Map();
|
|
93
|
+
let resultsRegion = false;
|
|
94
|
+
for (const message of messages) {
|
|
95
|
+
if (isAssistantMessage(message)) {
|
|
96
|
+
const calls = message.content.filter(isToolCallBlock);
|
|
97
|
+
if (calls.length > 0) {
|
|
98
|
+
// A new assistant message with tool calls while previous calls are
|
|
99
|
+
// still unresolved means a non-result interrupted the prior region.
|
|
100
|
+
if (pending.size > 0) {
|
|
101
|
+
for (const [id, name] of pending) {
|
|
102
|
+
issues.push({ kind: "interleaved_non_result", toolCallId: id, toolName: name });
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
for (const call of calls) {
|
|
107
|
+
pending.set(call.id, call.name);
|
|
108
|
+
}
|
|
109
|
+
resultsRegion = true;
|
|
110
|
+
}
|
|
111
|
+
else if (resultsRegion && pending.size > 0) {
|
|
112
|
+
for (const [id, name] of pending) {
|
|
113
|
+
issues.push({ kind: "interleaved_non_result", toolCallId: id, toolName: name });
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else if (isToolResultMessage(message)) {
|
|
119
|
+
if (resultsRegion && pending.has(message.toolCallId)) {
|
|
120
|
+
pending.delete(message.toolCallId);
|
|
121
|
+
if (pending.size === 0) {
|
|
122
|
+
resultsRegion = false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else if (callIdCount.has(message.toolCallId)) {
|
|
126
|
+
// Known call id arriving outside its contiguous results region:
|
|
127
|
+
// orphan-at-start, after-user, or assistant -> non-result -> result.
|
|
128
|
+
issues.push({ kind: "interleaved_non_result", toolCallId: message.toolCallId });
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
// Unknown-id orphans are already reported above; do not double report.
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
// user or custom non-result message: breaks an open results region.
|
|
135
|
+
if (resultsRegion && pending.size > 0) {
|
|
136
|
+
for (const [id, name] of pending) {
|
|
137
|
+
issues.push({ kind: "interleaved_non_result", toolCallId: id, toolName: name });
|
|
138
|
+
}
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const frozenIssues = Object.freeze(issues.map((issue) => Object.freeze({ ...issue })));
|
|
144
|
+
return Object.freeze({ ok: frozenIssues.length === 0, issues: frozenIssues });
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Create a synthetic terminal tool result. Shared by transcript repair and the
|
|
148
|
+
* agent-loop abort closure so the disposition of an unresolved call is encoded
|
|
149
|
+
* identically everywhere. The `details.omk` envelope marks the artifact as
|
|
150
|
+
* synthetic (`executionStarted: false`): it closes the provider transcript and
|
|
151
|
+
* never claims the tool actually ran.
|
|
152
|
+
*/
|
|
153
|
+
export function createSyntheticToolResult(toolCallId, toolName, reason, timestamp = Date.now(), disposition = "aborted") {
|
|
154
|
+
const envelope = createToolResultEnvelope({
|
|
155
|
+
synthetic: true,
|
|
156
|
+
disposition,
|
|
157
|
+
reason,
|
|
158
|
+
executionStarted: false,
|
|
159
|
+
});
|
|
160
|
+
return createImmutableSnapshot({
|
|
161
|
+
role: "toolResult",
|
|
162
|
+
toolCallId,
|
|
163
|
+
toolName,
|
|
164
|
+
content: [{ type: "text", text: reason }],
|
|
165
|
+
details: { omk: envelope },
|
|
166
|
+
isError: true,
|
|
167
|
+
timestamp,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Repair a transcript by appending synthetic results for unambiguous missing
|
|
172
|
+
* tail calls. Fails closed (throws {@link TranscriptIntegrityError}) for any
|
|
173
|
+
* duplicate, orphan, interleaving, or mid-transcript gap. Idempotent: a second
|
|
174
|
+
* call on already-repaired input returns an equal copy with no new messages.
|
|
175
|
+
*/
|
|
176
|
+
export function repairTranscriptIntegrity(messages, reason) {
|
|
177
|
+
const report = inspectTranscriptIntegrity(messages);
|
|
178
|
+
for (const issue of report.issues) {
|
|
179
|
+
if (issue.kind !== "missing_result") {
|
|
180
|
+
throw new TranscriptIntegrityError(`Cannot repair transcript: detected ${issue.kind} for tool call ${issue.toolCallId}`, report);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const missingIds = new Set(report.issues.map((issue) => issue.toolCallId));
|
|
184
|
+
if (missingIds.size === 0) {
|
|
185
|
+
return messages.slice();
|
|
186
|
+
}
|
|
187
|
+
let lastAssistantIndex = -1;
|
|
188
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
189
|
+
if (isAssistantMessage(messages[i])) {
|
|
190
|
+
lastAssistantIndex = i;
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (lastAssistantIndex === -1) {
|
|
195
|
+
throw new TranscriptIntegrityError("Cannot repair transcript: missing results without an assistant message", report);
|
|
196
|
+
}
|
|
197
|
+
const lastAssistant = messages[lastAssistantIndex];
|
|
198
|
+
if (!isAssistantMessage(lastAssistant)) {
|
|
199
|
+
throw new TranscriptIntegrityError("Cannot repair transcript: trailing message is not assistant", report);
|
|
200
|
+
}
|
|
201
|
+
const tailCalls = lastAssistant.content.filter(isToolCallBlock);
|
|
202
|
+
const tailCallIds = new Set(tailCalls.map((call) => call.id));
|
|
203
|
+
for (const id of missingIds) {
|
|
204
|
+
if (!tailCallIds.has(id)) {
|
|
205
|
+
throw new TranscriptIntegrityError(`Cannot repair transcript: missing result for ${id} is not in the trailing assistant message`, report);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
for (let i = lastAssistantIndex + 1; i < messages.length; i++) {
|
|
209
|
+
const message = messages[i];
|
|
210
|
+
if (!isToolResultMessage(message) || !tailCallIds.has(message.toolCallId)) {
|
|
211
|
+
throw new TranscriptIntegrityError("Cannot repair transcript: non-result message after the trailing assistant message", report);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const text = reason ?? "Tool result missing; synthesized by transcript repair";
|
|
215
|
+
const repaired = messages.slice();
|
|
216
|
+
for (const call of tailCalls) {
|
|
217
|
+
if (missingIds.has(call.id)) {
|
|
218
|
+
repaired.push(createSyntheticToolResult(call.id, call.name, text));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return repaired;
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=tool-transcript-integrity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-transcript-integrity.js","sourceRoot":"","sources":["../src/tool-transcript-integrity.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pure, browser-safe transcript integrity inspector and repair for the tool-call\n * transcripts produced by the agent loop.\n *\n * The inspector detects five classes of structural corruption:\n * - `missing_result`: a tool call with no matching tool result\n * - `duplicate_result`: two or more tool results for the same call id\n * - `orphan_result`: a tool result whose call id was never emitted\n * - `duplicate_call_id`: two or more tool calls sharing an id\n * - `interleaved_non_result`: a non-result message breaks the contiguous run of\n * results that must follow an assistant message's tool calls (including a\n * result that arrives before its call, after a user/custom boundary, or at the\n * start of the transcript)\n *\n * IDs are accounted globally, so an orphan or duplicate is caught regardless of\n * where it appears. Repair is intentionally conservative: it only appends\n * synthetic results for unambiguous missing tail calls. Any duplicate, orphan,\n * interleaving, or mid-transcript gap fails closed.\n *\n * This module uses no platform APIs (no `process`, fs, or timers) so it is safe\n * to run in a browser.\n */\n\nimport type { AssistantMessage, ToolCall, ToolResultMessage } from \"omk-ai\";\nimport { createImmutableSnapshot } from \"./plain-data.ts\";\nimport { type AgentMessage, createToolResultEnvelope } from \"./types.ts\";\n\nexport type TranscriptIntegrityIssueKind =\n\t| \"missing_result\"\n\t| \"duplicate_result\"\n\t| \"orphan_result\"\n\t| \"duplicate_call_id\"\n\t| \"interleaved_non_result\";\n\nexport interface TranscriptIntegrityIssue {\n\treadonly kind: TranscriptIntegrityIssueKind;\n\treadonly toolCallId: string;\n\treadonly toolName?: string;\n}\n\nexport interface TranscriptIntegrityReport {\n\treadonly ok: boolean;\n\treadonly issues: readonly TranscriptIntegrityIssue[];\n}\n\n/** Thrown by {@link repairTranscriptIntegrity} when a transcript cannot be safely repaired. */\nexport class TranscriptIntegrityError extends Error {\n\treadonly report: TranscriptIntegrityReport;\n\tconstructor(message: string, report: TranscriptIntegrityReport) {\n\t\tsuper(message);\n\t\tthis.name = \"TranscriptIntegrityError\";\n\t\tthis.report = report;\n\t}\n}\n\nfunction isAssistantMessage(message: AgentMessage): message is AssistantMessage {\n\treturn message.role === \"assistant\";\n}\n\nfunction isToolResultMessage(message: AgentMessage): message is ToolResultMessage {\n\treturn message.role === \"toolResult\";\n}\n\nfunction isToolCallBlock(block: unknown): block is ToolCall {\n\treturn (block as { type?: string } | null)?.type === \"toolCall\";\n}\n\n/**\n * Inspect a transcript and report every detected integrity issue. Pure: does not\n * mutate the input and uses no platform APIs.\n *\n * IDs are accounted globally first (catching duplicates, orphans, and missing\n * results anywhere in the transcript), then a single left-to-right pass checks\n * the ordering invariant that an assistant message's tool calls must be followed\n * by a contiguous run of their results with no intervening non-result message.\n */\nexport function inspectTranscriptIntegrity(messages: readonly AgentMessage[]): TranscriptIntegrityReport {\n\tconst issues: TranscriptIntegrityIssue[] = [];\n\n\tconst callIdCount = new Map<string, number>();\n\tconst resultIdCount = new Map<string, number>();\n\tconst callToolName = new Map<string, string>();\n\n\tfor (const message of messages) {\n\t\tif (isAssistantMessage(message)) {\n\t\t\tfor (const block of message.content) {\n\t\t\t\tif (isToolCallBlock(block)) {\n\t\t\t\t\tcallIdCount.set(block.id, (callIdCount.get(block.id) ?? 0) + 1);\n\t\t\t\t\tif (!callToolName.has(block.id)) {\n\t\t\t\t\t\tcallToolName.set(block.id, block.name);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (isToolResultMessage(message)) {\n\t\t\tresultIdCount.set(message.toolCallId, (resultIdCount.get(message.toolCallId) ?? 0) + 1);\n\t\t}\n\t}\n\n\tfor (const [id, count] of callIdCount) {\n\t\tif (count > 1) {\n\t\t\tissues.push({ kind: \"duplicate_call_id\", toolCallId: id, toolName: callToolName.get(id) });\n\t\t}\n\t}\n\tfor (const [id, count] of resultIdCount) {\n\t\tif (count > 1) {\n\t\t\tissues.push({ kind: \"duplicate_result\", toolCallId: id });\n\t\t}\n\t}\n\tfor (const [id] of resultIdCount) {\n\t\tif (!callIdCount.has(id)) {\n\t\t\tissues.push({ kind: \"orphan_result\", toolCallId: id });\n\t\t}\n\t}\n\tfor (const [id] of callIdCount) {\n\t\tif (!resultIdCount.has(id)) {\n\t\t\tissues.push({ kind: \"missing_result\", toolCallId: id, toolName: callToolName.get(id) });\n\t\t}\n\t}\n\n\tconst pending = new Map<string, string>();\n\tlet resultsRegion = false;\n\n\tfor (const message of messages) {\n\t\tif (isAssistantMessage(message)) {\n\t\t\tconst calls = message.content.filter(isToolCallBlock);\n\t\t\tif (calls.length > 0) {\n\t\t\t\t// A new assistant message with tool calls while previous calls are\n\t\t\t\t// still unresolved means a non-result interrupted the prior region.\n\t\t\t\tif (pending.size > 0) {\n\t\t\t\t\tfor (const [id, name] of pending) {\n\t\t\t\t\t\tissues.push({ kind: \"interleaved_non_result\", toolCallId: id, toolName: name });\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tfor (const call of calls) {\n\t\t\t\t\tpending.set(call.id, call.name);\n\t\t\t\t}\n\t\t\t\tresultsRegion = true;\n\t\t\t} else if (resultsRegion && pending.size > 0) {\n\t\t\t\tfor (const [id, name] of pending) {\n\t\t\t\t\tissues.push({ kind: \"interleaved_non_result\", toolCallId: id, toolName: name });\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t} else if (isToolResultMessage(message)) {\n\t\t\tif (resultsRegion && pending.has(message.toolCallId)) {\n\t\t\t\tpending.delete(message.toolCallId);\n\t\t\t\tif (pending.size === 0) {\n\t\t\t\t\tresultsRegion = false;\n\t\t\t\t}\n\t\t\t} else if (callIdCount.has(message.toolCallId)) {\n\t\t\t\t// Known call id arriving outside its contiguous results region:\n\t\t\t\t// orphan-at-start, after-user, or assistant -> non-result -> result.\n\t\t\t\tissues.push({ kind: \"interleaved_non_result\", toolCallId: message.toolCallId });\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\t// Unknown-id orphans are already reported above; do not double report.\n\t\t} else {\n\t\t\t// user or custom non-result message: breaks an open results region.\n\t\t\tif (resultsRegion && pending.size > 0) {\n\t\t\t\tfor (const [id, name] of pending) {\n\t\t\t\t\tissues.push({ kind: \"interleaved_non_result\", toolCallId: id, toolName: name });\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\tconst frozenIssues = Object.freeze(issues.map((issue) => Object.freeze({ ...issue })));\n\treturn Object.freeze({ ok: frozenIssues.length === 0, issues: frozenIssues });\n}\n\n/**\n * Create a synthetic terminal tool result. Shared by transcript repair and the\n * agent-loop abort closure so the disposition of an unresolved call is encoded\n * identically everywhere. The `details.omk` envelope marks the artifact as\n * synthetic (`executionStarted: false`): it closes the provider transcript and\n * never claims the tool actually ran.\n */\nexport function createSyntheticToolResult(\n\ttoolCallId: string,\n\ttoolName: string,\n\treason: string,\n\ttimestamp: number = Date.now(),\n\tdisposition: \"aborted\" | \"skipped\" = \"aborted\",\n): ToolResultMessage {\n\tconst envelope = createToolResultEnvelope({\n\t\tsynthetic: true,\n\t\tdisposition,\n\t\treason,\n\t\texecutionStarted: false,\n\t});\n\treturn createImmutableSnapshot({\n\t\trole: \"toolResult\",\n\t\ttoolCallId,\n\t\ttoolName,\n\t\tcontent: [{ type: \"text\", text: reason }],\n\t\tdetails: { omk: envelope },\n\t\tisError: true,\n\t\ttimestamp,\n\t});\n}\n\n/**\n * Repair a transcript by appending synthetic results for unambiguous missing\n * tail calls. Fails closed (throws {@link TranscriptIntegrityError}) for any\n * duplicate, orphan, interleaving, or mid-transcript gap. Idempotent: a second\n * call on already-repaired input returns an equal copy with no new messages.\n */\nexport function repairTranscriptIntegrity(messages: readonly AgentMessage[], reason?: string): AgentMessage[] {\n\tconst report = inspectTranscriptIntegrity(messages);\n\n\tfor (const issue of report.issues) {\n\t\tif (issue.kind !== \"missing_result\") {\n\t\t\tthrow new TranscriptIntegrityError(\n\t\t\t\t`Cannot repair transcript: detected ${issue.kind} for tool call ${issue.toolCallId}`,\n\t\t\t\treport,\n\t\t\t);\n\t\t}\n\t}\n\n\tconst missingIds = new Set(report.issues.map((issue) => issue.toolCallId));\n\tif (missingIds.size === 0) {\n\t\treturn messages.slice();\n\t}\n\n\tlet lastAssistantIndex = -1;\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tif (isAssistantMessage(messages[i])) {\n\t\t\tlastAssistantIndex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (lastAssistantIndex === -1) {\n\t\tthrow new TranscriptIntegrityError(\n\t\t\t\"Cannot repair transcript: missing results without an assistant message\",\n\t\t\treport,\n\t\t);\n\t}\n\n\tconst lastAssistant = messages[lastAssistantIndex];\n\tif (!isAssistantMessage(lastAssistant)) {\n\t\tthrow new TranscriptIntegrityError(\"Cannot repair transcript: trailing message is not assistant\", report);\n\t}\n\tconst tailCalls = lastAssistant.content.filter(isToolCallBlock);\n\tconst tailCallIds = new Set(tailCalls.map((call) => call.id));\n\tfor (const id of missingIds) {\n\t\tif (!tailCallIds.has(id)) {\n\t\t\tthrow new TranscriptIntegrityError(\n\t\t\t\t`Cannot repair transcript: missing result for ${id} is not in the trailing assistant message`,\n\t\t\t\treport,\n\t\t\t);\n\t\t}\n\t}\n\n\tfor (let i = lastAssistantIndex + 1; i < messages.length; i++) {\n\t\tconst message = messages[i];\n\t\tif (!isToolResultMessage(message) || !tailCallIds.has(message.toolCallId)) {\n\t\t\tthrow new TranscriptIntegrityError(\n\t\t\t\t\"Cannot repair transcript: non-result message after the trailing assistant message\",\n\t\t\t\treport,\n\t\t\t);\n\t\t}\n\t}\n\n\tconst text = reason ?? \"Tool result missing; synthesized by transcript repair\";\n\tconst repaired = messages.slice();\n\tfor (const call of tailCalls) {\n\t\tif (missingIds.has(call.id)) {\n\t\t\trepaired.push(createSyntheticToolResult(call.id, call.name, text));\n\t\t}\n\t}\n\treturn repaired;\n}\n"]}
|