@animalabs/agent-framework 0.26.1 → 0.26.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/src/framework.d.ts +70 -27
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +441 -97
- package/dist/src/framework.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +4 -4
- package/dist/src/mcpl/references.js +5 -5
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/tool-result-history.d.ts +14 -1
- package/dist/src/tool-result-history.d.ts.map +1 -1
- package/dist/src/tool-result-history.js +22 -3
- package/dist/src/tool-result-history.js.map +1 -1
- package/dist/src/types/events.d.ts +4 -1
- package/dist/src/types/events.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/framework.ts +443 -99
- package/src/mcpl/references.ts +5 -5
- package/src/tool-result-history.ts +22 -3
- package/src/types/events.ts +4 -1
package/src/framework.ts
CHANGED
|
@@ -117,6 +117,7 @@ import type { ToolResultGuard, WithheldSpill } from './tool-result-guard.js';
|
|
|
117
117
|
import {
|
|
118
118
|
nonErrorResultText,
|
|
119
119
|
shownToScript,
|
|
120
|
+
shownToScriptFromServer,
|
|
120
121
|
toolResultDataToHistoryString,
|
|
121
122
|
truncateForHistory,
|
|
122
123
|
DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS,
|
|
@@ -990,6 +991,71 @@ function resumesLogicalTurn(reason: string | undefined): boolean {
|
|
|
990
991
|
return reason === 'context_budget_restart' || reason === 'tool_result_guard_retry';
|
|
991
992
|
}
|
|
992
993
|
|
|
994
|
+
/**
|
|
995
|
+
* Marks a held turn's retry: the request a provider hold keeps for a turn
|
|
996
|
+
* whose call failed into it, or whose compile it held with nothing sent.
|
|
997
|
+
* That turn has begun and not ended: it starts again when the hold releases
|
|
998
|
+
* (a new start of its open turn, as 0.26.0 started it). So, as a
|
|
999
|
+
* continuation on its way does (continuesTurn), it keeps its agent's turn
|
|
1000
|
+
* alive meanwhile, passes a quiesce, and goes with an abandon, a stop() or
|
|
1001
|
+
* its agent's removal. A symbol, not a field: hosts build requests too, and
|
|
1002
|
+
* only the framework may say one is a turn's own retry. Spreads and rests
|
|
1003
|
+
* copy it, as every copy of a request the scheduler makes is one.
|
|
1004
|
+
*/
|
|
1005
|
+
const TURN_RETRY: unique symbol = Symbol('turn-retry');
|
|
1006
|
+
/**
|
|
1007
|
+
* The batch a start that carries a turn on (continuesTurn) was merged from,
|
|
1008
|
+
* when the scheduler merged wakes that aren't the turn's into it: every
|
|
1009
|
+
* request it took in, as each was. Until the start takes its turn token it
|
|
1010
|
+
* stands for them all. A scheduler pass that takes it back puts the batch in
|
|
1011
|
+
* its place, each request with its own identity and flags; a quiesce at the
|
|
1012
|
+
* auxiliary park sends the batch back to the scheduler; and a caller's stop
|
|
1013
|
+
* or an abandon that drops the start puts back the batch's wakes, the
|
|
1014
|
+
* requests that don't carry the turn on (dropContinuations), where they run
|
|
1015
|
+
* as a turn of their own, as wakes queued while a turn streams do. Once the
|
|
1016
|
+
* start takes its token the turn reads them, and beginAgentTurn drops the
|
|
1017
|
+
* record. A symbol, as TURN_RETRY is, so spreads and rests copy it.
|
|
1018
|
+
*/
|
|
1019
|
+
const MERGED_BATCH: unique symbol = Symbol('merged-batch');
|
|
1020
|
+
type MarkedRequest = InferenceRequest & { [TURN_RETRY]?: true; [MERGED_BATCH]?: InferenceRequest[] };
|
|
1021
|
+
|
|
1022
|
+
function mergedBatch(request: InferenceRequest): InferenceRequest[] | undefined {
|
|
1023
|
+
return (request as MarkedRequest)[MERGED_BATCH];
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/** The wakes of a merged start's batch (MERGED_BATCH): its requests that
|
|
1027
|
+
* don't carry the turn on. */
|
|
1028
|
+
function mergedWakes(request: InferenceRequest): InferenceRequest[] {
|
|
1029
|
+
return (mergedBatch(request) ?? []).filter((r) => !continuesTurn(r));
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function isTurnRetry(request: InferenceRequest): boolean {
|
|
1033
|
+
return (request as MarkedRequest)[TURN_RETRY] === true;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/** How to go on sooner than a hold keeping a turn's retry lets the turn go
|
|
1037
|
+
* on: what surgery's and the puppet's refusals advise. */
|
|
1038
|
+
function heldTurnWayOn(statedWait: boolean): string {
|
|
1039
|
+
return 'a quiesce lets a held turn start when its hold ends and finish, so to go on sooner, end it with quiesce ' +
|
|
1040
|
+
'with abandon: true, which ends it once the quiesce\'s wait for the drain runs out (at once on a host ' +
|
|
1041
|
+
'already quiesced)' +
|
|
1042
|
+
(statedWait
|
|
1043
|
+
? ", or lift a stated wait with release-provider-wait, so the turn tries again once no host's or " +
|
|
1044
|
+
'acceleration hold binds it'
|
|
1045
|
+
: '');
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/** A copy of a held turn's trigger, marked as its retry (TURN_RETRY). */
|
|
1049
|
+
function asTurnRetry(trigger: InferenceRequest): InferenceRequest {
|
|
1050
|
+
return { ...trigger, [TURN_RETRY]: true } as MarkedRequest;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/** A request that carries its agent's turn on: a continuation of it, or the
|
|
1054
|
+
* retry a provider hold keeps for it (TURN_RETRY). */
|
|
1055
|
+
function continuesTurn(request: InferenceRequest): boolean {
|
|
1056
|
+
return resumesLogicalTurn(request.reason) || isTurnRetry(request);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
993
1059
|
/**
|
|
994
1060
|
* The answers dispatchScriptToolCall's safety timer made, when no result came
|
|
995
1061
|
* for a script's call: the tool may still be running, or its result was lost,
|
|
@@ -998,6 +1064,20 @@ function resumesLogicalTurn(reason: string | undefined): boolean {
|
|
|
998
1064
|
*/
|
|
999
1065
|
const SCRIPT_CALL_NO_RESULT = new WeakSet<ToolResult>();
|
|
1000
1066
|
|
|
1067
|
+
/**
|
|
1068
|
+
* An MCPL peer's results whose blocks couldn't be given RFC-005's treatment
|
|
1069
|
+
* for a script, because JSON couldn't hold them (dispatchMcplToolCall's
|
|
1070
|
+
* reading), each with why. The failure is the script lane's alone: the
|
|
1071
|
+
* model's lane reads the same answer with no such round trip. So such a
|
|
1072
|
+
* result keeps the peer's own outcome, its blocks dropped, and the trace and
|
|
1073
|
+
* the call's terminal report what the peer answered, as on v0.26.1, while
|
|
1074
|
+
* the script lane's exit answers it as a result it can't convert to JSON
|
|
1075
|
+
* (scriptResultAnswer). A module's result that JSON can't hold differs: no
|
|
1076
|
+
* route can store or send it, so its dispatch makes it an error result for
|
|
1077
|
+
* everyone (checkedToolResult).
|
|
1078
|
+
*/
|
|
1079
|
+
const SCRIPT_RESULT_UNCONVERTIBLE = new WeakMap<ToolResult, unknown>();
|
|
1080
|
+
|
|
1001
1081
|
/**
|
|
1002
1082
|
* One logical turn's routing state and receipt tallies
|
|
1003
1083
|
* (AgentFramework.turnRouting): everything the turn has decided about where
|
|
@@ -1231,8 +1311,10 @@ function ephemeralRunStopped(name: string, observed?: string): Error {
|
|
|
1231
1311
|
|
|
1232
1312
|
/** The reason on the inference:aborted that ends a turn whose retry stop()
|
|
1233
1313
|
* cancelled, whichever frame was waiting to retry: beginAgentTurn's, when
|
|
1234
|
-
* the stream couldn't start, or driveStream's, when it failed once started
|
|
1235
|
-
*
|
|
1314
|
+
* the stream couldn't start, or driveStream's, when it failed once started;
|
|
1315
|
+
* or whose failure a provider hold would have kept for its retry once stop()
|
|
1316
|
+
* had closed admission (endFailureAtStop). Its inference:failed came first,
|
|
1317
|
+
* and this says no retry follows it. */
|
|
1236
1318
|
const RETRY_CANCELLED_BY_STOP = 'a retry waiting while the framework was stopping is dropped before it sends anything';
|
|
1237
1319
|
|
|
1238
1320
|
/** The reason on the inference:aborted that ends a turn whose start waited to
|
|
@@ -1260,6 +1342,12 @@ function isTurnContinuation(reason: string): boolean {
|
|
|
1260
1342
|
|| reason === 'tool_result_guard_retry';
|
|
1261
1343
|
}
|
|
1262
1344
|
|
|
1345
|
+
/** A request a quiesce lets through, so a turn already alive can finish:
|
|
1346
|
+
* a continuation of the turn, or a held turn's retry (TURN_RETRY). */
|
|
1347
|
+
function passesQuiesce(request: InferenceRequest | undefined): boolean {
|
|
1348
|
+
return request !== undefined && (isTurnContinuation(request.reason) || isTurnRetry(request));
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1263
1351
|
/** Wake sources the framework generates for itself (bounce wakes,
|
|
1264
1352
|
* continuations, retries, unsticks, admission requeues). A wake from any
|
|
1265
1353
|
* other source — a channel message, a heartbeat or other server event, a
|
|
@@ -1609,18 +1697,21 @@ export interface HostModeStatus {
|
|
|
1609
1697
|
reason?: string;
|
|
1610
1698
|
since?: number;
|
|
1611
1699
|
/** True when no turn is alive: no turn token (the token spans dequeue →
|
|
1612
|
-
* settled teardown, strictly wider than activeStreams)
|
|
1700
|
+
* settled teardown, strictly wider than activeStreams), no continuation
|
|
1613
1701
|
* of a turn on its way to its start (a budget restart's or a tool-result
|
|
1614
1702
|
* guard's retry, queued, held by a provider hold, parked or in a scheduler
|
|
1615
|
-
* pass),
|
|
1703
|
+
* pass), no turn whose call failed into a provider hold waiting for the
|
|
1704
|
+
* hold to release its retry (wherever that retry then waits), and no wake
|
|
1705
|
+
* parked on provider admission. It covers the
|
|
1616
1706
|
* framework's own turns: a caller of the direct Agent API
|
|
1617
1707
|
* (`Agent.runInference`) owns its turns' timing. */
|
|
1618
1708
|
drained: boolean;
|
|
1619
1709
|
activeTurns: number;
|
|
1620
1710
|
/** The agents with a turn alive, the ones `activeTurns` counts, sorted:
|
|
1621
1711
|
* each from its turn's dequeue to its settled teardown, so a turn in its
|
|
1622
|
-
* hooks or compile, one finishing its deliveries,
|
|
1623
|
-
* is on its way to its start,
|
|
1712
|
+
* hooks or compile, one finishing its deliveries, one whose continuation
|
|
1713
|
+
* is on its way to its start, or one a provider hold keeps for its retry,
|
|
1714
|
+
* is listed. These are the agents an abandon or
|
|
1624
1715
|
* a stop() made now would find mid-turn. */
|
|
1625
1716
|
activeTurnAgents: string[];
|
|
1626
1717
|
/** Wakes that own provider admission while waiting for an in-flight
|
|
@@ -1632,7 +1723,8 @@ export interface HostModeStatus {
|
|
|
1632
1723
|
/** Inference requests parked by the quiesce wake gate (while quiesced), or
|
|
1633
1724
|
* simply queued (while serving — non-zero right after a resume until the
|
|
1634
1725
|
* scheduler's next pass consumes them). Continuations of a held turn
|
|
1635
|
-
* (budget restart, tool results)
|
|
1726
|
+
* (budget restart, tool results) and a held turn's retry, which a quiesce
|
|
1727
|
+
* lets through, are never counted. */
|
|
1636
1728
|
gatedRequests: number;
|
|
1637
1729
|
/** code_execution scripts still running that no call is waiting on: left
|
|
1638
1730
|
* running after a yield, outside any turn. Quiesce does NOT stop them —
|
|
@@ -2048,12 +2140,14 @@ function scriptResultBrief(content: unknown[]): string {
|
|
|
2048
2140
|
|
|
2049
2141
|
/**
|
|
2050
2142
|
* What a script gets for a tool result that can't be converted to JSON,
|
|
2051
|
-
* such as a cyclic object or a BigInt that an in-process module returned
|
|
2143
|
+
* such as a cyclic object or a BigInt that an in-process module returned,
|
|
2144
|
+
* or an MCPL peer's blocks nested deeper than JSON.stringify can take.
|
|
2052
2145
|
* It names the tool, and says the call itself completed, so a script that
|
|
2053
2146
|
* reports it doesn't invite a blind retry. The host's log gets the same
|
|
2054
|
-
* line, since the tool
|
|
2055
|
-
* script reports it. The reason and the
|
|
2056
|
-
* VALUE_CHARS each, so the failure reaches the
|
|
2147
|
+
* line, since the tool, a module's or a server's, returns what a script
|
|
2148
|
+
* can't receive, whether or not the script reports it. The reason and the
|
|
2149
|
+
* tool's own error are cut to VALUE_CHARS each, so the failure reaches the
|
|
2150
|
+
* script whatever its memory.
|
|
2057
2151
|
*/
|
|
2058
2152
|
function unconvertibleScriptResult(toolName: string, err: unknown, ownError?: string): string {
|
|
2059
2153
|
let reason: string;
|
|
@@ -2091,7 +2185,9 @@ const SCRIPT_TEXT_ANSWER_ENVELOPE_BYTES = Buffer.byteLength(
|
|
|
2091
2185
|
* dense in small objects or a script already holding most of its memory.
|
|
2092
2186
|
* Such a result, or one past the figure that doesn't fit, reaches the script
|
|
2093
2187
|
* as ResultTooLarge, saying the call took effect, and nothing of it is cut or
|
|
2094
|
-
* saved.
|
|
2188
|
+
* saved. One the host can't convert to JSON at all, such as one nested deeper
|
|
2189
|
+
* than its runtime's JSON.stringify can take, is answered instead as a
|
|
2190
|
+
* failure naming the tool and saying the call itself completed.
|
|
2095
2191
|
*/
|
|
2096
2192
|
const scriptResultMaxChars = (memoryMaxBytes: number): number =>
|
|
2097
2193
|
Math.max(0, Math.floor((Math.min(maxResultBytes(memoryMaxBytes), bufferConstants.MAX_STRING_LENGTH) - SCRIPT_TEXT_ANSWER_ENVELOPE_BYTES) / 6));
|
|
@@ -2402,7 +2498,11 @@ type FrameworkCancelKind = 'turn_ended' | 'budget_restart' | 'quiesce_abandoned'
|
|
|
2402
2498
|
* operator's quiesce abandon or stop() settles it as a stop ends a turn's
|
|
2403
2499
|
* start; any other drop ends it as a start dropped before it sends anything.
|
|
2404
2500
|
* `why` is said in the line its end logs. */
|
|
2405
|
-
type ContinuationDrop =
|
|
2501
|
+
type ContinuationDrop =
|
|
2502
|
+
| { kind: 'quiesce_abandoned' | 'shutdown' | 'dropped'; why: string }
|
|
2503
|
+
/** A caller's stop (abortInference) of a turn whose continuation or retry
|
|
2504
|
+
* is on its way, with the reason it gave. */
|
|
2505
|
+
| { kind: 'caller'; reason?: string; why: string };
|
|
2406
2506
|
|
|
2407
2507
|
/** What opened a channel the agent didn't ask to open
|
|
2408
2508
|
* (recordChannelAutoOpenNotice). */
|
|
@@ -2889,12 +2989,14 @@ export class AgentFramework {
|
|
|
2889
2989
|
* is still the agent's, so a start that ends after a later frame opened
|
|
2890
2990
|
* one, such as its retry's stream waiting to retry, leaves that one open.
|
|
2891
2991
|
* An operator's abandon marks the window, and the frame's check collects
|
|
2892
|
-
* the mark, which goes with the window.
|
|
2893
|
-
*
|
|
2894
|
-
*
|
|
2895
|
-
*
|
|
2896
|
-
*
|
|
2897
|
-
*
|
|
2992
|
+
* the mark, which goes with the window. A stop of a turn with no stream
|
|
2993
|
+
* lands here, or, for a turn whose continuation or retry is on its way (a
|
|
2994
|
+
* provider hold's, a park's, the queue's or a pass's), through
|
|
2995
|
+
* abortInference(), an abandon or stop(), which drop that continuation or
|
|
2996
|
+
* retry (dropContinuations); a stop made anywhere else, as while an agent
|
|
2997
|
+
* is idle or a turn finishes its last delivery, has no check to end it,
|
|
2998
|
+
* and the next start drops it. Keyed by the Agent, so a replacement agent
|
|
2999
|
+
* under the same name never shares one.
|
|
2898
3000
|
*/
|
|
2899
3001
|
private checkAhead: Map<Agent, CheckWindow> = new Map();
|
|
2900
3002
|
|
|
@@ -4645,6 +4747,21 @@ export class AgentFramework {
|
|
|
4645
4747
|
a.coalescingSubject === b.coalescingSubject && batchSubjects(a) === batchSubjects(b) &&
|
|
4646
4748
|
a.readsDeferredWrite === b.readsDeferredWrite;
|
|
4647
4749
|
}
|
|
4750
|
+
/** Keep `request` among a hold's `held` requests, once by
|
|
4751
|
+
* sameInferenceRequest. A held turn's retry (TURN_RETRY) whose equal is
|
|
4752
|
+
* already held marks that one, so the hold still keeps the turn alive. */
|
|
4753
|
+
private holdRequest(held: InferenceRequest[], request: InferenceRequest): void {
|
|
4754
|
+
const same = held.find((r) => this.sameInferenceRequest(r, request));
|
|
4755
|
+
if (!same) {
|
|
4756
|
+
held.push(request);
|
|
4757
|
+
return;
|
|
4758
|
+
}
|
|
4759
|
+
if (isTurnRetry(request)) (same as MarkedRequest)[TURN_RETRY] = true;
|
|
4760
|
+
// A merged start's batch (MERGED_BATCH) stays recorded on the request
|
|
4761
|
+
// kept in its place, which then stands for it, as the start did.
|
|
4762
|
+
const batch = mergedBatch(request);
|
|
4763
|
+
if (batch) (same as MarkedRequest)[MERGED_BATCH] = [...new Set([...(mergedBatch(same) ?? []), ...batch])];
|
|
4764
|
+
}
|
|
4648
4765
|
private consultProviderHold(error: Error, agentName: string): { holdMs: number; reason: string } | undefined {
|
|
4649
4766
|
if (!this.providerHoldHook) return undefined;
|
|
4650
4767
|
try {
|
|
@@ -4696,14 +4813,19 @@ export class AgentFramework {
|
|
|
4696
4813
|
// failure streak), and the wait applies to the calls that follow it.
|
|
4697
4814
|
if (!acceleration && !hostHold) {
|
|
4698
4815
|
return wait !== undefined && isMembraneError(error) && error.retryable === true
|
|
4699
|
-
&& this.holdForProviderWait(agent, wait, trigger ? [trigger] : [], true);
|
|
4816
|
+
&& this.holdForProviderWait(agent, wait, trigger ? [asTurnRetry(trigger)] : [], true);
|
|
4700
4817
|
}
|
|
4818
|
+
// As in holdForProviderWait: once stop() has closed admission, nothing is
|
|
4819
|
+
// held, and nothing started.
|
|
4820
|
+
if (this.providerAdmissionClosed) return true;
|
|
4701
4821
|
const now = Date.now();
|
|
4702
4822
|
const pacing = hostHold ? undefined : this.accelerationCooldownMs(agent.name, error as MembraneError);
|
|
4703
4823
|
const delayMs = hostHold ? hostHold.holdMs : pacing!.delayMs;
|
|
4704
4824
|
const existing = this.providerAccelerationCooldowns.get(agent.name);
|
|
4705
4825
|
const held = existing?.heldRequests ?? [];
|
|
4706
|
-
|
|
4826
|
+
// The failed turn's own request, kept as its retry (TURN_RETRY). An
|
|
4827
|
+
// auxiliary call's failure passes no trigger: it holds no turn of its own.
|
|
4828
|
+
if (trigger) this.holdRequest(held, asTurnRetry(trigger));
|
|
4707
4829
|
if (existing) clearTimeout(existing.timer);
|
|
4708
4830
|
const timer = setTimeout(() => this.releaseProviderAccelerationCooldown(agent.name), Math.min(delayMs, MAX_TIMER_DELAY_MS)); timer.unref?.();
|
|
4709
4831
|
this.providerAccelerationCooldowns.set(agent.name, { startedAt: existing?.startedAt ?? now,
|
|
@@ -4752,7 +4874,7 @@ export class AgentFramework {
|
|
|
4752
4874
|
if (!isMembraneError(error) || typeof error.retryAfterMs !== 'number') return undefined;
|
|
4753
4875
|
if (this.conversationAgentHomes.has(agent.name)) {
|
|
4754
4876
|
const wait = this.recordProviderWait(agent.name, agent.model, error);
|
|
4755
|
-
return error.retryable === true && wait !== undefined && this.holdForProviderWait(agent, wait, trigger ? [trigger] : [], true)
|
|
4877
|
+
return error.retryable === true && wait !== undefined && this.holdForProviderWait(agent, wait, trigger ? [asTurnRetry(trigger)] : [], true)
|
|
4756
4878
|
? 'held'
|
|
4757
4879
|
: undefined;
|
|
4758
4880
|
}
|
|
@@ -4902,17 +5024,30 @@ export class AgentFramework {
|
|
|
4902
5024
|
* Hold this agent's primary turns for a provider wait on its model. The
|
|
4903
5025
|
* requests are kept and released together, into one fresh compile, when
|
|
4904
5026
|
* the wait ends: the same path an acceleration cooldown takes, without
|
|
4905
|
-
* parking auxiliary calls to other models.
|
|
5027
|
+
* parking auxiliary calls to other models. `report`, once the wait is
|
|
5028
|
+
* found to bind, runs before anything is held: a caller that reports its
|
|
5029
|
+
* turn's end there (inference:failed) does so before the hold takes the
|
|
5030
|
+
* turn's retry, as every failure site does.
|
|
4906
5031
|
*/
|
|
4907
|
-
private holdForProviderWait(agent: Agent, wait: ProviderWait, requests: InferenceRequest[], failed = false): boolean {
|
|
5032
|
+
private holdForProviderWait(agent: Agent, wait: ProviderWait, requests: InferenceRequest[], failed = false, report?: () => void): boolean {
|
|
4908
5033
|
const now = Date.now();
|
|
4909
5034
|
const until = wait.until ?? Number.POSITIVE_INFINITY;
|
|
4910
5035
|
if (until <= now) return false;
|
|
5036
|
+
report?.();
|
|
5037
|
+
// stop() has cleared every hold and dropped every turn on its way: a
|
|
5038
|
+
// failure or start reaching here after it holds nothing, so no retry
|
|
5039
|
+
// outlives the stop as a turn still alive. Nothing is started either.
|
|
5040
|
+
if (this.providerAdmissionClosed) return true;
|
|
5041
|
+
// A listener of the report may have released the wait
|
|
5042
|
+
// (releaseProviderWait) before anything was held: nothing binds now, so
|
|
5043
|
+
// the requests start, as the release of an installed hold starts them.
|
|
5044
|
+
if (report && !this.providerWaits?.active(agent.name, wait.model)) {
|
|
5045
|
+
this.pendingRequests.push(...requests);
|
|
5046
|
+
return true;
|
|
5047
|
+
}
|
|
4911
5048
|
const existing = this.providerAccelerationCooldowns.get(agent.name);
|
|
4912
5049
|
const held = existing?.heldRequests ?? [];
|
|
4913
|
-
for (const request of requests)
|
|
4914
|
-
if (!held.some((r) => this.sameInferenceRequest(r, request))) held.push(request);
|
|
4915
|
-
}
|
|
5050
|
+
for (const request of requests) this.holdRequest(held, request);
|
|
4916
5051
|
// An acceleration or host hold already parks the whole agent; its release
|
|
4917
5052
|
// re-checks this wait at admission.
|
|
4918
5053
|
if (existing && existing.waitModel === undefined) return true;
|
|
@@ -5517,9 +5652,11 @@ export class AgentFramework {
|
|
|
5517
5652
|
|
|
5518
5653
|
/**
|
|
5519
5654
|
* Abort an in-flight inference for an agent. True when there was one to
|
|
5520
|
-
* stop: a stream
|
|
5655
|
+
* stop: a stream; a turn with no stream and one of its checks ahead (its
|
|
5521
5656
|
* hooks or compile, a wait to retry, the tool-result guard's wait), which
|
|
5522
|
-
* ends at that check, before it sends anything more
|
|
5657
|
+
* ends at that check, before it sends anything more; or a turn whose
|
|
5658
|
+
* continuation or retry is on its way (queued, kept by a provider hold,
|
|
5659
|
+
* parked at admission or in a scheduler pass), which ends here.
|
|
5523
5660
|
*/
|
|
5524
5661
|
abortInference(agentName: string, reason?: string): boolean {
|
|
5525
5662
|
const agent = this.agents.get(agentName);
|
|
@@ -5549,6 +5686,20 @@ export class AgentFramework {
|
|
|
5549
5686
|
checkWindow.wake?.();
|
|
5550
5687
|
return true;
|
|
5551
5688
|
}
|
|
5689
|
+
// A turn whose continuation or retry is on its way (a budget restart or
|
|
5690
|
+
// a guard retry, or a failed turn's retry a provider hold keeps:
|
|
5691
|
+
// pendingContinuationAgents) ends here, as a stop of a turn's start ends
|
|
5692
|
+
// one, whether or not the frame that handed it on has let go of its
|
|
5693
|
+
// token yet (that frame ends on its own and settles nothing more): the
|
|
5694
|
+
// continuation or retry is dropped, with one inference:aborted carrying
|
|
5695
|
+
// the stop's reason. Wakes that arrived meanwhile aren't the turn's, and
|
|
5696
|
+
// still run; a hold of such wakes alone keeps no turn, and has nothing to
|
|
5697
|
+
// stop. Every failure site reports inference:failed before a hold takes
|
|
5698
|
+
// the retry, so a listener of it finds nothing held, and reports false.
|
|
5699
|
+
if (!result && this.pendingContinuationAgents().has(agentName)) {
|
|
5700
|
+
this.dropContinuations(agentName, { kind: 'caller', reason, why: 'a stop ended the turn' });
|
|
5701
|
+
return true;
|
|
5702
|
+
}
|
|
5552
5703
|
return !!result;
|
|
5553
5704
|
}
|
|
5554
5705
|
|
|
@@ -6507,9 +6658,10 @@ export class AgentFramework {
|
|
|
6507
6658
|
* it queues, or a tool-result guard's retry, which its frame starts at
|
|
6508
6659
|
* once) holds no turn token until its start takes one. Once its turn's
|
|
6509
6660
|
* frame has let go of its token, its start can wait in the queue, a
|
|
6510
|
-
* provider hold
|
|
6511
|
-
*
|
|
6512
|
-
*
|
|
6661
|
+
* provider hold, a park on admission or a scheduler pass's hands. Its turn
|
|
6662
|
+
* is alive meanwhile. So is a turn whose call failed into a provider hold,
|
|
6663
|
+
* which keeps the turn's retry (TURN_RETRY), until that retry starts,
|
|
6664
|
+
* wherever it waits. These are the agents with one on its way. A
|
|
6513
6665
|
* `tool_results_ready` continuation follows a tool round left waiting with
|
|
6514
6666
|
* no stream, which only the direct Agent API makes (Agent's
|
|
6515
6667
|
* runInferenceWithInjections; the framework's own rounds wait with their
|
|
@@ -6518,7 +6670,7 @@ export class AgentFramework {
|
|
|
6518
6670
|
*/
|
|
6519
6671
|
private pendingContinuationAgents(): Set<string> {
|
|
6520
6672
|
const agents = new Set<string>();
|
|
6521
|
-
const add = (r: InferenceRequest): void => { if (
|
|
6673
|
+
const add = (r: InferenceRequest): void => { if (continuesTurn(r)) agents.add(r.agentName); };
|
|
6522
6674
|
// Read defensively: a framework built from the prototype, as the
|
|
6523
6675
|
// operator tests build one, sets none of these.
|
|
6524
6676
|
for (const r of this.pendingRequests ?? []) add(r);
|
|
@@ -6529,7 +6681,7 @@ export class AgentFramework {
|
|
|
6529
6681
|
}
|
|
6530
6682
|
|
|
6531
6683
|
/** The agents with a turn alive: holding a turn token, or with a
|
|
6532
|
-
* continuation of their turn on its way. */
|
|
6684
|
+
* continuation of their turn, or its retry, on its way. */
|
|
6533
6685
|
private turnHolders(): Set<string> {
|
|
6534
6686
|
const holders = this.pendingContinuationAgents();
|
|
6535
6687
|
for (const name of this.activeTurnTokens.keys()) holders.add(name);
|
|
@@ -6540,6 +6692,26 @@ export class AgentFramework {
|
|
|
6540
6692
|
return this.activeTurnTokens.has(agentName) || this.pendingContinuationAgents().has(agentName);
|
|
6541
6693
|
}
|
|
6542
6694
|
|
|
6695
|
+
/** The provider hold keeping `agentName`'s turn for its retry (TURN_RETRY),
|
|
6696
|
+
* in words naming its end and its reason, and whether a stated wait holds
|
|
6697
|
+
* it; undefined when no hold keeps one. */
|
|
6698
|
+
private heldTurnRetry(agentName: string): { says: string; statedWait: boolean } | undefined {
|
|
6699
|
+
// Optional, as pendingContinuationAgents reads it: a framework built from
|
|
6700
|
+
// its prototype, as some suites build one, has no map of holds.
|
|
6701
|
+
const hold = this.providerAccelerationCooldowns?.get(agentName);
|
|
6702
|
+
if (!hold?.heldRequests.some(isTurnRetry)) return undefined;
|
|
6703
|
+
// A deadline past what a Date can name (an infinite wait, or a stated
|
|
6704
|
+
// one too long for an instant) is one only a release ends.
|
|
6705
|
+
const end = new Date(hold.until);
|
|
6706
|
+
const until = Number.isNaN(end.getTime()) ? 'until released' : `until ${end.toISOString()}`;
|
|
6707
|
+
return {
|
|
6708
|
+
says: `a provider hold keeps its turn to start again, ${until}: ${hold.reason}`,
|
|
6709
|
+
// release-provider-wait lifts a stated wait's hold, not a host's or an
|
|
6710
|
+
// acceleration cooldown's own pacing.
|
|
6711
|
+
statedWait: hold.waitModel !== undefined || hold.statedWait !== undefined,
|
|
6712
|
+
};
|
|
6713
|
+
}
|
|
6714
|
+
|
|
6543
6715
|
getHostModeStatus(): HostModeStatus {
|
|
6544
6716
|
const parkedAdmissions = this.parkedAdmissionCount();
|
|
6545
6717
|
const holders = this.turnHolders();
|
|
@@ -6551,7 +6723,7 @@ export class AgentFramework {
|
|
|
6551
6723
|
activeTurns: holders.size,
|
|
6552
6724
|
activeTurnAgents: [...holders].sort(),
|
|
6553
6725
|
parkedAdmissions,
|
|
6554
|
-
gatedRequests: this.pendingRequests.filter((r) => !
|
|
6726
|
+
gatedRequests: this.pendingRequests.filter((r) => !passesQuiesce(r)).length,
|
|
6555
6727
|
backgroundScripts: this.codeExecution?.scripts.leftRunning() ?? 0,
|
|
6556
6728
|
deferredWrites: this.deferredMessages.length,
|
|
6557
6729
|
...(this.lastUnabandonable.length > 0 ? { unabandonable: [...this.lastUnabandonable] } : {}),
|
|
@@ -6569,8 +6741,9 @@ export class AgentFramework {
|
|
|
6569
6741
|
* Enter quiesce mode: write the deferred-write queue, persist the flag,
|
|
6570
6742
|
* suppress gate deliveries, pause all MCPL data planes, and wait for
|
|
6571
6743
|
* in-flight turns to settle (turn-alive is a turn token, which spans
|
|
6572
|
-
* dequeue → settled teardown,
|
|
6573
|
-
*
|
|
6744
|
+
* dequeue → settled teardown, a continuation of the turn on its way to its
|
|
6745
|
+
* start, or the turn's retry, which a provider hold keeps or which waits
|
|
6746
|
+
* to start: turnHolders). Idempotent, but for the queue's write, which
|
|
6574
6747
|
* every call makes first: when the queue can't reach disk, because its
|
|
6575
6748
|
* write fails or it's over the cap, the call rejects with a
|
|
6576
6749
|
* QuiesceBlockedError and the host's mode as it was, unless it has
|
|
@@ -6580,8 +6753,8 @@ export class AgentFramework {
|
|
|
6580
6753
|
* On drain timeout the host STAYS quiesced (`drained: false` in the
|
|
6581
6754
|
* result); with `abandon: true` the undrained streams are cancelled via a
|
|
6582
6755
|
* dedicated cancel kind that settles the turn without feeding the
|
|
6583
|
-
* inference-failure accounting, and a turn whose continuation
|
|
6584
|
-
* started ends with
|
|
6756
|
+
* inference-failure accounting, and a turn whose continuation or retry
|
|
6757
|
+
* hasn't started ends with it dropped. Drain covers the framework's
|
|
6585
6758
|
* own turns: a caller of the direct Agent API (`Agent.runInference`) owns
|
|
6586
6759
|
* its turns' timing.
|
|
6587
6760
|
*/
|
|
@@ -6730,7 +6903,7 @@ export class AgentFramework {
|
|
|
6730
6903
|
// to the continuation.
|
|
6731
6904
|
const handedOff = new Set<string>();
|
|
6732
6905
|
for (const agentName of this.pendingContinuationAgents()) {
|
|
6733
|
-
console.error(`[host-mode] abandoning ${agentName}'s turn before its continuation starts:
|
|
6906
|
+
console.error(`[host-mode] abandoning ${agentName}'s turn before its continuation or held retry starts: it is dropped`);
|
|
6734
6907
|
this.dropContinuations(agentName, { kind: 'quiesce_abandoned', why: 'its turn was abandoned' });
|
|
6735
6908
|
handedOff.add(agentName);
|
|
6736
6909
|
}
|
|
@@ -10363,7 +10536,8 @@ export class AgentFramework {
|
|
|
10363
10536
|
* by stop() while it runs.
|
|
10364
10537
|
*
|
|
10365
10538
|
* The branch moves under every agent sharing the store, so the switch
|
|
10366
|
-
* reserves the store as a surgery does: every agent must be idle
|
|
10539
|
+
* reserves the store as a surgery does: every agent must be idle with no
|
|
10540
|
+
* turn alive (no continuation or held retry of one on its way), or it is
|
|
10367
10541
|
* refused (OperatorActionError 'agent-busy'), and no turn starts until the
|
|
10368
10542
|
* switch is done.
|
|
10369
10543
|
*/
|
|
@@ -10615,9 +10789,21 @@ export class AgentFramework {
|
|
|
10615
10789
|
);
|
|
10616
10790
|
}
|
|
10617
10791
|
const busy: string[] = [];
|
|
10792
|
+
let heldTurns = false;
|
|
10793
|
+
let statedWaits = false;
|
|
10618
10794
|
for (const [name, a] of this.agents) {
|
|
10619
|
-
if (a.state.status !== 'idle') busy.push(`${name} is ${a.state.status}`);
|
|
10620
|
-
|
|
10795
|
+
if (a.state.status !== 'idle') { busy.push(`${name} is ${a.state.status}`); continue; }
|
|
10796
|
+
if (!this.hasTurn(name)) continue;
|
|
10797
|
+
// A provider hold keeping the turn's retry is named: a quiesce alone
|
|
10798
|
+
// doesn't end that turn.
|
|
10799
|
+
const held = this.heldTurnRetry(name);
|
|
10800
|
+
if (held) {
|
|
10801
|
+
heldTurns = true;
|
|
10802
|
+
if (held.statedWait) statedWaits = true;
|
|
10803
|
+
busy.push(`${name} is idle+turn-alive (${held.says})`);
|
|
10804
|
+
} else {
|
|
10805
|
+
busy.push(`${name} is idle+turn-alive`);
|
|
10806
|
+
}
|
|
10621
10807
|
}
|
|
10622
10808
|
for (const name of this.activeTurnTokens.keys()) {
|
|
10623
10809
|
if (!this.agents.has(name)) busy.push(`${name} is turn-alive`);
|
|
@@ -10625,7 +10811,8 @@ export class AgentFramework {
|
|
|
10625
10811
|
if (busy.length > 0) {
|
|
10626
10812
|
throw new OperatorActionError(
|
|
10627
10813
|
'agent-busy',
|
|
10628
|
-
`Cannot ${verb} while ${busy.join(', ')} — every agent sharing the store must be idle (quiesce the host first)
|
|
10814
|
+
`Cannot ${verb} while ${busy.join(', ')} — every agent sharing the store must be idle (quiesce the host first)` +
|
|
10815
|
+
(heldTurns ? `; ${heldTurnWayOn(statedWaits)}` : ''),
|
|
10629
10816
|
);
|
|
10630
10817
|
}
|
|
10631
10818
|
const reserved = new Map<string, number>();
|
|
@@ -11559,10 +11746,11 @@ export class AgentFramework {
|
|
|
11559
11746
|
// Direct inference requests (e.g. runEphemeralToCompletion) bypass the
|
|
11560
11747
|
// event queue — without this the loop can exit before they're drained.
|
|
11561
11748
|
// While quiesced, gate-parked requests are NOT progress and must not
|
|
11562
|
-
// block idle
|
|
11563
|
-
//
|
|
11749
|
+
// block idle; what a quiesce lets through (passesQuiesce: a turn's
|
|
11750
|
+
// continuation, or a held turn's retry) can still run, and is.
|
|
11751
|
+
// Identity-equivalent to `.length > 0` when not quiesced.
|
|
11564
11752
|
this.pendingRequests.some(
|
|
11565
|
-
(r) => !this.quiesced ||
|
|
11753
|
+
(r) => !this.quiesced || passesQuiesce(r),
|
|
11566
11754
|
) ||
|
|
11567
11755
|
this.activeStreams.size > 0 ||
|
|
11568
11756
|
Array.from(this.agents.values()).some((a) => a.state.status !== 'idle')
|
|
@@ -13248,10 +13436,13 @@ export class AgentFramework {
|
|
|
13248
13436
|
// (request dropped as stale, inference policy veto) would otherwise stay
|
|
13249
13437
|
// registered forever. No turn, no active stream and no pending request =
|
|
13250
13438
|
// done. A turn holds its token from dequeue, through its hooks and
|
|
13251
|
-
// compile, before its stream registers, so a fork mid-compile is busy
|
|
13439
|
+
// compile, before its stream registers, so a fork mid-compile is busy;
|
|
13440
|
+
// and a turn is alive while its continuation or a provider hold's retry
|
|
13441
|
+
// of it is on its way (hasTurn), so a closure turn held for its retry
|
|
13442
|
+
// finishes when the hold ends.
|
|
13252
13443
|
for (const agentName of [...this.closingConversationAgents]) {
|
|
13253
13444
|
if (
|
|
13254
|
-
!this.
|
|
13445
|
+
!this.hasTurn(agentName) &&
|
|
13255
13446
|
!this.activeStreams.has(agentName) &&
|
|
13256
13447
|
!this.pendingRequests.some((r) => r.agentName === agentName)
|
|
13257
13448
|
) {
|
|
@@ -14071,6 +14262,19 @@ export class AgentFramework {
|
|
|
14071
14262
|
for (const cooldown of this.providerAccelerationCooldowns.values()) {
|
|
14072
14263
|
cooldown.heldRequests = cooldown.heldRequests.filter((r) => r.coalescingSubject !== subject);
|
|
14073
14264
|
}
|
|
14265
|
+
// A wake of the subject's merged into a start that carries a turn on
|
|
14266
|
+
// (MERGED_BATCH) is withdrawn from the start's batch, so no stop puts
|
|
14267
|
+
// it back and no pass unpacks it.
|
|
14268
|
+
const unrecord = (r: InferenceRequest): void => {
|
|
14269
|
+
const batch = mergedBatch(r);
|
|
14270
|
+
if (batch?.some((w) => w.coalescingSubject === subject)) {
|
|
14271
|
+
(r as MarkedRequest)[MERGED_BATCH] = batch.filter((w) => w.coalescingSubject !== subject);
|
|
14272
|
+
}
|
|
14273
|
+
};
|
|
14274
|
+
for (const r of this.pendingRequests) unrecord(r);
|
|
14275
|
+
for (const cooldown of this.providerAccelerationCooldowns.values()) cooldown.heldRequests.forEach(unrecord);
|
|
14276
|
+
for (const r of this.parkedContinuations ?? []) unrecord(r);
|
|
14277
|
+
for (const pass of this.passRequests ?? []) for (const requests of pass.values()) requests.forEach(unrecord);
|
|
14074
14278
|
}
|
|
14075
14279
|
|
|
14076
14280
|
/**
|
|
@@ -14451,7 +14655,7 @@ export class AgentFramework {
|
|
|
14451
14655
|
});
|
|
14452
14656
|
console.error(`[inference-dropped] agent=${agentName} reason=agent_not_found requests=${requests.length}`);
|
|
14453
14657
|
// A continuation among them leaves its turn to end here.
|
|
14454
|
-
if (requests.some(
|
|
14658
|
+
if (requests.some(continuesTurn)) {
|
|
14455
14659
|
this.endContinuedTurn(agentName, { kind: 'dropped', why: 'its agent is gone' });
|
|
14456
14660
|
}
|
|
14457
14661
|
continue;
|
|
@@ -14463,11 +14667,7 @@ export class AgentFramework {
|
|
|
14463
14667
|
// selected now, the turn proceeds (the wait stays recorded).
|
|
14464
14668
|
const otherModel = providerCooldown.waitModel !== undefined && providerCooldown.waitModel !== agent.model;
|
|
14465
14669
|
if (!otherModel && (now < providerCooldown.until || this.extendHostProviderHold(agentName, providerCooldown))) {
|
|
14466
|
-
for (const req of requests)
|
|
14467
|
-
if (!providerCooldown.heldRequests.some((r) => this.sameInferenceRequest(r, req))) {
|
|
14468
|
-
providerCooldown.heldRequests.push(req);
|
|
14469
|
-
}
|
|
14470
|
-
}
|
|
14670
|
+
for (const req of requests) this.holdRequest(providerCooldown.heldRequests, req);
|
|
14471
14671
|
const recovery = this.providerAccelerationRecoveries.get(agentName);
|
|
14472
14672
|
if (recovery) recovery.heldRequests = providerCooldown.heldRequests.length;
|
|
14473
14673
|
// The timer owns release. Do not hot-poll these requests.
|
|
@@ -14481,6 +14681,17 @@ export class AgentFramework {
|
|
|
14481
14681
|
if (recovery) recovery.releasedAt = now;
|
|
14482
14682
|
requests = [...providerCooldown.heldRequests, ...requests];
|
|
14483
14683
|
}
|
|
14684
|
+
// A start that carries a turn on and came back before it took its
|
|
14685
|
+
// token stands for the batch it was merged from (MERGED_BATCH),
|
|
14686
|
+
// including one a hold released just now. The batch takes its place,
|
|
14687
|
+
// each request as it was, judged one by one from here: a wait holds
|
|
14688
|
+
// them apart, a quiesce parks the wakes while the turn's request
|
|
14689
|
+
// passes, and the read check and the policy decide on them all; the
|
|
14690
|
+
// merge below records the batch anew.
|
|
14691
|
+
if (requests.some((r) => mergedBatch(r) !== undefined)) {
|
|
14692
|
+
requests = requests.flatMap((r) => mergedBatch(r) ?? [r]);
|
|
14693
|
+
}
|
|
14694
|
+
|
|
14484
14695
|
// A provider's stated wait on this agent's primary model, including one
|
|
14485
14696
|
// recorded before a restart, holds the turn until it passes. An
|
|
14486
14697
|
// ephemeral run records nothing, its name being single-use, so no wait,
|
|
@@ -14515,9 +14726,10 @@ export class AgentFramework {
|
|
|
14515
14726
|
// bounds a multi-day maintenance window's accumulation.
|
|
14516
14727
|
if (this.quiesced) {
|
|
14517
14728
|
// Continuations of a held turn (budget restart, non-streaming tool
|
|
14518
|
-
// results) pass
|
|
14519
|
-
|
|
14520
|
-
const
|
|
14729
|
+
// results) pass, and so does a held turn's retry (passesQuiesce), so
|
|
14730
|
+
// a turn alive at the quiesce can finish; everything else parks.
|
|
14731
|
+
const passThrough = requests.filter((r) => passesQuiesce(r));
|
|
14732
|
+
const parked = requests.filter((r) => !passesQuiesce(r));
|
|
14521
14733
|
// Coalesce per (reason, addressed): every channel wake shares one
|
|
14522
14734
|
// reason, so keying on reason alone would let an ambient message
|
|
14523
14735
|
// parked later evict a DM/mention parked earlier — and the
|
|
@@ -14627,7 +14839,7 @@ export class AgentFramework {
|
|
|
14627
14839
|
// A continuation inherits its predecessor's EventGate liveness and
|
|
14628
14840
|
// logical turn. If policy drops it, no driveStream remains to end them,
|
|
14629
14841
|
// so its turn ends here (endContinuedTurn).
|
|
14630
|
-
if (requests.some(
|
|
14842
|
+
if (requests.some(continuesTurn)) {
|
|
14631
14843
|
this.endContinuedTurn(agentName, { kind: 'dropped', why: 'the inference policy dropped its continuation' });
|
|
14632
14844
|
}
|
|
14633
14845
|
this.endPendingPrimary(agentName);
|
|
@@ -14705,7 +14917,7 @@ export class AgentFramework {
|
|
|
14705
14917
|
// exists only once assembly renders it. Recorded for the whole batch
|
|
14706
14918
|
// (not inherited from requests[0]), so a requeued trigger stays honest.
|
|
14707
14919
|
const batchOnly = !budgetRestart && requests.every((r) => r.coalescingBatch === true && !!r.coalescingSubject);
|
|
14708
|
-
|
|
14920
|
+
const merged: InferenceRequest = {
|
|
14709
14921
|
...trigger,
|
|
14710
14922
|
coalescingBatch: batchOnly || undefined,
|
|
14711
14923
|
coalescingBatchSubjects: batchOnly
|
|
@@ -14733,7 +14945,17 @@ export class AgentFramework {
|
|
|
14733
14945
|
counterparty: budgetRestart ? undefined : provReq?.counterparty,
|
|
14734
14946
|
wakeChannelId: budgetRestart ? undefined : provReq?.wakeChannelId,
|
|
14735
14947
|
wakeAt: budgetRestart ? undefined : provReq?.wakeAt,
|
|
14736
|
-
}
|
|
14948
|
+
};
|
|
14949
|
+
// A held turn's retry merged with other wakes leaves the start its
|
|
14950
|
+
// turn's retry (TURN_RETRY), whichever request leads the merge: held
|
|
14951
|
+
// again before it takes a token, it keeps the turn alive as before.
|
|
14952
|
+
const start: MarkedRequest = requests.some(isTurnRetry) ? asTurnRetry(merged) : merged;
|
|
14953
|
+
// A start that carries a turn on and took in wakes that don't records
|
|
14954
|
+
// the batch it was merged from (MERGED_BATCH): until it takes its
|
|
14955
|
+
// token it stands for them all.
|
|
14956
|
+
if (continuesTurn(start) && requests.some((r) => !continuesTurn(r))) start[MERGED_BATCH] = [...requests];
|
|
14957
|
+
else delete start[MERGED_BATCH];
|
|
14958
|
+
await this.startAgentStream(agent, start);
|
|
14737
14959
|
}
|
|
14738
14960
|
}
|
|
14739
14961
|
|
|
@@ -17084,7 +17306,7 @@ export class AgentFramework {
|
|
|
17084
17306
|
// Do not await inside processInferenceRequests: one slow auxiliary call for
|
|
17085
17307
|
// this resident must not block events or other residents. Admission stays
|
|
17086
17308
|
// owned and the continuation receives it after the in-flight call settles.
|
|
17087
|
-
const parkedContinuation = trigger !== undefined &&
|
|
17309
|
+
const parkedContinuation = trigger !== undefined && continuesTurn(trigger);
|
|
17088
17310
|
if (parkedContinuation) this.parkedContinuations.add(trigger);
|
|
17089
17311
|
void this.waitForAuxiliaryIdle(agent.name).then(async () => {
|
|
17090
17312
|
// A continuation dropped while it was parked left the set then, with
|
|
@@ -17116,8 +17338,9 @@ export class AgentFramework {
|
|
|
17116
17338
|
// park; starting here would bypass it and run a turn against a
|
|
17117
17339
|
// context the operator believes frozen. Give admission back and
|
|
17118
17340
|
// requeue — the scheduler re-parks it (coalesced) until resume.
|
|
17119
|
-
// Continuations of a held turn
|
|
17120
|
-
|
|
17341
|
+
// Continuations of a held turn, and a held turn's retry, pass, as
|
|
17342
|
+
// they do at the gate (passesQuiesce).
|
|
17343
|
+
if (this.quiesced && !passesQuiesce(trigger)) {
|
|
17121
17344
|
this.releasePrimaryProviderGate(agent.name);
|
|
17122
17345
|
this.pendingRequests.push(trigger ?? {
|
|
17123
17346
|
agentName: agent.name, reason: 'provider-admission:requeue', source: 'scheduler', timestamp: Date.now(),
|
|
@@ -17125,6 +17348,16 @@ export class AgentFramework {
|
|
|
17125
17348
|
console.error(`[provider-admission] ${agent.name}: host quiesced while parked — wake requeued, not started`);
|
|
17126
17349
|
return;
|
|
17127
17350
|
}
|
|
17351
|
+
// A start that stands for a merged batch (MERGED_BATCH) goes back
|
|
17352
|
+
// to the scheduler as that batch: its gate lets the turn's request
|
|
17353
|
+
// through alone and parks the wakes until resume.
|
|
17354
|
+
const batch = trigger && mergedBatch(trigger);
|
|
17355
|
+
if (this.quiesced && batch) {
|
|
17356
|
+
this.releasePrimaryProviderGate(agent.name);
|
|
17357
|
+
this.pendingRequests.push(...batch);
|
|
17358
|
+
console.error(`[provider-admission] ${agent.name}: host quiesced while parked — the merged start goes back as its ${batch.length} requests`);
|
|
17359
|
+
return;
|
|
17360
|
+
}
|
|
17128
17361
|
await this.startAgentStream(agent, trigger, attempt, true);
|
|
17129
17362
|
}).catch((error) => {
|
|
17130
17363
|
this.releasePrimaryProviderGate(agent.name);
|
|
@@ -17258,11 +17491,23 @@ export class AgentFramework {
|
|
|
17258
17491
|
* caller has ended the turn already.
|
|
17259
17492
|
*/
|
|
17260
17493
|
private dropContinuations(agentName: string, drop: ContinuationDrop | null): void {
|
|
17261
|
-
const isContinuation = (r: InferenceRequest): boolean => r.agentName === agentName &&
|
|
17494
|
+
const isContinuation = (r: InferenceRequest): boolean => r.agentName === agentName && continuesTurn(r);
|
|
17495
|
+
// A caller's stop and an abandon end the turn, not the wakes merged into
|
|
17496
|
+
// a start of it that hasn't taken its token (MERGED_BATCH): they go back
|
|
17497
|
+
// in the queue. A stop() or a disposal ends the agent's work, and puts
|
|
17498
|
+
// nothing back.
|
|
17499
|
+
const putsBack = drop?.kind === 'caller' || drop?.kind === 'quiesce_abandoned';
|
|
17500
|
+
const back: InferenceRequest[] = [];
|
|
17262
17501
|
let dropped = false;
|
|
17263
17502
|
const take = (requests: InferenceRequest[]): InferenceRequest[] => {
|
|
17264
|
-
const left =
|
|
17265
|
-
|
|
17503
|
+
const left: InferenceRequest[] = [];
|
|
17504
|
+
for (const r of requests) {
|
|
17505
|
+
if (!isContinuation(r)) left.push(r);
|
|
17506
|
+
else {
|
|
17507
|
+
dropped = true;
|
|
17508
|
+
if (putsBack) back.push(...mergedWakes(r));
|
|
17509
|
+
}
|
|
17510
|
+
}
|
|
17266
17511
|
return left;
|
|
17267
17512
|
};
|
|
17268
17513
|
this.pendingRequests = take(this.pendingRequests);
|
|
@@ -17277,6 +17522,7 @@ export class AgentFramework {
|
|
|
17277
17522
|
this.parkedContinuations.delete(parked);
|
|
17278
17523
|
this.releasePrimaryProviderGate(agentName);
|
|
17279
17524
|
dropped = true;
|
|
17525
|
+
if (putsBack) back.push(...mergedWakes(parked));
|
|
17280
17526
|
}
|
|
17281
17527
|
for (const pass of this.passRequests) {
|
|
17282
17528
|
const requests = pass.get(agentName);
|
|
@@ -17285,6 +17531,7 @@ export class AgentFramework {
|
|
|
17285
17531
|
requests.length = 0;
|
|
17286
17532
|
requests.push(...left);
|
|
17287
17533
|
}
|
|
17534
|
+
this.pendingRequests.push(...back);
|
|
17288
17535
|
if (!dropped || !drop) return;
|
|
17289
17536
|
// A primary a provider hold left pending for what it released won't run
|
|
17290
17537
|
// now, when nothing else of the agent's waits (endPendingPrimary).
|
|
@@ -17311,10 +17558,12 @@ export class AgentFramework {
|
|
|
17311
17558
|
const startedAt = this.lastInferenceAt.get(agentName)?.startedAt ?? Date.now();
|
|
17312
17559
|
if (agent && drop.kind !== 'dropped') {
|
|
17313
17560
|
const routing = this.openLogical.get(agentName) ?? this.routingOf(agentName);
|
|
17314
|
-
this.endStoppedStart(agent, { kind: drop.
|
|
17561
|
+
this.endStoppedStart(agent, drop.kind === 'caller' ? { kind: 'caller', reason: drop.reason } : { kind: drop.kind },
|
|
17562
|
+
routing, startedAt, undefined, false);
|
|
17315
17563
|
} else {
|
|
17316
17564
|
const owner = this.activeTurnTokens.get(agentName);
|
|
17317
|
-
this.abortDroppedTurn(agentName, owner ?? -1,
|
|
17565
|
+
this.abortDroppedTurn(agentName, owner ?? -1,
|
|
17566
|
+
drop.kind === 'dropped' ? CONTINUATION_DROPPED : drop.kind === 'caller' ? (drop.reason ?? 'user') : drop.kind, startedAt);
|
|
17318
17567
|
this.channelRegistry?.stopTyping();
|
|
17319
17568
|
if (agent) {
|
|
17320
17569
|
agent.toolResultGuard.recovering = false;
|
|
@@ -17336,6 +17585,34 @@ export class AgentFramework {
|
|
|
17336
17585
|
}
|
|
17337
17586
|
}
|
|
17338
17587
|
|
|
17588
|
+
/**
|
|
17589
|
+
* After a failure a provider hold would take: once stop() has closed
|
|
17590
|
+
* admission, the hold installs nothing (holdForProviderWait,
|
|
17591
|
+
* holdProviderAcceleration), so no retry follows the turn's
|
|
17592
|
+
* inference:failed, and the turn ends as a retry a stop cancelled ends, with
|
|
17593
|
+
* an inference:aborted saying so. Nothing is written. True when it ended
|
|
17594
|
+
* the turn.
|
|
17595
|
+
*/
|
|
17596
|
+
private endFailureAtStop(agentName: string, turnToken: number, startedAt: number): boolean {
|
|
17597
|
+
if (!this.providerAdmissionClosed) return false;
|
|
17598
|
+
this.abortDroppedTurn(agentName, turnToken, RETRY_CANCELLED_BY_STOP, startedAt);
|
|
17599
|
+
return true;
|
|
17600
|
+
}
|
|
17601
|
+
|
|
17602
|
+
/**
|
|
17603
|
+
* A turn's start whose failure a provider hold took (or, once stop() has
|
|
17604
|
+
* closed admission, would have: endFailureAtStop) ends here. Its window
|
|
17605
|
+
* closes now, not at the start's finally a few microtasks on: no check of
|
|
17606
|
+
* this start is ahead, and a stop made after the failure's report must
|
|
17607
|
+
* find the held retry (abortInference's drop), not a window nothing will
|
|
17608
|
+
* collect.
|
|
17609
|
+
*/
|
|
17610
|
+
private endHeldStart(agent: Agent, checkWindow: CheckWindow, turnToken: number, startedAt: number): void {
|
|
17611
|
+
if (this.checkAhead.get(agent) === checkWindow) this.checkAhead.delete(agent);
|
|
17612
|
+
this.endFailureAtStop(agent.name, turnToken, startedAt);
|
|
17613
|
+
this.eventGate?.onInferenceEnded(agent.name);
|
|
17614
|
+
}
|
|
17615
|
+
|
|
17339
17616
|
/**
|
|
17340
17617
|
* End a turn that beginAgentTurn drops before it sends anything. The turn
|
|
17341
17618
|
* emitted inference:started, and listeners that bracket a turn, such as a
|
|
@@ -17567,6 +17844,12 @@ export class AgentFramework {
|
|
|
17567
17844
|
const { coalescingSubject: _subject, coalescingBatch: _batch, coalescingBatchSubjects: _subjects, readsDeferredWrite: _read, ...retry } = trigger;
|
|
17568
17845
|
trigger = retry;
|
|
17569
17846
|
}
|
|
17847
|
+
// The wakes merged into this start are the turn's now: it reads them,
|
|
17848
|
+
// so a later drop of its retry puts none of them back (MERGED_BATCH).
|
|
17849
|
+
if (trigger && mergedBatch(trigger)) {
|
|
17850
|
+
const { [MERGED_BATCH]: _batch, ...own } = trigger as MarkedRequest;
|
|
17851
|
+
trigger = own;
|
|
17852
|
+
}
|
|
17570
17853
|
|
|
17571
17854
|
// Record turn checkpoint before inference (only on first attempt, not retries)
|
|
17572
17855
|
if (attempt === 0 && trigger?.reason !== 'tool_result_guard_retry') {
|
|
@@ -17862,9 +18145,8 @@ export class AgentFramework {
|
|
|
17862
18145
|
// hold the turn as the boundary would rather than send it into the wait.
|
|
17863
18146
|
if (this.conversationAgentHomes.has(agent.name)) {
|
|
17864
18147
|
const wait = this.providerWaits?.active(agent.name, agent.model);
|
|
17865
|
-
|
|
17866
|
-
|
|
17867
|
-
`(recorded while the turn compiled); turn held, nothing sent`);
|
|
18148
|
+
// The turn has begun (inference:started), so the hold keeps its retry.
|
|
18149
|
+
if (wait && this.holdForProviderWait(agent, wait, trigger ? [asTurnRetry(trigger)] : [], false, () => {
|
|
17868
18150
|
// The stream the compile made won't be sent: it's cancelled, and
|
|
17869
18151
|
// this start's window closes, before the hold is reported, so a stop
|
|
17870
18152
|
// made while inference:failed is delivered finds neither, and the
|
|
@@ -17875,8 +18157,15 @@ export class AgentFramework {
|
|
|
17875
18157
|
if (this.checkAhead.get(agent) === checkWindow) this.checkAhead.delete(agent);
|
|
17876
18158
|
// The turn emitted inference:started before its compile; every turn
|
|
17877
18159
|
// that does gets one terminal event, which listeners (a typing loop)
|
|
17878
|
-
// rely on
|
|
18160
|
+
// rely on. It's reported before the hold takes the turn's retry, as
|
|
18161
|
+
// at every failure site, so a listener's stop finds nothing held.
|
|
17879
18162
|
this.emitTrace({ type: 'inference:failed', agentName: agent.name, error: providerWaitText(agent.name, agent.model, wait) });
|
|
18163
|
+
})) {
|
|
18164
|
+
console.error(`[provider-wait] agent=${agent.name} model=${agent.model} binds after the compile ` +
|
|
18165
|
+
`(recorded while the turn compiled); ${this.providerAdmissionClosed ? 'the framework is stopping, so nothing is held'
|
|
18166
|
+
: this.providerAccelerationCooldowns.get(agent.name)?.heldRequests.some(isTurnRetry) ? 'turn held'
|
|
18167
|
+
: 'the wait was released as the failure was reported, so the retry is queued'}, nothing sent`);
|
|
18168
|
+
this.endFailureAtStop(agent.name, turnToken, turnStartedAt);
|
|
17880
18169
|
this.activeStreams.delete(agent.name);
|
|
17881
18170
|
this.pendingAssistantBlocks.delete(agent.name);
|
|
17882
18171
|
this.channelRegistry?.stopTyping();
|
|
@@ -17961,14 +18250,16 @@ export class AgentFramework {
|
|
|
17961
18250
|
if (ownsProviderGate && this.holdProviderAcceleration(agent, err, trigger)) {
|
|
17962
18251
|
// Capacity scheduling is not poisoned history. The cooldown timer owns
|
|
17963
18252
|
// one later fresh compile; no immediate retry or exhaustion marker.
|
|
17964
|
-
|
|
18253
|
+
// (Once stop() has closed admission, nothing is held, and the turn
|
|
18254
|
+
// ends here: endFailureAtStop.)
|
|
18255
|
+
this.endHeldStart(agent, checkWindow, turnToken, turnStartedAt);
|
|
17965
18256
|
return false;
|
|
17966
18257
|
}
|
|
17967
18258
|
const statedWait = ownsProviderGate ? undefined : this.nonOwningProviderWait(agent, err, trigger);
|
|
17968
18259
|
if (statedWait === 'held') {
|
|
17969
18260
|
// A conversation agent held by the provider's stated wait, as a
|
|
17970
18261
|
// resident is: the hold's release owns the later compile.
|
|
17971
|
-
this.
|
|
18262
|
+
this.endHeldStart(agent, checkWindow, turnToken, turnStartedAt);
|
|
17972
18263
|
return false;
|
|
17973
18264
|
}
|
|
17974
18265
|
|
|
@@ -18003,7 +18294,15 @@ export class AgentFramework {
|
|
|
18003
18294
|
}
|
|
18004
18295
|
// The retry re-enters startAgentStream, which replaces this frame's
|
|
18005
18296
|
// turn token with its own; cleanup belongs to the innermost frame.
|
|
18006
|
-
|
|
18297
|
+
// The retry is its turn's (TURN_RETRY): this frame has begun the turn,
|
|
18298
|
+
// and once the retry's start is held at the last admission boundary
|
|
18299
|
+
// (a conversation agent's own compression can record a wait during
|
|
18300
|
+
// the policy's delay), this frame lets go of its token. No check of
|
|
18301
|
+
// this start is ahead now, so its window closes first: a stop made
|
|
18302
|
+
// once the retry is held finds the held retry (abortInference's
|
|
18303
|
+
// drop), not a window nothing will collect.
|
|
18304
|
+
if (this.checkAhead.get(agent) === checkWindow) this.checkAhead.delete(agent);
|
|
18305
|
+
await this.startAgentStream(agent, trigger && asTurnRetry(trigger), attempt + 1);
|
|
18007
18306
|
} else {
|
|
18008
18307
|
// Terminal failure with no driveStream: this frame owns teardown.
|
|
18009
18308
|
// Token-matched (like the finally in driveStream) so a stale frame
|
|
@@ -19577,12 +19876,16 @@ export class AgentFramework {
|
|
|
19577
19876
|
|
|
19578
19877
|
this.abandonAgentScripts(agent.name, 'stream error', streamRouting);
|
|
19579
19878
|
|
|
19879
|
+
// A hold takes the turn's retry, or, once stop() has closed
|
|
19880
|
+
// admission, the turn ends here (endFailureAtStop).
|
|
19580
19881
|
if (ownsProviderGate && this.holdProviderAcceleration(agent, err, trigger)) {
|
|
19882
|
+
this.endFailureAtStop(agent.name, myTurnToken ?? -1, startTime);
|
|
19581
19883
|
if (this.agents.get(agent.name) === agent && agent.streamId === myStreamId) this.eventGate?.onInferenceEnded(agent.name);
|
|
19582
19884
|
break;
|
|
19583
19885
|
}
|
|
19584
19886
|
const statedWait = ownsProviderGate ? undefined : this.nonOwningProviderWait(agent, err, trigger);
|
|
19585
19887
|
if (statedWait === 'held') {
|
|
19888
|
+
this.endFailureAtStop(agent.name, myTurnToken ?? -1, startTime);
|
|
19586
19889
|
if (this.agents.get(agent.name) === agent && agent.streamId === myStreamId) this.eventGate?.onInferenceEnded(agent.name);
|
|
19587
19890
|
break;
|
|
19588
19891
|
}
|
|
@@ -19619,7 +19922,8 @@ export class AgentFramework {
|
|
|
19619
19922
|
}
|
|
19620
19923
|
return;
|
|
19621
19924
|
}
|
|
19622
|
-
|
|
19925
|
+
// Marked as its turn's retry, as at the start's failure.
|
|
19926
|
+
await this.startAgentStream(agent, trigger && asTurnRetry(trigger), attempt + 1);
|
|
19623
19927
|
} else {
|
|
19624
19928
|
// The turn ends here, and its inference:failed doesn't say so,
|
|
19625
19929
|
// since a retry usually follows one: an inference:aborted
|
|
@@ -19940,10 +20244,16 @@ export class AgentFramework {
|
|
|
19940
20244
|
const durationMs = Date.now() - startTime;
|
|
19941
20245
|
// The stream won't go on: the agent stops holding it before the
|
|
19942
20246
|
// failure is reported, as at the stream's error event, so a stop made
|
|
19943
|
-
// while inference:failed is delivered finds no stream.
|
|
20247
|
+
// while inference:failed is delivered finds no stream. It's reported
|
|
20248
|
+
// once, before a provider hold takes the turn's retry, as at the
|
|
20249
|
+
// stream's error event and at a start's failure: a listener's stop
|
|
20250
|
+
// finds nothing held, and one made after it ends the held turn.
|
|
19944
20251
|
agent.reset();
|
|
20252
|
+
this.emitTrace({ type: 'inference:failed', agentName: agent.name, error: err.message, stack: err.stack });
|
|
19945
20253
|
if (ownsProviderGate && this.holdProviderAcceleration(agent, err, trigger)) {
|
|
19946
|
-
|
|
20254
|
+
// Once stop() has closed admission, nothing is held, and the turn
|
|
20255
|
+
// ends here (endFailureAtStop).
|
|
20256
|
+
this.endFailureAtStop(agent.name, myTurnToken ?? -1, startTime);
|
|
19947
20257
|
logStream({ success: false,
|
|
19948
20258
|
error: `Provider acceleration cooldown: ${err.message}`,
|
|
19949
20259
|
request: compiledRequest ?? { note: 'streaming request rate-limited' }, durationMs });
|
|
@@ -19957,7 +20267,7 @@ export class AgentFramework {
|
|
|
19957
20267
|
// says why it ends.
|
|
19958
20268
|
const statedWait = ownsProviderGate ? undefined : this.nonOwningProviderWait(agent, err, trigger);
|
|
19959
20269
|
if (statedWait === 'held') {
|
|
19960
|
-
this.
|
|
20270
|
+
this.endFailureAtStop(agent.name, myTurnToken ?? -1, startTime);
|
|
19961
20271
|
logStream({ success: false,
|
|
19962
20272
|
error: `Provider wait: ${err.message}`,
|
|
19963
20273
|
request: compiledRequest ?? { note: 'streaming request rate-limited' }, durationMs });
|
|
@@ -19965,12 +20275,6 @@ export class AgentFramework {
|
|
|
19965
20275
|
lifecyclePhase = 'failed'; agent.reset(); if (this.agents.get(agent.name) === agent && agent.streamId === myStreamId) this.eventGate?.onInferenceEnded(agent.name);
|
|
19966
20276
|
return;
|
|
19967
20277
|
}
|
|
19968
|
-
this.emitTrace({
|
|
19969
|
-
type: 'inference:failed',
|
|
19970
|
-
agentName: agent.name,
|
|
19971
|
-
error: err.message,
|
|
19972
|
-
stack: err.stack,
|
|
19973
|
-
});
|
|
19974
20278
|
this.settleAgent(agent.name, {
|
|
19975
20279
|
stopReason: 'exhausted',
|
|
19976
20280
|
speech: '',
|
|
@@ -20182,8 +20486,11 @@ export class AgentFramework {
|
|
|
20182
20486
|
// frame that ends the turn disposes it, after the flush above, so what
|
|
20183
20487
|
// was held for the turn's end lands in the fork's store instead of
|
|
20184
20488
|
// being dropped as addressed to no agent. A frame that hands the turn
|
|
20185
|
-
// to a successor (a guard or budget restart) leaves the fork to it
|
|
20186
|
-
|
|
20489
|
+
// to a successor (a guard or budget restart) leaves the fork to it, and
|
|
20490
|
+
// so does one whose failure a provider hold keeps for its retry: the
|
|
20491
|
+
// turn isn't over (pendingContinuationAgents).
|
|
20492
|
+
if (frameReachedTerminal && ownsPhysicalStream && this.closingConversationAgents.has(agent.name)
|
|
20493
|
+
&& !this.pendingContinuationAgents().has(agent.name)) {
|
|
20187
20494
|
this.disposeConversationAgent(agent.name);
|
|
20188
20495
|
}
|
|
20189
20496
|
}
|
|
@@ -20277,9 +20584,14 @@ export class AgentFramework {
|
|
|
20277
20584
|
}
|
|
20278
20585
|
if (agent.state.status !== 'idle' || this.hasTurn(agentName)) {
|
|
20279
20586
|
const shown = agent.state.status === 'idle' ? 'idle+turn-alive' : agent.state.status;
|
|
20280
|
-
|
|
20281
|
-
|
|
20282
|
-
|
|
20587
|
+
// A turn a provider hold keeps for its retry has no stream: the refusal
|
|
20588
|
+
// names the hold, and how to go on sooner, as surgery's does.
|
|
20589
|
+
const held = agent.state.status === 'idle' ? this.heldTurnRetry(agentName) : undefined;
|
|
20590
|
+
throw new Error(held
|
|
20591
|
+
? `puppet refused: agent ${agentName} is idle+turn-alive (${held.says}; requires idle); ` +
|
|
20592
|
+
heldTurnWayOn(held.statedWait)
|
|
20593
|
+
: `puppet refused: agent ${agentName} is ${shown} (requires idle — ` +
|
|
20594
|
+
`injecting a turn under an active stream corrupts wire ordering)`,
|
|
20283
20595
|
);
|
|
20284
20596
|
}
|
|
20285
20597
|
const onSurface = this.getToolsForAgent(agentName)
|
|
@@ -21258,7 +21570,9 @@ export class AgentFramework {
|
|
|
21258
21570
|
* content where it sent one, `false`, `0` and `null` included, else null.
|
|
21259
21571
|
* A result that can't be converted to JSON (a cyclic object, a BigInt),
|
|
21260
21572
|
* which the dispatch's own check makes unlikely here, is a failure naming
|
|
21261
|
-
* the tool and saying the call itself completed.
|
|
21573
|
+
* the tool and saying the call itself completed. So is an MCPL peer's
|
|
21574
|
+
* result whose blocks were too deep for the dispatch's treatment, which
|
|
21575
|
+
* the dispatch marks (SCRIPT_RESULT_UNCONVERTIBLE).
|
|
21262
21576
|
*/
|
|
21263
21577
|
private async scriptResultAnswer(result: ToolResult, toolName: string, toServer: boolean): Promise<ToolAnswer> {
|
|
21264
21578
|
const structured = Object.prototype.hasOwnProperty.call(result, 'structured');
|
|
@@ -21300,6 +21614,9 @@ export class AgentFramework {
|
|
|
21300
21614
|
brief: error,
|
|
21301
21615
|
};
|
|
21302
21616
|
};
|
|
21617
|
+
// An MCPL peer's result whose blocks JSON couldn't hold, so that RFC-005's
|
|
21618
|
+
// treatment couldn't be given them at dispatch.
|
|
21619
|
+
if (SCRIPT_RESULT_UNCONVERTIBLE.has(result)) return failed(SCRIPT_RESULT_UNCONVERTIBLE.get(result));
|
|
21303
21620
|
let content: unknown[];
|
|
21304
21621
|
let text: string | undefined;
|
|
21305
21622
|
// A structured result's blocks, as #538 gives them: none when its content
|
|
@@ -27069,8 +27386,8 @@ export class AgentFramework {
|
|
|
27069
27386
|
}
|
|
27070
27387
|
|
|
27071
27388
|
/** An MCP tool call's trace: a result the server marked an error (or one
|
|
27072
|
-
* the reading
|
|
27073
|
-
* paths; anything else is tool:completed. */
|
|
27389
|
+
* the reading takes as one, such as malformed content) is tool:failed, as
|
|
27390
|
+
* on the other tool paths; anything else is tool:completed. */
|
|
27074
27391
|
private traceMcpToolOutcome(serverId: string, tool: string, callId: string, result: ToolResult, durationMs: number): void {
|
|
27075
27392
|
if (result.isError) {
|
|
27076
27393
|
this.emitTrace({ type: 'tool:failed', module: `mcpl:${serverId}`, tool, callId, durationMs, error: result.error ?? 'tool returned an error' });
|
|
@@ -27490,16 +27807,42 @@ export class AgentFramework {
|
|
|
27490
27807
|
// generated JSON) stands in for what the server sent. An MCPL peer's
|
|
27491
27808
|
// content blocks are the exception: what a script prints comes back to
|
|
27492
27809
|
// the model, so each gets the treatment RFC-005 gives it on every lane
|
|
27493
|
-
// the model reads, here, where the server that sent them is known
|
|
27494
|
-
//
|
|
27495
|
-
//
|
|
27810
|
+
// the model reads, here, where the server that sent them is known, and
|
|
27811
|
+
// so does what RFC-005 governs at any depth within them, by the same
|
|
27812
|
+
// peer reading, bound to that server (shownToScriptFromServer). A
|
|
27813
|
+
// module's blocks get it at
|
|
27814
|
+
// the script lane's exit (scriptResultAnswer). A standard peer's blocks
|
|
27815
|
+
// reach the script as sent.
|
|
27496
27816
|
const forScript = this.scriptToolWaiters?.has(call.id) ?? false;
|
|
27497
27817
|
const reading = async (result: { content?: unknown; isError?: boolean; structuredContent?: unknown }, mcpl: boolean): Promise<ToolResult> => {
|
|
27498
27818
|
if (!forScript || !Object.prototype.hasOwnProperty.call(result, 'structuredContent')) {
|
|
27499
27819
|
return this.mcpToolResult(result, call.id, mcpl, serverId, call.name);
|
|
27500
27820
|
}
|
|
27501
27821
|
const direct = AgentFramework.directToolResult(result, serverId, call.name);
|
|
27502
|
-
if (mcpl)
|
|
27822
|
+
if (mcpl) {
|
|
27823
|
+
if (!Array.isArray(direct.data)) return direct;
|
|
27824
|
+
// Each block by the classifier, then what's nested in each, at any
|
|
27825
|
+
// depth. The blocks came from the wire as JSON, so the round trip
|
|
27826
|
+
// changes nothing else. But a value that parsed can still fail to
|
|
27827
|
+
// go back: past some depth JSON.stringify overflows the stack where
|
|
27828
|
+
// JSON.parse didn't (on Node 24.19 an array 5,000 deep does, on Bun
|
|
27829
|
+
// 1.4.4 one 200,000 deep). That fails the script lane's treatment
|
|
27830
|
+
// alone, and the peer answered: the result keeps the peer's outcome
|
|
27831
|
+
// for the trace and the call's terminal, and the script lane's exit
|
|
27832
|
+
// answers it as a result it can't convert, not as a failed dispatch
|
|
27833
|
+
// (SCRIPT_RESULT_UNCONVERTIBLE). Its blocks are dropped, since they
|
|
27834
|
+
// are untreated and the result travels on, to the waiter and to the
|
|
27835
|
+
// listeners of its event's trace. Its structured content is kept, as
|
|
27836
|
+
// every result keeps it, since RFC-005's treatment doesn't reach it.
|
|
27837
|
+
const blocks = direct.data.map((block: unknown) => referenceBlockForScript(block, serverId));
|
|
27838
|
+
try {
|
|
27839
|
+
return { ...direct, data: JSON.parse(JSON.stringify(blocks, (_key, value: unknown) => shownToScriptFromServer(value, serverId))) as unknown[] };
|
|
27840
|
+
} catch (err) {
|
|
27841
|
+
const unconvertible: ToolResult = { ...direct, data: [] };
|
|
27842
|
+
SCRIPT_RESULT_UNCONVERTIBLE.set(unconvertible, err);
|
|
27843
|
+
return unconvertible;
|
|
27844
|
+
}
|
|
27845
|
+
}
|
|
27503
27846
|
// A standard peer's content that isn't a list of blocks is a malformed
|
|
27504
27847
|
// result on the model's reading (normalizeStandardToolResult), so the
|
|
27505
27848
|
// script reads it that way too, its structured content kept.
|
|
@@ -27724,7 +28067,8 @@ export class AgentFramework {
|
|
|
27724
28067
|
* direct path (a module's callTool, a channel's tool call) doesn't come
|
|
27725
28068
|
* here: it returns the server's result itself (directToolResult), for
|
|
27726
28069
|
* either engine, as does a script's call whose result is structured, with
|
|
27727
|
-
* an MCPL peer's blocks given RFC-005's treatment (referenceBlockForScript)
|
|
28070
|
+
* an MCPL peer's blocks given RFC-005's treatment (referenceBlockForScript),
|
|
28071
|
+
* and what's nested in them at any depth (shownToScriptFromServer).
|
|
27728
28072
|
*/
|
|
27729
28073
|
private async mcpToolResult(
|
|
27730
28074
|
result: { content?: unknown; isError?: boolean; structuredContent?: unknown },
|