@polpo-ai/node 0.18.6 → 0.18.8
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/dist/adapters/loop-engine.d.ts.map +1 -1
- package/dist/adapters/loop-engine.js +288 -8
- package/dist/adapters/loop-engine.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/runtime-hooks.d.ts +36 -0
- package/dist/runtime-hooks.d.ts.map +1 -0
- package/dist/runtime-hooks.js +292 -0
- package/dist/runtime-hooks.js.map +1 -0
- package/dist/server/app.d.ts +8 -1
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/app.js +28 -1
- package/dist/server/app.js.map +1 -1
- package/package.json +9 -9
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loop-engine.d.ts","sourceRoot":"","sources":["../../src/adapters/loop-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,IAAI,EAAc,MAAM,sBAAsB,CAAC;AAC1E,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACxE,OAAO,EAGL,KAAK,YAAY,EAGlB,MAAM,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"loop-engine.d.ts","sourceRoot":"","sources":["../../src/adapters/loop-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,IAAI,EAAc,MAAM,sBAAsB,CAAC;AAC1E,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACxE,OAAO,EAGL,KAAK,YAAY,EAGlB,MAAM,IAAI,CAAC;AA8CZ,OAAO,EAEL,KAAK,eAAe,EAErB,MAAM,yBAAyB,CAAC;AAkDjC;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,eAAe,GAAG,YAAY,CAwB7E;AA8CD;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,QAAkB,CAAC;AAiCpD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,YAAY,GAAG,WAAW,CAy5ClH"}
|
|
@@ -29,6 +29,7 @@ import { generateText, tool as aiTool, } from "ai";
|
|
|
29
29
|
import { normalizeResponseMessagesForHistory, prepareModelMessagesForTransport, runModelPolicyTurn, isStructuredModelOutputError, modelOutputForJsonSchema, classifyRuntimeError, markToolStrictness, toValidatedToolInputSchema, validateToolInput, } from "@polpo-ai/llm";
|
|
30
30
|
import { cleanupAgentBrowserSession } from "@polpo-ai/tools";
|
|
31
31
|
import { LoopRunner, LoopToolInputValidationError, PipelineExecutor, buildLoopStepAgent, loopContextPrompt, loopAgentInputPrompt, maybeParseJson, LoopAgentOutputError, prepareLoopAgentOutput, normalizeProjectLoop, normalizeToolInput, protectRuntimeToolResultMessages, renderRuntimeToolResult, resolveLoopSelection, compactIfNeeded, extractToolUsageRecord, } from "@polpo-ai/core";
|
|
32
|
+
import { runtimeHookFileChangeForTool } from "@polpo-ai/core/runtime-hooks";
|
|
32
33
|
import { InMemorySteeringController, } from "@polpo-ai/core/steering";
|
|
33
34
|
import { projectLoopConfigSchema } from "@polpo-ai/core/schemas";
|
|
34
35
|
import { NodeFileSystem } from "./node-filesystem.js";
|
|
@@ -220,6 +221,72 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
|
|
|
220
221
|
const sink = handle.onTranscript ?? ctx?.onTranscript;
|
|
221
222
|
sink?.(entry);
|
|
222
223
|
};
|
|
224
|
+
const runtimeHooks = ctx?.inject?.runtimeHooks ?? ctx?.runtimeHooks;
|
|
225
|
+
const dispatchRuntimeHook = async (event, data = {}, identity = {}) => {
|
|
226
|
+
if (!runtimeHooks)
|
|
227
|
+
return { action: "allow" };
|
|
228
|
+
const result = await runtimeHooks.dispatch({
|
|
229
|
+
event,
|
|
230
|
+
surface: ctx?.inject?.runtimeHookSurface
|
|
231
|
+
?? ctx?.runtimeHookSurface
|
|
232
|
+
?? (ctx?.inject ? "chat" : "task"),
|
|
233
|
+
identity: {
|
|
234
|
+
runId: ctx?.runId ?? task.id,
|
|
235
|
+
sessionId: ctx?.inject?.sessionId,
|
|
236
|
+
agent: agentConfig.name,
|
|
237
|
+
...identity,
|
|
238
|
+
},
|
|
239
|
+
data,
|
|
240
|
+
});
|
|
241
|
+
return result.decision;
|
|
242
|
+
};
|
|
243
|
+
const decisionError = (decision) => {
|
|
244
|
+
if (decision.action === "allow" || decision.action === "modify")
|
|
245
|
+
return undefined;
|
|
246
|
+
const reason = "reason" in decision ? decision.reason : "Runtime hook stopped execution";
|
|
247
|
+
return new Error(`Runtime hook ${decision.action}: ${reason ?? "stopped"}`);
|
|
248
|
+
};
|
|
249
|
+
const runHookedModel = async (data, operation, resultData = () => ({})) => {
|
|
250
|
+
const before = await dispatchRuntimeHook("model:before", data);
|
|
251
|
+
const beforeError = decisionError(before);
|
|
252
|
+
if (beforeError)
|
|
253
|
+
throw beforeError;
|
|
254
|
+
try {
|
|
255
|
+
const result = await operation();
|
|
256
|
+
const after = await dispatchRuntimeHook("model:after", { ...data, ...resultData(result) });
|
|
257
|
+
const afterError = decisionError(after);
|
|
258
|
+
if (afterError)
|
|
259
|
+
throw afterError;
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
await dispatchRuntimeHook("model:error", {
|
|
264
|
+
...data,
|
|
265
|
+
error: error instanceof Error ? error.message : String(error),
|
|
266
|
+
});
|
|
267
|
+
throw error;
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
const dispatchModelPolicyHook = async (event, identity) => {
|
|
271
|
+
if (event.type !== "model-fallback-selected")
|
|
272
|
+
return;
|
|
273
|
+
const { raw: _raw, ...reason } = event.reason;
|
|
274
|
+
const data = {
|
|
275
|
+
from: event.from.model,
|
|
276
|
+
to: event.to.model,
|
|
277
|
+
fromIndex: event.from.index,
|
|
278
|
+
toIndex: event.to.index,
|
|
279
|
+
reason,
|
|
280
|
+
};
|
|
281
|
+
const before = await dispatchRuntimeHook("model:switch:before", data, identity);
|
|
282
|
+
const beforeError = decisionError(before);
|
|
283
|
+
if (beforeError)
|
|
284
|
+
throw beforeError;
|
|
285
|
+
const after = await dispatchRuntimeHook("model:switch:after", data, identity);
|
|
286
|
+
const afterError = decisionError(after);
|
|
287
|
+
if (afterError)
|
|
288
|
+
throw afterError;
|
|
289
|
+
};
|
|
223
290
|
/**
|
|
224
291
|
* Execute one tool call: dispatch, activity tracking, outcome collection,
|
|
225
292
|
* toolUsage harvesting, transcript. Shared by LLM-session tool execution
|
|
@@ -232,6 +299,23 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
|
|
|
232
299
|
};
|
|
233
300
|
let isError = false;
|
|
234
301
|
let executionError;
|
|
302
|
+
const before = await dispatchRuntimeHook("tool:before", {
|
|
303
|
+
tool: toolCall.name,
|
|
304
|
+
input: toolCall.args,
|
|
305
|
+
}, { toolCallId: toolCall.id });
|
|
306
|
+
const beforeError = decisionError(before);
|
|
307
|
+
if (beforeError) {
|
|
308
|
+
isError = true;
|
|
309
|
+
executionError = beforeError;
|
|
310
|
+
result = { content: [{ type: "text", text: beforeError.message }], details: {} };
|
|
311
|
+
return { llmText: beforeError.message, isError, error: executionError };
|
|
312
|
+
}
|
|
313
|
+
const effectiveArgs = before.action === "modify"
|
|
314
|
+
&& before.data.input
|
|
315
|
+
&& typeof before.data.input === "object"
|
|
316
|
+
&& !Array.isArray(before.data.input)
|
|
317
|
+
? before.data.input
|
|
318
|
+
: toolCall.args;
|
|
235
319
|
const dispatch = async (args) => {
|
|
236
320
|
if (!pt) {
|
|
237
321
|
isError = true;
|
|
@@ -257,15 +341,38 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
|
|
|
257
341
|
};
|
|
258
342
|
}
|
|
259
343
|
}
|
|
260
|
-
|
|
344
|
+
let rendered = result.content
|
|
261
345
|
.map((content) => content.type === "text" ? content.text : `[image: ${content.mimeType}]`)
|
|
262
346
|
.join("\n");
|
|
347
|
+
const after = await dispatchRuntimeHook(isError ? "tool:error" : "tool:after", {
|
|
348
|
+
tool: toolCall.name,
|
|
349
|
+
input: args,
|
|
350
|
+
result: rendered,
|
|
351
|
+
}, { toolCallId: toolCall.id });
|
|
352
|
+
const afterError = decisionError(after);
|
|
353
|
+
if (afterError) {
|
|
354
|
+
isError = true;
|
|
355
|
+
executionError = afterError;
|
|
356
|
+
rendered = afterError.message;
|
|
357
|
+
result = { content: [{ type: "text", text: rendered }], details: {} };
|
|
358
|
+
}
|
|
359
|
+
else if (after.action === "modify" && typeof after.data.result === "string") {
|
|
360
|
+
rendered = after.data.result;
|
|
361
|
+
result = { ...result, content: [{ type: "text", text: rendered }] };
|
|
362
|
+
}
|
|
363
|
+
if (!isError) {
|
|
364
|
+
const fileChange = runtimeHookFileChangeForTool(toolCall.name, args, rendered);
|
|
365
|
+
if (fileChange) {
|
|
366
|
+
await dispatchRuntimeHook("sandbox:file_changed", fileChange, { toolCallId: toolCall.id });
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return rendered;
|
|
263
370
|
};
|
|
264
371
|
const llmText = ctx?.runToolMiddleware
|
|
265
372
|
? (await ctx.runToolMiddleware.execute({
|
|
266
373
|
callId: toolCall.id,
|
|
267
374
|
name: toolCall.name,
|
|
268
|
-
args:
|
|
375
|
+
args: effectiveArgs,
|
|
269
376
|
schema: pt?.parameters,
|
|
270
377
|
context: {
|
|
271
378
|
agent: agentConfig.name,
|
|
@@ -276,7 +383,7 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
|
|
|
276
383
|
},
|
|
277
384
|
signal: abortController.signal,
|
|
278
385
|
}, (request) => dispatch(request.args))).output
|
|
279
|
-
: await dispatch(
|
|
386
|
+
: await dispatch(effectiveArgs);
|
|
280
387
|
activity.lastUpdate = new Date().toISOString();
|
|
281
388
|
// Track file operations from tool details
|
|
282
389
|
const details = result.details;
|
|
@@ -327,9 +434,42 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
|
|
|
327
434
|
async function runLoopSession(options) {
|
|
328
435
|
const { sessionAgent, loopName, loop, contextPrompt } = options;
|
|
329
436
|
let sessionCleanup;
|
|
437
|
+
let agentHookStarted = false;
|
|
438
|
+
let sandboxHookStarted = false;
|
|
330
439
|
try {
|
|
331
440
|
const resume = usableResumeState(options.resume, loopName);
|
|
332
441
|
const inject = ctx?.inject;
|
|
442
|
+
await dispatchRuntimeHook("run:start", { loop: loopName, resumed: Boolean(resume) }, { agent: sessionAgent.name, loop: loopName });
|
|
443
|
+
await dispatchRuntimeHook(resume ? "session:resume" : "session:start", {
|
|
444
|
+
loop: loopName,
|
|
445
|
+
resumed: Boolean(resume),
|
|
446
|
+
}, { agent: sessionAgent.name, loop: loopName });
|
|
447
|
+
const spawnDecision = await dispatchRuntimeHook("agent:spawn", {
|
|
448
|
+
loop: loopName,
|
|
449
|
+
source: inject ? "chat-run" : "task-run",
|
|
450
|
+
}, { agent: sessionAgent.name, loop: loopName });
|
|
451
|
+
const spawnError = decisionError(spawnDecision);
|
|
452
|
+
if (spawnError)
|
|
453
|
+
throw spawnError;
|
|
454
|
+
agentHookStarted = true;
|
|
455
|
+
if (ctx?.fs || ctx?.shell) {
|
|
456
|
+
const sandboxDecision = await dispatchRuntimeHook("sandbox:start", {
|
|
457
|
+
cwd,
|
|
458
|
+
filesystem: Boolean(ctx.fs),
|
|
459
|
+
shell: Boolean(ctx.shell),
|
|
460
|
+
}, { agent: sessionAgent.name, loop: loopName });
|
|
461
|
+
const sandboxError = decisionError(sandboxDecision);
|
|
462
|
+
if (sandboxError)
|
|
463
|
+
throw sandboxError;
|
|
464
|
+
sandboxHookStarted = true;
|
|
465
|
+
}
|
|
466
|
+
const inputDecision = await dispatchRuntimeHook("input:before", {
|
|
467
|
+
loop: loopName,
|
|
468
|
+
hasProjectedInput: Boolean(options.initialUserMessage),
|
|
469
|
+
}, { agent: sessionAgent.name, loop: loopName });
|
|
470
|
+
const inputError = decisionError(inputDecision);
|
|
471
|
+
if (inputError)
|
|
472
|
+
throw inputError;
|
|
333
473
|
// Inject path (chat-via-executeRun, F1c): use the completions route's
|
|
334
474
|
// already-resolved model/prompt/tools/messages instead of re-resolving from
|
|
335
475
|
// the AgentConfig — parity by construction. Every branch below is
|
|
@@ -371,6 +511,9 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
|
|
|
371
511
|
toolSet = toToolDeclarations(allPolpoTools);
|
|
372
512
|
compactionTools = allPolpoTools.map((t) => ({ description: t.description ?? "" }));
|
|
373
513
|
}
|
|
514
|
+
if (inputDecision.action === "modify" && typeof inputDecision.data.context === "string" && inputDecision.data.context.trim()) {
|
|
515
|
+
systemPrompt += `\n\n## Runtime hook context\n\n${inputDecision.data.context.trim()}`;
|
|
516
|
+
}
|
|
374
517
|
const primaryResolved = { model, providerOptions };
|
|
375
518
|
const modelSelection = inject?.modelSelection ??
|
|
376
519
|
resolvedModelSelection ??
|
|
@@ -441,7 +584,7 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
|
|
|
441
584
|
const isRecovery = attemptIndex === 1;
|
|
442
585
|
const resolvedAttempts = new Map();
|
|
443
586
|
try {
|
|
444
|
-
const terminal = await runModelPolicyTurn({
|
|
587
|
+
const terminal = await runHookedModel({ phase: "terminal", recovery: isRecovery, model: model.id ?? model.provider }, () => runModelPolicyTurn({
|
|
445
588
|
selection: modelSelection,
|
|
446
589
|
resolveAttempt: async (attempt) => {
|
|
447
590
|
const resolvedAttempt = attempt.index === 0
|
|
@@ -461,13 +604,17 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
|
|
|
461
604
|
};
|
|
462
605
|
},
|
|
463
606
|
preserveSingleAttemptError: true,
|
|
607
|
+
onPolicyEvent: (event) => dispatchModelPolicyHook(event, {
|
|
608
|
+
agent: sessionAgent.name,
|
|
609
|
+
loop: loopName,
|
|
610
|
+
}),
|
|
464
611
|
allowCommittedFallback: true,
|
|
465
612
|
maxRecoverableStreamRetries: 0,
|
|
466
613
|
system: systemPrompt,
|
|
467
614
|
messages: terminalMessages(isRecovery),
|
|
468
615
|
output: modelOutput,
|
|
469
616
|
abortSignal: abortController.signal,
|
|
470
|
-
});
|
|
617
|
+
}), (result) => ({ finishReason: result.finishReason, toolCallCount: result.toolCalls?.length ?? 0 }));
|
|
471
618
|
if (terminal.toolCalls.length > 0 || terminal.output === undefined) {
|
|
472
619
|
throw new Error(terminal.toolCalls.length > 0
|
|
473
620
|
? "Terminal structured-output phase returned a tool call"
|
|
@@ -551,6 +698,10 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
|
|
|
551
698
|
}
|
|
552
699
|
// Context compaction — same call, same config, same quirks as the
|
|
553
700
|
// legacy engine (see engine-behavior characterization tests).
|
|
701
|
+
await dispatchRuntimeHook("context:compact:before", {
|
|
702
|
+
messageCount: messages.length,
|
|
703
|
+
model: model.id ?? model.provider,
|
|
704
|
+
});
|
|
554
705
|
const compactionResult = await compactIfNeeded({
|
|
555
706
|
systemPrompt,
|
|
556
707
|
messages,
|
|
@@ -577,6 +728,10 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
|
|
|
577
728
|
});
|
|
578
729
|
if (compactionResult.compacted) {
|
|
579
730
|
messages = normalizeResponseMessagesForHistory(compactionResult.messages);
|
|
731
|
+
await dispatchRuntimeHook("context:compact:after", {
|
|
732
|
+
messageCount: messages.length,
|
|
733
|
+
compacted: true,
|
|
734
|
+
});
|
|
580
735
|
}
|
|
581
736
|
messages = normalizeResponseMessagesForHistory(messages);
|
|
582
737
|
if (contextTrust === "enforce") {
|
|
@@ -588,7 +743,7 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
|
|
|
588
743
|
const resolvedAttempts = new Map();
|
|
589
744
|
let turn;
|
|
590
745
|
try {
|
|
591
|
-
turn = await runModelPolicyTurn({
|
|
746
|
+
turn = await runHookedModel({ phase: "operational", model: model.id ?? model.provider }, () => runModelPolicyTurn({
|
|
592
747
|
selection: modelSelection,
|
|
593
748
|
resolveAttempt: async (attempt) => {
|
|
594
749
|
const resolvedAttempt = attempt.index === 0
|
|
@@ -608,6 +763,10 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
|
|
|
608
763
|
};
|
|
609
764
|
},
|
|
610
765
|
preserveSingleAttemptError: true,
|
|
766
|
+
onPolicyEvent: (event) => dispatchModelPolicyHook(event, {
|
|
767
|
+
agent: sessionAgent.name,
|
|
768
|
+
loop: loopName,
|
|
769
|
+
}),
|
|
611
770
|
system: systemPrompt,
|
|
612
771
|
messages,
|
|
613
772
|
tools: toolSet,
|
|
@@ -768,7 +927,7 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
|
|
|
768
927
|
break;
|
|
769
928
|
}
|
|
770
929
|
}
|
|
771
|
-
});
|
|
930
|
+
}), (result) => ({ finishReason: result.finishReason, toolCallCount: result.toolCalls?.length ?? 0 }));
|
|
772
931
|
}
|
|
773
932
|
catch (error) {
|
|
774
933
|
if (outputSchema !== undefined && isStructuredModelOutputError(error)) {
|
|
@@ -919,7 +1078,7 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
|
|
|
919
1078
|
model: modelStep,
|
|
920
1079
|
executeTool,
|
|
921
1080
|
parallelToolCalls: inject?.parallelToolCalls,
|
|
922
|
-
onToolResults: (toolResults) => {
|
|
1081
|
+
onToolResults: async (toolResults) => {
|
|
923
1082
|
for (const { toolCall, result, isError } of toolResults) {
|
|
924
1083
|
// AI SDK already persisted provider-executed results and invalid
|
|
925
1084
|
// argument errors in responseMessages. Only local results belong
|
|
@@ -942,6 +1101,18 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
|
|
|
942
1101
|
}],
|
|
943
1102
|
});
|
|
944
1103
|
}
|
|
1104
|
+
if (toolResults.length > 0) {
|
|
1105
|
+
await dispatchRuntimeHook("tool:batch:after", {
|
|
1106
|
+
count: toolResults.length,
|
|
1107
|
+
failed: toolResults.filter((result) => result.isError).length,
|
|
1108
|
+
tools: toolResults.map(({ toolCall, isError, skipped }) => ({
|
|
1109
|
+
id: toolCall.id,
|
|
1110
|
+
name: toolCall.name,
|
|
1111
|
+
isError,
|
|
1112
|
+
skipped,
|
|
1113
|
+
})),
|
|
1114
|
+
}, { agent: sessionAgent.name, loop: loopName });
|
|
1115
|
+
}
|
|
945
1116
|
},
|
|
946
1117
|
steering,
|
|
947
1118
|
onSteering: async (steeringMessages) => {
|
|
@@ -972,9 +1143,118 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
|
|
|
972
1143
|
if (outputSchema !== undefined && !completedStructuredOutput && !interruptedForClientTool) {
|
|
973
1144
|
throw new LoopAgentOutputError("loop_agent_output_invalid", loopName, `Agent step ${JSON.stringify(loopName)} exhausted maxTurns before producing structured output`, { phase: "max_turns" });
|
|
974
1145
|
}
|
|
1146
|
+
const stopDecision = await dispatchRuntimeHook("turn:stop", {
|
|
1147
|
+
loop: loopName,
|
|
1148
|
+
textLength: lastStepText.length,
|
|
1149
|
+
}, { agent: sessionAgent.name, loop: loopName });
|
|
1150
|
+
const stopError = decisionError(stopDecision);
|
|
1151
|
+
if (stopError && stopDecision.action !== "stop")
|
|
1152
|
+
throw stopError;
|
|
1153
|
+
const stopFeedback = stopDecision.action === "stop" ? stopDecision.feedback?.trim() : undefined;
|
|
1154
|
+
if (stopFeedback && !hasStructuredOutput && !abortController.signal.aborted) {
|
|
1155
|
+
const resolvedAttempts = new Map();
|
|
1156
|
+
const retry = await runHookedModel({ phase: "stop-feedback", retry: 1, model: model.id ?? model.provider }, () => runModelPolicyTurn({
|
|
1157
|
+
selection: modelSelection,
|
|
1158
|
+
resolveAttempt: async (attempt) => {
|
|
1159
|
+
const resolvedAttempt = attempt.index === 0
|
|
1160
|
+
? primaryResolved
|
|
1161
|
+
: inject
|
|
1162
|
+
? await inject.resolveModelAttempt?.(attempt.model)
|
|
1163
|
+
: resolveSpawnModelAttempt(sessionAgent, attempt.model, ctx);
|
|
1164
|
+
if (!resolvedAttempt)
|
|
1165
|
+
throw new Error(`No model resolver is available for fallback "${attempt.model}"`);
|
|
1166
|
+
resolvedAttempts.set(attempt.index, resolvedAttempt);
|
|
1167
|
+
return {
|
|
1168
|
+
model: resolvedAttempt.model.aiModel,
|
|
1169
|
+
maxOutputTokens: resolvedAttempt.model.maxTokens,
|
|
1170
|
+
providerOptions: resolvedAttempt.providerOptions,
|
|
1171
|
+
capabilities: resolvedAttempt.model.capabilities,
|
|
1172
|
+
};
|
|
1173
|
+
},
|
|
1174
|
+
preserveSingleAttemptError: true,
|
|
1175
|
+
onPolicyEvent: (event) => dispatchModelPolicyHook(event, {
|
|
1176
|
+
agent: sessionAgent.name,
|
|
1177
|
+
loop: loopName,
|
|
1178
|
+
}),
|
|
1179
|
+
system: systemPrompt,
|
|
1180
|
+
messages: [
|
|
1181
|
+
...messages,
|
|
1182
|
+
{
|
|
1183
|
+
role: "user",
|
|
1184
|
+
content: `A trusted runtime hook rejected ending the turn and returned this feedback:\n\n${stopFeedback}\n\nContinue once and return the corrected final response. Do not repeat completed side effects.`,
|
|
1185
|
+
},
|
|
1186
|
+
],
|
|
1187
|
+
tools: {},
|
|
1188
|
+
toolChoice: "none",
|
|
1189
|
+
abortSignal: abortController.signal,
|
|
1190
|
+
}), (result) => ({ finishReason: result.finishReason, toolCallCount: result.toolCalls?.length ?? 0 }));
|
|
1191
|
+
if (retry.toolCalls.length > 0)
|
|
1192
|
+
throw new Error("Stop-feedback recovery returned a tool call");
|
|
1193
|
+
const selectedResolved = resolvedAttempts.get(retry.selectedAttempt.index);
|
|
1194
|
+
if (selectedResolved) {
|
|
1195
|
+
model = selectedResolved.model;
|
|
1196
|
+
providerOptions = selectedResolved.providerOptions;
|
|
1197
|
+
}
|
|
1198
|
+
let retryText = retry.text;
|
|
1199
|
+
if (retryText && ctx?.runOutputPolicy) {
|
|
1200
|
+
retryText = (await ctx.runOutputPolicy.evaluate({
|
|
1201
|
+
output: retryText,
|
|
1202
|
+
mode: ctx.runOutputPolicyMode ?? "enforce",
|
|
1203
|
+
context: { agent: sessionAgent.name, runId: ctx.runId, source: "task", surface: "task" },
|
|
1204
|
+
signal: abortController.signal,
|
|
1205
|
+
})).output;
|
|
1206
|
+
}
|
|
1207
|
+
if (retryText) {
|
|
1208
|
+
try {
|
|
1209
|
+
ctx?.onDelta?.({ text: `${lastStepText ? "\n\n" : ""}${retryText}` });
|
|
1210
|
+
}
|
|
1211
|
+
catch { /* subscriber cannot sink the run */ }
|
|
1212
|
+
emitTranscript({ type: "assistant", text: retryText });
|
|
1213
|
+
accumText += retryText;
|
|
1214
|
+
lastStepText = `${lastStepText ? `${lastStepText}\n\n` : ""}${retryText}`;
|
|
1215
|
+
}
|
|
1216
|
+
if (inject)
|
|
1217
|
+
emitTranscript({ type: "usage", usage: retry.usage, providerMetadata: retry.providerMetadata });
|
|
1218
|
+
const retryStop = await dispatchRuntimeHook("turn:stop", {
|
|
1219
|
+
loop: loopName,
|
|
1220
|
+
textLength: lastStepText.length,
|
|
1221
|
+
stopFeedbackRetry: 1,
|
|
1222
|
+
}, { agent: sessionAgent.name, loop: loopName });
|
|
1223
|
+
const retryStopError = decisionError(retryStop);
|
|
1224
|
+
if (retryStopError && retryStop.action !== "stop")
|
|
1225
|
+
throw retryStopError;
|
|
1226
|
+
}
|
|
975
1227
|
return { lastText: lastStepText, accumText };
|
|
976
1228
|
}
|
|
1229
|
+
catch (error) {
|
|
1230
|
+
await dispatchRuntimeHook("turn:error", {
|
|
1231
|
+
loop: loopName,
|
|
1232
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1233
|
+
}, { agent: sessionAgent.name, loop: loopName }).catch(() => { });
|
|
1234
|
+
throw error;
|
|
1235
|
+
}
|
|
977
1236
|
finally {
|
|
1237
|
+
if (sandboxHookStarted) {
|
|
1238
|
+
await dispatchRuntimeHook("sandbox:end", {
|
|
1239
|
+
cwd,
|
|
1240
|
+
aborted: abortController.signal.aborted,
|
|
1241
|
+
}, { agent: sessionAgent.name, loop: loopName }).catch(() => { });
|
|
1242
|
+
}
|
|
1243
|
+
if (agentHookStarted) {
|
|
1244
|
+
await dispatchRuntimeHook("agent:end", {
|
|
1245
|
+
loop: loopName,
|
|
1246
|
+
source: ctx?.inject ? "chat-run" : "task-run",
|
|
1247
|
+
aborted: abortController.signal.aborted,
|
|
1248
|
+
}, { agent: sessionAgent.name, loop: loopName }).catch(() => { });
|
|
1249
|
+
}
|
|
1250
|
+
await dispatchRuntimeHook("session:end", {
|
|
1251
|
+
loop: loopName,
|
|
1252
|
+
aborted: abortController.signal.aborted,
|
|
1253
|
+
}, { agent: sessionAgent.name, loop: loopName }).catch(() => { });
|
|
1254
|
+
await dispatchRuntimeHook("run:end", {
|
|
1255
|
+
loop: loopName,
|
|
1256
|
+
aborted: abortController.signal.aborted,
|
|
1257
|
+
}, { agent: sessionAgent.name, loop: loopName }).catch(() => { });
|
|
978
1258
|
if (sessionCleanup) {
|
|
979
1259
|
await sessionCleanup().catch(() => undefined);
|
|
980
1260
|
toolCleanups.delete(sessionCleanup);
|