@animalabs/agent-framework 0.26.1 → 0.26.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/README.md +1 -1
- package/dist/src/framework.d.ts +70 -27
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +441 -97
- package/dist/src/framework.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +4 -4
- package/dist/src/mcpl/references.js +5 -5
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/tool-result-history.d.ts +14 -1
- package/dist/src/tool-result-history.d.ts.map +1 -1
- package/dist/src/tool-result-history.js +22 -3
- package/dist/src/tool-result-history.js.map +1 -1
- package/dist/src/types/events.d.ts +4 -1
- package/dist/src/types/events.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/framework.ts +443 -99
- package/src/mcpl/references.ts +5 -5
- package/src/tool-result-history.ts +22 -3
- package/src/types/events.ts +4 -1
package/dist/src/framework.js
CHANGED
|
@@ -45,7 +45,7 @@ import { safeSlice } from './safe-slice.js';
|
|
|
45
45
|
import { SEND_TOOLS, bareToolName } from './send-tools.js';
|
|
46
46
|
import { boundedErrorMessage, errorMessage } from './error-message.js';
|
|
47
47
|
import { ProviderWaits, EVERY_MODEL, isReleaseModel, withoutControls } from './provider-waits.js';
|
|
48
|
-
import { nonErrorResultText, shownToScript, toolResultDataToHistoryString, truncateForHistory, DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS, } from './tool-result-history.js';
|
|
48
|
+
import { nonErrorResultText, shownToScript, shownToScriptFromServer, toolResultDataToHistoryString, truncateForHistory, DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS, } from './tool-result-history.js';
|
|
49
49
|
import { TOOL_IMAGE_REF_RE, ToolImageLedger, cutStoredToolResultToFit, formatKeptImageSlots, parseImagePlaceholders, sha256Hex, storedImageSlots, toolResultCutAt, toolResultShare, } from './tool-image-ledger.js';
|
|
50
50
|
import { randomUUID, createHash } from 'node:crypto';
|
|
51
51
|
import { describeCodeExecution, CODE_EXECUTION_TOOL_NAME, isolateSettings, validateCodeExecutionConfig, } from './code-execution/tool-definition.js';
|
|
@@ -671,6 +671,63 @@ function announcedDomainsOf(announce) {
|
|
|
671
671
|
function resumesLogicalTurn(reason) {
|
|
672
672
|
return reason === 'context_budget_restart' || reason === 'tool_result_guard_retry';
|
|
673
673
|
}
|
|
674
|
+
/**
|
|
675
|
+
* Marks a held turn's retry: the request a provider hold keeps for a turn
|
|
676
|
+
* whose call failed into it, or whose compile it held with nothing sent.
|
|
677
|
+
* That turn has begun and not ended: it starts again when the hold releases
|
|
678
|
+
* (a new start of its open turn, as 0.26.0 started it). So, as a
|
|
679
|
+
* continuation on its way does (continuesTurn), it keeps its agent's turn
|
|
680
|
+
* alive meanwhile, passes a quiesce, and goes with an abandon, a stop() or
|
|
681
|
+
* its agent's removal. A symbol, not a field: hosts build requests too, and
|
|
682
|
+
* only the framework may say one is a turn's own retry. Spreads and rests
|
|
683
|
+
* copy it, as every copy of a request the scheduler makes is one.
|
|
684
|
+
*/
|
|
685
|
+
const TURN_RETRY = Symbol('turn-retry');
|
|
686
|
+
/**
|
|
687
|
+
* The batch a start that carries a turn on (continuesTurn) was merged from,
|
|
688
|
+
* when the scheduler merged wakes that aren't the turn's into it: every
|
|
689
|
+
* request it took in, as each was. Until the start takes its turn token it
|
|
690
|
+
* stands for them all. A scheduler pass that takes it back puts the batch in
|
|
691
|
+
* its place, each request with its own identity and flags; a quiesce at the
|
|
692
|
+
* auxiliary park sends the batch back to the scheduler; and a caller's stop
|
|
693
|
+
* or an abandon that drops the start puts back the batch's wakes, the
|
|
694
|
+
* requests that don't carry the turn on (dropContinuations), where they run
|
|
695
|
+
* as a turn of their own, as wakes queued while a turn streams do. Once the
|
|
696
|
+
* start takes its token the turn reads them, and beginAgentTurn drops the
|
|
697
|
+
* record. A symbol, as TURN_RETRY is, so spreads and rests copy it.
|
|
698
|
+
*/
|
|
699
|
+
const MERGED_BATCH = Symbol('merged-batch');
|
|
700
|
+
function mergedBatch(request) {
|
|
701
|
+
return request[MERGED_BATCH];
|
|
702
|
+
}
|
|
703
|
+
/** The wakes of a merged start's batch (MERGED_BATCH): its requests that
|
|
704
|
+
* don't carry the turn on. */
|
|
705
|
+
function mergedWakes(request) {
|
|
706
|
+
return (mergedBatch(request) ?? []).filter((r) => !continuesTurn(r));
|
|
707
|
+
}
|
|
708
|
+
function isTurnRetry(request) {
|
|
709
|
+
return request[TURN_RETRY] === true;
|
|
710
|
+
}
|
|
711
|
+
/** How to go on sooner than a hold keeping a turn's retry lets the turn go
|
|
712
|
+
* on: what surgery's and the puppet's refusals advise. */
|
|
713
|
+
function heldTurnWayOn(statedWait) {
|
|
714
|
+
return 'a quiesce lets a held turn start when its hold ends and finish, so to go on sooner, end it with quiesce ' +
|
|
715
|
+
'with abandon: true, which ends it once the quiesce\'s wait for the drain runs out (at once on a host ' +
|
|
716
|
+
'already quiesced)' +
|
|
717
|
+
(statedWait
|
|
718
|
+
? ", or lift a stated wait with release-provider-wait, so the turn tries again once no host's or " +
|
|
719
|
+
'acceleration hold binds it'
|
|
720
|
+
: '');
|
|
721
|
+
}
|
|
722
|
+
/** A copy of a held turn's trigger, marked as its retry (TURN_RETRY). */
|
|
723
|
+
function asTurnRetry(trigger) {
|
|
724
|
+
return { ...trigger, [TURN_RETRY]: true };
|
|
725
|
+
}
|
|
726
|
+
/** A request that carries its agent's turn on: a continuation of it, or the
|
|
727
|
+
* retry a provider hold keeps for it (TURN_RETRY). */
|
|
728
|
+
function continuesTurn(request) {
|
|
729
|
+
return resumesLogicalTurn(request.reason) || isTurnRetry(request);
|
|
730
|
+
}
|
|
674
731
|
/**
|
|
675
732
|
* The answers dispatchScriptToolCall's safety timer made, when no result came
|
|
676
733
|
* for a script's call: the tool may still be running, or its result was lost,
|
|
@@ -678,6 +735,19 @@ function resumesLogicalTurn(reason) {
|
|
|
678
735
|
* place in the host taken until its run ends.
|
|
679
736
|
*/
|
|
680
737
|
const SCRIPT_CALL_NO_RESULT = new WeakSet();
|
|
738
|
+
/**
|
|
739
|
+
* An MCPL peer's results whose blocks couldn't be given RFC-005's treatment
|
|
740
|
+
* for a script, because JSON couldn't hold them (dispatchMcplToolCall's
|
|
741
|
+
* reading), each with why. The failure is the script lane's alone: the
|
|
742
|
+
* model's lane reads the same answer with no such round trip. So such a
|
|
743
|
+
* result keeps the peer's own outcome, its blocks dropped, and the trace and
|
|
744
|
+
* the call's terminal report what the peer answered, as on v0.26.1, while
|
|
745
|
+
* the script lane's exit answers it as a result it can't convert to JSON
|
|
746
|
+
* (scriptResultAnswer). A module's result that JSON can't hold differs: no
|
|
747
|
+
* route can store or send it, so its dispatch makes it an error result for
|
|
748
|
+
* everyone (checkedToolResult).
|
|
749
|
+
*/
|
|
750
|
+
const SCRIPT_RESULT_UNCONVERTIBLE = new WeakMap();
|
|
681
751
|
/** The last TurnRouting.turnId given out: ids increase, and are unique within the process. */
|
|
682
752
|
let lastTurnId = 0;
|
|
683
753
|
function freshTurnRouting() {
|
|
@@ -705,8 +775,10 @@ function ephemeralRunStopped(name, observed) {
|
|
|
705
775
|
}
|
|
706
776
|
/** The reason on the inference:aborted that ends a turn whose retry stop()
|
|
707
777
|
* cancelled, whichever frame was waiting to retry: beginAgentTurn's, when
|
|
708
|
-
* the stream couldn't start, or driveStream's, when it failed once started
|
|
709
|
-
*
|
|
778
|
+
* the stream couldn't start, or driveStream's, when it failed once started;
|
|
779
|
+
* or whose failure a provider hold would have kept for its retry once stop()
|
|
780
|
+
* had closed admission (endFailureAtStop). Its inference:failed came first,
|
|
781
|
+
* and this says no retry follows it. */
|
|
710
782
|
const RETRY_CANCELLED_BY_STOP = 'a retry waiting while the framework was stopping is dropped before it sends anything';
|
|
711
783
|
/** The reason on the inference:aborted that ends a turn whose start waited to
|
|
712
784
|
* retry a failed compile across a stop() and a start(): the retry would
|
|
@@ -719,6 +791,11 @@ function isTurnContinuation(reason) {
|
|
|
719
791
|
return reason === 'context_budget_restart' || reason === 'tool_results_ready'
|
|
720
792
|
|| reason === 'tool_result_guard_retry';
|
|
721
793
|
}
|
|
794
|
+
/** A request a quiesce lets through, so a turn already alive can finish:
|
|
795
|
+
* a continuation of the turn, or a held turn's retry (TURN_RETRY). */
|
|
796
|
+
function passesQuiesce(request) {
|
|
797
|
+
return request !== undefined && (isTurnContinuation(request.reason) || isTurnRetry(request));
|
|
798
|
+
}
|
|
722
799
|
/** Wake sources the framework generates for itself (bounce wakes,
|
|
723
800
|
* continuations, retries, unsticks, admission requeues). A wake from any
|
|
724
801
|
* other source — a channel message, a heartbeat or other server event, a
|
|
@@ -1190,12 +1267,14 @@ function scriptResultBrief(content) {
|
|
|
1190
1267
|
}
|
|
1191
1268
|
/**
|
|
1192
1269
|
* What a script gets for a tool result that can't be converted to JSON,
|
|
1193
|
-
* such as a cyclic object or a BigInt that an in-process module returned
|
|
1270
|
+
* such as a cyclic object or a BigInt that an in-process module returned,
|
|
1271
|
+
* or an MCPL peer's blocks nested deeper than JSON.stringify can take.
|
|
1194
1272
|
* It names the tool, and says the call itself completed, so a script that
|
|
1195
1273
|
* reports it doesn't invite a blind retry. The host's log gets the same
|
|
1196
|
-
* line, since the tool
|
|
1197
|
-
* script reports it. The reason and the
|
|
1198
|
-
* VALUE_CHARS each, so the failure reaches the
|
|
1274
|
+
* line, since the tool, a module's or a server's, returns what a script
|
|
1275
|
+
* can't receive, whether or not the script reports it. The reason and the
|
|
1276
|
+
* tool's own error are cut to VALUE_CHARS each, so the failure reaches the
|
|
1277
|
+
* script whatever its memory.
|
|
1199
1278
|
*/
|
|
1200
1279
|
function unconvertibleScriptResult(toolName, err, ownError) {
|
|
1201
1280
|
let reason;
|
|
@@ -1230,7 +1309,9 @@ const SCRIPT_TEXT_ANSWER_ENVELOPE_BYTES = Buffer.byteLength(JSON.stringify({ con
|
|
|
1230
1309
|
* dense in small objects or a script already holding most of its memory.
|
|
1231
1310
|
* Such a result, or one past the figure that doesn't fit, reaches the script
|
|
1232
1311
|
* as ResultTooLarge, saying the call took effect, and nothing of it is cut or
|
|
1233
|
-
* saved.
|
|
1312
|
+
* saved. One the host can't convert to JSON at all, such as one nested deeper
|
|
1313
|
+
* than its runtime's JSON.stringify can take, is answered instead as a
|
|
1314
|
+
* failure naming the tool and saying the call itself completed.
|
|
1234
1315
|
*/
|
|
1235
1316
|
const scriptResultMaxChars = (memoryMaxBytes) => Math.max(0, Math.floor((Math.min(maxResultBytes(memoryMaxBytes), bufferConstants.MAX_STRING_LENGTH) - SCRIPT_TEXT_ANSWER_ENVELOPE_BYTES) / 6));
|
|
1236
1317
|
/**
|
|
@@ -1974,12 +2055,14 @@ export class AgentFramework {
|
|
|
1974
2055
|
* is still the agent's, so a start that ends after a later frame opened
|
|
1975
2056
|
* one, such as its retry's stream waiting to retry, leaves that one open.
|
|
1976
2057
|
* An operator's abandon marks the window, and the frame's check collects
|
|
1977
|
-
* the mark, which goes with the window.
|
|
1978
|
-
*
|
|
1979
|
-
*
|
|
1980
|
-
*
|
|
1981
|
-
*
|
|
1982
|
-
*
|
|
2058
|
+
* the mark, which goes with the window. A stop of a turn with no stream
|
|
2059
|
+
* lands here, or, for a turn whose continuation or retry is on its way (a
|
|
2060
|
+
* provider hold's, a park's, the queue's or a pass's), through
|
|
2061
|
+
* abortInference(), an abandon or stop(), which drop that continuation or
|
|
2062
|
+
* retry (dropContinuations); a stop made anywhere else, as while an agent
|
|
2063
|
+
* is idle or a turn finishes its last delivery, has no check to end it,
|
|
2064
|
+
* and the next start drops it. Keyed by the Agent, so a replacement agent
|
|
2065
|
+
* under the same name never shares one.
|
|
1983
2066
|
*/
|
|
1984
2067
|
checkAhead = new Map();
|
|
1985
2068
|
/** Structured calls executed in the current logical turn. Unlike the
|
|
@@ -3649,6 +3732,23 @@ export class AgentFramework {
|
|
|
3649
3732
|
a.coalescingSubject === b.coalescingSubject && batchSubjects(a) === batchSubjects(b) &&
|
|
3650
3733
|
a.readsDeferredWrite === b.readsDeferredWrite;
|
|
3651
3734
|
}
|
|
3735
|
+
/** Keep `request` among a hold's `held` requests, once by
|
|
3736
|
+
* sameInferenceRequest. A held turn's retry (TURN_RETRY) whose equal is
|
|
3737
|
+
* already held marks that one, so the hold still keeps the turn alive. */
|
|
3738
|
+
holdRequest(held, request) {
|
|
3739
|
+
const same = held.find((r) => this.sameInferenceRequest(r, request));
|
|
3740
|
+
if (!same) {
|
|
3741
|
+
held.push(request);
|
|
3742
|
+
return;
|
|
3743
|
+
}
|
|
3744
|
+
if (isTurnRetry(request))
|
|
3745
|
+
same[TURN_RETRY] = true;
|
|
3746
|
+
// A merged start's batch (MERGED_BATCH) stays recorded on the request
|
|
3747
|
+
// kept in its place, which then stands for it, as the start did.
|
|
3748
|
+
const batch = mergedBatch(request);
|
|
3749
|
+
if (batch)
|
|
3750
|
+
same[MERGED_BATCH] = [...new Set([...(mergedBatch(same) ?? []), ...batch])];
|
|
3751
|
+
}
|
|
3652
3752
|
consultProviderHold(error, agentName) {
|
|
3653
3753
|
if (!this.providerHoldHook)
|
|
3654
3754
|
return undefined;
|
|
@@ -3706,15 +3806,21 @@ export class AgentFramework {
|
|
|
3706
3806
|
// failure streak), and the wait applies to the calls that follow it.
|
|
3707
3807
|
if (!acceleration && !hostHold) {
|
|
3708
3808
|
return wait !== undefined && isMembraneError(error) && error.retryable === true
|
|
3709
|
-
&& this.holdForProviderWait(agent, wait, trigger ? [trigger] : [], true);
|
|
3809
|
+
&& this.holdForProviderWait(agent, wait, trigger ? [asTurnRetry(trigger)] : [], true);
|
|
3710
3810
|
}
|
|
3811
|
+
// As in holdForProviderWait: once stop() has closed admission, nothing is
|
|
3812
|
+
// held, and nothing started.
|
|
3813
|
+
if (this.providerAdmissionClosed)
|
|
3814
|
+
return true;
|
|
3711
3815
|
const now = Date.now();
|
|
3712
3816
|
const pacing = hostHold ? undefined : this.accelerationCooldownMs(agent.name, error);
|
|
3713
3817
|
const delayMs = hostHold ? hostHold.holdMs : pacing.delayMs;
|
|
3714
3818
|
const existing = this.providerAccelerationCooldowns.get(agent.name);
|
|
3715
3819
|
const held = existing?.heldRequests ?? [];
|
|
3716
|
-
|
|
3717
|
-
|
|
3820
|
+
// The failed turn's own request, kept as its retry (TURN_RETRY). An
|
|
3821
|
+
// auxiliary call's failure passes no trigger: it holds no turn of its own.
|
|
3822
|
+
if (trigger)
|
|
3823
|
+
this.holdRequest(held, asTurnRetry(trigger));
|
|
3718
3824
|
if (existing)
|
|
3719
3825
|
clearTimeout(existing.timer);
|
|
3720
3826
|
const timer = setTimeout(() => this.releaseProviderAccelerationCooldown(agent.name), Math.min(delayMs, MAX_TIMER_DELAY_MS));
|
|
@@ -3767,7 +3873,7 @@ export class AgentFramework {
|
|
|
3767
3873
|
return undefined;
|
|
3768
3874
|
if (this.conversationAgentHomes.has(agent.name)) {
|
|
3769
3875
|
const wait = this.recordProviderWait(agent.name, agent.model, error);
|
|
3770
|
-
return error.retryable === true && wait !== undefined && this.holdForProviderWait(agent, wait, trigger ? [trigger] : [], true)
|
|
3876
|
+
return error.retryable === true && wait !== undefined && this.holdForProviderWait(agent, wait, trigger ? [asTurnRetry(trigger)] : [], true)
|
|
3771
3877
|
? 'held'
|
|
3772
3878
|
: undefined;
|
|
3773
3879
|
}
|
|
@@ -3922,19 +4028,33 @@ export class AgentFramework {
|
|
|
3922
4028
|
* Hold this agent's primary turns for a provider wait on its model. The
|
|
3923
4029
|
* requests are kept and released together, into one fresh compile, when
|
|
3924
4030
|
* the wait ends: the same path an acceleration cooldown takes, without
|
|
3925
|
-
* parking auxiliary calls to other models.
|
|
4031
|
+
* parking auxiliary calls to other models. `report`, once the wait is
|
|
4032
|
+
* found to bind, runs before anything is held: a caller that reports its
|
|
4033
|
+
* turn's end there (inference:failed) does so before the hold takes the
|
|
4034
|
+
* turn's retry, as every failure site does.
|
|
3926
4035
|
*/
|
|
3927
|
-
holdForProviderWait(agent, wait, requests, failed = false) {
|
|
4036
|
+
holdForProviderWait(agent, wait, requests, failed = false, report) {
|
|
3928
4037
|
const now = Date.now();
|
|
3929
4038
|
const until = wait.until ?? Number.POSITIVE_INFINITY;
|
|
3930
4039
|
if (until <= now)
|
|
3931
4040
|
return false;
|
|
4041
|
+
report?.();
|
|
4042
|
+
// stop() has cleared every hold and dropped every turn on its way: a
|
|
4043
|
+
// failure or start reaching here after it holds nothing, so no retry
|
|
4044
|
+
// outlives the stop as a turn still alive. Nothing is started either.
|
|
4045
|
+
if (this.providerAdmissionClosed)
|
|
4046
|
+
return true;
|
|
4047
|
+
// A listener of the report may have released the wait
|
|
4048
|
+
// (releaseProviderWait) before anything was held: nothing binds now, so
|
|
4049
|
+
// the requests start, as the release of an installed hold starts them.
|
|
4050
|
+
if (report && !this.providerWaits?.active(agent.name, wait.model)) {
|
|
4051
|
+
this.pendingRequests.push(...requests);
|
|
4052
|
+
return true;
|
|
4053
|
+
}
|
|
3932
4054
|
const existing = this.providerAccelerationCooldowns.get(agent.name);
|
|
3933
4055
|
const held = existing?.heldRequests ?? [];
|
|
3934
|
-
for (const request of requests)
|
|
3935
|
-
|
|
3936
|
-
held.push(request);
|
|
3937
|
-
}
|
|
4056
|
+
for (const request of requests)
|
|
4057
|
+
this.holdRequest(held, request);
|
|
3938
4058
|
// An acceleration or host hold already parks the whole agent; its release
|
|
3939
4059
|
// re-checks this wait at admission.
|
|
3940
4060
|
if (existing && existing.waitModel === undefined)
|
|
@@ -4514,9 +4634,11 @@ export class AgentFramework {
|
|
|
4514
4634
|
}
|
|
4515
4635
|
/**
|
|
4516
4636
|
* Abort an in-flight inference for an agent. True when there was one to
|
|
4517
|
-
* stop: a stream
|
|
4637
|
+
* stop: a stream; a turn with no stream and one of its checks ahead (its
|
|
4518
4638
|
* hooks or compile, a wait to retry, the tool-result guard's wait), which
|
|
4519
|
-
* ends at that check, before it sends anything more
|
|
4639
|
+
* ends at that check, before it sends anything more; or a turn whose
|
|
4640
|
+
* continuation or retry is on its way (queued, kept by a provider hold,
|
|
4641
|
+
* parked at admission or in a scheduler pass), which ends here.
|
|
4520
4642
|
*/
|
|
4521
4643
|
abortInference(agentName, reason) {
|
|
4522
4644
|
const agent = this.agents.get(agentName);
|
|
@@ -4546,6 +4668,20 @@ export class AgentFramework {
|
|
|
4546
4668
|
checkWindow.wake?.();
|
|
4547
4669
|
return true;
|
|
4548
4670
|
}
|
|
4671
|
+
// A turn whose continuation or retry is on its way (a budget restart or
|
|
4672
|
+
// a guard retry, or a failed turn's retry a provider hold keeps:
|
|
4673
|
+
// pendingContinuationAgents) ends here, as a stop of a turn's start ends
|
|
4674
|
+
// one, whether or not the frame that handed it on has let go of its
|
|
4675
|
+
// token yet (that frame ends on its own and settles nothing more): the
|
|
4676
|
+
// continuation or retry is dropped, with one inference:aborted carrying
|
|
4677
|
+
// the stop's reason. Wakes that arrived meanwhile aren't the turn's, and
|
|
4678
|
+
// still run; a hold of such wakes alone keeps no turn, and has nothing to
|
|
4679
|
+
// stop. Every failure site reports inference:failed before a hold takes
|
|
4680
|
+
// the retry, so a listener of it finds nothing held, and reports false.
|
|
4681
|
+
if (!result && this.pendingContinuationAgents().has(agentName)) {
|
|
4682
|
+
this.dropContinuations(agentName, { kind: 'caller', reason, why: 'a stop ended the turn' });
|
|
4683
|
+
return true;
|
|
4684
|
+
}
|
|
4549
4685
|
return !!result;
|
|
4550
4686
|
}
|
|
4551
4687
|
/**
|
|
@@ -5433,9 +5569,10 @@ export class AgentFramework {
|
|
|
5433
5569
|
* it queues, or a tool-result guard's retry, which its frame starts at
|
|
5434
5570
|
* once) holds no turn token until its start takes one. Once its turn's
|
|
5435
5571
|
* frame has let go of its token, its start can wait in the queue, a
|
|
5436
|
-
* provider hold
|
|
5437
|
-
*
|
|
5438
|
-
*
|
|
5572
|
+
* provider hold, a park on admission or a scheduler pass's hands. Its turn
|
|
5573
|
+
* is alive meanwhile. So is a turn whose call failed into a provider hold,
|
|
5574
|
+
* which keeps the turn's retry (TURN_RETRY), until that retry starts,
|
|
5575
|
+
* wherever it waits. These are the agents with one on its way. A
|
|
5439
5576
|
* `tool_results_ready` continuation follows a tool round left waiting with
|
|
5440
5577
|
* no stream, which only the direct Agent API makes (Agent's
|
|
5441
5578
|
* runInferenceWithInjections; the framework's own rounds wait with their
|
|
@@ -5444,7 +5581,7 @@ export class AgentFramework {
|
|
|
5444
5581
|
*/
|
|
5445
5582
|
pendingContinuationAgents() {
|
|
5446
5583
|
const agents = new Set();
|
|
5447
|
-
const add = (r) => { if (
|
|
5584
|
+
const add = (r) => { if (continuesTurn(r))
|
|
5448
5585
|
agents.add(r.agentName); };
|
|
5449
5586
|
// Read defensively: a framework built from the prototype, as the
|
|
5450
5587
|
// operator tests build one, sets none of these.
|
|
@@ -5462,7 +5599,7 @@ export class AgentFramework {
|
|
|
5462
5599
|
return agents;
|
|
5463
5600
|
}
|
|
5464
5601
|
/** The agents with a turn alive: holding a turn token, or with a
|
|
5465
|
-
* continuation of their turn on its way. */
|
|
5602
|
+
* continuation of their turn, or its retry, on its way. */
|
|
5466
5603
|
turnHolders() {
|
|
5467
5604
|
const holders = this.pendingContinuationAgents();
|
|
5468
5605
|
for (const name of this.activeTurnTokens.keys())
|
|
@@ -5472,6 +5609,26 @@ export class AgentFramework {
|
|
|
5472
5609
|
hasTurn(agentName) {
|
|
5473
5610
|
return this.activeTurnTokens.has(agentName) || this.pendingContinuationAgents().has(agentName);
|
|
5474
5611
|
}
|
|
5612
|
+
/** The provider hold keeping `agentName`'s turn for its retry (TURN_RETRY),
|
|
5613
|
+
* in words naming its end and its reason, and whether a stated wait holds
|
|
5614
|
+
* it; undefined when no hold keeps one. */
|
|
5615
|
+
heldTurnRetry(agentName) {
|
|
5616
|
+
// Optional, as pendingContinuationAgents reads it: a framework built from
|
|
5617
|
+
// its prototype, as some suites build one, has no map of holds.
|
|
5618
|
+
const hold = this.providerAccelerationCooldowns?.get(agentName);
|
|
5619
|
+
if (!hold?.heldRequests.some(isTurnRetry))
|
|
5620
|
+
return undefined;
|
|
5621
|
+
// A deadline past what a Date can name (an infinite wait, or a stated
|
|
5622
|
+
// one too long for an instant) is one only a release ends.
|
|
5623
|
+
const end = new Date(hold.until);
|
|
5624
|
+
const until = Number.isNaN(end.getTime()) ? 'until released' : `until ${end.toISOString()}`;
|
|
5625
|
+
return {
|
|
5626
|
+
says: `a provider hold keeps its turn to start again, ${until}: ${hold.reason}`,
|
|
5627
|
+
// release-provider-wait lifts a stated wait's hold, not a host's or an
|
|
5628
|
+
// acceleration cooldown's own pacing.
|
|
5629
|
+
statedWait: hold.waitModel !== undefined || hold.statedWait !== undefined,
|
|
5630
|
+
};
|
|
5631
|
+
}
|
|
5475
5632
|
getHostModeStatus() {
|
|
5476
5633
|
const parkedAdmissions = this.parkedAdmissionCount();
|
|
5477
5634
|
const holders = this.turnHolders();
|
|
@@ -5483,7 +5640,7 @@ export class AgentFramework {
|
|
|
5483
5640
|
activeTurns: holders.size,
|
|
5484
5641
|
activeTurnAgents: [...holders].sort(),
|
|
5485
5642
|
parkedAdmissions,
|
|
5486
|
-
gatedRequests: this.pendingRequests.filter((r) => !
|
|
5643
|
+
gatedRequests: this.pendingRequests.filter((r) => !passesQuiesce(r)).length,
|
|
5487
5644
|
backgroundScripts: this.codeExecution?.scripts.leftRunning() ?? 0,
|
|
5488
5645
|
deferredWrites: this.deferredMessages.length,
|
|
5489
5646
|
...(this.lastUnabandonable.length > 0 ? { unabandonable: [...this.lastUnabandonable] } : {}),
|
|
@@ -5499,8 +5656,9 @@ export class AgentFramework {
|
|
|
5499
5656
|
* Enter quiesce mode: write the deferred-write queue, persist the flag,
|
|
5500
5657
|
* suppress gate deliveries, pause all MCPL data planes, and wait for
|
|
5501
5658
|
* in-flight turns to settle (turn-alive is a turn token, which spans
|
|
5502
|
-
* dequeue → settled teardown,
|
|
5503
|
-
*
|
|
5659
|
+
* dequeue → settled teardown, a continuation of the turn on its way to its
|
|
5660
|
+
* start, or the turn's retry, which a provider hold keeps or which waits
|
|
5661
|
+
* to start: turnHolders). Idempotent, but for the queue's write, which
|
|
5504
5662
|
* every call makes first: when the queue can't reach disk, because its
|
|
5505
5663
|
* write fails or it's over the cap, the call rejects with a
|
|
5506
5664
|
* QuiesceBlockedError and the host's mode as it was, unless it has
|
|
@@ -5510,8 +5668,8 @@ export class AgentFramework {
|
|
|
5510
5668
|
* On drain timeout the host STAYS quiesced (`drained: false` in the
|
|
5511
5669
|
* result); with `abandon: true` the undrained streams are cancelled via a
|
|
5512
5670
|
* dedicated cancel kind that settles the turn without feeding the
|
|
5513
|
-
* inference-failure accounting, and a turn whose continuation
|
|
5514
|
-
* started ends with
|
|
5671
|
+
* inference-failure accounting, and a turn whose continuation or retry
|
|
5672
|
+
* hasn't started ends with it dropped. Drain covers the framework's
|
|
5515
5673
|
* own turns: a caller of the direct Agent API (`Agent.runInference`) owns
|
|
5516
5674
|
* its turns' timing.
|
|
5517
5675
|
*/
|
|
@@ -5638,7 +5796,7 @@ export class AgentFramework {
|
|
|
5638
5796
|
// to the continuation.
|
|
5639
5797
|
const handedOff = new Set();
|
|
5640
5798
|
for (const agentName of this.pendingContinuationAgents()) {
|
|
5641
|
-
console.error(`[host-mode] abandoning ${agentName}'s turn before its continuation starts:
|
|
5799
|
+
console.error(`[host-mode] abandoning ${agentName}'s turn before its continuation or held retry starts: it is dropped`);
|
|
5642
5800
|
this.dropContinuations(agentName, { kind: 'quiesce_abandoned', why: 'its turn was abandoned' });
|
|
5643
5801
|
handedOff.add(agentName);
|
|
5644
5802
|
}
|
|
@@ -8987,7 +9145,8 @@ export class AgentFramework {
|
|
|
8987
9145
|
* by stop() while it runs.
|
|
8988
9146
|
*
|
|
8989
9147
|
* The branch moves under every agent sharing the store, so the switch
|
|
8990
|
-
* reserves the store as a surgery does: every agent must be idle
|
|
9148
|
+
* reserves the store as a surgery does: every agent must be idle with no
|
|
9149
|
+
* turn alive (no continuation or held retry of one on its way), or it is
|
|
8991
9150
|
* refused (OperatorActionError 'agent-busy'), and no turn starts until the
|
|
8992
9151
|
* switch is done.
|
|
8993
9152
|
*/
|
|
@@ -9201,18 +9360,35 @@ export class AgentFramework {
|
|
|
9201
9360
|
throw new OperatorActionError('agent-busy', `Cannot ${verb}: a live ${this.surgeryHold.verb} for ${this.surgeryHold.agentName} is already in progress`);
|
|
9202
9361
|
}
|
|
9203
9362
|
const busy = [];
|
|
9363
|
+
let heldTurns = false;
|
|
9364
|
+
let statedWaits = false;
|
|
9204
9365
|
for (const [name, a] of this.agents) {
|
|
9205
|
-
if (a.state.status !== 'idle')
|
|
9366
|
+
if (a.state.status !== 'idle') {
|
|
9206
9367
|
busy.push(`${name} is ${a.state.status}`);
|
|
9207
|
-
|
|
9368
|
+
continue;
|
|
9369
|
+
}
|
|
9370
|
+
if (!this.hasTurn(name))
|
|
9371
|
+
continue;
|
|
9372
|
+
// A provider hold keeping the turn's retry is named: a quiesce alone
|
|
9373
|
+
// doesn't end that turn.
|
|
9374
|
+
const held = this.heldTurnRetry(name);
|
|
9375
|
+
if (held) {
|
|
9376
|
+
heldTurns = true;
|
|
9377
|
+
if (held.statedWait)
|
|
9378
|
+
statedWaits = true;
|
|
9379
|
+
busy.push(`${name} is idle+turn-alive (${held.says})`);
|
|
9380
|
+
}
|
|
9381
|
+
else {
|
|
9208
9382
|
busy.push(`${name} is idle+turn-alive`);
|
|
9383
|
+
}
|
|
9209
9384
|
}
|
|
9210
9385
|
for (const name of this.activeTurnTokens.keys()) {
|
|
9211
9386
|
if (!this.agents.has(name))
|
|
9212
9387
|
busy.push(`${name} is turn-alive`);
|
|
9213
9388
|
}
|
|
9214
9389
|
if (busy.length > 0) {
|
|
9215
|
-
throw new OperatorActionError('agent-busy', `Cannot ${verb} while ${busy.join(', ')} — every agent sharing the store must be idle (quiesce the host first)`
|
|
9390
|
+
throw new OperatorActionError('agent-busy', `Cannot ${verb} while ${busy.join(', ')} — every agent sharing the store must be idle (quiesce the host first)` +
|
|
9391
|
+
(heldTurns ? `; ${heldTurnWayOn(statedWaits)}` : ''));
|
|
9216
9392
|
}
|
|
9217
9393
|
const reserved = new Map();
|
|
9218
9394
|
for (const name of this.agents.keys()) {
|
|
@@ -9994,9 +10170,10 @@ export class AgentFramework {
|
|
|
9994
10170
|
// Direct inference requests (e.g. runEphemeralToCompletion) bypass the
|
|
9995
10171
|
// event queue — without this the loop can exit before they're drained.
|
|
9996
10172
|
// While quiesced, gate-parked requests are NOT progress and must not
|
|
9997
|
-
// block idle
|
|
9998
|
-
//
|
|
9999
|
-
|
|
10173
|
+
// block idle; what a quiesce lets through (passesQuiesce: a turn's
|
|
10174
|
+
// continuation, or a held turn's retry) can still run, and is.
|
|
10175
|
+
// Identity-equivalent to `.length > 0` when not quiesced.
|
|
10176
|
+
this.pendingRequests.some((r) => !this.quiesced || passesQuiesce(r)) ||
|
|
10000
10177
|
this.activeStreams.size > 0 ||
|
|
10001
10178
|
Array.from(this.agents.values()).some((a) => a.state.status !== 'idle')) {
|
|
10002
10179
|
await this.processNextEvent();
|
|
@@ -11553,9 +11730,12 @@ export class AgentFramework {
|
|
|
11553
11730
|
// (request dropped as stale, inference policy veto) would otherwise stay
|
|
11554
11731
|
// registered forever. No turn, no active stream and no pending request =
|
|
11555
11732
|
// done. A turn holds its token from dequeue, through its hooks and
|
|
11556
|
-
// compile, before its stream registers, so a fork mid-compile is busy
|
|
11733
|
+
// compile, before its stream registers, so a fork mid-compile is busy;
|
|
11734
|
+
// and a turn is alive while its continuation or a provider hold's retry
|
|
11735
|
+
// of it is on its way (hasTurn), so a closure turn held for its retry
|
|
11736
|
+
// finishes when the hold ends.
|
|
11557
11737
|
for (const agentName of [...this.closingConversationAgents]) {
|
|
11558
|
-
if (!this.
|
|
11738
|
+
if (!this.hasTurn(agentName) &&
|
|
11559
11739
|
!this.activeStreams.has(agentName) &&
|
|
11560
11740
|
!this.pendingRequests.some((r) => r.agentName === agentName)) {
|
|
11561
11741
|
this.disposeConversationAgent(agentName);
|
|
@@ -12342,6 +12522,24 @@ export class AgentFramework {
|
|
|
12342
12522
|
for (const cooldown of this.providerAccelerationCooldowns.values()) {
|
|
12343
12523
|
cooldown.heldRequests = cooldown.heldRequests.filter((r) => r.coalescingSubject !== subject);
|
|
12344
12524
|
}
|
|
12525
|
+
// A wake of the subject's merged into a start that carries a turn on
|
|
12526
|
+
// (MERGED_BATCH) is withdrawn from the start's batch, so no stop puts
|
|
12527
|
+
// it back and no pass unpacks it.
|
|
12528
|
+
const unrecord = (r) => {
|
|
12529
|
+
const batch = mergedBatch(r);
|
|
12530
|
+
if (batch?.some((w) => w.coalescingSubject === subject)) {
|
|
12531
|
+
r[MERGED_BATCH] = batch.filter((w) => w.coalescingSubject !== subject);
|
|
12532
|
+
}
|
|
12533
|
+
};
|
|
12534
|
+
for (const r of this.pendingRequests)
|
|
12535
|
+
unrecord(r);
|
|
12536
|
+
for (const cooldown of this.providerAccelerationCooldowns.values())
|
|
12537
|
+
cooldown.heldRequests.forEach(unrecord);
|
|
12538
|
+
for (const r of this.parkedContinuations ?? [])
|
|
12539
|
+
unrecord(r);
|
|
12540
|
+
for (const pass of this.passRequests ?? [])
|
|
12541
|
+
for (const requests of pass.values())
|
|
12542
|
+
requests.forEach(unrecord);
|
|
12345
12543
|
}
|
|
12346
12544
|
/**
|
|
12347
12545
|
* A placement in the deferred queue is upgraded in place once the flush has
|
|
@@ -12720,7 +12918,7 @@ export class AgentFramework {
|
|
|
12720
12918
|
});
|
|
12721
12919
|
console.error(`[inference-dropped] agent=${agentName} reason=agent_not_found requests=${requests.length}`);
|
|
12722
12920
|
// A continuation among them leaves its turn to end here.
|
|
12723
|
-
if (requests.some(
|
|
12921
|
+
if (requests.some(continuesTurn)) {
|
|
12724
12922
|
this.endContinuedTurn(agentName, { kind: 'dropped', why: 'its agent is gone' });
|
|
12725
12923
|
}
|
|
12726
12924
|
continue;
|
|
@@ -12731,11 +12929,8 @@ export class AgentFramework {
|
|
|
12731
12929
|
// selected now, the turn proceeds (the wait stays recorded).
|
|
12732
12930
|
const otherModel = providerCooldown.waitModel !== undefined && providerCooldown.waitModel !== agent.model;
|
|
12733
12931
|
if (!otherModel && (now < providerCooldown.until || this.extendHostProviderHold(agentName, providerCooldown))) {
|
|
12734
|
-
for (const req of requests)
|
|
12735
|
-
|
|
12736
|
-
providerCooldown.heldRequests.push(req);
|
|
12737
|
-
}
|
|
12738
|
-
}
|
|
12932
|
+
for (const req of requests)
|
|
12933
|
+
this.holdRequest(providerCooldown.heldRequests, req);
|
|
12739
12934
|
const recovery = this.providerAccelerationRecoveries.get(agentName);
|
|
12740
12935
|
if (recovery)
|
|
12741
12936
|
recovery.heldRequests = providerCooldown.heldRequests.length;
|
|
@@ -12751,6 +12946,16 @@ export class AgentFramework {
|
|
|
12751
12946
|
recovery.releasedAt = now;
|
|
12752
12947
|
requests = [...providerCooldown.heldRequests, ...requests];
|
|
12753
12948
|
}
|
|
12949
|
+
// A start that carries a turn on and came back before it took its
|
|
12950
|
+
// token stands for the batch it was merged from (MERGED_BATCH),
|
|
12951
|
+
// including one a hold released just now. The batch takes its place,
|
|
12952
|
+
// each request as it was, judged one by one from here: a wait holds
|
|
12953
|
+
// them apart, a quiesce parks the wakes while the turn's request
|
|
12954
|
+
// passes, and the read check and the policy decide on them all; the
|
|
12955
|
+
// merge below records the batch anew.
|
|
12956
|
+
if (requests.some((r) => mergedBatch(r) !== undefined)) {
|
|
12957
|
+
requests = requests.flatMap((r) => mergedBatch(r) ?? [r]);
|
|
12958
|
+
}
|
|
12754
12959
|
// A provider's stated wait on this agent's primary model, including one
|
|
12755
12960
|
// recorded before a restart, holds the turn until it passes. An
|
|
12756
12961
|
// ephemeral run records nothing, its name being single-use, so no wait,
|
|
@@ -12784,9 +12989,10 @@ export class AgentFramework {
|
|
|
12784
12989
|
// bounds a multi-day maintenance window's accumulation.
|
|
12785
12990
|
if (this.quiesced) {
|
|
12786
12991
|
// Continuations of a held turn (budget restart, non-streaming tool
|
|
12787
|
-
// results) pass
|
|
12788
|
-
|
|
12789
|
-
const
|
|
12992
|
+
// results) pass, and so does a held turn's retry (passesQuiesce), so
|
|
12993
|
+
// a turn alive at the quiesce can finish; everything else parks.
|
|
12994
|
+
const passThrough = requests.filter((r) => passesQuiesce(r));
|
|
12995
|
+
const parked = requests.filter((r) => !passesQuiesce(r));
|
|
12790
12996
|
// Coalesce per (reason, addressed): every channel wake shares one
|
|
12791
12997
|
// reason, so keying on reason alone would let an ambient message
|
|
12792
12998
|
// parked later evict a DM/mention parked earlier — and the
|
|
@@ -12888,7 +13094,7 @@ export class AgentFramework {
|
|
|
12888
13094
|
// A continuation inherits its predecessor's EventGate liveness and
|
|
12889
13095
|
// logical turn. If policy drops it, no driveStream remains to end them,
|
|
12890
13096
|
// so its turn ends here (endContinuedTurn).
|
|
12891
|
-
if (requests.some(
|
|
13097
|
+
if (requests.some(continuesTurn)) {
|
|
12892
13098
|
this.endContinuedTurn(agentName, { kind: 'dropped', why: 'the inference policy dropped its continuation' });
|
|
12893
13099
|
}
|
|
12894
13100
|
this.endPendingPrimary(agentName);
|
|
@@ -12970,7 +13176,7 @@ export class AgentFramework {
|
|
|
12970
13176
|
// exists only once assembly renders it. Recorded for the whole batch
|
|
12971
13177
|
// (not inherited from requests[0]), so a requeued trigger stays honest.
|
|
12972
13178
|
const batchOnly = !budgetRestart && requests.every((r) => r.coalescingBatch === true && !!r.coalescingSubject);
|
|
12973
|
-
|
|
13179
|
+
const merged = {
|
|
12974
13180
|
...trigger,
|
|
12975
13181
|
coalescingBatch: batchOnly || undefined,
|
|
12976
13182
|
coalescingBatchSubjects: batchOnly
|
|
@@ -12998,7 +13204,19 @@ export class AgentFramework {
|
|
|
12998
13204
|
counterparty: budgetRestart ? undefined : provReq?.counterparty,
|
|
12999
13205
|
wakeChannelId: budgetRestart ? undefined : provReq?.wakeChannelId,
|
|
13000
13206
|
wakeAt: budgetRestart ? undefined : provReq?.wakeAt,
|
|
13001
|
-
}
|
|
13207
|
+
};
|
|
13208
|
+
// A held turn's retry merged with other wakes leaves the start its
|
|
13209
|
+
// turn's retry (TURN_RETRY), whichever request leads the merge: held
|
|
13210
|
+
// again before it takes a token, it keeps the turn alive as before.
|
|
13211
|
+
const start = requests.some(isTurnRetry) ? asTurnRetry(merged) : merged;
|
|
13212
|
+
// A start that carries a turn on and took in wakes that don't records
|
|
13213
|
+
// the batch it was merged from (MERGED_BATCH): until it takes its
|
|
13214
|
+
// token it stands for them all.
|
|
13215
|
+
if (continuesTurn(start) && requests.some((r) => !continuesTurn(r)))
|
|
13216
|
+
start[MERGED_BATCH] = [...requests];
|
|
13217
|
+
else
|
|
13218
|
+
delete start[MERGED_BATCH];
|
|
13219
|
+
await this.startAgentStream(agent, start);
|
|
13002
13220
|
}
|
|
13003
13221
|
}
|
|
13004
13222
|
/**
|
|
@@ -15243,7 +15461,7 @@ export class AgentFramework {
|
|
|
15243
15461
|
// Do not await inside processInferenceRequests: one slow auxiliary call for
|
|
15244
15462
|
// this resident must not block events or other residents. Admission stays
|
|
15245
15463
|
// owned and the continuation receives it after the in-flight call settles.
|
|
15246
|
-
const parkedContinuation = trigger !== undefined &&
|
|
15464
|
+
const parkedContinuation = trigger !== undefined && continuesTurn(trigger);
|
|
15247
15465
|
if (parkedContinuation)
|
|
15248
15466
|
this.parkedContinuations.add(trigger);
|
|
15249
15467
|
void this.waitForAuxiliaryIdle(agent.name).then(async () => {
|
|
@@ -15277,8 +15495,9 @@ export class AgentFramework {
|
|
|
15277
15495
|
// park; starting here would bypass it and run a turn against a
|
|
15278
15496
|
// context the operator believes frozen. Give admission back and
|
|
15279
15497
|
// requeue — the scheduler re-parks it (coalesced) until resume.
|
|
15280
|
-
// Continuations of a held turn
|
|
15281
|
-
|
|
15498
|
+
// Continuations of a held turn, and a held turn's retry, pass, as
|
|
15499
|
+
// they do at the gate (passesQuiesce).
|
|
15500
|
+
if (this.quiesced && !passesQuiesce(trigger)) {
|
|
15282
15501
|
this.releasePrimaryProviderGate(agent.name);
|
|
15283
15502
|
this.pendingRequests.push(trigger ?? {
|
|
15284
15503
|
agentName: agent.name, reason: 'provider-admission:requeue', source: 'scheduler', timestamp: Date.now(),
|
|
@@ -15286,6 +15505,16 @@ export class AgentFramework {
|
|
|
15286
15505
|
console.error(`[provider-admission] ${agent.name}: host quiesced while parked — wake requeued, not started`);
|
|
15287
15506
|
return;
|
|
15288
15507
|
}
|
|
15508
|
+
// A start that stands for a merged batch (MERGED_BATCH) goes back
|
|
15509
|
+
// to the scheduler as that batch: its gate lets the turn's request
|
|
15510
|
+
// through alone and parks the wakes until resume.
|
|
15511
|
+
const batch = trigger && mergedBatch(trigger);
|
|
15512
|
+
if (this.quiesced && batch) {
|
|
15513
|
+
this.releasePrimaryProviderGate(agent.name);
|
|
15514
|
+
this.pendingRequests.push(...batch);
|
|
15515
|
+
console.error(`[provider-admission] ${agent.name}: host quiesced while parked — the merged start goes back as its ${batch.length} requests`);
|
|
15516
|
+
return;
|
|
15517
|
+
}
|
|
15289
15518
|
await this.startAgentStream(agent, trigger, attempt, true);
|
|
15290
15519
|
}).catch((error) => {
|
|
15291
15520
|
this.releasePrimaryProviderGate(agent.name);
|
|
@@ -15424,12 +15653,25 @@ export class AgentFramework {
|
|
|
15424
15653
|
* caller has ended the turn already.
|
|
15425
15654
|
*/
|
|
15426
15655
|
dropContinuations(agentName, drop) {
|
|
15427
|
-
const isContinuation = (r) => r.agentName === agentName &&
|
|
15656
|
+
const isContinuation = (r) => r.agentName === agentName && continuesTurn(r);
|
|
15657
|
+
// A caller's stop and an abandon end the turn, not the wakes merged into
|
|
15658
|
+
// a start of it that hasn't taken its token (MERGED_BATCH): they go back
|
|
15659
|
+
// in the queue. A stop() or a disposal ends the agent's work, and puts
|
|
15660
|
+
// nothing back.
|
|
15661
|
+
const putsBack = drop?.kind === 'caller' || drop?.kind === 'quiesce_abandoned';
|
|
15662
|
+
const back = [];
|
|
15428
15663
|
let dropped = false;
|
|
15429
15664
|
const take = (requests) => {
|
|
15430
|
-
const left =
|
|
15431
|
-
|
|
15432
|
-
|
|
15665
|
+
const left = [];
|
|
15666
|
+
for (const r of requests) {
|
|
15667
|
+
if (!isContinuation(r))
|
|
15668
|
+
left.push(r);
|
|
15669
|
+
else {
|
|
15670
|
+
dropped = true;
|
|
15671
|
+
if (putsBack)
|
|
15672
|
+
back.push(...mergedWakes(r));
|
|
15673
|
+
}
|
|
15674
|
+
}
|
|
15433
15675
|
return left;
|
|
15434
15676
|
};
|
|
15435
15677
|
this.pendingRequests = take(this.pendingRequests);
|
|
@@ -15446,6 +15688,8 @@ export class AgentFramework {
|
|
|
15446
15688
|
this.parkedContinuations.delete(parked);
|
|
15447
15689
|
this.releasePrimaryProviderGate(agentName);
|
|
15448
15690
|
dropped = true;
|
|
15691
|
+
if (putsBack)
|
|
15692
|
+
back.push(...mergedWakes(parked));
|
|
15449
15693
|
}
|
|
15450
15694
|
for (const pass of this.passRequests) {
|
|
15451
15695
|
const requests = pass.get(agentName);
|
|
@@ -15455,6 +15699,7 @@ export class AgentFramework {
|
|
|
15455
15699
|
requests.length = 0;
|
|
15456
15700
|
requests.push(...left);
|
|
15457
15701
|
}
|
|
15702
|
+
this.pendingRequests.push(...back);
|
|
15458
15703
|
if (!dropped || !drop)
|
|
15459
15704
|
return;
|
|
15460
15705
|
// A primary a provider hold left pending for what it released won't run
|
|
@@ -15481,11 +15726,11 @@ export class AgentFramework {
|
|
|
15481
15726
|
const startedAt = this.lastInferenceAt.get(agentName)?.startedAt ?? Date.now();
|
|
15482
15727
|
if (agent && drop.kind !== 'dropped') {
|
|
15483
15728
|
const routing = this.openLogical.get(agentName) ?? this.routingOf(agentName);
|
|
15484
|
-
this.endStoppedStart(agent, { kind: drop.kind }, routing, startedAt, undefined, false);
|
|
15729
|
+
this.endStoppedStart(agent, drop.kind === 'caller' ? { kind: 'caller', reason: drop.reason } : { kind: drop.kind }, routing, startedAt, undefined, false);
|
|
15485
15730
|
}
|
|
15486
15731
|
else {
|
|
15487
15732
|
const owner = this.activeTurnTokens.get(agentName);
|
|
15488
|
-
this.abortDroppedTurn(agentName, owner ?? -1, drop.kind === 'dropped' ? CONTINUATION_DROPPED : drop.kind, startedAt);
|
|
15733
|
+
this.abortDroppedTurn(agentName, owner ?? -1, drop.kind === 'dropped' ? CONTINUATION_DROPPED : drop.kind === 'caller' ? (drop.reason ?? 'user') : drop.kind, startedAt);
|
|
15489
15734
|
this.channelRegistry?.stopTyping();
|
|
15490
15735
|
if (agent) {
|
|
15491
15736
|
agent.toolResultGuard.recovering = false;
|
|
@@ -15508,6 +15753,34 @@ export class AgentFramework {
|
|
|
15508
15753
|
this.ackDeferredWrites();
|
|
15509
15754
|
}
|
|
15510
15755
|
}
|
|
15756
|
+
/**
|
|
15757
|
+
* After a failure a provider hold would take: once stop() has closed
|
|
15758
|
+
* admission, the hold installs nothing (holdForProviderWait,
|
|
15759
|
+
* holdProviderAcceleration), so no retry follows the turn's
|
|
15760
|
+
* inference:failed, and the turn ends as a retry a stop cancelled ends, with
|
|
15761
|
+
* an inference:aborted saying so. Nothing is written. True when it ended
|
|
15762
|
+
* the turn.
|
|
15763
|
+
*/
|
|
15764
|
+
endFailureAtStop(agentName, turnToken, startedAt) {
|
|
15765
|
+
if (!this.providerAdmissionClosed)
|
|
15766
|
+
return false;
|
|
15767
|
+
this.abortDroppedTurn(agentName, turnToken, RETRY_CANCELLED_BY_STOP, startedAt);
|
|
15768
|
+
return true;
|
|
15769
|
+
}
|
|
15770
|
+
/**
|
|
15771
|
+
* A turn's start whose failure a provider hold took (or, once stop() has
|
|
15772
|
+
* closed admission, would have: endFailureAtStop) ends here. Its window
|
|
15773
|
+
* closes now, not at the start's finally a few microtasks on: no check of
|
|
15774
|
+
* this start is ahead, and a stop made after the failure's report must
|
|
15775
|
+
* find the held retry (abortInference's drop), not a window nothing will
|
|
15776
|
+
* collect.
|
|
15777
|
+
*/
|
|
15778
|
+
endHeldStart(agent, checkWindow, turnToken, startedAt) {
|
|
15779
|
+
if (this.checkAhead.get(agent) === checkWindow)
|
|
15780
|
+
this.checkAhead.delete(agent);
|
|
15781
|
+
this.endFailureAtStop(agent.name, turnToken, startedAt);
|
|
15782
|
+
this.eventGate?.onInferenceEnded(agent.name);
|
|
15783
|
+
}
|
|
15511
15784
|
/**
|
|
15512
15785
|
* End a turn that beginAgentTurn drops before it sends anything. The turn
|
|
15513
15786
|
* emitted inference:started, and listeners that bracket a turn, such as a
|
|
@@ -15726,6 +15999,12 @@ export class AgentFramework {
|
|
|
15726
15999
|
const { coalescingSubject: _subject, coalescingBatch: _batch, coalescingBatchSubjects: _subjects, readsDeferredWrite: _read, ...retry } = trigger;
|
|
15727
16000
|
trigger = retry;
|
|
15728
16001
|
}
|
|
16002
|
+
// The wakes merged into this start are the turn's now: it reads them,
|
|
16003
|
+
// so a later drop of its retry puts none of them back (MERGED_BATCH).
|
|
16004
|
+
if (trigger && mergedBatch(trigger)) {
|
|
16005
|
+
const { [MERGED_BATCH]: _batch, ...own } = trigger;
|
|
16006
|
+
trigger = own;
|
|
16007
|
+
}
|
|
15729
16008
|
// Record turn checkpoint before inference (only on first attempt, not retries)
|
|
15730
16009
|
if (attempt === 0 && trigger?.reason !== 'tool_result_guard_retry') {
|
|
15731
16010
|
this.recordTurnCheckpoint(agent.name);
|
|
@@ -16011,9 +16290,8 @@ export class AgentFramework {
|
|
|
16011
16290
|
// hold the turn as the boundary would rather than send it into the wait.
|
|
16012
16291
|
if (this.conversationAgentHomes.has(agent.name)) {
|
|
16013
16292
|
const wait = this.providerWaits?.active(agent.name, agent.model);
|
|
16014
|
-
|
|
16015
|
-
|
|
16016
|
-
`(recorded while the turn compiled); turn held, nothing sent`);
|
|
16293
|
+
// The turn has begun (inference:started), so the hold keeps its retry.
|
|
16294
|
+
if (wait && this.holdForProviderWait(agent, wait, trigger ? [asTurnRetry(trigger)] : [], false, () => {
|
|
16017
16295
|
// The stream the compile made won't be sent: it's cancelled, and
|
|
16018
16296
|
// this start's window closes, before the hold is reported, so a stop
|
|
16019
16297
|
// made while inference:failed is delivered finds neither, and the
|
|
@@ -16025,8 +16303,15 @@ export class AgentFramework {
|
|
|
16025
16303
|
this.checkAhead.delete(agent);
|
|
16026
16304
|
// The turn emitted inference:started before its compile; every turn
|
|
16027
16305
|
// that does gets one terminal event, which listeners (a typing loop)
|
|
16028
|
-
// rely on
|
|
16306
|
+
// rely on. It's reported before the hold takes the turn's retry, as
|
|
16307
|
+
// at every failure site, so a listener's stop finds nothing held.
|
|
16029
16308
|
this.emitTrace({ type: 'inference:failed', agentName: agent.name, error: providerWaitText(agent.name, agent.model, wait) });
|
|
16309
|
+
})) {
|
|
16310
|
+
console.error(`[provider-wait] agent=${agent.name} model=${agent.model} binds after the compile ` +
|
|
16311
|
+
`(recorded while the turn compiled); ${this.providerAdmissionClosed ? 'the framework is stopping, so nothing is held'
|
|
16312
|
+
: this.providerAccelerationCooldowns.get(agent.name)?.heldRequests.some(isTurnRetry) ? 'turn held'
|
|
16313
|
+
: 'the wait was released as the failure was reported, so the retry is queued'}, nothing sent`);
|
|
16314
|
+
this.endFailureAtStop(agent.name, turnToken, turnStartedAt);
|
|
16030
16315
|
this.activeStreams.delete(agent.name);
|
|
16031
16316
|
this.pendingAssistantBlocks.delete(agent.name);
|
|
16032
16317
|
this.channelRegistry?.stopTyping();
|
|
@@ -16101,14 +16386,16 @@ export class AgentFramework {
|
|
|
16101
16386
|
if (ownsProviderGate && this.holdProviderAcceleration(agent, err, trigger)) {
|
|
16102
16387
|
// Capacity scheduling is not poisoned history. The cooldown timer owns
|
|
16103
16388
|
// one later fresh compile; no immediate retry or exhaustion marker.
|
|
16104
|
-
|
|
16389
|
+
// (Once stop() has closed admission, nothing is held, and the turn
|
|
16390
|
+
// ends here: endFailureAtStop.)
|
|
16391
|
+
this.endHeldStart(agent, checkWindow, turnToken, turnStartedAt);
|
|
16105
16392
|
return false;
|
|
16106
16393
|
}
|
|
16107
16394
|
const statedWait = ownsProviderGate ? undefined : this.nonOwningProviderWait(agent, err, trigger);
|
|
16108
16395
|
if (statedWait === 'held') {
|
|
16109
16396
|
// A conversation agent held by the provider's stated wait, as a
|
|
16110
16397
|
// resident is: the hold's release owns the later compile.
|
|
16111
|
-
this.
|
|
16398
|
+
this.endHeldStart(agent, checkWindow, turnToken, turnStartedAt);
|
|
16112
16399
|
return false;
|
|
16113
16400
|
}
|
|
16114
16401
|
const action = statedWait === 'fail' ? { retry: false } : this.errorPolicy.onInferenceError(err, agent.name, attempt);
|
|
@@ -16141,7 +16428,16 @@ export class AgentFramework {
|
|
|
16141
16428
|
}
|
|
16142
16429
|
// The retry re-enters startAgentStream, which replaces this frame's
|
|
16143
16430
|
// turn token with its own; cleanup belongs to the innermost frame.
|
|
16144
|
-
|
|
16431
|
+
// The retry is its turn's (TURN_RETRY): this frame has begun the turn,
|
|
16432
|
+
// and once the retry's start is held at the last admission boundary
|
|
16433
|
+
// (a conversation agent's own compression can record a wait during
|
|
16434
|
+
// the policy's delay), this frame lets go of its token. No check of
|
|
16435
|
+
// this start is ahead now, so its window closes first: a stop made
|
|
16436
|
+
// once the retry is held finds the held retry (abortInference's
|
|
16437
|
+
// drop), not a window nothing will collect.
|
|
16438
|
+
if (this.checkAhead.get(agent) === checkWindow)
|
|
16439
|
+
this.checkAhead.delete(agent);
|
|
16440
|
+
await this.startAgentStream(agent, trigger && asTurnRetry(trigger), attempt + 1);
|
|
16145
16441
|
}
|
|
16146
16442
|
else {
|
|
16147
16443
|
// Terminal failure with no driveStream: this frame owns teardown.
|
|
@@ -17625,13 +17921,17 @@ export class AgentFramework {
|
|
|
17625
17921
|
if (agent.streamId !== myStreamId)
|
|
17626
17922
|
break;
|
|
17627
17923
|
this.abandonAgentScripts(agent.name, 'stream error', streamRouting);
|
|
17924
|
+
// A hold takes the turn's retry, or, once stop() has closed
|
|
17925
|
+
// admission, the turn ends here (endFailureAtStop).
|
|
17628
17926
|
if (ownsProviderGate && this.holdProviderAcceleration(agent, err, trigger)) {
|
|
17927
|
+
this.endFailureAtStop(agent.name, myTurnToken ?? -1, startTime);
|
|
17629
17928
|
if (this.agents.get(agent.name) === agent && agent.streamId === myStreamId)
|
|
17630
17929
|
this.eventGate?.onInferenceEnded(agent.name);
|
|
17631
17930
|
break;
|
|
17632
17931
|
}
|
|
17633
17932
|
const statedWait = ownsProviderGate ? undefined : this.nonOwningProviderWait(agent, err, trigger);
|
|
17634
17933
|
if (statedWait === 'held') {
|
|
17934
|
+
this.endFailureAtStop(agent.name, myTurnToken ?? -1, startTime);
|
|
17635
17935
|
if (this.agents.get(agent.name) === agent && agent.streamId === myStreamId)
|
|
17636
17936
|
this.eventGate?.onInferenceEnded(agent.name);
|
|
17637
17937
|
break;
|
|
@@ -17670,7 +17970,8 @@ export class AgentFramework {
|
|
|
17670
17970
|
}
|
|
17671
17971
|
return;
|
|
17672
17972
|
}
|
|
17673
|
-
|
|
17973
|
+
// Marked as its turn's retry, as at the start's failure.
|
|
17974
|
+
await this.startAgentStream(agent, trigger && asTurnRetry(trigger), attempt + 1);
|
|
17674
17975
|
}
|
|
17675
17976
|
else {
|
|
17676
17977
|
// The turn ends here, and its inference:failed doesn't say so,
|
|
@@ -17980,10 +18281,16 @@ export class AgentFramework {
|
|
|
17980
18281
|
const durationMs = Date.now() - startTime;
|
|
17981
18282
|
// The stream won't go on: the agent stops holding it before the
|
|
17982
18283
|
// failure is reported, as at the stream's error event, so a stop made
|
|
17983
|
-
// while inference:failed is delivered finds no stream.
|
|
18284
|
+
// while inference:failed is delivered finds no stream. It's reported
|
|
18285
|
+
// once, before a provider hold takes the turn's retry, as at the
|
|
18286
|
+
// stream's error event and at a start's failure: a listener's stop
|
|
18287
|
+
// finds nothing held, and one made after it ends the held turn.
|
|
17984
18288
|
agent.reset();
|
|
18289
|
+
this.emitTrace({ type: 'inference:failed', agentName: agent.name, error: err.message, stack: err.stack });
|
|
17985
18290
|
if (ownsProviderGate && this.holdProviderAcceleration(agent, err, trigger)) {
|
|
17986
|
-
|
|
18291
|
+
// Once stop() has closed admission, nothing is held, and the turn
|
|
18292
|
+
// ends here (endFailureAtStop).
|
|
18293
|
+
this.endFailureAtStop(agent.name, myTurnToken ?? -1, startTime);
|
|
17987
18294
|
logStream({ success: false,
|
|
17988
18295
|
error: `Provider acceleration cooldown: ${err.message}`,
|
|
17989
18296
|
request: compiledRequest ?? { note: 'streaming request rate-limited' }, durationMs });
|
|
@@ -18000,7 +18307,7 @@ export class AgentFramework {
|
|
|
18000
18307
|
// says why it ends.
|
|
18001
18308
|
const statedWait = ownsProviderGate ? undefined : this.nonOwningProviderWait(agent, err, trigger);
|
|
18002
18309
|
if (statedWait === 'held') {
|
|
18003
|
-
this.
|
|
18310
|
+
this.endFailureAtStop(agent.name, myTurnToken ?? -1, startTime);
|
|
18004
18311
|
logStream({ success: false,
|
|
18005
18312
|
error: `Provider wait: ${err.message}`,
|
|
18006
18313
|
request: compiledRequest ?? { note: 'streaming request rate-limited' }, durationMs });
|
|
@@ -18011,12 +18318,6 @@ export class AgentFramework {
|
|
|
18011
18318
|
this.eventGate?.onInferenceEnded(agent.name);
|
|
18012
18319
|
return;
|
|
18013
18320
|
}
|
|
18014
|
-
this.emitTrace({
|
|
18015
|
-
type: 'inference:failed',
|
|
18016
|
-
agentName: agent.name,
|
|
18017
|
-
error: err.message,
|
|
18018
|
-
stack: err.stack,
|
|
18019
|
-
});
|
|
18020
18321
|
this.settleAgent(agent.name, {
|
|
18021
18322
|
stopReason: 'exhausted',
|
|
18022
18323
|
speech: '',
|
|
@@ -18224,8 +18525,11 @@ export class AgentFramework {
|
|
|
18224
18525
|
// frame that ends the turn disposes it, after the flush above, so what
|
|
18225
18526
|
// was held for the turn's end lands in the fork's store instead of
|
|
18226
18527
|
// being dropped as addressed to no agent. A frame that hands the turn
|
|
18227
|
-
// to a successor (a guard or budget restart) leaves the fork to it
|
|
18228
|
-
|
|
18528
|
+
// to a successor (a guard or budget restart) leaves the fork to it, and
|
|
18529
|
+
// so does one whose failure a provider hold keeps for its retry: the
|
|
18530
|
+
// turn isn't over (pendingContinuationAgents).
|
|
18531
|
+
if (frameReachedTerminal && ownsPhysicalStream && this.closingConversationAgents.has(agent.name)
|
|
18532
|
+
&& !this.pendingContinuationAgents().has(agent.name)) {
|
|
18229
18533
|
this.disposeConversationAgent(agent.name);
|
|
18230
18534
|
}
|
|
18231
18535
|
}
|
|
@@ -18307,8 +18611,14 @@ export class AgentFramework {
|
|
|
18307
18611
|
}
|
|
18308
18612
|
if (agent.state.status !== 'idle' || this.hasTurn(agentName)) {
|
|
18309
18613
|
const shown = agent.state.status === 'idle' ? 'idle+turn-alive' : agent.state.status;
|
|
18310
|
-
|
|
18311
|
-
|
|
18614
|
+
// A turn a provider hold keeps for its retry has no stream: the refusal
|
|
18615
|
+
// names the hold, and how to go on sooner, as surgery's does.
|
|
18616
|
+
const held = agent.state.status === 'idle' ? this.heldTurnRetry(agentName) : undefined;
|
|
18617
|
+
throw new Error(held
|
|
18618
|
+
? `puppet refused: agent ${agentName} is idle+turn-alive (${held.says}; requires idle); ` +
|
|
18619
|
+
heldTurnWayOn(held.statedWait)
|
|
18620
|
+
: `puppet refused: agent ${agentName} is ${shown} (requires idle — ` +
|
|
18621
|
+
`injecting a turn under an active stream corrupts wire ordering)`);
|
|
18312
18622
|
}
|
|
18313
18623
|
const onSurface = this.getToolsForAgent(agentName)
|
|
18314
18624
|
.some((t) => t.name === toolName && agent.canUseTool(t.name));
|
|
@@ -19233,7 +19543,9 @@ export class AgentFramework {
|
|
|
19233
19543
|
* content where it sent one, `false`, `0` and `null` included, else null.
|
|
19234
19544
|
* A result that can't be converted to JSON (a cyclic object, a BigInt),
|
|
19235
19545
|
* which the dispatch's own check makes unlikely here, is a failure naming
|
|
19236
|
-
* the tool and saying the call itself completed.
|
|
19546
|
+
* the tool and saying the call itself completed. So is an MCPL peer's
|
|
19547
|
+
* result whose blocks were too deep for the dispatch's treatment, which
|
|
19548
|
+
* the dispatch marks (SCRIPT_RESULT_UNCONVERTIBLE).
|
|
19237
19549
|
*/
|
|
19238
19550
|
async scriptResultAnswer(result, toolName, toServer) {
|
|
19239
19551
|
const structured = Object.prototype.hasOwnProperty.call(result, 'structured');
|
|
@@ -19277,6 +19589,10 @@ export class AgentFramework {
|
|
|
19277
19589
|
brief: error,
|
|
19278
19590
|
};
|
|
19279
19591
|
};
|
|
19592
|
+
// An MCPL peer's result whose blocks JSON couldn't hold, so that RFC-005's
|
|
19593
|
+
// treatment couldn't be given them at dispatch.
|
|
19594
|
+
if (SCRIPT_RESULT_UNCONVERTIBLE.has(result))
|
|
19595
|
+
return failed(SCRIPT_RESULT_UNCONVERTIBLE.get(result));
|
|
19280
19596
|
let content;
|
|
19281
19597
|
let text;
|
|
19282
19598
|
// A structured result's blocks, as #538 gives them: none when its content
|
|
@@ -24507,8 +24823,8 @@ export class AgentFramework {
|
|
|
24507
24823
|
});
|
|
24508
24824
|
}
|
|
24509
24825
|
/** An MCP tool call's trace: a result the server marked an error (or one
|
|
24510
|
-
* the reading
|
|
24511
|
-
* paths; anything else is tool:completed. */
|
|
24826
|
+
* the reading takes as one, such as malformed content) is tool:failed, as
|
|
24827
|
+
* on the other tool paths; anything else is tool:completed. */
|
|
24512
24828
|
traceMcpToolOutcome(serverId, tool, callId, result, durationMs) {
|
|
24513
24829
|
if (result.isError) {
|
|
24514
24830
|
this.emitTrace({ type: 'tool:failed', module: `mcpl:${serverId}`, tool, callId, durationMs, error: result.error ?? 'tool returned an error' });
|
|
@@ -24917,17 +25233,44 @@ export class AgentFramework {
|
|
|
24917
25233
|
// generated JSON) stands in for what the server sent. An MCPL peer's
|
|
24918
25234
|
// content blocks are the exception: what a script prints comes back to
|
|
24919
25235
|
// the model, so each gets the treatment RFC-005 gives it on every lane
|
|
24920
|
-
// the model reads, here, where the server that sent them is known
|
|
24921
|
-
//
|
|
24922
|
-
//
|
|
25236
|
+
// the model reads, here, where the server that sent them is known, and
|
|
25237
|
+
// so does what RFC-005 governs at any depth within them, by the same
|
|
25238
|
+
// peer reading, bound to that server (shownToScriptFromServer). A
|
|
25239
|
+
// module's blocks get it at
|
|
25240
|
+
// the script lane's exit (scriptResultAnswer). A standard peer's blocks
|
|
25241
|
+
// reach the script as sent.
|
|
24923
25242
|
const forScript = this.scriptToolWaiters?.has(call.id) ?? false;
|
|
24924
25243
|
const reading = async (result, mcpl) => {
|
|
24925
25244
|
if (!forScript || !Object.prototype.hasOwnProperty.call(result, 'structuredContent')) {
|
|
24926
25245
|
return this.mcpToolResult(result, call.id, mcpl, serverId, call.name);
|
|
24927
25246
|
}
|
|
24928
25247
|
const direct = AgentFramework.directToolResult(result, serverId, call.name);
|
|
24929
|
-
if (mcpl)
|
|
24930
|
-
|
|
25248
|
+
if (mcpl) {
|
|
25249
|
+
if (!Array.isArray(direct.data))
|
|
25250
|
+
return direct;
|
|
25251
|
+
// Each block by the classifier, then what's nested in each, at any
|
|
25252
|
+
// depth. The blocks came from the wire as JSON, so the round trip
|
|
25253
|
+
// changes nothing else. But a value that parsed can still fail to
|
|
25254
|
+
// go back: past some depth JSON.stringify overflows the stack where
|
|
25255
|
+
// JSON.parse didn't (on Node 24.19 an array 5,000 deep does, on Bun
|
|
25256
|
+
// 1.4.4 one 200,000 deep). That fails the script lane's treatment
|
|
25257
|
+
// alone, and the peer answered: the result keeps the peer's outcome
|
|
25258
|
+
// for the trace and the call's terminal, and the script lane's exit
|
|
25259
|
+
// answers it as a result it can't convert, not as a failed dispatch
|
|
25260
|
+
// (SCRIPT_RESULT_UNCONVERTIBLE). Its blocks are dropped, since they
|
|
25261
|
+
// are untreated and the result travels on, to the waiter and to the
|
|
25262
|
+
// listeners of its event's trace. Its structured content is kept, as
|
|
25263
|
+
// every result keeps it, since RFC-005's treatment doesn't reach it.
|
|
25264
|
+
const blocks = direct.data.map((block) => referenceBlockForScript(block, serverId));
|
|
25265
|
+
try {
|
|
25266
|
+
return { ...direct, data: JSON.parse(JSON.stringify(blocks, (_key, value) => shownToScriptFromServer(value, serverId))) };
|
|
25267
|
+
}
|
|
25268
|
+
catch (err) {
|
|
25269
|
+
const unconvertible = { ...direct, data: [] };
|
|
25270
|
+
SCRIPT_RESULT_UNCONVERTIBLE.set(unconvertible, err);
|
|
25271
|
+
return unconvertible;
|
|
25272
|
+
}
|
|
25273
|
+
}
|
|
24931
25274
|
// A standard peer's content that isn't a list of blocks is a malformed
|
|
24932
25275
|
// result on the model's reading (normalizeStandardToolResult), so the
|
|
24933
25276
|
// script reads it that way too, its structured content kept.
|
|
@@ -25125,7 +25468,8 @@ export class AgentFramework {
|
|
|
25125
25468
|
* direct path (a module's callTool, a channel's tool call) doesn't come
|
|
25126
25469
|
* here: it returns the server's result itself (directToolResult), for
|
|
25127
25470
|
* either engine, as does a script's call whose result is structured, with
|
|
25128
|
-
* an MCPL peer's blocks given RFC-005's treatment (referenceBlockForScript)
|
|
25471
|
+
* an MCPL peer's blocks given RFC-005's treatment (referenceBlockForScript),
|
|
25472
|
+
* and what's nested in them at any depth (shownToScriptFromServer).
|
|
25129
25473
|
*/
|
|
25130
25474
|
async mcpToolResult(result, callId, mcplPeer, serverId, callName) {
|
|
25131
25475
|
const hasStructured = Object.prototype.hasOwnProperty.call(result, 'structuredContent');
|