@sema-agent/core 5.9.0 → 5.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- package/dist/agents/roster-store.d.ts +1 -0
- package/dist/agents/send-message-tool.js +6 -0
- package/dist/agents/subagent.d.ts +31 -1
- package/dist/agents/subagent.js +67 -21
- package/dist/agents/teacher.js +15 -3
- package/dist/agents/team.js +10 -0
- package/dist/agents/verify.js +7 -0
- package/dist/brain/anthropic.js +27 -10
- package/dist/brain/open-responses.js +19 -4
- package/dist/brain/openai.js +32 -5
- package/dist/core/a2a.js +1 -1
- package/dist/core/background-agent-store.d.ts +2 -1
- package/dist/core/background-agent-store.js +1 -0
- package/dist/core/checkpoint-store.d.ts +2 -0
- package/dist/core/checkpoint-store.js +1 -0
- package/dist/core/memory-recall.js +8 -3
- package/dist/core/memory.d.ts +5 -0
- package/dist/core/memory.js +6 -4
- package/dist/core/runner/assemble-result.js +9 -0
- package/dist/core/runner/prepare-task.d.ts +12 -1
- package/dist/core/runner/prepare-task.js +110 -38
- package/dist/core/runner/runtask.d.ts +12 -0
- package/dist/core/runner/runtask.js +169 -37
- package/dist/core/runner/session-file-state-replay.d.ts +7 -0
- package/dist/core/runner/session-file-state-replay.js +56 -0
- package/dist/core/runner/synthetic-tools.js +1 -1
- package/dist/core/runner/tool-disclosure.d.ts +1 -0
- package/dist/core/runner/tool-disclosure.js +24 -9
- package/dist/core/runner/tool-output-projection.js +5 -4
- package/dist/core/runner/turn-attachments.d.ts +2 -0
- package/dist/core/runner/turn-attachments.js +14 -5
- package/dist/core/session-reconcile.d.ts +7 -3
- package/dist/core/session-reconcile.js +3 -2
- package/dist/core/strategy-store.d.ts +1 -1
- package/dist/core/strategy-store.js +27 -4
- package/dist/core/task-registry-agent.d.ts +3 -0
- package/dist/core/task-registry-agent.js +9 -2
- package/dist/core/task-registry-shared.d.ts +1 -0
- package/dist/core/task-registry.d.ts +2 -0
- package/dist/core/tools.js +9 -1
- package/dist/core/trace.d.ts +1 -0
- package/dist/core/types.d.ts +8 -1
- package/dist/engine/loop/agent-loop.js +168 -22
- package/dist/orchestration/run-workflow-tool.js +1 -1
- package/dist/orchestration/workflow-governance.js +19 -0
- package/dist/orchestration/workflow-primitives.d.ts +1 -1
- package/dist/orchestration/workflow-primitives.js +4 -1
- package/dist/orchestration/workflow.js +1 -1
- package/dist/prompts/coordinator.d.ts +1 -1
- package/dist/prompts/coordinator.js +1 -1
- package/dist/prompts/default.d.ts +1 -0
- package/dist/prompts/default.js +3 -0
- package/dist/stores/file/memory-store.js +3 -7
- package/dist/tools/fs/fs-bash.js +7 -4
- package/dist/tools/fs/fs-shared.d.ts +1 -0
- package/dist/tools/fs/fs-shared.js +4 -0
- package/dist/tools/fs/index.d.ts +1 -0
- package/dist/tools/fs/index.js +7 -4
- package/dist/tools/web.d.ts +21 -1
- package/dist/tools/web.js +126 -11
- package/package.json +2 -2
|
@@ -6,10 +6,14 @@ export interface OrphanToolCall {
|
|
|
6
6
|
toolName: string;
|
|
7
7
|
kind?: "result";
|
|
8
8
|
}
|
|
9
|
+
export type ReconciledErrorKind = "interrupted_never_started" | "interrupted_outcome_unknown";
|
|
10
|
+
export type RecoveredOrphan = OrphanToolCall & {
|
|
11
|
+
entryId: string;
|
|
12
|
+
text: string;
|
|
13
|
+
errorKind: ReconciledErrorKind;
|
|
14
|
+
};
|
|
9
15
|
export interface ReconcileReport {
|
|
10
|
-
recovered:
|
|
11
|
-
entryId: string;
|
|
12
|
-
}>;
|
|
16
|
+
recovered: RecoveredOrphan[];
|
|
13
17
|
}
|
|
14
18
|
export declare function findOrphanToolCalls(messages: AgentMessage[], suspendedBatch?: ReadonlySet<string>): OrphanToolCall[];
|
|
15
19
|
export declare function reconcileInterruptedSession(session: Session, toolEffects?: Map<string, ToolEffect>, suspendedBatch?: ReadonlySet<string>, startedToolCallIds?: ReadonlySet<string>): Promise<ReconcileReport>;
|
|
@@ -75,16 +75,17 @@ export async function reconcileInterruptedSession(session, toolEffects, suspende
|
|
|
75
75
|
const effectText = effect === "read" ? INTERRUPTED_SAFE : effect === "idempotent" ? INTERRUPTED_IDEMPOTENT : INTERRUPTED_UNKNOWN;
|
|
76
76
|
const neverStarted = startedToolCallIds !== undefined && !startedToolCallIds.has(orphan.toolCallId);
|
|
77
77
|
const text = neverStarted ? INTERRUPTED_NEVER_STARTED : effectText;
|
|
78
|
+
const errorKind = neverStarted ? "interrupted_never_started" : "interrupted_outcome_unknown";
|
|
78
79
|
const entryId = await session.appendMessage({
|
|
79
80
|
role: "toolResult",
|
|
80
81
|
toolCallId: orphan.toolCallId,
|
|
81
82
|
toolName: orphan.toolName,
|
|
82
83
|
content: [{ type: "text", text }],
|
|
83
|
-
details: { errorKind
|
|
84
|
+
details: { errorKind },
|
|
84
85
|
isError: true,
|
|
85
86
|
timestamp: Date.now(),
|
|
86
87
|
});
|
|
87
|
-
recovered.push({ ...orphan, entryId });
|
|
88
|
+
recovered.push({ ...orphan, entryId, text, errorKind });
|
|
88
89
|
}
|
|
89
90
|
return { recovered };
|
|
90
91
|
}
|
|
@@ -14,8 +14,8 @@ export interface StrategyStore {
|
|
|
14
14
|
prune?(scope: string, maxSize: number): Promise<void> | void;
|
|
15
15
|
}
|
|
16
16
|
export declare class InMemoryStrategyStore implements StrategyStore {
|
|
17
|
-
private readonly maxPerScope;
|
|
18
17
|
private byScope;
|
|
18
|
+
private readonly maxPerScope;
|
|
19
19
|
constructor(maxPerScope?: number);
|
|
20
20
|
save(s: StoredStrategy): void;
|
|
21
21
|
find(scope: string, query: string, limit: number): StoredStrategy[];
|
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
function invalidKnob(message, code) {
|
|
2
|
+
const e = new Error(message);
|
|
3
|
+
e.code = code;
|
|
4
|
+
return e;
|
|
5
|
+
}
|
|
6
|
+
function resolveFindLimit(limit) {
|
|
7
|
+
if (limit === Number.POSITIVE_INFINITY)
|
|
8
|
+
return limit;
|
|
9
|
+
if (!Number.isInteger(limit) || limit < 0) {
|
|
10
|
+
throw invalidKnob(`StrategyStore.find: limit must be a non-negative integer, or Infinity for "no cap" (got ${String(limit)})`, "config.strategy_find_limit_invalid");
|
|
11
|
+
}
|
|
12
|
+
return limit;
|
|
13
|
+
}
|
|
14
|
+
function resolveCapacityCap(name, cap) {
|
|
15
|
+
if (cap === Number.POSITIVE_INFINITY) {
|
|
16
|
+
throw invalidKnob(`InMemoryStrategyStore ${name} cannot be Infinity — a per-scope capacity cap can be widened but not turned off; pass a large finite integer instead`, "config.strategy_max_size_invalid");
|
|
17
|
+
}
|
|
18
|
+
if (!Number.isInteger(cap) || cap < 0) {
|
|
19
|
+
throw invalidKnob(`InMemoryStrategyStore ${name} must be a non-negative integer (got ${String(cap)})`, "config.strategy_max_size_invalid");
|
|
20
|
+
}
|
|
21
|
+
return cap;
|
|
22
|
+
}
|
|
1
23
|
const STOP = new Set([
|
|
2
24
|
"the", "and", "for", "with", "from", "this", "that", "into", "your", "you", "are", "was", "were", "has",
|
|
3
25
|
"have", "had", "not", "but", "all", "any", "can", "use", "using", "via", "then", "than", "out", "get",
|
|
@@ -24,10 +46,10 @@ function score(s) {
|
|
|
24
46
|
return (s.confidence + 1) * (1 / (1 + ageDays(s.ts)));
|
|
25
47
|
}
|
|
26
48
|
export class InMemoryStrategyStore {
|
|
27
|
-
maxPerScope;
|
|
28
49
|
byScope = new Map();
|
|
50
|
+
maxPerScope;
|
|
29
51
|
constructor(maxPerScope = 100) {
|
|
30
|
-
this.maxPerScope = maxPerScope;
|
|
52
|
+
this.maxPerScope = resolveCapacityCap("maxPerScope", maxPerScope);
|
|
31
53
|
}
|
|
32
54
|
save(s) {
|
|
33
55
|
const arr = this.byScope.get(s.scope) ?? [];
|
|
@@ -49,6 +71,7 @@ export class InMemoryStrategyStore {
|
|
|
49
71
|
this.byScope.set(s.scope, arr);
|
|
50
72
|
}
|
|
51
73
|
find(scope, query, limit) {
|
|
74
|
+
const cap = resolveFindLimit(limit);
|
|
52
75
|
const arr = this.byScope.get(scope);
|
|
53
76
|
if (!arr || arr.length === 0) {
|
|
54
77
|
return [];
|
|
@@ -61,11 +84,11 @@ export class InMemoryStrategyStore {
|
|
|
61
84
|
const probTokens = new Set(tokens(s.problem));
|
|
62
85
|
return qSig.every((t) => probTokens.has(t));
|
|
63
86
|
});
|
|
64
|
-
return matches.sort((a, b) => score(b) - score(a)).slice(0,
|
|
87
|
+
return matches.sort((a, b) => score(b) - score(a)).slice(0, cap);
|
|
65
88
|
}
|
|
66
89
|
prune(scope, maxSize) {
|
|
90
|
+
const n = resolveCapacityCap("maxSize", maxSize);
|
|
67
91
|
const arr = this.byScope.get(scope);
|
|
68
|
-
const n = Math.max(0, maxSize);
|
|
69
92
|
if (arr && arr.length > n) {
|
|
70
93
|
arr.sort((a, b) => score(b) - score(a));
|
|
71
94
|
arr.length = n;
|
|
@@ -69,6 +69,7 @@ export declare function settleBackgroundAgentLane(core: DurableAgentCore, id: st
|
|
|
69
69
|
errorCode?: string;
|
|
70
70
|
retryable?: boolean;
|
|
71
71
|
errorKind?: string;
|
|
72
|
+
retryAfterMs?: number;
|
|
72
73
|
stoppedBy?: StopSource;
|
|
73
74
|
seq?: number;
|
|
74
75
|
cycle?: number;
|
|
@@ -105,6 +106,7 @@ export declare function settleRevivedAgentLane(core: DurableAgentCore, id: strin
|
|
|
105
106
|
errorCode?: string;
|
|
106
107
|
retryable?: boolean;
|
|
107
108
|
errorKind?: string;
|
|
109
|
+
retryAfterMs?: number;
|
|
108
110
|
}): "completed" | "failed" | "killed" | undefined;
|
|
109
111
|
export declare function noteBackgroundAgentActivityLane(core: DurableAgentCore, id: string, now?: number): void;
|
|
110
112
|
export declare function reapStaleSessionBackgroundAgentsLane(core: DurableAgentCore, staleMs: number, now?: number, onTerminal?: (note: () => void) => void): number;
|
|
@@ -137,6 +139,7 @@ export interface AgentPollDetailsInput {
|
|
|
137
139
|
error?: string;
|
|
138
140
|
errorCode?: string;
|
|
139
141
|
errorRetryable?: boolean;
|
|
142
|
+
errorRetryAfterMs?: number;
|
|
140
143
|
resultIsPartial?: boolean;
|
|
141
144
|
completionId?: string;
|
|
142
145
|
}
|
|
@@ -713,6 +713,8 @@ export function settleBackgroundAgentLane(core, id, outcome) {
|
|
|
713
713
|
handle.errorRetryable = outcome.retryable;
|
|
714
714
|
if (outcome.errorKind !== undefined)
|
|
715
715
|
handle.errorKind = outcome.errorKind;
|
|
716
|
+
if (outcome.retryAfterMs !== undefined)
|
|
717
|
+
handle.errorRetryAfterMs = outcome.retryAfterMs;
|
|
716
718
|
}
|
|
717
719
|
handle.updatedAt = Date.now();
|
|
718
720
|
if (outcome.seq !== undefined)
|
|
@@ -731,6 +733,7 @@ export function settleBackgroundAgentLane(core, id, outcome) {
|
|
|
731
733
|
...(handle.errorCode !== undefined ? { errorCode: handle.errorCode } : {}),
|
|
732
734
|
...(handle.errorRetryable !== undefined ? { errorRetryable: handle.errorRetryable } : {}),
|
|
733
735
|
...(handle.errorKind !== undefined ? { errorKind: handle.errorKind } : {}),
|
|
736
|
+
...(handle.errorRetryAfterMs !== undefined ? { errorRetryAfterMs: handle.errorRetryAfterMs } : {}),
|
|
734
737
|
}, ["parkedCheckpointToken", "parkClaimId", "parkedAt"]);
|
|
735
738
|
return outcome.status;
|
|
736
739
|
}
|
|
@@ -857,6 +860,7 @@ export function reviveBackgroundAgentLane(core, id, access, abort) {
|
|
|
857
860
|
handle.errorCode = undefined;
|
|
858
861
|
handle.errorRetryable = undefined;
|
|
859
862
|
handle.errorKind = undefined;
|
|
863
|
+
handle.errorRetryAfterMs = undefined;
|
|
860
864
|
handle.resultIsPartial = undefined;
|
|
861
865
|
handle.stopSource = undefined;
|
|
862
866
|
handle.completionId = undefined;
|
|
@@ -1052,6 +1056,7 @@ export function buildAgentPollDetails(input) {
|
|
|
1052
1056
|
...(failed && input.error !== undefined ? { error: delimitUntrusted("agent error", boundedRedactedSummary(input.error, 300)) } : {}),
|
|
1053
1057
|
...(failed && input.errorCode !== undefined ? { errorCode: input.errorCode } : {}),
|
|
1054
1058
|
...(failed && input.errorRetryable !== undefined ? { retryable: input.errorRetryable } : {}),
|
|
1059
|
+
...(failed && input.errorRetryAfterMs !== undefined ? { retryAfterMs: input.errorRetryAfterMs } : {}),
|
|
1055
1060
|
...(input.resultIsPartial === true ? { partial_result: true } : {}),
|
|
1056
1061
|
...(input.completionId !== undefined ? { completionId: input.completionId } : {}),
|
|
1057
1062
|
};
|
|
@@ -1070,7 +1075,7 @@ The agent is durably suspended, waiting for an approval decision. It resumes whe
|
|
|
1070
1075
|
};
|
|
1071
1076
|
}
|
|
1072
1077
|
const kindClause = row.status === "failed" && row.errorKind !== undefined && row.errorRetryable !== undefined
|
|
1073
|
-
? ` (error_kind: ${row.errorKind}, retryable: ${row.errorRetryable})`
|
|
1078
|
+
? ` (error_kind: ${row.errorKind}, retryable: ${row.errorRetryable}${row.errorRetryAfterMs !== undefined ? `, retry_after_ms: ${row.errorRetryAfterMs}` : ""})`
|
|
1074
1079
|
: "";
|
|
1075
1080
|
const body = `status: ${row.status}
|
|
1076
1081
|
${row.error ? `error: ${row.error}${kindClause}
|
|
@@ -1087,6 +1092,7 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
|
|
|
1087
1092
|
...(row.error !== undefined ? { error: row.error } : {}),
|
|
1088
1093
|
...(row.errorCode !== undefined ? { errorCode: row.errorCode } : {}),
|
|
1089
1094
|
...(row.errorRetryable !== undefined ? { errorRetryable: row.errorRetryable } : {}),
|
|
1095
|
+
...(row.errorRetryAfterMs !== undefined ? { errorRetryAfterMs: row.errorRetryAfterMs } : {}),
|
|
1090
1096
|
...(row.resultIsPartial === true ? { resultIsPartial: true } : {}),
|
|
1091
1097
|
...(row.completionId !== undefined ? { completionId: row.completionId } : {}),
|
|
1092
1098
|
}),
|
|
@@ -1126,7 +1132,7 @@ The agent is durably suspended, waiting for an approval decision. It resumes whe
|
|
|
1126
1132
|
const fullResult = handle.result ? (handle.resultFull ?? handle.result) : undefined;
|
|
1127
1133
|
const resultText = fullResult !== undefined ? await spillClippedAgentResult(handle, fullResult, clipTaskOutput(fullResult, handle.outputFile), store, sessionId) : undefined;
|
|
1128
1134
|
const kindClause = handle.status === "failed" && handle.errorKind !== undefined && handle.errorRetryable !== undefined
|
|
1129
|
-
? ` (error_kind: ${handle.errorKind}, retryable: ${handle.errorRetryable})`
|
|
1135
|
+
? ` (error_kind: ${handle.errorKind}, retryable: ${handle.errorRetryable}${handle.errorRetryAfterMs !== undefined ? `, retry_after_ms: ${handle.errorRetryAfterMs}` : ""})`
|
|
1130
1136
|
: "";
|
|
1131
1137
|
const body = running
|
|
1132
1138
|
? oneShot === true
|
|
@@ -1149,6 +1155,7 @@ ${resultText}` : "(no result text)"}`;
|
|
|
1149
1155
|
...(handle.error !== undefined ? { error: handle.error } : {}),
|
|
1150
1156
|
...(handle.errorCode !== undefined ? { errorCode: handle.errorCode } : {}),
|
|
1151
1157
|
...(handle.errorRetryable !== undefined ? { errorRetryable: handle.errorRetryable } : {}),
|
|
1158
|
+
...(handle.errorRetryAfterMs !== undefined ? { errorRetryAfterMs: handle.errorRetryAfterMs } : {}),
|
|
1152
1159
|
...(handle.resultIsPartial === true ? { resultIsPartial: true } : {}),
|
|
1153
1160
|
...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
|
|
1154
1161
|
}),
|
|
@@ -139,6 +139,7 @@ export interface BackgroundAgentTaskHandle extends SemaTaskHandle {
|
|
|
139
139
|
errorCode?: string;
|
|
140
140
|
errorRetryable?: boolean;
|
|
141
141
|
errorKind?: string;
|
|
142
|
+
errorRetryAfterMs?: number;
|
|
142
143
|
resultIsPartial?: boolean;
|
|
143
144
|
stopSource?: StopSource;
|
|
144
145
|
stoppedBy?: StopSource;
|
|
@@ -141,6 +141,7 @@ export declare class TaskRegistry {
|
|
|
141
141
|
errorCode?: string;
|
|
142
142
|
retryable?: boolean;
|
|
143
143
|
errorKind?: string;
|
|
144
|
+
retryAfterMs?: number;
|
|
144
145
|
stoppedBy?: StopSource;
|
|
145
146
|
seq?: number;
|
|
146
147
|
cycle?: number;
|
|
@@ -180,6 +181,7 @@ export declare class TaskRegistry {
|
|
|
180
181
|
errorCode?: string;
|
|
181
182
|
retryable?: boolean;
|
|
182
183
|
errorKind?: string;
|
|
184
|
+
retryAfterMs?: number;
|
|
183
185
|
}): "completed" | "failed" | "killed" | undefined;
|
|
184
186
|
unmarkRetainedContinuation(id: string): void;
|
|
185
187
|
attachAgentNotify(id: string, notify: NonNullable<BackgroundAgentTaskHandle["notify"]>, cycle?: number): void;
|
package/dist/core/tools.js
CHANGED
|
@@ -64,7 +64,15 @@ export function defineTool(spec, options) {
|
|
|
64
64
|
ret = await spec.execute(params, options?.enrichCtx ? { ...options.enrichCtx(baseCtx), toolCallId, signal } : baseCtx);
|
|
65
65
|
}
|
|
66
66
|
catch (err) {
|
|
67
|
-
|
|
67
|
+
const wrapped = new Error(formatToolError(err));
|
|
68
|
+
if (err !== null && typeof err === "object") {
|
|
69
|
+
const src = err;
|
|
70
|
+
if (src.details !== undefined)
|
|
71
|
+
wrapped.details = src.details;
|
|
72
|
+
if (typeof src.errorKind === "string")
|
|
73
|
+
wrapped.errorKind = src.errorKind;
|
|
74
|
+
}
|
|
75
|
+
throw wrapped;
|
|
68
76
|
}
|
|
69
77
|
const { content, details, terminate, isError } = normalizeContent(ret);
|
|
70
78
|
const guarded = isEmptyToolContent(content)
|
package/dist/core/trace.d.ts
CHANGED
package/dist/core/types.d.ts
CHANGED
|
@@ -99,6 +99,9 @@ export interface ToolExecuteContext {
|
|
|
99
99
|
thinkingLevel?: ThinkingLevel;
|
|
100
100
|
principal?: string;
|
|
101
101
|
onAsk?: import("./tool-policy.js").OnAsk;
|
|
102
|
+
onQuestion?: import("./ask-question.js").OnQuestion;
|
|
103
|
+
handsReadOnly?: true;
|
|
104
|
+
interactiveTools?: false;
|
|
102
105
|
oneShot?: boolean;
|
|
103
106
|
clientContext?: TaskSpec["clientContext"];
|
|
104
107
|
excludeTools?: readonly string[];
|
|
@@ -106,6 +109,7 @@ export interface ToolExecuteContext {
|
|
|
106
109
|
alwaysLoadTools?: readonly string[];
|
|
107
110
|
promptProfile?: "simple" | "classic";
|
|
108
111
|
additionalDirectories?: readonly string[];
|
|
112
|
+
additionalReadDirectories?: readonly string[];
|
|
109
113
|
envFacts?: TaskSpec["envFacts"];
|
|
110
114
|
getApiKeyAndHeaders?: TaskSpec["getApiKeyAndHeaders"];
|
|
111
115
|
activeSkillScope?: () => readonly unknown[];
|
|
@@ -117,6 +121,7 @@ export interface ToolExecuteContext {
|
|
|
117
121
|
scope: string;
|
|
118
122
|
ttlMs?: number;
|
|
119
123
|
};
|
|
124
|
+
checkpointStoreDisabledForChildren?: true;
|
|
120
125
|
taskId?: string;
|
|
121
126
|
sessionId?: string;
|
|
122
127
|
backgroundScope?: "task" | "session";
|
|
@@ -293,6 +298,7 @@ export interface TaskSpec {
|
|
|
293
298
|
tools?: ToolSpec[];
|
|
294
299
|
excludeTools?: string[];
|
|
295
300
|
deferTools?: string[];
|
|
301
|
+
toolMaterializeStrategy?: "swap" | "static";
|
|
296
302
|
alwaysLoadTools?: string[];
|
|
297
303
|
deferSelfResolve?: boolean;
|
|
298
304
|
promptProfile?: "simple" | "classic";
|
|
@@ -318,9 +324,10 @@ export interface TaskSpec {
|
|
|
318
324
|
resilience?: ResilienceOptions;
|
|
319
325
|
finalVerification?: boolean;
|
|
320
326
|
maxSuspends?: number;
|
|
321
|
-
checkpointStore?: import("./checkpoint-store.js").CheckpointStore;
|
|
327
|
+
checkpointStore?: import("./checkpoint-store.js").CheckpointStore | null;
|
|
322
328
|
handsReadOnly?: boolean;
|
|
323
329
|
additionalDirectories?: string[];
|
|
330
|
+
additionalReadDirectories?: string[];
|
|
324
331
|
enablePlanMode?: boolean;
|
|
325
332
|
interactiveTools?: boolean;
|
|
326
333
|
enableFork?: boolean;
|
|
@@ -208,6 +208,7 @@ async function runSingleTurn(state, signal, emit, streamFn, runtime, trace) {
|
|
|
208
208
|
finally {
|
|
209
209
|
await executor.settle();
|
|
210
210
|
}
|
|
211
|
+
raiseFatalCancellation(executor);
|
|
211
212
|
{
|
|
212
213
|
if (ptl) {
|
|
213
214
|
const detect = ptlDetect;
|
|
@@ -245,6 +246,7 @@ async function runSingleTurn(state, signal, emit, streamFn, runtime, trace) {
|
|
|
245
246
|
finally {
|
|
246
247
|
await executor.settle();
|
|
247
248
|
}
|
|
249
|
+
raiseFatalCancellation(executor);
|
|
248
250
|
}
|
|
249
251
|
}
|
|
250
252
|
}
|
|
@@ -495,8 +497,7 @@ async function executeToolCallsSequential(currentContext, assistantMessage, tool
|
|
|
495
497
|
};
|
|
496
498
|
}
|
|
497
499
|
else {
|
|
498
|
-
|
|
499
|
-
finalized = await finalizeExecutedToolCall(currentContext, assistantMessage, preparation, executed, config, signal);
|
|
500
|
+
finalized = await executePreparedWithDisclosure(currentContext, assistantMessage, preparation, config, signal, emit);
|
|
500
501
|
}
|
|
501
502
|
await emitToolExecutionEnd(finalized, emit);
|
|
502
503
|
const toolResultMessage = createToolResultMessage(finalized);
|
|
@@ -559,15 +560,25 @@ function resolveToolConcurrency(configured) {
|
|
|
559
560
|
}
|
|
560
561
|
async function runCapped(thunks, limit) {
|
|
561
562
|
const results = new Array(thunks.length);
|
|
563
|
+
const failures = new Array(thunks.length);
|
|
562
564
|
let next = 0;
|
|
563
565
|
const width = Number.isFinite(limit) ? Math.max(1, Math.min(limit, thunks.length)) : 1;
|
|
564
566
|
const workers = Array.from({ length: width }, async () => {
|
|
565
567
|
while (next < thunks.length) {
|
|
566
568
|
const i = next++;
|
|
567
|
-
|
|
569
|
+
try {
|
|
570
|
+
results[i] = await thunks[i]();
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
failures[i] = { error };
|
|
574
|
+
}
|
|
568
575
|
}
|
|
569
576
|
});
|
|
570
577
|
await Promise.all(workers);
|
|
578
|
+
const firstFailure = failures.find((failure) => failure !== undefined);
|
|
579
|
+
if (firstFailure !== undefined) {
|
|
580
|
+
throw firstFailure.error;
|
|
581
|
+
}
|
|
571
582
|
return results;
|
|
572
583
|
}
|
|
573
584
|
async function executeToolCallsPartitioned(currentContext, assistantMessage, toolCalls, config, signal, emit, executor) {
|
|
@@ -625,8 +636,11 @@ async function executeToolCallsPartitioned(currentContext, assistantMessage, too
|
|
|
625
636
|
return { messages, terminate: shouldTerminateToolBatch(allFinalized) };
|
|
626
637
|
async function settleBatch(entries, concurrent) {
|
|
627
638
|
const runOne = (entry) => async () => {
|
|
628
|
-
const
|
|
629
|
-
|
|
639
|
+
const prepared = entry.prepared;
|
|
640
|
+
if (!prepared) {
|
|
641
|
+
throw new Error(`tool call ${entry.toolCall.id} reached execution without a preparation`);
|
|
642
|
+
}
|
|
643
|
+
const finalized = await executePreparedWithDisclosure(currentContext, assistantMessage, prepared, config, signal, emit);
|
|
630
644
|
await emitToolExecutionEnd(finalized, emit);
|
|
631
645
|
return finalized;
|
|
632
646
|
};
|
|
@@ -662,6 +676,7 @@ class StreamToolExecutor {
|
|
|
662
676
|
admittedOrder = [];
|
|
663
677
|
barrier = false;
|
|
664
678
|
inFlight = 0;
|
|
679
|
+
fatalCancellation;
|
|
665
680
|
constructor(context, config, signal, emit) {
|
|
666
681
|
this.context = context;
|
|
667
682
|
this.config = config;
|
|
@@ -707,9 +722,11 @@ class StreamToolExecutor {
|
|
|
707
722
|
return;
|
|
708
723
|
}
|
|
709
724
|
entry.prepared = preparation;
|
|
710
|
-
entry.executed = await executePreparedToolCall(preparation, this.signal, this.emit, this.config.abortResultDetails);
|
|
725
|
+
entry.executed = await executePreparedToolCall(preparation, this.signal, this.emit, this.config.abortResultDetails, (error) => this.retainCancellation(error));
|
|
711
726
|
}
|
|
712
727
|
catch (error) {
|
|
728
|
+
if (isAbortSemanticsError(error))
|
|
729
|
+
this.retainCancellation(error);
|
|
713
730
|
entry.immediate = {
|
|
714
731
|
kind: "immediate",
|
|
715
732
|
result: createErrorToolResult(error instanceof Error ? error.message : String(error), error),
|
|
@@ -724,6 +741,15 @@ class StreamToolExecutor {
|
|
|
724
741
|
this.barrier = true;
|
|
725
742
|
await Promise.all([...this.entries.values()].map((e) => e.promise));
|
|
726
743
|
}
|
|
744
|
+
retainCancellation(error) {
|
|
745
|
+
this.fatalCancellation ??= error;
|
|
746
|
+
this.barrier = true;
|
|
747
|
+
}
|
|
748
|
+
takeFatalCancellation() {
|
|
749
|
+
const cancellation = this.fatalCancellation;
|
|
750
|
+
this.fatalCancellation = undefined;
|
|
751
|
+
return cancellation;
|
|
752
|
+
}
|
|
727
753
|
take(id) {
|
|
728
754
|
const entry = this.entries.get(id);
|
|
729
755
|
if (entry)
|
|
@@ -829,7 +855,7 @@ async function prepareToolCall(currentContext, assistantMessage, toolCall, confi
|
|
|
829
855
|
if (signal?.aborted) {
|
|
830
856
|
return {
|
|
831
857
|
kind: "immediate",
|
|
832
|
-
result: createErrorToolResult("Operation aborted", { details: config.abortResultDetails
|
|
858
|
+
result: createErrorToolResult("Operation aborted", { details: safeAbortDetails(config.abortResultDetails) }),
|
|
833
859
|
isError: true,
|
|
834
860
|
};
|
|
835
861
|
}
|
|
@@ -869,44 +895,134 @@ async function prepareToolCall(currentContext, assistantMessage, toolCall, confi
|
|
|
869
895
|
};
|
|
870
896
|
}
|
|
871
897
|
}
|
|
872
|
-
async function executePreparedToolCall(prepared, signal, emit, abortResultDetails) {
|
|
898
|
+
async function executePreparedToolCall(prepared, signal, emit, abortResultDetails, onLateCancellation) {
|
|
873
899
|
const updateEvents = [];
|
|
874
900
|
let acceptingUpdates = true;
|
|
875
901
|
if (signal?.aborted) {
|
|
876
|
-
return { result: createErrorToolResult("operation aborted before execution", { details: abortResultDetails
|
|
902
|
+
return { result: createErrorToolResult("operation aborted before execution", { details: safeAbortDetails(abortResultDetails) }), isError: true };
|
|
877
903
|
}
|
|
878
904
|
const work = (async () => {
|
|
905
|
+
let outcome;
|
|
879
906
|
try {
|
|
880
907
|
const result = await prepared.tool.execute(prepared.toolCall.id, prepared.args, signal, (partialResult) => {
|
|
881
908
|
if (!acceptingUpdates) {
|
|
882
909
|
return;
|
|
883
910
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
911
|
+
let delivery;
|
|
912
|
+
try {
|
|
913
|
+
delivery = Promise.resolve(emit({
|
|
914
|
+
type: "tool_execution_update",
|
|
915
|
+
toolCallId: prepared.toolCall.id,
|
|
916
|
+
toolName: prepared.toolCall.name,
|
|
917
|
+
args: prepared.toolCall.arguments,
|
|
918
|
+
partialResult,
|
|
919
|
+
}));
|
|
920
|
+
}
|
|
921
|
+
catch (error) {
|
|
922
|
+
delivery = Promise.reject(error);
|
|
923
|
+
}
|
|
924
|
+
void delivery.catch((reason) => {
|
|
925
|
+
if (isAbortSemanticsError(reason))
|
|
926
|
+
onLateCancellation?.(reason);
|
|
927
|
+
});
|
|
928
|
+
updateEvents.push(delivery);
|
|
891
929
|
});
|
|
892
|
-
|
|
893
|
-
await Promise.all(updateEvents);
|
|
894
|
-
return { result, isError: result.isError === true };
|
|
930
|
+
outcome = { result, isError: result.isError === true };
|
|
895
931
|
}
|
|
896
932
|
catch (error) {
|
|
897
|
-
|
|
898
|
-
await Promise.all(updateEvents);
|
|
899
|
-
return {
|
|
933
|
+
outcome = {
|
|
900
934
|
result: createErrorToolResult(error instanceof Error ? error.message : String(error), error),
|
|
901
935
|
isError: true,
|
|
902
936
|
};
|
|
903
937
|
}
|
|
938
|
+
acceptingUpdates = false;
|
|
939
|
+
const deliveryFailure = await settleUpdateDeliveries(updateEvents);
|
|
940
|
+
if (!deliveryFailure)
|
|
941
|
+
return outcome;
|
|
942
|
+
const reason = deliveryFailure.reason;
|
|
943
|
+
const details = annotateDeliveryFailure(outcome.result, {
|
|
944
|
+
message: reason instanceof Error ? reason.message : String(reason),
|
|
945
|
+
});
|
|
946
|
+
return details === UNANNOTATED ? outcome : { result: { ...outcome.result, details }, isError: outcome.isError };
|
|
904
947
|
})();
|
|
905
948
|
return work;
|
|
906
949
|
}
|
|
950
|
+
const TOOL_PROGRESS_DELIVERY_FAILED = "toolProgressDeliveryFailed";
|
|
951
|
+
const UNANNOTATED = Symbol("unannotated");
|
|
952
|
+
function annotateDeliveryFailure(result, mark) {
|
|
953
|
+
try {
|
|
954
|
+
const raw = result.details;
|
|
955
|
+
const prior = raw === undefined ? {} : raw;
|
|
956
|
+
const proto = prior === null || typeof prior !== "object" ? undefined : Object.getPrototypeOf(prior);
|
|
957
|
+
if (proto !== Object.prototype && proto !== null)
|
|
958
|
+
return UNANNOTATED;
|
|
959
|
+
const details = { ...prior };
|
|
960
|
+
if (TOOL_PROGRESS_DELIVERY_FAILED in details)
|
|
961
|
+
return UNANNOTATED;
|
|
962
|
+
details[TOOL_PROGRESS_DELIVERY_FAILED] = mark;
|
|
963
|
+
return details;
|
|
964
|
+
}
|
|
965
|
+
catch {
|
|
966
|
+
return UNANNOTATED;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
function readDeliveryFailureMark(details) {
|
|
970
|
+
try {
|
|
971
|
+
if (details === null || typeof details !== "object")
|
|
972
|
+
return undefined;
|
|
973
|
+
const mark = details[TOOL_PROGRESS_DELIVERY_FAILED];
|
|
974
|
+
return mark;
|
|
975
|
+
}
|
|
976
|
+
catch {
|
|
977
|
+
return undefined;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
async function settleUpdateDeliveries(updateEvents) {
|
|
981
|
+
if (updateEvents.length === 0)
|
|
982
|
+
return undefined;
|
|
983
|
+
let reportFailure;
|
|
984
|
+
const firstFailure = new Promise((resolve) => {
|
|
985
|
+
reportFailure = resolve;
|
|
986
|
+
});
|
|
987
|
+
for (const delivery of updateEvents) {
|
|
988
|
+
void delivery.catch((reason) => reportFailure({ reason }));
|
|
989
|
+
}
|
|
990
|
+
const allSettled = Promise.allSettled(updateEvents).then((entries) => {
|
|
991
|
+
for (const entry of entries) {
|
|
992
|
+
if (entry.status === "rejected")
|
|
993
|
+
return { reason: entry.reason };
|
|
994
|
+
}
|
|
995
|
+
return undefined;
|
|
996
|
+
});
|
|
997
|
+
const failure = await Promise.race([firstFailure, allSettled]);
|
|
998
|
+
if (failure && isAbortSemanticsError(failure.reason))
|
|
999
|
+
throw failure.reason;
|
|
1000
|
+
return failure;
|
|
1001
|
+
}
|
|
1002
|
+
function raiseFatalCancellation(executor) {
|
|
1003
|
+
const cancellation = executor.takeFatalCancellation();
|
|
1004
|
+
if (cancellation !== undefined)
|
|
1005
|
+
throw cancellation;
|
|
1006
|
+
}
|
|
1007
|
+
async function executePreparedWithDisclosure(currentContext, assistantMessage, prepared, config, signal, emit) {
|
|
1008
|
+
let executed;
|
|
1009
|
+
try {
|
|
1010
|
+
executed = await executePreparedToolCall(prepared, signal, emit, config.abortResultDetails);
|
|
1011
|
+
}
|
|
1012
|
+
catch (error) {
|
|
1013
|
+
if (isAbortSemanticsError(error))
|
|
1014
|
+
throw error;
|
|
1015
|
+
executed = {
|
|
1016
|
+
result: createErrorToolResult(`[tool execution harness failed: ${error instanceof Error ? error.message : String(error)}]`, error),
|
|
1017
|
+
isError: true,
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
return finalizeExecutedToolCall(currentContext, assistantMessage, prepared, executed, config, signal);
|
|
1021
|
+
}
|
|
907
1022
|
async function finalizeExecutedToolCall(currentContext, assistantMessage, prepared, executed, config, signal) {
|
|
908
1023
|
let result = executed.result;
|
|
909
1024
|
let isError = executed.isError;
|
|
1025
|
+
const deliveryMark = readDeliveryFailureMark(executed.result.details);
|
|
910
1026
|
if (config.afterToolCall) {
|
|
911
1027
|
try {
|
|
912
1028
|
const afterResult = await config.afterToolCall({
|
|
@@ -930,6 +1046,11 @@ async function finalizeExecutedToolCall(currentContext, assistantMessage, prepar
|
|
|
930
1046
|
const note = `[post-tool processing failed (the tool already executed): ${error instanceof Error ? error.message : String(error)}]`;
|
|
931
1047
|
result = { ...result, content: [...result.content, { type: "text", text: note }] };
|
|
932
1048
|
}
|
|
1049
|
+
if (deliveryMark !== undefined && readDeliveryFailureMark(result.details) === undefined) {
|
|
1050
|
+
const details = annotateDeliveryFailure(result, deliveryMark);
|
|
1051
|
+
if (details !== UNANNOTATED)
|
|
1052
|
+
result = { ...result, details };
|
|
1053
|
+
}
|
|
933
1054
|
}
|
|
934
1055
|
return {
|
|
935
1056
|
toolCall: prepared.toolCall,
|
|
@@ -937,6 +1058,31 @@ async function finalizeExecutedToolCall(currentContext, assistantMessage, prepar
|
|
|
937
1058
|
isError,
|
|
938
1059
|
};
|
|
939
1060
|
}
|
|
1061
|
+
function safeAbortDetails(fn) {
|
|
1062
|
+
try {
|
|
1063
|
+
return fn?.();
|
|
1064
|
+
}
|
|
1065
|
+
catch {
|
|
1066
|
+
return undefined;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
function isAbortSemanticsError(error) {
|
|
1070
|
+
try {
|
|
1071
|
+
let cursor = error;
|
|
1072
|
+
for (let depth = 0; depth < 8 && cursor instanceof Error; depth++) {
|
|
1073
|
+
if (cursor.name === "AbortError")
|
|
1074
|
+
return true;
|
|
1075
|
+
const next = cursor.cause;
|
|
1076
|
+
if (next === cursor)
|
|
1077
|
+
break;
|
|
1078
|
+
cursor = next;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
catch {
|
|
1082
|
+
return false;
|
|
1083
|
+
}
|
|
1084
|
+
return false;
|
|
1085
|
+
}
|
|
940
1086
|
function createErrorToolResult(message, source) {
|
|
941
1087
|
let details = {};
|
|
942
1088
|
if (source !== null && typeof source === "object") {
|
|
@@ -380,7 +380,7 @@ export async function createRunWorkflowTool(d) {
|
|
|
380
380
|
return structuredError(`workflow script failed to compile: ${err instanceof Error ? err.message : String(err)}`);
|
|
381
381
|
}
|
|
382
382
|
const scriptFn = (wfCtx) => {
|
|
383
|
-
const primitives = buildWorkflowPrimitives(wfCtx, governance, d.onAgentSpawn, d.parentThinking, principal);
|
|
383
|
+
const primitives = buildWorkflowPrimitives(wfCtx, governance, d.onAgentSpawn, d.parentThinking, principal, ctx.checkpointStoreDisabledForChildren === true);
|
|
384
384
|
return d.scriptRunner.run({ scriptSource: script, primitives, scriptArgs: effectiveArgs, signal: wfCtx.signal }).then((r) => r.result);
|
|
385
385
|
};
|
|
386
386
|
if (ctx.signal?.aborted) {
|
|
@@ -118,6 +118,25 @@ function pickWhitelist(scriptSpec) {
|
|
|
118
118
|
return { safe, modelName };
|
|
119
119
|
}
|
|
120
120
|
function clampResourceLimits(safe, base, caps) {
|
|
121
|
+
const trustedCandidates = [
|
|
122
|
+
["baseline.limits.maxCostUsd", base.limits?.maxCostUsd],
|
|
123
|
+
["baseline.limits.maxTokens", base.limits?.maxTokens],
|
|
124
|
+
["baseline.limits.maxWalltimeMs", base.limits?.maxWalltimeMs],
|
|
125
|
+
["baseline.limits.maxTurns", base.limits?.maxTurns],
|
|
126
|
+
["caps.childMaxCostUsd", caps?.childMaxCostUsd],
|
|
127
|
+
["caps.childMaxTokens", caps?.childMaxTokens],
|
|
128
|
+
["caps.perAgentMaxWalltimeMs", caps?.perAgentMaxWalltimeMs],
|
|
129
|
+
["caps.childMaxTurns", caps?.childMaxTurns],
|
|
130
|
+
];
|
|
131
|
+
for (const [label, value] of trustedCandidates) {
|
|
132
|
+
if (value === undefined)
|
|
133
|
+
continue;
|
|
134
|
+
if (typeof value !== "number" || Number.isNaN(value) || value === Infinity || value === -Infinity) {
|
|
135
|
+
const e = new Error(`workflow governance: ${label} must be a real number (got ${String(value)}) — an unevaluable ceiling is not a ceiling, and dropping it would let a child run this axis unbounded under a limit nobody chose. Use 0 or a negative number to declare "no ceiling from this source" explicitly.`);
|
|
136
|
+
e.code = "config.limit_invalid";
|
|
137
|
+
throw e;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
121
140
|
const notes = [];
|
|
122
141
|
const requestedCost = safe.limits?.maxCostUsd;
|
|
123
142
|
const requestedTokens = safe.limits?.maxTokens;
|
|
@@ -8,4 +8,4 @@ export interface WorkflowGovernance {
|
|
|
8
8
|
models?: Record<string, Model>;
|
|
9
9
|
caps?: WorkflowChildCaps;
|
|
10
10
|
}
|
|
11
|
-
export declare function buildWorkflowPrimitives(ctx: WorkflowRunContext, governance?: WorkflowGovernance, onAgentSpawn?: (handle: WorkflowAgentHandle) => void, parentThinking?: () => TaskSpec["thinking"], parentPrincipal?: string): WorkflowPrimitives;
|
|
11
|
+
export declare function buildWorkflowPrimitives(ctx: WorkflowRunContext, governance?: WorkflowGovernance, onAgentSpawn?: (handle: WorkflowAgentHandle) => void, parentThinking?: () => TaskSpec["thinking"], parentPrincipal?: string, parentCheckpointStoreDisabled?: boolean): WorkflowPrimitives;
|
|
@@ -22,7 +22,7 @@ function formatResourceClampNote(notes) {
|
|
|
22
22
|
const parts = notes.map((n) => `${n.field}: requested ${n.requested === undefined ? "unset" : n.requested} → applied ${n.applied}`);
|
|
23
23
|
return `workflow governance tightened this agent's resource limits (${parts.join("; ")})`;
|
|
24
24
|
}
|
|
25
|
-
export function buildWorkflowPrimitives(ctx, governance, onAgentSpawn, parentThinking, parentPrincipal) {
|
|
25
|
+
export function buildWorkflowPrimitives(ctx, governance, onAgentSpawn, parentThinking, parentPrincipal, parentCheckpointStoreDisabled) {
|
|
26
26
|
const agent = (spec, opts) => {
|
|
27
27
|
if (typeof spec === "string")
|
|
28
28
|
spec = { objective: spec };
|
|
@@ -39,6 +39,9 @@ export function buildWorkflowPrimitives(ctx, governance, onAgentSpawn, parentThi
|
|
|
39
39
|
if (childSpec.principal === undefined && parentPrincipal !== undefined) {
|
|
40
40
|
childSpec.principal = parentPrincipal;
|
|
41
41
|
}
|
|
42
|
+
if (parentCheckpointStoreDisabled === true) {
|
|
43
|
+
childSpec.checkpointStore = null;
|
|
44
|
+
}
|
|
42
45
|
if (onAgentSpawn) {
|
|
43
46
|
return ctx.agentStream(childSpec, agentOpts).then((handle) => {
|
|
44
47
|
onAgentSpawn(handle);
|