omk-agent-core 0.90.7 → 0.90.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +97 -1
  2. package/dist/agent-loop.d.ts +25 -2
  3. package/dist/agent-loop.d.ts.map +1 -1
  4. package/dist/agent-loop.js +524 -189
  5. package/dist/agent-loop.js.map +1 -1
  6. package/dist/agent.d.ts +29 -7
  7. package/dist/agent.d.ts.map +1 -1
  8. package/dist/agent.js +81 -46
  9. package/dist/agent.js.map +1 -1
  10. package/dist/builtin-tool-resource-claims.d.ts +19 -0
  11. package/dist/builtin-tool-resource-claims.d.ts.map +1 -0
  12. package/dist/builtin-tool-resource-claims.js +200 -0
  13. package/dist/builtin-tool-resource-claims.js.map +1 -0
  14. package/dist/index.d.ts +5 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +8 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/node-resource-resolver.d.ts +42 -0
  19. package/dist/node-resource-resolver.d.ts.map +1 -0
  20. package/dist/node-resource-resolver.js +149 -0
  21. package/dist/node-resource-resolver.js.map +1 -0
  22. package/dist/node.d.ts +1 -0
  23. package/dist/node.d.ts.map +1 -1
  24. package/dist/node.js +2 -0
  25. package/dist/node.js.map +1 -1
  26. package/dist/parallel-tool-batch.d.ts +12 -1
  27. package/dist/parallel-tool-batch.d.ts.map +1 -1
  28. package/dist/parallel-tool-batch.js +71 -49
  29. package/dist/parallel-tool-batch.js.map +1 -1
  30. package/dist/path-segments.d.ts +21 -1
  31. package/dist/path-segments.d.ts.map +1 -1
  32. package/dist/path-segments.js +91 -9
  33. package/dist/path-segments.js.map +1 -1
  34. package/dist/plain-data.d.ts +7 -0
  35. package/dist/plain-data.d.ts.map +1 -0
  36. package/dist/plain-data.js +70 -0
  37. package/dist/plain-data.js.map +1 -0
  38. package/dist/tool-dag-scheduler.d.ts +86 -0
  39. package/dist/tool-dag-scheduler.d.ts.map +1 -0
  40. package/dist/tool-dag-scheduler.js +171 -0
  41. package/dist/tool-dag-scheduler.js.map +1 -0
  42. package/dist/tool-execution-boundary.d.ts +52 -0
  43. package/dist/tool-execution-boundary.d.ts.map +1 -0
  44. package/dist/tool-execution-boundary.js +185 -0
  45. package/dist/tool-execution-boundary.js.map +1 -0
  46. package/dist/tool-resource-claims.d.ts +31 -0
  47. package/dist/tool-resource-claims.d.ts.map +1 -0
  48. package/dist/tool-resource-claims.js +128 -0
  49. package/dist/tool-resource-claims.js.map +1 -0
  50. package/dist/tool-timeout.d.ts +96 -0
  51. package/dist/tool-timeout.d.ts.map +1 -0
  52. package/dist/tool-timeout.js +173 -0
  53. package/dist/tool-timeout.js.map +1 -0
  54. package/dist/tool-transcript-integrity.d.ts +65 -0
  55. package/dist/tool-transcript-integrity.d.ts.map +1 -0
  56. package/dist/tool-transcript-integrity.js +223 -0
  57. package/dist/tool-transcript-integrity.js.map +1 -0
  58. package/dist/types.d.ts +219 -10
  59. package/dist/types.d.ts.map +1 -1
  60. package/dist/types.js +50 -1
  61. package/dist/types.js.map +1 -1
  62. package/package.json +2 -2
