@animalabs/agent-framework 0.26.0 → 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 +141 -24
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +781 -133
- package/dist/src/framework.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +24 -3
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +34 -11
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +61 -27
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +11 -5
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +21 -6
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/tool-result-history.d.ts +25 -0
- package/dist/src/tool-result-history.d.ts.map +1 -1
- package/dist/src/tool-result-history.js +44 -3
- package/dist/src/tool-result-history.js.map +1 -1
- package/dist/src/types/events.d.ts +7 -2
- package/dist/src/types/events.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/framework.ts +781 -135
- package/src/mcpl/channel-registry.ts +23 -4
- package/src/mcpl/push-coalescer.ts +83 -34
- package/src/mcpl/references.ts +22 -7
- package/src/tool-result-history.ts +43 -3
- package/src/types/events.ts +7 -2
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, 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,17 +775,27 @@ 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
|
|
713
785
|
* revive the earlier lifecycle's turn in the next one. */
|
|
714
786
|
const RETRY_ACROSS_RESTART = 'a retry waiting while the framework was stopped and started again is dropped before it sends anything';
|
|
787
|
+
/** The reason on the inference:aborted that ends a turn whose continuation
|
|
788
|
+
* was dropped before it started, other than by an abandon or a stop. */
|
|
789
|
+
const CONTINUATION_DROPPED = "a turn's continuation was dropped before it started";
|
|
715
790
|
function isTurnContinuation(reason) {
|
|
716
791
|
return reason === 'context_budget_restart' || reason === 'tool_results_ready'
|
|
717
792
|
|| reason === 'tool_result_guard_retry';
|
|
718
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
|
+
}
|
|
719
799
|
/** Wake sources the framework generates for itself (bounce wakes,
|
|
720
800
|
* continuations, retries, unsticks, admission requeues). A wake from any
|
|
721
801
|
* other source — a channel message, a heartbeat or other server event, a
|
|
@@ -1187,12 +1267,14 @@ function scriptResultBrief(content) {
|
|
|
1187
1267
|
}
|
|
1188
1268
|
/**
|
|
1189
1269
|
* What a script gets for a tool result that can't be converted to JSON,
|
|
1190
|
-
* 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.
|
|
1191
1272
|
* It names the tool, and says the call itself completed, so a script that
|
|
1192
1273
|
* reports it doesn't invite a blind retry. The host's log gets the same
|
|
1193
|
-
* line, since the tool
|
|
1194
|
-
* script reports it. The reason and the
|
|
1195
|
-
* 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.
|
|
1196
1278
|
*/
|
|
1197
1279
|
function unconvertibleScriptResult(toolName, err, ownError) {
|
|
1198
1280
|
let reason;
|
|
@@ -1221,13 +1303,15 @@ const SCRIPT_TEXT_ANSWER_ENVELOPE_BYTES = Buffer.byteLength(JSON.stringify({ con
|
|
|
1221
1303
|
* memoryMaxBytes. So the figure moves with memoryMaxBytes. What it can't
|
|
1222
1304
|
* count: a result's `structured` value, which the measure leaves out and the
|
|
1223
1305
|
* answer carries as JSON beside the content, and that result's blocks past
|
|
1224
|
-
* their text, which the answer carries as
|
|
1225
|
-
* keys, an image's data); nesting past 2,000 levels; and a
|
|
1306
|
+
* their text, which the answer carries as the script lane gives them (each
|
|
1307
|
+
* further block's keys, an image's data); nesting past 2,000 levels; and a
|
|
1226
1308
|
* parse that needs more of the script's memory than is free, as for JSON
|
|
1227
1309
|
* dense in small objects or a script already holding most of its memory.
|
|
1228
1310
|
* Such a result, or one past the figure that doesn't fit, reaches the script
|
|
1229
1311
|
* as ResultTooLarge, saying the call took effect, and nothing of it is cut or
|
|
1230
|
-
* 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.
|
|
1231
1315
|
*/
|
|
1232
1316
|
const scriptResultMaxChars = (memoryMaxBytes) => Math.max(0, Math.floor((Math.min(maxResultBytes(memoryMaxBytes), bufferConstants.MAX_STRING_LENGTH) - SCRIPT_TEXT_ANSWER_ENVELOPE_BYTES) / 6));
|
|
1233
1317
|
/**
|
|
@@ -1918,6 +2002,15 @@ export class AgentFramework {
|
|
|
1918
2002
|
// and its settle except that turn's own blocks, and, before its checkpoint,
|
|
1919
2003
|
// the inputs its start writes (flushForStart).
|
|
1920
2004
|
activeTurnTokens = new Map();
|
|
2005
|
+
/** Each running scheduler pass's requests, which it took from
|
|
2006
|
+
* pendingRequests: it holds those of the agents it hasn't taken up yet
|
|
2007
|
+
* while it awaits an earlier agent's start, so a continuation among them
|
|
2008
|
+
* is still on its way (pendingContinuationAgents). */
|
|
2009
|
+
passRequests = new Set();
|
|
2010
|
+
/** Continuations whose start waits on provider admission behind an
|
|
2011
|
+
* auxiliary call, in no queue meanwhile. A drop takes one out and gives
|
|
2012
|
+
* its admission back; the park, woken, then finds it gone. */
|
|
2013
|
+
parkedContinuations = new Set();
|
|
1921
2014
|
/** Each turn whose settle has ended it (settleAgent, endDroppedTurn), for
|
|
1922
2015
|
* its agent's scripts: its start's teardown can still hold the agent's
|
|
1923
2016
|
* token, and a turn that has ended isn't live (liveTurns). A later start
|
|
@@ -1962,12 +2055,14 @@ export class AgentFramework {
|
|
|
1962
2055
|
* is still the agent's, so a start that ends after a later frame opened
|
|
1963
2056
|
* one, such as its retry's stream waiting to retry, leaves that one open.
|
|
1964
2057
|
* An operator's abandon marks the window, and the frame's check collects
|
|
1965
|
-
* the mark, which goes with the window.
|
|
1966
|
-
*
|
|
1967
|
-
*
|
|
1968
|
-
*
|
|
1969
|
-
*
|
|
1970
|
-
*
|
|
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.
|
|
1971
2066
|
*/
|
|
1972
2067
|
checkAhead = new Map();
|
|
1973
2068
|
/** Structured calls executed in the current logical turn. Unlike the
|
|
@@ -2050,7 +2145,15 @@ export class AgentFramework {
|
|
|
2050
2145
|
// config.codeExecution.enabled: its settings, and every agent's scripts,
|
|
2051
2146
|
// each in a process of its own on the engine (AgentScripts). Waiters
|
|
2052
2147
|
// resolve script-inner tool calls dispatched through the normal
|
|
2053
|
-
// dispatchToolCall path.
|
|
2148
|
+
// dispatchToolCall path. A waiter's route is where the dispatch sent its
|
|
2149
|
+
// call: `toServer` is set when it went to a server's own tool (an MCPL or
|
|
2150
|
+
// MCP-only peer's), so the script lane's exit treats every other result as
|
|
2151
|
+
// a module's (scriptResultAnswer), by where the call went rather than by a
|
|
2152
|
+
// name lookup a connect or disconnect can change while it runs. A
|
|
2153
|
+
// framework tool that talks to a server itself, such as a channel tool,
|
|
2154
|
+
// builds its own result, which counts as a module's. A caller of
|
|
2155
|
+
// dispatchScriptToolCall that passes no route gets one left false, the
|
|
2156
|
+
// reading that withholds more.
|
|
2054
2157
|
codeExecution = null;
|
|
2055
2158
|
scriptToolWaiters = new Map();
|
|
2056
2159
|
/** Per agent, a round that called code_execution and waits for those calls'
|
|
@@ -3026,6 +3129,16 @@ export class AgentFramework {
|
|
|
3026
3129
|
// Retained tool images are per-process by design; a retained stopped
|
|
3027
3130
|
// framework must not keep up to the whole ledger budget referenced.
|
|
3028
3131
|
this.toolImageLedgers.clear();
|
|
3132
|
+
// A turn whose continuation is still on its way ends with the stop, as
|
|
3133
|
+
// a turn's start a stop reaches does: the continuation would start only
|
|
3134
|
+
// after a start(), in a lifecycle where its logical turn is gone
|
|
3135
|
+
// (openLogical, below). Before the holds and the parks go, so the
|
|
3136
|
+
// continuation leaves them with its turn.
|
|
3137
|
+
step(() => {
|
|
3138
|
+
for (const agentName of this.pendingContinuationAgents()) {
|
|
3139
|
+
this.dropContinuations(agentName, { kind: 'shutdown', why: 'the framework is stopping' });
|
|
3140
|
+
}
|
|
3141
|
+
});
|
|
3029
3142
|
// A stopped host must never hang behind its own cooldown.
|
|
3030
3143
|
step(() => this.cancelProviderAdmission());
|
|
3031
3144
|
// An open logical turn doesn't survive a stop: a start after a restart
|
|
@@ -3619,6 +3732,23 @@ export class AgentFramework {
|
|
|
3619
3732
|
a.coalescingSubject === b.coalescingSubject && batchSubjects(a) === batchSubjects(b) &&
|
|
3620
3733
|
a.readsDeferredWrite === b.readsDeferredWrite;
|
|
3621
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
|
+
}
|
|
3622
3752
|
consultProviderHold(error, agentName) {
|
|
3623
3753
|
if (!this.providerHoldHook)
|
|
3624
3754
|
return undefined;
|
|
@@ -3676,15 +3806,21 @@ export class AgentFramework {
|
|
|
3676
3806
|
// failure streak), and the wait applies to the calls that follow it.
|
|
3677
3807
|
if (!acceleration && !hostHold) {
|
|
3678
3808
|
return wait !== undefined && isMembraneError(error) && error.retryable === true
|
|
3679
|
-
&& this.holdForProviderWait(agent, wait, trigger ? [trigger] : [], true);
|
|
3809
|
+
&& this.holdForProviderWait(agent, wait, trigger ? [asTurnRetry(trigger)] : [], true);
|
|
3680
3810
|
}
|
|
3811
|
+
// As in holdForProviderWait: once stop() has closed admission, nothing is
|
|
3812
|
+
// held, and nothing started.
|
|
3813
|
+
if (this.providerAdmissionClosed)
|
|
3814
|
+
return true;
|
|
3681
3815
|
const now = Date.now();
|
|
3682
3816
|
const pacing = hostHold ? undefined : this.accelerationCooldownMs(agent.name, error);
|
|
3683
3817
|
const delayMs = hostHold ? hostHold.holdMs : pacing.delayMs;
|
|
3684
3818
|
const existing = this.providerAccelerationCooldowns.get(agent.name);
|
|
3685
3819
|
const held = existing?.heldRequests ?? [];
|
|
3686
|
-
|
|
3687
|
-
|
|
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));
|
|
3688
3824
|
if (existing)
|
|
3689
3825
|
clearTimeout(existing.timer);
|
|
3690
3826
|
const timer = setTimeout(() => this.releaseProviderAccelerationCooldown(agent.name), Math.min(delayMs, MAX_TIMER_DELAY_MS));
|
|
@@ -3695,7 +3831,8 @@ export class AgentFramework {
|
|
|
3695
3831
|
// The stated wait (recorded above for agent.model) set this hold's
|
|
3696
3832
|
// length: an operator release of it returns the hold to AF's own pacing.
|
|
3697
3833
|
...(pacing?.stated ? { statedWait: { model: agent.model, ownUntil: now + pacing.ownMs } } : {}),
|
|
3698
|
-
...(auxiliary && (!existing || existing.auxiliaryOrigin) ? { auxiliaryOrigin: true } : {})
|
|
3834
|
+
...(auxiliary && (!existing || existing.auxiliaryOrigin) ? { auxiliaryOrigin: true } : {}),
|
|
3835
|
+
...(existing?.emptiedByDrop && held.length === 0 ? { emptiedByDrop: true } : {}) });
|
|
3699
3836
|
if (auxiliary && !existing) {
|
|
3700
3837
|
console.error(`[provider-cooldown] agent=${agent.name} host hold (${hostHold.reason}) from an auxiliary call — ` +
|
|
3701
3838
|
`holding primary/auxiliary for ${delayMs}ms`);
|
|
@@ -3736,7 +3873,7 @@ export class AgentFramework {
|
|
|
3736
3873
|
return undefined;
|
|
3737
3874
|
if (this.conversationAgentHomes.has(agent.name)) {
|
|
3738
3875
|
const wait = this.recordProviderWait(agent.name, agent.model, error);
|
|
3739
|
-
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)
|
|
3740
3877
|
? 'held'
|
|
3741
3878
|
: undefined;
|
|
3742
3879
|
}
|
|
@@ -3808,9 +3945,13 @@ export class AgentFramework {
|
|
|
3808
3945
|
? `no longer holds primary turns: the primary model is now ${model} (the wait stays recorded)`
|
|
3809
3946
|
: `primary hold ended after ${Date.now() - cooldown.startedAt}ms`));
|
|
3810
3947
|
}
|
|
3811
|
-
if (cooldown.auxiliaryOrigin && cooldown.heldRequests.length === 0) {
|
|
3812
|
-
// Nobody asked for an inference while parked
|
|
3813
|
-
|
|
3948
|
+
if ((cooldown.auxiliaryOrigin || cooldown.emptiedByDrop) && cooldown.heldRequests.length === 0) {
|
|
3949
|
+
// Nobody asked for an inference while parked, or what was held was a
|
|
3950
|
+
// continuation since dropped with its turn: just reopen the gate. A
|
|
3951
|
+
// primary the hold left pending won't run, so its recovery ends.
|
|
3952
|
+
console.error(`[provider-cooldown] agent=${agentName} ${cooldown.auxiliaryOrigin ? 'auxiliary hold' : 'hold, its held continuation dropped,'} released after ${Date.now() - cooldown.startedAt}ms`);
|
|
3953
|
+
if (cooldown.emptiedByDrop)
|
|
3954
|
+
this.endPendingPrimary(agentName);
|
|
3814
3955
|
this.flushAuxiliaryAdmission(agentName);
|
|
3815
3956
|
return;
|
|
3816
3957
|
}
|
|
@@ -3887,19 +4028,33 @@ export class AgentFramework {
|
|
|
3887
4028
|
* Hold this agent's primary turns for a provider wait on its model. The
|
|
3888
4029
|
* requests are kept and released together, into one fresh compile, when
|
|
3889
4030
|
* the wait ends: the same path an acceleration cooldown takes, without
|
|
3890
|
-
* 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.
|
|
3891
4035
|
*/
|
|
3892
|
-
holdForProviderWait(agent, wait, requests, failed = false) {
|
|
4036
|
+
holdForProviderWait(agent, wait, requests, failed = false, report) {
|
|
3893
4037
|
const now = Date.now();
|
|
3894
4038
|
const until = wait.until ?? Number.POSITIVE_INFINITY;
|
|
3895
4039
|
if (until <= now)
|
|
3896
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
|
+
}
|
|
3897
4054
|
const existing = this.providerAccelerationCooldowns.get(agent.name);
|
|
3898
4055
|
const held = existing?.heldRequests ?? [];
|
|
3899
|
-
for (const request of requests)
|
|
3900
|
-
|
|
3901
|
-
held.push(request);
|
|
3902
|
-
}
|
|
4056
|
+
for (const request of requests)
|
|
4057
|
+
this.holdRequest(held, request);
|
|
3903
4058
|
// An acceleration or host hold already parks the whole agent; its release
|
|
3904
4059
|
// re-checks this wait at admission.
|
|
3905
4060
|
if (existing && existing.waitModel === undefined)
|
|
@@ -4479,9 +4634,11 @@ export class AgentFramework {
|
|
|
4479
4634
|
}
|
|
4480
4635
|
/**
|
|
4481
4636
|
* Abort an in-flight inference for an agent. True when there was one to
|
|
4482
|
-
* stop: a stream
|
|
4637
|
+
* stop: a stream; a turn with no stream and one of its checks ahead (its
|
|
4483
4638
|
* hooks or compile, a wait to retry, the tool-result guard's wait), which
|
|
4484
|
-
* 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.
|
|
4485
4642
|
*/
|
|
4486
4643
|
abortInference(agentName, reason) {
|
|
4487
4644
|
const agent = this.agents.get(agentName);
|
|
@@ -4511,6 +4668,20 @@ export class AgentFramework {
|
|
|
4511
4668
|
checkWindow.wake?.();
|
|
4512
4669
|
return true;
|
|
4513
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
|
+
}
|
|
4514
4685
|
return !!result;
|
|
4515
4686
|
}
|
|
4516
4687
|
/**
|
|
@@ -5393,16 +5564,83 @@ export class AgentFramework {
|
|
|
5393
5564
|
}
|
|
5394
5565
|
return n;
|
|
5395
5566
|
}
|
|
5567
|
+
/**
|
|
5568
|
+
* A continuation the framework makes for a turn (a budget restart's, which
|
|
5569
|
+
* it queues, or a tool-result guard's retry, which its frame starts at
|
|
5570
|
+
* once) holds no turn token until its start takes one. Once its turn's
|
|
5571
|
+
* frame has let go of its token, its start can wait in the queue, a
|
|
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
|
|
5576
|
+
* `tool_results_ready` continuation follows a tool round left waiting with
|
|
5577
|
+
* no stream, which only the direct Agent API makes (Agent's
|
|
5578
|
+
* runInferenceWithInjections; the framework's own rounds wait with their
|
|
5579
|
+
* stream, enterWaitingForTools). Its caller owns that turn's timing, so it
|
|
5580
|
+
* isn't counted.
|
|
5581
|
+
*/
|
|
5582
|
+
pendingContinuationAgents() {
|
|
5583
|
+
const agents = new Set();
|
|
5584
|
+
const add = (r) => { if (continuesTurn(r))
|
|
5585
|
+
agents.add(r.agentName); };
|
|
5586
|
+
// Read defensively: a framework built from the prototype, as the
|
|
5587
|
+
// operator tests build one, sets none of these.
|
|
5588
|
+
for (const r of this.pendingRequests ?? [])
|
|
5589
|
+
add(r);
|
|
5590
|
+
for (const cooldown of this.providerAccelerationCooldowns?.values() ?? [])
|
|
5591
|
+
for (const r of cooldown.heldRequests)
|
|
5592
|
+
add(r);
|
|
5593
|
+
for (const r of this.parkedContinuations ?? [])
|
|
5594
|
+
add(r);
|
|
5595
|
+
for (const pass of this.passRequests ?? [])
|
|
5596
|
+
for (const requests of pass.values())
|
|
5597
|
+
for (const r of requests)
|
|
5598
|
+
add(r);
|
|
5599
|
+
return agents;
|
|
5600
|
+
}
|
|
5601
|
+
/** The agents with a turn alive: holding a turn token, or with a
|
|
5602
|
+
* continuation of their turn, or its retry, on its way. */
|
|
5603
|
+
turnHolders() {
|
|
5604
|
+
const holders = this.pendingContinuationAgents();
|
|
5605
|
+
for (const name of this.activeTurnTokens.keys())
|
|
5606
|
+
holders.add(name);
|
|
5607
|
+
return holders;
|
|
5608
|
+
}
|
|
5609
|
+
hasTurn(agentName) {
|
|
5610
|
+
return this.activeTurnTokens.has(agentName) || this.pendingContinuationAgents().has(agentName);
|
|
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
|
+
}
|
|
5396
5632
|
getHostModeStatus() {
|
|
5397
5633
|
const parkedAdmissions = this.parkedAdmissionCount();
|
|
5634
|
+
const holders = this.turnHolders();
|
|
5398
5635
|
return {
|
|
5399
5636
|
quiesced: this.quiesced,
|
|
5400
5637
|
...(this.quiesceReason ? { reason: this.quiesceReason } : {}),
|
|
5401
5638
|
...(this.quiescedAt !== undefined ? { since: this.quiescedAt } : {}),
|
|
5402
|
-
drained:
|
|
5403
|
-
activeTurns:
|
|
5639
|
+
drained: holders.size === 0 && parkedAdmissions === 0,
|
|
5640
|
+
activeTurns: holders.size,
|
|
5641
|
+
activeTurnAgents: [...holders].sort(),
|
|
5404
5642
|
parkedAdmissions,
|
|
5405
|
-
gatedRequests: this.pendingRequests.filter((r) => !
|
|
5643
|
+
gatedRequests: this.pendingRequests.filter((r) => !passesQuiesce(r)).length,
|
|
5406
5644
|
backgroundScripts: this.codeExecution?.scripts.leftRunning() ?? 0,
|
|
5407
5645
|
deferredWrites: this.deferredMessages.length,
|
|
5408
5646
|
...(this.lastUnabandonable.length > 0 ? { unabandonable: [...this.lastUnabandonable] } : {}),
|
|
@@ -5417,8 +5655,10 @@ export class AgentFramework {
|
|
|
5417
5655
|
/**
|
|
5418
5656
|
* Enter quiesce mode: write the deferred-write queue, persist the flag,
|
|
5419
5657
|
* suppress gate deliveries, pause all MCPL data planes, and wait for
|
|
5420
|
-
* in-flight turns to settle (turn-alive is
|
|
5421
|
-
* dequeue → settled teardown
|
|
5658
|
+
* in-flight turns to settle (turn-alive is a turn token, which spans
|
|
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
|
|
5422
5662
|
* every call makes first: when the queue can't reach disk, because its
|
|
5423
5663
|
* write fails or it's over the cap, the call rejects with a
|
|
5424
5664
|
* QuiesceBlockedError and the host's mode as it was, unless it has
|
|
@@ -5428,7 +5668,10 @@ export class AgentFramework {
|
|
|
5428
5668
|
* On drain timeout the host STAYS quiesced (`drained: false` in the
|
|
5429
5669
|
* result); with `abandon: true` the undrained streams are cancelled via a
|
|
5430
5670
|
* dedicated cancel kind that settles the turn without feeding the
|
|
5431
|
-
* inference-failure accounting
|
|
5671
|
+
* inference-failure accounting, and a turn whose continuation or retry
|
|
5672
|
+
* hasn't started ends with it dropped. Drain covers the framework's
|
|
5673
|
+
* own turns: a caller of the direct Agent API (`Agent.runInference`) owns
|
|
5674
|
+
* its turns' timing.
|
|
5432
5675
|
*/
|
|
5433
5676
|
async quiesce(opts) {
|
|
5434
5677
|
this.refuseOperatorAfterStop('quiesce');
|
|
@@ -5445,13 +5688,13 @@ export class AgentFramework {
|
|
|
5445
5688
|
// out undrained must be escalatable with a second quiesce({abandon})
|
|
5446
5689
|
// without resume()+re-quiesce (which would reopen data planes and
|
|
5447
5690
|
// release parked wakes mid-surgery).
|
|
5448
|
-
if (opts?.abandon && this.
|
|
5691
|
+
if (opts?.abandon && this.turnHolders().size > 0) {
|
|
5449
5692
|
const unabandonable = await this.abandonActiveTurns();
|
|
5450
5693
|
this.emitTrace({
|
|
5451
5694
|
type: 'host:quiesce',
|
|
5452
5695
|
...(this.quiesceReason ? { reason: this.quiesceReason } : {}),
|
|
5453
|
-
drained: this.
|
|
5454
|
-
activeTurns: this.
|
|
5696
|
+
drained: this.turnHolders().size === 0,
|
|
5697
|
+
activeTurns: this.turnHolders().size,
|
|
5455
5698
|
abandoned: true,
|
|
5456
5699
|
...(unabandonable.length > 0 ? { unabandonable } : {}),
|
|
5457
5700
|
});
|
|
@@ -5476,7 +5719,7 @@ export class AgentFramework {
|
|
|
5476
5719
|
this.quiescedAt = since;
|
|
5477
5720
|
this.lastUnabandonable = [];
|
|
5478
5721
|
console.error(`[host-mode] quiescing${opts?.reason ? ` (${opts.reason})` : ''}: parking wakes, ` +
|
|
5479
|
-
`pausing MCPL data planes, draining ${this.
|
|
5722
|
+
`pausing MCPL data planes, draining ${this.turnHolders().size} in-flight turn(s)`);
|
|
5480
5723
|
this.eventGate?.setQuiesced(true);
|
|
5481
5724
|
for (const connection of this.mcplServerRegistry?.getAllServers() ?? []) {
|
|
5482
5725
|
connection.pauseDataPlane();
|
|
@@ -5496,7 +5739,7 @@ export class AgentFramework {
|
|
|
5496
5739
|
// continuation's quiesce recheck requeues it — wait for that to happen).
|
|
5497
5740
|
while (this.quiesced &&
|
|
5498
5741
|
!this.queue?.isClosed &&
|
|
5499
|
-
(this.
|
|
5742
|
+
(this.turnHolders().size > 0 || this.parkedAdmissionCount() > 0) &&
|
|
5500
5743
|
Date.now() < deadline) {
|
|
5501
5744
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
5502
5745
|
}
|
|
@@ -5517,7 +5760,7 @@ export class AgentFramework {
|
|
|
5517
5760
|
}
|
|
5518
5761
|
let abandoned = false;
|
|
5519
5762
|
let unabandonable = [];
|
|
5520
|
-
if (this.
|
|
5763
|
+
if (this.turnHolders().size > 0 && opts?.abandon) {
|
|
5521
5764
|
abandoned = true;
|
|
5522
5765
|
unabandonable = await this.abandonActiveTurns();
|
|
5523
5766
|
if (!this.quiesced)
|
|
@@ -5547,7 +5790,19 @@ export class AgentFramework {
|
|
|
5547
5790
|
* the already-quiesced escalation path. */
|
|
5548
5791
|
async abandonActiveTurns() {
|
|
5549
5792
|
const unabandonable = [];
|
|
5793
|
+
// A turn whose continuation is still on its way ends with the
|
|
5794
|
+
// continuation dropped. Its stream has ended; a frame still letting go of
|
|
5795
|
+
// its token ends on its own, and settles nothing, since it handed the turn
|
|
5796
|
+
// to the continuation.
|
|
5797
|
+
const handedOff = new Set();
|
|
5798
|
+
for (const agentName of this.pendingContinuationAgents()) {
|
|
5799
|
+
console.error(`[host-mode] abandoning ${agentName}'s turn before its continuation or held retry starts: it is dropped`);
|
|
5800
|
+
this.dropContinuations(agentName, { kind: 'quiesce_abandoned', why: 'its turn was abandoned' });
|
|
5801
|
+
handedOff.add(agentName);
|
|
5802
|
+
}
|
|
5550
5803
|
for (const agentName of [...this.activeTurnTokens.keys()]) {
|
|
5804
|
+
if (handedOff.has(agentName))
|
|
5805
|
+
continue;
|
|
5551
5806
|
const agent = this.agents.get(agentName);
|
|
5552
5807
|
const state = agent?.state;
|
|
5553
5808
|
const stream = state && 'stream' in state ? state.stream : undefined;
|
|
@@ -7496,6 +7751,9 @@ export class AgentFramework {
|
|
|
7496
7751
|
this.activeTurnTokens.delete(agent.name);
|
|
7497
7752
|
this.activeTurnTriggers.delete(agent.name);
|
|
7498
7753
|
this.logicalTurnToolCalls.delete(agent);
|
|
7754
|
+
// A continuation of its turn still on its way goes too: the run, and
|
|
7755
|
+
// with it the turn, has ended above.
|
|
7756
|
+
this.dropContinuations(agent.name, null);
|
|
7499
7757
|
}
|
|
7500
7758
|
}
|
|
7501
7759
|
/**
|
|
@@ -8887,7 +9145,8 @@ export class AgentFramework {
|
|
|
8887
9145
|
* by stop() while it runs.
|
|
8888
9146
|
*
|
|
8889
9147
|
* The branch moves under every agent sharing the store, so the switch
|
|
8890
|
-
* 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
|
|
8891
9150
|
* refused (OperatorActionError 'agent-busy'), and no turn starts until the
|
|
8892
9151
|
* switch is done.
|
|
8893
9152
|
*/
|
|
@@ -9101,18 +9360,35 @@ export class AgentFramework {
|
|
|
9101
9360
|
throw new OperatorActionError('agent-busy', `Cannot ${verb}: a live ${this.surgeryHold.verb} for ${this.surgeryHold.agentName} is already in progress`);
|
|
9102
9361
|
}
|
|
9103
9362
|
const busy = [];
|
|
9363
|
+
let heldTurns = false;
|
|
9364
|
+
let statedWaits = false;
|
|
9104
9365
|
for (const [name, a] of this.agents) {
|
|
9105
|
-
if (a.state.status !== 'idle')
|
|
9366
|
+
if (a.state.status !== 'idle') {
|
|
9106
9367
|
busy.push(`${name} is ${a.state.status}`);
|
|
9107
|
-
|
|
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 {
|
|
9108
9382
|
busy.push(`${name} is idle+turn-alive`);
|
|
9383
|
+
}
|
|
9109
9384
|
}
|
|
9110
9385
|
for (const name of this.activeTurnTokens.keys()) {
|
|
9111
9386
|
if (!this.agents.has(name))
|
|
9112
9387
|
busy.push(`${name} is turn-alive`);
|
|
9113
9388
|
}
|
|
9114
9389
|
if (busy.length > 0) {
|
|
9115
|
-
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)}` : ''));
|
|
9116
9392
|
}
|
|
9117
9393
|
const reserved = new Map();
|
|
9118
9394
|
for (const name of this.agents.keys()) {
|
|
@@ -9894,9 +10170,10 @@ export class AgentFramework {
|
|
|
9894
10170
|
// Direct inference requests (e.g. runEphemeralToCompletion) bypass the
|
|
9895
10171
|
// event queue — without this the loop can exit before they're drained.
|
|
9896
10172
|
// While quiesced, gate-parked requests are NOT progress and must not
|
|
9897
|
-
// block idle
|
|
9898
|
-
//
|
|
9899
|
-
|
|
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)) ||
|
|
9900
10177
|
this.activeStreams.size > 0 ||
|
|
9901
10178
|
Array.from(this.agents.values()).some((a) => a.state.status !== 'idle')) {
|
|
9902
10179
|
await this.processNextEvent();
|
|
@@ -10087,7 +10364,7 @@ export class AgentFramework {
|
|
|
10087
10364
|
// was killed while the call ran: the result reaches no one, but the
|
|
10088
10365
|
// call did finish.
|
|
10089
10366
|
this.toolLifecycleEmitter?.onResult(event.agentName, event.callId, event.result);
|
|
10090
|
-
scriptWaiter(event.result);
|
|
10367
|
+
scriptWaiter.resolve(event.result);
|
|
10091
10368
|
return;
|
|
10092
10369
|
}
|
|
10093
10370
|
// RFC-007: the call's terminal (completed / failed) — the result
|
|
@@ -10513,7 +10790,27 @@ export class AgentFramework {
|
|
|
10513
10790
|
}
|
|
10514
10791
|
this.emitTrace({ type: 'inference:turn_ended', agentName: agent.name });
|
|
10515
10792
|
}
|
|
10793
|
+
else if ((overBudget || overPhysical) && this.providerAdmissionClosed) {
|
|
10794
|
+
// A stopping framework starts no continuation: the turn ends with
|
|
10795
|
+
// its stream, as stop() ends a live stream's turn.
|
|
10796
|
+
if (currentState.stream)
|
|
10797
|
+
this.frameworkCancelledStreams.set(`${agent.name}:${agent.streamId}`, 'shutdown');
|
|
10798
|
+
agent.cancelStream();
|
|
10799
|
+
}
|
|
10516
10800
|
else if (overBudget || overPhysical) {
|
|
10801
|
+
// The continuation is queued, and the stream cancelled, before the
|
|
10802
|
+
// boundary's usage report and the restart's trace, so a listener
|
|
10803
|
+
// that stops or quiesces the host on either finds the turn waiting
|
|
10804
|
+
// for its continuation, with nothing left of the stream to end.
|
|
10805
|
+
const continuation = {
|
|
10806
|
+
agentName: agent.name,
|
|
10807
|
+
reason: 'context_budget_restart',
|
|
10808
|
+
source: 'framework',
|
|
10809
|
+
timestamp: Date.now(),
|
|
10810
|
+
suppressProse: this.activeTurnTriggers.get(agent.name)?.suppressProse,
|
|
10811
|
+
silentHeartbeat: this.activeTurnTriggers.get(agent.name)?.silentHeartbeat,
|
|
10812
|
+
};
|
|
10813
|
+
this.pendingRequests.push(continuation);
|
|
10517
10814
|
// Context budget exceeded: break the stream, let compile() compress.
|
|
10518
10815
|
// Mark the cancel as framework-initiated BEFORE cancelling: the
|
|
10519
10816
|
// membrane delivers `aborted` before the restart bumps streamId,
|
|
@@ -10521,13 +10818,23 @@ export class AgentFramework {
|
|
|
10521
10818
|
// a terminal failure — rejecting an ephemeral's promise mid-run
|
|
10522
10819
|
// and emitting a spurious inference:exhausted (same race shape as
|
|
10523
10820
|
// endTurn above).
|
|
10524
|
-
if (currentState.stream)
|
|
10821
|
+
if (currentState.stream)
|
|
10525
10822
|
this.frameworkCancelledStreams.set(`${agent.name}:${agent.streamId}`, 'budget_restart');
|
|
10526
|
-
|
|
10527
|
-
|
|
10528
|
-
|
|
10823
|
+
try {
|
|
10824
|
+
agent.cancelStream();
|
|
10825
|
+
}
|
|
10826
|
+
catch (error) {
|
|
10827
|
+
// The cancel failed and the stream is still live, so the turn
|
|
10828
|
+
// has no continuation: take it back out of the queue.
|
|
10829
|
+
this.pendingRequests = this.pendingRequests.filter((r) => r !== continuation);
|
|
10830
|
+
throw error;
|
|
10831
|
+
}
|
|
10832
|
+
finally {
|
|
10833
|
+
// Its rounds were billed, even when the cancel throws; the
|
|
10834
|
+
// restart opens a fresh stream with its own usage.
|
|
10835
|
+
if (currentState.stream)
|
|
10836
|
+
this.streamBoundaryEnds.get(currentState.stream)?.(overBudget ? 'context_budget' : 'physical_window');
|
|
10529
10837
|
}
|
|
10530
|
-
agent.cancelStream();
|
|
10531
10838
|
this.emitTrace({
|
|
10532
10839
|
type: 'inference:stream_restarted',
|
|
10533
10840
|
agentName: agent.name,
|
|
@@ -10538,14 +10845,6 @@ export class AgentFramework {
|
|
|
10538
10845
|
inputTokens: overBudget ? agent.lastStreamInputTokens : projectedRealTokens,
|
|
10539
10846
|
budget: overBudget ? agent.maxStreamTokens : (agent.physicalWindowTokens ?? 0),
|
|
10540
10847
|
});
|
|
10541
|
-
this.pendingRequests.push({
|
|
10542
|
-
agentName: agent.name,
|
|
10543
|
-
reason: 'context_budget_restart',
|
|
10544
|
-
source: 'framework',
|
|
10545
|
-
timestamp: Date.now(),
|
|
10546
|
-
suppressProse: this.activeTurnTriggers.get(agent.name)?.suppressProse,
|
|
10547
|
-
silentHeartbeat: this.activeTurnTriggers.get(agent.name)?.silentHeartbeat,
|
|
10548
|
-
});
|
|
10549
10848
|
}
|
|
10550
10849
|
else if (currentState.stream) {
|
|
10551
10850
|
// What this round appends is in the next call's prompt. If that
|
|
@@ -11306,6 +11605,8 @@ export class AgentFramework {
|
|
|
11306
11605
|
*/
|
|
11307
11606
|
disposeConversationAgent(agentName) {
|
|
11308
11607
|
this.closingConversationAgents.delete(agentName);
|
|
11608
|
+
// A continuation of its turn still on its way goes with it.
|
|
11609
|
+
this.dropContinuations(agentName, { kind: 'dropped', why: 'its agent was disposed' });
|
|
11309
11610
|
const channelId = this.conversationAgentHomes.get(agentName);
|
|
11310
11611
|
const agent = this.agents.get(agentName);
|
|
11311
11612
|
// A fork stop() ends can still be running: its stream, its tool round.
|
|
@@ -11429,9 +11730,12 @@ export class AgentFramework {
|
|
|
11429
11730
|
// (request dropped as stale, inference policy veto) would otherwise stay
|
|
11430
11731
|
// registered forever. No turn, no active stream and no pending request =
|
|
11431
11732
|
// done. A turn holds its token from dequeue, through its hooks and
|
|
11432
|
-
// 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.
|
|
11433
11737
|
for (const agentName of [...this.closingConversationAgents]) {
|
|
11434
|
-
if (!this.
|
|
11738
|
+
if (!this.hasTurn(agentName) &&
|
|
11435
11739
|
!this.activeStreams.has(agentName) &&
|
|
11436
11740
|
!this.pendingRequests.some((r) => r.agentName === agentName)) {
|
|
11437
11741
|
this.disposeConversationAgent(agentName);
|
|
@@ -11780,9 +12084,19 @@ export class AgentFramework {
|
|
|
11780
12084
|
recordReceipt: (record) => {
|
|
11781
12085
|
// Written now, not at the throttled flush: an acknowledged acceptance
|
|
11782
12086
|
// must not lose its receipt, the subject's birth, or (deferred) the
|
|
11783
|
-
// accepted work itself to a crash. A failure here fails the
|
|
12087
|
+
// accepted work itself to a crash. A failure here fails the answer:
|
|
12088
|
+
// the coalescer keeps the receipt, and the producer's retry writes it
|
|
12089
|
+
// again, with the subject's state as it is then. So a record whose
|
|
12090
|
+
// write failed leaves the list: a later write would carry it after
|
|
12091
|
+
// the batch it holds had moved on.
|
|
11784
12092
|
this.coalescingRecentReceipts.push(record);
|
|
11785
|
-
|
|
12093
|
+
try {
|
|
12094
|
+
this.store.setStateJson(COALESCING_RECENT_ID, this.coalescingRecentReceipts);
|
|
12095
|
+
}
|
|
12096
|
+
catch (error) {
|
|
12097
|
+
this.coalescingRecentReceipts.pop();
|
|
12098
|
+
throw error;
|
|
12099
|
+
}
|
|
11786
12100
|
},
|
|
11787
12101
|
// The acceptance of coalesced work (deferred included) is observed
|
|
11788
12102
|
// here, once; its later delivery reuses the frozen envelope.
|
|
@@ -11904,6 +12218,9 @@ export class AgentFramework {
|
|
|
11904
12218
|
}
|
|
11905
12219
|
catch (err) {
|
|
11906
12220
|
console.error('[coalescing] could not persist coalescing state:', err);
|
|
12221
|
+
// Still unsaved: the next persist, or the stop's flush, writes it. No
|
|
12222
|
+
// timer of its own, so a store that keeps failing isn't retried here.
|
|
12223
|
+
this.coalescingSnapshotDirty ??= snapshot;
|
|
11907
12224
|
}
|
|
11908
12225
|
}
|
|
11909
12226
|
/** Serialize both lanes' admissions: create, edit and delete of one subject stay ordered. */
|
|
@@ -12205,6 +12522,24 @@ export class AgentFramework {
|
|
|
12205
12522
|
for (const cooldown of this.providerAccelerationCooldowns.values()) {
|
|
12206
12523
|
cooldown.heldRequests = cooldown.heldRequests.filter((r) => r.coalescingSubject !== subject);
|
|
12207
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);
|
|
12208
12543
|
}
|
|
12209
12544
|
/**
|
|
12210
12545
|
* A placement in the deferred queue is upgraded in place once the flush has
|
|
@@ -12532,15 +12867,26 @@ export class AgentFramework {
|
|
|
12532
12867
|
}
|
|
12533
12868
|
return undefined;
|
|
12534
12869
|
}
|
|
12870
|
+
/** One scheduler pass, whose requests stay visible as on their way until
|
|
12871
|
+
* it takes their agent up (passRequests). */
|
|
12535
12872
|
async processInferenceRequests() {
|
|
12536
12873
|
if (this.pendingRequests.length === 0) {
|
|
12537
12874
|
return;
|
|
12538
12875
|
}
|
|
12876
|
+
const requestsByAgent = new Map();
|
|
12877
|
+
this.passRequests?.add(requestsByAgent);
|
|
12878
|
+
try {
|
|
12879
|
+
await this.runInferencePass(requestsByAgent);
|
|
12880
|
+
}
|
|
12881
|
+
finally {
|
|
12882
|
+
this.passRequests?.delete(requestsByAgent);
|
|
12883
|
+
}
|
|
12884
|
+
}
|
|
12885
|
+
async runInferencePass(requestsByAgent) {
|
|
12539
12886
|
const STALE_REQUEST_MS = 30_000;
|
|
12540
12887
|
const now = Date.now();
|
|
12541
12888
|
const state = this.createFrameworkState();
|
|
12542
12889
|
// Group requests by agent
|
|
12543
|
-
const requestsByAgent = new Map();
|
|
12544
12890
|
for (const req of this.pendingRequests) {
|
|
12545
12891
|
const existing = requestsByAgent.get(req.agentName) ?? [];
|
|
12546
12892
|
existing.push(req);
|
|
@@ -12551,6 +12897,14 @@ export class AgentFramework {
|
|
|
12551
12897
|
// Check each agent
|
|
12552
12898
|
for (const [agentName, groupedRequests] of requestsByAgent) {
|
|
12553
12899
|
let requests = groupedRequests;
|
|
12900
|
+
// Taken up now: from here to its start's turn token (or its hold or
|
|
12901
|
+
// park) nothing awaits, so the requests leave the pass's map, which
|
|
12902
|
+
// shows only what the pass still holds while it awaits a start.
|
|
12903
|
+
requestsByAgent.delete(agentName);
|
|
12904
|
+
// A drop while this pass awaited an earlier agent's start can have
|
|
12905
|
+
// taken every request of this agent's (dropContinuations).
|
|
12906
|
+
if (requests.length === 0)
|
|
12907
|
+
continue;
|
|
12554
12908
|
const agent = this.agents.get(agentName);
|
|
12555
12909
|
if (!agent) {
|
|
12556
12910
|
// Agent not found — request is orphaned. Emit warning and drop.
|
|
@@ -12563,6 +12917,10 @@ export class AgentFramework {
|
|
|
12563
12917
|
oldestRequestAge: now - oldest,
|
|
12564
12918
|
});
|
|
12565
12919
|
console.error(`[inference-dropped] agent=${agentName} reason=agent_not_found requests=${requests.length}`);
|
|
12920
|
+
// A continuation among them leaves its turn to end here.
|
|
12921
|
+
if (requests.some(continuesTurn)) {
|
|
12922
|
+
this.endContinuedTurn(agentName, { kind: 'dropped', why: 'its agent is gone' });
|
|
12923
|
+
}
|
|
12566
12924
|
continue;
|
|
12567
12925
|
}
|
|
12568
12926
|
const providerCooldown = this.providerAccelerationCooldowns?.get(agentName);
|
|
@@ -12571,11 +12929,8 @@ export class AgentFramework {
|
|
|
12571
12929
|
// selected now, the turn proceeds (the wait stays recorded).
|
|
12572
12930
|
const otherModel = providerCooldown.waitModel !== undefined && providerCooldown.waitModel !== agent.model;
|
|
12573
12931
|
if (!otherModel && (now < providerCooldown.until || this.extendHostProviderHold(agentName, providerCooldown))) {
|
|
12574
|
-
for (const req of requests)
|
|
12575
|
-
|
|
12576
|
-
providerCooldown.heldRequests.push(req);
|
|
12577
|
-
}
|
|
12578
|
-
}
|
|
12932
|
+
for (const req of requests)
|
|
12933
|
+
this.holdRequest(providerCooldown.heldRequests, req);
|
|
12579
12934
|
const recovery = this.providerAccelerationRecoveries.get(agentName);
|
|
12580
12935
|
if (recovery)
|
|
12581
12936
|
recovery.heldRequests = providerCooldown.heldRequests.length;
|
|
@@ -12591,6 +12946,16 @@ export class AgentFramework {
|
|
|
12591
12946
|
recovery.releasedAt = now;
|
|
12592
12947
|
requests = [...providerCooldown.heldRequests, ...requests];
|
|
12593
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
|
+
}
|
|
12594
12959
|
// A provider's stated wait on this agent's primary model, including one
|
|
12595
12960
|
// recorded before a restart, holds the turn until it passes. An
|
|
12596
12961
|
// ephemeral run records nothing, its name being single-use, so no wait,
|
|
@@ -12624,9 +12989,10 @@ export class AgentFramework {
|
|
|
12624
12989
|
// bounds a multi-day maintenance window's accumulation.
|
|
12625
12990
|
if (this.quiesced) {
|
|
12626
12991
|
// Continuations of a held turn (budget restart, non-streaming tool
|
|
12627
|
-
// results) pass
|
|
12628
|
-
|
|
12629
|
-
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));
|
|
12630
12996
|
// Coalesce per (reason, addressed): every channel wake shares one
|
|
12631
12997
|
// reason, so keying on reason alone would let an ambient message
|
|
12632
12998
|
// parked later evict a DM/mention parked earlier — and the
|
|
@@ -12652,7 +13018,10 @@ export class AgentFramework {
|
|
|
12652
13018
|
requests.length = 0;
|
|
12653
13019
|
requests.push(...passThrough);
|
|
12654
13020
|
}
|
|
12655
|
-
|
|
13021
|
+
// Turn-alive includes a continuation of the agent's turn still on its
|
|
13022
|
+
// way elsewhere (a park, another pass's hands): a fresh wake waits for
|
|
13023
|
+
// it, as for a turn holding its token.
|
|
13024
|
+
const turnAlive = !budgetRestart && this.hasTurn(agentName);
|
|
12656
13025
|
const providerGate = this.providerGates?.get(agentName);
|
|
12657
13026
|
// A primary can own provider admission while yielding to an already in-flight
|
|
12658
13027
|
// auxiliary call. Keep this agent's later wakes queued, but do not block the
|
|
@@ -12722,10 +13091,12 @@ export class AgentFramework {
|
|
|
12722
13091
|
// exactly this class of silent drop. One stderr line per drop.
|
|
12723
13092
|
console.error(`[inference-dropped] agent=${agentName} reason=policy-skip ` +
|
|
12724
13093
|
`requests=${requests.length} triggers=${requests.map((r) => r.reason).join(',')}`);
|
|
12725
|
-
// A
|
|
12726
|
-
// If policy drops
|
|
12727
|
-
|
|
12728
|
-
|
|
13094
|
+
// A continuation inherits its predecessor's EventGate liveness and
|
|
13095
|
+
// logical turn. If policy drops it, no driveStream remains to end them,
|
|
13096
|
+
// so its turn ends here (endContinuedTurn).
|
|
13097
|
+
if (requests.some(continuesTurn)) {
|
|
13098
|
+
this.endContinuedTurn(agentName, { kind: 'dropped', why: 'the inference policy dropped its continuation' });
|
|
13099
|
+
}
|
|
12729
13100
|
this.endPendingPrimary(agentName);
|
|
12730
13101
|
continue;
|
|
12731
13102
|
}
|
|
@@ -12805,7 +13176,7 @@ export class AgentFramework {
|
|
|
12805
13176
|
// exists only once assembly renders it. Recorded for the whole batch
|
|
12806
13177
|
// (not inherited from requests[0]), so a requeued trigger stays honest.
|
|
12807
13178
|
const batchOnly = !budgetRestart && requests.every((r) => r.coalescingBatch === true && !!r.coalescingSubject);
|
|
12808
|
-
|
|
13179
|
+
const merged = {
|
|
12809
13180
|
...trigger,
|
|
12810
13181
|
coalescingBatch: batchOnly || undefined,
|
|
12811
13182
|
coalescingBatchSubjects: batchOnly
|
|
@@ -12833,7 +13204,19 @@ export class AgentFramework {
|
|
|
12833
13204
|
counterparty: budgetRestart ? undefined : provReq?.counterparty,
|
|
12834
13205
|
wakeChannelId: budgetRestart ? undefined : provReq?.wakeChannelId,
|
|
12835
13206
|
wakeAt: budgetRestart ? undefined : provReq?.wakeAt,
|
|
12836
|
-
}
|
|
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);
|
|
12837
13220
|
}
|
|
12838
13221
|
}
|
|
12839
13222
|
/**
|
|
@@ -15078,7 +15461,14 @@ export class AgentFramework {
|
|
|
15078
15461
|
// Do not await inside processInferenceRequests: one slow auxiliary call for
|
|
15079
15462
|
// this resident must not block events or other residents. Admission stays
|
|
15080
15463
|
// owned and the continuation receives it after the in-flight call settles.
|
|
15464
|
+
const parkedContinuation = trigger !== undefined && continuesTurn(trigger);
|
|
15465
|
+
if (parkedContinuation)
|
|
15466
|
+
this.parkedContinuations.add(trigger);
|
|
15081
15467
|
void this.waitForAuxiliaryIdle(agent.name).then(async () => {
|
|
15468
|
+
// A continuation dropped while it was parked left the set then, with
|
|
15469
|
+
// its admission given back.
|
|
15470
|
+
if (parkedContinuation && !this.parkedContinuations.delete(trigger))
|
|
15471
|
+
return;
|
|
15082
15472
|
if (this.providerAdmissionClosed) {
|
|
15083
15473
|
this.releasePrimaryProviderGate(agent.name);
|
|
15084
15474
|
return;
|
|
@@ -15105,8 +15495,9 @@ export class AgentFramework {
|
|
|
15105
15495
|
// park; starting here would bypass it and run a turn against a
|
|
15106
15496
|
// context the operator believes frozen. Give admission back and
|
|
15107
15497
|
// requeue — the scheduler re-parks it (coalesced) until resume.
|
|
15108
|
-
// Continuations of a held turn
|
|
15109
|
-
|
|
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)) {
|
|
15110
15501
|
this.releasePrimaryProviderGate(agent.name);
|
|
15111
15502
|
this.pendingRequests.push(trigger ?? {
|
|
15112
15503
|
agentName: agent.name, reason: 'provider-admission:requeue', source: 'scheduler', timestamp: Date.now(),
|
|
@@ -15114,6 +15505,16 @@ export class AgentFramework {
|
|
|
15114
15505
|
console.error(`[provider-admission] ${agent.name}: host quiesced while parked — wake requeued, not started`);
|
|
15115
15506
|
return;
|
|
15116
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
|
+
}
|
|
15117
15518
|
await this.startAgentStream(agent, trigger, attempt, true);
|
|
15118
15519
|
}).catch((error) => {
|
|
15119
15520
|
this.releasePrimaryProviderGate(agent.name);
|
|
@@ -15243,6 +15644,143 @@ export class AgentFramework {
|
|
|
15243
15644
|
}
|
|
15244
15645
|
}
|
|
15245
15646
|
}
|
|
15647
|
+
/**
|
|
15648
|
+
* Drop every continuation of `agentName`'s turn still on its way, wherever
|
|
15649
|
+
* it waits: the queue, a provider hold (which, left empty, then releases
|
|
15650
|
+
* with nothing to run), a park on admission (whose admission it gives
|
|
15651
|
+
* back), or a scheduler pass's hands (which then never reach it). If one
|
|
15652
|
+
* was there, the turn ends (endContinuedTurn), unless `drop` is null: the
|
|
15653
|
+
* caller has ended the turn already.
|
|
15654
|
+
*/
|
|
15655
|
+
dropContinuations(agentName, drop) {
|
|
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 = [];
|
|
15663
|
+
let dropped = false;
|
|
15664
|
+
const take = (requests) => {
|
|
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
|
+
}
|
|
15675
|
+
return left;
|
|
15676
|
+
};
|
|
15677
|
+
this.pendingRequests = take(this.pendingRequests);
|
|
15678
|
+
const hold = this.providerAccelerationCooldowns.get(agentName);
|
|
15679
|
+
if (hold) {
|
|
15680
|
+
const held = hold.heldRequests.length;
|
|
15681
|
+
hold.heldRequests = take(hold.heldRequests);
|
|
15682
|
+
if (held > 0 && hold.heldRequests.length === 0)
|
|
15683
|
+
hold.emptiedByDrop = true;
|
|
15684
|
+
}
|
|
15685
|
+
for (const parked of [...this.parkedContinuations]) {
|
|
15686
|
+
if (!isContinuation(parked))
|
|
15687
|
+
continue;
|
|
15688
|
+
this.parkedContinuations.delete(parked);
|
|
15689
|
+
this.releasePrimaryProviderGate(agentName);
|
|
15690
|
+
dropped = true;
|
|
15691
|
+
if (putsBack)
|
|
15692
|
+
back.push(...mergedWakes(parked));
|
|
15693
|
+
}
|
|
15694
|
+
for (const pass of this.passRequests) {
|
|
15695
|
+
const requests = pass.get(agentName);
|
|
15696
|
+
if (!requests)
|
|
15697
|
+
continue;
|
|
15698
|
+
const left = take(requests);
|
|
15699
|
+
requests.length = 0;
|
|
15700
|
+
requests.push(...left);
|
|
15701
|
+
}
|
|
15702
|
+
this.pendingRequests.push(...back);
|
|
15703
|
+
if (!dropped || !drop)
|
|
15704
|
+
return;
|
|
15705
|
+
// A primary a provider hold left pending for what it released won't run
|
|
15706
|
+
// now, when nothing else of the agent's waits (endPendingPrimary).
|
|
15707
|
+
if (!this.pendingRequests.some((r) => r.agentName === agentName) && !this.providerAccelerationCooldowns.get(agentName)?.heldRequests.length) {
|
|
15708
|
+
this.endPendingPrimary(agentName);
|
|
15709
|
+
}
|
|
15710
|
+
this.endContinuedTurn(agentName, drop);
|
|
15711
|
+
}
|
|
15712
|
+
/**
|
|
15713
|
+
* End a turn whose continuation won't run. The frame that handed the turn
|
|
15714
|
+
* to it kept the turn open for it: its logical turn, the EventGate's
|
|
15715
|
+
* window, the tool-result guard's batch and recovery, its typing. An
|
|
15716
|
+
* abandon or a stop of a registered agent settles the turn as it settles a
|
|
15717
|
+
* turn's start (endStoppedStart: one inference:aborted, the turn settled,
|
|
15718
|
+
* the gate released). Any other drop ends it as a start dropped before it
|
|
15719
|
+
* sends anything ends. That frame, if it hasn't let go of its token yet,
|
|
15720
|
+
* ends on its own and settles nothing. What was deferred for the agent
|
|
15721
|
+
* lands now, or at that frame's end.
|
|
15722
|
+
*/
|
|
15723
|
+
endContinuedTurn(agentName, drop) {
|
|
15724
|
+
console.error(`[turn-continuation] ${agentName}: its turn ends without its continuation: ${drop.why}`);
|
|
15725
|
+
const agent = this.agents.get(agentName);
|
|
15726
|
+
const startedAt = this.lastInferenceAt.get(agentName)?.startedAt ?? Date.now();
|
|
15727
|
+
if (agent && drop.kind !== 'dropped') {
|
|
15728
|
+
const routing = this.openLogical.get(agentName) ?? this.routingOf(agentName);
|
|
15729
|
+
this.endStoppedStart(agent, drop.kind === 'caller' ? { kind: 'caller', reason: drop.reason } : { kind: drop.kind }, routing, startedAt, undefined, false);
|
|
15730
|
+
}
|
|
15731
|
+
else {
|
|
15732
|
+
const owner = this.activeTurnTokens.get(agentName);
|
|
15733
|
+
this.abortDroppedTurn(agentName, owner ?? -1, drop.kind === 'dropped' ? CONTINUATION_DROPPED : drop.kind === 'caller' ? (drop.reason ?? 'user') : drop.kind, startedAt);
|
|
15734
|
+
this.channelRegistry?.stopTyping();
|
|
15735
|
+
if (agent) {
|
|
15736
|
+
agent.toolResultGuard.recovering = false;
|
|
15737
|
+
if (agent.toolResultGuard.hasPending) {
|
|
15738
|
+
try {
|
|
15739
|
+
agent.toolResultGuard.abandon('aborted');
|
|
15740
|
+
}
|
|
15741
|
+
catch (error) {
|
|
15742
|
+
console.error(`[tool-result-guard] agent=${agentName} failed to record aborted batch:`, error);
|
|
15743
|
+
}
|
|
15744
|
+
}
|
|
15745
|
+
}
|
|
15746
|
+
const routing = this.openLogical.get(agentName) ?? this.turnRouting.get(agentName);
|
|
15747
|
+
if (routing)
|
|
15748
|
+
this.endDroppedTurn(agentName, routing);
|
|
15749
|
+
this.eventGate?.onInferenceEnded(agentName);
|
|
15750
|
+
}
|
|
15751
|
+
if (!this.activeTurnTokens.has(agentName) && this.deferredMessages.length > 0 && !this.pendingAssistantBlocks.has(agentName)) {
|
|
15752
|
+
this.readdDrained(this.drainDeferredFor(agentName), 'continuation-drop flush');
|
|
15753
|
+
this.ackDeferredWrites();
|
|
15754
|
+
}
|
|
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
|
+
}
|
|
15246
15784
|
/**
|
|
15247
15785
|
* End a turn that beginAgentTurn drops before it sends anything. The turn
|
|
15248
15786
|
* emitted inference:started, and listeners that bracket a turn, such as a
|
|
@@ -15297,7 +15835,7 @@ export class AgentFramework {
|
|
|
15297
15835
|
* continues one (a restart or a retry) may follow rounds already sent,
|
|
15298
15836
|
* so the note says it was stopped before it finished. */
|
|
15299
15837
|
opensTurn) {
|
|
15300
|
-
console.error(`[turn] ${agent.name}: ${stop.kind === 'caller' ? 'a stop' : 'a quiesce abandon'} reached the turn's start before it ${opensTurn ? 'sent anything' : 'finished'}; it ends there`);
|
|
15838
|
+
console.error(`[turn] ${agent.name}: ${stop.kind === 'caller' ? 'a stop' : stop.kind === 'shutdown' ? "the framework's stop" : 'a quiesce abandon'} reached the turn's start before it ${opensTurn ? 'sent anything' : 'finished'}; it ends there`);
|
|
15301
15839
|
this.channelRegistry?.stopTyping();
|
|
15302
15840
|
// A start that continues a turn (a budget or guard restart) follows a
|
|
15303
15841
|
// frame that kept the turn's tool-result batch, and the guard's recovery,
|
|
@@ -15325,7 +15863,7 @@ export class AgentFramework {
|
|
|
15325
15863
|
this.settleDeliberateCancel({ ...common, reason: stop.reason ?? 'user', before: opensTurn ? 'sending' : 'finishing' });
|
|
15326
15864
|
}
|
|
15327
15865
|
else {
|
|
15328
|
-
this.settleFrameworkCancel({ ...common, kind:
|
|
15866
|
+
this.settleFrameworkCancel({ ...common, kind: stop.kind });
|
|
15329
15867
|
}
|
|
15330
15868
|
const last = this.lastInferenceAt.get(agent.name);
|
|
15331
15869
|
if (last?.startedAt === turnStartedAt)
|
|
@@ -15461,6 +15999,12 @@ export class AgentFramework {
|
|
|
15461
15999
|
const { coalescingSubject: _subject, coalescingBatch: _batch, coalescingBatchSubjects: _subjects, readsDeferredWrite: _read, ...retry } = trigger;
|
|
15462
16000
|
trigger = retry;
|
|
15463
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
|
+
}
|
|
15464
16008
|
// Record turn checkpoint before inference (only on first attempt, not retries)
|
|
15465
16009
|
if (attempt === 0 && trigger?.reason !== 'tool_result_guard_retry') {
|
|
15466
16010
|
this.recordTurnCheckpoint(agent.name);
|
|
@@ -15746,9 +16290,8 @@ export class AgentFramework {
|
|
|
15746
16290
|
// hold the turn as the boundary would rather than send it into the wait.
|
|
15747
16291
|
if (this.conversationAgentHomes.has(agent.name)) {
|
|
15748
16292
|
const wait = this.providerWaits?.active(agent.name, agent.model);
|
|
15749
|
-
|
|
15750
|
-
|
|
15751
|
-
`(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, () => {
|
|
15752
16295
|
// The stream the compile made won't be sent: it's cancelled, and
|
|
15753
16296
|
// this start's window closes, before the hold is reported, so a stop
|
|
15754
16297
|
// made while inference:failed is delivered finds neither, and the
|
|
@@ -15760,8 +16303,15 @@ export class AgentFramework {
|
|
|
15760
16303
|
this.checkAhead.delete(agent);
|
|
15761
16304
|
// The turn emitted inference:started before its compile; every turn
|
|
15762
16305
|
// that does gets one terminal event, which listeners (a typing loop)
|
|
15763
|
-
// 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.
|
|
15764
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);
|
|
15765
16315
|
this.activeStreams.delete(agent.name);
|
|
15766
16316
|
this.pendingAssistantBlocks.delete(agent.name);
|
|
15767
16317
|
this.channelRegistry?.stopTyping();
|
|
@@ -15836,14 +16386,16 @@ export class AgentFramework {
|
|
|
15836
16386
|
if (ownsProviderGate && this.holdProviderAcceleration(agent, err, trigger)) {
|
|
15837
16387
|
// Capacity scheduling is not poisoned history. The cooldown timer owns
|
|
15838
16388
|
// one later fresh compile; no immediate retry or exhaustion marker.
|
|
15839
|
-
|
|
16389
|
+
// (Once stop() has closed admission, nothing is held, and the turn
|
|
16390
|
+
// ends here: endFailureAtStop.)
|
|
16391
|
+
this.endHeldStart(agent, checkWindow, turnToken, turnStartedAt);
|
|
15840
16392
|
return false;
|
|
15841
16393
|
}
|
|
15842
16394
|
const statedWait = ownsProviderGate ? undefined : this.nonOwningProviderWait(agent, err, trigger);
|
|
15843
16395
|
if (statedWait === 'held') {
|
|
15844
16396
|
// A conversation agent held by the provider's stated wait, as a
|
|
15845
16397
|
// resident is: the hold's release owns the later compile.
|
|
15846
|
-
this.
|
|
16398
|
+
this.endHeldStart(agent, checkWindow, turnToken, turnStartedAt);
|
|
15847
16399
|
return false;
|
|
15848
16400
|
}
|
|
15849
16401
|
const action = statedWait === 'fail' ? { retry: false } : this.errorPolicy.onInferenceError(err, agent.name, attempt);
|
|
@@ -15876,7 +16428,16 @@ export class AgentFramework {
|
|
|
15876
16428
|
}
|
|
15877
16429
|
// The retry re-enters startAgentStream, which replaces this frame's
|
|
15878
16430
|
// turn token with its own; cleanup belongs to the innermost frame.
|
|
15879
|
-
|
|
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);
|
|
15880
16441
|
}
|
|
15881
16442
|
else {
|
|
15882
16443
|
// Terminal failure with no driveStream: this frame owns teardown.
|
|
@@ -16019,8 +16580,8 @@ export class AgentFramework {
|
|
|
16019
16580
|
this.recordProviderAccelerationRecovery(agent, compiledRequest, end);
|
|
16020
16581
|
// The accounting writes to the store, and a write that fails must not
|
|
16021
16582
|
// skip what the boundary does after this call: the rest of a turn's
|
|
16022
|
-
// end, or a restart's cancel and its request
|
|
16023
|
-
// dispatch to modules.
|
|
16583
|
+
// end, or a restart's trace (its cancel and its request come before
|
|
16584
|
+
// this call), and then the event's dispatch to modules.
|
|
16024
16585
|
try {
|
|
16025
16586
|
if (turnEnded) {
|
|
16026
16587
|
completeTurn(latestUsage, durationMs);
|
|
@@ -17360,13 +17921,17 @@ export class AgentFramework {
|
|
|
17360
17921
|
if (agent.streamId !== myStreamId)
|
|
17361
17922
|
break;
|
|
17362
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).
|
|
17363
17926
|
if (ownsProviderGate && this.holdProviderAcceleration(agent, err, trigger)) {
|
|
17927
|
+
this.endFailureAtStop(agent.name, myTurnToken ?? -1, startTime);
|
|
17364
17928
|
if (this.agents.get(agent.name) === agent && agent.streamId === myStreamId)
|
|
17365
17929
|
this.eventGate?.onInferenceEnded(agent.name);
|
|
17366
17930
|
break;
|
|
17367
17931
|
}
|
|
17368
17932
|
const statedWait = ownsProviderGate ? undefined : this.nonOwningProviderWait(agent, err, trigger);
|
|
17369
17933
|
if (statedWait === 'held') {
|
|
17934
|
+
this.endFailureAtStop(agent.name, myTurnToken ?? -1, startTime);
|
|
17370
17935
|
if (this.agents.get(agent.name) === agent && agent.streamId === myStreamId)
|
|
17371
17936
|
this.eventGate?.onInferenceEnded(agent.name);
|
|
17372
17937
|
break;
|
|
@@ -17405,7 +17970,8 @@ export class AgentFramework {
|
|
|
17405
17970
|
}
|
|
17406
17971
|
return;
|
|
17407
17972
|
}
|
|
17408
|
-
|
|
17973
|
+
// Marked as its turn's retry, as at the start's failure.
|
|
17974
|
+
await this.startAgentStream(agent, trigger && asTurnRetry(trigger), attempt + 1);
|
|
17409
17975
|
}
|
|
17410
17976
|
else {
|
|
17411
17977
|
// The turn ends here, and its inference:failed doesn't say so,
|
|
@@ -17715,10 +18281,16 @@ export class AgentFramework {
|
|
|
17715
18281
|
const durationMs = Date.now() - startTime;
|
|
17716
18282
|
// The stream won't go on: the agent stops holding it before the
|
|
17717
18283
|
// failure is reported, as at the stream's error event, so a stop made
|
|
17718
|
-
// 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.
|
|
17719
18288
|
agent.reset();
|
|
18289
|
+
this.emitTrace({ type: 'inference:failed', agentName: agent.name, error: err.message, stack: err.stack });
|
|
17720
18290
|
if (ownsProviderGate && this.holdProviderAcceleration(agent, err, trigger)) {
|
|
17721
|
-
|
|
18291
|
+
// Once stop() has closed admission, nothing is held, and the turn
|
|
18292
|
+
// ends here (endFailureAtStop).
|
|
18293
|
+
this.endFailureAtStop(agent.name, myTurnToken ?? -1, startTime);
|
|
17722
18294
|
logStream({ success: false,
|
|
17723
18295
|
error: `Provider acceleration cooldown: ${err.message}`,
|
|
17724
18296
|
request: compiledRequest ?? { note: 'streaming request rate-limited' }, durationMs });
|
|
@@ -17735,7 +18307,7 @@ export class AgentFramework {
|
|
|
17735
18307
|
// says why it ends.
|
|
17736
18308
|
const statedWait = ownsProviderGate ? undefined : this.nonOwningProviderWait(agent, err, trigger);
|
|
17737
18309
|
if (statedWait === 'held') {
|
|
17738
|
-
this.
|
|
18310
|
+
this.endFailureAtStop(agent.name, myTurnToken ?? -1, startTime);
|
|
17739
18311
|
logStream({ success: false,
|
|
17740
18312
|
error: `Provider wait: ${err.message}`,
|
|
17741
18313
|
request: compiledRequest ?? { note: 'streaming request rate-limited' }, durationMs });
|
|
@@ -17746,12 +18318,6 @@ export class AgentFramework {
|
|
|
17746
18318
|
this.eventGate?.onInferenceEnded(agent.name);
|
|
17747
18319
|
return;
|
|
17748
18320
|
}
|
|
17749
|
-
this.emitTrace({
|
|
17750
|
-
type: 'inference:failed',
|
|
17751
|
-
agentName: agent.name,
|
|
17752
|
-
error: err.message,
|
|
17753
|
-
stack: err.stack,
|
|
17754
|
-
});
|
|
17755
18321
|
this.settleAgent(agent.name, {
|
|
17756
18322
|
stopReason: 'exhausted',
|
|
17757
18323
|
speech: '',
|
|
@@ -17933,7 +18499,11 @@ export class AgentFramework {
|
|
|
17933
18499
|
// its ids when a successor turn has already taken the agent. A fork or
|
|
17934
18500
|
// run deregistered while the host is quiesced leaves its queue to its
|
|
17935
18501
|
// window's release at resume (drainDeferredFor).
|
|
17936
|
-
|
|
18502
|
+
// A frame that handed its turn to a continuation since dropped
|
|
18503
|
+
// (endContinuedTurn, which couldn't land it while this frame held the
|
|
18504
|
+
// token) lands it too: the turn is over.
|
|
18505
|
+
const handedOffTurnOver = preserveEventGateForSuccessor && !this.hasTurn(agent.name);
|
|
18506
|
+
if ((frameReachedTerminal || handedOffTurnOver) && this.deferredMessages.length > 0 && !this.pendingAssistantBlocks.has(agent.name)) {
|
|
17937
18507
|
this.readdDrained(this.drainDeferredFor(agent.name), 'turn-end flush');
|
|
17938
18508
|
this.ackDeferredWrites();
|
|
17939
18509
|
}
|
|
@@ -17955,8 +18525,11 @@ export class AgentFramework {
|
|
|
17955
18525
|
// frame that ends the turn disposes it, after the flush above, so what
|
|
17956
18526
|
// was held for the turn's end lands in the fork's store instead of
|
|
17957
18527
|
// being dropped as addressed to no agent. A frame that hands the turn
|
|
17958
|
-
// to a successor (a guard or budget restart) leaves the fork to it
|
|
17959
|
-
|
|
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)) {
|
|
17960
18533
|
this.disposeConversationAgent(agent.name);
|
|
17961
18534
|
}
|
|
17962
18535
|
}
|
|
@@ -18036,10 +18609,16 @@ export class AgentFramework {
|
|
|
18036
18609
|
if (this.surgeryHold) {
|
|
18037
18610
|
throw new Error(`puppet refused: the store is under live ${this.surgeryHold.verb} for ${this.surgeryHold.agentName} — retry when it completes`);
|
|
18038
18611
|
}
|
|
18039
|
-
if (agent.state.status !== 'idle' || this.
|
|
18612
|
+
if (agent.state.status !== 'idle' || this.hasTurn(agentName)) {
|
|
18040
18613
|
const shown = agent.state.status === 'idle' ? 'idle+turn-alive' : agent.state.status;
|
|
18041
|
-
|
|
18042
|
-
|
|
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)`);
|
|
18043
18622
|
}
|
|
18044
18623
|
const onSurface = this.getToolsForAgent(agentName)
|
|
18045
18624
|
.some((t) => t.name === toolName && agent.canUseTool(t.name));
|
|
@@ -18934,7 +19513,8 @@ export class AgentFramework {
|
|
|
18934
19513
|
}
|
|
18935
19514
|
if (refusal !== undefined)
|
|
18936
19515
|
return refused(refusal);
|
|
18937
|
-
const
|
|
19516
|
+
const route = { toServer: false };
|
|
19517
|
+
const result = await this.dispatchScriptToolCall(agentName, toolName, input, context.origin, context.watch?.turnId, route);
|
|
18938
19518
|
// Deferred: applied to the result of the code_execution call watching
|
|
18939
19519
|
// the script — ending the turn mid-script would cancel the stream while
|
|
18940
19520
|
// the script still runs and wedge the tool round. A call a left script
|
|
@@ -18946,7 +19526,7 @@ export class AgentFramework {
|
|
|
18946
19526
|
watching.endTurn = true;
|
|
18947
19527
|
if (result.refused)
|
|
18948
19528
|
return refused(result.error ?? `${toolName} was refused; nothing was done`);
|
|
18949
|
-
const answer = await this.scriptResultAnswer(result, toolName);
|
|
19529
|
+
const answer = await this.scriptResultAnswer(result, toolName, route.toServer);
|
|
18950
19530
|
return SCRIPT_CALL_NO_RESULT.has(result) ? { ...answer, held: true } : answer;
|
|
18951
19531
|
}
|
|
18952
19532
|
/**
|
|
@@ -18954,14 +19534,20 @@ export class AgentFramework {
|
|
|
18954
19534
|
* isError }`, one shape for every tool. `content` is
|
|
18955
19535
|
* what the model would read: a structured result's blocks as the dispatch
|
|
18956
19536
|
* gave them (the direct reading, with RFC-005's treatment of an MCPL peer's
|
|
18957
|
-
* blocks
|
|
18958
|
-
*
|
|
19537
|
+
* blocks there, bound to the server that sent them), a module's under
|
|
19538
|
+
* history's reading of RFC-005 at every depth (shownToScript), and
|
|
19539
|
+
* otherwise one text block of the model's text, which for a failure is its
|
|
19540
|
+
* error. A module's is any result whose call the dispatch didn't send to
|
|
19541
|
+
* a server (`toServer`, from the call's waiter). `structuredContent` is the
|
|
19542
|
+
* tool's own structured
|
|
18959
19543
|
* content where it sent one, `false`, `0` and `null` included, else null.
|
|
18960
19544
|
* A result that can't be converted to JSON (a cyclic object, a BigInt),
|
|
18961
19545
|
* which the dispatch's own check makes unlikely here, is a failure naming
|
|
18962
|
-
* 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).
|
|
18963
19549
|
*/
|
|
18964
|
-
async scriptResultAnswer(result, toolName) {
|
|
19550
|
+
async scriptResultAnswer(result, toolName, toServer) {
|
|
18965
19551
|
const structured = Object.prototype.hasOwnProperty.call(result, 'structured');
|
|
18966
19552
|
const isError = result.isError === true;
|
|
18967
19553
|
// RFC-005 "eagerly lazy": a script receiving a result is the strongest
|
|
@@ -18978,7 +19564,10 @@ export class AgentFramework {
|
|
|
18978
19564
|
// which isn't fetched.
|
|
18979
19565
|
// Whenever the result's blocks reach the script: a structured failure's
|
|
18980
19566
|
// included, as a module's error can carry content.
|
|
18981
|
-
|
|
19567
|
+
// Not a server's result: a module's, whose blocks no dispatch has given
|
|
19568
|
+
// RFC-005's treatment.
|
|
19569
|
+
const fromModule = !toServer;
|
|
19570
|
+
if ((!isError || structured) && Array.isArray(result.data) && fromModule) {
|
|
18982
19571
|
const fetcher = this.getReferenceFetcher();
|
|
18983
19572
|
for (const block of result.data) {
|
|
18984
19573
|
const c = classifyBlock(block);
|
|
@@ -19000,6 +19589,10 @@ export class AgentFramework {
|
|
|
19000
19589
|
brief: error,
|
|
19001
19590
|
};
|
|
19002
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));
|
|
19003
19596
|
let content;
|
|
19004
19597
|
let text;
|
|
19005
19598
|
// A structured result's blocks, as #538 gives them: none when its content
|
|
@@ -19007,6 +19600,21 @@ export class AgentFramework {
|
|
|
19007
19600
|
// no blocks reads as its error text.
|
|
19008
19601
|
if (structured && (!isError || (Array.isArray(result.data) && result.data.length > 0))) {
|
|
19009
19602
|
content = Array.isArray(result.data) ? result.data : [];
|
|
19603
|
+
// A module's blocks, such as an MCPL peer's it got from ctx.callTool
|
|
19604
|
+
// and returns whole, are read as history reads a module's data: what
|
|
19605
|
+
// RFC-005 governs, at any depth, as the script lane shows it, and
|
|
19606
|
+
// anything else as sent. What a script prints comes back to the model,
|
|
19607
|
+
// so no uri the model's lanes withhold reaches it. What this gives is
|
|
19608
|
+
// a stub, a notice or the value itself, so a module that returns
|
|
19609
|
+
// blocks it already gave has them left as they are.
|
|
19610
|
+
if (fromModule) {
|
|
19611
|
+
try {
|
|
19612
|
+
content = JSON.parse(JSON.stringify(content, (_key, value) => shownToScript(value)));
|
|
19613
|
+
}
|
|
19614
|
+
catch (err) {
|
|
19615
|
+
return failed(err);
|
|
19616
|
+
}
|
|
19617
|
+
}
|
|
19010
19618
|
}
|
|
19011
19619
|
else {
|
|
19012
19620
|
// As in history, a reference is its stub at any depth and inline data
|
|
@@ -19045,9 +19653,11 @@ export class AgentFramework {
|
|
|
19045
19653
|
* RFC-007: with an `origin`, observers see the call as they see a model
|
|
19046
19654
|
* call (register, dispatch with its refusal sites, open; the terminal at
|
|
19047
19655
|
* the waiter intercept), under the inner tool's own name and class.
|
|
19048
|
-
* `turnId` is the turn the call was made in (dispatchToolCall).
|
|
19656
|
+
* `turnId` is the turn the call was made in (dispatchToolCall). `route` is
|
|
19657
|
+
* the waiter's: the dispatch sets `toServer` when it sends the call to a
|
|
19658
|
+
* server.
|
|
19049
19659
|
*/
|
|
19050
|
-
dispatchScriptToolCall(agentName, toolName, args, origin, turnId) {
|
|
19660
|
+
dispatchScriptToolCall(agentName, toolName, args, origin, turnId, route = { toServer: false }) {
|
|
19051
19661
|
return new Promise((resolve) => {
|
|
19052
19662
|
const callId = `pytc-${randomUUID()}`;
|
|
19053
19663
|
// Host-side leak guard: the script's runner already gives the script a
|
|
@@ -19073,9 +19683,12 @@ export class AgentFramework {
|
|
|
19073
19683
|
}
|
|
19074
19684
|
}, safetyMs);
|
|
19075
19685
|
safety.unref?.();
|
|
19076
|
-
this.scriptToolWaiters.set(callId,
|
|
19077
|
-
|
|
19078
|
-
|
|
19686
|
+
this.scriptToolWaiters.set(callId, {
|
|
19687
|
+
resolve: (result) => {
|
|
19688
|
+
clearTimeout(safety);
|
|
19689
|
+
resolve(result);
|
|
19690
|
+
},
|
|
19691
|
+
route,
|
|
19079
19692
|
});
|
|
19080
19693
|
const call = { id: callId, name: toolName, input: args };
|
|
19081
19694
|
if (origin)
|
|
@@ -19606,6 +20219,11 @@ export class AgentFramework {
|
|
|
19606
20219
|
// Route MCPL tool calls to the appropriate server via prefix map
|
|
19607
20220
|
const mcplMatch = this.resolveMcplTool(enrichedCall.name);
|
|
19608
20221
|
if (mcplMatch && this.mcplServerRegistry) {
|
|
20222
|
+
// A script's call: its waiter keeps that it went to a server, for the
|
|
20223
|
+
// script lane's exit (scriptToolWaiters).
|
|
20224
|
+
const scriptWaiter = this.scriptToolWaiters.get(enrichedCall.id);
|
|
20225
|
+
if (scriptWaiter)
|
|
20226
|
+
scriptWaiter.route.toServer = true;
|
|
19609
20227
|
this.dispatchMcplToolCall(agentName, enrichedCall, mcplMatch[0], mcplMatch[1]);
|
|
19610
20228
|
return;
|
|
19611
20229
|
}
|
|
@@ -24205,8 +24823,8 @@ export class AgentFramework {
|
|
|
24205
24823
|
});
|
|
24206
24824
|
}
|
|
24207
24825
|
/** An MCP tool call's trace: a result the server marked an error (or one
|
|
24208
|
-
* the reading
|
|
24209
|
-
* 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. */
|
|
24210
24828
|
traceMcpToolOutcome(serverId, tool, callId, result, durationMs) {
|
|
24211
24829
|
if (result.isError) {
|
|
24212
24830
|
this.emitTrace({ type: 'tool:failed', module: `mcpl:${serverId}`, tool, callId, durationMs, error: result.error ?? 'tool returned an error' });
|
|
@@ -24615,15 +25233,44 @@ export class AgentFramework {
|
|
|
24615
25233
|
// generated JSON) stands in for what the server sent. An MCPL peer's
|
|
24616
25234
|
// content blocks are the exception: what a script prints comes back to
|
|
24617
25235
|
// the model, so each gets the treatment RFC-005 gives it on every lane
|
|
24618
|
-
// the model reads
|
|
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.
|
|
24619
25242
|
const forScript = this.scriptToolWaiters?.has(call.id) ?? false;
|
|
24620
25243
|
const reading = async (result, mcpl) => {
|
|
24621
25244
|
if (!forScript || !Object.prototype.hasOwnProperty.call(result, 'structuredContent')) {
|
|
24622
25245
|
return this.mcpToolResult(result, call.id, mcpl, serverId, call.name);
|
|
24623
25246
|
}
|
|
24624
25247
|
const direct = AgentFramework.directToolResult(result, serverId, call.name);
|
|
24625
|
-
if (mcpl)
|
|
24626
|
-
|
|
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
|
+
}
|
|
24627
25274
|
// A standard peer's content that isn't a list of blocks is a malformed
|
|
24628
25275
|
// result on the model's reading (normalizeStandardToolResult), so the
|
|
24629
25276
|
// script reads it that way too, its structured content kept.
|
|
@@ -24821,7 +25468,8 @@ export class AgentFramework {
|
|
|
24821
25468
|
* direct path (a module's callTool, a channel's tool call) doesn't come
|
|
24822
25469
|
* here: it returns the server's result itself (directToolResult), for
|
|
24823
25470
|
* either engine, as does a script's call whose result is structured, with
|
|
24824
|
-
* 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).
|
|
24825
25473
|
*/
|
|
24826
25474
|
async mcpToolResult(result, callId, mcplPeer, serverId, callName) {
|
|
24827
25475
|
const hasStructured = Object.prototype.hasOwnProperty.call(result, 'structuredContent');
|