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.
- 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 +524 -189
- 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 +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- 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/parallel-tool-batch.d.ts +12 -1
- package/dist/parallel-tool-batch.d.ts.map +1 -1
- package/dist/parallel-tool-batch.js +71 -49
- package/dist/parallel-tool-batch.js.map +1 -1
- package/dist/path-segments.d.ts +21 -1
- package/dist/path-segments.d.ts.map +1 -1
- package/dist/path-segments.js +91 -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 {
|
|
6
|
+
import { bindToolIdentity, isPlainArguments } from "./builtin-tool-resource-claims.js";
|
|
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
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* - A trailing assistant turn is fine ONLY when it carries no unresolved tool
|
|
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.
|
|
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.
|
|
136
178
|
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
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 ?? []) {
|
|
@@ -340,26 +435,228 @@ async function executeToolCalls(currentContext, assistantMessage, config, signal
|
|
|
340
435
|
toolPolicies.set(tool.name, tool.executionMode);
|
|
341
436
|
}
|
|
342
437
|
}
|
|
343
|
-
const
|
|
438
|
+
const batchWaves = partitionToolBatchWaves(toolCalls.map((tc) => ({ name: tc.name, arguments: tc.arguments })), {
|
|
344
439
|
cwd: config.cwd ?? process.cwd(),
|
|
345
440
|
toolPolicies,
|
|
346
441
|
allowUnknownParallel: (toolName) => toolPolicies.get(toolName) === "parallel",
|
|
347
442
|
});
|
|
348
|
-
if (config.toolExecution === "sequential" ||
|
|
443
|
+
if (config.toolExecution === "sequential" ||
|
|
444
|
+
hasSequentialToolCall ||
|
|
445
|
+
batchWaves.every((wave) => wave.length === 1)) {
|
|
349
446
|
return executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, signal, emit);
|
|
350
447
|
}
|
|
351
|
-
|
|
448
|
+
if (batchWaves.length === 1) {
|
|
449
|
+
return executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, signal, emit);
|
|
450
|
+
}
|
|
451
|
+
return executeToolCallsInWaves(currentContext, assistantMessage, toolCalls, batchWaves, config, signal, emit);
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Execute a partitioned tool-call batch wave by wave: waves run in source
|
|
455
|
+
* order, calls inside a multi-call wave run concurrently, and solo waves run
|
|
456
|
+
* sequentially. Waves are contiguous index runs, so the returned tool result
|
|
457
|
+
* messages keep the model's original tool-call order.
|
|
458
|
+
*/
|
|
459
|
+
async function executeToolCallsInWaves(currentContext, assistantMessage, toolCalls, waves, config, signal, emit) {
|
|
460
|
+
const messages = [];
|
|
461
|
+
const waveTerminates = [];
|
|
462
|
+
for (const wave of waves) {
|
|
463
|
+
const waveCalls = wave.map((index) => toolCalls[index]);
|
|
464
|
+
const executedWave = waveCalls.length === 1
|
|
465
|
+
? await executeToolCallsSequential(currentContext, assistantMessage, waveCalls, config, signal, emit)
|
|
466
|
+
: await executeToolCallsParallel(currentContext, assistantMessage, waveCalls, config, signal, emit);
|
|
467
|
+
messages.push(...executedWave.messages);
|
|
468
|
+
waveTerminates.push(executedWave.terminate);
|
|
469
|
+
if (signal?.aborted)
|
|
470
|
+
break;
|
|
471
|
+
}
|
|
472
|
+
return {
|
|
473
|
+
messages,
|
|
474
|
+
terminate: waveTerminates.length > 0 && waveTerminates.every(Boolean),
|
|
475
|
+
};
|
|
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 };
|
|
352
655
|
}
|
|
353
656
|
async function executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, signal, emit) {
|
|
354
657
|
const finalizedCalls = [];
|
|
355
658
|
const messages = [];
|
|
356
659
|
for (const toolCall of toolCalls) {
|
|
357
|
-
await emit({
|
|
358
|
-
type: "tool_execution_start",
|
|
359
|
-
toolCallId: toolCall.id,
|
|
360
|
-
toolName: toolCall.name,
|
|
361
|
-
args: toolCall.arguments,
|
|
362
|
-
});
|
|
363
660
|
const preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal);
|
|
364
661
|
let finalized;
|
|
365
662
|
if (preparation.kind === "immediate") {
|
|
@@ -367,15 +664,31 @@ async function executeToolCallsSequential(currentContext, assistantMessage, tool
|
|
|
367
664
|
toolCall,
|
|
368
665
|
result: preparation.result,
|
|
369
666
|
isError: preparation.isError,
|
|
667
|
+
envelope: preparation.envelope,
|
|
370
668
|
};
|
|
371
669
|
}
|
|
372
670
|
else {
|
|
373
|
-
|
|
374
|
-
|
|
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
|
+
});
|
|
375
681
|
}
|
|
376
|
-
await emitToolExecutionEnd(finalized, emit);
|
|
377
682
|
const toolResultMessage = createToolResultMessage(finalized);
|
|
378
|
-
|
|
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
|
+
}
|
|
379
692
|
finalizedCalls.push(finalized);
|
|
380
693
|
messages.push(toolResultMessage);
|
|
381
694
|
if (signal?.aborted) {
|
|
@@ -390,20 +703,14 @@ async function executeToolCallsSequential(currentContext, assistantMessage, tool
|
|
|
390
703
|
async function executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, signal, emit) {
|
|
391
704
|
const finalizedCalls = [];
|
|
392
705
|
for (const toolCall of toolCalls) {
|
|
393
|
-
await emit({
|
|
394
|
-
type: "tool_execution_start",
|
|
395
|
-
toolCallId: toolCall.id,
|
|
396
|
-
toolName: toolCall.name,
|
|
397
|
-
args: toolCall.arguments,
|
|
398
|
-
});
|
|
399
706
|
const preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal);
|
|
400
707
|
if (preparation.kind === "immediate") {
|
|
401
708
|
const finalized = {
|
|
402
709
|
toolCall,
|
|
403
710
|
result: preparation.result,
|
|
404
711
|
isError: preparation.isError,
|
|
712
|
+
envelope: preparation.envelope,
|
|
405
713
|
};
|
|
406
|
-
await emitToolExecutionEnd(finalized, emit);
|
|
407
714
|
finalizedCalls.push(finalized);
|
|
408
715
|
if (signal?.aborted) {
|
|
409
716
|
break;
|
|
@@ -411,8 +718,16 @@ async function executeToolCallsParallel(currentContext, assistantMessage, toolCa
|
|
|
411
718
|
continue;
|
|
412
719
|
}
|
|
413
720
|
finalizedCalls.push(async () => {
|
|
414
|
-
|
|
415
|
-
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
|
+
});
|
|
416
731
|
await emitToolExecutionEnd(finalized, emit);
|
|
417
732
|
return finalized;
|
|
418
733
|
});
|
|
@@ -424,7 +739,13 @@ async function executeToolCallsParallel(currentContext, assistantMessage, toolCa
|
|
|
424
739
|
const messages = [];
|
|
425
740
|
for (const finalized of orderedFinalizedCalls) {
|
|
426
741
|
const toolResultMessage = createToolResultMessage(finalized);
|
|
427
|
-
|
|
742
|
+
currentContext.messages.push(toolResultMessage);
|
|
743
|
+
try {
|
|
744
|
+
await emitToolResultMessage(toolResultMessage, emit);
|
|
745
|
+
}
|
|
746
|
+
finally {
|
|
747
|
+
finalized.commitTerminal?.();
|
|
748
|
+
}
|
|
428
749
|
messages.push(toolResultMessage);
|
|
429
750
|
}
|
|
430
751
|
return {
|
|
@@ -448,146 +769,160 @@ function prepareToolCallArguments(tool, toolCall) {
|
|
|
448
769
|
arguments: preparedArguments,
|
|
449
770
|
};
|
|
450
771
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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 };
|
|
793
|
+
}
|
|
794
|
+
catch (error) {
|
|
795
|
+
return immediateOutcome("failed", error instanceof Error ? error.message : String(error));
|
|
459
796
|
}
|
|
797
|
+
}
|
|
798
|
+
async function authorizePlannedToolCall(currentContext, assistantMessage, plan, config, signal) {
|
|
460
799
|
try {
|
|
461
|
-
const
|
|
462
|
-
const
|
|
463
|
-
if (
|
|
464
|
-
const
|
|
465
|
-
assistantMessage,
|
|
466
|
-
toolCall,
|
|
467
|
-
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,
|
|
468
807
|
context: currentContext,
|
|
469
|
-
}, signal);
|
|
470
|
-
if (signal?.aborted)
|
|
471
|
-
return
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
isError: true,
|
|
475
|
-
};
|
|
476
|
-
}
|
|
477
|
-
if (beforeResult?.block) {
|
|
478
|
-
return {
|
|
479
|
-
kind: "immediate",
|
|
480
|
-
result: createErrorToolResult(beforeResult.reason || "Tool execution was blocked"),
|
|
481
|
-
isError: true,
|
|
482
|
-
};
|
|
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");
|
|
483
813
|
}
|
|
484
814
|
}
|
|
485
|
-
if (signal?.aborted)
|
|
486
|
-
return
|
|
487
|
-
|
|
488
|
-
result: createErrorToolResult("Operation aborted"),
|
|
489
|
-
isError: true,
|
|
490
|
-
};
|
|
491
|
-
}
|
|
815
|
+
if (signal?.aborted)
|
|
816
|
+
return immediateOutcome("aborted", "Operation aborted");
|
|
817
|
+
const args = createImmutableJsonSnapshot(hookArgs);
|
|
492
818
|
return {
|
|
493
819
|
kind: "prepared",
|
|
494
|
-
toolCall,
|
|
495
|
-
tool,
|
|
496
|
-
args
|
|
820
|
+
toolCall: plan.toolCall,
|
|
821
|
+
tool: plan.tool,
|
|
822
|
+
args,
|
|
823
|
+
eventArgs: createImmutableSnapshot(args),
|
|
824
|
+
timeoutMs: resolveToolTimeoutMs(plan.tool, config, plan.toolCall.name),
|
|
497
825
|
};
|
|
498
826
|
}
|
|
499
827
|
catch (error) {
|
|
500
|
-
return
|
|
501
|
-
kind: "immediate",
|
|
502
|
-
result: createErrorToolResult(error instanceof Error ? error.message : String(error)),
|
|
503
|
-
isError: true,
|
|
504
|
-
};
|
|
828
|
+
return immediateOutcome("failed", error instanceof Error ? error.message : String(error));
|
|
505
829
|
}
|
|
506
830
|
}
|
|
507
|
-
async function
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
type: "tool_execution_update",
|
|
513
|
-
toolCallId: prepared.toolCall.id,
|
|
514
|
-
toolName: prepared.toolCall.name,
|
|
515
|
-
args: prepared.toolCall.arguments,
|
|
516
|
-
partialResult,
|
|
517
|
-
})));
|
|
518
|
-
});
|
|
519
|
-
await Promise.all(updateEvents);
|
|
520
|
-
return { result, isError: false };
|
|
521
|
-
}
|
|
522
|
-
catch (error) {
|
|
523
|
-
await Promise.all(updateEvents);
|
|
524
|
-
return {
|
|
525
|
-
result: createErrorToolResult(error instanceof Error ? error.message : String(error)),
|
|
526
|
-
isError: true,
|
|
527
|
-
};
|
|
528
|
-
}
|
|
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);
|
|
529
836
|
}
|
|
530
|
-
async function
|
|
531
|
-
let
|
|
532
|
-
let
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
content: afterResult.content ?? result.content,
|
|
546
|
-
details: afterResult.details ?? result.details,
|
|
547
|
-
terminate: afterResult.terminate ?? result.terminate,
|
|
548
|
-
};
|
|
549
|
-
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);
|
|
550
852
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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 };
|
|
562
877
|
}
|
|
563
|
-
function
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
+
});
|
|
568
885
|
}
|
|
569
886
|
async function emitToolExecutionEnd(finalized, emit) {
|
|
570
887
|
await emit({
|
|
571
888
|
type: "tool_execution_end",
|
|
572
889
|
toolCallId: finalized.toolCall.id,
|
|
573
890
|
toolName: finalized.toolCall.name,
|
|
574
|
-
result: finalized.result,
|
|
891
|
+
result: createImmutableSnapshot(finalized.result),
|
|
575
892
|
isError: finalized.isError,
|
|
576
893
|
});
|
|
577
894
|
}
|
|
578
895
|
function createToolResultMessage(finalized) {
|
|
579
|
-
return {
|
|
896
|
+
return createImmutableSnapshot({
|
|
580
897
|
role: "toolResult",
|
|
581
898
|
toolCallId: finalized.toolCall.id,
|
|
582
899
|
toolName: finalized.toolCall.name,
|
|
583
900
|
content: finalized.result.content,
|
|
584
|
-
details: finalized.result.details,
|
|
901
|
+
details: stampToolResultEnvelope(finalized.result.details, finalized.envelope),
|
|
585
902
|
isError: finalized.isError,
|
|
586
903
|
timestamp: Date.now(),
|
|
587
|
-
};
|
|
904
|
+
});
|
|
588
905
|
}
|
|
589
906
|
async function emitToolResultMessage(toolResultMessage, emit) {
|
|
590
907
|
await emit({ type: "message_start", message: toolResultMessage });
|
|
591
908
|
await emit({ type: "message_end", message: toolResultMessage });
|
|
592
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
|
+
}
|
|
593
928
|
//# sourceMappingURL=agent-loop.js.map
|