omk-agent-core 0.90.8 → 0.91.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -1
- package/dist/agent-loop.d.ts +25 -2
- package/dist/agent-loop.d.ts.map +1 -1
- package/dist/agent-loop.js +492 -187
- package/dist/agent-loop.js.map +1 -1
- package/dist/agent.d.ts +29 -7
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +81 -46
- package/dist/agent.js.map +1 -1
- package/dist/builtin-tool-resource-claims.d.ts +19 -0
- package/dist/builtin-tool-resource-claims.d.ts.map +1 -0
- package/dist/builtin-tool-resource-claims.js +200 -0
- package/dist/builtin-tool-resource-claims.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/node-resource-resolver.d.ts +42 -0
- package/dist/node-resource-resolver.d.ts.map +1 -0
- package/dist/node-resource-resolver.js +149 -0
- package/dist/node-resource-resolver.js.map +1 -0
- package/dist/node.d.ts +1 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +2 -0
- package/dist/node.js.map +1 -1
- package/dist/path-segments.d.ts +8 -0
- package/dist/path-segments.d.ts.map +1 -1
- package/dist/path-segments.js +62 -9
- package/dist/path-segments.js.map +1 -1
- package/dist/plain-data.d.ts +7 -0
- package/dist/plain-data.d.ts.map +1 -0
- package/dist/plain-data.js +70 -0
- package/dist/plain-data.js.map +1 -0
- package/dist/tool-dag-scheduler.d.ts +86 -0
- package/dist/tool-dag-scheduler.d.ts.map +1 -0
- package/dist/tool-dag-scheduler.js +171 -0
- package/dist/tool-dag-scheduler.js.map +1 -0
- package/dist/tool-execution-boundary.d.ts +52 -0
- package/dist/tool-execution-boundary.d.ts.map +1 -0
- package/dist/tool-execution-boundary.js +185 -0
- package/dist/tool-execution-boundary.js.map +1 -0
- package/dist/tool-resource-claims.d.ts +31 -0
- package/dist/tool-resource-claims.d.ts.map +1 -0
- package/dist/tool-resource-claims.js +128 -0
- package/dist/tool-resource-claims.js.map +1 -0
- package/dist/tool-timeout.d.ts +96 -0
- package/dist/tool-timeout.d.ts.map +1 -0
- package/dist/tool-timeout.js +173 -0
- package/dist/tool-timeout.js.map +1 -0
- package/dist/tool-transcript-integrity.d.ts +65 -0
- package/dist/tool-transcript-integrity.d.ts.map +1 -0
- package/dist/tool-transcript-integrity.js +223 -0
- package/dist/tool-transcript-integrity.js.map +1 -0
- package/dist/types.d.ts +219 -10
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +50 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
package/dist/agent-loop.js
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
* Transforms to Message[] only at the LLM call boundary.
|
|
4
4
|
*/
|
|
5
5
|
import { EventStream, streamSimple, validateToolArguments, } from "omk-ai";
|
|
6
|
+
import { bindToolIdentity, isPlainArguments } from "./builtin-tool-resource-claims.js";
|
|
6
7
|
import { partitionToolBatchWaves } from "./parallel-tool-batch.js";
|
|
8
|
+
import { scheduleDagLevels } from "./tool-dag-scheduler.js";
|
|
9
|
+
import { awaitWithAbort, createErrorToolResult, createImmutableJsonSnapshot, createImmutableSnapshot, finalizeExecutedToolCall, parseJsonValue, stampToolResultEnvelope, } from "./tool-execution-boundary.js";
|
|
10
|
+
import { resolveToolTimeoutMs, runToolCallWithTimeout } from "./tool-timeout.js";
|
|
11
|
+
import { createSyntheticToolResult, inspectTranscriptIntegrity, repairTranscriptIntegrity, } from "./tool-transcript-integrity.js";
|
|
12
|
+
import { createToolResultEnvelope, } from "./types.js";
|
|
7
13
|
const EMPTY_USAGE = {
|
|
8
14
|
input: 0,
|
|
9
15
|
output: 0,
|
|
@@ -13,35 +19,76 @@ const EMPTY_USAGE = {
|
|
|
13
19
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
14
20
|
};
|
|
15
21
|
/**
|
|
16
|
-
*
|
|
22
|
+
* Decide how to terminate a run after the underlying loop rejected.
|
|
17
23
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
24
|
+
* A synthetic assistant failure may only be appended on top of a transcript
|
|
25
|
+
* whose tool turns are all closed. When `completedMessages` ends with an open
|
|
26
|
+
* tool turn, a safe missing-only closure (synthetic results for the unambiguous
|
|
27
|
+
* missing tail calls) is appended first so the failure assistant never creates
|
|
28
|
+
* an `assistant(tool calls) -> assistant(failure)` interleaving.
|
|
22
29
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
30
|
+
* If the transcript is ambiguous (duplicate/orphan/interleave, or a
|
|
31
|
+
* mid-transcript gap) it is never auto-repaired: the plan returns no failure
|
|
32
|
+
* message so the caller ends the stream without fabricating a turn over
|
|
33
|
+
* corruption. Pure apart from `Date.now()` on the failure message.
|
|
26
34
|
*/
|
|
27
|
-
function
|
|
28
|
-
const
|
|
35
|
+
export function planFailureTermination(completedMessages, model, error, aborted) {
|
|
36
|
+
const messages = [...completedMessages];
|
|
37
|
+
const closureResults = [];
|
|
38
|
+
if (!inspectTranscriptIntegrity(messages).ok) {
|
|
39
|
+
try {
|
|
40
|
+
const repaired = repairTranscriptIntegrity(messages, "Tool result missing; run terminated by error");
|
|
41
|
+
// repairTranscriptIntegrity appends synthetic results only for
|
|
42
|
+
// unambiguous missing tail calls; anything ambiguous throws above.
|
|
43
|
+
for (let i = messages.length; i < repaired.length; i++) {
|
|
44
|
+
const result = createImmutableSnapshot(repaired[i]);
|
|
45
|
+
closureResults.push(result);
|
|
46
|
+
messages.push(result);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Ambiguous transcript: never auto-repair. Fail closed without a
|
|
51
|
+
// synthetic assistant turn over a corrupt transcript.
|
|
52
|
+
return { messages, failureMessage: undefined, closureResults: [] };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const failureMessage = createImmutableSnapshot({
|
|
29
56
|
role: "assistant",
|
|
30
57
|
content: [{ type: "text", text: "" }],
|
|
31
|
-
api:
|
|
32
|
-
provider:
|
|
33
|
-
model:
|
|
58
|
+
api: model.api,
|
|
59
|
+
provider: model.provider,
|
|
60
|
+
model: model.id,
|
|
34
61
|
usage: EMPTY_USAGE,
|
|
35
|
-
stopReason:
|
|
62
|
+
stopReason: aborted ? "aborted" : "error",
|
|
36
63
|
errorMessage: error instanceof Error ? error.message : String(error),
|
|
37
64
|
timestamp: Date.now(),
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
65
|
+
});
|
|
66
|
+
return { messages: [...messages, failureMessage], failureMessage, closureResults };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Terminate the public event stream after the underlying loop rejected.
|
|
70
|
+
*
|
|
71
|
+
* Uses {@link planFailureTermination} so the disposition of any unresolved tool
|
|
72
|
+
* calls matches transcript repair exactly: an unambiguous open turn is closed
|
|
73
|
+
* with synthetic results before a coherent
|
|
74
|
+
* message_start/message_end/turn_end/agent_end sequence for the failure
|
|
75
|
+
* assistant, and an ambiguous transcript fails closed (agent_end only, no
|
|
76
|
+
* fabricated assistant). The stream always settles for `for await` consumers
|
|
77
|
+
* and `stream.result()`.
|
|
78
|
+
*/
|
|
79
|
+
function endStreamWithFailure(stream, config, completedMessages, error, signal) {
|
|
80
|
+
const plan = planFailureTermination(completedMessages, config.model, error, signal?.aborted ?? false);
|
|
81
|
+
for (const result of plan.closureResults) {
|
|
82
|
+
stream.push({ type: "message_start", message: result });
|
|
83
|
+
stream.push({ type: "message_end", message: result });
|
|
84
|
+
}
|
|
85
|
+
if (plan.failureMessage) {
|
|
86
|
+
stream.push({ type: "message_start", message: plan.failureMessage });
|
|
87
|
+
stream.push({ type: "message_end", message: plan.failureMessage });
|
|
88
|
+
stream.push({ type: "turn_end", message: plan.failureMessage, toolResults: [] });
|
|
89
|
+
}
|
|
90
|
+
stream.push({ type: "agent_end", messages: plan.messages });
|
|
91
|
+
stream.end(plan.messages);
|
|
45
92
|
}
|
|
46
93
|
/**
|
|
47
94
|
* Start an agent loop with a new prompt message.
|
|
@@ -79,7 +126,7 @@ export function agentLoopContinue(context, config, signal, streamFn) {
|
|
|
79
126
|
// provider supports assistant pre-fill); only an assistant turn that still
|
|
80
127
|
// carries unresolved tool calls is a hard error because the provider will
|
|
81
128
|
// reject the request without matching tool results.
|
|
82
|
-
|
|
129
|
+
assertContinuableTranscript(context.messages);
|
|
83
130
|
const stream = createAgentStream();
|
|
84
131
|
const completedMessages = [];
|
|
85
132
|
void runAgentLoopContinue(context, config, async (event) => {
|
|
@@ -96,60 +143,55 @@ export function agentLoopContinue(context, config, signal, streamFn) {
|
|
|
96
143
|
}
|
|
97
144
|
export async function runAgentLoop(prompts, context, config, emit, signal, streamFn) {
|
|
98
145
|
const newMessages = [...prompts];
|
|
99
|
-
const currentContext = {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
};
|
|
103
|
-
await emit({ type: "agent_start" });
|
|
104
|
-
await emit({ type: "turn_start" });
|
|
146
|
+
const currentContext = { ...context, messages: [...context.messages, ...prompts] };
|
|
147
|
+
const publish = (event) => emit(createImmutableSnapshot(event));
|
|
148
|
+
await publish({ type: "agent_start" });
|
|
149
|
+
await publish({ type: "turn_start" });
|
|
105
150
|
for (const prompt of prompts) {
|
|
106
|
-
await
|
|
107
|
-
await
|
|
151
|
+
await publish({ type: "message_start", message: prompt });
|
|
152
|
+
await publish({ type: "message_end", message: prompt });
|
|
108
153
|
}
|
|
109
|
-
await runLoop(currentContext, newMessages, config, signal,
|
|
154
|
+
await runLoop(currentContext, newMessages, config, signal, publish, streamFn);
|
|
110
155
|
return newMessages;
|
|
111
156
|
}
|
|
112
157
|
export async function runAgentLoopContinue(context, config, emit, signal, streamFn) {
|
|
113
158
|
if (context.messages.length === 0) {
|
|
114
159
|
throw new Error("Cannot continue: no messages in context");
|
|
115
160
|
}
|
|
116
|
-
|
|
161
|
+
assertContinuableTranscript(context.messages);
|
|
117
162
|
const newMessages = [];
|
|
118
163
|
const currentContext = { ...context };
|
|
119
|
-
|
|
120
|
-
await
|
|
121
|
-
await
|
|
164
|
+
const publish = (event) => emit(createImmutableSnapshot(event));
|
|
165
|
+
await publish({ type: "agent_start" });
|
|
166
|
+
await publish({ type: "turn_start" });
|
|
167
|
+
await runLoop(currentContext, newMessages, config, signal, publish, streamFn);
|
|
122
168
|
return newMessages;
|
|
123
169
|
}
|
|
124
170
|
function createAgentStream() {
|
|
125
171
|
return new EventStream((event) => event.type === "agent_end", (event) => (event.type === "agent_end" ? event.messages : []));
|
|
126
172
|
}
|
|
127
173
|
/**
|
|
128
|
-
* Validate
|
|
129
|
-
*
|
|
174
|
+
* Validate the full transcript before continuing. Replaces the earlier
|
|
175
|
+
* last-message-only tail check: `assistant(A,B) -> result(A)` and any
|
|
176
|
+
* duplicate/orphan/interleaved structure now fail before the first provider
|
|
177
|
+
* request, not only a trailing assistant message that still carries tool calls.
|
|
130
178
|
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* calls. If it does carry tool calls, there is no matching toolResult to send
|
|
134
|
-
* and every provider (OpenAI/Anthropic/Google) rejects the request, so we
|
|
135
|
-
* fail fast with a clear, actionable error.
|
|
136
|
-
*
|
|
137
|
-
* This intentionally does NOT inspect `convertToLlm` output: that transform is
|
|
138
|
-
* provider-specific and runs once per turn. The conservative check here keeps
|
|
139
|
-
* the common "continue after a finished text answer" path working (compaction,
|
|
140
|
-
* session resume, explicit retries) while still protecting the broken tool-call
|
|
141
|
-
* case.
|
|
179
|
+
* A trailing assistant turn with no tool calls (plain text/thinking) remains
|
|
180
|
+
* continuable, so compaction, session resume, and explicit retries keep working.
|
|
142
181
|
*/
|
|
143
|
-
function
|
|
144
|
-
const
|
|
145
|
-
if (
|
|
182
|
+
function assertContinuableTranscript(messages) {
|
|
183
|
+
const report = inspectTranscriptIntegrity(messages);
|
|
184
|
+
if (report.ok) {
|
|
146
185
|
return;
|
|
147
186
|
}
|
|
148
|
-
const
|
|
149
|
-
if (
|
|
187
|
+
const last = messages[messages.length - 1];
|
|
188
|
+
if (last !== undefined && last.role === "assistant" && last.content.some((block) => block.type === "toolCall")) {
|
|
150
189
|
throw new Error("Cannot continue: the last assistant message has pending tool calls without matching results. " +
|
|
151
190
|
"Add tool results or a new user message before continuing.");
|
|
152
191
|
}
|
|
192
|
+
const summary = report.issues.map((issue) => `${issue.kind}:${issue.toolCallId}`).join(", ");
|
|
193
|
+
throw new Error(`Cannot continue: invalid tool transcript (${summary}). ` +
|
|
194
|
+
"Append terminal tool results or repair the transcript before continuing.");
|
|
153
195
|
}
|
|
154
196
|
/**
|
|
155
197
|
* Main loop logic shared by agentLoop and agentLoopContinue.
|
|
@@ -184,25 +226,59 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
|
|
|
184
226
|
// Stream assistant response
|
|
185
227
|
const message = await streamAssistantResponse(currentContext, config, signal, emit, streamFn);
|
|
186
228
|
newMessages.push(message);
|
|
229
|
+
// Provider output is untrusted protocol input. Reject duplicate call IDs
|
|
230
|
+
// and every other ambiguous turn before any tool can execute.
|
|
231
|
+
const emittedIntegrity = inspectTranscriptIntegrity(currentContext.messages);
|
|
232
|
+
const emittedAmbiguities = emittedIntegrity.issues.filter((issue) => issue.kind !== "missing_result");
|
|
233
|
+
if (emittedAmbiguities.length > 0) {
|
|
234
|
+
const summary = emittedAmbiguities.map((issue) => `${issue.kind}:${issue.toolCallId}`).join(", ");
|
|
235
|
+
throw new Error(`Refusing tool execution: invalid emitted tool transcript (${summary}).`);
|
|
236
|
+
}
|
|
237
|
+
const toolCalls = message.content.filter((c) => c.type === "toolCall");
|
|
187
238
|
if (message.stopReason === "error" || message.stopReason === "aborted") {
|
|
188
|
-
|
|
239
|
+
const toolResults = [];
|
|
240
|
+
const reason = message.stopReason === "aborted"
|
|
241
|
+
? "Operation aborted"
|
|
242
|
+
: "Skipped because the provider terminated before tool execution";
|
|
243
|
+
const disposition = message.stopReason === "aborted" ? "aborted" : "skipped";
|
|
244
|
+
for (const toolCall of toolCalls) {
|
|
245
|
+
const result = createImmutableSnapshot(createSyntheticToolResult(toolCall.id, toolCall.name, reason, Date.now(), disposition));
|
|
246
|
+
currentContext.messages.push(result);
|
|
247
|
+
newMessages.push(result);
|
|
248
|
+
toolResults.push(result);
|
|
249
|
+
await emitToolResultMessage(result, emit);
|
|
250
|
+
}
|
|
251
|
+
await emit({ type: "turn_end", message, toolResults });
|
|
189
252
|
await emit({ type: "agent_end", messages: newMessages });
|
|
190
253
|
return;
|
|
191
254
|
}
|
|
192
|
-
// Check for tool calls
|
|
193
|
-
const toolCalls = message.content.filter((c) => c.type === "toolCall");
|
|
194
255
|
const toolResults = [];
|
|
256
|
+
let stopAfterToolBatch = false;
|
|
195
257
|
hasMoreToolCalls = false;
|
|
196
258
|
if (toolCalls.length > 0) {
|
|
197
259
|
const executedToolBatch = await executeToolCalls(currentContext, message, config, signal, emit);
|
|
198
260
|
toolResults.push(...executedToolBatch.messages);
|
|
199
261
|
hasMoreToolCalls = !executedToolBatch.terminate;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
262
|
+
stopAfterToolBatch = executedToolBatch.stopRun ?? false;
|
|
263
|
+
if (signal?.aborted) {
|
|
264
|
+
// Close only unresolved calls, preserving finalized results, then stop
|
|
265
|
+
// before hooks, queues, or another provider request.
|
|
266
|
+
const synthesized = await closeAbortedToolBatch(currentContext, toolCalls, toolResults, emit);
|
|
267
|
+
toolResults.push(...synthesized);
|
|
268
|
+
for (const result of toolResults)
|
|
269
|
+
newMessages.push(result);
|
|
270
|
+
await emit({ type: "turn_end", message, toolResults });
|
|
271
|
+
await emit({ type: "agent_end", messages: newMessages });
|
|
272
|
+
return;
|
|
203
273
|
}
|
|
274
|
+
for (const result of toolResults)
|
|
275
|
+
newMessages.push(result);
|
|
204
276
|
}
|
|
205
277
|
await emit({ type: "turn_end", message, toolResults });
|
|
278
|
+
if (stopAfterToolBatch) {
|
|
279
|
+
await emit({ type: "agent_end", messages: newMessages });
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
206
282
|
const nextTurnContext = {
|
|
207
283
|
message,
|
|
208
284
|
toolResults,
|
|
@@ -250,10 +326,24 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
|
|
|
250
326
|
* This is where AgentMessage[] gets transformed to Message[] for the LLM.
|
|
251
327
|
*/
|
|
252
328
|
async function streamAssistantResponse(context, config, signal, emit, streamFn) {
|
|
329
|
+
// Validate the full transcript before every provider request. This fails
|
|
330
|
+
// fast for `assistant(A,B) -> result(A)` and any duplicate/orphan/interleaved
|
|
331
|
+
// structure that the provider would otherwise reject opaquely.
|
|
332
|
+
const integrityReport = inspectTranscriptIntegrity(context.messages);
|
|
333
|
+
if (!integrityReport.ok) {
|
|
334
|
+
const summary = integrityReport.issues.map((issue) => `${issue.kind}:${issue.toolCallId}`).join(", ");
|
|
335
|
+
throw new Error(`Refusing provider request: invalid tool transcript (${summary}). ` +
|
|
336
|
+
"Append terminal tool results or repair the transcript before retrying.");
|
|
337
|
+
}
|
|
253
338
|
// Apply context transform if configured (AgentMessage[] → AgentMessage[])
|
|
254
339
|
let messages = context.messages;
|
|
255
340
|
if (config.transformContext) {
|
|
256
341
|
messages = await config.transformContext(messages, signal);
|
|
342
|
+
const transformedIntegrity = inspectTranscriptIntegrity(messages);
|
|
343
|
+
if (!transformedIntegrity.ok) {
|
|
344
|
+
const summary = transformedIntegrity.issues.map((issue) => `${issue.kind}:${issue.toolCallId}`).join(", ");
|
|
345
|
+
throw new Error(`Refusing provider request: transformed context has an invalid tool transcript (${summary}).`);
|
|
346
|
+
}
|
|
257
347
|
}
|
|
258
348
|
// Convert to LLM-compatible messages (AgentMessage[] → Message[])
|
|
259
349
|
const llmMessages = await config.convertToLlm(messages);
|
|
@@ -333,6 +423,11 @@ async function streamAssistantResponse(context, config, signal, emit, streamFn)
|
|
|
333
423
|
*/
|
|
334
424
|
async function executeToolCalls(currentContext, assistantMessage, config, signal, emit) {
|
|
335
425
|
const toolCalls = assistantMessage.content.filter((c) => c.type === "toolCall");
|
|
426
|
+
// dag-v2 is opt-in only. An explicit sequential execution mode takes
|
|
427
|
+
// precedence and continues through the established waves-v1 path below.
|
|
428
|
+
if (config.toolScheduler === "dag-v2" && config.toolExecution !== "sequential") {
|
|
429
|
+
return executeToolCallsDagLevels(currentContext, assistantMessage, toolCalls, config, signal, emit);
|
|
430
|
+
}
|
|
336
431
|
const hasSequentialToolCall = toolCalls.some((tc) => currentContext.tools?.find((t) => t.name === tc.name)?.executionMode === "sequential");
|
|
337
432
|
const toolPolicies = new Map();
|
|
338
433
|
for (const tool of currentContext.tools ?? []) {
|
|
@@ -371,25 +466,197 @@ async function executeToolCallsInWaves(currentContext, assistantMessage, toolCal
|
|
|
371
466
|
: await executeToolCallsParallel(currentContext, assistantMessage, waveCalls, config, signal, emit);
|
|
372
467
|
messages.push(...executedWave.messages);
|
|
373
468
|
waveTerminates.push(executedWave.terminate);
|
|
374
|
-
if (signal?.aborted)
|
|
469
|
+
if (signal?.aborted)
|
|
375
470
|
break;
|
|
376
|
-
}
|
|
377
471
|
}
|
|
378
472
|
return {
|
|
379
473
|
messages,
|
|
380
474
|
terminate: waveTerminates.length > 0 && waveTerminates.every(Boolean),
|
|
381
475
|
};
|
|
382
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* Execute a tool-call batch using the dag-v2 scheduler.
|
|
479
|
+
*
|
|
480
|
+
* Initial planning applies only the pure argument compatibility shim. Each
|
|
481
|
+
* candidate level authorizes calls, re-resolves claims from exact final args,
|
|
482
|
+
* and emits lifecycle starts only when a final safe sublevel begins. Results
|
|
483
|
+
* remain globally buffered and are emitted in source order.
|
|
484
|
+
*/
|
|
485
|
+
async function executeToolCallsDagLevels(currentContext, assistantMessage, toolCalls, config, signal, emit) {
|
|
486
|
+
const plans = toolCalls.map((toolCall) => planToolCall(currentContext, toolCall));
|
|
487
|
+
const boundTools = plans.flatMap((plan) => (plan.kind === "planned" ? [plan.tool] : []));
|
|
488
|
+
const toolPolicies = new Map();
|
|
489
|
+
for (const tool of boundTools) {
|
|
490
|
+
if (tool.executionMode && !toolPolicies.has(tool.name))
|
|
491
|
+
toolPolicies.set(tool.name, tool.executionMode);
|
|
492
|
+
}
|
|
493
|
+
const claimableCalls = [];
|
|
494
|
+
for (const plan of plans) {
|
|
495
|
+
if (plan.kind === "immediate" || !isPlainArguments(plan.args)) {
|
|
496
|
+
claimableCalls.length = 0;
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
claimableCalls.push({ id: plan.toolCall.id, name: plan.toolCall.name, arguments: plan.args });
|
|
500
|
+
}
|
|
501
|
+
let levels;
|
|
502
|
+
if (claimableCalls.length === toolCalls.length) {
|
|
503
|
+
const scheduled = await awaitWithAbort(() => scheduleDagLevels(claimableCalls, {
|
|
504
|
+
cwd: config.cwd ?? process.cwd(),
|
|
505
|
+
toolPolicies,
|
|
506
|
+
registeredTools: boundTools,
|
|
507
|
+
strictExtensionClaims: config.strictExtensionClaims,
|
|
508
|
+
maxConcurrency: config.maxToolConcurrency,
|
|
509
|
+
resourceKeyResolver: config.resourceKeyResolver,
|
|
510
|
+
}), signal);
|
|
511
|
+
levels = scheduled.kind === "aborted" ? [] : scheduled.value.levels;
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
levels = toolCalls.map((_toolCall, sourceIndex) => [sourceIndex]);
|
|
515
|
+
}
|
|
516
|
+
const finalizedByIndex = new Array(toolCalls.length).fill(undefined);
|
|
517
|
+
let skippedReason;
|
|
518
|
+
let stoppedByUnsettledTimeout = false;
|
|
519
|
+
for (const level of levels) {
|
|
520
|
+
if (signal?.aborted)
|
|
521
|
+
break;
|
|
522
|
+
const executedLevel = await runDagLevelCalls(currentContext, assistantMessage, level, toolCalls, plans, toolPolicies, config, signal, emit);
|
|
523
|
+
for (const outcome of executedLevel.outcomes)
|
|
524
|
+
finalizedByIndex[outcome.sourceIndex] = outcome.finalized;
|
|
525
|
+
if (signal?.aborted)
|
|
526
|
+
break;
|
|
527
|
+
if (executedLevel.stoppedByUnsettledTimeout) {
|
|
528
|
+
skippedReason = "Skipped because a preceding DAG tool timed out before its execution promise settled";
|
|
529
|
+
stoppedByUnsettledTimeout = true;
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
if (shouldTerminateToolBatch(executedLevel.outcomes.map((outcome) => outcome.finalized))) {
|
|
533
|
+
skippedReason = "Skipped because the preceding DAG level requested termination";
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
const messages = [];
|
|
538
|
+
const finalizedCalls = [];
|
|
539
|
+
for (let index = 0; index < toolCalls.length; index++) {
|
|
540
|
+
const finalized = finalizedByIndex[index];
|
|
541
|
+
if (finalized) {
|
|
542
|
+
messages.push(createToolResultMessage(finalized));
|
|
543
|
+
finalizedCalls.push(finalized);
|
|
544
|
+
}
|
|
545
|
+
else if (signal?.aborted) {
|
|
546
|
+
const toolCall = toolCalls[index];
|
|
547
|
+
messages.push(createImmutableSnapshot(createSyntheticToolResult(toolCall.id, toolCall.name, "Operation aborted")));
|
|
548
|
+
}
|
|
549
|
+
else if (skippedReason !== undefined) {
|
|
550
|
+
const toolCall = toolCalls[index];
|
|
551
|
+
messages.push(createImmutableSnapshot(createSyntheticToolResult(toolCall.id, toolCall.name, skippedReason, Date.now(), "skipped")));
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
// Close the full source-ordered batch before result notification; calls not
|
|
555
|
+
// reached by a candidate level receive no execution lifecycle.
|
|
556
|
+
for (const message of messages) {
|
|
557
|
+
currentContext.messages.push(message);
|
|
558
|
+
try {
|
|
559
|
+
await emitToolResultMessage(message, emit);
|
|
560
|
+
}
|
|
561
|
+
finally {
|
|
562
|
+
finalizedCalls.find(({ toolCall }) => toolCall.id === message.toolCallId)?.commitTerminal?.();
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return {
|
|
566
|
+
messages,
|
|
567
|
+
terminate: skippedReason !== undefined || shouldTerminateToolBatch(finalizedCalls),
|
|
568
|
+
stopRun: stoppedByUnsettledTimeout,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
/** Authorize one candidate DAG level, re-plan final claims, and run its safe sublevels. */
|
|
572
|
+
async function runDagLevelCalls(currentContext, assistantMessage, levelIndices, toolCalls, plans, toolPolicies, config, signal, emit) {
|
|
573
|
+
const outcomes = [];
|
|
574
|
+
const runnable = [];
|
|
575
|
+
for (const sourceIndex of levelIndices) {
|
|
576
|
+
const toolCall = toolCalls[sourceIndex];
|
|
577
|
+
const plan = plans[sourceIndex];
|
|
578
|
+
const preparation = plan.kind === "immediate"
|
|
579
|
+
? plan
|
|
580
|
+
: await authorizePlannedToolCall(currentContext, assistantMessage, plan, config, signal);
|
|
581
|
+
if (preparation.kind === "immediate") {
|
|
582
|
+
outcomes.push({
|
|
583
|
+
sourceIndex,
|
|
584
|
+
finalized: {
|
|
585
|
+
toolCall,
|
|
586
|
+
result: preparation.result,
|
|
587
|
+
isError: preparation.isError,
|
|
588
|
+
envelope: preparation.envelope,
|
|
589
|
+
},
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
runnable.push({ sourceIndex, preparation });
|
|
594
|
+
}
|
|
595
|
+
if (signal?.aborted)
|
|
596
|
+
return { outcomes, stoppedByUnsettledTimeout: false };
|
|
597
|
+
}
|
|
598
|
+
const finalClaimableCalls = [];
|
|
599
|
+
for (const { preparation } of runnable) {
|
|
600
|
+
if (!isPlainArguments(preparation.args)) {
|
|
601
|
+
finalClaimableCalls.length = 0;
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
finalClaimableCalls.push({
|
|
605
|
+
id: preparation.toolCall.id,
|
|
606
|
+
name: preparation.toolCall.name,
|
|
607
|
+
arguments: preparation.args,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
let executionLevels;
|
|
611
|
+
if (finalClaimableCalls.length === runnable.length) {
|
|
612
|
+
const scheduled = await awaitWithAbort(() => scheduleDagLevels(finalClaimableCalls, {
|
|
613
|
+
cwd: config.cwd ?? process.cwd(),
|
|
614
|
+
toolPolicies,
|
|
615
|
+
registeredTools: runnable.map(({ preparation }) => preparation.tool),
|
|
616
|
+
strictExtensionClaims: config.strictExtensionClaims,
|
|
617
|
+
maxConcurrency: config.maxToolConcurrency,
|
|
618
|
+
resourceKeyResolver: config.resourceKeyResolver,
|
|
619
|
+
}), signal);
|
|
620
|
+
if (scheduled.kind === "aborted")
|
|
621
|
+
return { outcomes, stoppedByUnsettledTimeout: false };
|
|
622
|
+
executionLevels = scheduled.value.levels;
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
executionLevels = runnable.map((_entry, index) => [index]);
|
|
626
|
+
}
|
|
627
|
+
for (const executionLevel of executionLevels) {
|
|
628
|
+
if (signal?.aborted)
|
|
629
|
+
break;
|
|
630
|
+
for (const entryIndex of executionLevel) {
|
|
631
|
+
await emitToolExecutionStart(runnable[entryIndex].preparation, emit);
|
|
632
|
+
}
|
|
633
|
+
const finalizedLevel = await Promise.all(executionLevel.map(async (entryIndex) => {
|
|
634
|
+
const entry = runnable[entryIndex];
|
|
635
|
+
const executed = await executePreparedToolCall(entry.preparation, config, signal, emit);
|
|
636
|
+
const finalized = await finalizeExecutedToolCall({
|
|
637
|
+
currentContext,
|
|
638
|
+
assistantMessage,
|
|
639
|
+
prepared: entry.preparation,
|
|
640
|
+
executed,
|
|
641
|
+
afterToolCall: config.afterToolCall,
|
|
642
|
+
signal,
|
|
643
|
+
});
|
|
644
|
+
await emitToolExecutionEnd(finalized, emit);
|
|
645
|
+
return { sourceIndex: entry.sourceIndex, finalized };
|
|
646
|
+
}));
|
|
647
|
+
outcomes.push(...finalizedLevel);
|
|
648
|
+
if (signal?.aborted)
|
|
649
|
+
break;
|
|
650
|
+
if (finalizedLevel.some(({ finalized }) => finalized.envelope.disposition === "timeout" && finalized.isRealPromiseSettled?.() === false)) {
|
|
651
|
+
return { outcomes, stoppedByUnsettledTimeout: true };
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return { outcomes, stoppedByUnsettledTimeout: false };
|
|
655
|
+
}
|
|
383
656
|
async function executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, signal, emit) {
|
|
384
657
|
const finalizedCalls = [];
|
|
385
658
|
const messages = [];
|
|
386
659
|
for (const toolCall of toolCalls) {
|
|
387
|
-
await emit({
|
|
388
|
-
type: "tool_execution_start",
|
|
389
|
-
toolCallId: toolCall.id,
|
|
390
|
-
toolName: toolCall.name,
|
|
391
|
-
args: toolCall.arguments,
|
|
392
|
-
});
|
|
393
660
|
const preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal);
|
|
394
661
|
let finalized;
|
|
395
662
|
if (preparation.kind === "immediate") {
|
|
@@ -397,15 +664,31 @@ async function executeToolCallsSequential(currentContext, assistantMessage, tool
|
|
|
397
664
|
toolCall,
|
|
398
665
|
result: preparation.result,
|
|
399
666
|
isError: preparation.isError,
|
|
667
|
+
envelope: preparation.envelope,
|
|
400
668
|
};
|
|
401
669
|
}
|
|
402
670
|
else {
|
|
403
|
-
|
|
404
|
-
|
|
671
|
+
await emitToolExecutionStart(preparation, emit);
|
|
672
|
+
const executed = await executePreparedToolCall(preparation, config, signal, emit);
|
|
673
|
+
finalized = await finalizeExecutedToolCall({
|
|
674
|
+
currentContext,
|
|
675
|
+
assistantMessage,
|
|
676
|
+
prepared: preparation,
|
|
677
|
+
executed,
|
|
678
|
+
afterToolCall: config.afterToolCall,
|
|
679
|
+
signal,
|
|
680
|
+
});
|
|
405
681
|
}
|
|
406
|
-
await emitToolExecutionEnd(finalized, emit);
|
|
407
682
|
const toolResultMessage = createToolResultMessage(finalized);
|
|
408
|
-
|
|
683
|
+
currentContext.messages.push(toolResultMessage);
|
|
684
|
+
if (preparation.kind === "prepared")
|
|
685
|
+
await emitToolExecutionEnd(finalized, emit);
|
|
686
|
+
try {
|
|
687
|
+
await emitToolResultMessage(toolResultMessage, emit);
|
|
688
|
+
}
|
|
689
|
+
finally {
|
|
690
|
+
finalized.commitTerminal?.();
|
|
691
|
+
}
|
|
409
692
|
finalizedCalls.push(finalized);
|
|
410
693
|
messages.push(toolResultMessage);
|
|
411
694
|
if (signal?.aborted) {
|
|
@@ -420,20 +703,14 @@ async function executeToolCallsSequential(currentContext, assistantMessage, tool
|
|
|
420
703
|
async function executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, signal, emit) {
|
|
421
704
|
const finalizedCalls = [];
|
|
422
705
|
for (const toolCall of toolCalls) {
|
|
423
|
-
await emit({
|
|
424
|
-
type: "tool_execution_start",
|
|
425
|
-
toolCallId: toolCall.id,
|
|
426
|
-
toolName: toolCall.name,
|
|
427
|
-
args: toolCall.arguments,
|
|
428
|
-
});
|
|
429
706
|
const preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal);
|
|
430
707
|
if (preparation.kind === "immediate") {
|
|
431
708
|
const finalized = {
|
|
432
709
|
toolCall,
|
|
433
710
|
result: preparation.result,
|
|
434
711
|
isError: preparation.isError,
|
|
712
|
+
envelope: preparation.envelope,
|
|
435
713
|
};
|
|
436
|
-
await emitToolExecutionEnd(finalized, emit);
|
|
437
714
|
finalizedCalls.push(finalized);
|
|
438
715
|
if (signal?.aborted) {
|
|
439
716
|
break;
|
|
@@ -441,8 +718,16 @@ async function executeToolCallsParallel(currentContext, assistantMessage, toolCa
|
|
|
441
718
|
continue;
|
|
442
719
|
}
|
|
443
720
|
finalizedCalls.push(async () => {
|
|
444
|
-
|
|
445
|
-
const
|
|
721
|
+
await emitToolExecutionStart(preparation, emit);
|
|
722
|
+
const executed = await executePreparedToolCall(preparation, config, signal, emit);
|
|
723
|
+
const finalized = await finalizeExecutedToolCall({
|
|
724
|
+
currentContext,
|
|
725
|
+
assistantMessage,
|
|
726
|
+
prepared: preparation,
|
|
727
|
+
executed,
|
|
728
|
+
afterToolCall: config.afterToolCall,
|
|
729
|
+
signal,
|
|
730
|
+
});
|
|
446
731
|
await emitToolExecutionEnd(finalized, emit);
|
|
447
732
|
return finalized;
|
|
448
733
|
});
|
|
@@ -454,7 +739,13 @@ async function executeToolCallsParallel(currentContext, assistantMessage, toolCa
|
|
|
454
739
|
const messages = [];
|
|
455
740
|
for (const finalized of orderedFinalizedCalls) {
|
|
456
741
|
const toolResultMessage = createToolResultMessage(finalized);
|
|
457
|
-
|
|
742
|
+
currentContext.messages.push(toolResultMessage);
|
|
743
|
+
try {
|
|
744
|
+
await emitToolResultMessage(toolResultMessage, emit);
|
|
745
|
+
}
|
|
746
|
+
finally {
|
|
747
|
+
finalized.commitTerminal?.();
|
|
748
|
+
}
|
|
458
749
|
messages.push(toolResultMessage);
|
|
459
750
|
}
|
|
460
751
|
return {
|
|
@@ -478,146 +769,160 @@ function prepareToolCallArguments(tool, toolCall) {
|
|
|
478
769
|
arguments: preparedArguments,
|
|
479
770
|
};
|
|
480
771
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
772
|
+
function immediateOutcome(disposition, reason) {
|
|
773
|
+
return {
|
|
774
|
+
kind: "immediate",
|
|
775
|
+
result: createErrorToolResult(reason),
|
|
776
|
+
isError: true,
|
|
777
|
+
envelope: createToolResultEnvelope({ disposition, synthetic: true, executionStarted: false, reason }),
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
function planToolCall(currentContext, untrustedToolCall) {
|
|
781
|
+
try {
|
|
782
|
+
const toolCall = createImmutableJsonSnapshot(untrustedToolCall);
|
|
783
|
+
if (toolCall.id.length === 0 || toolCall.name.length === 0)
|
|
784
|
+
throw new TypeError("Invalid empty tool identity");
|
|
785
|
+
const candidate = currentContext.tools?.find((tool) => tool.name === toolCall.name);
|
|
786
|
+
if (!candidate)
|
|
787
|
+
return immediateOutcome("failed", `Tool ${toolCall.name} not found`);
|
|
788
|
+
const tool = bindToolIdentity(candidate, toolCall.name);
|
|
789
|
+
const prepared = prepareToolCallArguments(tool, toolCall);
|
|
790
|
+
const args = createImmutableJsonSnapshot(prepared.arguments);
|
|
791
|
+
const preparedToolCall = createImmutableSnapshot({ ...toolCall, arguments: args });
|
|
792
|
+
return { kind: "planned", toolCall, preparedToolCall, tool, args };
|
|
489
793
|
}
|
|
794
|
+
catch (error) {
|
|
795
|
+
return immediateOutcome("failed", error instanceof Error ? error.message : String(error));
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
async function authorizePlannedToolCall(currentContext, assistantMessage, plan, config, signal) {
|
|
490
799
|
try {
|
|
491
|
-
const
|
|
492
|
-
const
|
|
493
|
-
if (
|
|
494
|
-
const
|
|
495
|
-
assistantMessage,
|
|
496
|
-
toolCall,
|
|
497
|
-
args:
|
|
800
|
+
const hookArgs = parseJsonValue(validateToolArguments(plan.tool, plan.preparedToolCall));
|
|
801
|
+
const beforeToolCall = config.beforeToolCall;
|
|
802
|
+
if (beforeToolCall) {
|
|
803
|
+
const bounded = await awaitWithAbort(() => beforeToolCall({
|
|
804
|
+
assistantMessage: createImmutableSnapshot(assistantMessage),
|
|
805
|
+
toolCall: plan.toolCall,
|
|
806
|
+
args: hookArgs,
|
|
498
807
|
context: currentContext,
|
|
499
|
-
}, signal);
|
|
500
|
-
if (signal?.aborted)
|
|
501
|
-
return
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
isError: true,
|
|
505
|
-
};
|
|
506
|
-
}
|
|
507
|
-
if (beforeResult?.block) {
|
|
508
|
-
return {
|
|
509
|
-
kind: "immediate",
|
|
510
|
-
result: createErrorToolResult(beforeResult.reason || "Tool execution was blocked"),
|
|
511
|
-
isError: true,
|
|
512
|
-
};
|
|
808
|
+
}, signal), signal);
|
|
809
|
+
if (bounded.kind === "aborted" || signal?.aborted)
|
|
810
|
+
return immediateOutcome("aborted", "Operation aborted");
|
|
811
|
+
if (bounded.value?.block) {
|
|
812
|
+
return immediateOutcome("blocked", bounded.value.reason || "Tool execution was blocked");
|
|
513
813
|
}
|
|
514
814
|
}
|
|
515
|
-
if (signal?.aborted)
|
|
516
|
-
return
|
|
517
|
-
|
|
518
|
-
result: createErrorToolResult("Operation aborted"),
|
|
519
|
-
isError: true,
|
|
520
|
-
};
|
|
521
|
-
}
|
|
815
|
+
if (signal?.aborted)
|
|
816
|
+
return immediateOutcome("aborted", "Operation aborted");
|
|
817
|
+
const args = createImmutableJsonSnapshot(hookArgs);
|
|
522
818
|
return {
|
|
523
819
|
kind: "prepared",
|
|
524
|
-
toolCall,
|
|
525
|
-
tool,
|
|
526
|
-
args
|
|
820
|
+
toolCall: plan.toolCall,
|
|
821
|
+
tool: plan.tool,
|
|
822
|
+
args,
|
|
823
|
+
eventArgs: createImmutableSnapshot(args),
|
|
824
|
+
timeoutMs: resolveToolTimeoutMs(plan.tool, config, plan.toolCall.name),
|
|
527
825
|
};
|
|
528
826
|
}
|
|
529
827
|
catch (error) {
|
|
530
|
-
return
|
|
531
|
-
kind: "immediate",
|
|
532
|
-
result: createErrorToolResult(error instanceof Error ? error.message : String(error)),
|
|
533
|
-
isError: true,
|
|
534
|
-
};
|
|
828
|
+
return immediateOutcome("failed", error instanceof Error ? error.message : String(error));
|
|
535
829
|
}
|
|
536
830
|
}
|
|
537
|
-
async function
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
type: "tool_execution_update",
|
|
543
|
-
toolCallId: prepared.toolCall.id,
|
|
544
|
-
toolName: prepared.toolCall.name,
|
|
545
|
-
args: prepared.toolCall.arguments,
|
|
546
|
-
partialResult,
|
|
547
|
-
})));
|
|
548
|
-
});
|
|
549
|
-
await Promise.all(updateEvents);
|
|
550
|
-
return { result, isError: false };
|
|
551
|
-
}
|
|
552
|
-
catch (error) {
|
|
553
|
-
await Promise.all(updateEvents);
|
|
554
|
-
return {
|
|
555
|
-
result: createErrorToolResult(error instanceof Error ? error.message : String(error)),
|
|
556
|
-
isError: true,
|
|
557
|
-
};
|
|
558
|
-
}
|
|
831
|
+
async function prepareToolCall(currentContext, assistantMessage, toolCall, config, signal) {
|
|
832
|
+
const plan = planToolCall(currentContext, toolCall);
|
|
833
|
+
return plan.kind === "immediate"
|
|
834
|
+
? plan
|
|
835
|
+
: authorizePlannedToolCall(currentContext, assistantMessage, plan, config, signal);
|
|
559
836
|
}
|
|
560
|
-
async function
|
|
561
|
-
let
|
|
562
|
-
let
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
content: afterResult.content ?? result.content,
|
|
576
|
-
details: afterResult.details ?? result.details,
|
|
577
|
-
terminate: afterResult.terminate ?? result.terminate,
|
|
578
|
-
};
|
|
579
|
-
isError = afterResult.isError ?? isError;
|
|
837
|
+
async function executePreparedToolCall(prepared, config, signal, emit) {
|
|
838
|
+
let realPromiseSettled = false;
|
|
839
|
+
let commitTerminal = () => { };
|
|
840
|
+
const terminalCommitted = new Promise((resolve) => {
|
|
841
|
+
commitTerminal = resolve;
|
|
842
|
+
});
|
|
843
|
+
const executed = await runToolCallWithTimeout({
|
|
844
|
+
toolCallId: prepared.toolCall.id,
|
|
845
|
+
toolName: prepared.toolCall.name,
|
|
846
|
+
timeoutMs: prepared.timeoutMs,
|
|
847
|
+
lateSettlement: config.toolExecutionPolicy?.lateSettlement,
|
|
848
|
+
signal,
|
|
849
|
+
start: async (childSignal, onUpdate) => {
|
|
850
|
+
try {
|
|
851
|
+
return await prepared.tool.execute(prepared.toolCall.id, prepared.args, childSignal, onUpdate);
|
|
580
852
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
853
|
+
finally {
|
|
854
|
+
realPromiseSettled = true;
|
|
855
|
+
}
|
|
856
|
+
},
|
|
857
|
+
emitUpdate: (partialResult) => emit({
|
|
858
|
+
type: "tool_execution_update",
|
|
859
|
+
toolCallId: prepared.toolCall.id,
|
|
860
|
+
toolName: prepared.toolCall.name,
|
|
861
|
+
args: prepared.eventArgs,
|
|
862
|
+
partialResult: createImmutableSnapshot(partialResult),
|
|
863
|
+
}),
|
|
864
|
+
emitLateSettlement: async (settlement) => {
|
|
865
|
+
await terminalCommitted;
|
|
866
|
+
await emit({
|
|
867
|
+
type: "tool_execution_late_settlement",
|
|
868
|
+
toolCallId: settlement.toolCallId,
|
|
869
|
+
toolName: settlement.toolName,
|
|
870
|
+
disposition: settlement.disposition,
|
|
871
|
+
outcome: settlement.outcome,
|
|
872
|
+
});
|
|
873
|
+
},
|
|
874
|
+
toErrorResult: (error) => createErrorToolResult(error instanceof Error ? error.message : String(error)),
|
|
875
|
+
});
|
|
876
|
+
return { ...executed, isRealPromiseSettled: () => realPromiseSettled, commitTerminal };
|
|
592
877
|
}
|
|
593
|
-
function
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
878
|
+
async function emitToolExecutionStart(prepared, emit) {
|
|
879
|
+
await emit({
|
|
880
|
+
type: "tool_execution_start",
|
|
881
|
+
toolCallId: prepared.toolCall.id,
|
|
882
|
+
toolName: prepared.toolCall.name,
|
|
883
|
+
args: prepared.eventArgs,
|
|
884
|
+
});
|
|
598
885
|
}
|
|
599
886
|
async function emitToolExecutionEnd(finalized, emit) {
|
|
600
887
|
await emit({
|
|
601
888
|
type: "tool_execution_end",
|
|
602
889
|
toolCallId: finalized.toolCall.id,
|
|
603
890
|
toolName: finalized.toolCall.name,
|
|
604
|
-
result: finalized.result,
|
|
891
|
+
result: createImmutableSnapshot(finalized.result),
|
|
605
892
|
isError: finalized.isError,
|
|
606
893
|
});
|
|
607
894
|
}
|
|
608
895
|
function createToolResultMessage(finalized) {
|
|
609
|
-
return {
|
|
896
|
+
return createImmutableSnapshot({
|
|
610
897
|
role: "toolResult",
|
|
611
898
|
toolCallId: finalized.toolCall.id,
|
|
612
899
|
toolName: finalized.toolCall.name,
|
|
613
900
|
content: finalized.result.content,
|
|
614
|
-
details: finalized.result.details,
|
|
901
|
+
details: stampToolResultEnvelope(finalized.result.details, finalized.envelope),
|
|
615
902
|
isError: finalized.isError,
|
|
616
903
|
timestamp: Date.now(),
|
|
617
|
-
};
|
|
904
|
+
});
|
|
618
905
|
}
|
|
619
906
|
async function emitToolResultMessage(toolResultMessage, emit) {
|
|
620
907
|
await emit({ type: "message_start", message: toolResultMessage });
|
|
621
908
|
await emit({ type: "message_end", message: toolResultMessage });
|
|
622
909
|
}
|
|
910
|
+
/** Commit and notify one aborted terminal for each unresolved, unstarted call. */
|
|
911
|
+
async function closeAbortedToolBatch(currentContext, toolCalls, existingResults, emit) {
|
|
912
|
+
const resolvedIds = new Set(existingResults.map((result) => result.toolCallId));
|
|
913
|
+
const synthesized = [];
|
|
914
|
+
for (const toolCall of toolCalls) {
|
|
915
|
+
if (resolvedIds.has(toolCall.id)) {
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
918
|
+
const result = createImmutableSnapshot(createSyntheticToolResult(toolCall.id, toolCall.name, "Operation aborted"));
|
|
919
|
+
currentContext.messages.push(result);
|
|
920
|
+
await emit({ type: "message_start", message: result });
|
|
921
|
+
await emit({ type: "message_end", message: result });
|
|
922
|
+
synthesized.push(result);
|
|
923
|
+
// Guard against a duplicated call id within the same assistant message.
|
|
924
|
+
resolvedIds.add(toolCall.id);
|
|
925
|
+
}
|
|
926
|
+
return synthesized;
|
|
927
|
+
}
|
|
623
928
|
//# sourceMappingURL=agent-loop.js.map
|