@polpo-ai/node 0.15.143 → 0.16.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/dist/adapters/composite-spawner.d.ts +1 -0
- package/dist/adapters/composite-spawner.d.ts.map +1 -1
- package/dist/adapters/composite-spawner.js +3 -0
- package/dist/adapters/composite-spawner.js.map +1 -1
- package/dist/adapters/in-process-spawner.d.ts +10 -2
- package/dist/adapters/in-process-spawner.d.ts.map +1 -1
- package/dist/adapters/in-process-spawner.js +9 -7
- package/dist/adapters/in-process-spawner.js.map +1 -1
- package/dist/adapters/loop-engine.d.ts.map +1 -1
- package/dist/adapters/loop-engine.js +510 -491
- package/dist/adapters/loop-engine.js.map +1 -1
- package/dist/adapters/node-spawner.d.ts +2 -0
- package/dist/adapters/node-spawner.d.ts.map +1 -1
- package/dist/adapters/node-spawner.js +15 -0
- package/dist/adapters/node-spawner.js.map +1 -1
- package/dist/adapters/spawn-helpers.d.ts +8 -2
- package/dist/adapters/spawn-helpers.d.ts.map +1 -1
- package/dist/adapters/spawn-helpers.js +33 -13
- package/dist/adapters/spawn-helpers.js.map +1 -1
- package/dist/core/deadlock-resolver.d.ts +8 -10
- package/dist/core/deadlock-resolver.d.ts.map +1 -1
- package/dist/core/deadlock-resolver.js +125 -132
- package/dist/core/deadlock-resolver.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/orchestrator.d.ts +8 -4
- package/dist/core/orchestrator.d.ts.map +1 -1
- package/dist/core/orchestrator.js +43 -79
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/run-lifecycle.d.ts +11 -1
- package/dist/core/run-lifecycle.d.ts.map +1 -1
- package/dist/core/run-lifecycle.js +197 -44
- package/dist/core/run-lifecycle.js.map +1 -1
- package/dist/core/runner-capability-bootstrap.d.ts +19 -0
- package/dist/core/runner-capability-bootstrap.d.ts.map +1 -0
- package/dist/core/runner-capability-bootstrap.js +98 -0
- package/dist/core/runner-capability-bootstrap.js.map +1 -0
- package/dist/core/runner.js +10 -0
- package/dist/core/runner.js.map +1 -1
- package/dist/custom-tools/runtime.d.ts +1 -0
- package/dist/custom-tools/runtime.d.ts.map +1 -1
- package/dist/custom-tools/runtime.js +1 -0
- package/dist/custom-tools/runtime.js.map +1 -1
- package/dist/data/index.d.ts +17 -0
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/index.js +125 -0
- package/dist/data/index.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/sandbox/lease.d.ts +7 -0
- package/dist/sandbox/lease.d.ts.map +1 -1
- package/dist/sandbox/lease.js +35 -5
- package/dist/sandbox/lease.js.map +1 -1
- package/dist/server/app.d.ts +6 -1
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/app.js +39 -14
- package/dist/server/app.js.map +1 -1
- package/dist/server/deps.d.ts +5 -4
- package/dist/server/deps.d.ts.map +1 -1
- package/dist/server/deps.js +0 -7
- package/dist/server/deps.js.map +1 -1
- package/dist/server/ephemeral-run-store.d.ts +2 -1
- package/dist/server/ephemeral-run-store.d.ts.map +1 -1
- package/dist/server/ephemeral-run-store.js +14 -1
- package/dist/server/ephemeral-run-store.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +6 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/types.d.ts +3 -0
- package/dist/server/types.d.ts.map +1 -1
- package/package.json +9 -9
|
@@ -188,6 +188,7 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
|
|
|
188
188
|
const start = Date.now();
|
|
189
189
|
let alive = true;
|
|
190
190
|
const abortController = new AbortController();
|
|
191
|
+
const toolCleanups = new Set();
|
|
191
192
|
const steering = ctx?.steering
|
|
192
193
|
?? (ctx?.resumeState?.steering
|
|
193
194
|
? InMemorySteeringController.fromSnapshot(ctx.resumeState.steering)
|
|
@@ -321,549 +322,562 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
|
|
|
321
322
|
*/
|
|
322
323
|
async function runLoopSession(options) {
|
|
323
324
|
const { sessionAgent, loopName, loop, contextPrompt } = options;
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const prep = prepareSpawn(sessionAgent, cwd, ctx);
|
|
351
|
-
model = prep.model;
|
|
352
|
-
resolvedModelSelection = prep.modelSelection;
|
|
353
|
-
providerOptions = prep.providerOptions;
|
|
354
|
-
systemPrompt = contextPrompt
|
|
355
|
-
? `${prep.systemPrompt}\n\n${contextPrompt}`
|
|
356
|
-
: prep.systemPrompt;
|
|
357
|
-
maxTurns = loop.maxTurns ?? prep.maxTurns;
|
|
358
|
-
const allPolpoTools = await buildAgentTools(sessionAgent, cwd, prep, ctx);
|
|
359
|
-
toolByName = new Map(allPolpoTools.map((t) => [t.name, t]));
|
|
360
|
-
toolSet = toToolDeclarations(allPolpoTools);
|
|
361
|
-
compactionTools = allPolpoTools.map((t) => ({ description: t.description ?? "" }));
|
|
362
|
-
}
|
|
363
|
-
const primaryResolved = { model, providerOptions };
|
|
364
|
-
const modelSelection = inject?.modelSelection ??
|
|
365
|
-
resolvedModelSelection ??
|
|
366
|
-
modelSelectionForResolvedModel(model);
|
|
367
|
-
const contextTrust = inject?.contextTrust ?? ctx?.contextTrust ?? "off";
|
|
368
|
-
const outputSchema = loop.output?.schema;
|
|
369
|
-
const modelOutput = inject?.output ?? (outputSchema !== undefined
|
|
370
|
-
? modelOutputForJsonSchema(outputSchema, `loop_${loopName}_output`)
|
|
371
|
-
: undefined);
|
|
372
|
-
const hasStructuredOutput = modelOutput !== undefined;
|
|
373
|
-
// Conversation state owned by the host, exactly like the legacy loop.
|
|
374
|
-
// On resume the recorded history (already containing tool-call and
|
|
375
|
-
// tool-result pairs from completed turns) replaces the fresh prompt —
|
|
376
|
-
// side-effects replay from recorded results, they never re-execute.
|
|
377
|
-
let messages = normalizeResponseMessagesForHistory(inject
|
|
378
|
-
? [...inject.seedMessages]
|
|
379
|
-
: resume
|
|
380
|
-
? [...resume.history]
|
|
381
|
-
: [{
|
|
382
|
-
role: "user",
|
|
383
|
-
content: options.initialUserMessage ?? buildPrompt(task, ctx?.promptContextSegments, ctx?.contextTrust),
|
|
384
|
-
}]);
|
|
385
|
-
if (contextTrust === "enforce") {
|
|
386
|
-
messages = protectRuntimeToolResultMessages(messages);
|
|
387
|
-
}
|
|
388
|
-
let lastStepText = "";
|
|
389
|
-
let accumText = resume?.accumText ?? "";
|
|
390
|
-
let completedStructuredOutput = false;
|
|
391
|
-
const providerToolResults = new Map();
|
|
392
|
-
const renderProviderResult = (value) => {
|
|
393
|
-
if (typeof value === "string")
|
|
394
|
-
return value;
|
|
395
|
-
if (value === undefined)
|
|
396
|
-
return "";
|
|
397
|
-
try {
|
|
398
|
-
return JSON.stringify(value);
|
|
399
|
-
}
|
|
400
|
-
catch {
|
|
401
|
-
return String(value);
|
|
402
|
-
}
|
|
403
|
-
};
|
|
404
|
-
const summarize = async (msgs, compactionPrompt) => {
|
|
405
|
-
const response = await generateText({
|
|
406
|
-
model: model.aiModel,
|
|
407
|
-
system: compactionPrompt,
|
|
408
|
-
messages: prepareModelMessagesForTransport(msgs, model.aiModel),
|
|
409
|
-
maxOutputTokens: model.maxTokens,
|
|
410
|
-
abortSignal: abortController.signal,
|
|
411
|
-
providerOptions,
|
|
412
|
-
});
|
|
413
|
-
return response.text;
|
|
414
|
-
};
|
|
415
|
-
// ── LoopRunner model callback: one model turn per runner step ──
|
|
416
|
-
const modelStep = async () => {
|
|
417
|
-
// Abort between turns mirrors the legacy `if (aborted) break`:
|
|
418
|
-
// report no tool calls so the runner stops cleanly (exit 0).
|
|
419
|
-
if (abortController.signal.aborted) {
|
|
420
|
-
return { text: "", toolCalls: [] };
|
|
325
|
+
let sessionCleanup;
|
|
326
|
+
try {
|
|
327
|
+
const resume = usableResumeState(options.resume, loopName);
|
|
328
|
+
const inject = ctx?.inject;
|
|
329
|
+
// Inject path (chat-via-executeRun, F1c): use the completions route's
|
|
330
|
+
// already-resolved model/prompt/tools/messages instead of re-resolving from
|
|
331
|
+
// the AgentConfig — parity by construction. Every branch below is
|
|
332
|
+
// inject-gated so the task path stays byte-identical.
|
|
333
|
+
let model;
|
|
334
|
+
let providerOptions;
|
|
335
|
+
let systemPrompt;
|
|
336
|
+
let maxTurns;
|
|
337
|
+
let toolSet;
|
|
338
|
+
let toolByName = new Map();
|
|
339
|
+
// Tools value handed to compaction's token estimator (JSON.stringify'd); MUST
|
|
340
|
+
// match the chat path's shape so the compaction trigger point is identical.
|
|
341
|
+
let compactionTools;
|
|
342
|
+
let resolvedModelSelection;
|
|
343
|
+
const compactionMode = inject?.compactionMode ?? "task";
|
|
344
|
+
if (inject) {
|
|
345
|
+
model = inject.model;
|
|
346
|
+
providerOptions = inject.providerOptions;
|
|
347
|
+
systemPrompt = inject.systemPrompt;
|
|
348
|
+
maxTurns = inject.maxTurns;
|
|
349
|
+
toolSet = inject.toolSet;
|
|
350
|
+
compactionTools = inject.compactionTools;
|
|
421
351
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
tokensAfter: event.tokensAfter,
|
|
440
|
-
tokensReclaimed: event.tokensReclaimed,
|
|
441
|
-
messagesBefore: event.messagesBefore,
|
|
442
|
-
messagesAfter: event.messagesAfter,
|
|
443
|
-
toolOutputsPruned: event.toolOutputsPruned,
|
|
444
|
-
summary: event.summary,
|
|
445
|
-
});
|
|
446
|
-
},
|
|
447
|
-
});
|
|
448
|
-
if (compactionResult.compacted) {
|
|
449
|
-
messages = normalizeResponseMessagesForHistory(compactionResult.messages);
|
|
352
|
+
else {
|
|
353
|
+
const prep = prepareSpawn(sessionAgent, cwd, ctx);
|
|
354
|
+
model = prep.model;
|
|
355
|
+
resolvedModelSelection = prep.modelSelection;
|
|
356
|
+
providerOptions = prep.providerOptions;
|
|
357
|
+
systemPrompt = contextPrompt
|
|
358
|
+
? `${prep.systemPrompt}\n\n${contextPrompt}`
|
|
359
|
+
: prep.systemPrompt;
|
|
360
|
+
maxTurns = loop.maxTurns ?? prep.maxTurns;
|
|
361
|
+
const resolved = await buildAgentTools(sessionAgent, cwd, prep, ctx, { signal: abortController.signal,
|
|
362
|
+
policy: { loop: options.projectAllowedTools, step: loop.allowedTools ?? loop.tools } });
|
|
363
|
+
toolCleanups.add(resolved.cleanup);
|
|
364
|
+
sessionCleanup = resolved.cleanup;
|
|
365
|
+
const allPolpoTools = resolved.tools;
|
|
366
|
+
toolByName = new Map(allPolpoTools.map((t) => [t.name, t]));
|
|
367
|
+
toolSet = toToolDeclarations(allPolpoTools);
|
|
368
|
+
compactionTools = allPolpoTools.map((t) => ({ description: t.description ?? "" }));
|
|
450
369
|
}
|
|
451
|
-
|
|
370
|
+
const primaryResolved = { model, providerOptions };
|
|
371
|
+
const modelSelection = inject?.modelSelection ??
|
|
372
|
+
resolvedModelSelection ??
|
|
373
|
+
modelSelectionForResolvedModel(model);
|
|
374
|
+
const contextTrust = inject?.contextTrust ?? ctx?.contextTrust ?? "off";
|
|
375
|
+
const outputSchema = loop.output?.schema;
|
|
376
|
+
const modelOutput = inject?.output ?? (outputSchema !== undefined
|
|
377
|
+
? modelOutputForJsonSchema(outputSchema, `loop_${loopName}_output`)
|
|
378
|
+
: undefined);
|
|
379
|
+
const hasStructuredOutput = modelOutput !== undefined;
|
|
380
|
+
// Conversation state owned by the host, exactly like the legacy loop.
|
|
381
|
+
// On resume the recorded history (already containing tool-call and
|
|
382
|
+
// tool-result pairs from completed turns) replaces the fresh prompt —
|
|
383
|
+
// side-effects replay from recorded results, they never re-execute.
|
|
384
|
+
let messages = normalizeResponseMessagesForHistory(inject
|
|
385
|
+
? [...inject.seedMessages]
|
|
386
|
+
: resume
|
|
387
|
+
? [...resume.history]
|
|
388
|
+
: [{
|
|
389
|
+
role: "user",
|
|
390
|
+
content: options.initialUserMessage ?? buildPrompt(task, ctx?.promptContextSegments, ctx?.contextTrust),
|
|
391
|
+
}]);
|
|
452
392
|
if (contextTrust === "enforce") {
|
|
453
393
|
messages = protectRuntimeToolResultMessages(messages);
|
|
454
394
|
}
|
|
455
|
-
let
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
resolvedAttempts.set(attempt.index, resolvedAttempt);
|
|
478
|
-
return {
|
|
479
|
-
model: resolvedAttempt.model.aiModel,
|
|
480
|
-
maxOutputTokens: resolvedAttempt.model.maxTokens,
|
|
481
|
-
providerOptions: resolvedAttempt.providerOptions,
|
|
482
|
-
};
|
|
483
|
-
},
|
|
484
|
-
preserveSingleAttemptError: true,
|
|
485
|
-
system: systemPrompt,
|
|
486
|
-
messages,
|
|
487
|
-
tools: toolSet,
|
|
488
|
-
...(inject?.activeToolNames ? { activeTools: inject.activeToolNames() } : {}),
|
|
489
|
-
...(inject?.toolChoice ? { toolChoice: inject.toolChoice } : {}),
|
|
490
|
-
...(inject?.parallelToolCalls !== undefined
|
|
491
|
-
? { parallelToolCalls: inject.parallelToolCalls }
|
|
492
|
-
: {}),
|
|
493
|
-
...(hasStructuredOutput ? { output: modelOutput } : {}),
|
|
395
|
+
let lastStepText = "";
|
|
396
|
+
let accumText = resume?.accumText ?? "";
|
|
397
|
+
let completedStructuredOutput = false;
|
|
398
|
+
const providerToolResults = new Map();
|
|
399
|
+
const renderProviderResult = (value) => {
|
|
400
|
+
if (typeof value === "string")
|
|
401
|
+
return value;
|
|
402
|
+
if (value === undefined)
|
|
403
|
+
return "";
|
|
404
|
+
try {
|
|
405
|
+
return JSON.stringify(value);
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
return String(value);
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
const summarize = async (msgs, compactionPrompt) => {
|
|
412
|
+
const response = await generateText({
|
|
413
|
+
model: model.aiModel,
|
|
414
|
+
system: compactionPrompt,
|
|
415
|
+
messages: prepareModelMessagesForTransport(msgs, model.aiModel),
|
|
416
|
+
maxOutputTokens: model.maxTokens,
|
|
494
417
|
abortSignal: abortController.signal,
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
418
|
+
providerOptions,
|
|
419
|
+
});
|
|
420
|
+
return response.text;
|
|
421
|
+
};
|
|
422
|
+
// ── LoopRunner model callback: one model turn per runner step ──
|
|
423
|
+
const modelStep = async () => {
|
|
424
|
+
// Abort between turns mirrors the legacy `if (aborted) break`:
|
|
425
|
+
// report no tool calls so the runner stops cleanly (exit 0).
|
|
426
|
+
if (abortController.signal.aborted) {
|
|
427
|
+
return { text: "", toolCalls: [] };
|
|
428
|
+
}
|
|
429
|
+
// Context compaction — same call, same config, same quirks as the
|
|
430
|
+
// legacy engine (see engine-behavior characterization tests).
|
|
431
|
+
const compactionResult = await compactIfNeeded({
|
|
432
|
+
systemPrompt,
|
|
433
|
+
messages,
|
|
434
|
+
tools: (inject?.activeCompactionTools?.() ?? compactionTools),
|
|
435
|
+
config: {
|
|
436
|
+
contextWindow: model.contextWindow ?? 200_000,
|
|
437
|
+
maxOutputTokens: model.maxTokens ?? 8192,
|
|
438
|
+
},
|
|
439
|
+
summarize,
|
|
440
|
+
mode: compactionMode,
|
|
441
|
+
onCompaction: (event) => {
|
|
442
|
+
emitTranscript({
|
|
443
|
+
type: "compaction",
|
|
444
|
+
phase: event.phase,
|
|
445
|
+
tokensBefore: event.tokensBefore,
|
|
446
|
+
tokensAfter: event.tokensAfter,
|
|
447
|
+
tokensReclaimed: event.tokensReclaimed,
|
|
448
|
+
messagesBefore: event.messagesBefore,
|
|
449
|
+
messagesAfter: event.messagesAfter,
|
|
450
|
+
toolOutputsPruned: event.toolOutputsPruned,
|
|
451
|
+
summary: event.summary,
|
|
452
|
+
});
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
if (compactionResult.compacted) {
|
|
456
|
+
messages = normalizeResponseMessagesForHistory(compactionResult.messages);
|
|
457
|
+
}
|
|
458
|
+
messages = normalizeResponseMessagesForHistory(messages);
|
|
459
|
+
if (contextTrust === "enforce") {
|
|
460
|
+
messages = protectRuntimeToolResultMessages(messages);
|
|
461
|
+
}
|
|
462
|
+
let stepText = "";
|
|
463
|
+
// Chat injections stream a structured answer's raw JSON as it arrives;
|
|
464
|
+
// the validated serialization still follows once the step completes.
|
|
465
|
+
const structuredGate = inject && hasStructuredOutput
|
|
466
|
+
? new StructuredOutputDeltaGate()
|
|
467
|
+
: undefined;
|
|
468
|
+
const toolCalls = [];
|
|
469
|
+
const seenModelToolCallIds = new Set();
|
|
470
|
+
const resolvedAttempts = new Map();
|
|
471
|
+
let turn;
|
|
472
|
+
try {
|
|
473
|
+
turn = await runModelPolicyTurn({
|
|
474
|
+
selection: modelSelection,
|
|
475
|
+
resolveAttempt: async (attempt) => {
|
|
476
|
+
const resolvedAttempt = attempt.index === 0
|
|
477
|
+
? primaryResolved
|
|
478
|
+
: inject
|
|
479
|
+
? await inject.resolveModelAttempt?.(attempt.model)
|
|
480
|
+
: resolveSpawnModelAttempt(sessionAgent, attempt.model, ctx);
|
|
481
|
+
if (!resolvedAttempt) {
|
|
482
|
+
throw new Error(`No model resolver is available for fallback "${attempt.model}"`);
|
|
504
483
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
484
|
+
resolvedAttempts.set(attempt.index, resolvedAttempt);
|
|
485
|
+
return {
|
|
486
|
+
model: resolvedAttempt.model.aiModel,
|
|
487
|
+
maxOutputTokens: resolvedAttempt.model.maxTokens,
|
|
488
|
+
providerOptions: resolvedAttempt.providerOptions,
|
|
489
|
+
};
|
|
490
|
+
},
|
|
491
|
+
preserveSingleAttemptError: true,
|
|
492
|
+
system: systemPrompt,
|
|
493
|
+
messages,
|
|
494
|
+
tools: toolSet,
|
|
495
|
+
...(inject?.activeToolNames ? { activeTools: inject.activeToolNames() } : {}),
|
|
496
|
+
...(inject?.toolChoice ? { toolChoice: inject.toolChoice } : {}),
|
|
497
|
+
...(inject?.parallelToolCalls !== undefined
|
|
498
|
+
? { parallelToolCalls: inject.parallelToolCalls }
|
|
499
|
+
: {}),
|
|
500
|
+
...(hasStructuredOutput ? { output: modelOutput } : {}),
|
|
501
|
+
abortSignal: abortController.signal,
|
|
502
|
+
}, (event) => {
|
|
503
|
+
switch (event.type) {
|
|
504
|
+
case "reasoning-delta": {
|
|
505
|
+
// Chat parity (F1c): surface model reasoning as a thinking delta.
|
|
506
|
+
if (inject) {
|
|
507
|
+
try {
|
|
508
|
+
ctx?.onDelta?.({ text: event.text, kind: "reasoning" });
|
|
509
|
+
}
|
|
510
|
+
catch { /* subscriber can't sink the run */ }
|
|
514
511
|
}
|
|
515
|
-
|
|
512
|
+
break;
|
|
516
513
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
514
|
+
case "text-delta": {
|
|
515
|
+
stepText += event.text;
|
|
516
|
+
// F1b: token-level streaming sink (separate from onTranscript, which
|
|
517
|
+
// stays turn-granularity for persistence). Best-effort.
|
|
518
|
+
if (!hasStructuredOutput) {
|
|
520
519
|
try {
|
|
521
|
-
ctx?.onDelta?.({ text:
|
|
520
|
+
ctx?.onDelta?.({ text: event.text });
|
|
522
521
|
}
|
|
523
522
|
catch { /* a delta subscriber can't sink the run */ }
|
|
524
523
|
}
|
|
524
|
+
else {
|
|
525
|
+
const structuredText = structuredGate?.push(event.text);
|
|
526
|
+
if (structuredText) {
|
|
527
|
+
try {
|
|
528
|
+
ctx?.onDelta?.({ text: structuredText, kind: "structured" });
|
|
529
|
+
}
|
|
530
|
+
catch { /* a delta subscriber can't sink the run */ }
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
break;
|
|
525
534
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
emitTranscript({ type: "tool_input_start", toolId: event.id, tool: event.name });
|
|
532
|
-
break;
|
|
533
|
-
}
|
|
534
|
-
case "tool-input-delta": {
|
|
535
|
-
// Chat parity (F1c): forward raw argument text while it streams so
|
|
536
|
-
// the run path exposes the same live tool-call details as inline.
|
|
537
|
-
if (inject)
|
|
538
|
-
emitTranscript({ type: "tool_input_delta", toolId: event.id, delta: event.delta });
|
|
539
|
-
break;
|
|
540
|
-
}
|
|
541
|
-
case "tool-input-aborted": {
|
|
542
|
-
if (inject) {
|
|
543
|
-
emitTranscript({
|
|
544
|
-
type: "tool_input_interrupted",
|
|
545
|
-
toolId: event.id,
|
|
546
|
-
tool: event.name,
|
|
547
|
-
error: event.error,
|
|
548
|
-
});
|
|
535
|
+
case "tool-input-start": {
|
|
536
|
+
// Chat parity (F1c): "preparing" tool state before args stream in.
|
|
537
|
+
if (inject)
|
|
538
|
+
emitTranscript({ type: "tool_input_start", toolId: event.id, tool: event.name });
|
|
539
|
+
break;
|
|
549
540
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
541
|
+
case "tool-input-delta": {
|
|
542
|
+
// Chat parity (F1c): forward raw argument text while it streams so
|
|
543
|
+
// the run path exposes the same live tool-call details as inline.
|
|
544
|
+
if (inject)
|
|
545
|
+
emitTranscript({ type: "tool_input_delta", toolId: event.id, delta: event.delta });
|
|
554
546
|
break;
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
547
|
+
}
|
|
548
|
+
case "tool-input-aborted": {
|
|
549
|
+
if (inject) {
|
|
550
|
+
emitTranscript({
|
|
551
|
+
type: "tool_input_interrupted",
|
|
552
|
+
toolId: event.id,
|
|
553
|
+
tool: event.name,
|
|
554
|
+
error: event.error,
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
case "tool-call": {
|
|
560
|
+
if (!event.id || seenModelToolCallIds.has(event.id))
|
|
561
|
+
break;
|
|
562
|
+
seenModelToolCallIds.add(event.id);
|
|
563
|
+
activity.toolCalls++;
|
|
564
|
+
activity.lastTool = event.name;
|
|
565
|
+
activity.lastUpdate = new Date().toISOString();
|
|
566
|
+
if (event.invalid) {
|
|
567
|
+
const detail = event.error instanceof Error
|
|
568
|
+
? event.error.message
|
|
569
|
+
: typeof event.error === "string"
|
|
570
|
+
? event.error
|
|
571
|
+
: "Tool arguments do not match the declared input schema.";
|
|
572
|
+
// Keep the call in the loop so the SDK-provided tool error in
|
|
573
|
+
// responseMessages reaches the next model turn, but never emit a
|
|
574
|
+
// local calling event or dispatch it to an executor.
|
|
575
|
+
toolCalls.push({
|
|
576
|
+
id: event.id,
|
|
577
|
+
name: event.name,
|
|
578
|
+
args: (event.args ?? {}),
|
|
579
|
+
inputValidationError: `Error: Invalid tool arguments: ${detail}`,
|
|
580
|
+
});
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
// Chat parity (F1c): a client-side tool ends the server loop and is
|
|
584
|
+
// returned to the caller to execute — never dispatched here. Not
|
|
585
|
+
// pushing it to toolCalls ⇒ the turn ends with no executable tools.
|
|
586
|
+
if (inject && inject.clientSideToolNames.has(event.name)) {
|
|
587
|
+
emitTranscript({
|
|
588
|
+
type: "client_tool_call",
|
|
589
|
+
toolId: event.id,
|
|
590
|
+
tool: event.name,
|
|
591
|
+
input: event.args,
|
|
592
|
+
});
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
// Background task transcripts keep their historical use,use,result
|
|
596
|
+
// ordering. Interactive chat defers tool_use until executeTool so
|
|
597
|
+
// local calls are emitted calling,result one at a time, exactly like
|
|
598
|
+
// the inline completions handler. Provider tools never emit a local
|
|
599
|
+
// calling event because the model provider executes them.
|
|
600
|
+
if (!inject) {
|
|
601
|
+
emitTranscript({
|
|
602
|
+
type: "tool_use",
|
|
603
|
+
tool: event.name,
|
|
604
|
+
toolId: event.id,
|
|
605
|
+
input: event.args,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
568
608
|
toolCalls.push({
|
|
569
609
|
id: event.id,
|
|
570
610
|
name: event.name,
|
|
571
611
|
args: (event.args ?? {}),
|
|
572
|
-
inputValidationError: `Error: Invalid tool arguments: ${detail}`,
|
|
573
612
|
});
|
|
574
613
|
break;
|
|
575
614
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
tool: event.name,
|
|
584
|
-
input: event.args,
|
|
585
|
-
});
|
|
615
|
+
case "tool-result": {
|
|
616
|
+
if (inject?.providerToolNames.has(event.name)) {
|
|
617
|
+
providerToolResults.set(event.id, {
|
|
618
|
+
content: renderProviderResult(event.output),
|
|
619
|
+
isError: false,
|
|
620
|
+
});
|
|
621
|
+
}
|
|
586
622
|
break;
|
|
587
623
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
tool: event.name,
|
|
597
|
-
toolId: event.id,
|
|
598
|
-
input: event.args,
|
|
599
|
-
});
|
|
600
|
-
}
|
|
601
|
-
toolCalls.push({
|
|
602
|
-
id: event.id,
|
|
603
|
-
name: event.name,
|
|
604
|
-
args: (event.args ?? {}),
|
|
605
|
-
});
|
|
606
|
-
break;
|
|
607
|
-
}
|
|
608
|
-
case "tool-result": {
|
|
609
|
-
if (inject?.providerToolNames.has(event.name)) {
|
|
610
|
-
providerToolResults.set(event.id, {
|
|
611
|
-
content: renderProviderResult(event.output),
|
|
612
|
-
isError: false,
|
|
613
|
-
});
|
|
624
|
+
case "tool-error": {
|
|
625
|
+
if (inject?.providerToolNames.has(event.name)) {
|
|
626
|
+
providerToolResults.set(event.id, {
|
|
627
|
+
content: renderProviderResult(event.error),
|
|
628
|
+
isError: true,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
break;
|
|
614
632
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
}
|
|
633
|
+
case "finish": {
|
|
634
|
+
if (inject && event.finishReason === "error") {
|
|
635
|
+
emitTranscript({
|
|
636
|
+
type: "error",
|
|
637
|
+
message: "Model returned an error",
|
|
638
|
+
error: { message: "Model returned an error", type: "model_error" },
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
break;
|
|
623
642
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
case "finish": {
|
|
627
|
-
if (inject && event.finishReason === "error") {
|
|
643
|
+
case "error": {
|
|
644
|
+
const normalized = classifyRuntimeError(event.error);
|
|
628
645
|
emitTranscript({
|
|
629
646
|
type: "error",
|
|
630
|
-
message: "
|
|
631
|
-
error:
|
|
647
|
+
message: normalized.message ?? "Unknown model runtime error",
|
|
648
|
+
error: runtimeErrorMetadata(event.error),
|
|
632
649
|
});
|
|
650
|
+
break;
|
|
633
651
|
}
|
|
634
|
-
break;
|
|
635
|
-
}
|
|
636
|
-
case "error": {
|
|
637
|
-
const normalized = classifyRuntimeError(event.error);
|
|
638
|
-
emitTranscript({
|
|
639
|
-
type: "error",
|
|
640
|
-
message: normalized.message ?? "Unknown model runtime error",
|
|
641
|
-
error: runtimeErrorMetadata(event.error),
|
|
642
|
-
});
|
|
643
|
-
break;
|
|
644
652
|
}
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
catch (error) {
|
|
656
|
+
if (outputSchema !== undefined && isStructuredModelOutputError(error)) {
|
|
657
|
+
throw new LoopAgentOutputError("loop_agent_output_invalid", loopName, `Agent step ${JSON.stringify(loopName)} did not produce valid structured output`, { phase: "model_output" }, { cause: error });
|
|
645
658
|
}
|
|
646
|
-
|
|
647
|
-
}
|
|
648
|
-
catch (error) {
|
|
649
|
-
if (outputSchema !== undefined && isStructuredModelOutputError(error)) {
|
|
650
|
-
throw new LoopAgentOutputError("loop_agent_output_invalid", loopName, `Agent step ${JSON.stringify(loopName)} did not produce valid structured output`, { phase: "model_output" }, { cause: error });
|
|
659
|
+
throw error;
|
|
651
660
|
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
model = selectedResolved.model;
|
|
658
|
-
providerOptions = selectedResolved.providerOptions;
|
|
659
|
-
}
|
|
660
|
-
if (stepUsage) {
|
|
661
|
-
activity.totalTokens += (stepUsage.totalTokens ?? 0);
|
|
662
|
-
}
|
|
663
|
-
activity.lastUpdate = new Date().toISOString();
|
|
664
|
-
if (structuredGate?.streamed && toolCalls.length > 0) {
|
|
665
|
-
try {
|
|
666
|
-
ctx?.onDelta?.({ text: "", kind: "structured-reset" });
|
|
661
|
+
const stepUsage = turn.usage;
|
|
662
|
+
const selectedResolved = resolvedAttempts.get(turn.selectedAttempt.index);
|
|
663
|
+
if (selectedResolved) {
|
|
664
|
+
model = selectedResolved.model;
|
|
665
|
+
providerOptions = selectedResolved.providerOptions;
|
|
667
666
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
if (toolCalls.length
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
667
|
+
if (stepUsage) {
|
|
668
|
+
activity.totalTokens += (stepUsage.totalTokens ?? 0);
|
|
669
|
+
}
|
|
670
|
+
activity.lastUpdate = new Date().toISOString();
|
|
671
|
+
if (structuredGate?.streamed && toolCalls.length > 0) {
|
|
672
|
+
try {
|
|
673
|
+
ctx?.onDelta?.({ text: "", kind: "structured-reset" });
|
|
674
|
+
}
|
|
675
|
+
catch { /* a delta subscriber can't sink the run */ }
|
|
676
|
+
}
|
|
677
|
+
let rawText;
|
|
678
|
+
if (hasStructuredOutput) {
|
|
679
|
+
if (toolCalls.length === 0 && turn.output !== undefined) {
|
|
680
|
+
if (outputSchema !== undefined) {
|
|
681
|
+
const prepared = prepareLoopAgentOutput(loopName, turn.output, outputSchema);
|
|
682
|
+
rawText = prepared.serialized;
|
|
683
|
+
completedStructuredOutput = true;
|
|
684
|
+
}
|
|
685
|
+
else {
|
|
686
|
+
rawText = JSON.stringify(turn.output);
|
|
687
|
+
}
|
|
677
688
|
}
|
|
678
689
|
else {
|
|
679
|
-
rawText =
|
|
690
|
+
rawText = "";
|
|
680
691
|
}
|
|
681
692
|
}
|
|
682
693
|
else {
|
|
683
|
-
rawText = "";
|
|
694
|
+
rawText = stepText || (toolCalls.length === 0 ? turn.text : "");
|
|
684
695
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
try {
|
|
704
|
-
ctx?.onDelta?.({ text: guardedText });
|
|
696
|
+
const guardedText = rawText && ctx?.runOutputPolicy
|
|
697
|
+
? (await ctx.runOutputPolicy.evaluate({
|
|
698
|
+
output: rawText,
|
|
699
|
+
mode: ctx.runOutputPolicyMode ?? "enforce",
|
|
700
|
+
context: {
|
|
701
|
+
agent: sessionAgent.name,
|
|
702
|
+
runId: ctx.runId,
|
|
703
|
+
source: "task",
|
|
704
|
+
surface: "task",
|
|
705
|
+
},
|
|
706
|
+
signal: abortController.signal,
|
|
707
|
+
})).output
|
|
708
|
+
: rawText;
|
|
709
|
+
if (hasStructuredOutput && toolCalls.length === 0 && guardedText) {
|
|
710
|
+
try {
|
|
711
|
+
ctx?.onDelta?.({ text: guardedText });
|
|
712
|
+
}
|
|
713
|
+
catch { /* a delta subscriber can't sink the run */ }
|
|
705
714
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
const executeTool = async (toolCall) => {
|
|
735
|
-
if (toolCall.inputValidationError) {
|
|
736
|
-
emitTranscript({
|
|
737
|
-
type: "tool_result",
|
|
738
|
-
toolId: toolCall.id,
|
|
739
|
-
tool: toolCall.name,
|
|
740
|
-
content: toolCall.inputValidationError,
|
|
741
|
-
isError: true,
|
|
742
|
-
invalid: true,
|
|
743
|
-
});
|
|
744
|
-
// AI SDK already included the corresponding tool-error output in
|
|
745
|
-
// turn.responseMessages; do not append a duplicate history message.
|
|
746
|
-
return toolCall.inputValidationError;
|
|
747
|
-
}
|
|
748
|
-
let llmText;
|
|
749
|
-
let isError;
|
|
750
|
-
if (inject) {
|
|
751
|
-
// Chat parity (F1c): dispatch via the route's own executor and emit the
|
|
752
|
-
// FULL result (no 2000-char task truncation). Provider-executed tools
|
|
753
|
-
// are already resolved by the model provider: record their result for
|
|
754
|
-
// observability, but never dispatch them through the local executor.
|
|
755
|
-
if (inject.providerToolNames.has(toolCall.name)) {
|
|
756
|
-
const providerResult = providerToolResults.get(toolCall.id) ?? { content: "", isError: false };
|
|
715
|
+
if (guardedText) {
|
|
716
|
+
activity.summary = guardedText.slice(0, 200);
|
|
717
|
+
emitTranscript({ type: "assistant", text: guardedText });
|
|
718
|
+
accumText += guardedText;
|
|
719
|
+
}
|
|
720
|
+
lastStepText = guardedText;
|
|
721
|
+
// Append the assistant message (with its tool-call parts) to history.
|
|
722
|
+
const normalizedResponseMessages = normalizeResponseMessagesForHistory(turn.responseMessages);
|
|
723
|
+
const responseMessages = guardedText !== rawText
|
|
724
|
+
? replaceAssistantText(normalizedResponseMessages, guardedText)
|
|
725
|
+
: normalizedResponseMessages;
|
|
726
|
+
messages.push(...(contextTrust === "enforce"
|
|
727
|
+
? protectRuntimeToolResultMessages(responseMessages)
|
|
728
|
+
: responseMessages));
|
|
729
|
+
// Chat parity (F1c): surface per-turn usage + provider metadata so the
|
|
730
|
+
// driver can accumulate them for onCompletionFinished, matching chat.
|
|
731
|
+
if (inject) {
|
|
732
|
+
emitTranscript({
|
|
733
|
+
type: "usage",
|
|
734
|
+
usage: stepUsage,
|
|
735
|
+
providerMetadata: turn.providerMetadata,
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
return { text: guardedText, toolCalls, usage: stepUsage };
|
|
739
|
+
};
|
|
740
|
+
// ── LoopRunner tool executor: dispatch + history append ──
|
|
741
|
+
const executeTool = async (toolCall) => {
|
|
742
|
+
if (toolCall.inputValidationError) {
|
|
757
743
|
emitTranscript({
|
|
758
744
|
type: "tool_result",
|
|
759
745
|
toolId: toolCall.id,
|
|
760
746
|
tool: toolCall.name,
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
providerExecuted: true,
|
|
747
|
+
content: toolCall.inputValidationError,
|
|
748
|
+
isError: true,
|
|
749
|
+
invalid: true,
|
|
765
750
|
});
|
|
766
|
-
|
|
751
|
+
// AI SDK already included the corresponding tool-error output in
|
|
752
|
+
// turn.responseMessages; do not append a duplicate history message.
|
|
753
|
+
return toolCall.inputValidationError;
|
|
767
754
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
// Emitted at end-of-turn, so `messages` is post-tool-execution and
|
|
788
|
-
// post-compaction by construction (compaction rewrites `messages` at
|
|
789
|
-
// the START of a model step). The JSON round-trip here pins store
|
|
790
|
-
// fidelity: what a resumed run sees is exactly what survives JSON.
|
|
791
|
-
const checkpointCreatedAt = resume?.createdAt ?? new Date().toISOString();
|
|
792
|
-
const onTurnCheckpoint = options.onCheckpoint
|
|
793
|
-
? async ({ turn, steering }) => {
|
|
794
|
-
const serialized = JSON.stringify({ messages, steering });
|
|
795
|
-
if (serialized.length > MAX_CHECKPOINT_BYTES)
|
|
796
|
-
return; // keep the previous checkpoint
|
|
797
|
-
const checkpoint = JSON.parse(serialized);
|
|
798
|
-
await options.onCheckpoint({
|
|
799
|
-
context: {},
|
|
800
|
-
steps: [],
|
|
801
|
-
loopName,
|
|
802
|
-
turn,
|
|
803
|
-
history: checkpoint.messages,
|
|
804
|
-
accumText,
|
|
805
|
-
steering: checkpoint.steering,
|
|
806
|
-
createdAt: checkpointCreatedAt,
|
|
807
|
-
updatedAt: new Date().toISOString(),
|
|
808
|
-
});
|
|
809
|
-
}
|
|
810
|
-
: undefined;
|
|
811
|
-
const runner = new LoopRunner();
|
|
812
|
-
await runner.run({
|
|
813
|
-
agent: sessionAgent,
|
|
814
|
-
loop: { ...loop, name: loopName },
|
|
815
|
-
maxTurns,
|
|
816
|
-
startTurn: resume ? resume.turn + 1 : 0,
|
|
817
|
-
model: modelStep,
|
|
818
|
-
executeTool,
|
|
819
|
-
parallelToolCalls: inject?.parallelToolCalls,
|
|
820
|
-
onToolResults: (toolResults) => {
|
|
821
|
-
for (const { toolCall, result, isError } of toolResults) {
|
|
822
|
-
// AI SDK already persisted provider-executed results and invalid
|
|
823
|
-
// argument errors in responseMessages. Only local results belong
|
|
824
|
-
// in this host-owned ordered append.
|
|
825
|
-
if (toolCall.inputValidationError || inject?.providerToolNames.has(toolCall.name)) {
|
|
826
|
-
continue;
|
|
755
|
+
let llmText;
|
|
756
|
+
let isError;
|
|
757
|
+
if (inject) {
|
|
758
|
+
// Chat parity (F1c): dispatch via the route's own executor and emit the
|
|
759
|
+
// FULL result (no 2000-char task truncation). Provider-executed tools
|
|
760
|
+
// are already resolved by the model provider: record their result for
|
|
761
|
+
// observability, but never dispatch them through the local executor.
|
|
762
|
+
if (inject.providerToolNames.has(toolCall.name)) {
|
|
763
|
+
const providerResult = providerToolResults.get(toolCall.id) ?? { content: "", isError: false };
|
|
764
|
+
emitTranscript({
|
|
765
|
+
type: "tool_result",
|
|
766
|
+
toolId: toolCall.id,
|
|
767
|
+
tool: toolCall.name,
|
|
768
|
+
input: toolCall.args,
|
|
769
|
+
content: providerResult.content,
|
|
770
|
+
isError: providerResult.isError,
|
|
771
|
+
providerExecuted: true,
|
|
772
|
+
});
|
|
773
|
+
return "";
|
|
827
774
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
:
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
output: isError
|
|
838
|
-
? { type: "error-text", value: modelResult }
|
|
839
|
-
: { type: "text", value: modelResult },
|
|
840
|
-
}],
|
|
775
|
+
emitTranscript({
|
|
776
|
+
type: "tool_use",
|
|
777
|
+
tool: toolCall.name,
|
|
778
|
+
toolId: toolCall.id,
|
|
779
|
+
input: toolCall.args,
|
|
780
|
+
});
|
|
781
|
+
llmText = await inject.executor(toolCall.name, toolCall.args, {
|
|
782
|
+
callId: toolCall.id,
|
|
783
|
+
signal: abortController.signal,
|
|
841
784
|
});
|
|
785
|
+
isError = llmText.startsWith("Error:");
|
|
786
|
+
emitTranscript({ type: "tool_result", toolId: toolCall.id, tool: toolCall.name, content: llmText, isError });
|
|
842
787
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
788
|
+
else {
|
|
789
|
+
({ llmText, isError } = await performToolCall(toolByName.get(toolCall.name), toolCall));
|
|
790
|
+
}
|
|
791
|
+
return llmText;
|
|
792
|
+
};
|
|
793
|
+
// ── Durable turns: one checkpoint write per completed turn ──
|
|
794
|
+
// Emitted at end-of-turn, so `messages` is post-tool-execution and
|
|
795
|
+
// post-compaction by construction (compaction rewrites `messages` at
|
|
796
|
+
// the START of a model step). The JSON round-trip here pins store
|
|
797
|
+
// fidelity: what a resumed run sees is exactly what survives JSON.
|
|
798
|
+
const checkpointCreatedAt = resume?.createdAt ?? new Date().toISOString();
|
|
799
|
+
const onTurnCheckpoint = options.onCheckpoint
|
|
800
|
+
? async ({ turn, steering }) => {
|
|
801
|
+
const serialized = JSON.stringify({ messages, steering });
|
|
802
|
+
if (serialized.length > MAX_CHECKPOINT_BYTES)
|
|
803
|
+
return; // keep the previous checkpoint
|
|
804
|
+
const checkpoint = JSON.parse(serialized);
|
|
805
|
+
await options.onCheckpoint({
|
|
806
|
+
context: {},
|
|
807
|
+
steps: [],
|
|
808
|
+
loopName,
|
|
809
|
+
turn,
|
|
810
|
+
history: checkpoint.messages,
|
|
811
|
+
accumText,
|
|
812
|
+
steering: checkpoint.steering,
|
|
813
|
+
createdAt: checkpointCreatedAt,
|
|
814
|
+
updatedAt: new Date().toISOString(),
|
|
858
815
|
});
|
|
859
816
|
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
817
|
+
: undefined;
|
|
818
|
+
const runner = new LoopRunner();
|
|
819
|
+
await runner.run({
|
|
820
|
+
agent: sessionAgent,
|
|
821
|
+
loop: { ...loop, name: loopName },
|
|
822
|
+
maxTurns,
|
|
823
|
+
startTurn: resume ? resume.turn + 1 : 0,
|
|
824
|
+
model: modelStep,
|
|
825
|
+
executeTool,
|
|
826
|
+
parallelToolCalls: inject?.parallelToolCalls,
|
|
827
|
+
onToolResults: (toolResults) => {
|
|
828
|
+
for (const { toolCall, result, isError } of toolResults) {
|
|
829
|
+
// AI SDK already persisted provider-executed results and invalid
|
|
830
|
+
// argument errors in responseMessages. Only local results belong
|
|
831
|
+
// in this host-owned ordered append.
|
|
832
|
+
if (toolCall.inputValidationError || inject?.providerToolNames.has(toolCall.name)) {
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
const modelResult = contextTrust === "enforce"
|
|
836
|
+
? renderRuntimeToolResult(toolCall.name, toolCall.id, result)
|
|
837
|
+
: result;
|
|
838
|
+
messages.push({
|
|
839
|
+
role: "tool",
|
|
840
|
+
content: [{
|
|
841
|
+
type: "tool-result",
|
|
842
|
+
toolCallId: toolCall.id,
|
|
843
|
+
toolName: toolCall.name,
|
|
844
|
+
output: isError
|
|
845
|
+
? { type: "error-text", value: modelResult }
|
|
846
|
+
: { type: "text", value: modelResult },
|
|
847
|
+
}],
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
},
|
|
851
|
+
steering,
|
|
852
|
+
onSteering: async (steeringMessages) => {
|
|
853
|
+
for (const message of steeringMessages) {
|
|
854
|
+
messages.push(toSteeringModelMessage(message));
|
|
855
|
+
emitTranscript({
|
|
856
|
+
type: "steering",
|
|
857
|
+
messageId: message.id,
|
|
858
|
+
mode: message.mode,
|
|
859
|
+
text: message.content.text,
|
|
860
|
+
attachments: (message.content.attachments ?? []).map((attachment) => ({
|
|
861
|
+
type: attachment.type,
|
|
862
|
+
mediaType: attachment.mediaType,
|
|
863
|
+
name: attachment.name,
|
|
864
|
+
})),
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
},
|
|
868
|
+
onTurnCheckpoint,
|
|
869
|
+
});
|
|
870
|
+
if (outputSchema !== undefined && !completedStructuredOutput) {
|
|
871
|
+
throw new LoopAgentOutputError("loop_agent_output_invalid", loopName, `Agent step ${JSON.stringify(loopName)} exhausted maxTurns before producing structured output`, { phase: "max_turns" });
|
|
872
|
+
}
|
|
873
|
+
return { lastText: lastStepText, accumText };
|
|
874
|
+
}
|
|
875
|
+
finally {
|
|
876
|
+
if (sessionCleanup) {
|
|
877
|
+
await sessionCleanup().catch(() => undefined);
|
|
878
|
+
toolCleanups.delete(sessionCleanup);
|
|
879
|
+
}
|
|
865
880
|
}
|
|
866
|
-
return { lastText: lastStepText, accumText };
|
|
867
881
|
}
|
|
868
882
|
/**
|
|
869
883
|
* Run a project loop graph (.polpo/loops/<name>.json) through the
|
|
@@ -893,8 +907,10 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
|
|
|
893
907
|
const getBaseTools = () => {
|
|
894
908
|
baseToolsPromise ??= (async () => {
|
|
895
909
|
const prep = prepareSpawn(agentConfig, cwd, ctx);
|
|
896
|
-
const
|
|
897
|
-
|
|
910
|
+
const resolved = await buildAgentTools(agentConfig, cwd, prep, ctx, { signal: abortController.signal,
|
|
911
|
+
policy: { loop: projectLoop.allowedTools } });
|
|
912
|
+
toolCleanups.add(resolved.cleanup);
|
|
913
|
+
return new Map(resolved.tools.map((t) => [t.name, t]));
|
|
898
914
|
})();
|
|
899
915
|
return baseToolsPromise;
|
|
900
916
|
};
|
|
@@ -983,6 +999,7 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
|
|
|
983
999
|
const stepAgent = buildLoopStepAgent(agentConfig, name, loop);
|
|
984
1000
|
const session = await runLoopSession({
|
|
985
1001
|
sessionAgent: stepAgent,
|
|
1002
|
+
projectAllowedTools: projectLoop.allowedTools,
|
|
986
1003
|
loopName: name,
|
|
987
1004
|
loop,
|
|
988
1005
|
contextPrompt: agentInput
|
|
@@ -1127,6 +1144,8 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
|
|
|
1127
1144
|
};
|
|
1128
1145
|
}
|
|
1129
1146
|
finally {
|
|
1147
|
+
await Promise.allSettled([...toolCleanups].map(cleanup => cleanup()));
|
|
1148
|
+
toolCleanups.clear();
|
|
1130
1149
|
steering.signal.removeEventListener("abort", onSteeringAbort);
|
|
1131
1150
|
steering.close();
|
|
1132
1151
|
// Close agent-browser session (profile data auto-persisted by --profile).
|