@animalabs/agent-framework 0.33.0 → 0.34.0
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 +136 -8
- package/dist/src/agent.d.ts +25 -4
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +68 -13
- package/dist/src/agent.js.map +1 -1
- package/dist/src/append-all.d.ts +5 -0
- package/dist/src/append-all.d.ts.map +1 -0
- package/dist/src/append-all.js +8 -0
- package/dist/src/append-all.js.map +1 -0
- package/dist/src/context-options.d.ts +9 -6
- package/dist/src/context-options.d.ts.map +1 -1
- package/dist/src/context-options.js +13 -8
- package/dist/src/context-options.js.map +1 -1
- package/dist/src/cut-off-round.js +4 -4
- package/dist/src/dendrite/registry.d.ts.map +1 -1
- package/dist/src/dendrite/registry.js +4 -2
- package/dist/src/dendrite/registry.js.map +1 -1
- package/dist/src/failure-text.d.ts +36 -0
- package/dist/src/failure-text.d.ts.map +1 -0
- package/dist/src/failure-text.js +89 -0
- package/dist/src/failure-text.js.map +1 -0
- package/dist/src/framework.d.ts +94 -30
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +987 -284
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +40 -6
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +66 -25
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/reply-judge.d.ts +4 -0
- package/dist/src/gate/reply-judge.d.ts.map +1 -1
- package/dist/src/gate/reply-judge.js +12 -3
- package/dist/src/gate/reply-judge.js.map +1 -1
- package/dist/src/gate/suppression-review.js +2 -2
- package/dist/src/gate/suppression-review.js.map +1 -1
- package/dist/src/gate/types.d.ts +7 -0
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/heartbeat/heartbeat.js +1 -1
- package/dist/src/heartbeat/heartbeat.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/inference-bounds.d.ts +2 -1
- package/dist/src/inference-bounds.d.ts.map +1 -1
- package/dist/src/inference-bounds.js +9 -6
- package/dist/src/inference-bounds.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +57 -13
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +136 -24
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts +109 -15
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
- package/dist/src/mcpl/modern-connection.js +593 -86
- package/dist/src/mcpl/modern-connection.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts +3 -1
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +15 -9
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +37 -18
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +31 -16
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
- package/dist/src/mcpl/tool-presence.js +10 -6
- package/dist/src/mcpl/tool-presence.js.map +1 -1
- package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
- package/dist/src/mcpl/toolset-host.js +7 -6
- package/dist/src/mcpl/toolset-host.js.map +1 -1
- package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
- package/dist/src/mcpl/toolsets-module.js +2 -1
- package/dist/src/mcpl/toolsets-module.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +25 -4
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/modules/discord/index.js +1 -1
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/health/index.js +2 -2
- package/dist/src/modules/health/index.js.map +1 -1
- package/dist/src/modules/history/semantic.js +3 -3
- package/dist/src/modules/history/semantic.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +4 -3
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +2 -1
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +3 -2
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/notice-tags.d.ts +10 -8
- package/dist/src/notice-tags.d.ts.map +1 -1
- package/dist/src/notice-tags.js +10 -8
- package/dist/src/notice-tags.js.map +1 -1
- package/dist/src/presentation.d.ts +2 -1
- package/dist/src/presentation.d.ts.map +1 -1
- package/dist/src/presentation.js +4 -2
- package/dist/src/presentation.js.map +1 -1
- package/dist/src/reasoning-loss.d.ts +5 -0
- package/dist/src/reasoning-loss.d.ts.map +1 -0
- package/dist/src/reasoning-loss.js +8 -0
- package/dist/src/reasoning-loss.js.map +1 -0
- package/dist/src/recovery/offline-branch.d.ts.map +1 -1
- package/dist/src/recovery/offline-branch.js +5 -4
- package/dist/src/recovery/offline-branch.js.map +1 -1
- package/dist/src/recovery/recover-cli.js +2 -1
- package/dist/src/recovery/recover-cli.js.map +1 -1
- package/dist/src/sent-rows.js +1 -1
- package/dist/src/supplied-text.d.ts +1 -1
- package/dist/src/supplied-text.js +3 -3
- package/dist/src/supplied-text.js.map +1 -1
- package/dist/src/thinking-drops.d.ts +2 -0
- package/dist/src/thinking-drops.d.ts.map +1 -1
- package/dist/src/thinking-drops.js.map +1 -1
- package/dist/src/tool-presentation.d.ts.map +1 -1
- package/dist/src/tool-presentation.js +38 -9
- package/dist/src/tool-presentation.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +13 -4
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +33 -16
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/tune-out/tools.js +2 -1
- package/dist/src/tune-out/tools.js.map +1 -1
- package/dist/src/types/agent.d.ts +39 -5
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +43 -8
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/inference-log.d.ts +1 -0
- package/dist/src/types/inference-log.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +64 -4
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agent.ts +71 -14
- package/src/append-all.ts +6 -0
- package/src/context-options.ts +18 -8
- package/src/cut-off-round.ts +4 -4
- package/src/dendrite/registry.ts +4 -2
- package/src/failure-text.ts +88 -0
- package/src/framework.ts +1018 -293
- package/src/gate/event-gate.ts +90 -26
- package/src/gate/reply-judge.ts +12 -3
- package/src/gate/suppression-review.ts +2 -2
- package/src/gate/types.ts +7 -0
- package/src/heartbeat/heartbeat.ts +1 -1
- package/src/index.ts +1 -0
- package/src/inference-bounds.ts +11 -6
- package/src/mcpl/channel-registry.ts +160 -31
- package/src/mcpl/modern-connection.ts +629 -91
- package/src/mcpl/protocol-family.ts +15 -9
- package/src/mcpl/server-connection.ts +28 -15
- package/src/mcpl/tool-presence.ts +10 -5
- package/src/mcpl/toolset-host.ts +7 -6
- package/src/mcpl/toolsets-module.ts +2 -1
- package/src/mcpl/types.ts +25 -4
- package/src/modules/discord/index.ts +1 -1
- package/src/modules/health/index.ts +2 -2
- package/src/modules/history/semantic.ts +3 -3
- package/src/modules/workspace/index.ts +4 -3
- package/src/modules/workspace/observe.ts +2 -1
- package/src/modules/workspace/reconcile.ts +3 -2
- package/src/notice-tags.ts +11 -8
- package/src/presentation.ts +4 -2
- package/src/reasoning-loss.ts +7 -0
- package/src/recovery/offline-branch.ts +5 -4
- package/src/recovery/recover-cli.ts +2 -1
- package/src/sent-rows.ts +1 -1
- package/src/supplied-text.ts +3 -3
- package/src/thinking-drops.ts +2 -0
- package/src/tool-presentation.ts +34 -9
- package/src/tune-out/coordinator.ts +32 -15
- package/src/tune-out/tools.ts +2 -1
- package/src/types/agent.ts +36 -5
- package/src/types/framework.ts +44 -8
- package/src/types/inference-log.ts +1 -0
- package/src/types/trace.ts +66 -4
package/src/framework.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isReasoningLoss } from './reasoning-loss.js';
|
|
2
|
+
export { residentViewFilter, subconsciousViewFilter } from './context-options.js';
|
|
1
3
|
import { ToolPresentation, presentationTools, isPresentationTool, renderCatalogue, type PresentationSnapshot } from "./tool-presentation.js";
|
|
2
4
|
import { presentAuthor } from './author-presentation.js';
|
|
3
5
|
import { constants as bufferConstants } from 'node:buffer';
|
|
@@ -13,7 +15,8 @@ import type { Membrane, ContentBlock, NormalizedRequest, NormalizedResponse, Yie
|
|
|
13
15
|
import { MembraneError } from '@animalabs/membrane';
|
|
14
16
|
import { classifyFailure, retryDecision, MEMBRANE_RETRY_SETTINGS, type RetrySettings } from '@animalabs/membrane/retry';
|
|
15
17
|
import { deadlineCut, isDeadlineExpiry, resolveInferenceBounds, wholeCallMs, type InferenceBounds } from './inference-bounds.js';
|
|
16
|
-
import {
|
|
18
|
+
import { boundFailureText, compileRefusalType, failureWords, FAILURE_TEXT_MAX_CHARS, houseReason, isMembraneError, PROVIDER_ERROR_CODE_MAX_CHARS } from './failure-text.js';
|
|
19
|
+
import { ContextManager, PassthroughStrategy, WindowedPassthroughStrategy, stripUnpairedToolBlocks, continuesBody, MalformedContentError, malformedContentPlaces, CompressionWithdrawn } from '@animalabs/context-manager';
|
|
17
20
|
// Not named imports from context-manager: 0.24.0 has neither, and a named
|
|
18
21
|
// import of either would fail this package's load there before create()
|
|
19
22
|
// could name the release it needs.
|
|
@@ -229,6 +232,7 @@ import { AgentPresentation, asCompiled, bearingOf, heldFromResidents, suppressed
|
|
|
229
232
|
import { coverTest, covers, readMarkThrough, type MarkedRow, type ReadMark } from './read-marks.js';
|
|
230
233
|
import { contentAsStored, nameAsStored } from './text-as-stored.js';
|
|
231
234
|
import { STORED_INJECTION_KEY, STORED_INJECTION_KIND, asStoredRow, hasRenderedContent, isAnotherAgentsInjection, isOwnInjection, isStoredKind, storedInjectionMarkOf, unitHash, type StoredInjectionMark } from './stored-injections.js';
|
|
235
|
+
import { appendAll } from './append-all.js';
|
|
232
236
|
|
|
233
237
|
/** An MCPL tool call refused because its server isn't in the registry: one
|
|
234
238
|
* closed for good, one removed, or one whose connect failed without
|
|
@@ -1295,18 +1299,29 @@ const TURN_RETRY: unique symbol = Symbol('turn-retry');
|
|
|
1295
1299
|
*/
|
|
1296
1300
|
const MERGED_BATCH: unique symbol = Symbol('merged-batch');
|
|
1297
1301
|
const REARMED: unique symbol = Symbol('rearmed');
|
|
1302
|
+
const UNPROMPTED: unique symbol = Symbol('unprompted');
|
|
1298
1303
|
const TURN_CARRY: unique symbol = Symbol('turn-carry');
|
|
1304
|
+
const BUDGET_WAIT: unique symbol = Symbol('budget-wait');
|
|
1305
|
+
interface BudgetWait {
|
|
1306
|
+
since: number;
|
|
1307
|
+
row?: string;
|
|
1308
|
+
alerted?: true;
|
|
1309
|
+
}
|
|
1299
1310
|
interface TurnCarry {
|
|
1300
1311
|
silence: TurnSilence | null;
|
|
1301
1312
|
sends: SendsReached | null;
|
|
1302
1313
|
read: number;
|
|
1303
1314
|
}
|
|
1304
|
-
type MarkedRequest = InferenceRequest & { [TURN_RETRY]?: true; [MERGED_BATCH]?: InferenceRequest[]; [TURN_CARRY]?: TurnCarry; [REARMED]?: true };
|
|
1315
|
+
type MarkedRequest = InferenceRequest & { [TURN_RETRY]?: true; [MERGED_BATCH]?: InferenceRequest[]; [TURN_CARRY]?: TurnCarry; [REARMED]?: true; [UNPROMPTED]?: true; [BUDGET_WAIT]?: BudgetWait };
|
|
1305
1316
|
|
|
1306
1317
|
function isRearmed(request: InferenceRequest | undefined): boolean {
|
|
1307
1318
|
return request !== undefined && (request as MarkedRequest)[REARMED] === true;
|
|
1308
1319
|
}
|
|
1309
1320
|
|
|
1321
|
+
function isUnprompted(request: InferenceRequest | undefined): boolean {
|
|
1322
|
+
return request !== undefined && (request as MarkedRequest)[UNPROMPTED] === true;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1310
1325
|
interface StatedWaitRun { since: number; holds: number; alerted: boolean; model: string; reason: string; heldUntil: number }
|
|
1311
1326
|
|
|
1312
1327
|
function waitAlertsOnItsOwn(wait: ProviderWait): boolean {
|
|
@@ -1362,6 +1377,10 @@ function continuesTurn(request: InferenceRequest): boolean {
|
|
|
1362
1377
|
return resumesLogicalTurn(request.reason) || isTurnRetry(request);
|
|
1363
1378
|
}
|
|
1364
1379
|
|
|
1380
|
+
function budgetWaitOf(request: InferenceRequest | undefined): BudgetWait | undefined {
|
|
1381
|
+
return request ? (request as MarkedRequest)[BUDGET_WAIT] : undefined;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1365
1384
|
function turnCarryOf(request: InferenceRequest | undefined): TurnCarry | undefined {
|
|
1366
1385
|
return request && continuesTurn(request) ? (request as MarkedRequest)[TURN_CARRY] : undefined;
|
|
1367
1386
|
}
|
|
@@ -1459,6 +1478,8 @@ interface TurnRouting {
|
|
|
1459
1478
|
* wake, so a heartbeat can't start a loop of turns.
|
|
1460
1479
|
*/
|
|
1461
1480
|
heartbeat?: true;
|
|
1481
|
+
unprompted?: true;
|
|
1482
|
+
wakeChannels?: ChannelRef[];
|
|
1462
1483
|
/**
|
|
1463
1484
|
* Locked mode (AgentConfig.proseRouting 'locked'): the turn's locus is the
|
|
1464
1485
|
* agent's prose lock, taken when the turn opens, and only prose_locus moves
|
|
@@ -1481,7 +1502,8 @@ interface TurnRouting {
|
|
|
1481
1502
|
* lands. Locked mode's is the agent's lock (TurnRouting.locked); the rest
|
|
1482
1503
|
* of this describes locus and hybrid mode.
|
|
1483
1504
|
* Frozen when the turn opens (home channel for a fork, else the trigger
|
|
1484
|
-
* channel, else the global default
|
|
1505
|
+
* channel, else the global default, with the configured unprompted channel
|
|
1506
|
+
* in it where the turn takes it), so ambient chatter, reactions and
|
|
1485
1507
|
* system markers can't move it (2026-07-21 Cairn lounge misroute). Since
|
|
1486
1508
|
* 2026-07-31 a re-pin moves it (repinTurnLocus): an addressed message, or a
|
|
1487
1509
|
* follow-up in a channel the turn engaged. The agent's own channel_open
|
|
@@ -1525,6 +1547,7 @@ interface TurnRouting {
|
|
|
1525
1547
|
/** The turn asked for a ` !` continuation; taken once. */
|
|
1526
1548
|
continuation: boolean;
|
|
1527
1549
|
start?: { branch: string; sequence: number };
|
|
1550
|
+
firstRow?: string;
|
|
1528
1551
|
roundsRan?: true;
|
|
1529
1552
|
unflushedRounds?: number;
|
|
1530
1553
|
unpostedSpeech?: true;
|
|
@@ -1868,15 +1891,20 @@ const RETRY_CANCELLED_BY_STOP = 'a retry waiting while the framework was stoppin
|
|
|
1868
1891
|
* was dropped before it started, other than by an abandon or a stop. */
|
|
1869
1892
|
const CONTINUATION_DROPPED = "a turn's continuation was dropped before it started";
|
|
1870
1893
|
|
|
1894
|
+
const BUDGET_HELD_TURN_REMOVED = 'a turn that waited for compression was dropped: what it stood on was removed';
|
|
1895
|
+
|
|
1871
1896
|
/** One frame's window while it holds a turn with no stream and one of its
|
|
1872
1897
|
* checks is ahead (AgentFramework.checkAhead). `abandoned` is an operator's
|
|
1873
1898
|
* quiesce abandon that reached the turn there, which the frame's check
|
|
1874
1899
|
* collects; it goes with the window. `wake`, set while the frame waits to
|
|
1875
1900
|
* retry, ends that wait, so the check after it comes at once; called once
|
|
1876
|
-
* the wait is over, it does nothing.
|
|
1901
|
+
* the wait is over, it does nothing. `gateReleased` is set when a budget
|
|
1902
|
+
* hold gave back the frame's provider admission as its window closed, so
|
|
1903
|
+
* the frame's end doesn't give it back again. */
|
|
1877
1904
|
interface CheckWindow {
|
|
1878
1905
|
abandoned: boolean;
|
|
1879
1906
|
wake?: () => void;
|
|
1907
|
+
gateReleased?: true;
|
|
1880
1908
|
}
|
|
1881
1909
|
|
|
1882
1910
|
function isTurnContinuation(reason: string): boolean {
|
|
@@ -1905,6 +1933,10 @@ function hasOwnCauseForRun(r: InferenceRequest): boolean {
|
|
|
1905
1933
|
return !FRAMEWORK_WAKE_SOURCES.has(r.source) && r.suppressProse !== true;
|
|
1906
1934
|
}
|
|
1907
1935
|
|
|
1936
|
+
function bringsOwnChannels(r: InferenceRequest): boolean {
|
|
1937
|
+
return hasOwnCauseForRun(r) && (r.wakeChannels?.length ?? 0) > 0;
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1908
1940
|
/** Whether a wake carries an autoContinue continuation: its own wake, or a
|
|
1909
1941
|
* requeued turn trigger that kept the mark (InferenceRequest.autoContinueRun). */
|
|
1910
1942
|
function carriesContinuation(r: InferenceRequest): boolean {
|
|
@@ -1952,6 +1984,11 @@ interface TurnCheckpoint {
|
|
|
1952
1984
|
windowBranch?: string;
|
|
1953
1985
|
}
|
|
1954
1986
|
|
|
1987
|
+
interface RecordedTurnCheckpoint {
|
|
1988
|
+
turnIndex: number;
|
|
1989
|
+
evicted: TurnCheckpoint[];
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1955
1992
|
/** What a report of writes kept in memory says of them: how many of each kind (`metadata.kind`, else `message`), how many are mail, and the sentences that say so. */
|
|
1956
1993
|
function heldWritesTold(writes: readonly SurgeryHeldWrite[]): { kinds: Record<string, number>; mail: number; told: string } {
|
|
1957
1994
|
const kinds: Record<string, number> = {};
|
|
@@ -2094,6 +2131,12 @@ function assertFrameworkSwitches(config: FrameworkConfig): void {
|
|
|
2094
2131
|
'and false or leaving it out doesn\'t',
|
|
2095
2132
|
);
|
|
2096
2133
|
}
|
|
2134
|
+
const unprompted: unknown = config.unpromptedChannel;
|
|
2135
|
+
if (unprompted !== undefined && unprompted !== null && (typeof unprompted !== 'string' || unprompted.trim() === '')) {
|
|
2136
|
+
throw new Error(
|
|
2137
|
+
`unpromptedChannel must name a channel, such as "discord:<guildId>:<channelId>", not ${typeof unprompted === 'string' ? 'a blank string' : kindOf(unprompted)}`,
|
|
2138
|
+
);
|
|
2139
|
+
}
|
|
2097
2140
|
// The heartbeat: true or an object turns it on; false, null or leaving it
|
|
2098
2141
|
// out leaves it off. Any other value (a "false" read from text, a 0) is
|
|
2099
2142
|
// refused rather than read as on.
|
|
@@ -2513,11 +2556,14 @@ export interface HostModeStatus {
|
|
|
2513
2556
|
* gate while quiesced, or, while serving, until the scheduler's next pass
|
|
2514
2557
|
* takes them), in a scheduler pass's hands, or held by a provider hold (an
|
|
2515
2558
|
* acceleration or host hold, or a provider's stated wait). There, a start
|
|
2516
|
-
* merged from several requests counts as those it took in. A start parked
|
|
2517
|
-
*
|
|
2518
|
-
* `parkedAdmissions` instead, however many wakes are merged into it.
|
|
2519
|
-
*
|
|
2520
|
-
*
|
|
2559
|
+
* merged from several requests counts as those it took in. A start parked on
|
|
2560
|
+
* provider admission behind an auxiliary call counts once in
|
|
2561
|
+
* `parkedAdmissions` instead, however many wakes are merged into it. A turn
|
|
2562
|
+
* a budget hold holds counts once, continuation or not. The wakes merged
|
|
2563
|
+
* into its start are part of it. Wakes that joined it while it waited count
|
|
2564
|
+
* as other gated requests do. Other continuations (budget restart, tool
|
|
2565
|
+
* results, the tool-result guard's retry) and a held turn's retry, which a
|
|
2566
|
+
* quiesce lets through, are never counted. */
|
|
2521
2567
|
gatedRequests: number;
|
|
2522
2568
|
/** code_execution scripts still running that no call is waiting on: left
|
|
2523
2569
|
* running after a yield, outside any turn. Quiesce does NOT stop them —
|
|
@@ -2597,8 +2643,9 @@ export class ProviderWaitNotSleptError extends Error {
|
|
|
2597
2643
|
constructor(
|
|
2598
2644
|
message: string,
|
|
2599
2645
|
readonly retryAfterMs: number,
|
|
2646
|
+
options?: ErrorOptions,
|
|
2600
2647
|
) {
|
|
2601
|
-
super(message);
|
|
2648
|
+
super(message, options);
|
|
2602
2649
|
this.name = 'ProviderWaitNotSleptError';
|
|
2603
2650
|
}
|
|
2604
2651
|
}
|
|
@@ -2892,7 +2939,7 @@ export class DefaultErrorPolicy implements ErrorPolicy {
|
|
|
2892
2939
|
|
|
2893
2940
|
constructor(settings: RetrySettings = TURN_RETRY_SETTINGS) {
|
|
2894
2941
|
this.settings = settings;
|
|
2895
|
-
this.maxRetries =
|
|
2942
|
+
this.maxRetries = Object.values(settings).reduce((max, s) => Math.max(max, (s?.maxAttempts ?? 1) - 1), 0);
|
|
2896
2943
|
}
|
|
2897
2944
|
|
|
2898
2945
|
onInferenceError(error: Error, _agentName: string, attempt: number): ErrorAction {
|
|
@@ -3062,6 +3109,33 @@ const TURN_RETRY_SETTINGS: RetrySettings = {
|
|
|
3062
3109
|
deadline: { maxAttempts: DEADLINE_TURN_TRIES, firstDelayMs: 1_000, multiplier: 1, maxDelayMs: 1_000, statedWaitCeilingMs: null },
|
|
3063
3110
|
} as RetrySettings;
|
|
3064
3111
|
const PROVIDER_ACCELERATION_JITTER_MS = 5_000;
|
|
3112
|
+
const BUDGET_HOLD_NOTE_NAMESPACE = 'framework:budget-hold';
|
|
3113
|
+
type BudgetHoldOutcome = 'released' | 'stopped' | 'removed';
|
|
3114
|
+
function describeWait(ms: number): string {
|
|
3115
|
+
return ms < 60_000 ? `${Math.max(1, Math.round(ms / 1000))} s` : `${Math.round(ms / 60_000)} min`;
|
|
3116
|
+
}
|
|
3117
|
+
interface BudgetHoldBaseline {
|
|
3118
|
+
progress: string | undefined;
|
|
3119
|
+
settings: number;
|
|
3120
|
+
}
|
|
3121
|
+
|
|
3122
|
+
interface BudgetHold {
|
|
3123
|
+
startedAt: number;
|
|
3124
|
+
wait: BudgetWait;
|
|
3125
|
+
turn: InferenceRequest;
|
|
3126
|
+
standsOn: string[];
|
|
3127
|
+
wakes: InferenceRequest[];
|
|
3128
|
+
errorType: 'over_budget' | 'context_refusal';
|
|
3129
|
+
reason: string;
|
|
3130
|
+
budget: number | undefined;
|
|
3131
|
+
actual: number | undefined;
|
|
3132
|
+
recheck?: ReturnType<typeof setInterval>;
|
|
3133
|
+
managerCalls: number;
|
|
3134
|
+
progressAtHold: string | undefined;
|
|
3135
|
+
settingsAtHold: number;
|
|
3136
|
+
checkQueued?: boolean;
|
|
3137
|
+
drainKicked?: boolean;
|
|
3138
|
+
}
|
|
3065
3139
|
/** Longest delay one setTimeout can represent (2^31-1 ms, about 24.8 days);
|
|
3066
3140
|
* a longer timer fires almost at once, so longer waits re-arm in steps. */
|
|
3067
3141
|
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
@@ -3077,17 +3151,6 @@ const WAITING_TRACE_EVERY_MS = 60_000;
|
|
|
3077
3151
|
* waits in a row hold one turn longer than this (noteStatedWaitRun,
|
|
3078
3152
|
* endStatedWaitRun). */
|
|
3079
3153
|
const PROVIDER_WAIT_ALERT_MS = 60 * 60_000;
|
|
3080
|
-
/**
|
|
3081
|
-
* A MembraneError, from this framework's copy of membrane or from another: the
|
|
3082
|
-
* host supplies the Membrane instance (config.membrane), so with two copies
|
|
3083
|
-
* installed its errors are instances of a class this framework's `instanceof`
|
|
3084
|
-
* doesn't match. Recognised by name as well, as context-manager's errors are;
|
|
3085
|
-
* its fields are the same, so callers still check each one's type.
|
|
3086
|
-
*/
|
|
3087
|
-
function isMembraneError(error: unknown): error is MembraneError {
|
|
3088
|
-
return error instanceof MembraneError || (error instanceof Error && error.name === 'MembraneError');
|
|
3089
|
-
}
|
|
3090
|
-
|
|
3091
3154
|
/**
|
|
3092
3155
|
* agent-framework#734. Membrane's own streams send a retryable failure as an
|
|
3093
3156
|
* `error` event and send nothing after their terminal event. A stream of
|
|
@@ -3595,44 +3658,16 @@ const SCRIPT_TEXT_ANSWER_ENVELOPE_BYTES = Buffer.byteLength(
|
|
|
3595
3658
|
const scriptResultMaxChars = (memoryMaxBytes: number): number =>
|
|
3596
3659
|
Math.max(0, Math.floor((Math.min(maxResultBytes(memoryMaxBytes), bufferConstants.MAX_STRING_LENGTH) - SCRIPT_TEXT_ANSWER_ENVELOPE_BYTES) / 6));
|
|
3597
3660
|
|
|
3598
|
-
/**
|
|
3599
|
-
*
|
|
3600
|
-
* reason in the exhaustion stderr line, a field of a logs/failures.log record,
|
|
3601
|
-
* an ops alert's message, the inference log's `error`, and a trace event's
|
|
3602
|
-
* `error` and `stack`. A failure's text is whatever its source put in
|
|
3603
|
-
* `Error.message`, and a provider can echo the entire rejected request there
|
|
3604
|
-
* (one OpenAI-compatible 400 echoed ~1.7 MB). The sinks with shorter bounds
|
|
3605
|
-
* name them below, and every cut goes through boundFailureText. This bound
|
|
3606
|
-
* doesn't depend on membrane bounding its own errors.
|
|
3607
|
-
*/
|
|
3608
|
-
const FAILURE_TEXT_MAX_CHARS = 2_000;
|
|
3609
|
-
/** Longest failure reason written into an agent's own context (the
|
|
3610
|
-
* `[inference-failed]` marker): enough for a provider's explanation, not a
|
|
3611
|
-
* self-dump the agent then carries in every later request. */
|
|
3661
|
+
/** Longest failure reason in the `[inference-failed]` marker: the reason its
|
|
3662
|
+
* text gives, and the excerpt its metadata keeps. */
|
|
3612
3663
|
const FAILURE_MARKER_REASON_MAX_CHARS = 600;
|
|
3613
|
-
/** Longest failure reason
|
|
3614
|
-
* enough to identify the failure class without a wall of provider JSON. */
|
|
3664
|
+
/** Longest failure reason the rewind marker's metadata keeps as its category. */
|
|
3615
3665
|
const REWIND_MARKER_REASON_MAX_CHARS = 160;
|
|
3616
3666
|
/** Longest failure reason in an agent's status (`lastInference.lastError`) and
|
|
3617
3667
|
* in the compile-refused ops alert. */
|
|
3618
3668
|
const FAILURE_SUMMARY_MAX_CHARS = 300;
|
|
3619
3669
|
/** Longest alert message posted to the ops webhook. */
|
|
3620
3670
|
const OPS_WEBHOOK_MESSAGE_MAX_CHARS = 500;
|
|
3621
|
-
/** Longest provider error code recorded verbatim; real codes are short tokens.
|
|
3622
|
-
* A failure's name and type are bounded the same way. */
|
|
3623
|
-
const PROVIDER_ERROR_CODE_MAX_CHARS = 128;
|
|
3624
|
-
|
|
3625
|
-
const isHighSurrogate = (text: string, index: number): boolean => {
|
|
3626
|
-
const code = text.charCodeAt(index);
|
|
3627
|
-
return code >= 0xd800 && code <= 0xdbff;
|
|
3628
|
-
};
|
|
3629
|
-
const isLowSurrogate = (text: string, index: number): boolean => {
|
|
3630
|
-
const code = text.charCodeAt(index);
|
|
3631
|
-
return code >= 0xdc00 && code <= 0xdfff;
|
|
3632
|
-
};
|
|
3633
|
-
/** Whether a cut before `index` would separate the halves of a surrogate pair. */
|
|
3634
|
-
const splitsPair = (text: string, index: number): boolean =>
|
|
3635
|
-
index > 0 && index < text.length && isHighSurrogate(text, index - 1) && isLowSurrogate(text, index);
|
|
3636
3671
|
|
|
3637
3672
|
/**
|
|
3638
3673
|
* Whether an MCPL server's child ended the way a server ends when it's asked
|
|
@@ -3659,26 +3694,6 @@ function closeWords(reason: string | null | undefined, code: number | null | und
|
|
|
3659
3694
|
return how.length > 0 ? ` (${how.join(', ')})` : '';
|
|
3660
3695
|
}
|
|
3661
3696
|
|
|
3662
|
-
/**
|
|
3663
|
-
* `text` when it fits within `max` characters; otherwise its head and tail
|
|
3664
|
-
* around a marker stating how many of how many characters were omitted, the
|
|
3665
|
-
* whole within `max`. A cut that would separate a surrogate pair moves toward
|
|
3666
|
-
* omission, so the result never holds half of a pair the text held whole, and
|
|
3667
|
-
* bounding bounded text returns it as is. Each sink cuts the whole text, so
|
|
3668
|
-
* its marker states the whole length.
|
|
3669
|
-
*/
|
|
3670
|
-
function boundFailureText(text: string, max: number = FAILURE_TEXT_MAX_CHARS): string {
|
|
3671
|
-
if (text.length <= max) return text;
|
|
3672
|
-
const markerLength = ` …[${text.length} of ${text.length} characters omitted]… `.length;
|
|
3673
|
-
const budget = Math.max(0, max - markerLength);
|
|
3674
|
-
const tailLength = Math.floor(budget / 4);
|
|
3675
|
-
let headEnd = budget - tailLength;
|
|
3676
|
-
if (splitsPair(text, headEnd)) headEnd--;
|
|
3677
|
-
let tailStart = text.length - tailLength;
|
|
3678
|
-
if (splitsPair(text, tailStart)) tailStart++;
|
|
3679
|
-
return `${text.slice(0, headEnd)} …[${tailStart - headEnd} of ${text.length} characters omitted]… ${text.slice(tailStart)}`;
|
|
3680
|
-
}
|
|
3681
|
-
|
|
3682
3697
|
export function opsWebhookText(message: string): string {
|
|
3683
3698
|
return boundFailureText(message, OPS_WEBHOOK_MESSAGE_MAX_CHARS);
|
|
3684
3699
|
}
|
|
@@ -3751,6 +3766,33 @@ function failureShown(text: string): string {
|
|
|
3751
3766
|
return text === UNDESCRIBED_FAILURE ? text : suppliedValue(text);
|
|
3752
3767
|
}
|
|
3753
3768
|
|
|
3769
|
+
function failedEntry(prefix: string, err: Error): { error: string; errorWords: string } {
|
|
3770
|
+
return { error: `${prefix}${err.message}`, errorWords: `${prefix}${failureWords(err)}` };
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3773
|
+
function shownReasonOf(err: Error): { shownReason?: string } {
|
|
3774
|
+
const own = houseReason(err);
|
|
3775
|
+
return own === undefined ? {} : { shownReason: own };
|
|
3776
|
+
}
|
|
3777
|
+
|
|
3778
|
+
function boundedCause(err: Error): Error {
|
|
3779
|
+
const cause = new Error(boundFailureText(String(err.message)));
|
|
3780
|
+
cause.name = boundFailureText(String(err.name), PROVIDER_ERROR_CODE_MAX_CHARS);
|
|
3781
|
+
cause.stack = boundFailureText(String(err.stack ?? `${cause.name}: ${cause.message}`));
|
|
3782
|
+
if (isMembraneError(err)) {
|
|
3783
|
+
const fields = err as Error & { type?: unknown; retryable?: unknown; httpStatus?: unknown; providerErrorCode?: unknown; retryAfterMs?: unknown };
|
|
3784
|
+
const code = boundProviderErrorCode(fields.providerErrorCode);
|
|
3785
|
+
Object.assign(cause, {
|
|
3786
|
+
...(typeof fields.type === 'string' ? { type: boundFailureText(fields.type, PROVIDER_ERROR_CODE_MAX_CHARS) } : {}),
|
|
3787
|
+
...(typeof fields.retryable === 'boolean' ? { retryable: fields.retryable } : {}),
|
|
3788
|
+
...(typeof fields.httpStatus === 'number' ? { httpStatus: fields.httpStatus } : {}),
|
|
3789
|
+
...(code !== undefined ? { providerErrorCode: code } : {}),
|
|
3790
|
+
...(typeof fields.retryAfterMs === 'number' ? { retryAfterMs: fields.retryAfterMs } : {}),
|
|
3791
|
+
});
|
|
3792
|
+
}
|
|
3793
|
+
return cause;
|
|
3794
|
+
}
|
|
3795
|
+
|
|
3754
3796
|
/**
|
|
3755
3797
|
* A bounded string describing a failure, for logs that would otherwise print
|
|
3756
3798
|
* the error object; it is one line unless one of its parts has newlines, as a
|
|
@@ -3949,7 +3991,8 @@ type FrameworkCancelKind = 'turn_ended' | 'budget_restart' | 'quiesce_abandoned'
|
|
|
3949
3991
|
* start; any other drop ends it as a start dropped before it sends anything.
|
|
3950
3992
|
* `why` is said in the line its end logs. */
|
|
3951
3993
|
type ContinuationDrop =
|
|
3952
|
-
| { kind: 'quiesce_abandoned' | 'shutdown'
|
|
3994
|
+
| { kind: 'quiesce_abandoned' | 'shutdown'; why: string }
|
|
3995
|
+
| { kind: 'dropped'; why: string; reason?: string }
|
|
3953
3996
|
/** A caller's stop (abortInference) of a turn whose continuation or retry
|
|
3954
3997
|
* is on its way, with the reason it gave. */
|
|
3955
3998
|
| { kind: 'caller'; reason?: string; why: string };
|
|
@@ -4128,6 +4171,14 @@ function mergeWakeLines(requests: readonly InferenceRequest[]): WakeLine[] | und
|
|
|
4128
4171
|
return byEntry.size > 0 ? [...byEntry.values()] : undefined;
|
|
4129
4172
|
}
|
|
4130
4173
|
|
|
4174
|
+
function wakeChannelsOf(requests: readonly ({ wakeChannels?: ChannelRef[] } | undefined)[]): ChannelRef[] | undefined {
|
|
4175
|
+
const channels: ChannelRef[] = [];
|
|
4176
|
+
for (const channel of requests.flatMap((r) => r?.wakeChannels ?? [])) {
|
|
4177
|
+
if (!channels.some((c) => sameChannel(c, channel))) channels.push(channel);
|
|
4178
|
+
}
|
|
4179
|
+
return channels.length > 0 ? channels : undefined;
|
|
4180
|
+
}
|
|
4181
|
+
|
|
4131
4182
|
/** A batched-wake line's identity: its event's stored entry. */
|
|
4132
4183
|
function wakeLineKey(line: WakeLine): string {
|
|
4133
4184
|
const { eventType, serverId, channelId, messageId, eventId } = line.entry;
|
|
@@ -4299,6 +4350,9 @@ export class AgentFramework {
|
|
|
4299
4350
|
private providerAccelerationCooldowns: Map<string, ProviderAccelerationCooldown> = new Map();
|
|
4300
4351
|
private providerAccelerationRecoveries: Map<string, ProviderAccelerationRecovery> = new Map();
|
|
4301
4352
|
private providerAccelerationLastRecovery: Map<string, ProviderAccelerationReceipt> = new Map();
|
|
4353
|
+
private budgetHolds: Map<string, BudgetHold> = new Map();
|
|
4354
|
+
private overBudgetDrainAfter: Map<string, Array<() => void>> = new Map();
|
|
4355
|
+
private budgetHoldRecheckMs = DEFAULT_MAINTENANCE_INTERVAL_MS;
|
|
4302
4356
|
private providerAccelerationDefaultCooldownMs = PROVIDER_ACCELERATION_DEFAULT_COOLDOWN_MS;
|
|
4303
4357
|
private providerAccelerationJitterMs = PROVIDER_ACCELERATION_JITTER_MS;
|
|
4304
4358
|
private inferenceBounds: InferenceBounds | null = resolveInferenceBounds(undefined);
|
|
@@ -4828,11 +4882,11 @@ export class AgentFramework {
|
|
|
4828
4882
|
private readonly managerReleases = new WeakMap<Agent, ManagerRelease>();
|
|
4829
4883
|
private readonly releasedByCleanup = new WeakSet<Agent>();
|
|
4830
4884
|
private readonly resumesPending = new Map<string, Agent | undefined>();
|
|
4831
|
-
// context-manager 0.
|
|
4832
|
-
// AutobiographicalStrategy does (hostContext, autobiographical.ts:
|
|
4885
|
+
// context-manager 0.25.1: a strategy that keeps its context, as
|
|
4886
|
+
// AutobiographicalStrategy does (hostContext, autobiographical.ts:3156),
|
|
4833
4887
|
// keeps its manager reachable while its work is pending, since the
|
|
4834
4888
|
// context's tools and systemPrompt getters close over the manager
|
|
4835
|
-
// (context-manager.ts:
|
|
4889
|
+
// (context-manager.ts:4602-4621). So such a released manager is collected
|
|
4836
4890
|
// only once nothing of its work can still read the store, and stop()
|
|
4837
4891
|
// closes, before an owned store, each one alive.
|
|
4838
4892
|
private readonly openManagers = new Set<WeakRef<ContextManager>>();
|
|
@@ -4958,7 +5012,8 @@ export class AgentFramework {
|
|
|
4958
5012
|
private toolPatternKnownPrefixes: Map<string, string> = new Map();
|
|
4959
5013
|
/** Maps serverId → McplServerConfig for prefix lookup. */
|
|
4960
5014
|
private mcplServerConfigs: Map<string, import('./mcpl/types.js').McplServerConfig> = new Map();
|
|
4961
|
-
/** Modern-MCP (2026-07-28)
|
|
5015
|
+
/** Modern-MCP servers (2026-07-28, or a 2025 revision over HTTP), by id;
|
|
5016
|
+
* read through modernServers().
|
|
4962
5017
|
* They are kept apart from the MCPL registry on purpose: they have no
|
|
4963
5018
|
* grant, planes or server→host traffic, so the MCPL machinery (quiesce's
|
|
4964
5019
|
* plane pauses, policy) never sees them. They take part
|
|
@@ -4995,6 +5050,7 @@ export class AgentFramework {
|
|
|
4995
5050
|
* subsystem can be lazily initialized by connectMcplServer when the
|
|
4996
5051
|
* framework started with zero configured servers. */
|
|
4997
5052
|
private mcplInferenceRoutingConfig: import('./mcpl/types.js').InferenceRoutingPolicy | null = null;
|
|
5053
|
+
private mcplUnpromptedChannel: string | null = null;
|
|
4998
5054
|
/** The awareness ledger; null for a framework with neither storePath nor
|
|
4999
5055
|
* discordAwarenessOutboxPath, which refuses a marks choice. */
|
|
5000
5056
|
private readonly discordAwarenessOutbox: DiscordAwarenessOutbox | null;
|
|
@@ -5130,13 +5186,16 @@ export class AgentFramework {
|
|
|
5130
5186
|
assertAgentNamesUnique(config.agents);
|
|
5131
5187
|
assertObservedAgentsConfigured(config.agents);
|
|
5132
5188
|
// The presentation index (#291) needs a context-manager that keeps a
|
|
5133
|
-
// window index per agent (presentRows)
|
|
5134
|
-
//
|
|
5135
|
-
//
|
|
5136
|
-
|
|
5189
|
+
// window index per agent (presentRows), which 0.23.x and 0.24.x were
|
|
5190
|
+
// published without, and a stream tells it of dropped thinking
|
|
5191
|
+
// (noteThinkingDropped), which 0.25.0 was published without. It needs no
|
|
5192
|
+
// store, so it is checked before one is opened.
|
|
5193
|
+
const contextManagerProto = ContextManager.prototype as { presentRows?: unknown; noteThinkingDropped?: unknown };
|
|
5194
|
+
if (typeof contextManagerProto.presentRows !== 'function' || typeof contextManagerProto.noteThinkingDropped !== 'function') {
|
|
5137
5195
|
throw new Error(
|
|
5138
|
-
'agent-framework needs @animalabs/context-manager with ContextManager.presentRows
|
|
5139
|
-
|
|
5196
|
+
'agent-framework needs @animalabs/context-manager with ContextManager.presentRows and ContextManager.noteThinkingDropped: ' +
|
|
5197
|
+
"a context-manager that keeps a window index per agent and can be told which stored replies' thinking the provider dropped. " +
|
|
5198
|
+
'Install @animalabs/context-manager ^0.25.1, which this package declares (0.23.x and 0.24.x have no window index, and 0.25.0 has no noteThinkingDropped)',
|
|
5140
5199
|
);
|
|
5141
5200
|
}
|
|
5142
5201
|
|
|
@@ -5582,7 +5641,7 @@ export class AgentFramework {
|
|
|
5582
5641
|
: undefined);
|
|
5583
5642
|
},
|
|
5584
5643
|
requestInference: (agentName, reason, source, provenance, after) => {
|
|
5585
|
-
|
|
5644
|
+
const request: MarkedRequest = {
|
|
5586
5645
|
agentName, reason, source, timestamp: Date.now(),
|
|
5587
5646
|
...framework.readsRowFor(after, agentName),
|
|
5588
5647
|
// gate-requested wakes carry where/who (EventGate wakeProvenance)
|
|
@@ -5610,11 +5669,18 @@ export class AgentFramework {
|
|
|
5610
5669
|
const route = provenance?.routeChannelId !== undefined
|
|
5611
5670
|
? framework.channelNamed(provenance.routeChannelId, provenance.routeServerId)
|
|
5612
5671
|
: undefined;
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5672
|
+
if (route && framework.mayTakeGateRoute(agentName, route)) {
|
|
5673
|
+
return { channelId: route.channelId, channelServerId: route.serverId, addressed: true };
|
|
5674
|
+
}
|
|
5675
|
+
const came = (provenance?.batchChannels ?? [])
|
|
5676
|
+
.map((c) => framework.channelNamed(c.channelId, c.serverId))
|
|
5677
|
+
.filter((c): c is ChannelRef => c !== undefined)
|
|
5678
|
+
.filter((c, i, all) => all.findIndex((d) => sameChannel(d, c)) === i);
|
|
5679
|
+
return came.length > 0 ? { wakeChannels: came } : {};
|
|
5616
5680
|
})(),
|
|
5617
|
-
}
|
|
5681
|
+
};
|
|
5682
|
+
if (source === 'self-wake' || source === 'sleep') request[UNPROMPTED] = true;
|
|
5683
|
+
framework.pendingRequests.push(request);
|
|
5618
5684
|
},
|
|
5619
5685
|
getAgentNames: () => framework.registry.gateRecipients(),
|
|
5620
5686
|
isHeld: (serverId, channelId) =>
|
|
@@ -5725,6 +5791,7 @@ export class AgentFramework {
|
|
|
5725
5791
|
// Stored for lazy MCPL initialization (connectMcplServer on a framework
|
|
5726
5792
|
// that started with zero configured servers).
|
|
5727
5793
|
framework.mcplInferenceRoutingConfig = config.inferenceRouting ?? null;
|
|
5794
|
+
framework.mcplUnpromptedChannel = config.unpromptedChannel ?? null;
|
|
5728
5795
|
|
|
5729
5796
|
// Client-side programmatic tool calling (code_execution). Made only when
|
|
5730
5797
|
// enabled — everything downstream gates on the field. create() checked
|
|
@@ -6285,6 +6352,9 @@ export class AgentFramework {
|
|
|
6285
6352
|
});
|
|
6286
6353
|
// A stopped host must never hang behind its own cooldown.
|
|
6287
6354
|
step(() => this.cancelProviderAdmission());
|
|
6355
|
+
for (const name of [...(this.budgetHolds?.keys() ?? [])]) {
|
|
6356
|
+
step(() => this.dropBudgetHold(name, { kind: 'shutdown', why: 'the framework is stopping' }));
|
|
6357
|
+
}
|
|
6288
6358
|
// An open logical turn doesn't survive a stop.
|
|
6289
6359
|
this.openLogical.clear();
|
|
6290
6360
|
|
|
@@ -6673,7 +6743,12 @@ export class AgentFramework {
|
|
|
6673
6743
|
// A wait recorded while this call was parked binds it too.
|
|
6674
6744
|
this.refuseHeldProviderModel(agentName, model);
|
|
6675
6745
|
gate.auxiliaryInFlight++;
|
|
6676
|
-
|
|
6746
|
+
let landed = false;
|
|
6747
|
+
try {
|
|
6748
|
+
const result = await run();
|
|
6749
|
+
landed = true;
|
|
6750
|
+
return result;
|
|
6751
|
+
}
|
|
6677
6752
|
catch (error) {
|
|
6678
6753
|
// Compression can be the first call to find a spent quota (idle agent,
|
|
6679
6754
|
// scheduled maintenance). Record a stated wait for this call's model,
|
|
@@ -6689,6 +6764,7 @@ export class AgentFramework {
|
|
|
6689
6764
|
finally {
|
|
6690
6765
|
gate.auxiliaryInFlight = Math.max(0, gate.auxiliaryInFlight - 1);
|
|
6691
6766
|
if (gate.auxiliaryInFlight === 0) for (const resolve of gate.idleWaiters.splice(0)) resolve();
|
|
6767
|
+
if (landed) this.managerCallLanded(agentName);
|
|
6692
6768
|
}
|
|
6693
6769
|
}
|
|
6694
6770
|
/**
|
|
@@ -6736,7 +6812,7 @@ export class AgentFramework {
|
|
|
6736
6812
|
? `a turn whose retries ran out, sent again (${cooldown.rearm.number} of ${cooldown.rearm.of})`
|
|
6737
6813
|
: 'a turn held by a provider cooldown, when it ends' });
|
|
6738
6814
|
}
|
|
6739
|
-
wakes
|
|
6815
|
+
appendAll(wakes, this.heartbeatWakesFor(agentName));
|
|
6740
6816
|
return wakes;
|
|
6741
6817
|
}
|
|
6742
6818
|
|
|
@@ -6926,6 +7002,14 @@ export class AgentFramework {
|
|
|
6926
7002
|
private auxiliaryMembraneFor(agentName: string): Membrane {
|
|
6927
7003
|
return this.membraneAdmitting((run, model) => this.withAuxiliaryAdmission(agentName, run, model));
|
|
6928
7004
|
}
|
|
7005
|
+
private managerCallLanded(agentName: string): void {
|
|
7006
|
+
const hold = this.budgetHolds?.get(agentName);
|
|
7007
|
+
if (!hold) return;
|
|
7008
|
+
hold.managerCalls++;
|
|
7009
|
+
if (hold.checkQueued) return;
|
|
7010
|
+
const look = setTimeout(() => this.releaseBudgetHoldOnEvidence(agentName), 0);
|
|
7011
|
+
look.unref?.();
|
|
7012
|
+
}
|
|
6929
7013
|
/**
|
|
6930
7014
|
* The reply judge's calls. None waits: under the agent's holds a call is
|
|
6931
7015
|
* refused without being made, and the agent's turn stops the judge. A call
|
|
@@ -6978,11 +7062,14 @@ export class AgentFramework {
|
|
|
6978
7062
|
this.withdrawMaintenanceUnderSurgery();
|
|
6979
7063
|
this.withdrawMaintenanceWhenStopping(agentName);
|
|
6980
7064
|
this.refuseHeldProviderModel(agentName, model);
|
|
6981
|
-
|
|
7065
|
+
let result: Awaited<ReturnType<typeof run>>;
|
|
7066
|
+
try { result = await run(); }
|
|
6982
7067
|
catch (error) {
|
|
6983
7068
|
if (model !== undefined && error instanceof Error) this.recordProviderWait(agentName, model, error);
|
|
6984
7069
|
throw error;
|
|
6985
7070
|
}
|
|
7071
|
+
this.managerCallLanded(agentName);
|
|
7072
|
+
return result;
|
|
6986
7073
|
});
|
|
6987
7074
|
}
|
|
6988
7075
|
private ephemeralAuxiliaryMembraneFor(agentName: string, release: ManagerRelease): Membrane {
|
|
@@ -7105,11 +7192,21 @@ export class AgentFramework {
|
|
|
7105
7192
|
held.push(request);
|
|
7106
7193
|
return;
|
|
7107
7194
|
}
|
|
7195
|
+
// Read the kept request's carry before marking it.
|
|
7196
|
+
const keptCarry = turnCarryOf(same);
|
|
7108
7197
|
if (isTurnRetry(request)) (same as MarkedRequest)[TURN_RETRY] = true;
|
|
7198
|
+
const wait = budgetWaitOf(request);
|
|
7199
|
+
if (wait && !budgetWaitOf(same)) (same as MarkedRequest)[BUDGET_WAIT] = wait;
|
|
7109
7200
|
// A merged start's batch (MERGED_BATCH) stays recorded on the request
|
|
7110
7201
|
// kept in its place, which then stands for it, as the start did.
|
|
7111
7202
|
const batch = mergedBatch(request);
|
|
7112
7203
|
if (batch) (same as MarkedRequest)[MERGED_BATCH] = [...new Set([...(mergedBatch(same) ?? []), ...batch])];
|
|
7204
|
+
const carry = keptCarry ?? turnCarryOf(request);
|
|
7205
|
+
if (carry) (same as MarkedRequest)[TURN_CARRY] = carry;
|
|
7206
|
+
else delete (same as MarkedRequest)[TURN_CARRY];
|
|
7207
|
+
if (!isRearmed(request)) delete (same as MarkedRequest)[REARMED];
|
|
7208
|
+
const channels = wakeChannelsOf([same, request]);
|
|
7209
|
+
if (channels) same.wakeChannels = channels;
|
|
7113
7210
|
}
|
|
7114
7211
|
private consultProviderHold(error: Error, agentName: string): { holdMs: number; reason: string } | undefined {
|
|
7115
7212
|
if (!this.providerHoldHook) return undefined;
|
|
@@ -7162,7 +7259,7 @@ export class AgentFramework {
|
|
|
7162
7259
|
// failure streak), and the wait applies to the calls that follow it.
|
|
7163
7260
|
if (!acceleration && !hostHold) {
|
|
7164
7261
|
return wait !== undefined && isMembraneError(error) && error.retryable === true
|
|
7165
|
-
&& this.holdForProviderWait(agent, wait, trigger ? [
|
|
7262
|
+
&& this.holdForProviderWait(agent, wait, trigger ? [this.turnRetryOf(trigger, this.turnRouting.get(agent.name))] : [], true);
|
|
7166
7263
|
}
|
|
7167
7264
|
// As in holdForProviderWait: once stop() has closed admission, nothing is
|
|
7168
7265
|
// held, and nothing started.
|
|
@@ -7174,7 +7271,7 @@ export class AgentFramework {
|
|
|
7174
7271
|
const held = existing?.heldRequests ?? [];
|
|
7175
7272
|
// The failed turn's own request, kept as its retry (TURN_RETRY). An
|
|
7176
7273
|
// auxiliary call's failure passes no trigger: it holds no turn of its own.
|
|
7177
|
-
if (trigger) this.holdRequest(held,
|
|
7274
|
+
if (trigger) this.holdRequest(held, this.turnRetryOf(trigger, this.turnRouting.get(agent.name)));
|
|
7178
7275
|
if (existing) clearTimeout(existing.timer);
|
|
7179
7276
|
const timer = setTimeout(() => this.releaseProviderAccelerationCooldown(agent.name), Math.min(delayMs, MAX_TIMER_DELAY_MS)); timer.unref?.();
|
|
7180
7277
|
this.providerAccelerationCooldowns.set(agent.name, { startedAt: existing?.startedAt ?? now,
|
|
@@ -7234,6 +7331,8 @@ export class AgentFramework {
|
|
|
7234
7331
|
const request: MarkedRequest = { ...trigger, [REARMED]: true };
|
|
7235
7332
|
delete request[TURN_RETRY];
|
|
7236
7333
|
delete request[MERGED_BATCH];
|
|
7334
|
+
delete request.wakeChannels;
|
|
7335
|
+
Object.assign(request, this.wakeChannelsMark(routing));
|
|
7237
7336
|
if (continuation) {
|
|
7238
7337
|
request.reason = REARM_REASON;
|
|
7239
7338
|
delete request.counterparty;
|
|
@@ -7286,7 +7385,7 @@ export class AgentFramework {
|
|
|
7286
7385
|
if (!isMembraneError(error) || typeof error.retryAfterMs !== 'number') return undefined;
|
|
7287
7386
|
if (this.registry.homeChannel(agent.name) !== undefined) {
|
|
7288
7387
|
const wait = this.recordProviderWait(agent.name, agent.model, error);
|
|
7289
|
-
return error.retryable === true && wait !== undefined && this.holdForProviderWait(agent, wait, trigger ? [
|
|
7388
|
+
return error.retryable === true && wait !== undefined && this.holdForProviderWait(agent, wait, trigger ? [this.turnRetryOf(trigger, this.turnRouting.get(agent.name))] : [], true)
|
|
7290
7389
|
? 'held'
|
|
7291
7390
|
: undefined;
|
|
7292
7391
|
}
|
|
@@ -7306,18 +7405,468 @@ export class AgentFramework {
|
|
|
7306
7405
|
const cap = `${PROVIDER_ACCELERATION_MAX_COOLDOWN_MS / 1000} s`;
|
|
7307
7406
|
if (Number.isNaN(stated)) {
|
|
7308
7407
|
return {
|
|
7309
|
-
error: `${error
|
|
7408
|
+
error: `${failureWords(error)} (the provider's stated wait is not a number, and an ephemeral run sleeps only a ` +
|
|
7310
7409
|
`stated wait of ${cap} or less, so it ends now)`,
|
|
7311
7410
|
statedWaitMs: Number.NaN,
|
|
7312
7411
|
};
|
|
7313
7412
|
}
|
|
7314
7413
|
const asked = Number.isFinite(stated) ? `${Math.ceil(stated / 1000)} s` : 'longer than can be represented';
|
|
7315
7414
|
return {
|
|
7316
|
-
error: `${error
|
|
7415
|
+
error: `${failureWords(error)} (the provider asked to wait ${asked}; an ephemeral run doesn't sleep a stated wait ` +
|
|
7317
7416
|
`longer than ${cap}, so it ends now)`,
|
|
7318
7417
|
statedWaitMs: stated,
|
|
7319
7418
|
};
|
|
7320
7419
|
}
|
|
7420
|
+
private holdForBudgetRefusal(
|
|
7421
|
+
agent: Agent,
|
|
7422
|
+
error: Error,
|
|
7423
|
+
retry: InferenceRequest | undefined,
|
|
7424
|
+
checkpoint: RecordedTurnCheckpoint | undefined,
|
|
7425
|
+
routing: TurnRouting,
|
|
7426
|
+
baseline: BudgetHoldBaseline | undefined,
|
|
7427
|
+
closeWindow: () => void,
|
|
7428
|
+
): boolean {
|
|
7429
|
+
const { errorType } = this.classifyInferenceError(error);
|
|
7430
|
+
if (errorType !== 'over_budget' && errorType !== 'context_refusal') return false;
|
|
7431
|
+
if (!this.budgetHolds || !this.running || this.providerAdmissionClosed) return false;
|
|
7432
|
+
if (this.ephemeralRuns.has(agent.name) || this.agents.get(agent.name) !== agent || this.budgetHolds.has(agent.name)) return false;
|
|
7433
|
+
if (!this.compressionPending(agent)) return false;
|
|
7434
|
+
if (checkpoint !== undefined) {
|
|
7435
|
+
try {
|
|
7436
|
+
this.dropTurnCheckpoint(agent.name, checkpoint);
|
|
7437
|
+
} catch (error) {
|
|
7438
|
+
console.error(`[budget-hold] agent=${agent.name}: its refused start's checkpoint could not be dropped, so the refusal takes the error policy's path:`, error);
|
|
7439
|
+
return false;
|
|
7440
|
+
}
|
|
7441
|
+
}
|
|
7442
|
+
const turn = retry ?? { agentName: agent.name, reason: 'budget-hold-release', source: 'framework', timestamp: Date.now() };
|
|
7443
|
+
const carried = budgetWaitOf(turn);
|
|
7444
|
+
const startedAt = Date.now();
|
|
7445
|
+
const standsOn = !continuesTurn(turn) && carried
|
|
7446
|
+
? (carried.row !== undefined ? [carried.row] : [])
|
|
7447
|
+
: this.rowsTurnStandsOn(agent, turn, routing);
|
|
7448
|
+
const wait: BudgetWait = {
|
|
7449
|
+
since: carried?.since ?? startedAt,
|
|
7450
|
+
...(!continuesTurn(turn) && standsOn[0] !== undefined ? { row: standsOn[0] } : {}),
|
|
7451
|
+
...(carried?.alerted ? { alerted: true } : {}),
|
|
7452
|
+
};
|
|
7453
|
+
(turn as MarkedRequest)[BUDGET_WAIT] = wait;
|
|
7454
|
+
const figures = error as Error & { budget?: unknown; actual?: unknown };
|
|
7455
|
+
const hold: BudgetHold = {
|
|
7456
|
+
startedAt,
|
|
7457
|
+
wait,
|
|
7458
|
+
turn,
|
|
7459
|
+
standsOn,
|
|
7460
|
+
wakes: [],
|
|
7461
|
+
errorType,
|
|
7462
|
+
reason: boundFailureText(String(error.message)),
|
|
7463
|
+
budget: typeof figures.budget === 'number' ? figures.budget : undefined,
|
|
7464
|
+
actual: typeof figures.actual === 'number' ? figures.actual : undefined,
|
|
7465
|
+
managerCalls: 0,
|
|
7466
|
+
progressAtHold: baseline ? baseline.progress : this.compressionProgressOf(agent),
|
|
7467
|
+
settingsAtHold: baseline ? baseline.settings : agent.contextSettingsChanges(),
|
|
7468
|
+
};
|
|
7469
|
+
this.budgetHolds.set(agent.name, hold);
|
|
7470
|
+
this.startBudgetHoldRecheck(agent, hold);
|
|
7471
|
+
closeWindow();
|
|
7472
|
+
const endGate = () => this.eventGate?.onInferenceEnded(agent.name);
|
|
7473
|
+
this.emitTrace({
|
|
7474
|
+
type: 'inference:held',
|
|
7475
|
+
agentName: agent.name,
|
|
7476
|
+
errorType,
|
|
7477
|
+
error: hold.reason,
|
|
7478
|
+
...(hold.budget !== undefined ? { budget: hold.budget } : {}),
|
|
7479
|
+
...(hold.actual !== undefined ? { actual: hold.actual } : {}),
|
|
7480
|
+
since: hold.startedAt,
|
|
7481
|
+
heldRequests: 1,
|
|
7482
|
+
});
|
|
7483
|
+
if (this.budgetHolds.get(agent.name) !== hold) {
|
|
7484
|
+
endGate();
|
|
7485
|
+
return true;
|
|
7486
|
+
}
|
|
7487
|
+
if (!wait.alerted) {
|
|
7488
|
+
wait.alerted = true;
|
|
7489
|
+
this.opsAlert('context-refusal', agent.name, this.budgetHoldOpenText(hold), {
|
|
7490
|
+
skipLog: true,
|
|
7491
|
+
key: String(hold.wait.since),
|
|
7492
|
+
data: {
|
|
7493
|
+
errorType,
|
|
7494
|
+
since: hold.wait.since,
|
|
7495
|
+
...(hold.budget !== undefined ? { budget: hold.budget } : {}),
|
|
7496
|
+
...(hold.actual !== undefined ? { actual: hold.actual } : {}),
|
|
7497
|
+
},
|
|
7498
|
+
});
|
|
7499
|
+
if (this.budgetHolds.get(agent.name) !== hold) {
|
|
7500
|
+
endGate();
|
|
7501
|
+
return true;
|
|
7502
|
+
}
|
|
7503
|
+
}
|
|
7504
|
+
endGate();
|
|
7505
|
+
if (this.budgetHolds.get(agent.name) !== hold) return true;
|
|
7506
|
+
this.retractUnsentInjections(agent);
|
|
7507
|
+
if (this.budgetHolds.get(agent.name) !== hold) return true;
|
|
7508
|
+
const readiness = this.contextReadiness(agent.getContextManager());
|
|
7509
|
+
const compression = 'pending' in readiness && readiness.pending ? ` Compression: ${boundFailureText(readiness.pending)}` : '';
|
|
7510
|
+
const again = carried ? `, as of a wait that began ${describeWait(startedAt - carried.since)} ago` : '';
|
|
7511
|
+
console.error(`[budget-hold] agent=${agent.name} compile refused (${errorType}): ${hold.reason} \u2014 Held${again}: ` +
|
|
7512
|
+
`the turn waits, idle, and runs once compression makes room.${compression}`);
|
|
7513
|
+
this.driveBudgetHoldSoon(agent);
|
|
7514
|
+
return true;
|
|
7515
|
+
}
|
|
7516
|
+
|
|
7517
|
+
private rowsTurnStandsOn(agent: Agent, turn: InferenceRequest, routing: TurnRouting): string[] {
|
|
7518
|
+
if (!continuesTurn(turn)) {
|
|
7519
|
+
const row = this.activeTurnTriggers.get(agent.name)?.readsRow;
|
|
7520
|
+
return row !== undefined ? [row] : [];
|
|
7521
|
+
}
|
|
7522
|
+
const rows = routing.firstRow !== undefined ? [routing.firstRow] : [];
|
|
7523
|
+
const from = routing.start?.sequence;
|
|
7524
|
+
if (from === undefined) return rows;
|
|
7525
|
+
try {
|
|
7526
|
+
const ownCalls = new Set<string>();
|
|
7527
|
+
for (const row of agent.getContextManager().getPresentedMessages()) {
|
|
7528
|
+
if (!ownWindowRow(row, agent, ownCalls) || row.sequence <= from) continue;
|
|
7529
|
+
if (storedInjectionMarkOf(row.metadata) !== undefined || clockStampOf(row.metadata) !== undefined) continue;
|
|
7530
|
+
rows.push(String(row.id));
|
|
7531
|
+
}
|
|
7532
|
+
} catch (error) {
|
|
7533
|
+
console.error(`[budget-hold] agent=${agent.name}: could not read the rows its held turn stands on, so a surgery checks its first row alone:`, error);
|
|
7534
|
+
}
|
|
7535
|
+
return rows;
|
|
7536
|
+
}
|
|
7537
|
+
|
|
7538
|
+
private compressionPending(agent: Agent): boolean {
|
|
7539
|
+
try {
|
|
7540
|
+
return !agent.getContextManager().isReady();
|
|
7541
|
+
} catch {
|
|
7542
|
+
return false;
|
|
7543
|
+
}
|
|
7544
|
+
}
|
|
7545
|
+
|
|
7546
|
+
private startBudgetHoldRecheck(agent: Agent, hold: BudgetHold): void {
|
|
7547
|
+
clearInterval(hold.recheck);
|
|
7548
|
+
hold.recheck = undefined;
|
|
7549
|
+
if (this.maintenanceIntervalMs > 0) return;
|
|
7550
|
+
hold.recheck = setInterval(() => this.driveBudgetHold(agent), this.budgetHoldRecheckMs);
|
|
7551
|
+
hold.recheck.unref?.();
|
|
7552
|
+
}
|
|
7553
|
+
|
|
7554
|
+
private driveBudgetHoldSoon(agent: Agent): void {
|
|
7555
|
+
const kick = setTimeout(() => this.driveBudgetHold(agent), 0);
|
|
7556
|
+
kick.unref?.();
|
|
7557
|
+
}
|
|
7558
|
+
|
|
7559
|
+
private driveBudgetHold(agent: Agent): void {
|
|
7560
|
+
const hold = this.budgetHolds?.get(agent.name);
|
|
7561
|
+
if (!hold || this.agents.get(agent.name) !== agent || hold.checkQueued) return;
|
|
7562
|
+
if (this.providerGateBlocked(agent.name)) return;
|
|
7563
|
+
const kicked = this.kickOverBudgetDrain(agent.name, agent, () => {
|
|
7564
|
+
hold.checkQueued = false;
|
|
7565
|
+
if (this.budgetHolds.get(agent.name) !== hold) return;
|
|
7566
|
+
if (this.budgetHoldMoved(agent, hold)) this.releaseBudgetHold(agent.name);
|
|
7567
|
+
else if (kicked === 'already draining') this.driveBudgetHold(agent);
|
|
7568
|
+
}, 'hold', hold.drainKicked);
|
|
7569
|
+
if (kicked === 'kicking' || kicked === 'already draining') hold.checkQueued = true;
|
|
7570
|
+
if (kicked === 'kicking') hold.drainKicked = true;
|
|
7571
|
+
}
|
|
7572
|
+
|
|
7573
|
+
private releaseBudgetHolds(): void {
|
|
7574
|
+
for (const name of [...(this.budgetHolds?.keys() ?? [])]) this.releaseBudgetHold(name);
|
|
7575
|
+
}
|
|
7576
|
+
|
|
7577
|
+
private releaseBudgetHoldsOnEvidence(): void {
|
|
7578
|
+
for (const name of [...(this.budgetHolds?.keys() ?? [])]) this.releaseBudgetHoldOnEvidence(name);
|
|
7579
|
+
}
|
|
7580
|
+
|
|
7581
|
+
private releaseBudgetHoldOnEvidence(agentName: string): void {
|
|
7582
|
+
const hold = this.budgetHolds?.get(agentName);
|
|
7583
|
+
const agent = this.agents.get(agentName);
|
|
7584
|
+
if (hold && agent && this.budgetHoldMoved(agent, hold)) this.releaseBudgetHold(agentName);
|
|
7585
|
+
}
|
|
7586
|
+
|
|
7587
|
+
private budgetHoldMoved(agent: Agent, hold: BudgetHold): boolean {
|
|
7588
|
+
if (!this.compressionPending(agent) || agent.contextSettingsChanges() !== hold.settingsAtHold) return true;
|
|
7589
|
+
const progress = this.compressionProgressOf(agent);
|
|
7590
|
+
if (progress !== undefined && hold.progressAtHold !== undefined) return progress !== hold.progressAtHold;
|
|
7591
|
+
if (progress !== undefined) hold.progressAtHold = progress;
|
|
7592
|
+
return hold.managerCalls !== 0;
|
|
7593
|
+
}
|
|
7594
|
+
|
|
7595
|
+
private compressionProgressOf(agent: Agent): string | undefined {
|
|
7596
|
+
const progress = this.contextProgress(agent.getContextManager());
|
|
7597
|
+
if (!progress) return undefined;
|
|
7598
|
+
try {
|
|
7599
|
+
const { chunksCompressed, summaryCounts } = progress;
|
|
7600
|
+
if (chunksCompressed === undefined && summaryCounts === undefined) return undefined;
|
|
7601
|
+
return JSON.stringify([chunksCompressed, summaryCounts]);
|
|
7602
|
+
} catch (error) {
|
|
7603
|
+
console.error(`[budget-hold] agent=${agent.name}: could not read its compression progress, so it reads as none:`, error);
|
|
7604
|
+
return undefined;
|
|
7605
|
+
}
|
|
7606
|
+
}
|
|
7607
|
+
|
|
7608
|
+
private closureWaits(agentName: string): boolean {
|
|
7609
|
+
const closure = (r: InferenceRequest): boolean => r.agentName === agentName
|
|
7610
|
+
&& (mergedBatch(r) ?? [r]).some((m) => m.reason === 'conversation:closure');
|
|
7611
|
+
return this.pendingRequests.some(closure)
|
|
7612
|
+
|| [...(this.passRequests ?? [])].some((pass) => (pass.get(agentName) ?? []).some(closure))
|
|
7613
|
+
|| (this.providerAccelerationCooldowns.get(agentName)?.heldRequests ?? []).some(closure)
|
|
7614
|
+
|| (this.budgetHolds?.get(agentName)?.wakes ?? []).some(closure);
|
|
7615
|
+
}
|
|
7616
|
+
|
|
7617
|
+
private releaseBudgetHold(agentName: string): void {
|
|
7618
|
+
const hold = this.budgetHolds?.get(agentName);
|
|
7619
|
+
const agent = this.agents.get(agentName);
|
|
7620
|
+
if (!hold || !agent || this.quiesced || this.surgeryHold) return;
|
|
7621
|
+
const requests = [hold.turn, ...hold.wakes];
|
|
7622
|
+
hold.wakes = [];
|
|
7623
|
+
this.budgetHolds!.delete(agentName);
|
|
7624
|
+
clearInterval(hold.recheck);
|
|
7625
|
+
hold.recheck = undefined;
|
|
7626
|
+
appendAll(this.pendingRequests, requests);
|
|
7627
|
+
console.error(`[budget-hold] agent=${agentName} released after ${Date.now() - hold.startedAt}ms: ` +
|
|
7628
|
+
`${requests.length} request(s) queued, and the start's own compile is the check`);
|
|
7629
|
+
const note = this.storeBudgetHoldNote(agent, hold);
|
|
7630
|
+
if (note !== undefined) this.emitTrace({ type: 'message:added', messageId: note, source: `context-injection:${BUDGET_HOLD_NOTE_NAMESPACE}` });
|
|
7631
|
+
this.finishBudgetHold(agentName, hold, 'released');
|
|
7632
|
+
}
|
|
7633
|
+
|
|
7634
|
+
private storeBudgetHoldNote(agent: Agent, hold: BudgetHold): MessageId | undefined {
|
|
7635
|
+
const size = hold.budget !== undefined && hold.actual !== undefined
|
|
7636
|
+
? ` (${hold.actual.toLocaleString('en-US')} tokens against ${hold.budget.toLocaleString('en-US')})`
|
|
7637
|
+
: '';
|
|
7638
|
+
const text = `[budget-hold] Your context didn't fit its budget${size} when a turn came to start. ` +
|
|
7639
|
+
`That turn waited ${describeWait(Date.now() - hold.wait.since)}.`;
|
|
7640
|
+
const row = asStoredRow({ namespace: BUDGET_HOLD_NOTE_NAMESPACE, position: 'afterUser', content: [{ type: 'text', text }] });
|
|
7641
|
+
const mark: StoredInjectionMark = { agent: agent.name, namespace: row.namespace, contentHash: unitHash([row]), part: 0, parts: 1 };
|
|
7642
|
+
try {
|
|
7643
|
+
agent.markUnsentPoint();
|
|
7644
|
+
return storeOwnRow(agent, row.participant, row.content, {
|
|
7645
|
+
system: true, kind: STORED_INJECTION_KIND, [STORED_INJECTION_KEY]: mark,
|
|
7646
|
+
} as MessageMetadata, undefined, !this.logicalTurnAlive(agent));
|
|
7647
|
+
} catch (error) {
|
|
7648
|
+
console.error(`[budget-hold] agent=${agent.name}: the note for its released turn could not be stored:`, error);
|
|
7649
|
+
return undefined;
|
|
7650
|
+
}
|
|
7651
|
+
}
|
|
7652
|
+
|
|
7653
|
+
private finishBudgetHold(agentName: string, hold: BudgetHold, outcome: BudgetHoldOutcome): void {
|
|
7654
|
+
if (this.budgetHolds?.get(agentName) === hold) this.budgetHolds.delete(agentName);
|
|
7655
|
+
clearInterval(hold.recheck);
|
|
7656
|
+
hold.recheck = undefined;
|
|
7657
|
+
const waitedMs = Date.now() - hold.startedAt;
|
|
7658
|
+
console.error(`[budget-hold] agent=${agentName} hold ended (${outcome}) after ${waitedMs}ms`);
|
|
7659
|
+
this.emitTrace({ type: 'inference:hold_ended', agentName, outcome, waitedMs });
|
|
7660
|
+
if (outcome === 'released' || !hold.wait.alerted) return;
|
|
7661
|
+
const waitedFor = Date.now() - hold.wait.since;
|
|
7662
|
+
const after = describeWait(waitedFor);
|
|
7663
|
+
const words = outcome === 'removed'
|
|
7664
|
+
? `what the waiting turn was for was removed or taken out of its window after ${after}, so its waiting start doesn't run`
|
|
7665
|
+
: `the waiting turn was stopped after ${after}, so its waiting start doesn't run`;
|
|
7666
|
+
this.opsAlert('context-refusal-clear', agentName, words, {
|
|
7667
|
+
skipLog: true,
|
|
7668
|
+
key: String(hold.wait.since),
|
|
7669
|
+
data: { outcome, waitedMs: waitedFor },
|
|
7670
|
+
});
|
|
7671
|
+
}
|
|
7672
|
+
|
|
7673
|
+
private budgetHoldOpenText(hold: BudgetHold): string {
|
|
7674
|
+
const head = `compile refused (${hold.errorType}): `;
|
|
7675
|
+
const tail = ' \u2014 the turn waits while compression makes room, and runs once its context fits';
|
|
7676
|
+
return head + boundFailureText(hold.reason, FAILURE_TEXT_MAX_CHARS - head.length - tail.length) + tail;
|
|
7677
|
+
}
|
|
7678
|
+
|
|
7679
|
+
private retractUnsentInjections(agent: Agent): void {
|
|
7680
|
+
if (!this.presents(agent)) return;
|
|
7681
|
+
try {
|
|
7682
|
+
const ids = agent.unsentInjectionRows();
|
|
7683
|
+
if (ids.length === 0) return;
|
|
7684
|
+
agent.getContextManager().retractRows(ids, { undone: true });
|
|
7685
|
+
this.emitTrace({ type: 'presentation:retracted', agentName: agent.name, messageIds: ids, reason: 'unsent-injection' });
|
|
7686
|
+
} catch (error) {
|
|
7687
|
+
console.error(`[budget-hold] agent=${agent.name}: the injection rows its refused start stored could not leave its window:`, error);
|
|
7688
|
+
}
|
|
7689
|
+
}
|
|
7690
|
+
|
|
7691
|
+
private endHeldTurn(agentName: string, turn: InferenceRequest, drop: ContinuationDrop): void {
|
|
7692
|
+
this.endContinuedTurn(agentName, drop, !continuesTurn(turn));
|
|
7693
|
+
}
|
|
7694
|
+
|
|
7695
|
+
private dropBudgetHold(agentName: string, drop: ContinuationDrop, wakes: 'back' | 'gone' = 'gone'): boolean {
|
|
7696
|
+
const hold = this.budgetHolds?.get(agentName);
|
|
7697
|
+
if (!hold) return false;
|
|
7698
|
+
this.budgetHolds.delete(agentName);
|
|
7699
|
+
clearInterval(hold.recheck);
|
|
7700
|
+
hold.recheck = undefined;
|
|
7701
|
+
if (wakes === 'back') appendAll(this.pendingRequests, hold.wakes);
|
|
7702
|
+
hold.wakes = [];
|
|
7703
|
+
try {
|
|
7704
|
+
this.endHeldTurn(agentName, hold.turn, drop);
|
|
7705
|
+
} finally {
|
|
7706
|
+
this.finishBudgetHold(agentName, hold, 'stopped');
|
|
7707
|
+
}
|
|
7708
|
+
return true;
|
|
7709
|
+
}
|
|
7710
|
+
|
|
7711
|
+
private heldContinuation(agentName: string): boolean {
|
|
7712
|
+
const hold = this.budgetHolds?.get(agentName);
|
|
7713
|
+
return hold !== undefined && continuesTurn(hold.turn);
|
|
7714
|
+
}
|
|
7715
|
+
|
|
7716
|
+
private dropHeldTurn(agentName: string, hold: BudgetHold, why: string): void {
|
|
7717
|
+
this.endDetachedHold(agentName, this.detachBudgetHold(agentName, hold), why);
|
|
7718
|
+
}
|
|
7719
|
+
|
|
7720
|
+
/** Take `hold` out of its agent's wait, with no trace yet. */
|
|
7721
|
+
private detachBudgetHold(agentName: string, hold: BudgetHold): BudgetHold {
|
|
7722
|
+
this.budgetHolds.delete(agentName);
|
|
7723
|
+
clearInterval(hold.recheck);
|
|
7724
|
+
hold.recheck = undefined;
|
|
7725
|
+
return hold;
|
|
7726
|
+
}
|
|
7727
|
+
|
|
7728
|
+
/** End a turn detachBudgetHold took out of its wait: its wakes back in the queue, then its end traced. */
|
|
7729
|
+
private endDetachedHold(agentName: string, hold: BudgetHold, why: string): void {
|
|
7730
|
+
appendAll(this.pendingRequests, hold.wakes);
|
|
7731
|
+
hold.wakes = [];
|
|
7732
|
+
try {
|
|
7733
|
+
this.endHeldTurn(agentName, hold.turn, { kind: 'dropped', why, reason: BUDGET_HELD_TURN_REMOVED });
|
|
7734
|
+
} finally {
|
|
7735
|
+
this.finishBudgetHold(agentName, hold, 'removed');
|
|
7736
|
+
}
|
|
7737
|
+
}
|
|
7738
|
+
|
|
7739
|
+
private rowGone(agent: Agent, row: string): boolean {
|
|
7740
|
+
try {
|
|
7741
|
+
return agent.getContextManager().getMessage(row as MessageId) === null;
|
|
7742
|
+
} catch (error) {
|
|
7743
|
+
console.error(`[budget-hold] agent=${agent.name}: could not read whether row ${row} is still there, so what waits on it stays:`, error);
|
|
7744
|
+
return false;
|
|
7745
|
+
}
|
|
7746
|
+
}
|
|
7747
|
+
|
|
7748
|
+
private rowLeftWindow(agent: Agent, row: string): boolean {
|
|
7749
|
+
if (this.rowGone(agent, row)) return true;
|
|
7750
|
+
if (!this.presents(agent)) return false;
|
|
7751
|
+
try {
|
|
7752
|
+
return agent.getContextManager().ordinalOf(row as MessageId) === undefined;
|
|
7753
|
+
} catch (error) {
|
|
7754
|
+
console.error(`[budget-hold] agent=${agent.name}: could not read whether row ${row} is still in its window, so what waits on it stays:`, error);
|
|
7755
|
+
return false;
|
|
7756
|
+
}
|
|
7757
|
+
}
|
|
7758
|
+
|
|
7759
|
+
private budgetWaitEnded(agent: Agent, trigger: InferenceRequest | undefined): boolean {
|
|
7760
|
+
const row = budgetWaitOf(trigger)?.row;
|
|
7761
|
+
if (row === undefined || !this.rowLeftWindow(agent, row)) return false;
|
|
7762
|
+
const batch = mergedBatch(trigger!);
|
|
7763
|
+
if (batch) appendAll(this.pendingRequests, batch.filter((r) => budgetWaitOf(r) === undefined));
|
|
7764
|
+
this.endContinuedTurn(agent.name, { kind: 'dropped', why: 'what its held turn stood on was removed', reason: BUDGET_HELD_TURN_REMOVED }, true);
|
|
7765
|
+
return true;
|
|
7766
|
+
}
|
|
7767
|
+
|
|
7768
|
+
private checkBudgetHoldsAfterWindowChange(only?: string, release = true): void {
|
|
7769
|
+
for (const [agentName, hold] of [...(this.budgetHolds?.entries() ?? [])]) {
|
|
7770
|
+
const agent = this.agents.get(agentName);
|
|
7771
|
+
if (!agent || (only !== undefined && agentName !== only) || this.budgetHolds?.get(agentName) !== hold) continue;
|
|
7772
|
+
const wakes = hold.wakes.filter((wake) => wake.readsRow === undefined || !this.rowGone(agent, wake.readsRow));
|
|
7773
|
+
if (wakes.length < hold.wakes.length) {
|
|
7774
|
+
console.error(`[budget-hold] agent=${agentName}: ${hold.wakes.length - wakes.length} held wake(s) dropped, their rows removed`);
|
|
7775
|
+
}
|
|
7776
|
+
hold.wakes = wakes;
|
|
7777
|
+
if (hold.standsOn.some((row) => this.rowLeftWindow(agent, row))) {
|
|
7778
|
+
this.dropHeldTurn(agentName, hold, 'a surgery or an undo removed what its held turn stood on');
|
|
7779
|
+
} else if (release) {
|
|
7780
|
+
this.releaseBudgetHold(agentName);
|
|
7781
|
+
}
|
|
7782
|
+
}
|
|
7783
|
+
}
|
|
7784
|
+
|
|
7785
|
+
private kickOverBudgetDrain(
|
|
7786
|
+
agentName: string,
|
|
7787
|
+
agent: Agent,
|
|
7788
|
+
after?: () => void,
|
|
7789
|
+
by: 'breaker' | 'hold' = 'breaker',
|
|
7790
|
+
quiet = false,
|
|
7791
|
+
): 'not kicked: the framework is stopping' | 'not kicked: a surgery holds the store' | 'already draining' | 'kicking' {
|
|
7792
|
+
// Like a maintenance pass: none starts once stop() has begun, it starts
|
|
7793
|
+
// no tick while a surgery holds the store or once its agent has been
|
|
7794
|
+
// disposed, and a surgery, a switchBranch or stop() waits for the tick
|
|
7795
|
+
// in flight. It joins the drains they wait for before the breaker's alert is
|
|
7796
|
+
// raised and before its body runs, so the alert's listeners find it:
|
|
7797
|
+
// one that begins stop() or a surgery waits for it, and its body then
|
|
7798
|
+
// ticks none and says why. So does a stop() made from inside its first
|
|
7799
|
+
// tick, before that tick's first await.
|
|
7800
|
+
if (this.providerAdmissionClosed) return 'not kicked: the framework is stopping';
|
|
7801
|
+
if (this.surgeryHold) return 'not kicked: a surgery holds the store';
|
|
7802
|
+
if (after) {
|
|
7803
|
+
const waiting = this.overBudgetDrainAfter.get(agentName);
|
|
7804
|
+
if (waiting) waiting.push(after);
|
|
7805
|
+
else this.overBudgetDrainAfter.set(agentName, [after]);
|
|
7806
|
+
}
|
|
7807
|
+
if (this.overBudgetDrainInFlight.has(agentName)) return 'already draining';
|
|
7808
|
+
this.overBudgetDrainInFlight.add(agentName);
|
|
7809
|
+
const tag = by === 'hold' ? '[budget-hold]' : '[inference-failed]';
|
|
7810
|
+
const label = by === 'hold' ? 'budget hold' : 'OverBudget breaker';
|
|
7811
|
+
const drain = Promise.resolve().then(async () => {
|
|
7812
|
+
let ticks = 0;
|
|
7813
|
+
let record: ContextMaintenanceAgentRun | undefined;
|
|
7814
|
+
try {
|
|
7815
|
+
const cm = agent.getContextManager();
|
|
7816
|
+
if (by === 'hold') {
|
|
7817
|
+
const readiness = this.contextReadiness(cm);
|
|
7818
|
+
const progress = this.contextProgress(cm);
|
|
7819
|
+
record = {
|
|
7820
|
+
agentName, startedAt: Date.now(), ticks: 0, readyBefore: false,
|
|
7821
|
+
...('pending' in readiness && readiness.pending ? { pendingBefore: readiness.pending } : {}),
|
|
7822
|
+
...(progress ? { progressBefore: progress } : {}),
|
|
7823
|
+
};
|
|
7824
|
+
}
|
|
7825
|
+
const holding = () => this.budgetHolds.has(agentName);
|
|
7826
|
+
while ((by === 'hold' ? holding() : ticks < 8) && !this.providerAdmissionClosed && !this.surgeryHold && this.agents.get(agentName) === agent) {
|
|
7827
|
+
const calls = by === 'hold' ? this.budgetHolds.get(agentName)?.managerCalls : undefined;
|
|
7828
|
+
await cm.tick();
|
|
7829
|
+
ticks++;
|
|
7830
|
+
if (by === 'breaker') continue;
|
|
7831
|
+
const held = this.budgetHolds.get(agentName);
|
|
7832
|
+
if (held && this.budgetHoldMoved(agent, held)) {
|
|
7833
|
+
this.releaseBudgetHold(agentName);
|
|
7834
|
+
if (this.quiesced) break;
|
|
7835
|
+
} else if (held?.managerCalls === calls || this.compressionProgressOf(agent) !== undefined) {
|
|
7836
|
+
break;
|
|
7837
|
+
}
|
|
7838
|
+
}
|
|
7839
|
+
if (by === 'hold' && ticks > 0) this.maintenanceFailureReported.delete(agentName);
|
|
7840
|
+
const stopped = by === 'breaker' && ticks >= 8 ? ''
|
|
7841
|
+
: this.agents.get(agentName) !== agent ? ', stopped as the agent was disposed'
|
|
7842
|
+
: this.providerAdmissionClosed ? ', stopped as the framework stops'
|
|
7843
|
+
: this.surgeryHold ? ', stopped while a surgery holds the store'
|
|
7844
|
+
: '';
|
|
7845
|
+
if (!quiet || stopped) console.error(`${tag} drain kicked for ${agentName} (${label}, ${ticks} ticks${stopped})`);
|
|
7846
|
+
} catch (err) {
|
|
7847
|
+
if (err instanceof MaintenanceWithdrawnError) {
|
|
7848
|
+
console.error(`${tag} drain kicked for ${agentName} (${label}, ${ticks} ticks, stopped: ${err.message})`);
|
|
7849
|
+
} else if (record) {
|
|
7850
|
+
record.ticks = ticks;
|
|
7851
|
+
record.finishedAt = Date.now();
|
|
7852
|
+
this.reportMaintenanceFailure(agentName, record, err instanceof Error ? String(err.message) : failureDescription(err));
|
|
7853
|
+
} else {
|
|
7854
|
+
console.error(`${tag} drain kick failed for ${agentName} after ${ticks} ticks: ${describeFailureWithFrames(err)}`);
|
|
7855
|
+
}
|
|
7856
|
+
} finally {
|
|
7857
|
+
this.overBudgetDrainInFlight.delete(agentName);
|
|
7858
|
+
}
|
|
7859
|
+
const waiting = this.overBudgetDrainAfter?.get(agentName);
|
|
7860
|
+
if (waiting) {
|
|
7861
|
+
this.overBudgetDrainAfter.delete(agentName);
|
|
7862
|
+
for (const run of waiting) run();
|
|
7863
|
+
}
|
|
7864
|
+
});
|
|
7865
|
+
this.overBudgetDrains.add(drain);
|
|
7866
|
+
void drain.finally(() => { this.overBudgetDrains.delete(drain); });
|
|
7867
|
+
return 'kicking';
|
|
7868
|
+
}
|
|
7869
|
+
|
|
7321
7870
|
/** When a hold's timer next fires: at its deadline, stepped past what one
|
|
7322
7871
|
* timer can hold. A hold with no deadline (a wait held until released, or
|
|
7323
7872
|
* the stand-in for unreadable records) asks again as often as unreadable
|
|
@@ -7370,7 +7919,7 @@ export class AgentFramework {
|
|
|
7370
7919
|
if (recovery) { recovery.releasedAt = Date.now(); recovery.heldRequests = cooldown.heldRequests.length; }
|
|
7371
7920
|
const requests = cooldown.heldRequests.length > 0 ? cooldown.heldRequests : [{ agentName,
|
|
7372
7921
|
reason: 'provider-acceleration-retry', source: 'framework', timestamp: Date.now() } satisfies InferenceRequest];
|
|
7373
|
-
this.pendingRequests
|
|
7922
|
+
appendAll(this.pendingRequests, requests);
|
|
7374
7923
|
console.error(cooldown.rearm
|
|
7375
7924
|
? `[inference-rearm] agent=${agentName} turn queued again (${cooldown.rearm.number} of ${cooldown.rearm.of})`
|
|
7376
7925
|
: `[provider-cooldown] agent=${agentName} released after ${Date.now() - cooldown.startedAt}ms — ` +
|
|
@@ -7523,7 +8072,7 @@ export class AgentFramework {
|
|
|
7523
8072
|
// (releaseProviderWait) before anything was held: nothing binds now, so
|
|
7524
8073
|
// the requests start, as the release of an installed hold starts them.
|
|
7525
8074
|
if (report && !this.providerWaits?.active(agent.name, wait.model)) {
|
|
7526
|
-
this.pendingRequests
|
|
8075
|
+
appendAll(this.pendingRequests, requests);
|
|
7527
8076
|
return true;
|
|
7528
8077
|
}
|
|
7529
8078
|
const existing = this.providerAccelerationCooldowns.get(agent.name);
|
|
@@ -7705,6 +8254,7 @@ export class AgentFramework {
|
|
|
7705
8254
|
this.maintenancePass = pass;
|
|
7706
8255
|
void pass.finally(() => {
|
|
7707
8256
|
if (this.maintenancePass === pass) this.maintenancePass = null;
|
|
8257
|
+
this.releaseBudgetHoldsOnEvidence();
|
|
7708
8258
|
});
|
|
7709
8259
|
begin(this.runQueuedMaintenance().catch((error) => {
|
|
7710
8260
|
// The alert cuts its own copies from the whole message, so the
|
|
@@ -8242,6 +8792,9 @@ export class AgentFramework {
|
|
|
8242
8792
|
this.dropContinuations(agentName, { kind: 'caller', reason, why: 'a stop ended the turn' });
|
|
8243
8793
|
return true;
|
|
8244
8794
|
}
|
|
8795
|
+
if (!result && this.budgetHolds?.has(agentName)) {
|
|
8796
|
+
return this.dropBudgetHold(agentName, { kind: 'caller', reason, why: 'a stop ended the turn' }, 'back');
|
|
8797
|
+
}
|
|
8245
8798
|
return !!result;
|
|
8246
8799
|
}
|
|
8247
8800
|
|
|
@@ -8370,6 +8923,7 @@ export class AgentFramework {
|
|
|
8370
8923
|
log({
|
|
8371
8924
|
success: false,
|
|
8372
8925
|
error: `Stream cancelled (${reason})`,
|
|
8926
|
+
errorWords: `Stream cancelled (${reason})`,
|
|
8373
8927
|
request: compiledRequest ?? { note: 'streaming request aborted' },
|
|
8374
8928
|
durationMs,
|
|
8375
8929
|
});
|
|
@@ -8450,6 +9004,7 @@ export class AgentFramework {
|
|
|
8450
9004
|
log({
|
|
8451
9005
|
success: false,
|
|
8452
9006
|
error: `Stream aborted: ${kind}`,
|
|
9007
|
+
errorWords: `Stream aborted: ${kind}`,
|
|
8453
9008
|
request: compiledRequest ?? { note: `streaming request aborted by ${kind}` },
|
|
8454
9009
|
durationMs,
|
|
8455
9010
|
});
|
|
@@ -9115,7 +9670,7 @@ export class AgentFramework {
|
|
|
9115
9670
|
const extKeys: string[] = [];
|
|
9116
9671
|
for (const ext of extensions.values()) {
|
|
9117
9672
|
Object.assign(extProperties, ext.properties);
|
|
9118
|
-
extKeys
|
|
9673
|
+
appendAll(extKeys, ext.keys);
|
|
9119
9674
|
}
|
|
9120
9675
|
const settingsProp = baseSchema.properties.settings as { items?: { enum?: string[] } };
|
|
9121
9676
|
return {
|
|
@@ -9505,7 +10060,9 @@ export class AgentFramework {
|
|
|
9505
10060
|
...this.pendingRequests,
|
|
9506
10061
|
...[...this.passRequests].flatMap((pass) => [...pass.values()].flat()),
|
|
9507
10062
|
...[...this.providerAccelerationCooldowns.values()].flatMap((c) => c.heldRequests),
|
|
9508
|
-
].flatMap((r) => mergedBatch(r) ?? [r]).filter((r) => !passesQuiesce(r)).length
|
|
10063
|
+
].flatMap((r) => mergedBatch(r) ?? [r]).filter((r) => !passesQuiesce(r)).length
|
|
10064
|
+
+ [...(this.budgetHolds?.values() ?? [])].reduce((n, h) =>
|
|
10065
|
+
n + 1 + h.wakes.flatMap((r) => mergedBatch(r) ?? [r]).filter((r) => !passesQuiesce(r)).length, 0),
|
|
9509
10066
|
backgroundScripts: this.codeExecution?.scripts.leftRunning() ?? 0,
|
|
9510
10067
|
// Deprecated (#291): no queue is kept. What a live surgery's hold
|
|
9511
10068
|
// keeps in memory, to land at its release, and what waits in memory
|
|
@@ -9900,6 +10457,7 @@ export class AgentFramework {
|
|
|
9900
10457
|
console.error(this.queue?.isClosed
|
|
9901
10458
|
? `[host-mode] resumed — ${releasedRequests} parked wake(s) won't run because the framework is stopping`
|
|
9902
10459
|
: `[host-mode] resumed — ${releasedRequests} parked wake(s) released`);
|
|
10460
|
+
this.releaseBudgetHolds();
|
|
9903
10461
|
}
|
|
9904
10462
|
return this.getHostModeStatus();
|
|
9905
10463
|
}
|
|
@@ -11429,10 +11987,12 @@ export class AgentFramework {
|
|
|
11429
11987
|
store: this.store,
|
|
11430
11988
|
namespace,
|
|
11431
11989
|
isolate: true,
|
|
11990
|
+
viewFilter: (message) => !isReasoningLoss(message.metadata),
|
|
11432
11991
|
strategy: config.strategy ?? new PassthroughStrategy(),
|
|
11433
11992
|
membrane: this.ephemeralAuxiliaryMembraneFor(config.name, release),
|
|
11434
11993
|
debugLogContext: !!process.env.DEBUG_CONTEXT,
|
|
11435
11994
|
});
|
|
11995
|
+
this.storeSees.set(contextManager, (metadata) => !isReasoningLoss(metadata));
|
|
11436
11996
|
ownSlot = { namespace };
|
|
11437
11997
|
}
|
|
11438
11998
|
// Either manager is the run's to release (main's manager release), the derived one included.
|
|
@@ -11842,7 +12402,8 @@ export class AgentFramework {
|
|
|
11842
12402
|
* The returned manager uses a passthrough strategy, so nothing is
|
|
11843
12403
|
* folded: its compile is the agent's window as the agent's view shows it
|
|
11844
12404
|
* (the rows the window holds, through its view filter), and in legacy
|
|
11845
|
-
* mode, where the window is the record, the record so.
|
|
12405
|
+
* mode, where the window is the record, the record so. These compiled
|
|
12406
|
+
* views omit legacy reasoning-loss notices. getAllMessages()
|
|
11846
12407
|
* lists the record. An ended agent's context is the real one — treat it as
|
|
11847
12408
|
* read-only unless you mean to change what a later resume starts from.
|
|
11848
12409
|
*/
|
|
@@ -11966,9 +12527,9 @@ export class AgentFramework {
|
|
|
11966
12527
|
* and what it observes (context-options.ts): a resident's, on the
|
|
11967
12528
|
* residents' slot, or for an isolated agent that watches that slot, the
|
|
11968
12529
|
* residents' view when its edge leaves held traffic out, and the
|
|
11969
|
-
* subconscious's when it shows it.
|
|
12530
|
+
* subconscious's when it shows it. For an isolated agent that
|
|
11970
12531
|
* watches no residents' slot on its branch (a worker, a conversation
|
|
11971
|
-
* fork), whose
|
|
12532
|
+
* fork), whose view excludes only legacy reasoning-loss notices.
|
|
11972
12533
|
*/
|
|
11973
12534
|
private liveViewFilterOf(record: AgentRecord): ((message: StoredMessage) => boolean) | undefined {
|
|
11974
12535
|
if (record.ownSlot?.namespace === null) return residentViewFilter(record.name);
|
|
@@ -11976,7 +12537,7 @@ export class AgentFramework {
|
|
|
11976
12537
|
const slot = this.registry.observedSlotOf(edge);
|
|
11977
12538
|
return slot !== undefined && slot.branch === undefined && sameSlot(slot, RESIDENTS_SLOT);
|
|
11978
12539
|
});
|
|
11979
|
-
if (residents.length === 0) return
|
|
12540
|
+
if (residents.length === 0) return (message) => !isReasoningLoss(message.metadata);
|
|
11980
12541
|
return residents.some((edge) => edge.includeHeld === true) ? subconsciousViewFilter(record.name) : residentViewFilter(record.name);
|
|
11981
12542
|
}
|
|
11982
12543
|
|
|
@@ -12003,7 +12564,7 @@ export class AgentFramework {
|
|
|
12003
12564
|
* (deriveSharedContext): what the agent its chain of derivations started
|
|
12004
12565
|
* from sees (liveViewFilterOf), and the stored injections of each agent in
|
|
12005
12566
|
* the chain, which carry that agent's name and would otherwise read as
|
|
12006
|
-
* another's.
|
|
12567
|
+
* another's. Legacy notices remain excluded even for an own injection.
|
|
12007
12568
|
*/
|
|
12008
12569
|
private derivedViewFilterOf(record: AgentRecord): ((message: StoredMessage) => boolean) | undefined {
|
|
12009
12570
|
const owner = this.viewOwnerOf(record);
|
|
@@ -12018,7 +12579,8 @@ export class AgentFramework {
|
|
|
12018
12579
|
current = parent;
|
|
12019
12580
|
}
|
|
12020
12581
|
if (names.length === 0) return ownerSees;
|
|
12021
|
-
return (message) =>
|
|
12582
|
+
return (message) => !isReasoningLoss(message.metadata)
|
|
12583
|
+
&& (ownerSees(message) || names.some((name) => isOwnInjection(message.metadata, name)));
|
|
12022
12584
|
}
|
|
12023
12585
|
|
|
12024
12586
|
/** Open a derived agent's context on a branch of its own (see deriveAgent). */
|
|
@@ -12065,13 +12627,11 @@ export class AgentFramework {
|
|
|
12065
12627
|
// reads through it, and is recorded beside the manager (storeSees), so
|
|
12066
12628
|
// main's checks of whether an agent sees a row (agentSees: a tool
|
|
12067
12629
|
// boundary's injection, an arrival, a reader of an unread message) ask
|
|
12068
|
-
// what its compile shows.
|
|
12069
|
-
//
|
|
12070
|
-
// sees its own.
|
|
12630
|
+
// what its compile shows. The global legacy-notice exclusion applies
|
|
12631
|
+
// even when the source has no additional view recorded.
|
|
12071
12632
|
const sourceSees = this.storeSees.get(source);
|
|
12072
|
-
const sees =
|
|
12073
|
-
|
|
12074
|
-
: undefined;
|
|
12633
|
+
const sees = (metadata: unknown): boolean => !isReasoningLoss(metadata)
|
|
12634
|
+
&& (!sourceSees || sourceSees(metadata) || isOwnInjection(metadata, config.name));
|
|
12075
12635
|
const contextManager = await source.derive({
|
|
12076
12636
|
branch: ownBranch,
|
|
12077
12637
|
strategy: config.strategy ?? new PassthroughStrategy(),
|
|
@@ -12081,10 +12641,10 @@ export class AgentFramework {
|
|
|
12081
12641
|
solve: inherit.solve ?? 'reuse',
|
|
12082
12642
|
inherit: { refusals: inherit.refusals === true },
|
|
12083
12643
|
...(inherit.atSequence !== undefined ? { atSequence: inherit.atSequence } : {}),
|
|
12084
|
-
|
|
12644
|
+
viewFilter: (message) => sees(message.metadata),
|
|
12085
12645
|
debugLogContext: !!process.env.DEBUG_CONTEXT,
|
|
12086
12646
|
});
|
|
12087
|
-
|
|
12647
|
+
this.storeSees.set(contextManager, sees);
|
|
12088
12648
|
const derivation = (contextManager as ContextManager & Partial<DerivingContextManager>).getDerivation?.();
|
|
12089
12649
|
return {
|
|
12090
12650
|
contextManager,
|
|
@@ -12633,6 +13193,7 @@ export class AgentFramework {
|
|
|
12633
13193
|
this.closingConversationAgents.delete(name);
|
|
12634
13194
|
// A continuation of its turn still on its way goes with it.
|
|
12635
13195
|
this.dropContinuations(name, { kind: 'dropped', why });
|
|
13196
|
+
this.dropBudgetHold(name, { kind: 'dropped', why });
|
|
12636
13197
|
// A batch its guard still holds is settled as `aborted`: the stream's
|
|
12637
13198
|
// own finally does neither once the agent has gone.
|
|
12638
13199
|
if (agent.toolResultGuard.hasPending) {
|
|
@@ -13303,6 +13864,7 @@ export class AgentFramework {
|
|
|
13303
13864
|
stopReason: AgentSettleResult['stopReason'];
|
|
13304
13865
|
speech: string;
|
|
13305
13866
|
error?: string;
|
|
13867
|
+
cause?: Error;
|
|
13306
13868
|
/** An ephemeral run that ended rather than sleep the provider's stated
|
|
13307
13869
|
* wait: that wait (statedWaitNotSlept). */
|
|
13308
13870
|
statedWaitMs?: number;
|
|
@@ -13346,9 +13908,10 @@ export class AgentFramework {
|
|
|
13346
13908
|
if (outcome.stopReason === 'exhausted') {
|
|
13347
13909
|
// The caller may log or relay the message, which can echo the whole request.
|
|
13348
13910
|
const message = boundFailureText(outcome.error ?? 'Unknown error');
|
|
13911
|
+
const options = outcome.cause !== undefined ? { cause: boundedCause(outcome.cause) } : undefined;
|
|
13349
13912
|
run.settle.reject(outcome.statedWaitMs === undefined
|
|
13350
|
-
? new Error(message)
|
|
13351
|
-
: new ProviderWaitNotSleptError(message, outcome.statedWaitMs));
|
|
13913
|
+
? new Error(message, options)
|
|
13914
|
+
: new ProviderWaitNotSleptError(message, outcome.statedWaitMs, options));
|
|
13352
13915
|
return;
|
|
13353
13916
|
}
|
|
13354
13917
|
run.settle.resolve({
|
|
@@ -13367,7 +13930,19 @@ export class AgentFramework {
|
|
|
13367
13930
|
* Rejects with ProviderWaitNotSleptError when the run ends rather than sleep
|
|
13368
13931
|
* a retryable failure's stated wait longer than the framework's ten-minute
|
|
13369
13932
|
* cap, or one it can't represent, and with a plain Error for any other
|
|
13370
|
-
* ending, a stated wait its error policy declines among them.
|
|
13933
|
+
* ending, a stated wait its error policy declines among them. A failed
|
|
13934
|
+
* turn's rejection never carries a provider's message: a provider's failure
|
|
13935
|
+
* is told by membrane's type and the HTTP status, a safety failure only as
|
|
13936
|
+
* a request that couldn't be completed, a progress bound's cut, a stream's
|
|
13937
|
+
* abort and context-manager's refusal of a compile in their own words, and
|
|
13938
|
+
* any other failure as the turn failing. Where an error ended the turn, the
|
|
13939
|
+
* rejection's `cause` is a bounded copy of it: a plain `Error` with its
|
|
13940
|
+
* `name`, its message and stack cut to 2,000 characters, and, from a
|
|
13941
|
+
* MembraneError, its `type`, `retryable`, `httpStatus`, `providerErrorCode`
|
|
13942
|
+
* and `retryAfterMs`, which a host reads by name. It carries no `rawError`
|
|
13943
|
+
* or `rawRequest`, and `instanceof MembraneError` doesn't hold on it. Its
|
|
13944
|
+
* message and stack are the failure's own, a provider's words included, for
|
|
13945
|
+
* classifying the failure and for logs, and aren't for showing to an agent.
|
|
13371
13946
|
*
|
|
13372
13947
|
* The caller provides a pre-created agent + contextManager (from createEphemeralAgent).
|
|
13373
13948
|
* The task message should already be in the context manager.
|
|
@@ -14432,7 +15007,7 @@ export class AgentFramework {
|
|
|
14432
15007
|
const turnMembers = all.slice(turn.from, turn.to + 1);
|
|
14433
15008
|
if (turnMembers.some((m) => hasBlock(m, 'tool_use'))) {
|
|
14434
15009
|
from = turn.from;
|
|
14435
|
-
removedIds
|
|
15010
|
+
appendAll(removedIds, turnMembers.map((m) => m.id).reverse());
|
|
14436
15011
|
heads.unshift(all[turn.from]!.id);
|
|
14437
15012
|
}
|
|
14438
15013
|
}
|
|
@@ -14475,17 +15050,10 @@ export class AgentFramework {
|
|
|
14475
15050
|
const cm = agent.getContextManager();
|
|
14476
15051
|
const ep = this.rewindEpisode.get(agent.name);
|
|
14477
15052
|
const count = (ep?.count ?? 0) + 1;
|
|
14478
|
-
const why = cause === 'refusal'
|
|
14479
|
-
? `the model refused on them (content filter: ${failureShown(category)}). Their content is ` +
|
|
14480
|
-
`not reproduced here (so this note can't re-trigger the filter); the ` +
|
|
14481
|
-
`originals remain in the raw record`
|
|
14482
|
-
: `the model API kept refusing the requests that carried them (${failureShown(category)}), ` +
|
|
14483
|
-
`so that the conversation could go on. Their content is not reproduced ` +
|
|
14484
|
-
`here (so this note can't trigger the same refusal); the originals remain ` +
|
|
14485
|
-
`in the raw record`;
|
|
14486
15053
|
const text =
|
|
14487
15054
|
`[${NOTICE_TAG.refusalRewind}] The ${count} most recent turn(s) were set aside because ` +
|
|
14488
|
-
|
|
15055
|
+
`the requests that carried them couldn't be completed. Their content isn't shown here, ` +
|
|
15056
|
+
`and the originals remain in the raw record. You are clear to continue — if you ` +
|
|
14489
15057
|
`were mid-task, take a different approach; otherwise carry on with whatever ` +
|
|
14490
15058
|
`is now in front of you, or briefly acknowledge the gap and ask what's next.`;
|
|
14491
15059
|
const blocks: ContentBlock[] = [{ type: 'text', text }];
|
|
@@ -14533,10 +15101,10 @@ export class AgentFramework {
|
|
|
14533
15101
|
: '';
|
|
14534
15102
|
const content = success
|
|
14535
15103
|
? `🔧 Unstuck **${agentName}** — shed ${n} turn(s); the model responded.${list}`
|
|
14536
|
-
: `⚠️ Couldn't unstick **${agentName}** — still
|
|
14537
|
-
`${category ? ` (category=${category})` : ''}.${list}`;
|
|
15104
|
+
: `⚠️ Couldn't unstick **${agentName}** — its requests still couldn't be completed after ${n} rewind(s).${list}`;
|
|
14538
15105
|
console.error(
|
|
14539
|
-
`[unstick] agent=${agentName} ${success ? 'succeeded' : 'gave up'} after ${n} rewind(s)
|
|
15106
|
+
`[unstick] agent=${agentName} ${success ? 'succeeded' : 'gave up'} after ${n} rewind(s)` +
|
|
15107
|
+
`${category ? ` category=${logValue(category)}` : ''}`,
|
|
14540
15108
|
);
|
|
14541
15109
|
if (s.channelId) void this.postToChannel(s.serverId, s.channelId, content);
|
|
14542
15110
|
}
|
|
@@ -14573,8 +15141,9 @@ export class AgentFramework {
|
|
|
14573
15141
|
/** Agent state at queue time — 'idle' means no turn holds the agent, so
|
|
14574
15142
|
* the turn starts on the next scheduler pass unless a provider hold (a
|
|
14575
15143
|
* provider's stated wait, or a capacity cooldown) holds it until the hold
|
|
14576
|
-
* ends, or a
|
|
14577
|
-
*
|
|
15144
|
+
* ends, or a budget hold keeps it until compression makes room, or a
|
|
15145
|
+
* start of the agent's that waits for an auxiliary call to finish goes
|
|
15146
|
+
* first; anything else means it runs after the current turn. */
|
|
14578
15147
|
agentStatus?: string;
|
|
14579
15148
|
/** True when the host is quiesced: the wake is PARKED, not running, until resume(). */
|
|
14580
15149
|
quiesced?: boolean;
|
|
@@ -15225,6 +15794,7 @@ export class AgentFramework {
|
|
|
15225
15794
|
requestId: entry.requestId,
|
|
15226
15795
|
success: entry.success,
|
|
15227
15796
|
error: entry.error,
|
|
15797
|
+
errorWords: entry.errorWords,
|
|
15228
15798
|
durationMs: entry.durationMs,
|
|
15229
15799
|
tokenUsage: entry.tokenUsage,
|
|
15230
15800
|
stopReason: entry.stopReason,
|
|
@@ -15571,7 +16141,7 @@ export class AgentFramework {
|
|
|
15571
16141
|
let failures: ManagerReinitFailure[] = [];
|
|
15572
16142
|
try {
|
|
15573
16143
|
this.subconsciousAnchor?.branchMoved(this.tuneOutCoordinator?.activeEpochIds());
|
|
15574
|
-
this.tuneOutCoordinator?.branchMoved();
|
|
16144
|
+
this.tuneOutCoordinator?.branchMoved(sameBranch);
|
|
15575
16145
|
if (!sameBranch) this.redoStacks.clear();
|
|
15576
16146
|
failures = await this.reinitializeManagers(except);
|
|
15577
16147
|
// The agent the move was made through initialized its own manager,
|
|
@@ -15700,6 +16270,27 @@ export class AgentFramework {
|
|
|
15700
16270
|
fromBranch?: string;
|
|
15701
16271
|
toBranch?: string;
|
|
15702
16272
|
window?: { retracted: number; represented: number };
|
|
16273
|
+
} {
|
|
16274
|
+
// A turn under way that waits for compression stands on what the undo
|
|
16275
|
+
// takes out. It leaves its wait first, so the undo finds the agent idle,
|
|
16276
|
+
// and it ends once the undo has kept its record, so a listener of its
|
|
16277
|
+
// end finds the undo done.
|
|
16278
|
+
const held = this.budgetHolds?.get(agent.name);
|
|
16279
|
+
const taken = held && continuesTurn(held.turn) && !this.hasTurn(agent.name) && !this.turnAlive(agent)
|
|
16280
|
+
? this.detachBudgetHold(agent.name, held) : undefined;
|
|
16281
|
+
try {
|
|
16282
|
+
return this.undoTurnOnWindow(agent, requester);
|
|
16283
|
+
} finally {
|
|
16284
|
+
if (taken) this.endDetachedHold(agent.name, taken, 'an undo took out what its held turn stood on');
|
|
16285
|
+
}
|
|
16286
|
+
}
|
|
16287
|
+
|
|
16288
|
+
private undoTurnOnWindow(agent: Agent, requester?: OperatorRequester): {
|
|
16289
|
+
undone: boolean;
|
|
16290
|
+
turnIndex?: number;
|
|
16291
|
+
fromBranch?: string;
|
|
16292
|
+
toBranch?: string;
|
|
16293
|
+
window?: { retracted: number; represented: number };
|
|
15703
16294
|
} {
|
|
15704
16295
|
const agentName = agent.name;
|
|
15705
16296
|
// A turn alive (its start, its stream, its tools, a continuation or a
|
|
@@ -15746,6 +16337,7 @@ export class AgentFramework {
|
|
|
15746
16337
|
}
|
|
15747
16338
|
this.emitTrace({ type: 'window:undone', agentName, retracted: 0 });
|
|
15748
16339
|
record(window);
|
|
16340
|
+
this.checkBudgetHoldsAfterWindowChange(agentName);
|
|
15749
16341
|
return { undone: true, turnIndex: checkpoint.turnIndex, fromBranch: branch, toBranch: branch, window };
|
|
15750
16342
|
}
|
|
15751
16343
|
const entry: RedoEntry = {
|
|
@@ -15775,6 +16367,7 @@ export class AgentFramework {
|
|
|
15775
16367
|
const reason = error instanceof Error ? error.message : String(error);
|
|
15776
16368
|
this.emitTrace({ type: 'window:undone', agentName, retracted: left });
|
|
15777
16369
|
record({ retracted: left, represented: 0 }, reason);
|
|
16370
|
+
this.checkBudgetHoldsAfterWindowChange(agentName, false);
|
|
15778
16371
|
throw new OperatorActionError(
|
|
15779
16372
|
'failed',
|
|
15780
16373
|
`Undo of ${agentName}'s turn ${checkpoint.turnIndex} was cut short: ${left} of the ${tail.length} rows it takes out of the window left it ` +
|
|
@@ -15798,6 +16391,7 @@ export class AgentFramework {
|
|
|
15798
16391
|
const reason = error instanceof Error ? error.message : String(error);
|
|
15799
16392
|
this.emitTrace({ type: 'window:undone', agentName, retracted: tail.length });
|
|
15800
16393
|
record(window, reason);
|
|
16394
|
+
this.checkBudgetHoldsAfterWindowChange(agentName, false);
|
|
15801
16395
|
const rows = tail.length === 1 ? '1 row' : `${tail.length} rows`;
|
|
15802
16396
|
throw new OperatorActionError(
|
|
15803
16397
|
'failed',
|
|
@@ -15808,6 +16402,7 @@ export class AgentFramework {
|
|
|
15808
16402
|
}
|
|
15809
16403
|
this.emitTrace({ type: 'window:undone', agentName, retracted: tail.length });
|
|
15810
16404
|
record(window);
|
|
16405
|
+
this.checkBudgetHoldsAfterWindowChange(agentName);
|
|
15811
16406
|
return { undone: true, turnIndex: checkpoint.turnIndex, fromBranch: branch, toBranch: branch, window };
|
|
15812
16407
|
}
|
|
15813
16408
|
|
|
@@ -16007,7 +16602,7 @@ export class AgentFramework {
|
|
|
16007
16602
|
this.retakeMarks(agent, cm, entry, back, below);
|
|
16008
16603
|
const ordinals = back.map((id) => cm.ordinalOf(id)).filter((ordinal): ordinal is number => ordinal !== undefined);
|
|
16009
16604
|
const boundary = {
|
|
16010
|
-
indexLengthBefore: ordinals.length > 0 ? Math.min(
|
|
16605
|
+
indexLengthBefore: ordinals.length > 0 ? ordinals.reduce((min, ordinal) => Math.min(min, ordinal), Infinity) : cm.indexLength(),
|
|
16011
16606
|
windowBranch: cm.currentBranch().name,
|
|
16012
16607
|
};
|
|
16013
16608
|
const checkpoints = this.getTurnCheckpoints(agentName);
|
|
@@ -16290,6 +16885,7 @@ export class AgentFramework {
|
|
|
16290
16885
|
// turn's end (a rollback's returned arrivals wait: holdUntilStart).
|
|
16291
16886
|
this.releaseSurgeryHeldWrites();
|
|
16292
16887
|
for (const name of reserved.keys()) this.presentAtEndOf(this.agents.get(name));
|
|
16888
|
+
this.checkBudgetHoldsAfterWindowChange();
|
|
16293
16889
|
};
|
|
16294
16890
|
}
|
|
16295
16891
|
|
|
@@ -16345,14 +16941,16 @@ export class AgentFramework {
|
|
|
16345
16941
|
`[surgery] agent=${agentName}: its context manager is re-initialized, so the ${writes.length} write(s) kept in ` +
|
|
16346
16942
|
"memory for it while it refused its work go to the move's release, ahead of the writes its hold keeps",
|
|
16347
16943
|
);
|
|
16348
|
-
recovered
|
|
16944
|
+
appendAll(recovered, writes);
|
|
16349
16945
|
}
|
|
16350
16946
|
if (recovered.length === 0) return;
|
|
16351
16947
|
// In arrival order across the agents, as each agent's are: writes for
|
|
16352
16948
|
// two agents whose slot one record holds (residents) land there in the
|
|
16353
16949
|
// order they came.
|
|
16354
16950
|
recovered.sort((x, y) => x.heldOrder - y.heldOrder);
|
|
16355
|
-
this.surgeryHeldWrites.
|
|
16951
|
+
const later = this.surgeryHeldWrites.splice(0);
|
|
16952
|
+
appendAll(this.surgeryHeldWrites, recovered);
|
|
16953
|
+
appendAll(this.surgeryHeldWrites, later);
|
|
16356
16954
|
if (!this.surgeryHold) this.releaseSurgeryHeldWrites();
|
|
16357
16955
|
}
|
|
16358
16956
|
|
|
@@ -16419,7 +17017,7 @@ export class AgentFramework {
|
|
|
16419
17017
|
return found;
|
|
16420
17018
|
};
|
|
16421
17019
|
for (const [agentName, writes] of this.heldForRecovery ?? []) {
|
|
16422
|
-
entry(agentName).recovery
|
|
17020
|
+
appendAll(entry(agentName).recovery, writes.filter((write) => !told.has(write)));
|
|
16423
17021
|
}
|
|
16424
17022
|
for (const write of this.surgeryHeldWrites ?? []) {
|
|
16425
17023
|
if (told.has(write)) continue;
|
|
@@ -17866,7 +18464,7 @@ export class AgentFramework {
|
|
|
17866
18464
|
// Turn checkpoint internals
|
|
17867
18465
|
// ==========================================================================
|
|
17868
18466
|
|
|
17869
|
-
private recordTurnCheckpoint(agentName: string):
|
|
18467
|
+
private recordTurnCheckpoint(agentName: string): RecordedTurnCheckpoint {
|
|
17870
18468
|
const turnIndex = this.turnCounters.get(agentName) ?? 0;
|
|
17871
18469
|
this.turnCounters.set(agentName, turnIndex + 1);
|
|
17872
18470
|
|
|
@@ -17889,11 +18487,20 @@ export class AgentFramework {
|
|
|
17889
18487
|
checkpoints.push(checkpoint);
|
|
17890
18488
|
|
|
17891
18489
|
// Trim to max depth
|
|
17892
|
-
|
|
17893
|
-
checkpoints.splice(0, checkpoints.length - MAX_TURN_CHECKPOINTS)
|
|
17894
|
-
|
|
18490
|
+
const evicted = checkpoints.length > MAX_TURN_CHECKPOINTS
|
|
18491
|
+
? checkpoints.splice(0, checkpoints.length - MAX_TURN_CHECKPOINTS)
|
|
18492
|
+
: [];
|
|
17895
18493
|
|
|
17896
18494
|
this.saveTurnCheckpoints(agentName, checkpoints);
|
|
18495
|
+
return { turnIndex, evicted };
|
|
18496
|
+
}
|
|
18497
|
+
|
|
18498
|
+
private dropTurnCheckpoint(agentName: string, recorded: RecordedTurnCheckpoint): void {
|
|
18499
|
+
const checkpoints = this.getTurnCheckpoints(agentName);
|
|
18500
|
+
if (checkpoints.at(-1)?.turnIndex !== recorded.turnIndex) return;
|
|
18501
|
+
checkpoints.pop();
|
|
18502
|
+
this.saveTurnCheckpoints(agentName, recorded.evicted.concat(checkpoints));
|
|
18503
|
+
this.turnCounters.set(agentName, recorded.turnIndex);
|
|
17897
18504
|
}
|
|
17898
18505
|
|
|
17899
18506
|
/**
|
|
@@ -18830,7 +19437,7 @@ export class AgentFramework {
|
|
|
18830
19437
|
// reads the time it's written (clock-rows.ts).
|
|
18831
19438
|
const clock = injects ? this.storeDueClock(agent, 'tool-boundary') : undefined;
|
|
18832
19439
|
if (presentsWindow) {
|
|
18833
|
-
heard
|
|
19440
|
+
appendAll(heard, this.presentation!.presentAtBoundary(agent, [replyId, resultsId, clock?.id], { injects }).heard);
|
|
18834
19441
|
}
|
|
18835
19442
|
// The clock's store and the boundary's presentations run their
|
|
18836
19443
|
// listeners synchronously too, and one may have stopped the turn,
|
|
@@ -19115,6 +19722,7 @@ export class AgentFramework {
|
|
|
19115
19722
|
timestamp: Date.now(),
|
|
19116
19723
|
suppressProse: this.activeTurnTriggers.get(agent.name)?.suppressProse,
|
|
19117
19724
|
silentHeartbeat: this.activeTurnTriggers.get(agent.name)?.silentHeartbeat,
|
|
19725
|
+
...this.wakeChannelsMark(this.turnRouting.get(agent.name)),
|
|
19118
19726
|
}, currentState.stream ? this.streamCarries.get(currentState.stream)?.() : undefined);
|
|
19119
19727
|
this.pendingRequests.push(continuation);
|
|
19120
19728
|
// Context budget exceeded: break the stream, let compile() compress.
|
|
@@ -19252,6 +19860,7 @@ export class AgentFramework {
|
|
|
19252
19860
|
timestamp: Date.now(),
|
|
19253
19861
|
suppressProse: this.activeTurnTriggers.get(agent.name)?.suppressProse,
|
|
19254
19862
|
silentHeartbeat: this.activeTurnTriggers.get(agent.name)?.silentHeartbeat,
|
|
19863
|
+
...this.wakeChannelsMark(this.turnRouting.get(agent.name)),
|
|
19255
19864
|
});
|
|
19256
19865
|
}
|
|
19257
19866
|
}
|
|
@@ -20100,6 +20709,7 @@ export class AgentFramework {
|
|
|
20100
20709
|
store: this.store,
|
|
20101
20710
|
namespace: `conversations/${name}`,
|
|
20102
20711
|
isolate: true,
|
|
20712
|
+
viewFilter: (message) => !isReasoningLoss(message.metadata),
|
|
20103
20713
|
// Strategy instances are stateful — never share the template's.
|
|
20104
20714
|
strategy: router.strategyFactory?.() ?? new PassthroughStrategy(),
|
|
20105
20715
|
// Provider cooldown ownership belongs to the persistent resident only;
|
|
@@ -20110,6 +20720,7 @@ export class AgentFramework {
|
|
|
20110
20720
|
});
|
|
20111
20721
|
release.manager = contextManager;
|
|
20112
20722
|
this.trackManager(contextManager);
|
|
20723
|
+
this.storeSees.set(contextManager, (metadata) => !isReasoningLoss(metadata));
|
|
20113
20724
|
|
|
20114
20725
|
// Seed with the template's compiled context, renaming the template
|
|
20115
20726
|
// participant so the fork reads its inheritance as its own history.
|
|
@@ -20374,6 +20985,7 @@ export class AgentFramework {
|
|
|
20374
20985
|
this.thinkingLossIncidents.forget(agentName);
|
|
20375
20986
|
// A continuation of its turn still on its way goes with it.
|
|
20376
20987
|
this.dropContinuations(agentName, { kind: 'dropped', why: 'its agent was disposed' });
|
|
20988
|
+
this.dropBudgetHold(agentName, { kind: 'dropped', why: 'its agent was disposed' });
|
|
20377
20989
|
this.turnRouting.delete(agentName);
|
|
20378
20990
|
this.proseSpeechChains.delete(agentName);
|
|
20379
20991
|
this.openLogical.delete(agentName);
|
|
@@ -20483,8 +21095,8 @@ export class AgentFramework {
|
|
|
20483
21095
|
|
|
20484
21096
|
// Fallback reaper: a closing fork whose closure inference never ran
|
|
20485
21097
|
// (request dropped as stale, inference policy veto) would otherwise stay
|
|
20486
|
-
// registered forever. No turn, no active stream
|
|
20487
|
-
// done. A turn holds its token from dequeue, through its hooks and
|
|
21098
|
+
// registered forever. No turn, no active stream, no pending request, no
|
|
21099
|
+
// closure wake waiting and no budget hold = done. A turn holds its token from dequeue, through its hooks and
|
|
20488
21100
|
// compile, before its stream registers, so a fork mid-compile is busy;
|
|
20489
21101
|
// and a turn is alive while its continuation or a provider hold's retry
|
|
20490
21102
|
// of it is on its way (hasTurn), so a closure turn held for its retry
|
|
@@ -20493,7 +21105,9 @@ export class AgentFramework {
|
|
|
20493
21105
|
if (
|
|
20494
21106
|
!this.hasTurn(agentName) &&
|
|
20495
21107
|
!this.activeStreams.has(agentName) &&
|
|
20496
|
-
!this.pendingRequests.some((r) => r.agentName === agentName)
|
|
21108
|
+
!this.pendingRequests.some((r) => r.agentName === agentName) &&
|
|
21109
|
+
!this.closureWaits(agentName) &&
|
|
21110
|
+
!this.budgetHolds?.has(agentName)
|
|
20497
21111
|
) {
|
|
20498
21112
|
this.disposeConversationAgent(agentName, 'idle-ttl');
|
|
20499
21113
|
}
|
|
@@ -21449,7 +22063,7 @@ export class AgentFramework {
|
|
|
21449
22063
|
eventType: 'mcpl:push-event',
|
|
21450
22064
|
...(occ.tags ? { tags: occ.tags } : {}),
|
|
21451
22065
|
},
|
|
21452
|
-
}, { suppressible, ...remembered });
|
|
22066
|
+
}, { suppressible, ...remembered, folds: true });
|
|
21453
22067
|
// Its batch's row, written later, reads this decision, since a rule's
|
|
21454
22068
|
// script answers once per event. (A copy of the occurrence, as a wake to
|
|
21455
22069
|
// read a row has, was decided before it was copied.)
|
|
@@ -21522,6 +22136,7 @@ export class AgentFramework {
|
|
|
21522
22136
|
};
|
|
21523
22137
|
for (const r of this.pendingRequests) unrecord(r);
|
|
21524
22138
|
for (const cooldown of this.providerAccelerationCooldowns.values()) cooldown.heldRequests.forEach(unrecord);
|
|
22139
|
+
for (const hold of this.budgetHolds?.values() ?? []) hold.wakes.forEach(unrecord);
|
|
21525
22140
|
for (const r of this.parkedContinuations ?? []) unrecord(r);
|
|
21526
22141
|
for (const pass of this.passRequests ?? []) for (const requests of pass.values()) requests.forEach(unrecord);
|
|
21527
22142
|
const withdrawn = (r: InferenceRequest): boolean => r.coalescingSubject === subject || emptied.has(r);
|
|
@@ -21531,12 +22146,13 @@ export class AgentFramework {
|
|
|
21531
22146
|
cooldown.heldRequests = cooldown.heldRequests.filter((r) => !withdrawn(r));
|
|
21532
22147
|
if (held > 0 && cooldown.heldRequests.length === 0) cooldown.emptiedBy = 'withdrawal';
|
|
21533
22148
|
}
|
|
22149
|
+
for (const hold of this.budgetHolds?.values() ?? []) hold.wakes = hold.wakes.filter((r) => !withdrawn(r));
|
|
21534
22150
|
for (const pass of this.passRequests ?? []) {
|
|
21535
22151
|
for (const requests of pass.values()) {
|
|
21536
22152
|
if (!requests.some((r) => emptied.has(r))) continue;
|
|
21537
22153
|
const left = requests.filter((r) => !emptied.has(r));
|
|
21538
22154
|
requests.length = 0;
|
|
21539
|
-
requests
|
|
22155
|
+
appendAll(requests, left);
|
|
21540
22156
|
}
|
|
21541
22157
|
}
|
|
21542
22158
|
for (const start of emptied) {
|
|
@@ -22086,7 +22702,7 @@ export class AgentFramework {
|
|
|
22086
22702
|
const agent = this.agents.get(agentName);
|
|
22087
22703
|
if (!agent) {
|
|
22088
22704
|
// Agent not found — request is orphaned. Emit warning and drop.
|
|
22089
|
-
const oldest =
|
|
22705
|
+
const oldest = requests.reduce((min, r) => Math.min(min, r.timestamp), Infinity);
|
|
22090
22706
|
this.emitTrace({
|
|
22091
22707
|
type: 'inference:request_dropped',
|
|
22092
22708
|
agentName,
|
|
@@ -22102,6 +22718,11 @@ export class AgentFramework {
|
|
|
22102
22718
|
continue;
|
|
22103
22719
|
}
|
|
22104
22720
|
|
|
22721
|
+
const budgetHold = this.budgetHolds?.get(agentName);
|
|
22722
|
+
if (budgetHold) {
|
|
22723
|
+
for (const req of requests) this.holdRequest(budgetHold.wakes, req);
|
|
22724
|
+
continue;
|
|
22725
|
+
}
|
|
22105
22726
|
const providerCooldown = this.providerAccelerationCooldowns?.get(agentName);
|
|
22106
22727
|
if (providerCooldown) {
|
|
22107
22728
|
// A provider wait binds its own model; with a different primary model
|
|
@@ -22135,7 +22756,22 @@ export class AgentFramework {
|
|
|
22135
22756
|
// passes, and the read check and the policy decide on them all; the
|
|
22136
22757
|
// merge below records the batch anew.
|
|
22137
22758
|
if (requests.some((r) => mergedBatch(r) !== undefined)) {
|
|
22138
|
-
requests = requests.flatMap((r) =>
|
|
22759
|
+
requests = requests.flatMap((r) => {
|
|
22760
|
+
const batch = mergedBatch(r);
|
|
22761
|
+
if (!batch?.length) return [r];
|
|
22762
|
+
const wait = budgetWaitOf(r);
|
|
22763
|
+
const waitMarked = wait !== undefined && !batch.some((m) => budgetWaitOf(m) !== undefined);
|
|
22764
|
+
if (waitMarked && !batch.some((m) => this.sameInferenceRequest(m, r))) {
|
|
22765
|
+
const { [MERGED_BATCH]: _batch, ...folded } = r as MarkedRequest;
|
|
22766
|
+
return [...batch, folded];
|
|
22767
|
+
}
|
|
22768
|
+
const own = r.wakeChannels?.filter((c) => !batch.some((m) => m.wakeChannels?.some((x) => sameChannel(x, c))));
|
|
22769
|
+
const channels = own?.length ? wakeChannelsOf([batch[0], { wakeChannels: own }]) : undefined;
|
|
22770
|
+
const members = channels ? [{ ...batch[0]!, wakeChannels: channels }, ...batch.slice(1)] : batch;
|
|
22771
|
+
if (!waitMarked) return members;
|
|
22772
|
+
const at = members.findIndex((m) => this.sameInferenceRequest(m, r));
|
|
22773
|
+
return members.map((m, i) => (i === at ? { ...m, [BUDGET_WAIT]: wait } as MarkedRequest : m));
|
|
22774
|
+
});
|
|
22139
22775
|
}
|
|
22140
22776
|
|
|
22141
22777
|
// A provider's stated wait on this agent's primary model, including one
|
|
@@ -22204,22 +22840,25 @@ export class AgentFramework {
|
|
|
22204
22840
|
// A heartbeat's wake keeps its own key, so a later channel-less wake
|
|
22205
22841
|
// can't absorb it: resumed together, they make a heartbeat's batch
|
|
22206
22842
|
// (heartbeatTurn), as they would have without the quiesce.
|
|
22207
|
-
const key = `${request.reason}|${request.addressed ? 'a' : 'n'}|${isHeartbeatWake(request) ? 'h' : ''}|${request.coalescingSubject ?? ''}|${request.readsRow ?? ''}`;
|
|
22843
|
+
const key = `${request.reason}|${request.addressed ? 'a' : 'n'}|${isHeartbeatWake(request) ? 'h' : ''}|${isUnprompted(request) ? 'u' : ''}|${request.coalescingSubject ?? ''}|${request.readsRow ?? ''}|${budgetWaitOf(request) ? 'waited' : ''}`;
|
|
22208
22844
|
const prev = newestByKey.get(key);
|
|
22209
22845
|
const kept = !prev || request.timestamp >= prev.timestamp ? request : prev;
|
|
22210
22846
|
// The wake it absorbs hands on its lines (WakeLine): each event's
|
|
22211
22847
|
// line stays to be decided when the host resumes.
|
|
22212
22848
|
const lines = prev ? mergeWakeLines([prev, request]) : undefined;
|
|
22213
22849
|
if (lines) kept.wakeLines = lines;
|
|
22850
|
+
const pair = prev ? [prev, request] : undefined;
|
|
22851
|
+
const channels = pair ? wakeChannelsOf(pair) : undefined;
|
|
22852
|
+
if (channels) kept.wakeChannels = channels;
|
|
22214
22853
|
newestByKey.set(key, kept);
|
|
22215
22854
|
}
|
|
22216
|
-
this.pendingRequests
|
|
22855
|
+
appendAll(this.pendingRequests, [...newestByKey.values()]);
|
|
22217
22856
|
if (passThrough.length === 0) continue;
|
|
22218
22857
|
// The continuation continues its held turn; its parked siblings must
|
|
22219
22858
|
// NOT be consumed by that turn — they were re-parked above, so narrow
|
|
22220
22859
|
// the batch to the continuation(s) alone before the trigger selection.
|
|
22221
22860
|
requests.length = 0;
|
|
22222
|
-
requests
|
|
22861
|
+
appendAll(requests, passThrough);
|
|
22223
22862
|
}
|
|
22224
22863
|
|
|
22225
22864
|
// A batch that carries a continuation which resumes its logical turn
|
|
@@ -22237,10 +22876,10 @@ export class AgentFramework {
|
|
|
22237
22876
|
if (requests.some((r) => resumesLogicalTurn(r.reason))) {
|
|
22238
22877
|
const siblings = requests.filter((r) => !isTurnContinuation(r.reason));
|
|
22239
22878
|
if (siblings.length > 0) {
|
|
22240
|
-
this.pendingRequests
|
|
22879
|
+
appendAll(this.pendingRequests, siblings);
|
|
22241
22880
|
const continuations = requests.filter((r) => isTurnContinuation(r.reason));
|
|
22242
22881
|
requests.length = 0;
|
|
22243
|
-
requests
|
|
22882
|
+
appendAll(requests, continuations);
|
|
22244
22883
|
}
|
|
22245
22884
|
}
|
|
22246
22885
|
|
|
@@ -22263,7 +22902,7 @@ export class AgentFramework {
|
|
|
22263
22902
|
// Re-queue requests, but warn if they've been pending too long.
|
|
22264
22903
|
// Suppressed while quiesced: a drain-phase busy requeue is expected,
|
|
22265
22904
|
// not a wedge tell.
|
|
22266
|
-
const oldest =
|
|
22905
|
+
const oldest = requests.reduce((min, r) => Math.min(min, r.timestamp), Infinity);
|
|
22267
22906
|
if (
|
|
22268
22907
|
!this.quiesced &&
|
|
22269
22908
|
now - oldest > STALE_REQUEST_MS &&
|
|
@@ -22295,7 +22934,7 @@ export class AgentFramework {
|
|
|
22295
22934
|
`${requests.length} request(s) waiting ${Math.round((now - oldest) / 1000)}s`,
|
|
22296
22935
|
);
|
|
22297
22936
|
}
|
|
22298
|
-
this.pendingRequests
|
|
22937
|
+
appendAll(this.pendingRequests, requests);
|
|
22299
22938
|
continue;
|
|
22300
22939
|
}
|
|
22301
22940
|
|
|
@@ -22323,7 +22962,7 @@ export class AgentFramework {
|
|
|
22323
22962
|
if (read.length > 0) {
|
|
22324
22963
|
const left = requests.filter((r) => !read.includes(r));
|
|
22325
22964
|
requests.length = 0;
|
|
22326
|
-
requests
|
|
22965
|
+
appendAll(requests, left);
|
|
22327
22966
|
console.error(`[wake] ${agentName}: dropped ${read.length} wake(s) for writes an answered request carried, and that it spoke after in the wake's channel where it carries one not marked ambient, or that were removed` +
|
|
22328
22967
|
(requests.length === 0 ? '; no turn starts' : ''));
|
|
22329
22968
|
if (requests.length === 0) {
|
|
@@ -22495,6 +23134,7 @@ export class AgentFramework {
|
|
|
22495
23134
|
// channel is that message's, never one server's id with another's.
|
|
22496
23135
|
channelId: channelReq?.channelId,
|
|
22497
23136
|
channelServerId: channelReq?.channelServerId,
|
|
23137
|
+
wakeChannels: wakeChannelsOf(requests.some(bringsOwnChannels) ? requests.filter(hasOwnCauseForRun) : requests),
|
|
22498
23138
|
// Only the mark this batch decided, never one requests[0] carried.
|
|
22499
23139
|
autoContinueRun: continuationMark,
|
|
22500
23140
|
addressed: addressedReq !== undefined,
|
|
@@ -22517,8 +23157,13 @@ export class AgentFramework {
|
|
|
22517
23157
|
const carry = requests.map(turnCarryOf).find((c) => c !== undefined);
|
|
22518
23158
|
if (carry) start[TURN_CARRY] = carry;
|
|
22519
23159
|
else delete start[TURN_CARRY];
|
|
23160
|
+
const wait = requests.map(budgetWaitOf).find((w) => w !== undefined);
|
|
23161
|
+
if (wait) start[BUDGET_WAIT] = wait;
|
|
23162
|
+
else delete start[BUDGET_WAIT];
|
|
22520
23163
|
if (requests.every(isRearmed)) start[REARMED] = true;
|
|
22521
23164
|
else delete start[REARMED];
|
|
23165
|
+
if (requests.every(isUnprompted)) start[UNPROMPTED] = true;
|
|
23166
|
+
else delete start[UNPROMPTED];
|
|
22522
23167
|
// A start merged from more than one request, any of them not the
|
|
22523
23168
|
// turn's, records the batch it was merged from (MERGED_BATCH): until
|
|
22524
23169
|
// it takes its token it stands for them all.
|
|
@@ -22546,7 +23191,7 @@ export class AgentFramework {
|
|
|
22546
23191
|
*/
|
|
22547
23192
|
private publishInputWithTurnLocus(input: unknown, agentName: string, callingTurn: TurnRouting | undefined): unknown {
|
|
22548
23193
|
const given = (input as { channelId?: unknown } | null | undefined)?.channelId;
|
|
22549
|
-
if (
|
|
23194
|
+
if (given !== undefined && given !== null) return input;
|
|
22550
23195
|
// A locked turn's default is its lock, or none: never what the registry
|
|
22551
23196
|
// resolves now, which reads the agent's latest routing (a silent start's).
|
|
22552
23197
|
const locus = callingTurn
|
|
@@ -22835,7 +23480,7 @@ export class AgentFramework {
|
|
|
22835
23480
|
logical.draftFailures = { count: (logical.draftFailures?.count ?? 0) + notHeld.count, error: notHeld.error };
|
|
22836
23481
|
}
|
|
22837
23482
|
if (held.length === 0) return held;
|
|
22838
|
-
logical.drafts
|
|
23483
|
+
appendAll(logical.drafts, held);
|
|
22839
23484
|
console.error(`[drafts] ${agent.name}: held ${held.length} segment(s) as ${held.map((d) => d.id).join(', ')} (${reason})`);
|
|
22840
23485
|
this.emitTrace({
|
|
22841
23486
|
type: 'prose:drafts-held',
|
|
@@ -23570,7 +24215,13 @@ export class AgentFramework {
|
|
|
23570
24215
|
this.holdHeartbeatProse(agent, [text], round);
|
|
23571
24216
|
return null;
|
|
23572
24217
|
}
|
|
23573
|
-
return this.channelRegistry!.routeSpeech(agent.name, text, locus);
|
|
24218
|
+
return this.channelRegistry!.routeSpeech(agent.name, text, locus, this.cameFrom(round.routing, locus));
|
|
24219
|
+
}
|
|
24220
|
+
|
|
24221
|
+
private cameFrom(routing: TurnRouting, locus: ChannelRef | null): { cameFrom: Array<{ channel: ChannelRef; shown: string }> } | undefined {
|
|
24222
|
+
return locus === null && !routing.noChannel && routing.wakeChannels
|
|
24223
|
+
? { cameFrom: routing.wakeChannels.map((c) => ({ channel: c, shown: this.channelShown(c) })) }
|
|
24224
|
+
: undefined;
|
|
23574
24225
|
}
|
|
23575
24226
|
|
|
23576
24227
|
/**
|
|
@@ -23595,6 +24246,20 @@ export class AgentFramework {
|
|
|
23595
24246
|
return routing.heartbeat && (!routing.locus || routing.locked) ? { heartbeatTurn: true } : {};
|
|
23596
24247
|
}
|
|
23597
24248
|
|
|
24249
|
+
private unpromptedTurnMark(routing: TurnRouting): { [UNPROMPTED]?: true } {
|
|
24250
|
+
return routing.unprompted && !routing.locus ? { [UNPROMPTED]: true } : {};
|
|
24251
|
+
}
|
|
24252
|
+
|
|
24253
|
+
private wakeChannelsMark(routing: TurnRouting | undefined): { wakeChannels?: ChannelRef[] } {
|
|
24254
|
+
return routing && !routing.noChannel && routing.wakeChannels ? { wakeChannels: routing.wakeChannels } : {};
|
|
24255
|
+
}
|
|
24256
|
+
|
|
24257
|
+
private turnRetryOf(trigger: InferenceRequest, routing: TurnRouting | undefined): InferenceRequest {
|
|
24258
|
+
const retry: MarkedRequest = asTurnRetry(trigger);
|
|
24259
|
+
delete retry.wakeChannels;
|
|
24260
|
+
return Object.assign(retry, this.wakeChannelsMark(routing));
|
|
24261
|
+
}
|
|
24262
|
+
|
|
23598
24263
|
/** Hold a heartbeat turn's plain speech as drafts ('no-destination'), named by `round`'s notice. */
|
|
23599
24264
|
private holdHeartbeatProse(agent: Agent, texts: string[], round: ProseRound): Draft[] {
|
|
23600
24265
|
console.error(`[prose] ${agent.name}: heartbeat turn — ${texts.length} plain-speech segment(s) held as drafts (no destination)`);
|
|
@@ -24245,7 +24910,7 @@ export class AgentFramework {
|
|
|
24245
24910
|
* and no undo or redo is made.
|
|
24246
24911
|
*/
|
|
24247
24912
|
private logicalTurnAlive(agent: Agent): boolean {
|
|
24248
|
-
return this.hasTurn(agent.name) || this.turnAlive(agent);
|
|
24913
|
+
return this.hasTurn(agent.name) || this.turnAlive(agent) || this.heldContinuation(agent.name);
|
|
24249
24914
|
}
|
|
24250
24915
|
|
|
24251
24916
|
/** Whether `agent` has a turn alive: starting, streaming or waiting on its tools. */
|
|
@@ -24977,7 +25642,7 @@ export class AgentFramework {
|
|
|
24977
25642
|
|
|
24978
25643
|
private agentSees(agent: Agent, metadata: unknown): boolean {
|
|
24979
25644
|
const sees = this.storeSees.get(agent.getContextManager());
|
|
24980
|
-
return !sees || sees(metadata);
|
|
25645
|
+
return !isReasoningLoss(metadata) && (!sees || sees(metadata));
|
|
24981
25646
|
}
|
|
24982
25647
|
|
|
24983
25648
|
/**
|
|
@@ -25628,6 +26293,8 @@ export class AgentFramework {
|
|
|
25628
26293
|
source: 'framework',
|
|
25629
26294
|
timestamp: Date.now(),
|
|
25630
26295
|
...this.heartbeatTurnMark(routing),
|
|
26296
|
+
...this.unpromptedTurnMark(routing),
|
|
26297
|
+
...this.wakeChannelsMark(routing),
|
|
25631
26298
|
});
|
|
25632
26299
|
}
|
|
25633
26300
|
|
|
@@ -25724,6 +26391,8 @@ export class AgentFramework {
|
|
|
25724
26391
|
...channel,
|
|
25725
26392
|
autoContinueRun: { stop, ...(opts.heldAfterSend ? { heldAfterSend: true } : {}) },
|
|
25726
26393
|
...this.heartbeatTurnMark(routing),
|
|
26394
|
+
...this.unpromptedTurnMark(routing),
|
|
26395
|
+
...this.wakeChannelsMark(routing),
|
|
25727
26396
|
});
|
|
25728
26397
|
}
|
|
25729
26398
|
|
|
@@ -26191,7 +26860,7 @@ export class AgentFramework {
|
|
|
26191
26860
|
}
|
|
26192
26861
|
const target = sticky ?? locus;
|
|
26193
26862
|
try {
|
|
26194
|
-
const outcome = await this.channelRegistry!.routeSpeech(agent.name, envelope, target);
|
|
26863
|
+
const outcome = await this.channelRegistry!.routeSpeech(agent.name, envelope, target, this.cameFrom(routing, target));
|
|
26195
26864
|
this.recordProseDelivery(routing, outcome);
|
|
26196
26865
|
if (outcome?.delivered) {
|
|
26197
26866
|
routing.liveSpeech = true;
|
|
@@ -26566,6 +27235,8 @@ export class AgentFramework {
|
|
|
26566
27235
|
source: 'framework',
|
|
26567
27236
|
timestamp: Date.now(),
|
|
26568
27237
|
...this.heartbeatTurnMark(round.routing),
|
|
27238
|
+
...this.unpromptedTurnMark(round.routing),
|
|
27239
|
+
...this.wakeChannelsMark(round.routing),
|
|
26569
27240
|
});
|
|
26570
27241
|
}
|
|
26571
27242
|
}
|
|
@@ -26700,7 +27371,7 @@ export class AgentFramework {
|
|
|
26700
27371
|
const batch = trigger && mergedBatch(trigger);
|
|
26701
27372
|
if (this.quiesced && batch) {
|
|
26702
27373
|
this.releasePrimaryProviderGate(agent.name);
|
|
26703
|
-
this.pendingRequests
|
|
27374
|
+
appendAll(this.pendingRequests, batch);
|
|
26704
27375
|
console.error(`[provider-admission] ${agent.name}: host quiesced while parked — the merged start goes back as its ${batch.length} requests`);
|
|
26705
27376
|
return;
|
|
26706
27377
|
}
|
|
@@ -26797,8 +27468,9 @@ export class AgentFramework {
|
|
|
26797
27468
|
// one's routing, and an ephemeral run fails with the error rather than
|
|
26798
27469
|
// waiting out its watchdog. A silent start has no part in an open turn.
|
|
26799
27470
|
if (!trigger?.suppressProse) {
|
|
27471
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
26800
27472
|
this.settleAgent(agent.name, {
|
|
26801
|
-
stopReason: 'exhausted', speech: '', error:
|
|
27473
|
+
stopReason: 'exhausted', speech: '', error: failureWords(err), cause: err,
|
|
26802
27474
|
}, this.openLogical.get(agent.name));
|
|
26803
27475
|
}
|
|
26804
27476
|
this.endStatedWaitRun(agent.name);
|
|
@@ -26822,7 +27494,7 @@ export class AgentFramework {
|
|
|
26822
27494
|
this.activeTurnTokens.delete(agent.name);
|
|
26823
27495
|
this.activeTurnTriggers.delete(agent.name);
|
|
26824
27496
|
}
|
|
26825
|
-
if (!tokenHandedOff && ownsProviderGate) this.releasePrimaryProviderGate(agent.name);
|
|
27497
|
+
if (!tokenHandedOff && ownsProviderGate && !checkWindow.gateReleased) this.releasePrimaryProviderGate(agent.name);
|
|
26826
27498
|
// A start that ends before its stream exists is its turn's end: what
|
|
26827
27499
|
// arrived during it enters its window, as at a stream's teardown,
|
|
26828
27500
|
// unless its turn lives on (a retry on its way). Left out, it would
|
|
@@ -26854,7 +27526,7 @@ export class AgentFramework {
|
|
|
26854
27526
|
if (!isContinuation(r)) left.push(r);
|
|
26855
27527
|
else {
|
|
26856
27528
|
dropped = true;
|
|
26857
|
-
if (putsBack) back
|
|
27529
|
+
if (putsBack) appendAll(back, mergedWakes(r));
|
|
26858
27530
|
}
|
|
26859
27531
|
}
|
|
26860
27532
|
return left;
|
|
@@ -26871,16 +27543,16 @@ export class AgentFramework {
|
|
|
26871
27543
|
this.parkedContinuations.delete(parked);
|
|
26872
27544
|
this.releasePrimaryProviderGate(agentName);
|
|
26873
27545
|
dropped = true;
|
|
26874
|
-
if (putsBack) back
|
|
27546
|
+
if (putsBack) appendAll(back, mergedWakes(parked));
|
|
26875
27547
|
}
|
|
26876
27548
|
for (const pass of this.passRequests) {
|
|
26877
27549
|
const requests = pass.get(agentName);
|
|
26878
27550
|
if (!requests) continue;
|
|
26879
27551
|
const left = take(requests);
|
|
26880
27552
|
requests.length = 0;
|
|
26881
|
-
requests
|
|
27553
|
+
appendAll(requests, left);
|
|
26882
27554
|
}
|
|
26883
|
-
this.pendingRequests
|
|
27555
|
+
appendAll(this.pendingRequests, back);
|
|
26884
27556
|
if (!dropped || !drop) return;
|
|
26885
27557
|
// A primary a provider hold left pending for what it released won't run
|
|
26886
27558
|
// now, when nothing else of the agent's waits (endPendingPrimary).
|
|
@@ -26902,18 +27574,19 @@ export class AgentFramework {
|
|
|
26902
27574
|
* ends on its own and settles nothing. What was stored for the agent
|
|
26903
27575
|
* meanwhile is presented now (presentAtEndOf), or at that frame's end.
|
|
26904
27576
|
*/
|
|
26905
|
-
private endContinuedTurn(agentName: string, drop: ContinuationDrop): void {
|
|
26906
|
-
|
|
27577
|
+
private endContinuedTurn(agentName: string, drop: ContinuationDrop, opensTurn = false): void {
|
|
27578
|
+
const ends = opensTurn ? 'its waiting start ends' : 'its turn ends without its continuation';
|
|
27579
|
+
console.error(`[turn-continuation] ${agentName}: ${ends}: ${drop.why}`);
|
|
26907
27580
|
const agent = this.agents.get(agentName);
|
|
26908
27581
|
const startedAt = this.lastInferenceAt.get(agentName)?.startedAt ?? Date.now();
|
|
26909
27582
|
if (agent && drop.kind !== 'dropped') {
|
|
26910
27583
|
const routing = this.openLogical.get(agentName) ?? this.routingOf(agentName);
|
|
26911
27584
|
this.endStoppedStart(agent, drop.kind === 'caller' ? { kind: 'caller', reason: drop.reason } : { kind: drop.kind },
|
|
26912
|
-
routing, startedAt, undefined,
|
|
27585
|
+
routing, startedAt, undefined, opensTurn);
|
|
26913
27586
|
} else {
|
|
26914
27587
|
const owner = this.activeTurnTokens.get(agentName);
|
|
26915
27588
|
this.abortDroppedTurn(agentName, owner ?? -1,
|
|
26916
|
-
drop.kind === 'dropped' ? CONTINUATION_DROPPED : drop.kind === 'caller' ? (drop.reason ?? 'user') : drop.kind, startedAt);
|
|
27589
|
+
drop.kind === 'dropped' ? (drop.reason ?? CONTINUATION_DROPPED) : drop.kind === 'caller' ? (drop.reason ?? 'user') : drop.kind, startedAt);
|
|
26917
27590
|
this.channelRegistry?.stopTyping();
|
|
26918
27591
|
if (agent) {
|
|
26919
27592
|
agent.toolResultGuard.recovering = false;
|
|
@@ -26931,7 +27604,7 @@ export class AgentFramework {
|
|
|
26931
27604
|
// A registered ephemeral run's caller waits on the run's settle, which
|
|
26932
27605
|
// no frame gives once its continuation is gone: the turn's end is the
|
|
26933
27606
|
// run's, with the drop's reason.
|
|
26934
|
-
this.settleAgent(agentName, { stopReason: 'exhausted', speech: '', error: `${agentName}'s turn ended without its continuation: ${drop.why}` }, undefined);
|
|
27607
|
+
this.settleAgent(agentName, { stopReason: 'exhausted', speech: '', error: `${agentName}'s ${opensTurn ? 'waiting start ended' : 'turn ended without its continuation'}: ${drop.why}` }, undefined);
|
|
26935
27608
|
}
|
|
26936
27609
|
// The turn's end; a frame that still holds its token presents at its
|
|
26937
27610
|
// own end instead.
|
|
@@ -27166,6 +27839,7 @@ export class AgentFramework {
|
|
|
27166
27839
|
* operator's abandon. */
|
|
27167
27840
|
checkWindow: CheckWindow,
|
|
27168
27841
|
): Promise<boolean> {
|
|
27842
|
+
if (attempt === 0 && this.budgetWaitEnded(agent, trigger)) return false;
|
|
27169
27843
|
const resumesTurn = resumesLogicalTurn(trigger?.reason);
|
|
27170
27844
|
let carry = turnCarryOf(trigger);
|
|
27171
27845
|
const turnProseRouting = trigger?.suppressProse ? 'disabled' : this.proseRoutingOf(agent);
|
|
@@ -27328,8 +28002,10 @@ export class AgentFramework {
|
|
|
27328
28002
|
}
|
|
27329
28003
|
|
|
27330
28004
|
// Record turn checkpoint before inference (only on first attempt, not retries)
|
|
28005
|
+
let checkpoint: RecordedTurnCheckpoint | undefined;
|
|
28006
|
+
let compileBaseline: BudgetHoldBaseline | undefined;
|
|
27331
28007
|
if (attempt === 0 && trigger?.reason !== 'tool_result_guard_retry') {
|
|
27332
|
-
this.recordTurnCheckpoint(agent.name);
|
|
28008
|
+
checkpoint = this.recordTurnCheckpoint(agent.name);
|
|
27333
28009
|
this.redoStacks.delete(agent.name); // new work invalidates redo
|
|
27334
28010
|
}
|
|
27335
28011
|
|
|
@@ -27369,6 +28045,8 @@ export class AgentFramework {
|
|
|
27369
28045
|
routing.silent = true;
|
|
27370
28046
|
if (continued?.roundsRan) routing.roundsRan = true;
|
|
27371
28047
|
if (continued?.unflushedRounds) routing.unflushedRounds = continued.unflushedRounds;
|
|
28048
|
+
if (continued?.start) routing.start = continued.start;
|
|
28049
|
+
if (continued?.firstRow !== undefined) routing.firstRow = continued.firstRow;
|
|
27372
28050
|
opensTurn = true;
|
|
27373
28051
|
} else {
|
|
27374
28052
|
const open = this.openLogical.get(agent.name);
|
|
@@ -27382,7 +28060,9 @@ export class AgentFramework {
|
|
|
27382
28060
|
}
|
|
27383
28061
|
if (opensTurn) {
|
|
27384
28062
|
const head = agent.branchHead();
|
|
27385
|
-
if (head) routing.start = head;
|
|
28063
|
+
if (head && !routing.start) routing.start = head;
|
|
28064
|
+
const firstRow = this.activeTurnTriggers.get(agent.name)?.readsRow;
|
|
28065
|
+
if (firstRow !== undefined) routing.firstRow = firstRow;
|
|
27386
28066
|
// A silent start, and an agent whose configuration turns the pin off
|
|
27387
28067
|
// (turnInputsPin: false), pin nothing, and a pin of the name left from
|
|
27388
28068
|
// before goes.
|
|
@@ -27419,12 +28099,16 @@ export class AgentFramework {
|
|
|
27419
28099
|
// no longer holds are still on disk goes in before this start's compile.
|
|
27420
28100
|
this.noticeHeadMoves(agent);
|
|
27421
28101
|
|
|
28102
|
+
const wakeChannels = wakeChannelsOf([routing, trigger]);
|
|
28103
|
+
if (wakeChannels) routing.wakeChannels = wakeChannels;
|
|
28104
|
+
|
|
27422
28105
|
if (opensTurn) {
|
|
27423
28106
|
// The turn's trigger channel (item-3 redux): the channel of a
|
|
27424
28107
|
// channel/DM-triggered turn, with its server, and none for a
|
|
27425
28108
|
// no-trigger turn, which doesn't inherit a previous turn's channel: a
|
|
27426
28109
|
// heartbeat's takes no locus at all (TurnRouting.heartbeat), and any
|
|
27427
|
-
// other falls back to the global default
|
|
28110
|
+
// other falls back to the global default, with the configured
|
|
28111
|
+
// unprompted channel in it where the turn takes it. A wake whose channel,
|
|
27428
28112
|
// given without its server, names no one registered channel (a host's
|
|
27429
28113
|
// id that two servers' channels answer to, or that none does) has none
|
|
27430
28114
|
// either, and takes no fallback: it was for a channel, and the latest
|
|
@@ -27443,9 +28127,12 @@ export class AgentFramework {
|
|
|
27443
28127
|
// speech is held as a draft (TurnRouting.heartbeat).
|
|
27444
28128
|
if (isHeartbeatWake(trigger)) routing.heartbeat = true;
|
|
27445
28129
|
else delete routing.heartbeat;
|
|
28130
|
+
if (isUnprompted(trigger)) routing.unprompted = true;
|
|
28131
|
+
else delete routing.unprompted;
|
|
27446
28132
|
// Resolve the turn's outbound locus now: the home channel for forks,
|
|
27447
28133
|
// else the trigger channel, else the global default, which an
|
|
27448
|
-
// ephemeral run never takes (takesGlobalFallback)
|
|
28134
|
+
// ephemeral run never takes (takesGlobalFallback), with the configured
|
|
28135
|
+
// unprompted channel in it where the turn takes it. Ambient mid-turn
|
|
27449
28136
|
// injections must not redirect the agent's plain prose (ambient
|
|
27450
28137
|
// chatter and reactions hijacked it before, 2026-07-21); only an
|
|
27451
28138
|
// addressed message, a follow-up in a channel the turn engaged
|
|
@@ -27523,7 +28210,9 @@ export class AgentFramework {
|
|
|
27523
28210
|
? 'at your budget restart'
|
|
27524
28211
|
: isTurnContinuation(trigger?.reason ?? '')
|
|
27525
28212
|
? 'before this re-inference on your tool results'
|
|
27526
|
-
:
|
|
28213
|
+
: budgetWaitOf(trigger)
|
|
28214
|
+
? 'while your turn waited'
|
|
28215
|
+
: 'while your turn waited on a provider hold';
|
|
27527
28216
|
this.noticeAddressedArrivals(agent, routing, arrivals, when);
|
|
27528
28217
|
}
|
|
27529
28218
|
}
|
|
@@ -27681,6 +28370,7 @@ export class AgentFramework {
|
|
|
27681
28370
|
|
|
27682
28371
|
if (carry && this.heardSince(agent, routing, carry.read)) carry = undefined;
|
|
27683
28372
|
const streamCarry: TurnCarry = { silence: carry?.silence ?? null, sends: carry?.sends ?? null, read: lastHeardAt };
|
|
28373
|
+
compileBaseline = { progress: this.compressionProgressOf(agent), settings: agent.contextSettingsChanges() };
|
|
27684
28374
|
const {
|
|
27685
28375
|
stream,
|
|
27686
28376
|
request: compiledRequest,
|
|
@@ -27691,6 +28381,7 @@ export class AgentFramework {
|
|
|
27691
28381
|
thinkingRecordedAfter,
|
|
27692
28382
|
thinkingBranch,
|
|
27693
28383
|
noteRequestSent,
|
|
28384
|
+
noteThinkingDropped,
|
|
27694
28385
|
carriedBodies,
|
|
27695
28386
|
} = await agent.startStreamWithInjections(tools, injections, undefined, compressionTools, {
|
|
27696
28387
|
silentHeartbeat: trigger?.silentHeartbeat,
|
|
@@ -27699,6 +28390,7 @@ export class AgentFramework {
|
|
|
27699
28390
|
membraneRetries: false,
|
|
27700
28391
|
inferenceBounds: this.inferenceBounds,
|
|
27701
28392
|
});
|
|
28393
|
+
if (trigger) delete (trigger as MarkedRequest)[BUDGET_WAIT];
|
|
27702
28394
|
// Each later round this stream sends is noted as it goes (noteRoundSent).
|
|
27703
28395
|
if (noteRequestSent) this.streamRequestNotes.set(stream, { note: noteRequestSent, rounds: [], stopped: false });
|
|
27704
28396
|
if (carriedBodies) this.streamCarried.set(stream, new Set(carriedBodies));
|
|
@@ -27751,7 +28443,7 @@ export class AgentFramework {
|
|
|
27751
28443
|
if (this.registry.homeChannel(agent.name) !== undefined) {
|
|
27752
28444
|
const wait = this.providerWaits?.active(agent.name, agent.model);
|
|
27753
28445
|
// The turn has begun (inference:started), so the hold keeps its retry.
|
|
27754
|
-
if (wait && this.holdForProviderWait(agent, wait, trigger ? [
|
|
28446
|
+
if (wait && this.holdForProviderWait(agent, wait, trigger ? [this.turnRetryOf(withTurnCarry(trigger, carry), routing)] : [], false, () => {
|
|
27755
28447
|
// The stream the compile made won't be sent: it's cancelled, and
|
|
27756
28448
|
// this start's window closes, before the hold is reported, so a stop
|
|
27757
28449
|
// made while inference:failed is delivered finds neither, and the
|
|
@@ -27812,6 +28504,7 @@ export class AgentFramework {
|
|
|
27812
28504
|
place: placeDrops,
|
|
27813
28505
|
...(thinkingRecordedAfter !== undefined ? { recordedAfter: thinkingRecordedAfter } : {}),
|
|
27814
28506
|
...(thinkingBranch !== undefined ? { branch: thinkingBranch } : {}),
|
|
28507
|
+
...(noteThinkingDropped ? { noteDropped: noteThinkingDropped } : {}),
|
|
27815
28508
|
}
|
|
27816
28509
|
: undefined,
|
|
27817
28510
|
streamCarry,
|
|
@@ -27862,6 +28555,17 @@ export class AgentFramework {
|
|
|
27862
28555
|
|
|
27863
28556
|
const retry = trigger && withTurnCarry(trigger, carry);
|
|
27864
28557
|
const asTransient = this.takeAsTransient(agent.name, err, trigger);
|
|
28558
|
+
if (this.holdForBudgetRefusal(agent, err, retry, checkpoint, routing, compileBaseline, () => {
|
|
28559
|
+
if (this.checkAhead.get(agent) === checkWindow) this.checkAhead.delete(agent);
|
|
28560
|
+
if (this.activeTurnTokens.get(agent.name) === turnToken) {
|
|
28561
|
+
this.activeTurnTokens.delete(agent.name);
|
|
28562
|
+
this.activeTurnTriggers.delete(agent.name);
|
|
28563
|
+
}
|
|
28564
|
+
if (ownsProviderGate && !checkWindow.gateReleased) {
|
|
28565
|
+
checkWindow.gateReleased = true;
|
|
28566
|
+
this.releasePrimaryProviderGate(agent.name);
|
|
28567
|
+
}
|
|
28568
|
+
})) return false;
|
|
27865
28569
|
if (ownsProviderGate && this.holdProviderAcceleration(agent, err, retry)) {
|
|
27866
28570
|
// Capacity scheduling is not poisoned history. The cooldown timer owns
|
|
27867
28571
|
// one later fresh compile; no immediate retry or exhaustion marker.
|
|
@@ -27921,7 +28625,7 @@ export class AgentFramework {
|
|
|
27921
28625
|
// once the retry is held finds the held retry (abortInference's
|
|
27922
28626
|
// drop), not a window nothing will collect.
|
|
27923
28627
|
if (this.checkAhead.get(agent) === checkWindow) this.checkAhead.delete(agent);
|
|
27924
|
-
await this.startAgentStream(agent, retry &&
|
|
28628
|
+
await this.startAgentStream(agent, retry && this.turnRetryOf(retry, routing), attempt + 1);
|
|
27925
28629
|
} else {
|
|
27926
28630
|
this.noteFailedTurnEnd(agent, routing);
|
|
27927
28631
|
// Terminal failure with no driveStream: this frame owns teardown.
|
|
@@ -27938,13 +28642,15 @@ export class AgentFramework {
|
|
|
27938
28642
|
this.settleAgent(agent.name, {
|
|
27939
28643
|
stopReason: 'exhausted',
|
|
27940
28644
|
speech: '',
|
|
27941
|
-
...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: err
|
|
28645
|
+
...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: failureWords(err) }),
|
|
28646
|
+
cause: err,
|
|
27942
28647
|
}, routing);
|
|
27943
28648
|
if (isRearmed(trigger) && asTransient) this.exhaustingRearms.add(agent.name);
|
|
27944
28649
|
this.emitTrace({
|
|
27945
28650
|
type: 'inference:exhausted',
|
|
27946
28651
|
agentName: agent.name,
|
|
27947
28652
|
error: err.message,
|
|
28653
|
+
...shownReasonOf(err),
|
|
27948
28654
|
// Drives the poison-history breaker (shedMayHelp: an
|
|
27949
28655
|
// `invalid_request` at HTTP 400, 413 or 422 or with no status, or a
|
|
27950
28656
|
// `context_length` at 413, suggests the provider refused the
|
|
@@ -28939,7 +29645,7 @@ export class AgentFramework {
|
|
|
28939
29645
|
this.noteRefusal(agent.name, category, tokenUsage);
|
|
28940
29646
|
countUsage(abandoned);
|
|
28941
29647
|
logStream({
|
|
28942
|
-
success: false, error: 'Tool output withheld by the guard',
|
|
29648
|
+
success: false, error: 'Tool output withheld by the guard', errorWords: 'Tool output withheld by the guard',
|
|
28943
29649
|
request: compiledRequest ?? {}, response, durationMs, tokenUsage, stopReason: 'refusal',
|
|
28944
29650
|
});
|
|
28945
29651
|
console.error(`[tool-result-guard] agent=${agent.name} withheld ${withheld.length} result(s); retrying inference`);
|
|
@@ -28998,10 +29704,12 @@ export class AgentFramework {
|
|
|
28998
29704
|
// a postscript to a message already delivered.
|
|
28999
29705
|
// Restart inference inside this logical turn. No tool is
|
|
29000
29706
|
// executed again and no settle/checkpoint/locus reset occurs.
|
|
29001
|
-
|
|
29707
|
+
const retried: InferenceRequest = {
|
|
29002
29708
|
...trigger, agentName: agent.name, reason: 'tool_result_guard_retry',
|
|
29003
29709
|
source: trigger?.source ?? 'framework', timestamp: Date.now(),
|
|
29004
|
-
}
|
|
29710
|
+
};
|
|
29711
|
+
delete retried.wakeChannels;
|
|
29712
|
+
await this.startAgentStream(agent, withTurnCarry({ ...retried, ...this.wakeChannelsMark(streamRouting) }, { silence, sends: turnSends, read: carry.read }), attempt);
|
|
29005
29713
|
return;
|
|
29006
29714
|
}
|
|
29007
29715
|
const lastResult = response.content.reduce((last, block, index) =>
|
|
@@ -29406,6 +30114,7 @@ export class AgentFramework {
|
|
|
29406
30114
|
reason: forced ? 'unstick-retry' : 'refusal-rewind-retry',
|
|
29407
30115
|
source: 'framework',
|
|
29408
30116
|
timestamp: Date.now(),
|
|
30117
|
+
...this.wakeChannelsMark(streamRouting),
|
|
29409
30118
|
});
|
|
29410
30119
|
handledByRewind = true;
|
|
29411
30120
|
};
|
|
@@ -29798,7 +30507,7 @@ export class AgentFramework {
|
|
|
29798
30507
|
|
|
29799
30508
|
logStream({
|
|
29800
30509
|
success: false,
|
|
29801
|
-
|
|
30510
|
+
...failedEntry('', err),
|
|
29802
30511
|
request: compiledRequest ?? { note: 'streaming request failed' },
|
|
29803
30512
|
durationMs,
|
|
29804
30513
|
});
|
|
@@ -29862,7 +30571,7 @@ export class AgentFramework {
|
|
|
29862
30571
|
return;
|
|
29863
30572
|
}
|
|
29864
30573
|
// Marked as its turn's retry, as at the start's failure.
|
|
29865
|
-
await this.startAgentStream(agent, retry &&
|
|
30574
|
+
await this.startAgentStream(agent, retry && this.turnRetryOf(retry, streamRouting), attempt + 1);
|
|
29866
30575
|
} else {
|
|
29867
30576
|
// The turn ends here, and its inference:failed doesn't say so,
|
|
29868
30577
|
// since a retry usually follows one: an inference:aborted
|
|
@@ -29884,13 +30593,15 @@ export class AgentFramework {
|
|
|
29884
30593
|
this.settleAgent(agent.name, {
|
|
29885
30594
|
stopReason: 'exhausted',
|
|
29886
30595
|
speech: '',
|
|
29887
|
-
...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: err
|
|
30596
|
+
...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: failureWords(err) }),
|
|
30597
|
+
cause: err,
|
|
29888
30598
|
}, this.agents.get(agent.name) === agent && agent.streamId === myStreamId ? streamRouting : undefined, streamRouting);
|
|
29889
30599
|
if (isRearmed(trigger) && asTransient) this.exhaustingRearms.add(agent.name);
|
|
29890
30600
|
this.emitTrace({
|
|
29891
30601
|
type: 'inference:exhausted',
|
|
29892
30602
|
agentName: agent.name,
|
|
29893
30603
|
error: err.message,
|
|
30604
|
+
...shownReasonOf(err),
|
|
29894
30605
|
// Drives the poison-history breaker. `retryable` is kept for
|
|
29895
30606
|
// observability, but the breaker gates on
|
|
29896
30607
|
// shedMayHelp(errorType, httpStatus): only an `invalid_request`
|
|
@@ -29978,6 +30689,7 @@ export class AgentFramework {
|
|
|
29978
30689
|
type: 'inference:exhausted',
|
|
29979
30690
|
agentName: agent.name,
|
|
29980
30691
|
error: terminal,
|
|
30692
|
+
shownReason: terminal,
|
|
29981
30693
|
});
|
|
29982
30694
|
this.rearmExhaustedTurn(agent, new Error(terminal), trigger, ownsProviderGate, streamRouting, false);
|
|
29983
30695
|
// Postmortem 2026-05-28 P2 #7: persist the abort to the
|
|
@@ -29987,6 +30699,7 @@ export class AgentFramework {
|
|
|
29987
30699
|
logStream({
|
|
29988
30700
|
success: false,
|
|
29989
30701
|
error: terminal,
|
|
30702
|
+
errorWords: terminal,
|
|
29990
30703
|
request: compiledRequest ?? { note: 'streaming request aborted' },
|
|
29991
30704
|
durationMs,
|
|
29992
30705
|
});
|
|
@@ -30093,16 +30806,33 @@ export class AgentFramework {
|
|
|
30093
30806
|
// carries what the stream appended, so once a call has dropped
|
|
30094
30807
|
// blocks, none of the stream's later calls is given either. A
|
|
30095
30808
|
// compile whose first call dropped blocks gives no sample, and the
|
|
30096
|
-
// calibration keeps its value.
|
|
30097
|
-
// model's blocks after a model switch
|
|
30098
|
-
// they drop on every call until they
|
|
30099
|
-
// calibration takes up the new model's
|
|
30809
|
+
// calibration keeps its value. Without `binds`, if the provider
|
|
30810
|
+
// drops an earlier model's blocks after a model switch
|
|
30811
|
+
// (`model_binding_mismatch`), they drop on every call until they
|
|
30812
|
+
// leave the context, and the calibration takes up the new model's
|
|
30813
|
+
// tokenizer only after that. With `binds`, the switch changes the
|
|
30814
|
+
// request's identity, so the compile leaves the replies' thinking
|
|
30815
|
+
// out itself, and a drop that does come is noted below.
|
|
30100
30816
|
const thinkingReport = event.round?.thinking;
|
|
30101
30817
|
const thinkingDropped = (thinkingReport?.dropped.length ?? 0) > 0;
|
|
30102
30818
|
if (thinkingDropped) thinkingDroppedInStream = true;
|
|
30103
30819
|
// Against the branch's binding record, when the context manager keeps one: a drop it can't account
|
|
30104
30820
|
// for tells the operators (noteThinkingLoss), and a call with no report shows no such drop.
|
|
30105
30821
|
const placedDrops = thinkingDrops && thinkingReport ? thinkingDrops.place(thinkingReport.dropped) : [];
|
|
30822
|
+
// The stored replies' bodies among them, as placed, go to the record, so the next compile sends them
|
|
30823
|
+
// without their thinking and the provider drops none of them again.
|
|
30824
|
+
const droppedBodies = placedDrops.flatMap((drop) => drop.kind === 'reply' ? drop.bodies.map((body) => body.messageId) : []);
|
|
30825
|
+
if (droppedBodies.length > 0 && thinkingDrops?.noteDropped) {
|
|
30826
|
+
try {
|
|
30827
|
+
thinkingDrops.noteDropped(droppedBodies);
|
|
30828
|
+
} catch (error) {
|
|
30829
|
+
console.error(
|
|
30830
|
+
`[thinking-binding] ${agent.name}: couldn't note the stored replies whose thinking the provider dropped, ` +
|
|
30831
|
+
'so the next compile still sends them with it:',
|
|
30832
|
+
error,
|
|
30833
|
+
);
|
|
30834
|
+
}
|
|
30835
|
+
}
|
|
30106
30836
|
if (thinkingDrops?.recordedAfter !== undefined) {
|
|
30107
30837
|
this.noteThinkingLoss(agent.name, placedDrops, thinkingDrops.recordedAfter);
|
|
30108
30838
|
}
|
|
@@ -30243,7 +30973,7 @@ export class AgentFramework {
|
|
|
30243
30973
|
// ends here (endFailureAtStop).
|
|
30244
30974
|
this.endFailureAtStop(agent.name, myTurnToken ?? -1, startTime, streamRouting);
|
|
30245
30975
|
logStream({ success: false,
|
|
30246
|
-
|
|
30976
|
+
...failedEntry('Provider acceleration cooldown: ', err),
|
|
30247
30977
|
request: compiledRequest ?? { note: 'streaming request rate-limited' }, durationMs });
|
|
30248
30978
|
this.abandonAgentScripts(agent.name, 'provider acceleration cooldown', streamRouting);
|
|
30249
30979
|
lifecyclePhase = 'failed'; agent.reset(); if (this.agents.get(agent.name) === agent && agent.streamId === myStreamId) this.eventGate?.onInferenceEnded(agent.name);
|
|
@@ -30257,7 +30987,7 @@ export class AgentFramework {
|
|
|
30257
30987
|
if (statedWait === 'held') {
|
|
30258
30988
|
this.endFailureAtStop(agent.name, myTurnToken ?? -1, startTime, streamRouting);
|
|
30259
30989
|
logStream({ success: false,
|
|
30260
|
-
|
|
30990
|
+
...failedEntry('Provider wait: ', err),
|
|
30261
30991
|
request: compiledRequest ?? { note: 'streaming request rate-limited' }, durationMs });
|
|
30262
30992
|
this.abandonAgentScripts(agent.name, 'provider wait', streamRouting);
|
|
30263
30993
|
lifecyclePhase = 'failed'; agent.reset(); if (this.agents.get(agent.name) === agent && agent.streamId === myStreamId) this.eventGate?.onInferenceEnded(agent.name);
|
|
@@ -30267,13 +30997,15 @@ export class AgentFramework {
|
|
|
30267
30997
|
this.settleAgent(agent.name, {
|
|
30268
30998
|
stopReason: 'exhausted',
|
|
30269
30999
|
speech: '',
|
|
30270
|
-
...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: err
|
|
31000
|
+
...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: failureWords(err) }),
|
|
31001
|
+
cause: err,
|
|
30271
31002
|
}, streamRouting);
|
|
30272
31003
|
if (isRearmed(trigger) && asTransient) this.exhaustingRearms.add(agent.name);
|
|
30273
31004
|
this.emitTrace({
|
|
30274
31005
|
type: 'inference:exhausted',
|
|
30275
31006
|
agentName: agent.name,
|
|
30276
31007
|
error: err.message,
|
|
31008
|
+
...shownReasonOf(err),
|
|
30277
31009
|
...this.classifyInferenceError(err),
|
|
30278
31010
|
});
|
|
30279
31011
|
this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, streamRouting, asTransient);
|
|
@@ -30282,7 +31014,7 @@ export class AgentFramework {
|
|
|
30282
31014
|
// Persist to inference log for forensic attribution.
|
|
30283
31015
|
logStream({
|
|
30284
31016
|
success: false,
|
|
30285
|
-
|
|
31017
|
+
...failedEntry('Stream threw: ', err),
|
|
30286
31018
|
request: compiledRequest ?? { note: 'streaming request threw' },
|
|
30287
31019
|
durationMs,
|
|
30288
31020
|
});
|
|
@@ -30306,13 +31038,15 @@ export class AgentFramework {
|
|
|
30306
31038
|
if (countedUsage && !usageLogged) {
|
|
30307
31039
|
const replaced = this.agents.get(agent.name) !== agent;
|
|
30308
31040
|
const superseded = generationLost || agent.streamId !== myStreamId;
|
|
31041
|
+
const ended = replaced
|
|
31042
|
+
? 'Stream abandoned: its agent was disposed or replaced'
|
|
31043
|
+
: superseded
|
|
31044
|
+
? 'Stream superseded: a newer stream of the agent took over'
|
|
31045
|
+
: 'Stream ended without a final response';
|
|
30309
31046
|
logStream({
|
|
30310
31047
|
success: false,
|
|
30311
|
-
error:
|
|
30312
|
-
|
|
30313
|
-
: superseded
|
|
30314
|
-
? 'Stream superseded: a newer stream of the agent took over'
|
|
30315
|
-
: 'Stream ended without a final response',
|
|
31048
|
+
error: ended,
|
|
31049
|
+
errorWords: ended,
|
|
30316
31050
|
request: compiledRequest ?? { note: 'streaming request' },
|
|
30317
31051
|
durationMs: Date.now() - startTime,
|
|
30318
31052
|
tokenUsage: tokenUsageOf(countedUsage),
|
|
@@ -30468,7 +31202,7 @@ export class AgentFramework {
|
|
|
30468
31202
|
// so does one whose failure a provider hold keeps for its retry: the
|
|
30469
31203
|
// turn isn't over (pendingContinuationAgents).
|
|
30470
31204
|
if (frameReachedTerminal && ownsPhysicalStream && this.closingConversationAgents.has(agent.name)
|
|
30471
|
-
&& !this.pendingContinuationAgents().has(agent.name)) {
|
|
31205
|
+
&& !this.pendingContinuationAgents().has(agent.name) && !this.closureWaits(agent.name)) {
|
|
30472
31206
|
this.disposeConversationAgent(agent.name, 'idle-ttl');
|
|
30473
31207
|
}
|
|
30474
31208
|
}
|
|
@@ -30573,7 +31307,7 @@ export class AgentFramework {
|
|
|
30573
31307
|
`puppet refused: the store is under live ${this.surgeryHold.verb} for ${this.surgeryHold.agentName} — retry when it completes`,
|
|
30574
31308
|
);
|
|
30575
31309
|
}
|
|
30576
|
-
if (agent.state.status !== 'idle' || this.hasTurn(agentName)) {
|
|
31310
|
+
if (agent.state.status !== 'idle' || this.hasTurn(agentName) || this.heldContinuation(agentName)) {
|
|
30577
31311
|
const shown = agent.state.status === 'idle' ? 'idle+turn-alive' : agent.state.status;
|
|
30578
31312
|
// A turn a provider hold keeps for its retry has no stream: the refusal
|
|
30579
31313
|
// names the hold, and how to go on sooner, as surgery's does.
|
|
@@ -30581,6 +31315,9 @@ export class AgentFramework {
|
|
|
30581
31315
|
throw new Error(held
|
|
30582
31316
|
? `puppet refused: agent ${agentName} is idle+turn-alive (${held.says}; requires idle); ` +
|
|
30583
31317
|
heldTurnWayOn(held.statedWait)
|
|
31318
|
+
: agent.state.status === 'idle' && !this.hasTurn(agentName)
|
|
31319
|
+
? `puppet refused: agent ${agentName} is idle+turn-alive (its turn waits for compression to make room; ` +
|
|
31320
|
+
'requires idle); abortInference() ends that turn'
|
|
30584
31321
|
: `puppet refused: agent ${agentName} is ${shown} (requires idle — ` +
|
|
30585
31322
|
`injecting a turn under an active stream corrupts wire ordering)`,
|
|
30586
31323
|
);
|
|
@@ -32430,6 +33167,7 @@ export class AgentFramework {
|
|
|
32430
33167
|
// The failure text is stored inline in every record; the request that
|
|
32431
33168
|
// produced it is already stored separately as a blob.
|
|
32432
33169
|
if (typeof entryToStore.error === 'string') entryToStore.error = boundFailureText(entryToStore.error);
|
|
33170
|
+
if (typeof entryToStore.errorWords === 'string') entryToStore.errorWords = boundFailureText(entryToStore.errorWords);
|
|
32433
33171
|
|
|
32434
33172
|
// Blob threshold: 10KB - typical context-heavy requests exceed this
|
|
32435
33173
|
const BLOB_THRESHOLD = 10000;
|
|
@@ -32659,19 +33397,21 @@ export class AgentFramework {
|
|
|
32659
33397
|
|
|
32660
33398
|
/**
|
|
32661
33399
|
* Why this host refuses a skip_reply call, as the model will read it, or
|
|
32662
|
-
* undefined when it doesn't
|
|
32663
|
-
*
|
|
32664
|
-
*
|
|
32665
|
-
*
|
|
32666
|
-
*
|
|
32667
|
-
*
|
|
32668
|
-
*
|
|
32669
|
-
*
|
|
32670
|
-
*
|
|
33400
|
+
* undefined when it doesn't: a reason that isn't a string
|
|
33401
|
+
* (ChannelRegistry.nonStringRefusal), or (skipReplyWakeRefusal) without an
|
|
33402
|
+
* EventGate, any wake_in_seconds, and with one, a wake_in_seconds that isn't
|
|
33403
|
+
* a number of seconds. Only when the channel registry handles skip_reply.
|
|
33404
|
+
* The dispatch that refuses the call reads it here, and so does the turn's
|
|
33405
|
+
* bookkeeping of a refused round (roundCallOf), for the model's calls and a
|
|
33406
|
+
* script's calls that reach the host while a call of the round watches it:
|
|
33407
|
+
* the silence before it, which comes back after its round, and in XML mode
|
|
33408
|
+
* which round's prose stays private. A skip_reply round's own prose stays
|
|
33409
|
+
* private by the tool's name, refused or not.
|
|
32671
33410
|
*/
|
|
32672
33411
|
private skipReplyRefusal(call: { name?: string; input?: unknown }): string | undefined {
|
|
32673
33412
|
if (call.name !== 'skip_reply' || !this.channelRegistry) return undefined;
|
|
32674
|
-
return
|
|
33413
|
+
return this.channelRegistry.nonStringRefusal('skip_reply', call.input)?.error
|
|
33414
|
+
?? skipReplyWakeRefusal(readSkipReplyWake(call.input), !!this.eventGate);
|
|
32675
33415
|
}
|
|
32676
33416
|
|
|
32677
33417
|
private refusesSelfWake(call: { name?: string; input?: unknown }): boolean {
|
|
@@ -32682,8 +33422,8 @@ export class AgentFramework {
|
|
|
32682
33422
|
* A call as its round's routing counts it, made directly, or by a script
|
|
32683
33423
|
* and reaching the host while a run or wait call of the round watches it
|
|
32684
33424
|
* (ScriptRound). A skip_reply refused, because the agent's allowedTools
|
|
32685
|
-
* doesn't name it, because its arguments are refused (markedInputRefusal)
|
|
32686
|
-
* or because this host
|
|
33425
|
+
* doesn't name it, because its arguments are refused (markedInputRefusal),
|
|
33426
|
+
* or because this host refuses its arguments (skipReplyRefusal), is
|
|
32687
33427
|
* marked refused: it keeps its own round private and silences nothing after
|
|
32688
33428
|
* it. A refusal withholds the call's effect, not the model's choice to keep
|
|
32689
33429
|
* its words private. Any other call the list refuses posted nothing, so it
|
|
@@ -32934,7 +33674,7 @@ export class AgentFramework {
|
|
|
32934
33674
|
const error = this.skipReplyRefusal(enrichedCall);
|
|
32935
33675
|
if (error !== undefined) {
|
|
32936
33676
|
const wakeValue = (enrichedCall.input as { wake_in_seconds?: unknown } | null | undefined)?.wake_in_seconds;
|
|
32937
|
-
console.error(`[self-wake] agent=${agentName} wake_in_seconds=${logValue(wakeValue)} skip_reply refused: ${logDetail(
|
|
33677
|
+
console.error(`[self-wake] agent=${agentName} wake_in_seconds=${logValue(wakeValue)} skip_reply refused: ${logDetail(error)}`);
|
|
32938
33678
|
const result = this.refusedResult(agentName, enrichedCall.id, error);
|
|
32939
33679
|
this.emitTrace({ type: 'tool:failed', module: 'channels', tool: enrichedCall.name, callId: enrichedCall.id, error });
|
|
32940
33680
|
this.pushEvent({ type: 'tool-result', callId: enrichedCall.id, toolName: enrichedCall.name, agentName, moduleName: 'channels', result });
|
|
@@ -34538,6 +35278,7 @@ export class AgentFramework {
|
|
|
34538
35278
|
{
|
|
34539
35279
|
httpStatus: event.httpStatus as number | undefined,
|
|
34540
35280
|
providerErrorCode: event.providerErrorCode as string | undefined,
|
|
35281
|
+
shownReason: event.shownReason as string | undefined,
|
|
34541
35282
|
},
|
|
34542
35283
|
);
|
|
34543
35284
|
} else if (event.type === 'inference:completed') {
|
|
@@ -34600,6 +35341,7 @@ export class AgentFramework {
|
|
|
34600
35341
|
const bounded = traceEvent as unknown as Record<string, unknown>;
|
|
34601
35342
|
if (typeof bounded.error === 'string') bounded.error = boundFailureText(bounded.error);
|
|
34602
35343
|
if (typeof bounded.stack === 'string') bounded.stack = boundFailureText(bounded.stack);
|
|
35344
|
+
if (typeof bounded.shownReason === 'string') bounded.shownReason = boundFailureText(bounded.shownReason);
|
|
34603
35345
|
|
|
34604
35346
|
if (this.earlyTraces) {
|
|
34605
35347
|
if (this.earlyTraces.length < 1000) this.earlyTraces.push(traceEvent);
|
|
@@ -34696,6 +35438,19 @@ export class AgentFramework {
|
|
|
34696
35438
|
};
|
|
34697
35439
|
})(),
|
|
34698
35440
|
lastInference: this.lastInferenceAt.get(name) ?? null,
|
|
35441
|
+
budgetHold: (() => {
|
|
35442
|
+
const hold = this.budgetHolds?.get(name);
|
|
35443
|
+
if (!hold) return null;
|
|
35444
|
+
const readiness = this.contextReadiness(agent.getContextManager());
|
|
35445
|
+
return {
|
|
35446
|
+
since: hold.startedAt,
|
|
35447
|
+
pending: 'pending' in readiness && readiness.pending ? boundFailureText(readiness.pending) : null,
|
|
35448
|
+
heldRequests: 1 + hold.wakes.flatMap((r) => mergedBatch(r) ?? [r]).filter((r) => !passesQuiesce(r)).length,
|
|
35449
|
+
errorType: hold.errorType,
|
|
35450
|
+
...(hold.budget !== undefined ? { budget: hold.budget } : {}),
|
|
35451
|
+
...(hold.actual !== undefined ? { actual: hold.actual } : {}),
|
|
35452
|
+
};
|
|
35453
|
+
})(),
|
|
34699
35454
|
refusalStats: this.refusalStats.get(name) ?? null,
|
|
34700
35455
|
...this.upcomingWakesHealth(name),
|
|
34701
35456
|
})),
|
|
@@ -34723,7 +35478,8 @@ export class AgentFramework {
|
|
|
34723
35478
|
|
|
34724
35479
|
/**
|
|
34725
35480
|
* Ops alert — the one escalation path for "a human should hear about this":
|
|
34726
|
-
* (1) durable failures.log record (unless the caller already wrote one
|
|
35481
|
+
* (1) durable failures.log record (unless the caller already wrote one, or
|
|
35482
|
+
* the alert keeps none, as a budget hold's open and clear don't),
|
|
34727
35483
|
* (2) an `ops:alert` trace so authorized observers get it on the wire,
|
|
34728
35484
|
* (3) if CONNECTOME_OPS_WEBHOOK is set, a Discord post throttled to one per
|
|
34729
35485
|
* (agent, kind) per cooldown window — a persistent failure re-posts
|
|
@@ -34759,7 +35515,7 @@ export class AgentFramework {
|
|
|
34759
35515
|
const notes = this.streamRequestNotes.get(stream);
|
|
34760
35516
|
if (!notes || notes.stopped) return;
|
|
34761
35517
|
if (unvouched) { notes.stopped = true; return; }
|
|
34762
|
-
notes.rounds
|
|
35518
|
+
appendAll(notes.rounds, rows);
|
|
34763
35519
|
this.noteStreamRows(agent, notes);
|
|
34764
35520
|
}
|
|
34765
35521
|
|
|
@@ -35000,17 +35756,8 @@ export class AgentFramework {
|
|
|
35000
35756
|
...(providerErrorCode !== undefined ? { providerErrorCode } : {}),
|
|
35001
35757
|
};
|
|
35002
35758
|
}
|
|
35003
|
-
|
|
35004
|
-
|
|
35005
|
-
}
|
|
35006
|
-
// context-manager's fatal coverage invariant (fix/coverage-invariant-fatal,
|
|
35007
|
-
// 2026-07-26): a compile REFUSES rather than shipping a context with
|
|
35008
|
-
// silently-dropped messages. Recovery is identical to over_budget — kick
|
|
35009
|
-
// the compression drain so summaries cover the un-represented span.
|
|
35010
|
-
if (err instanceof UncoveredDropError || err.name === 'UncoveredDropError') {
|
|
35011
|
-
return { errorType: 'context_refusal' };
|
|
35012
|
-
}
|
|
35013
|
-
return {};
|
|
35759
|
+
const refusal = compileRefusalType(err);
|
|
35760
|
+
return refusal !== undefined ? { errorType: refusal } : {};
|
|
35014
35761
|
}
|
|
35015
35762
|
|
|
35016
35763
|
/**
|
|
@@ -35029,7 +35776,7 @@ export class AgentFramework {
|
|
|
35029
35776
|
rawReason: string,
|
|
35030
35777
|
retryable?: boolean,
|
|
35031
35778
|
rawErrorType?: string,
|
|
35032
|
-
detail?: { httpStatus?: number; providerErrorCode?: string },
|
|
35779
|
+
detail?: { httpStatus?: number; providerErrorCode?: string; shownReason?: string },
|
|
35033
35780
|
): void {
|
|
35034
35781
|
const ended = this.exhaustedTurns?.get(agentName);
|
|
35035
35782
|
this.exhaustedTurns?.delete(agentName);
|
|
@@ -35041,10 +35788,12 @@ export class AgentFramework {
|
|
|
35041
35788
|
// sink below gets an excerpt cut from the whole reason at its own bound
|
|
35042
35789
|
// (boundFailureText, so its marker states the whole length): the stderr
|
|
35043
35790
|
// line, failures.log and the hard-down alert FAILURE_TEXT_MAX_CHARS, the
|
|
35044
|
-
// [inference-failed] marker FAILURE_MARKER_REASON_MAX_CHARS, the
|
|
35045
|
-
// lastError and the compile-refused alert FAILURE_SUMMARY_MAX_CHARS,
|
|
35046
|
-
// the rewind marker REWIND_MARKER_REASON_MAX_CHARS. The
|
|
35047
|
-
// travels as structured fields beside the text.
|
|
35791
|
+
// [inference-failed] marker's metadata FAILURE_MARKER_REASON_MAX_CHARS, the
|
|
35792
|
+
// status's lastError and the compile-refused alert FAILURE_SUMMARY_MAX_CHARS,
|
|
35793
|
+
// and the rewind marker's metadata REWIND_MARKER_REASON_MAX_CHARS. The
|
|
35794
|
+
// classification travels as structured fields beside the text. The
|
|
35795
|
+
// [inference-failed] marker's text quotes only `shownReason`, which holds
|
|
35796
|
+
// no provider's text, and the rewind marker's quotes no reason.
|
|
35048
35797
|
//
|
|
35049
35798
|
// A reason that isn't a string (the stream paths always pass a message)
|
|
35050
35799
|
// is described rather than allowed to throw before anything is recorded.
|
|
@@ -35092,9 +35841,12 @@ export class AgentFramework {
|
|
|
35092
35841
|
const agent = this.agents.get(agentName);
|
|
35093
35842
|
if (agent && !(rearmed && !ended) && process.env.SUPPRESS_INFERENCE_FAILED_MARKER !== '1') {
|
|
35094
35843
|
const timedOut = errorType === 'timeout';
|
|
35095
|
-
const causeParts = [errorType, httpStatus !== undefined ? `HTTP ${httpStatus}` : undefined]
|
|
35844
|
+
const causeParts = errorType === 'safety' ? [] : [errorType, httpStatus !== undefined ? `HTTP ${httpStatus}` : undefined]
|
|
35096
35845
|
.filter((part): part is string => part !== undefined);
|
|
35097
35846
|
const cause = timedOut && causeParts.length === 1 ? '' : causeParts.join(', ');
|
|
35847
|
+
const shownText = typeof detail?.shownReason === 'string'
|
|
35848
|
+
? failureShown(boundFailureText(detail.shownReason, FAILURE_MARKER_REASON_MAX_CHARS)) : undefined;
|
|
35849
|
+
const shown = shownText !== undefined ? ` Reason: ${shownText}${/[.!?]$/.test(shownText) ? '' : '.'}` : '';
|
|
35098
35850
|
try {
|
|
35099
35851
|
storeOwnRow(
|
|
35100
35852
|
agent,
|
|
@@ -35109,11 +35861,11 @@ export class AgentFramework {
|
|
|
35109
35861
|
`${ended.roundsRan ? ' Its earlier rounds ran, and their tool calls ran as their results show.' : ''}` +
|
|
35110
35862
|
`${ended.callsCutOff ? ` ${CALLS_CUT_OFF}` : ''}` +
|
|
35111
35863
|
`${ended.liveSpeech ? ` ${LIVE_SPEECH_UNRECEIPTED}` : ''}` +
|
|
35112
|
-
`${ended.speechRouting ? ` ${SPEECH_STILL_ROUTING}` : ''}
|
|
35864
|
+
`${ended.speechRouting ? ` ${SPEECH_STILL_ROUTING}` : ''}`
|
|
35113
35865
|
: `[${NOTICE_TAG.inferenceFailed}] Your previous turn did not complete: ` +
|
|
35114
35866
|
`the model call ${timedOut ? 'timed out' : 'failed'}${cause ? ` (${suppliedValue(cause)})` : ''} ` +
|
|
35115
|
-
`and produced no response, so nothing was sent.${madeAgain ? ` ${MADE_AGAIN}` : ''}
|
|
35116
|
-
`${
|
|
35867
|
+
`and produced no response, so nothing was sent.${madeAgain ? ` ${MADE_AGAIN}` : ''}`) +
|
|
35868
|
+
`${shown} ${timedOut ? TIMEOUT_ADVICE : FAILURE_ADVICE}`,
|
|
35117
35869
|
}],
|
|
35118
35870
|
{ system: true, kind: 'inference-failed', reason: markerReason, consecutive: streak, ...classification, ...cutAt(markerReason) },
|
|
35119
35871
|
);
|
|
@@ -35157,46 +35909,7 @@ export class AgentFramework {
|
|
|
35157
35909
|
// runUntilIdle() has no maintenance timer, so a drain is its recovery.
|
|
35158
35910
|
const drainState = !overBudget ? undefined
|
|
35159
35911
|
: !agent ? 'unavailable (no agent handle)'
|
|
35160
|
-
: this.
|
|
35161
|
-
: this.surgeryHold ? 'not kicked: a surgery holds the store'
|
|
35162
|
-
: this.overBudgetDrainInFlight.has(agentName) ? 'already draining'
|
|
35163
|
-
: 'kicking';
|
|
35164
|
-
if (agent && drainState === 'kicking') {
|
|
35165
|
-
this.overBudgetDrainInFlight.add(agentName);
|
|
35166
|
-
// Like a maintenance pass: none starts once stop() has begun, it starts
|
|
35167
|
-
// no tick while a surgery holds the store or once its agent has been
|
|
35168
|
-
// disposed, and a surgery, a switchBranch or stop() waits for the tick
|
|
35169
|
-
// in flight. It joins the drains they wait for before the alert is
|
|
35170
|
-
// raised and before its body runs, so the alert's listeners find it:
|
|
35171
|
-
// one that begins stop() or a surgery waits for it, and its body then
|
|
35172
|
-
// ticks none and says why. So does a stop() made from inside its first
|
|
35173
|
-
// tick, before that tick's first await.
|
|
35174
|
-
const drain = Promise.resolve().then(async () => {
|
|
35175
|
-
let ticks = 0;
|
|
35176
|
-
try {
|
|
35177
|
-
const cm = agent.getContextManager();
|
|
35178
|
-
while (ticks < 8 && !this.providerAdmissionClosed && !this.surgeryHold && this.agents.get(agentName) === agent) {
|
|
35179
|
-
await cm.tick();
|
|
35180
|
-
ticks++;
|
|
35181
|
-
}
|
|
35182
|
-
const stopped = ticks >= 8 ? ''
|
|
35183
|
-
: this.agents.get(agentName) !== agent ? ', stopped as the agent was disposed'
|
|
35184
|
-
: this.providerAdmissionClosed ? ', stopped as the framework stops'
|
|
35185
|
-
: ', stopped while a surgery holds the store';
|
|
35186
|
-
console.error(`[inference-failed] drain kicked for ${agentName} (OverBudget breaker, ${ticks} ticks${stopped})`);
|
|
35187
|
-
} catch (err) {
|
|
35188
|
-
if (err instanceof MaintenanceWithdrawnError) {
|
|
35189
|
-
console.error(`[inference-failed] drain kicked for ${agentName} (OverBudget breaker, ${ticks} ticks, stopped: ${err.message})`);
|
|
35190
|
-
} else {
|
|
35191
|
-
console.error(`[inference-failed] drain kick failed for ${agentName} after ${ticks} ticks: ${describeFailureWithFrames(err)}`);
|
|
35192
|
-
}
|
|
35193
|
-
} finally {
|
|
35194
|
-
this.overBudgetDrainInFlight.delete(agentName);
|
|
35195
|
-
}
|
|
35196
|
-
});
|
|
35197
|
-
this.overBudgetDrains.add(drain);
|
|
35198
|
-
void drain.finally(() => { this.overBudgetDrains.delete(drain); });
|
|
35199
|
-
}
|
|
35912
|
+
: this.kickOverBudgetDrain(agentName, agent);
|
|
35200
35913
|
if (overBudget) {
|
|
35201
35914
|
this.opsAlert(
|
|
35202
35915
|
'context-refusal',
|
|
@@ -35296,6 +36009,7 @@ export class AgentFramework {
|
|
|
35296
36009
|
reason: 'inference-failure-rewind-retry',
|
|
35297
36010
|
source: 'framework',
|
|
35298
36011
|
timestamp: Date.now(),
|
|
36012
|
+
...this.wakeChannelsMark(this.turnRouting?.get(agent.name)),
|
|
35299
36013
|
});
|
|
35300
36014
|
}
|
|
35301
36015
|
|
|
@@ -35502,7 +36216,8 @@ export class AgentFramework {
|
|
|
35502
36216
|
// a later start of the turn is told where its unprefixed speech lands
|
|
35503
36217
|
// unless a hybrid `>>>` destination takes it. Empty for a no-trigger
|
|
35504
36218
|
// turn until its locus is resolved, so that resolution takes the
|
|
35505
|
-
// global fallback
|
|
36219
|
+
// global fallback, the configured unprompted channel among it (none
|
|
36220
|
+
// for an ephemeral run), and the turn then keeps
|
|
35506
36221
|
// that channel; null for a heartbeat's turn, which takes none
|
|
35507
36222
|
// (TurnRouting.heartbeat).
|
|
35508
36223
|
// A turn with no channel of its own (a heartbeat's, or a wake for a
|
|
@@ -35521,7 +36236,9 @@ export class AgentFramework {
|
|
|
35521
36236
|
// message's channel is nobody's choice for it (#551). By name, so a
|
|
35522
36237
|
// speech dispatch that comes after the run's release still counts.
|
|
35523
36238
|
takesGlobalFallback: (conversationId) => this.usedEphemeralAgentNames.get(conversationId) !== 'run',
|
|
36239
|
+
takesUnpromptedChannel: (conversationId) => this.openLogical.get(conversationId)?.unprompted === true,
|
|
35524
36240
|
eventGatePresent: this.eventGate !== null,
|
|
36241
|
+
unpromptedChannel: this.mcplUnpromptedChannel ?? undefined,
|
|
35525
36242
|
// A channel tool reads the connect and restart marks a tool call's
|
|
35526
36243
|
// "connecting" text reads (mcplConnectInFlight, mcplServerAbsentText).
|
|
35527
36244
|
// This host is connecting to a server while a connect or restart of
|
|
@@ -35541,7 +36258,7 @@ export class AgentFramework {
|
|
|
35541
36258
|
// or wasn't confirmed to. addMessage() alone does not request
|
|
35542
36259
|
// inference, so this never wakes her (matching the
|
|
35543
36260
|
// `discord-send-failed-skip` gate intent: context yes, wake no).
|
|
35544
|
-
onRouteFailure: ({ conversationId, channelId, serverId, reason, textLen, outcome, detail }) => {
|
|
36261
|
+
onRouteFailure: ({ conversationId, channelId, serverId, reason, textLen, outcome, detail, byDesign }) => {
|
|
35545
36262
|
try {
|
|
35546
36263
|
// The marker is `system: true`, so it is never conversational and
|
|
35547
36264
|
// never influences routing. The channel is named by its label when
|
|
@@ -35571,7 +36288,7 @@ export class AgentFramework {
|
|
|
35571
36288
|
// have been posted (in part or whole). Never claim it wasn't.
|
|
35572
36289
|
? `[${NOTICE_TAG.discordSendFailed}] Delivery of your previous reply (${textLen} chars) to ${where} was not confirmed (${suppliedValue(reason)}). It may or may not have been posted, so sending it again could post it twice. It is saved in your archive.`
|
|
35573
36290
|
: `[${NOTICE_TAG.discordSendFailed}] Your previous reply (${textLen} chars) could not be delivered to ${where} (${suppliedValue(reason)}). Nothing was posted; it is saved in your archive.`)
|
|
35574
|
-
: `[${NOTICE_TAG.sendUndeliverable}] Your previous reply (${textLen} chars) had no channel to go to — ${suppliedValue(reason)}. This is a routing/configuration situation, not a channel failure. This route delivered it nowhere; if another surface showed it (a reply thread, the console), that copy stands. It is saved in your archive.`;
|
|
36291
|
+
: `[${NOTICE_TAG.sendUndeliverable}] Your previous reply (${textLen} chars) had no channel to go to — ${suppliedValue(reason)}. This is a ${byDesign ? 'routing' : 'routing/configuration'} situation, not a channel failure. This route delivered it nowhere; if another surface showed it (a reply thread, the console), that copy stands. It is saved in your archive.`;
|
|
35575
36292
|
// A registered speaker's notice is addressed to it (noticeTargetFor).
|
|
35576
36293
|
this.addMessage(
|
|
35577
36294
|
'user',
|
|
@@ -37884,8 +38601,9 @@ export class AgentFramework {
|
|
|
37884
38601
|
closedTab: boolean;
|
|
37885
38602
|
retrying: boolean;
|
|
37886
38603
|
/** Which engine serves it: `legacy` (MCP 2024-11-05 + MCPL) or `modern`
|
|
37887
|
-
* (MCP 2026-07-28
|
|
37888
|
-
* configuration
|
|
38604
|
+
* (the SDK's client: MCP 2026-07-28, or over HTTP a 2025 revision), as
|
|
38605
|
+
* its configuration selects; null when the configuration names no
|
|
38606
|
+
* usable transport (see `refused`). */
|
|
37889
38607
|
family: 'legacy' | 'modern' | null;
|
|
37890
38608
|
/** The MCP revision its last handshake established; null until one has. */
|
|
37891
38609
|
protocolVersion: string | null;
|
|
@@ -38942,7 +39660,7 @@ export class AgentFramework {
|
|
|
38942
39660
|
.filter(([, owner]) => owner === serverId)
|
|
38943
39661
|
.map(([name]) => name);
|
|
38944
39662
|
const known = new Set(names);
|
|
38945
|
-
tools
|
|
39663
|
+
appendAll(tools, (this.mcplTools ?? []).filter((tool) => known.has(tool.name)));
|
|
38946
39664
|
for (const name of names) {
|
|
38947
39665
|
toolServers.set(name, serverId);
|
|
38948
39666
|
const featureSet = this.mcplToolFeatureSets?.get(name);
|
|
@@ -38952,7 +39670,7 @@ export class AgentFramework {
|
|
|
38952
39670
|
}
|
|
38953
39671
|
kept.set(serverId, names.length);
|
|
38954
39672
|
} else if (listedServers.has(serverId)) {
|
|
38955
|
-
tools
|
|
39673
|
+
appendAll(tools, staged.tools);
|
|
38956
39674
|
for (const [name, featureSet] of staged.featureSets) toolFeatureSets.set(name, featureSet);
|
|
38957
39675
|
for (const [name, classes] of staged.classes) toolClasses.set(name, classes);
|
|
38958
39676
|
for (const [name, owner] of staged.servers) toolServers.set(name, owner);
|
|
@@ -38965,7 +39683,7 @@ export class AgentFramework {
|
|
|
38965
39683
|
console.error(`[mcpl] server "${serverId}": tools/list failed (${error}); ${count > 0 ? `keeping its ${count} known tool(s)` : 'none of its tools are offered'}`);
|
|
38966
39684
|
}
|
|
38967
39685
|
}
|
|
38968
|
-
tools
|
|
39686
|
+
appendAll(tools, over.size > 0 ? modernTools.filter((tool) => toolServers.has(tool.name)) : modernTools);
|
|
38969
39687
|
this.mcplToolsRefreshPublished = generation;
|
|
38970
39688
|
this.mcplTools = tools;
|
|
38971
39689
|
this.mcplPublishedConnections = publishedConnections;
|
|
@@ -39943,6 +40661,11 @@ export class AgentFramework {
|
|
|
39943
40661
|
return;
|
|
39944
40662
|
}
|
|
39945
40663
|
if (call.name === 'channel_publish' || call.name === 'channel_close') {
|
|
40664
|
+
const refusal = this.channelRegistry?.nonStringRefusal(call.name, call.input);
|
|
40665
|
+
if (refusal) {
|
|
40666
|
+
reject(refusal.error!);
|
|
40667
|
+
return;
|
|
40668
|
+
}
|
|
39946
40669
|
const input = (call.input ?? {}) as { channelId?: string; serverId?: string };
|
|
39947
40670
|
// An empty serverId is read as none given, as the channel tools and
|
|
39948
40671
|
// channelNamed read it.
|
|
@@ -40484,7 +41207,7 @@ export class AgentFramework {
|
|
|
40484
41207
|
.filter(([field]) => !(field.startsWith('behavior.') && typeof input.behavior === 'object')));
|
|
40485
41208
|
const policy = this.eventGate!.addPolicy(rawPolicy, {
|
|
40486
41209
|
...(position ? { position } : {}),
|
|
40487
|
-
onIgnoredFields: (notes) => { ignored
|
|
41210
|
+
onIgnoredFields: (notes) => { appendAll(ignored, notes); },
|
|
40488
41211
|
fieldNames,
|
|
40489
41212
|
});
|
|
40490
41213
|
const after = this.eventGate!.probeTable();
|
|
@@ -40816,8 +41539,9 @@ export class AgentFramework {
|
|
|
40816
41539
|
/**
|
|
40817
41540
|
* Whether `agent`'s view leaves suppressed rows out, asked of the view
|
|
40818
41541
|
* itself (storeSees): every resident's, the subconscious's and a view
|
|
40819
|
-
* derived from one does
|
|
40820
|
-
* so a row stamped
|
|
41542
|
+
* derived from one does. An isolated agent's own view excludes legacy
|
|
41543
|
+
* reasoning-loss notices, not suppressed rows, so a suppressed row stamped
|
|
41544
|
+
* there would render.
|
|
40821
41545
|
*/
|
|
40822
41546
|
private viewHidesSuppressed(agent: Agent | undefined): boolean {
|
|
40823
41547
|
if (!agent) return false;
|
|
@@ -40864,7 +41588,7 @@ export class AgentFramework {
|
|
|
40864
41588
|
* this occurrence earlier (a coalesced push's, at its wake), used instead
|
|
40865
41589
|
* of asking again, since a rule's script answers once per event.
|
|
40866
41590
|
*/
|
|
40867
|
-
opts: { suppressible?: boolean; decided?: string | null } = {},
|
|
41591
|
+
opts: { suppressible?: boolean; decided?: string | null; folds?: boolean } = {},
|
|
40868
41592
|
): AdmissionDecision & { gateDecision?: GateDecision } {
|
|
40869
41593
|
// The gate's whole decision, for wake onboarding (which rule let the event
|
|
40870
41594
|
// go by); admission itself reads only whether it wakes.
|
|
@@ -40882,6 +41606,7 @@ export class AgentFramework {
|
|
|
40882
41606
|
tags: Array.isArray(e.gateMetadata.tags) ? (e.gateMetadata.tags as string[]) : undefined,
|
|
40883
41607
|
...(e.targetAgents !== undefined ? { recipients: e.targetAgents.filter((name) => this.agents.has(name)) } : {}),
|
|
40884
41608
|
...(asked ? { suppressionAsked: true } : {}),
|
|
41609
|
+
...(opts.folds ? { folds: true as const } : {}),
|
|
40885
41610
|
});
|
|
40886
41611
|
const decision = admit(env, {
|
|
40887
41612
|
untargetedRecipients: () => this.registry.untargetedRecipients(),
|
|
@@ -41241,7 +41966,7 @@ export class AgentFramework {
|
|
|
41241
41966
|
console.error(`[sleep] ${agentName}: a heartbeat turn has no channel — sleep announcement not posted`);
|
|
41242
41967
|
} else {
|
|
41243
41968
|
const locus = callingTurn.locus ?? null;
|
|
41244
|
-
registry.routeSpeech(agentName, announcement(locus), locus).then(noted).catch((err) => {
|
|
41969
|
+
registry.routeSpeech(agentName, announcement(locus), locus, this.cameFrom(callingTurn, locus)).then(noted).catch((err) => {
|
|
41245
41970
|
console.error('[sleep] announce failed:', err instanceof Error ? err.message : err);
|
|
41246
41971
|
});
|
|
41247
41972
|
}
|