@animalabs/agent-framework 0.26.0 → 0.26.1
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 +83 -9
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +364 -60
- 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 +12 -0
- package/dist/src/tool-result-history.d.ts.map +1 -1
- package/dist/src/tool-result-history.js +23 -1
- package/dist/src/tool-result-history.js.map +1 -1
- package/dist/src/types/events.d.ts +3 -1
- package/dist/src/types/events.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/framework.ts +365 -63
- 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 +22 -1
- package/src/types/events.ts +3 -1
package/dist/src/framework.js
CHANGED
|
@@ -45,7 +45,7 @@ import { safeSlice } from './safe-slice.js';
|
|
|
45
45
|
import { SEND_TOOLS, bareToolName } from './send-tools.js';
|
|
46
46
|
import { boundedErrorMessage, errorMessage } from './error-message.js';
|
|
47
47
|
import { ProviderWaits, EVERY_MODEL, isReleaseModel, withoutControls } from './provider-waits.js';
|
|
48
|
-
import { nonErrorResultText, toolResultDataToHistoryString, truncateForHistory, DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS, } from './tool-result-history.js';
|
|
48
|
+
import { nonErrorResultText, shownToScript, 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';
|
|
@@ -712,6 +712,9 @@ const RETRY_CANCELLED_BY_STOP = 'a retry waiting while the framework was stoppin
|
|
|
712
712
|
* retry a failed compile across a stop() and a start(): the retry would
|
|
713
713
|
* revive the earlier lifecycle's turn in the next one. */
|
|
714
714
|
const RETRY_ACROSS_RESTART = 'a retry waiting while the framework was stopped and started again is dropped before it sends anything';
|
|
715
|
+
/** The reason on the inference:aborted that ends a turn whose continuation
|
|
716
|
+
* was dropped before it started, other than by an abandon or a stop. */
|
|
717
|
+
const CONTINUATION_DROPPED = "a turn's continuation was dropped before it started";
|
|
715
718
|
function isTurnContinuation(reason) {
|
|
716
719
|
return reason === 'context_budget_restart' || reason === 'tool_results_ready'
|
|
717
720
|
|| reason === 'tool_result_guard_retry';
|
|
@@ -1221,8 +1224,8 @@ const SCRIPT_TEXT_ANSWER_ENVELOPE_BYTES = Buffer.byteLength(JSON.stringify({ con
|
|
|
1221
1224
|
* memoryMaxBytes. So the figure moves with memoryMaxBytes. What it can't
|
|
1222
1225
|
* count: a result's `structured` value, which the measure leaves out and the
|
|
1223
1226
|
* 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
|
|
1227
|
+
* their text, which the answer carries as the script lane gives them (each
|
|
1228
|
+
* further block's keys, an image's data); nesting past 2,000 levels; and a
|
|
1226
1229
|
* parse that needs more of the script's memory than is free, as for JSON
|
|
1227
1230
|
* dense in small objects or a script already holding most of its memory.
|
|
1228
1231
|
* Such a result, or one past the figure that doesn't fit, reaches the script
|
|
@@ -1918,6 +1921,15 @@ export class AgentFramework {
|
|
|
1918
1921
|
// and its settle except that turn's own blocks, and, before its checkpoint,
|
|
1919
1922
|
// the inputs its start writes (flushForStart).
|
|
1920
1923
|
activeTurnTokens = new Map();
|
|
1924
|
+
/** Each running scheduler pass's requests, which it took from
|
|
1925
|
+
* pendingRequests: it holds those of the agents it hasn't taken up yet
|
|
1926
|
+
* while it awaits an earlier agent's start, so a continuation among them
|
|
1927
|
+
* is still on its way (pendingContinuationAgents). */
|
|
1928
|
+
passRequests = new Set();
|
|
1929
|
+
/** Continuations whose start waits on provider admission behind an
|
|
1930
|
+
* auxiliary call, in no queue meanwhile. A drop takes one out and gives
|
|
1931
|
+
* its admission back; the park, woken, then finds it gone. */
|
|
1932
|
+
parkedContinuations = new Set();
|
|
1921
1933
|
/** Each turn whose settle has ended it (settleAgent, endDroppedTurn), for
|
|
1922
1934
|
* its agent's scripts: its start's teardown can still hold the agent's
|
|
1923
1935
|
* token, and a turn that has ended isn't live (liveTurns). A later start
|
|
@@ -2050,7 +2062,15 @@ export class AgentFramework {
|
|
|
2050
2062
|
// config.codeExecution.enabled: its settings, and every agent's scripts,
|
|
2051
2063
|
// each in a process of its own on the engine (AgentScripts). Waiters
|
|
2052
2064
|
// resolve script-inner tool calls dispatched through the normal
|
|
2053
|
-
// dispatchToolCall path.
|
|
2065
|
+
// dispatchToolCall path. A waiter's route is where the dispatch sent its
|
|
2066
|
+
// call: `toServer` is set when it went to a server's own tool (an MCPL or
|
|
2067
|
+
// MCP-only peer's), so the script lane's exit treats every other result as
|
|
2068
|
+
// a module's (scriptResultAnswer), by where the call went rather than by a
|
|
2069
|
+
// name lookup a connect or disconnect can change while it runs. A
|
|
2070
|
+
// framework tool that talks to a server itself, such as a channel tool,
|
|
2071
|
+
// builds its own result, which counts as a module's. A caller of
|
|
2072
|
+
// dispatchScriptToolCall that passes no route gets one left false, the
|
|
2073
|
+
// reading that withholds more.
|
|
2054
2074
|
codeExecution = null;
|
|
2055
2075
|
scriptToolWaiters = new Map();
|
|
2056
2076
|
/** Per agent, a round that called code_execution and waits for those calls'
|
|
@@ -3026,6 +3046,16 @@ export class AgentFramework {
|
|
|
3026
3046
|
// Retained tool images are per-process by design; a retained stopped
|
|
3027
3047
|
// framework must not keep up to the whole ledger budget referenced.
|
|
3028
3048
|
this.toolImageLedgers.clear();
|
|
3049
|
+
// A turn whose continuation is still on its way ends with the stop, as
|
|
3050
|
+
// a turn's start a stop reaches does: the continuation would start only
|
|
3051
|
+
// after a start(), in a lifecycle where its logical turn is gone
|
|
3052
|
+
// (openLogical, below). Before the holds and the parks go, so the
|
|
3053
|
+
// continuation leaves them with its turn.
|
|
3054
|
+
step(() => {
|
|
3055
|
+
for (const agentName of this.pendingContinuationAgents()) {
|
|
3056
|
+
this.dropContinuations(agentName, { kind: 'shutdown', why: 'the framework is stopping' });
|
|
3057
|
+
}
|
|
3058
|
+
});
|
|
3029
3059
|
// A stopped host must never hang behind its own cooldown.
|
|
3030
3060
|
step(() => this.cancelProviderAdmission());
|
|
3031
3061
|
// An open logical turn doesn't survive a stop: a start after a restart
|
|
@@ -3695,7 +3725,8 @@ export class AgentFramework {
|
|
|
3695
3725
|
// The stated wait (recorded above for agent.model) set this hold's
|
|
3696
3726
|
// length: an operator release of it returns the hold to AF's own pacing.
|
|
3697
3727
|
...(pacing?.stated ? { statedWait: { model: agent.model, ownUntil: now + pacing.ownMs } } : {}),
|
|
3698
|
-
...(auxiliary && (!existing || existing.auxiliaryOrigin) ? { auxiliaryOrigin: true } : {})
|
|
3728
|
+
...(auxiliary && (!existing || existing.auxiliaryOrigin) ? { auxiliaryOrigin: true } : {}),
|
|
3729
|
+
...(existing?.emptiedByDrop && held.length === 0 ? { emptiedByDrop: true } : {}) });
|
|
3699
3730
|
if (auxiliary && !existing) {
|
|
3700
3731
|
console.error(`[provider-cooldown] agent=${agent.name} host hold (${hostHold.reason}) from an auxiliary call — ` +
|
|
3701
3732
|
`holding primary/auxiliary for ${delayMs}ms`);
|
|
@@ -3808,9 +3839,13 @@ export class AgentFramework {
|
|
|
3808
3839
|
? `no longer holds primary turns: the primary model is now ${model} (the wait stays recorded)`
|
|
3809
3840
|
: `primary hold ended after ${Date.now() - cooldown.startedAt}ms`));
|
|
3810
3841
|
}
|
|
3811
|
-
if (cooldown.auxiliaryOrigin && cooldown.heldRequests.length === 0) {
|
|
3812
|
-
// Nobody asked for an inference while parked
|
|
3813
|
-
|
|
3842
|
+
if ((cooldown.auxiliaryOrigin || cooldown.emptiedByDrop) && cooldown.heldRequests.length === 0) {
|
|
3843
|
+
// Nobody asked for an inference while parked, or what was held was a
|
|
3844
|
+
// continuation since dropped with its turn: just reopen the gate. A
|
|
3845
|
+
// primary the hold left pending won't run, so its recovery ends.
|
|
3846
|
+
console.error(`[provider-cooldown] agent=${agentName} ${cooldown.auxiliaryOrigin ? 'auxiliary hold' : 'hold, its held continuation dropped,'} released after ${Date.now() - cooldown.startedAt}ms`);
|
|
3847
|
+
if (cooldown.emptiedByDrop)
|
|
3848
|
+
this.endPendingPrimary(agentName);
|
|
3814
3849
|
this.flushAuxiliaryAdmission(agentName);
|
|
3815
3850
|
return;
|
|
3816
3851
|
}
|
|
@@ -5393,14 +5428,60 @@ export class AgentFramework {
|
|
|
5393
5428
|
}
|
|
5394
5429
|
return n;
|
|
5395
5430
|
}
|
|
5431
|
+
/**
|
|
5432
|
+
* A continuation the framework makes for a turn (a budget restart's, which
|
|
5433
|
+
* it queues, or a tool-result guard's retry, which its frame starts at
|
|
5434
|
+
* once) holds no turn token until its start takes one. Once its turn's
|
|
5435
|
+
* frame has let go of its token, its start can wait in the queue, a
|
|
5436
|
+
* provider hold (a retry too, when a wait holds it), a park on admission
|
|
5437
|
+
* or a scheduler pass's hands. Its turn is alive meanwhile. These are the
|
|
5438
|
+
* agents with one on its way. A
|
|
5439
|
+
* `tool_results_ready` continuation follows a tool round left waiting with
|
|
5440
|
+
* no stream, which only the direct Agent API makes (Agent's
|
|
5441
|
+
* runInferenceWithInjections; the framework's own rounds wait with their
|
|
5442
|
+
* stream, enterWaitingForTools). Its caller owns that turn's timing, so it
|
|
5443
|
+
* isn't counted.
|
|
5444
|
+
*/
|
|
5445
|
+
pendingContinuationAgents() {
|
|
5446
|
+
const agents = new Set();
|
|
5447
|
+
const add = (r) => { if (resumesLogicalTurn(r.reason))
|
|
5448
|
+
agents.add(r.agentName); };
|
|
5449
|
+
// Read defensively: a framework built from the prototype, as the
|
|
5450
|
+
// operator tests build one, sets none of these.
|
|
5451
|
+
for (const r of this.pendingRequests ?? [])
|
|
5452
|
+
add(r);
|
|
5453
|
+
for (const cooldown of this.providerAccelerationCooldowns?.values() ?? [])
|
|
5454
|
+
for (const r of cooldown.heldRequests)
|
|
5455
|
+
add(r);
|
|
5456
|
+
for (const r of this.parkedContinuations ?? [])
|
|
5457
|
+
add(r);
|
|
5458
|
+
for (const pass of this.passRequests ?? [])
|
|
5459
|
+
for (const requests of pass.values())
|
|
5460
|
+
for (const r of requests)
|
|
5461
|
+
add(r);
|
|
5462
|
+
return agents;
|
|
5463
|
+
}
|
|
5464
|
+
/** The agents with a turn alive: holding a turn token, or with a
|
|
5465
|
+
* continuation of their turn on its way. */
|
|
5466
|
+
turnHolders() {
|
|
5467
|
+
const holders = this.pendingContinuationAgents();
|
|
5468
|
+
for (const name of this.activeTurnTokens.keys())
|
|
5469
|
+
holders.add(name);
|
|
5470
|
+
return holders;
|
|
5471
|
+
}
|
|
5472
|
+
hasTurn(agentName) {
|
|
5473
|
+
return this.activeTurnTokens.has(agentName) || this.pendingContinuationAgents().has(agentName);
|
|
5474
|
+
}
|
|
5396
5475
|
getHostModeStatus() {
|
|
5397
5476
|
const parkedAdmissions = this.parkedAdmissionCount();
|
|
5477
|
+
const holders = this.turnHolders();
|
|
5398
5478
|
return {
|
|
5399
5479
|
quiesced: this.quiesced,
|
|
5400
5480
|
...(this.quiesceReason ? { reason: this.quiesceReason } : {}),
|
|
5401
5481
|
...(this.quiescedAt !== undefined ? { since: this.quiescedAt } : {}),
|
|
5402
|
-
drained:
|
|
5403
|
-
activeTurns:
|
|
5482
|
+
drained: holders.size === 0 && parkedAdmissions === 0,
|
|
5483
|
+
activeTurns: holders.size,
|
|
5484
|
+
activeTurnAgents: [...holders].sort(),
|
|
5404
5485
|
parkedAdmissions,
|
|
5405
5486
|
gatedRequests: this.pendingRequests.filter((r) => !isTurnContinuation(r.reason)).length,
|
|
5406
5487
|
backgroundScripts: this.codeExecution?.scripts.leftRunning() ?? 0,
|
|
@@ -5417,8 +5498,9 @@ export class AgentFramework {
|
|
|
5417
5498
|
/**
|
|
5418
5499
|
* Enter quiesce mode: write the deferred-write queue, persist the flag,
|
|
5419
5500
|
* suppress gate deliveries, pause all MCPL data planes, and wait for
|
|
5420
|
-
* in-flight turns to settle (turn-alive is
|
|
5421
|
-
* dequeue → settled teardown
|
|
5501
|
+
* in-flight turns to settle (turn-alive is a turn token, which spans
|
|
5502
|
+
* dequeue → settled teardown, or a continuation of the turn on its way to
|
|
5503
|
+
* its start: turnHolders). Idempotent, but for the queue's write, which
|
|
5422
5504
|
* every call makes first: when the queue can't reach disk, because its
|
|
5423
5505
|
* write fails or it's over the cap, the call rejects with a
|
|
5424
5506
|
* QuiesceBlockedError and the host's mode as it was, unless it has
|
|
@@ -5428,7 +5510,10 @@ export class AgentFramework {
|
|
|
5428
5510
|
* On drain timeout the host STAYS quiesced (`drained: false` in the
|
|
5429
5511
|
* result); with `abandon: true` the undrained streams are cancelled via a
|
|
5430
5512
|
* dedicated cancel kind that settles the turn without feeding the
|
|
5431
|
-
* inference-failure accounting
|
|
5513
|
+
* inference-failure accounting, and a turn whose continuation hasn't
|
|
5514
|
+
* started ends with the continuation dropped. Drain covers the framework's
|
|
5515
|
+
* own turns: a caller of the direct Agent API (`Agent.runInference`) owns
|
|
5516
|
+
* its turns' timing.
|
|
5432
5517
|
*/
|
|
5433
5518
|
async quiesce(opts) {
|
|
5434
5519
|
this.refuseOperatorAfterStop('quiesce');
|
|
@@ -5445,13 +5530,13 @@ export class AgentFramework {
|
|
|
5445
5530
|
// out undrained must be escalatable with a second quiesce({abandon})
|
|
5446
5531
|
// without resume()+re-quiesce (which would reopen data planes and
|
|
5447
5532
|
// release parked wakes mid-surgery).
|
|
5448
|
-
if (opts?.abandon && this.
|
|
5533
|
+
if (opts?.abandon && this.turnHolders().size > 0) {
|
|
5449
5534
|
const unabandonable = await this.abandonActiveTurns();
|
|
5450
5535
|
this.emitTrace({
|
|
5451
5536
|
type: 'host:quiesce',
|
|
5452
5537
|
...(this.quiesceReason ? { reason: this.quiesceReason } : {}),
|
|
5453
|
-
drained: this.
|
|
5454
|
-
activeTurns: this.
|
|
5538
|
+
drained: this.turnHolders().size === 0,
|
|
5539
|
+
activeTurns: this.turnHolders().size,
|
|
5455
5540
|
abandoned: true,
|
|
5456
5541
|
...(unabandonable.length > 0 ? { unabandonable } : {}),
|
|
5457
5542
|
});
|
|
@@ -5476,7 +5561,7 @@ export class AgentFramework {
|
|
|
5476
5561
|
this.quiescedAt = since;
|
|
5477
5562
|
this.lastUnabandonable = [];
|
|
5478
5563
|
console.error(`[host-mode] quiescing${opts?.reason ? ` (${opts.reason})` : ''}: parking wakes, ` +
|
|
5479
|
-
`pausing MCPL data planes, draining ${this.
|
|
5564
|
+
`pausing MCPL data planes, draining ${this.turnHolders().size} in-flight turn(s)`);
|
|
5480
5565
|
this.eventGate?.setQuiesced(true);
|
|
5481
5566
|
for (const connection of this.mcplServerRegistry?.getAllServers() ?? []) {
|
|
5482
5567
|
connection.pauseDataPlane();
|
|
@@ -5496,7 +5581,7 @@ export class AgentFramework {
|
|
|
5496
5581
|
// continuation's quiesce recheck requeues it — wait for that to happen).
|
|
5497
5582
|
while (this.quiesced &&
|
|
5498
5583
|
!this.queue?.isClosed &&
|
|
5499
|
-
(this.
|
|
5584
|
+
(this.turnHolders().size > 0 || this.parkedAdmissionCount() > 0) &&
|
|
5500
5585
|
Date.now() < deadline) {
|
|
5501
5586
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
5502
5587
|
}
|
|
@@ -5517,7 +5602,7 @@ export class AgentFramework {
|
|
|
5517
5602
|
}
|
|
5518
5603
|
let abandoned = false;
|
|
5519
5604
|
let unabandonable = [];
|
|
5520
|
-
if (this.
|
|
5605
|
+
if (this.turnHolders().size > 0 && opts?.abandon) {
|
|
5521
5606
|
abandoned = true;
|
|
5522
5607
|
unabandonable = await this.abandonActiveTurns();
|
|
5523
5608
|
if (!this.quiesced)
|
|
@@ -5547,7 +5632,19 @@ export class AgentFramework {
|
|
|
5547
5632
|
* the already-quiesced escalation path. */
|
|
5548
5633
|
async abandonActiveTurns() {
|
|
5549
5634
|
const unabandonable = [];
|
|
5635
|
+
// A turn whose continuation is still on its way ends with the
|
|
5636
|
+
// continuation dropped. Its stream has ended; a frame still letting go of
|
|
5637
|
+
// its token ends on its own, and settles nothing, since it handed the turn
|
|
5638
|
+
// to the continuation.
|
|
5639
|
+
const handedOff = new Set();
|
|
5640
|
+
for (const agentName of this.pendingContinuationAgents()) {
|
|
5641
|
+
console.error(`[host-mode] abandoning ${agentName}'s turn before its continuation starts: the continuation is dropped`);
|
|
5642
|
+
this.dropContinuations(agentName, { kind: 'quiesce_abandoned', why: 'its turn was abandoned' });
|
|
5643
|
+
handedOff.add(agentName);
|
|
5644
|
+
}
|
|
5550
5645
|
for (const agentName of [...this.activeTurnTokens.keys()]) {
|
|
5646
|
+
if (handedOff.has(agentName))
|
|
5647
|
+
continue;
|
|
5551
5648
|
const agent = this.agents.get(agentName);
|
|
5552
5649
|
const state = agent?.state;
|
|
5553
5650
|
const stream = state && 'stream' in state ? state.stream : undefined;
|
|
@@ -7496,6 +7593,9 @@ export class AgentFramework {
|
|
|
7496
7593
|
this.activeTurnTokens.delete(agent.name);
|
|
7497
7594
|
this.activeTurnTriggers.delete(agent.name);
|
|
7498
7595
|
this.logicalTurnToolCalls.delete(agent);
|
|
7596
|
+
// A continuation of its turn still on its way goes too: the run, and
|
|
7597
|
+
// with it the turn, has ended above.
|
|
7598
|
+
this.dropContinuations(agent.name, null);
|
|
7499
7599
|
}
|
|
7500
7600
|
}
|
|
7501
7601
|
/**
|
|
@@ -9104,7 +9204,7 @@ export class AgentFramework {
|
|
|
9104
9204
|
for (const [name, a] of this.agents) {
|
|
9105
9205
|
if (a.state.status !== 'idle')
|
|
9106
9206
|
busy.push(`${name} is ${a.state.status}`);
|
|
9107
|
-
else if (this.
|
|
9207
|
+
else if (this.hasTurn(name))
|
|
9108
9208
|
busy.push(`${name} is idle+turn-alive`);
|
|
9109
9209
|
}
|
|
9110
9210
|
for (const name of this.activeTurnTokens.keys()) {
|
|
@@ -10087,7 +10187,7 @@ export class AgentFramework {
|
|
|
10087
10187
|
// was killed while the call ran: the result reaches no one, but the
|
|
10088
10188
|
// call did finish.
|
|
10089
10189
|
this.toolLifecycleEmitter?.onResult(event.agentName, event.callId, event.result);
|
|
10090
|
-
scriptWaiter(event.result);
|
|
10190
|
+
scriptWaiter.resolve(event.result);
|
|
10091
10191
|
return;
|
|
10092
10192
|
}
|
|
10093
10193
|
// RFC-007: the call's terminal (completed / failed) — the result
|
|
@@ -10513,7 +10613,27 @@ export class AgentFramework {
|
|
|
10513
10613
|
}
|
|
10514
10614
|
this.emitTrace({ type: 'inference:turn_ended', agentName: agent.name });
|
|
10515
10615
|
}
|
|
10616
|
+
else if ((overBudget || overPhysical) && this.providerAdmissionClosed) {
|
|
10617
|
+
// A stopping framework starts no continuation: the turn ends with
|
|
10618
|
+
// its stream, as stop() ends a live stream's turn.
|
|
10619
|
+
if (currentState.stream)
|
|
10620
|
+
this.frameworkCancelledStreams.set(`${agent.name}:${agent.streamId}`, 'shutdown');
|
|
10621
|
+
agent.cancelStream();
|
|
10622
|
+
}
|
|
10516
10623
|
else if (overBudget || overPhysical) {
|
|
10624
|
+
// The continuation is queued, and the stream cancelled, before the
|
|
10625
|
+
// boundary's usage report and the restart's trace, so a listener
|
|
10626
|
+
// that stops or quiesces the host on either finds the turn waiting
|
|
10627
|
+
// for its continuation, with nothing left of the stream to end.
|
|
10628
|
+
const continuation = {
|
|
10629
|
+
agentName: agent.name,
|
|
10630
|
+
reason: 'context_budget_restart',
|
|
10631
|
+
source: 'framework',
|
|
10632
|
+
timestamp: Date.now(),
|
|
10633
|
+
suppressProse: this.activeTurnTriggers.get(agent.name)?.suppressProse,
|
|
10634
|
+
silentHeartbeat: this.activeTurnTriggers.get(agent.name)?.silentHeartbeat,
|
|
10635
|
+
};
|
|
10636
|
+
this.pendingRequests.push(continuation);
|
|
10517
10637
|
// Context budget exceeded: break the stream, let compile() compress.
|
|
10518
10638
|
// Mark the cancel as framework-initiated BEFORE cancelling: the
|
|
10519
10639
|
// membrane delivers `aborted` before the restart bumps streamId,
|
|
@@ -10521,13 +10641,23 @@ export class AgentFramework {
|
|
|
10521
10641
|
// a terminal failure — rejecting an ephemeral's promise mid-run
|
|
10522
10642
|
// and emitting a spurious inference:exhausted (same race shape as
|
|
10523
10643
|
// endTurn above).
|
|
10524
|
-
if (currentState.stream)
|
|
10644
|
+
if (currentState.stream)
|
|
10525
10645
|
this.frameworkCancelledStreams.set(`${agent.name}:${agent.streamId}`, 'budget_restart');
|
|
10526
|
-
|
|
10527
|
-
|
|
10528
|
-
|
|
10646
|
+
try {
|
|
10647
|
+
agent.cancelStream();
|
|
10648
|
+
}
|
|
10649
|
+
catch (error) {
|
|
10650
|
+
// The cancel failed and the stream is still live, so the turn
|
|
10651
|
+
// has no continuation: take it back out of the queue.
|
|
10652
|
+
this.pendingRequests = this.pendingRequests.filter((r) => r !== continuation);
|
|
10653
|
+
throw error;
|
|
10654
|
+
}
|
|
10655
|
+
finally {
|
|
10656
|
+
// Its rounds were billed, even when the cancel throws; the
|
|
10657
|
+
// restart opens a fresh stream with its own usage.
|
|
10658
|
+
if (currentState.stream)
|
|
10659
|
+
this.streamBoundaryEnds.get(currentState.stream)?.(overBudget ? 'context_budget' : 'physical_window');
|
|
10529
10660
|
}
|
|
10530
|
-
agent.cancelStream();
|
|
10531
10661
|
this.emitTrace({
|
|
10532
10662
|
type: 'inference:stream_restarted',
|
|
10533
10663
|
agentName: agent.name,
|
|
@@ -10538,14 +10668,6 @@ export class AgentFramework {
|
|
|
10538
10668
|
inputTokens: overBudget ? agent.lastStreamInputTokens : projectedRealTokens,
|
|
10539
10669
|
budget: overBudget ? agent.maxStreamTokens : (agent.physicalWindowTokens ?? 0),
|
|
10540
10670
|
});
|
|
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
10671
|
}
|
|
10550
10672
|
else if (currentState.stream) {
|
|
10551
10673
|
// What this round appends is in the next call's prompt. If that
|
|
@@ -11306,6 +11428,8 @@ export class AgentFramework {
|
|
|
11306
11428
|
*/
|
|
11307
11429
|
disposeConversationAgent(agentName) {
|
|
11308
11430
|
this.closingConversationAgents.delete(agentName);
|
|
11431
|
+
// A continuation of its turn still on its way goes with it.
|
|
11432
|
+
this.dropContinuations(agentName, { kind: 'dropped', why: 'its agent was disposed' });
|
|
11309
11433
|
const channelId = this.conversationAgentHomes.get(agentName);
|
|
11310
11434
|
const agent = this.agents.get(agentName);
|
|
11311
11435
|
// A fork stop() ends can still be running: its stream, its tool round.
|
|
@@ -11780,9 +11904,19 @@ export class AgentFramework {
|
|
|
11780
11904
|
recordReceipt: (record) => {
|
|
11781
11905
|
// Written now, not at the throttled flush: an acknowledged acceptance
|
|
11782
11906
|
// must not lose its receipt, the subject's birth, or (deferred) the
|
|
11783
|
-
// accepted work itself to a crash. A failure here fails the
|
|
11907
|
+
// accepted work itself to a crash. A failure here fails the answer:
|
|
11908
|
+
// the coalescer keeps the receipt, and the producer's retry writes it
|
|
11909
|
+
// again, with the subject's state as it is then. So a record whose
|
|
11910
|
+
// write failed leaves the list: a later write would carry it after
|
|
11911
|
+
// the batch it holds had moved on.
|
|
11784
11912
|
this.coalescingRecentReceipts.push(record);
|
|
11785
|
-
|
|
11913
|
+
try {
|
|
11914
|
+
this.store.setStateJson(COALESCING_RECENT_ID, this.coalescingRecentReceipts);
|
|
11915
|
+
}
|
|
11916
|
+
catch (error) {
|
|
11917
|
+
this.coalescingRecentReceipts.pop();
|
|
11918
|
+
throw error;
|
|
11919
|
+
}
|
|
11786
11920
|
},
|
|
11787
11921
|
// The acceptance of coalesced work (deferred included) is observed
|
|
11788
11922
|
// here, once; its later delivery reuses the frozen envelope.
|
|
@@ -11904,6 +12038,9 @@ export class AgentFramework {
|
|
|
11904
12038
|
}
|
|
11905
12039
|
catch (err) {
|
|
11906
12040
|
console.error('[coalescing] could not persist coalescing state:', err);
|
|
12041
|
+
// Still unsaved: the next persist, or the stop's flush, writes it. No
|
|
12042
|
+
// timer of its own, so a store that keeps failing isn't retried here.
|
|
12043
|
+
this.coalescingSnapshotDirty ??= snapshot;
|
|
11907
12044
|
}
|
|
11908
12045
|
}
|
|
11909
12046
|
/** Serialize both lanes' admissions: create, edit and delete of one subject stay ordered. */
|
|
@@ -12532,15 +12669,26 @@ export class AgentFramework {
|
|
|
12532
12669
|
}
|
|
12533
12670
|
return undefined;
|
|
12534
12671
|
}
|
|
12672
|
+
/** One scheduler pass, whose requests stay visible as on their way until
|
|
12673
|
+
* it takes their agent up (passRequests). */
|
|
12535
12674
|
async processInferenceRequests() {
|
|
12536
12675
|
if (this.pendingRequests.length === 0) {
|
|
12537
12676
|
return;
|
|
12538
12677
|
}
|
|
12678
|
+
const requestsByAgent = new Map();
|
|
12679
|
+
this.passRequests?.add(requestsByAgent);
|
|
12680
|
+
try {
|
|
12681
|
+
await this.runInferencePass(requestsByAgent);
|
|
12682
|
+
}
|
|
12683
|
+
finally {
|
|
12684
|
+
this.passRequests?.delete(requestsByAgent);
|
|
12685
|
+
}
|
|
12686
|
+
}
|
|
12687
|
+
async runInferencePass(requestsByAgent) {
|
|
12539
12688
|
const STALE_REQUEST_MS = 30_000;
|
|
12540
12689
|
const now = Date.now();
|
|
12541
12690
|
const state = this.createFrameworkState();
|
|
12542
12691
|
// Group requests by agent
|
|
12543
|
-
const requestsByAgent = new Map();
|
|
12544
12692
|
for (const req of this.pendingRequests) {
|
|
12545
12693
|
const existing = requestsByAgent.get(req.agentName) ?? [];
|
|
12546
12694
|
existing.push(req);
|
|
@@ -12551,6 +12699,14 @@ export class AgentFramework {
|
|
|
12551
12699
|
// Check each agent
|
|
12552
12700
|
for (const [agentName, groupedRequests] of requestsByAgent) {
|
|
12553
12701
|
let requests = groupedRequests;
|
|
12702
|
+
// Taken up now: from here to its start's turn token (or its hold or
|
|
12703
|
+
// park) nothing awaits, so the requests leave the pass's map, which
|
|
12704
|
+
// shows only what the pass still holds while it awaits a start.
|
|
12705
|
+
requestsByAgent.delete(agentName);
|
|
12706
|
+
// A drop while this pass awaited an earlier agent's start can have
|
|
12707
|
+
// taken every request of this agent's (dropContinuations).
|
|
12708
|
+
if (requests.length === 0)
|
|
12709
|
+
continue;
|
|
12554
12710
|
const agent = this.agents.get(agentName);
|
|
12555
12711
|
if (!agent) {
|
|
12556
12712
|
// Agent not found — request is orphaned. Emit warning and drop.
|
|
@@ -12563,6 +12719,10 @@ export class AgentFramework {
|
|
|
12563
12719
|
oldestRequestAge: now - oldest,
|
|
12564
12720
|
});
|
|
12565
12721
|
console.error(`[inference-dropped] agent=${agentName} reason=agent_not_found requests=${requests.length}`);
|
|
12722
|
+
// A continuation among them leaves its turn to end here.
|
|
12723
|
+
if (requests.some((r) => resumesLogicalTurn(r.reason))) {
|
|
12724
|
+
this.endContinuedTurn(agentName, { kind: 'dropped', why: 'its agent is gone' });
|
|
12725
|
+
}
|
|
12566
12726
|
continue;
|
|
12567
12727
|
}
|
|
12568
12728
|
const providerCooldown = this.providerAccelerationCooldowns?.get(agentName);
|
|
@@ -12652,7 +12812,10 @@ export class AgentFramework {
|
|
|
12652
12812
|
requests.length = 0;
|
|
12653
12813
|
requests.push(...passThrough);
|
|
12654
12814
|
}
|
|
12655
|
-
|
|
12815
|
+
// Turn-alive includes a continuation of the agent's turn still on its
|
|
12816
|
+
// way elsewhere (a park, another pass's hands): a fresh wake waits for
|
|
12817
|
+
// it, as for a turn holding its token.
|
|
12818
|
+
const turnAlive = !budgetRestart && this.hasTurn(agentName);
|
|
12656
12819
|
const providerGate = this.providerGates?.get(agentName);
|
|
12657
12820
|
// A primary can own provider admission while yielding to an already in-flight
|
|
12658
12821
|
// auxiliary call. Keep this agent's later wakes queued, but do not block the
|
|
@@ -12722,10 +12885,12 @@ export class AgentFramework {
|
|
|
12722
12885
|
// exactly this class of silent drop. One stderr line per drop.
|
|
12723
12886
|
console.error(`[inference-dropped] agent=${agentName} reason=policy-skip ` +
|
|
12724
12887
|
`requests=${requests.length} triggers=${requests.map((r) => r.reason).join(',')}`);
|
|
12725
|
-
// A
|
|
12726
|
-
// If policy drops
|
|
12727
|
-
|
|
12728
|
-
|
|
12888
|
+
// A continuation inherits its predecessor's EventGate liveness and
|
|
12889
|
+
// logical turn. If policy drops it, no driveStream remains to end them,
|
|
12890
|
+
// so its turn ends here (endContinuedTurn).
|
|
12891
|
+
if (requests.some((r) => resumesLogicalTurn(r.reason))) {
|
|
12892
|
+
this.endContinuedTurn(agentName, { kind: 'dropped', why: 'the inference policy dropped its continuation' });
|
|
12893
|
+
}
|
|
12729
12894
|
this.endPendingPrimary(agentName);
|
|
12730
12895
|
continue;
|
|
12731
12896
|
}
|
|
@@ -15078,7 +15243,14 @@ export class AgentFramework {
|
|
|
15078
15243
|
// Do not await inside processInferenceRequests: one slow auxiliary call for
|
|
15079
15244
|
// this resident must not block events or other residents. Admission stays
|
|
15080
15245
|
// owned and the continuation receives it after the in-flight call settles.
|
|
15246
|
+
const parkedContinuation = trigger !== undefined && resumesLogicalTurn(trigger.reason);
|
|
15247
|
+
if (parkedContinuation)
|
|
15248
|
+
this.parkedContinuations.add(trigger);
|
|
15081
15249
|
void this.waitForAuxiliaryIdle(agent.name).then(async () => {
|
|
15250
|
+
// A continuation dropped while it was parked left the set then, with
|
|
15251
|
+
// its admission given back.
|
|
15252
|
+
if (parkedContinuation && !this.parkedContinuations.delete(trigger))
|
|
15253
|
+
return;
|
|
15082
15254
|
if (this.providerAdmissionClosed) {
|
|
15083
15255
|
this.releasePrimaryProviderGate(agent.name);
|
|
15084
15256
|
return;
|
|
@@ -15243,6 +15415,99 @@ export class AgentFramework {
|
|
|
15243
15415
|
}
|
|
15244
15416
|
}
|
|
15245
15417
|
}
|
|
15418
|
+
/**
|
|
15419
|
+
* Drop every continuation of `agentName`'s turn still on its way, wherever
|
|
15420
|
+
* it waits: the queue, a provider hold (which, left empty, then releases
|
|
15421
|
+
* with nothing to run), a park on admission (whose admission it gives
|
|
15422
|
+
* back), or a scheduler pass's hands (which then never reach it). If one
|
|
15423
|
+
* was there, the turn ends (endContinuedTurn), unless `drop` is null: the
|
|
15424
|
+
* caller has ended the turn already.
|
|
15425
|
+
*/
|
|
15426
|
+
dropContinuations(agentName, drop) {
|
|
15427
|
+
const isContinuation = (r) => r.agentName === agentName && resumesLogicalTurn(r.reason);
|
|
15428
|
+
let dropped = false;
|
|
15429
|
+
const take = (requests) => {
|
|
15430
|
+
const left = requests.filter((r) => !isContinuation(r));
|
|
15431
|
+
if (left.length < requests.length)
|
|
15432
|
+
dropped = true;
|
|
15433
|
+
return left;
|
|
15434
|
+
};
|
|
15435
|
+
this.pendingRequests = take(this.pendingRequests);
|
|
15436
|
+
const hold = this.providerAccelerationCooldowns.get(agentName);
|
|
15437
|
+
if (hold) {
|
|
15438
|
+
const held = hold.heldRequests.length;
|
|
15439
|
+
hold.heldRequests = take(hold.heldRequests);
|
|
15440
|
+
if (held > 0 && hold.heldRequests.length === 0)
|
|
15441
|
+
hold.emptiedByDrop = true;
|
|
15442
|
+
}
|
|
15443
|
+
for (const parked of [...this.parkedContinuations]) {
|
|
15444
|
+
if (!isContinuation(parked))
|
|
15445
|
+
continue;
|
|
15446
|
+
this.parkedContinuations.delete(parked);
|
|
15447
|
+
this.releasePrimaryProviderGate(agentName);
|
|
15448
|
+
dropped = true;
|
|
15449
|
+
}
|
|
15450
|
+
for (const pass of this.passRequests) {
|
|
15451
|
+
const requests = pass.get(agentName);
|
|
15452
|
+
if (!requests)
|
|
15453
|
+
continue;
|
|
15454
|
+
const left = take(requests);
|
|
15455
|
+
requests.length = 0;
|
|
15456
|
+
requests.push(...left);
|
|
15457
|
+
}
|
|
15458
|
+
if (!dropped || !drop)
|
|
15459
|
+
return;
|
|
15460
|
+
// A primary a provider hold left pending for what it released won't run
|
|
15461
|
+
// now, when nothing else of the agent's waits (endPendingPrimary).
|
|
15462
|
+
if (!this.pendingRequests.some((r) => r.agentName === agentName) && !this.providerAccelerationCooldowns.get(agentName)?.heldRequests.length) {
|
|
15463
|
+
this.endPendingPrimary(agentName);
|
|
15464
|
+
}
|
|
15465
|
+
this.endContinuedTurn(agentName, drop);
|
|
15466
|
+
}
|
|
15467
|
+
/**
|
|
15468
|
+
* End a turn whose continuation won't run. The frame that handed the turn
|
|
15469
|
+
* to it kept the turn open for it: its logical turn, the EventGate's
|
|
15470
|
+
* window, the tool-result guard's batch and recovery, its typing. An
|
|
15471
|
+
* abandon or a stop of a registered agent settles the turn as it settles a
|
|
15472
|
+
* turn's start (endStoppedStart: one inference:aborted, the turn settled,
|
|
15473
|
+
* the gate released). Any other drop ends it as a start dropped before it
|
|
15474
|
+
* sends anything ends. That frame, if it hasn't let go of its token yet,
|
|
15475
|
+
* ends on its own and settles nothing. What was deferred for the agent
|
|
15476
|
+
* lands now, or at that frame's end.
|
|
15477
|
+
*/
|
|
15478
|
+
endContinuedTurn(agentName, drop) {
|
|
15479
|
+
console.error(`[turn-continuation] ${agentName}: its turn ends without its continuation: ${drop.why}`);
|
|
15480
|
+
const agent = this.agents.get(agentName);
|
|
15481
|
+
const startedAt = this.lastInferenceAt.get(agentName)?.startedAt ?? Date.now();
|
|
15482
|
+
if (agent && drop.kind !== 'dropped') {
|
|
15483
|
+
const routing = this.openLogical.get(agentName) ?? this.routingOf(agentName);
|
|
15484
|
+
this.endStoppedStart(agent, { kind: drop.kind }, routing, startedAt, undefined, false);
|
|
15485
|
+
}
|
|
15486
|
+
else {
|
|
15487
|
+
const owner = this.activeTurnTokens.get(agentName);
|
|
15488
|
+
this.abortDroppedTurn(agentName, owner ?? -1, drop.kind === 'dropped' ? CONTINUATION_DROPPED : drop.kind, startedAt);
|
|
15489
|
+
this.channelRegistry?.stopTyping();
|
|
15490
|
+
if (agent) {
|
|
15491
|
+
agent.toolResultGuard.recovering = false;
|
|
15492
|
+
if (agent.toolResultGuard.hasPending) {
|
|
15493
|
+
try {
|
|
15494
|
+
agent.toolResultGuard.abandon('aborted');
|
|
15495
|
+
}
|
|
15496
|
+
catch (error) {
|
|
15497
|
+
console.error(`[tool-result-guard] agent=${agentName} failed to record aborted batch:`, error);
|
|
15498
|
+
}
|
|
15499
|
+
}
|
|
15500
|
+
}
|
|
15501
|
+
const routing = this.openLogical.get(agentName) ?? this.turnRouting.get(agentName);
|
|
15502
|
+
if (routing)
|
|
15503
|
+
this.endDroppedTurn(agentName, routing);
|
|
15504
|
+
this.eventGate?.onInferenceEnded(agentName);
|
|
15505
|
+
}
|
|
15506
|
+
if (!this.activeTurnTokens.has(agentName) && this.deferredMessages.length > 0 && !this.pendingAssistantBlocks.has(agentName)) {
|
|
15507
|
+
this.readdDrained(this.drainDeferredFor(agentName), 'continuation-drop flush');
|
|
15508
|
+
this.ackDeferredWrites();
|
|
15509
|
+
}
|
|
15510
|
+
}
|
|
15246
15511
|
/**
|
|
15247
15512
|
* End a turn that beginAgentTurn drops before it sends anything. The turn
|
|
15248
15513
|
* emitted inference:started, and listeners that bracket a turn, such as a
|
|
@@ -15297,7 +15562,7 @@ export class AgentFramework {
|
|
|
15297
15562
|
* continues one (a restart or a retry) may follow rounds already sent,
|
|
15298
15563
|
* so the note says it was stopped before it finished. */
|
|
15299
15564
|
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`);
|
|
15565
|
+
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
15566
|
this.channelRegistry?.stopTyping();
|
|
15302
15567
|
// A start that continues a turn (a budget or guard restart) follows a
|
|
15303
15568
|
// frame that kept the turn's tool-result batch, and the guard's recovery,
|
|
@@ -15325,7 +15590,7 @@ export class AgentFramework {
|
|
|
15325
15590
|
this.settleDeliberateCancel({ ...common, reason: stop.reason ?? 'user', before: opensTurn ? 'sending' : 'finishing' });
|
|
15326
15591
|
}
|
|
15327
15592
|
else {
|
|
15328
|
-
this.settleFrameworkCancel({ ...common, kind:
|
|
15593
|
+
this.settleFrameworkCancel({ ...common, kind: stop.kind });
|
|
15329
15594
|
}
|
|
15330
15595
|
const last = this.lastInferenceAt.get(agent.name);
|
|
15331
15596
|
if (last?.startedAt === turnStartedAt)
|
|
@@ -16019,8 +16284,8 @@ export class AgentFramework {
|
|
|
16019
16284
|
this.recordProviderAccelerationRecovery(agent, compiledRequest, end);
|
|
16020
16285
|
// The accounting writes to the store, and a write that fails must not
|
|
16021
16286
|
// 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.
|
|
16287
|
+
// end, or a restart's trace (its cancel and its request come before
|
|
16288
|
+
// this call), and then the event's dispatch to modules.
|
|
16024
16289
|
try {
|
|
16025
16290
|
if (turnEnded) {
|
|
16026
16291
|
completeTurn(latestUsage, durationMs);
|
|
@@ -17933,7 +18198,11 @@ export class AgentFramework {
|
|
|
17933
18198
|
// its ids when a successor turn has already taken the agent. A fork or
|
|
17934
18199
|
// run deregistered while the host is quiesced leaves its queue to its
|
|
17935
18200
|
// window's release at resume (drainDeferredFor).
|
|
17936
|
-
|
|
18201
|
+
// A frame that handed its turn to a continuation since dropped
|
|
18202
|
+
// (endContinuedTurn, which couldn't land it while this frame held the
|
|
18203
|
+
// token) lands it too: the turn is over.
|
|
18204
|
+
const handedOffTurnOver = preserveEventGateForSuccessor && !this.hasTurn(agent.name);
|
|
18205
|
+
if ((frameReachedTerminal || handedOffTurnOver) && this.deferredMessages.length > 0 && !this.pendingAssistantBlocks.has(agent.name)) {
|
|
17937
18206
|
this.readdDrained(this.drainDeferredFor(agent.name), 'turn-end flush');
|
|
17938
18207
|
this.ackDeferredWrites();
|
|
17939
18208
|
}
|
|
@@ -18036,7 +18305,7 @@ export class AgentFramework {
|
|
|
18036
18305
|
if (this.surgeryHold) {
|
|
18037
18306
|
throw new Error(`puppet refused: the store is under live ${this.surgeryHold.verb} for ${this.surgeryHold.agentName} — retry when it completes`);
|
|
18038
18307
|
}
|
|
18039
|
-
if (agent.state.status !== 'idle' || this.
|
|
18308
|
+
if (agent.state.status !== 'idle' || this.hasTurn(agentName)) {
|
|
18040
18309
|
const shown = agent.state.status === 'idle' ? 'idle+turn-alive' : agent.state.status;
|
|
18041
18310
|
throw new Error(`puppet refused: agent ${agentName} is ${shown} (requires idle — ` +
|
|
18042
18311
|
`injecting a turn under an active stream corrupts wire ordering)`);
|
|
@@ -18934,7 +19203,8 @@ export class AgentFramework {
|
|
|
18934
19203
|
}
|
|
18935
19204
|
if (refusal !== undefined)
|
|
18936
19205
|
return refused(refusal);
|
|
18937
|
-
const
|
|
19206
|
+
const route = { toServer: false };
|
|
19207
|
+
const result = await this.dispatchScriptToolCall(agentName, toolName, input, context.origin, context.watch?.turnId, route);
|
|
18938
19208
|
// Deferred: applied to the result of the code_execution call watching
|
|
18939
19209
|
// the script — ending the turn mid-script would cancel the stream while
|
|
18940
19210
|
// the script still runs and wedge the tool round. A call a left script
|
|
@@ -18946,7 +19216,7 @@ export class AgentFramework {
|
|
|
18946
19216
|
watching.endTurn = true;
|
|
18947
19217
|
if (result.refused)
|
|
18948
19218
|
return refused(result.error ?? `${toolName} was refused; nothing was done`);
|
|
18949
|
-
const answer = await this.scriptResultAnswer(result, toolName);
|
|
19219
|
+
const answer = await this.scriptResultAnswer(result, toolName, route.toServer);
|
|
18950
19220
|
return SCRIPT_CALL_NO_RESULT.has(result) ? { ...answer, held: true } : answer;
|
|
18951
19221
|
}
|
|
18952
19222
|
/**
|
|
@@ -18954,14 +19224,18 @@ export class AgentFramework {
|
|
|
18954
19224
|
* isError }`, one shape for every tool. `content` is
|
|
18955
19225
|
* what the model would read: a structured result's blocks as the dispatch
|
|
18956
19226
|
* gave them (the direct reading, with RFC-005's treatment of an MCPL peer's
|
|
18957
|
-
* blocks
|
|
18958
|
-
*
|
|
19227
|
+
* blocks there, bound to the server that sent them), a module's under
|
|
19228
|
+
* history's reading of RFC-005 at every depth (shownToScript), and
|
|
19229
|
+
* otherwise one text block of the model's text, which for a failure is its
|
|
19230
|
+
* error. A module's is any result whose call the dispatch didn't send to
|
|
19231
|
+
* a server (`toServer`, from the call's waiter). `structuredContent` is the
|
|
19232
|
+
* tool's own structured
|
|
18959
19233
|
* content where it sent one, `false`, `0` and `null` included, else null.
|
|
18960
19234
|
* A result that can't be converted to JSON (a cyclic object, a BigInt),
|
|
18961
19235
|
* which the dispatch's own check makes unlikely here, is a failure naming
|
|
18962
19236
|
* the tool and saying the call itself completed.
|
|
18963
19237
|
*/
|
|
18964
|
-
async scriptResultAnswer(result, toolName) {
|
|
19238
|
+
async scriptResultAnswer(result, toolName, toServer) {
|
|
18965
19239
|
const structured = Object.prototype.hasOwnProperty.call(result, 'structured');
|
|
18966
19240
|
const isError = result.isError === true;
|
|
18967
19241
|
// RFC-005 "eagerly lazy": a script receiving a result is the strongest
|
|
@@ -18978,7 +19252,10 @@ export class AgentFramework {
|
|
|
18978
19252
|
// which isn't fetched.
|
|
18979
19253
|
// Whenever the result's blocks reach the script: a structured failure's
|
|
18980
19254
|
// included, as a module's error can carry content.
|
|
18981
|
-
|
|
19255
|
+
// Not a server's result: a module's, whose blocks no dispatch has given
|
|
19256
|
+
// RFC-005's treatment.
|
|
19257
|
+
const fromModule = !toServer;
|
|
19258
|
+
if ((!isError || structured) && Array.isArray(result.data) && fromModule) {
|
|
18982
19259
|
const fetcher = this.getReferenceFetcher();
|
|
18983
19260
|
for (const block of result.data) {
|
|
18984
19261
|
const c = classifyBlock(block);
|
|
@@ -19007,6 +19284,21 @@ export class AgentFramework {
|
|
|
19007
19284
|
// no blocks reads as its error text.
|
|
19008
19285
|
if (structured && (!isError || (Array.isArray(result.data) && result.data.length > 0))) {
|
|
19009
19286
|
content = Array.isArray(result.data) ? result.data : [];
|
|
19287
|
+
// A module's blocks, such as an MCPL peer's it got from ctx.callTool
|
|
19288
|
+
// and returns whole, are read as history reads a module's data: what
|
|
19289
|
+
// RFC-005 governs, at any depth, as the script lane shows it, and
|
|
19290
|
+
// anything else as sent. What a script prints comes back to the model,
|
|
19291
|
+
// so no uri the model's lanes withhold reaches it. What this gives is
|
|
19292
|
+
// a stub, a notice or the value itself, so a module that returns
|
|
19293
|
+
// blocks it already gave has them left as they are.
|
|
19294
|
+
if (fromModule) {
|
|
19295
|
+
try {
|
|
19296
|
+
content = JSON.parse(JSON.stringify(content, (_key, value) => shownToScript(value)));
|
|
19297
|
+
}
|
|
19298
|
+
catch (err) {
|
|
19299
|
+
return failed(err);
|
|
19300
|
+
}
|
|
19301
|
+
}
|
|
19010
19302
|
}
|
|
19011
19303
|
else {
|
|
19012
19304
|
// As in history, a reference is its stub at any depth and inline data
|
|
@@ -19045,9 +19337,11 @@ export class AgentFramework {
|
|
|
19045
19337
|
* RFC-007: with an `origin`, observers see the call as they see a model
|
|
19046
19338
|
* call (register, dispatch with its refusal sites, open; the terminal at
|
|
19047
19339
|
* the waiter intercept), under the inner tool's own name and class.
|
|
19048
|
-
* `turnId` is the turn the call was made in (dispatchToolCall).
|
|
19340
|
+
* `turnId` is the turn the call was made in (dispatchToolCall). `route` is
|
|
19341
|
+
* the waiter's: the dispatch sets `toServer` when it sends the call to a
|
|
19342
|
+
* server.
|
|
19049
19343
|
*/
|
|
19050
|
-
dispatchScriptToolCall(agentName, toolName, args, origin, turnId) {
|
|
19344
|
+
dispatchScriptToolCall(agentName, toolName, args, origin, turnId, route = { toServer: false }) {
|
|
19051
19345
|
return new Promise((resolve) => {
|
|
19052
19346
|
const callId = `pytc-${randomUUID()}`;
|
|
19053
19347
|
// Host-side leak guard: the script's runner already gives the script a
|
|
@@ -19073,9 +19367,12 @@ export class AgentFramework {
|
|
|
19073
19367
|
}
|
|
19074
19368
|
}, safetyMs);
|
|
19075
19369
|
safety.unref?.();
|
|
19076
|
-
this.scriptToolWaiters.set(callId,
|
|
19077
|
-
|
|
19078
|
-
|
|
19370
|
+
this.scriptToolWaiters.set(callId, {
|
|
19371
|
+
resolve: (result) => {
|
|
19372
|
+
clearTimeout(safety);
|
|
19373
|
+
resolve(result);
|
|
19374
|
+
},
|
|
19375
|
+
route,
|
|
19079
19376
|
});
|
|
19080
19377
|
const call = { id: callId, name: toolName, input: args };
|
|
19081
19378
|
if (origin)
|
|
@@ -19606,6 +19903,11 @@ export class AgentFramework {
|
|
|
19606
19903
|
// Route MCPL tool calls to the appropriate server via prefix map
|
|
19607
19904
|
const mcplMatch = this.resolveMcplTool(enrichedCall.name);
|
|
19608
19905
|
if (mcplMatch && this.mcplServerRegistry) {
|
|
19906
|
+
// A script's call: its waiter keeps that it went to a server, for the
|
|
19907
|
+
// script lane's exit (scriptToolWaiters).
|
|
19908
|
+
const scriptWaiter = this.scriptToolWaiters.get(enrichedCall.id);
|
|
19909
|
+
if (scriptWaiter)
|
|
19910
|
+
scriptWaiter.route.toServer = true;
|
|
19609
19911
|
this.dispatchMcplToolCall(agentName, enrichedCall, mcplMatch[0], mcplMatch[1]);
|
|
19610
19912
|
return;
|
|
19611
19913
|
}
|
|
@@ -24615,7 +24917,9 @@ export class AgentFramework {
|
|
|
24615
24917
|
// generated JSON) stands in for what the server sent. An MCPL peer's
|
|
24616
24918
|
// content blocks are the exception: what a script prints comes back to
|
|
24617
24919
|
// the model, so each gets the treatment RFC-005 gives it on every lane
|
|
24618
|
-
// the model reads
|
|
24920
|
+
// the model reads, here, where the server that sent them is known. A
|
|
24921
|
+
// module's blocks get it at the script lane's exit (scriptResultAnswer).
|
|
24922
|
+
// A standard peer's blocks reach the script as sent.
|
|
24619
24923
|
const forScript = this.scriptToolWaiters?.has(call.id) ?? false;
|
|
24620
24924
|
const reading = async (result, mcpl) => {
|
|
24621
24925
|
if (!forScript || !Object.prototype.hasOwnProperty.call(result, 'structuredContent')) {
|