@@ -0,0 +1,128 @@
1
+ /** Browser-safe resource claims for the opt-in dag-v2 scheduler. */
2
+ import { findRegisteredToolClaimDefinition, isBuiltinPathClaimTool, isPlainArguments, pathClaimsOverlap, resolveBuiltinPathClaimWithIdentity, resolvePathClaimWithIdentity, resolveToolClaims, resolveToolPolicy, } from "./builtin-tool-resource-claims.js";
3
+ import { NEVER_PARALLEL_TOOLS } from "./parallel-tool-batch.js";
4
+ export { resolvePathClaimKey, resolveToolClaims } from "./builtin-tool-resource-claims.js";
5
+ function isNonPathKind(value) {
6
+ return value === "session" || value === "terminal" || value === "network" || value === "global";
7
+ }
8
+ async function normalizeCustomClaims(value, options) {
9
+ if (value === "exclusive")
10
+ return { kind: "exclusive" };
11
+ if (!Array.isArray(value) || value.length === 0)
12
+ return { kind: "exclusive" };
13
+ const claims = [];
14
+ let hasExclusiveAccess = false;
15
+ for (const candidate of value) {
16
+ if (!isPlainArguments(candidate) || typeof candidate.key !== "string" || candidate.key.trim().length === 0) {
17
+ return { kind: "exclusive" };
18
+ }
19
+ if (candidate.kind === "path") {
20
+ if (candidate.access !== "read" && candidate.access !== "write")
21
+ return { kind: "exclusive" };
22
+ const pathResolution = await resolvePathClaimWithIdentity(candidate.key, candidate.access, options);
23
+ if (pathResolution.kind === "exclusive")
24
+ return pathResolution;
25
+ claims.push(...pathResolution.claims);
26
+ continue;
27
+ }
28
+ if (!isNonPathKind(candidate.kind))
29
+ return { kind: "exclusive" };
30
+ if (candidate.access !== "read" && candidate.access !== "write" && candidate.access !== "exclusive") {
31
+ return { kind: "exclusive" };
32
+ }
33
+ const access = candidate.access;
34
+ claims.push({ kind: candidate.kind, key: candidate.key, access });
35
+ if (access === "exclusive")
36
+ hasExclusiveAccess = true;
37
+ }
38
+ return hasExclusiveAccess ? { kind: "exclusive" } : { kind: "claims", claims };
39
+ }
40
+ /** Resolve one call, failing malformed or rejected extension claims closed. */
41
+ export async function resolveToolClaimsForCall(toolCall, options) {
42
+ const registeredTool = findRegisteredToolClaimDefinition(toolCall.name, options.registeredTools);
43
+ if (!registeredTool?.resourceClaims) {
44
+ if (options.resourceKeyResolver &&
45
+ isBuiltinPathClaimTool(toolCall.name) &&
46
+ isPlainArguments(toolCall.arguments) &&
47
+ !NEVER_PARALLEL_TOOLS.has(toolCall.name) &&
48
+ resolveToolPolicy(toolCall.name, options) !== "sequential") {
49
+ return resolveBuiltinPathClaimWithIdentity(toolCall, options);
50
+ }
51
+ return resolveToolClaims(toolCall, options);
52
+ }
53
+ if (!isPlainArguments(toolCall.arguments))
54
+ return { kind: "exclusive" };
55
+ let resolution;
56
+ try {
57
+ const claims = await registeredTool.resourceClaims(toolCall.arguments, {
58
+ cwd: options.cwd,
59
+ toolCallId: toolCall.id ?? "",
60
+ });
61
+ resolution = await normalizeCustomClaims(claims, options);
62
+ }
63
+ catch {
64
+ return { kind: "exclusive" };
65
+ }
66
+ if (NEVER_PARALLEL_TOOLS.has(toolCall.name) ||
67
+ toolCall.name === "bash" ||
68
+ resolveToolPolicy(toolCall.name, options) === "sequential") {
69
+ return { kind: "exclusive" };
70
+ }
71
+ return resolution;
72
+ }
73
+ export function compareClaims(left, right) {
74
+ if (left.kind !== right.kind)
75
+ return left.kind < right.kind ? -1 : 1;
76
+ if (left.key !== right.key)
77
+ return left.key < right.key ? -1 : 1;
78
+ if (left.access !== right.access)
79
+ return left.access < right.access ? -1 : 1;
80
+ const leftReal = left.kind === "path" ? (left.realKey ?? "") : "";
81
+ const rightReal = right.kind === "path" ? (right.realKey ?? "") : "";
82
+ if (leftReal !== rightReal)
83
+ return leftReal < rightReal ? -1 : 1;
84
+ const leftInode = left.kind === "path" ? (left.inodeKey ?? "") : "";
85
+ const rightInode = right.kind === "path" ? (right.inodeKey ?? "") : "";
86
+ if (leftInode !== rightInode)
87
+ return leftInode < rightInode ? -1 : 1;
88
+ return 0;
89
+ }
90
+ export function canonicalizeClaims(claims) {
91
+ return claims
92
+ .map((claim) => claim.kind === "path"
93
+ ? {
94
+ kind: "path",
95
+ key: claim.key,
96
+ access: claim.access,
97
+ ...(claim.realKey === undefined ? {} : { realKey: claim.realKey }),
98
+ ...(claim.inodeKey === undefined ? {} : { inodeKey: claim.inodeKey }),
99
+ }
100
+ : { kind: claim.kind, key: claim.key, access: claim.access })
101
+ .sort(compareClaims);
102
+ }
103
+ export function claimsConflict(left, right) {
104
+ if (left.access === "exclusive" || right.access === "exclusive")
105
+ return true;
106
+ if (left.kind !== right.kind)
107
+ return false;
108
+ if (left.kind === "path" && right.kind === "path") {
109
+ if (!pathClaimsOverlap(left, right))
110
+ return false;
111
+ }
112
+ else if (left.key !== right.key) {
113
+ return false;
114
+ }
115
+ return !(left.access === "read" && right.access === "read");
116
+ }
117
+ export function resolutionsConflict(left, right) {
118
+ if (left.kind === "exclusive" || right.kind === "exclusive")
119
+ return true;
120
+ for (const leftClaim of left.claims) {
121
+ for (const rightClaim of right.claims) {
122
+ if (claimsConflict(leftClaim, rightClaim))
123
+ return true;
124
+ }
125
+ }
126
+ return false;
127
+ }
128
+ //# sourceMappingURL=tool-resource-claims.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-resource-claims.js","sourceRoot":"","sources":["../src/tool-resource-claims.ts"],"names":[],"mappings":"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","sourcesContent":["/** Browser-safe resource claims for the opt-in dag-v2 scheduler. */\n\nimport {\n\tfindRegisteredToolClaimDefinition,\n\tisBuiltinPathClaimTool,\n\tisPlainArguments,\n\tpathClaimsOverlap,\n\tresolveBuiltinPathClaimWithIdentity,\n\tresolvePathClaimWithIdentity,\n\tresolveToolClaims,\n\tresolveToolPolicy,\n} from \"./builtin-tool-resource-claims.ts\";\nimport type { ToolParallelPolicy } from \"./parallel-tool-batch.ts\";\nimport { NEVER_PARALLEL_TOOLS } from \"./parallel-tool-batch.ts\";\nimport type { AgentTool, ResourceAccess, ResourceKeyResolver, ToolResourceClaim } from \"./types.ts\";\n\nexport { resolvePathClaimKey, resolveToolClaims } from \"./builtin-tool-resource-claims.ts\";\nexport type {\n\tResourceAccess,\n\tToolResourceAccess,\n\tToolResourceClaim,\n\tToolResourceClaims,\n\tToolResourceClaimsContext,\n} from \"./types.ts\";\n\nexport type ToolClaimResolution = { kind: \"exclusive\" } | { kind: \"claims\"; claims: ToolResourceClaim[] };\n\nexport interface ClaimableToolCall {\n\tid?: string;\n\tname: string;\n\targuments: unknown;\n}\n\nexport type RegisteredToolClaimDefinition = Pick<AgentTool, \"name\" | \"executionMode\" | \"resourceClaims\">;\n\nexport interface ResolveToolClaimsOptions {\n\tcwd: string;\n\ttoolPolicies?: ReadonlyMap<string, ToolParallelPolicy>;\n\tregisteredTools?: readonly RegisteredToolClaimDefinition[];\n\tstrictExtensionClaims?: boolean;\n\tresourceKeyResolver?: ResourceKeyResolver;\n}\n\nfunction isNonPathKind(value: unknown): value is Exclude<ToolResourceClaim[\"kind\"], \"path\"> {\n\treturn value === \"session\" || value === \"terminal\" || value === \"network\" || value === \"global\";\n}\n\nasync function normalizeCustomClaims(value: unknown, options: ResolveToolClaimsOptions): Promise<ToolClaimResolution> {\n\tif (value === \"exclusive\") return { kind: \"exclusive\" };\n\tif (!Array.isArray(value) || value.length === 0) return { kind: \"exclusive\" };\n\n\tconst claims: ToolResourceClaim[] = [];\n\tlet hasExclusiveAccess = false;\n\tfor (const candidate of value) {\n\t\tif (!isPlainArguments(candidate) || typeof candidate.key !== \"string\" || candidate.key.trim().length === 0) {\n\t\t\treturn { kind: \"exclusive\" };\n\t\t}\n\t\tif (candidate.kind === \"path\") {\n\t\t\tif (candidate.access !== \"read\" && candidate.access !== \"write\") return { kind: \"exclusive\" };\n\t\t\tconst pathResolution = await resolvePathClaimWithIdentity(candidate.key, candidate.access, options);\n\t\t\tif (pathResolution.kind === \"exclusive\") return pathResolution;\n\t\t\tclaims.push(...pathResolution.claims);\n\t\t\tcontinue;\n\t\t}\n\t\tif (!isNonPathKind(candidate.kind)) return { kind: \"exclusive\" };\n\t\tif (candidate.access !== \"read\" && candidate.access !== \"write\" && candidate.access !== \"exclusive\") {\n\t\t\treturn { kind: \"exclusive\" };\n\t\t}\n\t\tconst access: ResourceAccess = candidate.access;\n\t\tclaims.push({ kind: candidate.kind, key: candidate.key, access });\n\t\tif (access === \"exclusive\") hasExclusiveAccess = true;\n\t}\n\treturn hasExclusiveAccess ? { kind: \"exclusive\" } : { kind: \"claims\", claims };\n}\n\n/** Resolve one call, failing malformed or rejected extension claims closed. */\nexport async function resolveToolClaimsForCall(\n\ttoolCall: ClaimableToolCall,\n\toptions: ResolveToolClaimsOptions,\n): Promise<ToolClaimResolution> {\n\tconst registeredTool = findRegisteredToolClaimDefinition(toolCall.name, options.registeredTools);\n\tif (!registeredTool?.resourceClaims) {\n\t\tif (\n\t\t\toptions.resourceKeyResolver &&\n\t\t\tisBuiltinPathClaimTool(toolCall.name) &&\n\t\t\tisPlainArguments(toolCall.arguments) &&\n\t\t\t!NEVER_PARALLEL_TOOLS.has(toolCall.name) &&\n\t\t\tresolveToolPolicy(toolCall.name, options) !== \"sequential\"\n\t\t) {\n\t\t\treturn resolveBuiltinPathClaimWithIdentity(toolCall, options);\n\t\t}\n\t\treturn resolveToolClaims(toolCall, options);\n\t}\n\tif (!isPlainArguments(toolCall.arguments)) return { kind: \"exclusive\" };\n\n\tlet resolution: ToolClaimResolution;\n\ttry {\n\t\tconst claims = await registeredTool.resourceClaims(toolCall.arguments, {\n\t\t\tcwd: options.cwd,\n\t\t\ttoolCallId: toolCall.id ?? \"\",\n\t\t});\n\t\tresolution = await normalizeCustomClaims(claims, options);\n\t} catch {\n\t\treturn { kind: \"exclusive\" };\n\t}\n\tif (\n\t\tNEVER_PARALLEL_TOOLS.has(toolCall.name) ||\n\t\ttoolCall.name === \"bash\" ||\n\t\tresolveToolPolicy(toolCall.name, options) === \"sequential\"\n\t) {\n\t\treturn { kind: \"exclusive\" };\n\t}\n\treturn resolution;\n}\n\nexport function compareClaims(left: ToolResourceClaim, right: ToolResourceClaim): number {\n\tif (left.kind !== right.kind) return left.kind < right.kind ? -1 : 1;\n\tif (left.key !== right.key) return left.key < right.key ? -1 : 1;\n\tif (left.access !== right.access) return left.access < right.access ? -1 : 1;\n\tconst leftReal = left.kind === \"path\" ? (left.realKey ?? \"\") : \"\";\n\tconst rightReal = right.kind === \"path\" ? (right.realKey ?? \"\") : \"\";\n\tif (leftReal !== rightReal) return leftReal < rightReal ? -1 : 1;\n\tconst leftInode = left.kind === \"path\" ? (left.inodeKey ?? \"\") : \"\";\n\tconst rightInode = right.kind === \"path\" ? (right.inodeKey ?? \"\") : \"\";\n\tif (leftInode !== rightInode) return leftInode < rightInode ? -1 : 1;\n\treturn 0;\n}\n\nexport function canonicalizeClaims(claims: readonly ToolResourceClaim[]): ToolResourceClaim[] {\n\treturn claims\n\t\t.map(\n\t\t\t(claim): ToolResourceClaim =>\n\t\t\t\tclaim.kind === \"path\"\n\t\t\t\t\t? {\n\t\t\t\t\t\t\tkind: \"path\",\n\t\t\t\t\t\t\tkey: claim.key,\n\t\t\t\t\t\t\taccess: claim.access,\n\t\t\t\t\t\t\t...(claim.realKey === undefined ? {} : { realKey: claim.realKey }),\n\t\t\t\t\t\t\t...(claim.inodeKey === undefined ? {} : { inodeKey: claim.inodeKey }),\n\t\t\t\t\t\t}\n\t\t\t\t\t: { kind: claim.kind, key: claim.key, access: claim.access },\n\t\t)\n\t\t.sort(compareClaims);\n}\n\nexport function claimsConflict(left: ToolResourceClaim, right: ToolResourceClaim): boolean {\n\tif (left.access === \"exclusive\" || right.access === \"exclusive\") return true;\n\tif (left.kind !== right.kind) return false;\n\tif (left.kind === \"path\" && right.kind === \"path\") {\n\t\tif (!pathClaimsOverlap(left, right)) return false;\n\t} else if (left.key !== right.key) {\n\t\treturn false;\n\t}\n\treturn !(left.access === \"read\" && right.access === \"read\");\n}\n\nexport function resolutionsConflict(left: ToolClaimResolution, right: ToolClaimResolution): boolean {\n\tif (left.kind === \"exclusive\" || right.kind === \"exclusive\") return true;\n\tfor (const leftClaim of left.claims) {\n\t\tfor (const rightClaim of right.claims) {\n\t\t\tif (claimsConflict(leftClaim, rightClaim)) return true;\n\t\t}\n\t}\n\treturn false;\n}\n"]}
@@ -0,0 +1,96 @@
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 type { AgentLoopConfig, AgentTool, AgentToolResult, AgentToolUpdateCallback, ToolExecutionPolicy, ToolLateSettlementOutcome, ToolTimeoutDisposition } from "./types.ts";
24
+ export type { ToolDispositionEnvelope } from "./tool-execution-boundary.ts";
25
+ export { createAbortedToolResult, createTimeoutToolResult } from "./tool-execution-boundary.ts";
26
+ /**
27
+ * Resolve the effective {@link ToolExecutionPolicy} with the release default
28
+ * `lateSettlement: "audit"`: late settlements are observable audit events
29
+ * unless a caller explicitly opts out with `"ignore"`.
30
+ */
31
+ export declare function resolveToolExecutionPolicy(policy: Partial<ToolExecutionPolicy> | undefined): ToolExecutionPolicy;
32
+ /** Audit-only description of a real tool promise settling after its terminal cause won. */
33
+ export interface ToolLateSettlement {
34
+ toolCallId: string;
35
+ toolName: string;
36
+ /** The terminal disposition that was already committed when the real promise settled. */
37
+ disposition: ToolTimeoutDisposition;
38
+ /** How the real tool promise eventually settled. Disposition-safe metadata only. */
39
+ outcome: ToolLateSettlementOutcome;
40
+ }
41
+ /**
42
+ * Resolve the effective per-call timeout with strict precedence:
43
+ * per-tool `AgentTool.timeoutMs` > per-name `config.toolTimeouts[name]` >
44
+ * global `config.toolTimeoutMs`.
45
+ *
46
+ * The first level that is *present* (not `undefined`) wins, so a per-tool `0`
47
+ * deliberately disables the timeout even when a global default is set. A
48
+ * resolved value that is absent, non-finite, or non-positive returns `0`, which
49
+ * disables only the timer. Parent cancellation is still raced so an
50
+ * uncooperative tool cannot keep an aborted run open.
51
+ */
52
+ export declare function resolveToolTimeoutMs(tool: Pick<AgentTool<any>, "timeoutMs">, config: Pick<AgentLoopConfig, "toolTimeoutMs" | "toolTimeouts">, toolName: string): number;
53
+ export interface RunToolCallWithTimeoutOptions<TDetails = any> {
54
+ toolCallId: string;
55
+ toolName: string;
56
+ /** Effective timeout in ms. A non-positive value disables the timer, not parent cancellation. */
57
+ timeoutMs: number;
58
+ /** Parent run abort signal, if any. */
59
+ signal: AbortSignal | undefined;
60
+ /** Start the real tool, passing the child (best-effort) signal and update sink. */
61
+ start: (childSignal: AbortSignal, onUpdate: AgentToolUpdateCallback<TDetails>) => Promise<AgentToolResult<TDetails>>;
62
+ /** Emit a `tool_execution_update` for a partial result observed before terminality. */
63
+ emitUpdate: (partialResult: AgentToolResult<TDetails>) => Promise<void> | void;
64
+ /** Emit the audit-only late-settlement event exactly once, after the terminal cause won. */
65
+ emitLateSettlement: (settlement: ToolLateSettlement) => Promise<void> | void;
66
+ /** Map a thrown/rejected tool error to a normal error result (real completion path). */
67
+ toErrorResult: (error: unknown) => AgentToolResult<any>;
68
+ /**
69
+ * Late-settlement policy (default `"audit"`). `"ignore"` drops the audit
70
+ * event; the committed terminal result is immutable under both policies.
71
+ */
72
+ lateSettlement?: ToolExecutionPolicy["lateSettlement"];
73
+ }
74
+ export interface RunToolCallWithTimeoutResult {
75
+ result: AgentToolResult<any>;
76
+ isError: boolean;
77
+ /** True when the tool's `execute` actually began before the result was committed. */
78
+ executionStarted: boolean;
79
+ /** Terminal cause when the runtime (not the tool) committed the result. */
80
+ terminalDisposition?: ToolTimeoutDisposition;
81
+ }
82
+ /**
83
+ * Race a tool's `execute` promise against a per-call timeout and parent abort.
84
+ *
85
+ * Control flow (single path, no `AbortSignal.any()`):
86
+ * - A timer and a parent-abort listener each resolve one shared "terminal cause"
87
+ * deferred. Whichever fires first wins; resolving is idempotent.
88
+ * - The timer callback resolves the timeout cause *before* aborting the child so
89
+ * a tool that rejects promptly on abort cannot make "aborted" win a timeout.
90
+ * - `Promise.race` picks the real settlement or the terminal cause. Timer and
91
+ * listener disposal is idempotent and runs on every outcome.
92
+ * - On a terminal-cause win, the real promise (wrapped so it never rejects) is
93
+ * observed once for the audit event and the committed result is immutable.
94
+ */
95
+ export declare function runToolCallWithTimeout<TDetails = any>(options: RunToolCallWithTimeoutOptions<TDetails>): Promise<RunToolCallWithTimeoutResult>;
96
+ //# sourceMappingURL=tool-timeout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-timeout.d.ts","sourceRoot":"","sources":["../src/tool-timeout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,KAAK,EACX,eAAe,EACf,SAAS,EACT,eAAe,EACf,uBAAuB,EACvB,mBAAmB,EACnB,yBAAyB,EACzB,sBAAsB,EACtB,MAAM,YAAY,CAAC;AAEpB,YAAY,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAC5E,OAAO,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAEhG;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,OAAO,CAAC,mBAAmB,CAAC,GAAG,SAAS,GAAG,mBAAmB,CAMhH;AAED,2FAA2F;AAC3F,MAAM,WAAW,kBAAkB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,yFAAyF;IACzF,WAAW,EAAE,sBAAsB,CAAC;IACpC,oFAAoF;IACpF,OAAO,EAAE,yBAAyB,CAAC;CACnC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CACnC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,WAAW,CAAC,EACvC,MAAM,EAAE,IAAI,CAAC,eAAe,EAAE,eAAe,GAAG,cAAc,CAAC,EAC/D,QAAQ,EAAE,MAAM,GACd,MAAM,CAaR;AAQD,MAAM,WAAW,6BAA6B,CAAC,QAAQ,GAAG,GAAG;IAC5D,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,iGAAiG;IACjG,SAAS,EAAE,MAAM,CAAC;IAClB,uCAAuC;IACvC,MAAM,EAAE,WAAW,GAAG,SAAS,CAAC;IAChC,mFAAmF;IACnF,KAAK,EAAE,CAAC,WAAW,EAAE,WAAW,EAAE,QAAQ,EAAE,uBAAuB,CAAC,QAAQ,CAAC,KAAK,OAAO,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC,CAAC;IACrH,uFAAuF;IACvF,UAAU,EAAE,CAAC,aAAa,EAAE,eAAe,CAAC,QAAQ,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC/E,4FAA4F;IAC5F,kBAAkB,EAAE,CAAC,UAAU,EAAE,kBAAkB,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC7E,wFAAwF;IACxF,aAAa,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,eAAe,CAAC,GAAG,CAAC,CAAC;IACxD;;;OAGG;IACH,cAAc,CAAC,EAAE,mBAAmB,CAAC,gBAAgB,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,4BAA4B;IAC5C,MAAM,EAAE,eAAe,CAAC,GAAG,CAAC,CAAC;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,qFAAqF;IACrF,gBAAgB,EAAE,OAAO,CAAC;IAC1B,2EAA2E;IAC3E,mBAAmB,CAAC,EAAE,sBAAsB,CAAC;CAC7C;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,sBAAsB,CAAC,QAAQ,GAAG,GAAG,EAC1D,OAAO,EAAE,6BAA6B,CAAC,QAAQ,CAAC,GAC9C,OAAO,CAAC,4BAA4B,CAAC,CAsHvC","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,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"]}