@f5-sales-demo/pi-agent-core 22.13.2 → 22.14.2
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/package.json +3 -3
- package/src/agent-loop.ts +211 -199
- package/src/agent.ts +7 -0
- package/src/types.ts +2 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/pi-agent-core",
|
|
4
|
-
"version": "22.
|
|
4
|
+
"version": "22.14.2",
|
|
5
5
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
6
6
|
"homepage": "https://github.com/f5-sales-demo/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"fmt": "biome format --write ."
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@f5-sales-demo/pi-ai": "22.
|
|
39
|
-
"@f5-sales-demo/pi-utils": "22.
|
|
38
|
+
"@f5-sales-demo/pi-ai": "22.14.2",
|
|
39
|
+
"@f5-sales-demo/pi-utils": "22.14.2"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@sinclair/typebox": "0.34.52",
|
package/src/agent-loop.ts
CHANGED
|
@@ -379,231 +379,243 @@ async function runLoop(
|
|
|
379
379
|
// Check for steering messages at start (user may have typed while waiting)
|
|
380
380
|
let pendingMessages: AgentMessage[] = (await config.getSteeringMessages?.()) || [];
|
|
381
381
|
|
|
382
|
+
let followUpTurn = false;
|
|
382
383
|
// Outer loop: continues when queued follow-up messages arrive after agent would stop
|
|
383
384
|
while (true) {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
385
|
+
const runTurn = config.runConversationTurn ?? (async <T>(action: () => Promise<T>) => action());
|
|
386
|
+
const stopped = await runTurn(async () => {
|
|
387
|
+
let hasMoreToolCalls = true;
|
|
388
|
+
|
|
389
|
+
// Inner loop: process tool calls and steering messages
|
|
390
|
+
while (hasMoreToolCalls || pendingMessages.length > 0) {
|
|
391
|
+
if (!firstTurn) {
|
|
392
|
+
stream.push({ type: "turn_start" });
|
|
393
|
+
} else {
|
|
394
|
+
firstTurn = false;
|
|
395
|
+
}
|
|
393
396
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
397
|
+
// Process pending messages (inject before next assistant response)
|
|
398
|
+
if (pendingMessages.length > 0) {
|
|
399
|
+
for (const message of pendingMessages) {
|
|
400
|
+
stream.push({ type: "message_start", message });
|
|
401
|
+
stream.push({ type: "message_end", message });
|
|
402
|
+
currentContext.messages.push(message);
|
|
403
|
+
newMessages.push(message);
|
|
404
|
+
}
|
|
405
|
+
pendingMessages = [];
|
|
401
406
|
}
|
|
402
|
-
pendingMessages = [];
|
|
403
|
-
}
|
|
404
407
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
408
|
+
// Refresh prompt/tool context from live state before each model call
|
|
409
|
+
if (config.syncContextBeforeModelCall) {
|
|
410
|
+
await logger.ttftAttr("ttft.sync-context", () => config.syncContextBeforeModelCall!(currentContext));
|
|
411
|
+
}
|
|
409
412
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
stream,
|
|
438
|
-
config.getSteeringMessages,
|
|
439
|
-
config.interruptMode,
|
|
440
|
-
config.getToolContext,
|
|
441
|
-
config.transformToolCallArguments,
|
|
442
|
-
config.intentTracing,
|
|
443
|
-
completedTools,
|
|
444
|
-
toolKey,
|
|
445
|
-
);
|
|
446
|
-
const streamingDispatch = config.model.api === "openai-codex-responses";
|
|
447
|
-
const responseController = new AbortController();
|
|
448
|
-
const responseSignal = signal
|
|
449
|
-
? AbortSignal.any([signal, responseController.signal])
|
|
450
|
-
: responseController.signal;
|
|
451
|
-
const responseStartIndex = currentContext.messages.length;
|
|
452
|
-
let message: AssistantMessage;
|
|
453
|
-
let lastCheckpoint: AssistantMessage | undefined;
|
|
454
|
-
try {
|
|
455
|
-
message = await streamAssistantResponse(
|
|
456
|
-
currentContext,
|
|
457
|
-
newMessages,
|
|
458
|
-
{
|
|
459
|
-
...config,
|
|
460
|
-
...(live ? { liveSteering: live } : {}),
|
|
461
|
-
onAssistantCheckpoint: async checkpoint => {
|
|
462
|
-
await config.onAssistantCheckpoint?.(checkpoint);
|
|
463
|
-
lastCheckpoint = checkpoint;
|
|
464
|
-
},
|
|
465
|
-
getToolChoice: undefined,
|
|
466
|
-
toolChoice: selectedToolChoice,
|
|
467
|
-
...(streamingDispatch ? { maxRetries: Math.max(0, 2 - recoveryAttempts) } : {}),
|
|
468
|
-
},
|
|
469
|
-
responseSignal,
|
|
413
|
+
const selectedToolChoice = recoveryToolChoice
|
|
414
|
+
? recoveryToolChoice.value
|
|
415
|
+
: (config.getToolChoice?.() ?? config.toolChoice);
|
|
416
|
+
|
|
417
|
+
// Offer opt-in, plain user steering to the active provider without consuming other message kinds.
|
|
418
|
+
const live =
|
|
419
|
+
(config.model.compat as import("@f5-sales-demo/pi-ai").OpenAIResponsesCompat | undefined)
|
|
420
|
+
?.supportsWebSocketSteering &&
|
|
421
|
+
config.waitForSteeringMessages &&
|
|
422
|
+
config.getSteeringMessages &&
|
|
423
|
+
!exactToolName(selectedToolChoice)
|
|
424
|
+
? new LiveSteeringChannel({
|
|
425
|
+
wait: config.waitForSteeringMessages,
|
|
426
|
+
take: () => config.getSteeringMessages!(),
|
|
427
|
+
toProvider: async messages => {
|
|
428
|
+
const converted = await config.convertToLlm(messages);
|
|
429
|
+
// Correlated question replies must enter at a model boundary. They
|
|
430
|
+
// resolve an existing item, rather than steering its in-flight text.
|
|
431
|
+
if (converted.some(message => message.role !== "user" || isStructuredInputReply(message)))
|
|
432
|
+
return undefined;
|
|
433
|
+
return converted as import("@f5-sales-demo/pi-ai").UserMessage[];
|
|
434
|
+
},
|
|
435
|
+
})
|
|
436
|
+
: undefined;
|
|
437
|
+
const scheduler = createToolScheduler(
|
|
438
|
+
currentContext.tools,
|
|
439
|
+
signal,
|
|
470
440
|
stream,
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
message =
|
|
479
|
-
prior?.role === "assistant"
|
|
480
|
-
? {
|
|
481
|
-
...prior,
|
|
482
|
-
stopReason: signal?.aborted ? "aborted" : "error",
|
|
483
|
-
errorMessage: error instanceof Error ? error.message : String(error),
|
|
484
|
-
}
|
|
485
|
-
: {
|
|
486
|
-
...sanitizedForcedToolMessage(config.model, signal?.aborted ? "aborted" : "error"),
|
|
487
|
-
errorMessage: String(error),
|
|
488
|
-
};
|
|
489
|
-
const ownedIndex = currentContext.messages.findIndex(
|
|
490
|
-
(m, index) => index >= responseStartIndex && m.role === "assistant" && m.timestamp === message.timestamp,
|
|
441
|
+
config.getSteeringMessages,
|
|
442
|
+
config.interruptMode,
|
|
443
|
+
config.getToolContext,
|
|
444
|
+
config.transformToolCallArguments,
|
|
445
|
+
config.intentTracing,
|
|
446
|
+
completedTools,
|
|
447
|
+
toolKey,
|
|
491
448
|
);
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
449
|
+
const streamingDispatch = config.model.api === "openai-codex-responses";
|
|
450
|
+
const responseController = new AbortController();
|
|
451
|
+
const responseSignal = signal
|
|
452
|
+
? AbortSignal.any([signal, responseController.signal])
|
|
453
|
+
: responseController.signal;
|
|
454
|
+
const responseStartIndex = currentContext.messages.length;
|
|
455
|
+
let message: AssistantMessage;
|
|
456
|
+
let lastCheckpoint: AssistantMessage | undefined;
|
|
457
|
+
try {
|
|
458
|
+
message = await streamAssistantResponse(
|
|
459
|
+
currentContext,
|
|
460
|
+
newMessages,
|
|
461
|
+
{
|
|
462
|
+
...config,
|
|
463
|
+
...(live ? { liveSteering: live } : {}),
|
|
464
|
+
onAssistantCheckpoint: async checkpoint => {
|
|
465
|
+
await config.onAssistantCheckpoint?.(checkpoint);
|
|
466
|
+
lastCheckpoint = checkpoint;
|
|
467
|
+
},
|
|
468
|
+
getToolChoice: undefined,
|
|
469
|
+
toolChoice: selectedToolChoice,
|
|
470
|
+
...(streamingDispatch ? { maxRetries: Math.max(0, 2 - recoveryAttempts) } : {}),
|
|
471
|
+
},
|
|
472
|
+
responseSignal,
|
|
473
|
+
stream,
|
|
474
|
+
streamFn,
|
|
475
|
+
streamingDispatch ? (partial, call) => scheduler.admit(partial, [call]) : undefined,
|
|
476
|
+
);
|
|
477
|
+
} catch (error) {
|
|
478
|
+
responseController.abort();
|
|
479
|
+
const prior =
|
|
480
|
+
lastCheckpoint ?? currentContext.messages.slice(responseStartIndex).find(m => m.role === "assistant");
|
|
481
|
+
message =
|
|
482
|
+
prior?.role === "assistant"
|
|
483
|
+
? {
|
|
484
|
+
...prior,
|
|
485
|
+
stopReason: signal?.aborted ? "aborted" : "error",
|
|
486
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
487
|
+
}
|
|
488
|
+
: {
|
|
489
|
+
...sanitizedForcedToolMessage(config.model, signal?.aborted ? "aborted" : "error"),
|
|
490
|
+
errorMessage: String(error),
|
|
491
|
+
};
|
|
492
|
+
const ownedIndex = currentContext.messages.findIndex(
|
|
493
|
+
(m, index) =>
|
|
494
|
+
index >= responseStartIndex && m.role === "assistant" && m.timestamp === message.timestamp,
|
|
495
|
+
);
|
|
496
|
+
if (ownedIndex >= 0) currentContext.messages[ownedIndex] = message;
|
|
497
|
+
else currentContext.messages.push(message);
|
|
498
|
+
stream.push({ type: "message_end", message });
|
|
528
499
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
const retained = retainedAssistant(message);
|
|
543
|
-
const index = newMessages.findIndex(m => m.role === "assistant" && m.timestamp === message.timestamp);
|
|
544
|
-
if (index >= 0) newMessages[index] = retained;
|
|
545
|
-
const contextIndex = currentContext.messages.findIndex(
|
|
546
|
-
m => m.role === "assistant" && m.timestamp === message.timestamp,
|
|
547
|
-
);
|
|
548
|
-
if (contextIndex >= 0) currentContext.messages[contextIndex] = retained;
|
|
549
|
-
stream.push({ type: "message_end", message: retained });
|
|
550
|
-
}
|
|
551
|
-
if (!signal?.aborted) {
|
|
552
|
-
pendingMessages = steeringMessagesFromExecution ?? ((await config.getSteeringMessages?.()) || []);
|
|
553
|
-
hasMoreToolCalls = true;
|
|
554
|
-
recoveryToolChoice = {
|
|
555
|
-
value:
|
|
556
|
-
exactToolName(selectedToolChoice) && dispatchedResults.length > 0 ? "none" : selectedToolChoice,
|
|
557
|
-
};
|
|
558
|
-
continue;
|
|
559
|
-
}
|
|
500
|
+
newMessages.push(retainedAssistant(message));
|
|
501
|
+
if (message.stopReason !== "error" && message.stopReason !== "aborted") {
|
|
502
|
+
recoveryAttempts = 0;
|
|
503
|
+
recoveryToolChoice = undefined;
|
|
504
|
+
recoveryToolChoiceServed = false;
|
|
505
|
+
const required = exactToolName(selectedToolChoice);
|
|
506
|
+
scheduler.admit(
|
|
507
|
+
message,
|
|
508
|
+
message.content.filter(
|
|
509
|
+
(c): c is Extract<AssistantMessage["content"][number], { type: "toolCall" }> =>
|
|
510
|
+
c.type === "toolCall" && (!required || c.name === required),
|
|
511
|
+
),
|
|
512
|
+
);
|
|
560
513
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
type ToolCallContent = Extract<AssistantMessage["content"][number], { type: "toolCall" }>;
|
|
566
|
-
const toolCalls = message.content.filter((c): c is ToolCallContent => c.type === "toolCall");
|
|
567
|
-
const toolResults: ToolResultMessage[] = [...dispatchedResults];
|
|
568
|
-
for (const toolCall of toolCalls) {
|
|
569
|
-
if (scheduler.has(toolCall) || message.interruption) continue;
|
|
570
|
-
const result = createAbortedToolResult(toolCall, stream, message.stopReason, message.errorMessage);
|
|
514
|
+
const executionResult = await scheduler.drain();
|
|
515
|
+
const dispatchedResults = executionResult.toolResults;
|
|
516
|
+
for (const result of dispatchedResults) {
|
|
517
|
+
if (currentContext.messages.includes(result)) continue;
|
|
571
518
|
currentContext.messages.push(result);
|
|
572
519
|
newMessages.push(result);
|
|
573
|
-
toolResults.push(result);
|
|
574
520
|
}
|
|
521
|
+
if (live) {
|
|
522
|
+
if (message.stopReason === "error" || message.stopReason === "aborted")
|
|
523
|
+
config.restoreSteeringMessages?.([...live.accepted, ...live.deferred]);
|
|
524
|
+
else {
|
|
525
|
+
for (const accepted of live.accepted) {
|
|
526
|
+
currentContext.messages.push(accepted);
|
|
527
|
+
newMessages.push(accepted);
|
|
528
|
+
stream.push({ type: "message_start", message: accepted });
|
|
529
|
+
stream.push({ type: "message_end", message: accepted });
|
|
530
|
+
}
|
|
531
|
+
config.restoreSteeringMessages?.(live.deferred);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
const steeringMessagesFromExecution = executionResult.steeringMessages;
|
|
535
|
+
|
|
536
|
+
if (streamingDispatch && message.interruption && message.stopReason === "error" && !signal?.aborted) {
|
|
537
|
+
recoveryToolChoiceServed ||= !!exactToolName(selectedToolChoice) && dispatchedResults.length > 0;
|
|
538
|
+
if (recoveryToolChoiceServed) message.interruption.toolChoiceServed = true;
|
|
539
|
+
recoveryAttempts += message.interruption.providerRetriesConsumed;
|
|
540
|
+
if (recoveryAttempts < 2) {
|
|
541
|
+
recoveryAttempts += 1;
|
|
542
|
+
try {
|
|
543
|
+
await recoveryBackoff(500 * recoveryAttempts, signal);
|
|
544
|
+
} catch {
|
|
545
|
+
message.stopReason = "aborted";
|
|
546
|
+
const retained = retainedAssistant(message);
|
|
547
|
+
const index = newMessages.findIndex(
|
|
548
|
+
m => m.role === "assistant" && m.timestamp === message.timestamp,
|
|
549
|
+
);
|
|
550
|
+
if (index >= 0) newMessages[index] = retained;
|
|
551
|
+
const contextIndex = currentContext.messages.findIndex(
|
|
552
|
+
m => m.role === "assistant" && m.timestamp === message.timestamp,
|
|
553
|
+
);
|
|
554
|
+
if (contextIndex >= 0) currentContext.messages[contextIndex] = retained;
|
|
555
|
+
stream.push({ type: "message_end", message: retained });
|
|
556
|
+
}
|
|
557
|
+
if (!signal?.aborted) {
|
|
558
|
+
pendingMessages = steeringMessagesFromExecution ?? ((await config.getSteeringMessages?.()) || []);
|
|
559
|
+
hasMoreToolCalls = true;
|
|
560
|
+
recoveryToolChoice = {
|
|
561
|
+
value:
|
|
562
|
+
exactToolName(selectedToolChoice) && dispatchedResults.length > 0
|
|
563
|
+
? "none"
|
|
564
|
+
: selectedToolChoice,
|
|
565
|
+
};
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (message.stopReason === "error" || message.stopReason === "aborted") {
|
|
571
|
+
// Create placeholder tool results for any tool calls in the aborted message
|
|
572
|
+
// This maintains the tool_use/tool_result pairing that the API requires
|
|
573
|
+
type ToolCallContent = Extract<AssistantMessage["content"][number], { type: "toolCall" }>;
|
|
574
|
+
const toolCalls = message.content.filter((c): c is ToolCallContent => c.type === "toolCall");
|
|
575
|
+
const toolResults: ToolResultMessage[] = [...dispatchedResults];
|
|
576
|
+
for (const toolCall of toolCalls) {
|
|
577
|
+
if (scheduler.has(toolCall) || message.interruption) continue;
|
|
578
|
+
const result = createAbortedToolResult(toolCall, stream, message.stopReason, message.errorMessage);
|
|
579
|
+
currentContext.messages.push(result);
|
|
580
|
+
newMessages.push(result);
|
|
581
|
+
toolResults.push(result);
|
|
582
|
+
}
|
|
583
|
+
stream.push({
|
|
584
|
+
type: "turn_end",
|
|
585
|
+
message,
|
|
586
|
+
toolResults,
|
|
587
|
+
...(recoveryToolChoiceServed ? { toolChoiceServed: true } : {}),
|
|
588
|
+
});
|
|
589
|
+
stream.push({ type: "agent_end", messages: newMessages });
|
|
590
|
+
stream.end(newMessages);
|
|
591
|
+
return true;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Check for tool calls
|
|
595
|
+
const toolCalls = message.content.filter(c => c.type === "toolCall");
|
|
596
|
+
hasMoreToolCalls = toolCalls.length > 0 || Boolean(live?.accepted.length);
|
|
597
|
+
|
|
598
|
+
const toolResults = dispatchedResults;
|
|
599
|
+
|
|
575
600
|
stream.push({
|
|
576
601
|
type: "turn_end",
|
|
577
602
|
message,
|
|
578
603
|
toolResults,
|
|
579
604
|
...(recoveryToolChoiceServed ? { toolChoiceServed: true } : {}),
|
|
580
605
|
});
|
|
581
|
-
stream.push({ type: "agent_end", messages: newMessages });
|
|
582
|
-
stream.end(newMessages);
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
// Check for tool calls
|
|
587
|
-
const toolCalls = message.content.filter(c => c.type === "toolCall");
|
|
588
|
-
hasMoreToolCalls = toolCalls.length > 0 || Boolean(live?.accepted.length);
|
|
589
606
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
stream.push({
|
|
593
|
-
type: "turn_end",
|
|
594
|
-
message,
|
|
595
|
-
toolResults,
|
|
596
|
-
...(recoveryToolChoiceServed ? { toolChoiceServed: true } : {}),
|
|
597
|
-
});
|
|
598
|
-
|
|
599
|
-
pendingMessages = steeringMessagesFromExecution ?? ((await config.getSteeringMessages?.()) || []);
|
|
600
|
-
}
|
|
607
|
+
pendingMessages = steeringMessagesFromExecution ?? ((await config.getSteeringMessages?.()) || []);
|
|
608
|
+
}
|
|
601
609
|
|
|
610
|
+
return false;
|
|
611
|
+
}, followUpTurn);
|
|
612
|
+
if (stopped) return;
|
|
602
613
|
// Agent would stop here. Check for follow-up messages.
|
|
603
614
|
const followUpMessages = (await config.getFollowUpMessages?.()) || [];
|
|
604
615
|
if (followUpMessages.length > 0) {
|
|
605
616
|
// Set as pending so inner loop processes them
|
|
606
617
|
pendingMessages = followUpMessages;
|
|
618
|
+
followUpTurn = true;
|
|
607
619
|
continue;
|
|
608
620
|
}
|
|
609
621
|
|
package/src/agent.ts
CHANGED
|
@@ -259,6 +259,7 @@ export class Agent {
|
|
|
259
259
|
#abortController?: AbortController;
|
|
260
260
|
#convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
|
261
261
|
#contextMessages?: AgentLoopConfig["getContextMessages"];
|
|
262
|
+
#runConversationTurn?: AgentLoopConfig["runConversationTurn"];
|
|
262
263
|
#transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>;
|
|
263
264
|
#steeringQueue: AgentMessage[] = [];
|
|
264
265
|
#steeringWaiters = new Set<() => void>();
|
|
@@ -564,6 +565,11 @@ export class Agent {
|
|
|
564
565
|
}
|
|
565
566
|
|
|
566
567
|
/** Install the owning session's model-context provider. It must not start work. */
|
|
568
|
+
|
|
569
|
+
setConversationTurnScope(scope: AgentLoopConfig["runConversationTurn"]): void {
|
|
570
|
+
this.#runConversationTurn = scope;
|
|
571
|
+
}
|
|
572
|
+
|
|
567
573
|
setContextMessagesProvider(provider?: AgentLoopConfig["getContextMessages"]): () => void {
|
|
568
574
|
this.#contextMessages = provider;
|
|
569
575
|
return () => {
|
|
@@ -892,6 +898,7 @@ export class Agent {
|
|
|
892
898
|
return this.#dequeueSteeringMessages();
|
|
893
899
|
},
|
|
894
900
|
getFollowUpMessages: async () => this.#dequeueFollowUpMessages(),
|
|
901
|
+
runConversationTurn: this.#runConversationTurn,
|
|
895
902
|
};
|
|
896
903
|
|
|
897
904
|
return { context, config };
|
package/src/types.ts
CHANGED
|
@@ -113,6 +113,8 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
113
113
|
* continues with another turn.
|
|
114
114
|
*/
|
|
115
115
|
getFollowUpMessages?: () => Promise<AgentMessage[]>;
|
|
116
|
+
/** Scope one admitted user turn, including all tool continuations and retries. */
|
|
117
|
+
runConversationTurn?: <T>(action: () => Promise<T>, followUp: boolean) => Promise<T>;
|
|
116
118
|
|
|
117
119
|
/**
|
|
118
120
|
* Provides tool execution context, resolved per tool call.
|