@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/dist/src/framework.js
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 } from "./tool-presentation.js";
|
|
2
4
|
import { presentAuthor } from './author-presentation.js';
|
|
3
5
|
import { constants as bufferConstants } from 'node:buffer';
|
|
@@ -9,7 +11,8 @@ import { JsStore } from '@animalabs/chronicle';
|
|
|
9
11
|
import { MembraneError } from '@animalabs/membrane';
|
|
10
12
|
import { classifyFailure, retryDecision, MEMBRANE_RETRY_SETTINGS } from '@animalabs/membrane/retry';
|
|
11
13
|
import { deadlineCut, isDeadlineExpiry, resolveInferenceBounds, wholeCallMs } from './inference-bounds.js';
|
|
12
|
-
import {
|
|
14
|
+
import { boundFailureText, compileRefusalType, failureWords, FAILURE_TEXT_MAX_CHARS, houseReason, isMembraneError, PROVIDER_ERROR_CODE_MAX_CHARS } from './failure-text.js';
|
|
15
|
+
import { ContextManager, PassthroughStrategy, stripUnpairedToolBlocks, continuesBody, MalformedContentError, malformedContentPlaces, CompressionWithdrawn } from '@animalabs/context-manager';
|
|
13
16
|
// Not named imports from context-manager: 0.24.0 has neither, and a named
|
|
14
17
|
// import of either would fail this package's load there before create()
|
|
15
18
|
// could name the release it needs.
|
|
@@ -80,6 +83,7 @@ import { AgentPresentation, asCompiled, bearingOf, heldFromResidents, suppressed
|
|
|
80
83
|
import { coverTest, covers, readMarkThrough } from './read-marks.js';
|
|
81
84
|
import { contentAsStored, nameAsStored } from './text-as-stored.js';
|
|
82
85
|
import { STORED_INJECTION_KEY, STORED_INJECTION_KIND, asStoredRow, hasRenderedContent, isOwnInjection, isStoredKind, storedInjectionMarkOf, unitHash } from './stored-injections.js';
|
|
86
|
+
import { appendAll } from './append-all.js';
|
|
83
87
|
/** An MCPL tool call refused because its server isn't in the registry: one
|
|
84
88
|
* closed for good, one removed, or one whose connect failed without
|
|
85
89
|
* reconnect, which this host doesn't bring back on its own. A server that's
|
|
@@ -916,10 +920,15 @@ const TURN_RETRY = Symbol('turn-retry');
|
|
|
916
920
|
*/
|
|
917
921
|
const MERGED_BATCH = Symbol('merged-batch');
|
|
918
922
|
const REARMED = Symbol('rearmed');
|
|
923
|
+
const UNPROMPTED = Symbol('unprompted');
|
|
919
924
|
const TURN_CARRY = Symbol('turn-carry');
|
|
925
|
+
const BUDGET_WAIT = Symbol('budget-wait');
|
|
920
926
|
function isRearmed(request) {
|
|
921
927
|
return request !== undefined && request[REARMED] === true;
|
|
922
928
|
}
|
|
929
|
+
function isUnprompted(request) {
|
|
930
|
+
return request !== undefined && request[UNPROMPTED] === true;
|
|
931
|
+
}
|
|
923
932
|
function waitAlertsOnItsOwn(wait) {
|
|
924
933
|
return wait.until === null || wait.until - wait.setAt > PROVIDER_WAIT_ALERT_MS;
|
|
925
934
|
}
|
|
@@ -965,6 +974,9 @@ function asTurnRetry(trigger) {
|
|
|
965
974
|
function continuesTurn(request) {
|
|
966
975
|
return resumesLogicalTurn(request.reason) || isTurnRetry(request);
|
|
967
976
|
}
|
|
977
|
+
function budgetWaitOf(request) {
|
|
978
|
+
return request ? request[BUDGET_WAIT] : undefined;
|
|
979
|
+
}
|
|
968
980
|
function turnCarryOf(request) {
|
|
969
981
|
return request && continuesTurn(request) ? request[TURN_CARRY] : undefined;
|
|
970
982
|
}
|
|
@@ -1143,6 +1155,7 @@ const RETRY_CANCELLED_BY_STOP = 'a retry waiting while the framework was stoppin
|
|
|
1143
1155
|
/** The reason on the inference:aborted that ends a turn whose continuation
|
|
1144
1156
|
* was dropped before it started, other than by an abandon or a stop. */
|
|
1145
1157
|
const CONTINUATION_DROPPED = "a turn's continuation was dropped before it started";
|
|
1158
|
+
const BUDGET_HELD_TURN_REMOVED = 'a turn that waited for compression was dropped: what it stood on was removed';
|
|
1146
1159
|
function isTurnContinuation(reason) {
|
|
1147
1160
|
return reason === 'context_budget_restart' || reason === 'tool_results_ready'
|
|
1148
1161
|
|| reason === 'tool_result_guard_retry';
|
|
@@ -1165,6 +1178,9 @@ const FRAMEWORK_WAKE_SOURCES = new Set(['framework', 'scheduler']);
|
|
|
1165
1178
|
function hasOwnCauseForRun(r) {
|
|
1166
1179
|
return !FRAMEWORK_WAKE_SOURCES.has(r.source) && r.suppressProse !== true;
|
|
1167
1180
|
}
|
|
1181
|
+
function bringsOwnChannels(r) {
|
|
1182
|
+
return hasOwnCauseForRun(r) && (r.wakeChannels?.length ?? 0) > 0;
|
|
1183
|
+
}
|
|
1168
1184
|
/** Whether a wake carries an autoContinue continuation: its own wake, or a
|
|
1169
1185
|
* requeued turn trigger that kept the mark (InferenceRequest.autoContinueRun). */
|
|
1170
1186
|
function carriesContinuation(r) {
|
|
@@ -1264,6 +1280,10 @@ function assertFrameworkSwitches(config) {
|
|
|
1264
1280
|
assertBooleanSetting('watchdog.enabled', config.watchdog.enabled, 'true arms the liveness watchdog, which ends the process when the main thread misses its heartbeat for thresholdMs, ' +
|
|
1265
1281
|
'and false or leaving it out doesn\'t');
|
|
1266
1282
|
}
|
|
1283
|
+
const unprompted = config.unpromptedChannel;
|
|
1284
|
+
if (unprompted !== undefined && unprompted !== null && (typeof unprompted !== 'string' || unprompted.trim() === '')) {
|
|
1285
|
+
throw new Error(`unpromptedChannel must name a channel, such as "discord:<guildId>:<channelId>", not ${typeof unprompted === 'string' ? 'a blank string' : kindOf(unprompted)}`);
|
|
1286
|
+
}
|
|
1267
1287
|
// The heartbeat: true or an object turns it on; false, null or leaving it
|
|
1268
1288
|
// out leaves it off. Any other value (a "false" read from text, a 0) is
|
|
1269
1289
|
// refused rather than read as on.
|
|
@@ -1627,8 +1647,8 @@ export class QuiesceBlockedError extends Error {
|
|
|
1627
1647
|
*/
|
|
1628
1648
|
export class ProviderWaitNotSleptError extends Error {
|
|
1629
1649
|
retryAfterMs;
|
|
1630
|
-
constructor(message, retryAfterMs) {
|
|
1631
|
-
super(message);
|
|
1650
|
+
constructor(message, retryAfterMs, options) {
|
|
1651
|
+
super(message, options);
|
|
1632
1652
|
this.retryAfterMs = retryAfterMs;
|
|
1633
1653
|
this.name = 'ProviderWaitNotSleptError';
|
|
1634
1654
|
}
|
|
@@ -1779,7 +1799,7 @@ export class DefaultErrorPolicy {
|
|
|
1779
1799
|
settings;
|
|
1780
1800
|
constructor(settings = TURN_RETRY_SETTINGS) {
|
|
1781
1801
|
this.settings = settings;
|
|
1782
|
-
this.maxRetries =
|
|
1802
|
+
this.maxRetries = Object.values(settings).reduce((max, s) => Math.max(max, (s?.maxAttempts ?? 1) - 1), 0);
|
|
1783
1803
|
}
|
|
1784
1804
|
onInferenceError(error, _agentName, attempt) {
|
|
1785
1805
|
if (isDeadlineExpiry(error)) {
|
|
@@ -1885,6 +1905,10 @@ const TURN_RETRY_SETTINGS = {
|
|
|
1885
1905
|
deadline: { maxAttempts: DEADLINE_TURN_TRIES, firstDelayMs: 1_000, multiplier: 1, maxDelayMs: 1_000, statedWaitCeilingMs: null },
|
|
1886
1906
|
};
|
|
1887
1907
|
const PROVIDER_ACCELERATION_JITTER_MS = 5_000;
|
|
1908
|
+
const BUDGET_HOLD_NOTE_NAMESPACE = 'framework:budget-hold';
|
|
1909
|
+
function describeWait(ms) {
|
|
1910
|
+
return ms < 60_000 ? `${Math.max(1, Math.round(ms / 1000))} s` : `${Math.round(ms / 60_000)} min`;
|
|
1911
|
+
}
|
|
1888
1912
|
/** Longest delay one setTimeout can represent (2^31-1 ms, about 24.8 days);
|
|
1889
1913
|
* a longer timer fires almost at once, so longer waits re-arm in steps. */
|
|
1890
1914
|
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
@@ -1900,16 +1924,6 @@ const WAITING_TRACE_EVERY_MS = 60_000;
|
|
|
1900
1924
|
* waits in a row hold one turn longer than this (noteStatedWaitRun,
|
|
1901
1925
|
* endStatedWaitRun). */
|
|
1902
1926
|
const PROVIDER_WAIT_ALERT_MS = 60 * 60_000;
|
|
1903
|
-
/**
|
|
1904
|
-
* A MembraneError, from this framework's copy of membrane or from another: the
|
|
1905
|
-
* host supplies the Membrane instance (config.membrane), so with two copies
|
|
1906
|
-
* installed its errors are instances of a class this framework's `instanceof`
|
|
1907
|
-
* doesn't match. Recognised by name as well, as context-manager's errors are;
|
|
1908
|
-
* its fields are the same, so callers still check each one's type.
|
|
1909
|
-
*/
|
|
1910
|
-
function isMembraneError(error) {
|
|
1911
|
-
return error instanceof MembraneError || (error instanceof Error && error.name === 'MembraneError');
|
|
1912
|
-
}
|
|
1913
1927
|
/**
|
|
1914
1928
|
* agent-framework#734. Membrane's own streams send a retryable failure as an
|
|
1915
1929
|
* `error` event and send nothing after their terminal event. A stream of
|
|
@@ -2171,42 +2185,16 @@ const SCRIPT_TEXT_ANSWER_ENVELOPE_BYTES = Buffer.byteLength(JSON.stringify({ con
|
|
|
2171
2185
|
* failure naming the tool and saying the call itself completed.
|
|
2172
2186
|
*/
|
|
2173
2187
|
const scriptResultMaxChars = (memoryMaxBytes) => Math.max(0, Math.floor((Math.min(maxResultBytes(memoryMaxBytes), bufferConstants.MAX_STRING_LENGTH) - SCRIPT_TEXT_ANSWER_ENVELOPE_BYTES) / 6));
|
|
2174
|
-
/**
|
|
2175
|
-
*
|
|
2176
|
-
* reason in the exhaustion stderr line, a field of a logs/failures.log record,
|
|
2177
|
-
* an ops alert's message, the inference log's `error`, and a trace event's
|
|
2178
|
-
* `error` and `stack`. A failure's text is whatever its source put in
|
|
2179
|
-
* `Error.message`, and a provider can echo the entire rejected request there
|
|
2180
|
-
* (one OpenAI-compatible 400 echoed ~1.7 MB). The sinks with shorter bounds
|
|
2181
|
-
* name them below, and every cut goes through boundFailureText. This bound
|
|
2182
|
-
* doesn't depend on membrane bounding its own errors.
|
|
2183
|
-
*/
|
|
2184
|
-
const FAILURE_TEXT_MAX_CHARS = 2_000;
|
|
2185
|
-
/** Longest failure reason written into an agent's own context (the
|
|
2186
|
-
* `[inference-failed]` marker): enough for a provider's explanation, not a
|
|
2187
|
-
* self-dump the agent then carries in every later request. */
|
|
2188
|
+
/** Longest failure reason in the `[inference-failed]` marker: the reason its
|
|
2189
|
+
* text gives, and the excerpt its metadata keeps. */
|
|
2188
2190
|
const FAILURE_MARKER_REASON_MAX_CHARS = 600;
|
|
2189
|
-
/** Longest failure reason
|
|
2190
|
-
* enough to identify the failure class without a wall of provider JSON. */
|
|
2191
|
+
/** Longest failure reason the rewind marker's metadata keeps as its category. */
|
|
2191
2192
|
const REWIND_MARKER_REASON_MAX_CHARS = 160;
|
|
2192
2193
|
/** Longest failure reason in an agent's status (`lastInference.lastError`) and
|
|
2193
2194
|
* in the compile-refused ops alert. */
|
|
2194
2195
|
const FAILURE_SUMMARY_MAX_CHARS = 300;
|
|
2195
2196
|
/** Longest alert message posted to the ops webhook. */
|
|
2196
2197
|
const OPS_WEBHOOK_MESSAGE_MAX_CHARS = 500;
|
|
2197
|
-
/** Longest provider error code recorded verbatim; real codes are short tokens.
|
|
2198
|
-
* A failure's name and type are bounded the same way. */
|
|
2199
|
-
const PROVIDER_ERROR_CODE_MAX_CHARS = 128;
|
|
2200
|
-
const isHighSurrogate = (text, index) => {
|
|
2201
|
-
const code = text.charCodeAt(index);
|
|
2202
|
-
return code >= 0xd800 && code <= 0xdbff;
|
|
2203
|
-
};
|
|
2204
|
-
const isLowSurrogate = (text, index) => {
|
|
2205
|
-
const code = text.charCodeAt(index);
|
|
2206
|
-
return code >= 0xdc00 && code <= 0xdfff;
|
|
2207
|
-
};
|
|
2208
|
-
/** Whether a cut before `index` would separate the halves of a surrogate pair. */
|
|
2209
|
-
const splitsPair = (text, index) => index > 0 && index < text.length && isHighSurrogate(text, index - 1) && isLowSurrogate(text, index);
|
|
2210
2198
|
/**
|
|
2211
2199
|
* Whether an MCPL server's child ended the way a server ends when it's asked
|
|
2212
2200
|
* to: exit 0, death by a signal that asks a process to end (SIGTERM, SIGINT,
|
|
@@ -2231,28 +2219,6 @@ function closeWords(reason, code, signal) {
|
|
|
2231
2219
|
const how = [reason ?? null, code != null ? `code ${code}` : null, signal ? `signal ${signal}` : null].filter((part) => part !== null);
|
|
2232
2220
|
return how.length > 0 ? ` (${how.join(', ')})` : '';
|
|
2233
2221
|
}
|
|
2234
|
-
/**
|
|
2235
|
-
* `text` when it fits within `max` characters; otherwise its head and tail
|
|
2236
|
-
* around a marker stating how many of how many characters were omitted, the
|
|
2237
|
-
* whole within `max`. A cut that would separate a surrogate pair moves toward
|
|
2238
|
-
* omission, so the result never holds half of a pair the text held whole, and
|
|
2239
|
-
* bounding bounded text returns it as is. Each sink cuts the whole text, so
|
|
2240
|
-
* its marker states the whole length.
|
|
2241
|
-
*/
|
|
2242
|
-
function boundFailureText(text, max = FAILURE_TEXT_MAX_CHARS) {
|
|
2243
|
-
if (text.length <= max)
|
|
2244
|
-
return text;
|
|
2245
|
-
const markerLength = ` …[${text.length} of ${text.length} characters omitted]… `.length;
|
|
2246
|
-
const budget = Math.max(0, max - markerLength);
|
|
2247
|
-
const tailLength = Math.floor(budget / 4);
|
|
2248
|
-
let headEnd = budget - tailLength;
|
|
2249
|
-
if (splitsPair(text, headEnd))
|
|
2250
|
-
headEnd--;
|
|
2251
|
-
let tailStart = text.length - tailLength;
|
|
2252
|
-
if (splitsPair(text, tailStart))
|
|
2253
|
-
tailStart++;
|
|
2254
|
-
return `${text.slice(0, headEnd)} …[${tailStart - headEnd} of ${text.length} characters omitted]… ${text.slice(tailStart)}`;
|
|
2255
|
-
}
|
|
2256
2222
|
export function opsWebhookText(message) {
|
|
2257
2223
|
return boundFailureText(message, OPS_WEBHOOK_MESSAGE_MAX_CHARS);
|
|
2258
2224
|
}
|
|
@@ -2326,6 +2292,30 @@ const UNDESCRIBED_FAILURE = `[${NOTICE_TAG.undescribedFailure}]`;
|
|
|
2326
2292
|
function failureShown(text) {
|
|
2327
2293
|
return text === UNDESCRIBED_FAILURE ? text : suppliedValue(text);
|
|
2328
2294
|
}
|
|
2295
|
+
function failedEntry(prefix, err) {
|
|
2296
|
+
return { error: `${prefix}${err.message}`, errorWords: `${prefix}${failureWords(err)}` };
|
|
2297
|
+
}
|
|
2298
|
+
function shownReasonOf(err) {
|
|
2299
|
+
const own = houseReason(err);
|
|
2300
|
+
return own === undefined ? {} : { shownReason: own };
|
|
2301
|
+
}
|
|
2302
|
+
function boundedCause(err) {
|
|
2303
|
+
const cause = new Error(boundFailureText(String(err.message)));
|
|
2304
|
+
cause.name = boundFailureText(String(err.name), PROVIDER_ERROR_CODE_MAX_CHARS);
|
|
2305
|
+
cause.stack = boundFailureText(String(err.stack ?? `${cause.name}: ${cause.message}`));
|
|
2306
|
+
if (isMembraneError(err)) {
|
|
2307
|
+
const fields = err;
|
|
2308
|
+
const code = boundProviderErrorCode(fields.providerErrorCode);
|
|
2309
|
+
Object.assign(cause, {
|
|
2310
|
+
...(typeof fields.type === 'string' ? { type: boundFailureText(fields.type, PROVIDER_ERROR_CODE_MAX_CHARS) } : {}),
|
|
2311
|
+
...(typeof fields.retryable === 'boolean' ? { retryable: fields.retryable } : {}),
|
|
2312
|
+
...(typeof fields.httpStatus === 'number' ? { httpStatus: fields.httpStatus } : {}),
|
|
2313
|
+
...(code !== undefined ? { providerErrorCode: code } : {}),
|
|
2314
|
+
...(typeof fields.retryAfterMs === 'number' ? { retryAfterMs: fields.retryAfterMs } : {}),
|
|
2315
|
+
});
|
|
2316
|
+
}
|
|
2317
|
+
return cause;
|
|
2318
|
+
}
|
|
2329
2319
|
/**
|
|
2330
2320
|
* A bounded string describing a failure, for logs that would otherwise print
|
|
2331
2321
|
* the error object; it is one line unless one of its parts has newlines, as a
|
|
@@ -2648,6 +2638,14 @@ function mergeWakeLines(requests) {
|
|
|
2648
2638
|
}
|
|
2649
2639
|
return byEntry.size > 0 ? [...byEntry.values()] : undefined;
|
|
2650
2640
|
}
|
|
2641
|
+
function wakeChannelsOf(requests) {
|
|
2642
|
+
const channels = [];
|
|
2643
|
+
for (const channel of requests.flatMap((r) => r?.wakeChannels ?? [])) {
|
|
2644
|
+
if (!channels.some((c) => sameChannel(c, channel)))
|
|
2645
|
+
channels.push(channel);
|
|
2646
|
+
}
|
|
2647
|
+
return channels.length > 0 ? channels : undefined;
|
|
2648
|
+
}
|
|
2651
2649
|
/** A batched-wake line's identity: its event's stored entry. */
|
|
2652
2650
|
function wakeLineKey(line) {
|
|
2653
2651
|
const { eventType, serverId, channelId, messageId, eventId } = line.entry;
|
|
@@ -2805,6 +2803,9 @@ export class AgentFramework {
|
|
|
2805
2803
|
providerAccelerationCooldowns = new Map();
|
|
2806
2804
|
providerAccelerationRecoveries = new Map();
|
|
2807
2805
|
providerAccelerationLastRecovery = new Map();
|
|
2806
|
+
budgetHolds = new Map();
|
|
2807
|
+
overBudgetDrainAfter = new Map();
|
|
2808
|
+
budgetHoldRecheckMs = DEFAULT_MAINTENANCE_INTERVAL_MS;
|
|
2808
2809
|
providerAccelerationDefaultCooldownMs = PROVIDER_ACCELERATION_DEFAULT_COOLDOWN_MS;
|
|
2809
2810
|
providerAccelerationJitterMs = PROVIDER_ACCELERATION_JITTER_MS;
|
|
2810
2811
|
inferenceBounds = resolveInferenceBounds(undefined);
|
|
@@ -3305,11 +3306,11 @@ export class AgentFramework {
|
|
|
3305
3306
|
managerReleases = new WeakMap();
|
|
3306
3307
|
releasedByCleanup = new WeakSet();
|
|
3307
3308
|
resumesPending = new Map();
|
|
3308
|
-
// context-manager 0.
|
|
3309
|
-
// AutobiographicalStrategy does (hostContext, autobiographical.ts:
|
|
3309
|
+
// context-manager 0.25.1: a strategy that keeps its context, as
|
|
3310
|
+
// AutobiographicalStrategy does (hostContext, autobiographical.ts:3156),
|
|
3310
3311
|
// keeps its manager reachable while its work is pending, since the
|
|
3311
3312
|
// context's tools and systemPrompt getters close over the manager
|
|
3312
|
-
// (context-manager.ts:
|
|
3313
|
+
// (context-manager.ts:4602-4621). So such a released manager is collected
|
|
3313
3314
|
// only once nothing of its work can still read the store, and stop()
|
|
3314
3315
|
// closes, before an owned store, each one alive.
|
|
3315
3316
|
openManagers = new Set();
|
|
@@ -3433,7 +3434,8 @@ export class AgentFramework {
|
|
|
3433
3434
|
toolPatternKnownPrefixes = new Map();
|
|
3434
3435
|
/** Maps serverId → McplServerConfig for prefix lookup. */
|
|
3435
3436
|
mcplServerConfigs = new Map();
|
|
3436
|
-
/** Modern-MCP (2026-07-28)
|
|
3437
|
+
/** Modern-MCP servers (2026-07-28, or a 2025 revision over HTTP), by id;
|
|
3438
|
+
* read through modernServers().
|
|
3437
3439
|
* They are kept apart from the MCPL registry on purpose: they have no
|
|
3438
3440
|
* grant, planes or server→host traffic, so the MCPL machinery (quiesce's
|
|
3439
3441
|
* plane pauses, policy) never sees them. They take part
|
|
@@ -3470,6 +3472,7 @@ export class AgentFramework {
|
|
|
3470
3472
|
* subsystem can be lazily initialized by connectMcplServer when the
|
|
3471
3473
|
* framework started with zero configured servers. */
|
|
3472
3474
|
mcplInferenceRoutingConfig = null;
|
|
3475
|
+
mcplUnpromptedChannel = null;
|
|
3473
3476
|
/** The awareness ledger; null for a framework with neither storePath nor
|
|
3474
3477
|
* discordAwarenessOutboxPath, which refuses a marks choice. */
|
|
3475
3478
|
discordAwarenessOutbox;
|
|
@@ -3580,12 +3583,15 @@ export class AgentFramework {
|
|
|
3580
3583
|
assertAgentNamesUnique(config.agents);
|
|
3581
3584
|
assertObservedAgentsConfigured(config.agents);
|
|
3582
3585
|
// The presentation index (#291) needs a context-manager that keeps a
|
|
3583
|
-
// window index per agent (presentRows)
|
|
3584
|
-
//
|
|
3585
|
-
//
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3586
|
+
// window index per agent (presentRows), which 0.23.x and 0.24.x were
|
|
3587
|
+
// published without, and a stream tells it of dropped thinking
|
|
3588
|
+
// (noteThinkingDropped), which 0.25.0 was published without. It needs no
|
|
3589
|
+
// store, so it is checked before one is opened.
|
|
3590
|
+
const contextManagerProto = ContextManager.prototype;
|
|
3591
|
+
if (typeof contextManagerProto.presentRows !== 'function' || typeof contextManagerProto.noteThinkingDropped !== 'function') {
|
|
3592
|
+
throw new Error('agent-framework needs @animalabs/context-manager with ContextManager.presentRows and ContextManager.noteThinkingDropped: ' +
|
|
3593
|
+
"a context-manager that keeps a window index per agent and can be told which stored replies' thinking the provider dropped. " +
|
|
3594
|
+
'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)');
|
|
3589
3595
|
}
|
|
3590
3596
|
// Create or use existing store
|
|
3591
3597
|
let store;
|
|
@@ -3982,7 +3988,7 @@ export class AgentFramework {
|
|
|
3982
3988
|
: undefined);
|
|
3983
3989
|
},
|
|
3984
3990
|
requestInference: (agentName, reason, source, provenance, after) => {
|
|
3985
|
-
|
|
3991
|
+
const request = {
|
|
3986
3992
|
agentName, reason, source, timestamp: Date.now(),
|
|
3987
3993
|
...framework.readsRowFor(after, agentName),
|
|
3988
3994
|
// gate-requested wakes carry where/who (EventGate wakeProvenance)
|
|
@@ -4010,11 +4016,19 @@ export class AgentFramework {
|
|
|
4010
4016
|
const route = provenance?.routeChannelId !== undefined
|
|
4011
4017
|
? framework.channelNamed(provenance.routeChannelId, provenance.routeServerId)
|
|
4012
4018
|
: undefined;
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4019
|
+
if (route && framework.mayTakeGateRoute(agentName, route)) {
|
|
4020
|
+
return { channelId: route.channelId, channelServerId: route.serverId, addressed: true };
|
|
4021
|
+
}
|
|
4022
|
+
const came = (provenance?.batchChannels ?? [])
|
|
4023
|
+
.map((c) => framework.channelNamed(c.channelId, c.serverId))
|
|
4024
|
+
.filter((c) => c !== undefined)
|
|
4025
|
+
.filter((c, i, all) => all.findIndex((d) => sameChannel(d, c)) === i);
|
|
4026
|
+
return came.length > 0 ? { wakeChannels: came } : {};
|
|
4016
4027
|
})(),
|
|
4017
|
-
}
|
|
4028
|
+
};
|
|
4029
|
+
if (source === 'self-wake' || source === 'sleep')
|
|
4030
|
+
request[UNPROMPTED] = true;
|
|
4031
|
+
framework.pendingRequests.push(request);
|
|
4018
4032
|
},
|
|
4019
4033
|
getAgentNames: () => framework.registry.gateRecipients(),
|
|
4020
4034
|
isHeld: (serverId, channelId) => !!(serverId && channelId && framework.tuneOutCoordinator?.holdFor(serverId, channelId)),
|
|
@@ -4119,6 +4133,7 @@ export class AgentFramework {
|
|
|
4119
4133
|
// Stored for lazy MCPL initialization (connectMcplServer on a framework
|
|
4120
4134
|
// that started with zero configured servers).
|
|
4121
4135
|
framework.mcplInferenceRoutingConfig = config.inferenceRouting ?? null;
|
|
4136
|
+
framework.mcplUnpromptedChannel = config.unpromptedChannel ?? null;
|
|
4122
4137
|
// Client-side programmatic tool calling (code_execution). Made only when
|
|
4123
4138
|
// enabled — everything downstream gates on the field. create() checked
|
|
4124
4139
|
// the configuration (validateCodeExecutionConfig).
|
|
@@ -4679,6 +4694,9 @@ export class AgentFramework {
|
|
|
4679
4694
|
});
|
|
4680
4695
|
// A stopped host must never hang behind its own cooldown.
|
|
4681
4696
|
step(() => this.cancelProviderAdmission());
|
|
4697
|
+
for (const name of [...(this.budgetHolds?.keys() ?? [])]) {
|
|
4698
|
+
step(() => this.dropBudgetHold(name, { kind: 'shutdown', why: 'the framework is stopping' }));
|
|
4699
|
+
}
|
|
4682
4700
|
// An open logical turn doesn't survive a stop.
|
|
4683
4701
|
this.openLogical.clear();
|
|
4684
4702
|
// Cancel the stream of every agent whose state holds one: streaming,
|
|
@@ -5063,8 +5081,11 @@ export class AgentFramework {
|
|
|
5063
5081
|
// A wait recorded while this call was parked binds it too.
|
|
5064
5082
|
this.refuseHeldProviderModel(agentName, model);
|
|
5065
5083
|
gate.auxiliaryInFlight++;
|
|
5084
|
+
let landed = false;
|
|
5066
5085
|
try {
|
|
5067
|
-
|
|
5086
|
+
const result = await run();
|
|
5087
|
+
landed = true;
|
|
5088
|
+
return result;
|
|
5068
5089
|
}
|
|
5069
5090
|
catch (error) {
|
|
5070
5091
|
// Compression can be the first call to find a spent quota (idle agent,
|
|
@@ -5084,6 +5105,8 @@ export class AgentFramework {
|
|
|
5084
5105
|
if (gate.auxiliaryInFlight === 0)
|
|
5085
5106
|
for (const resolve of gate.idleWaiters.splice(0))
|
|
5086
5107
|
resolve();
|
|
5108
|
+
if (landed)
|
|
5109
|
+
this.managerCallLanded(agentName);
|
|
5087
5110
|
}
|
|
5088
5111
|
}
|
|
5089
5112
|
/**
|
|
@@ -5131,7 +5154,7 @@ export class AgentFramework {
|
|
|
5131
5154
|
? `a turn whose retries ran out, sent again (${cooldown.rearm.number} of ${cooldown.rearm.of})`
|
|
5132
5155
|
: 'a turn held by a provider cooldown, when it ends' });
|
|
5133
5156
|
}
|
|
5134
|
-
wakes
|
|
5157
|
+
appendAll(wakes, this.heartbeatWakesFor(agentName));
|
|
5135
5158
|
return wakes;
|
|
5136
5159
|
}
|
|
5137
5160
|
/**
|
|
@@ -5318,6 +5341,16 @@ export class AgentFramework {
|
|
|
5318
5341
|
auxiliaryMembraneFor(agentName) {
|
|
5319
5342
|
return this.membraneAdmitting((run, model) => this.withAuxiliaryAdmission(agentName, run, model));
|
|
5320
5343
|
}
|
|
5344
|
+
managerCallLanded(agentName) {
|
|
5345
|
+
const hold = this.budgetHolds?.get(agentName);
|
|
5346
|
+
if (!hold)
|
|
5347
|
+
return;
|
|
5348
|
+
hold.managerCalls++;
|
|
5349
|
+
if (hold.checkQueued)
|
|
5350
|
+
return;
|
|
5351
|
+
const look = setTimeout(() => this.releaseBudgetHoldOnEvidence(agentName), 0);
|
|
5352
|
+
look.unref?.();
|
|
5353
|
+
}
|
|
5321
5354
|
/**
|
|
5322
5355
|
* The reply judge's calls. None waits: under the agent's holds a call is
|
|
5323
5356
|
* refused without being made, and the agent's turn stops the judge. A call
|
|
@@ -5374,14 +5407,17 @@ export class AgentFramework {
|
|
|
5374
5407
|
this.withdrawMaintenanceUnderSurgery();
|
|
5375
5408
|
this.withdrawMaintenanceWhenStopping(agentName);
|
|
5376
5409
|
this.refuseHeldProviderModel(agentName, model);
|
|
5410
|
+
let result;
|
|
5377
5411
|
try {
|
|
5378
|
-
|
|
5412
|
+
result = await run();
|
|
5379
5413
|
}
|
|
5380
5414
|
catch (error) {
|
|
5381
5415
|
if (model !== undefined && error instanceof Error)
|
|
5382
5416
|
this.recordProviderWait(agentName, model, error);
|
|
5383
5417
|
throw error;
|
|
5384
5418
|
}
|
|
5419
|
+
this.managerCallLanded(agentName);
|
|
5420
|
+
return result;
|
|
5385
5421
|
});
|
|
5386
5422
|
}
|
|
5387
5423
|
ephemeralAuxiliaryMembraneFor(agentName, release) {
|
|
@@ -5512,13 +5548,28 @@ export class AgentFramework {
|
|
|
5512
5548
|
held.push(request);
|
|
5513
5549
|
return;
|
|
5514
5550
|
}
|
|
5551
|
+
// Read the kept request's carry before marking it.
|
|
5552
|
+
const keptCarry = turnCarryOf(same);
|
|
5515
5553
|
if (isTurnRetry(request))
|
|
5516
5554
|
same[TURN_RETRY] = true;
|
|
5555
|
+
const wait = budgetWaitOf(request);
|
|
5556
|
+
if (wait && !budgetWaitOf(same))
|
|
5557
|
+
same[BUDGET_WAIT] = wait;
|
|
5517
5558
|
// A merged start's batch (MERGED_BATCH) stays recorded on the request
|
|
5518
5559
|
// kept in its place, which then stands for it, as the start did.
|
|
5519
5560
|
const batch = mergedBatch(request);
|
|
5520
5561
|
if (batch)
|
|
5521
5562
|
same[MERGED_BATCH] = [...new Set([...(mergedBatch(same) ?? []), ...batch])];
|
|
5563
|
+
const carry = keptCarry ?? turnCarryOf(request);
|
|
5564
|
+
if (carry)
|
|
5565
|
+
same[TURN_CARRY] = carry;
|
|
5566
|
+
else
|
|
5567
|
+
delete same[TURN_CARRY];
|
|
5568
|
+
if (!isRearmed(request))
|
|
5569
|
+
delete same[REARMED];
|
|
5570
|
+
const channels = wakeChannelsOf([same, request]);
|
|
5571
|
+
if (channels)
|
|
5572
|
+
same.wakeChannels = channels;
|
|
5522
5573
|
}
|
|
5523
5574
|
consultProviderHold(error, agentName) {
|
|
5524
5575
|
if (!this.providerHoldHook)
|
|
@@ -5577,7 +5628,7 @@ export class AgentFramework {
|
|
|
5577
5628
|
// failure streak), and the wait applies to the calls that follow it.
|
|
5578
5629
|
if (!acceleration && !hostHold) {
|
|
5579
5630
|
return wait !== undefined && isMembraneError(error) && error.retryable === true
|
|
5580
|
-
&& this.holdForProviderWait(agent, wait, trigger ? [
|
|
5631
|
+
&& this.holdForProviderWait(agent, wait, trigger ? [this.turnRetryOf(trigger, this.turnRouting.get(agent.name))] : [], true);
|
|
5581
5632
|
}
|
|
5582
5633
|
// As in holdForProviderWait: once stop() has closed admission, nothing is
|
|
5583
5634
|
// held, and nothing started.
|
|
@@ -5591,7 +5642,7 @@ export class AgentFramework {
|
|
|
5591
5642
|
// The failed turn's own request, kept as its retry (TURN_RETRY). An
|
|
5592
5643
|
// auxiliary call's failure passes no trigger: it holds no turn of its own.
|
|
5593
5644
|
if (trigger)
|
|
5594
|
-
this.holdRequest(held,
|
|
5645
|
+
this.holdRequest(held, this.turnRetryOf(trigger, this.turnRouting.get(agent.name)));
|
|
5595
5646
|
if (existing)
|
|
5596
5647
|
clearTimeout(existing.timer);
|
|
5597
5648
|
const timer = setTimeout(() => this.releaseProviderAccelerationCooldown(agent.name), Math.min(delayMs, MAX_TIMER_DELAY_MS));
|
|
@@ -5657,6 +5708,8 @@ export class AgentFramework {
|
|
|
5657
5708
|
const request = { ...trigger, [REARMED]: true };
|
|
5658
5709
|
delete request[TURN_RETRY];
|
|
5659
5710
|
delete request[MERGED_BATCH];
|
|
5711
|
+
delete request.wakeChannels;
|
|
5712
|
+
Object.assign(request, this.wakeChannelsMark(routing));
|
|
5660
5713
|
if (continuation) {
|
|
5661
5714
|
request.reason = REARM_REASON;
|
|
5662
5715
|
delete request.counterparty;
|
|
@@ -5711,7 +5764,7 @@ export class AgentFramework {
|
|
|
5711
5764
|
return undefined;
|
|
5712
5765
|
if (this.registry.homeChannel(agent.name) !== undefined) {
|
|
5713
5766
|
const wait = this.recordProviderWait(agent.name, agent.model, error);
|
|
5714
|
-
return error.retryable === true && wait !== undefined && this.holdForProviderWait(agent, wait, trigger ? [
|
|
5767
|
+
return error.retryable === true && wait !== undefined && this.holdForProviderWait(agent, wait, trigger ? [this.turnRetryOf(trigger, this.turnRouting.get(agent.name))] : [], true)
|
|
5715
5768
|
? 'held'
|
|
5716
5769
|
: undefined;
|
|
5717
5770
|
}
|
|
@@ -5731,18 +5784,490 @@ export class AgentFramework {
|
|
|
5731
5784
|
const cap = `${PROVIDER_ACCELERATION_MAX_COOLDOWN_MS / 1000} s`;
|
|
5732
5785
|
if (Number.isNaN(stated)) {
|
|
5733
5786
|
return {
|
|
5734
|
-
error: `${error
|
|
5787
|
+
error: `${failureWords(error)} (the provider's stated wait is not a number, and an ephemeral run sleeps only a ` +
|
|
5735
5788
|
`stated wait of ${cap} or less, so it ends now)`,
|
|
5736
5789
|
statedWaitMs: Number.NaN,
|
|
5737
5790
|
};
|
|
5738
5791
|
}
|
|
5739
5792
|
const asked = Number.isFinite(stated) ? `${Math.ceil(stated / 1000)} s` : 'longer than can be represented';
|
|
5740
5793
|
return {
|
|
5741
|
-
error: `${error
|
|
5794
|
+
error: `${failureWords(error)} (the provider asked to wait ${asked}; an ephemeral run doesn't sleep a stated wait ` +
|
|
5742
5795
|
`longer than ${cap}, so it ends now)`,
|
|
5743
5796
|
statedWaitMs: stated,
|
|
5744
5797
|
};
|
|
5745
5798
|
}
|
|
5799
|
+
holdForBudgetRefusal(agent, error, retry, checkpoint, routing, baseline, closeWindow) {
|
|
5800
|
+
const { errorType } = this.classifyInferenceError(error);
|
|
5801
|
+
if (errorType !== 'over_budget' && errorType !== 'context_refusal')
|
|
5802
|
+
return false;
|
|
5803
|
+
if (!this.budgetHolds || !this.running || this.providerAdmissionClosed)
|
|
5804
|
+
return false;
|
|
5805
|
+
if (this.ephemeralRuns.has(agent.name) || this.agents.get(agent.name) !== agent || this.budgetHolds.has(agent.name))
|
|
5806
|
+
return false;
|
|
5807
|
+
if (!this.compressionPending(agent))
|
|
5808
|
+
return false;
|
|
5809
|
+
if (checkpoint !== undefined) {
|
|
5810
|
+
try {
|
|
5811
|
+
this.dropTurnCheckpoint(agent.name, checkpoint);
|
|
5812
|
+
}
|
|
5813
|
+
catch (error) {
|
|
5814
|
+
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);
|
|
5815
|
+
return false;
|
|
5816
|
+
}
|
|
5817
|
+
}
|
|
5818
|
+
const turn = retry ?? { agentName: agent.name, reason: 'budget-hold-release', source: 'framework', timestamp: Date.now() };
|
|
5819
|
+
const carried = budgetWaitOf(turn);
|
|
5820
|
+
const startedAt = Date.now();
|
|
5821
|
+
const standsOn = !continuesTurn(turn) && carried
|
|
5822
|
+
? (carried.row !== undefined ? [carried.row] : [])
|
|
5823
|
+
: this.rowsTurnStandsOn(agent, turn, routing);
|
|
5824
|
+
const wait = {
|
|
5825
|
+
since: carried?.since ?? startedAt,
|
|
5826
|
+
...(!continuesTurn(turn) && standsOn[0] !== undefined ? { row: standsOn[0] } : {}),
|
|
5827
|
+
...(carried?.alerted ? { alerted: true } : {}),
|
|
5828
|
+
};
|
|
5829
|
+
turn[BUDGET_WAIT] = wait;
|
|
5830
|
+
const figures = error;
|
|
5831
|
+
const hold = {
|
|
5832
|
+
startedAt,
|
|
5833
|
+
wait,
|
|
5834
|
+
turn,
|
|
5835
|
+
standsOn,
|
|
5836
|
+
wakes: [],
|
|
5837
|
+
errorType,
|
|
5838
|
+
reason: boundFailureText(String(error.message)),
|
|
5839
|
+
budget: typeof figures.budget === 'number' ? figures.budget : undefined,
|
|
5840
|
+
actual: typeof figures.actual === 'number' ? figures.actual : undefined,
|
|
5841
|
+
managerCalls: 0,
|
|
5842
|
+
progressAtHold: baseline ? baseline.progress : this.compressionProgressOf(agent),
|
|
5843
|
+
settingsAtHold: baseline ? baseline.settings : agent.contextSettingsChanges(),
|
|
5844
|
+
};
|
|
5845
|
+
this.budgetHolds.set(agent.name, hold);
|
|
5846
|
+
this.startBudgetHoldRecheck(agent, hold);
|
|
5847
|
+
closeWindow();
|
|
5848
|
+
const endGate = () => this.eventGate?.onInferenceEnded(agent.name);
|
|
5849
|
+
this.emitTrace({
|
|
5850
|
+
type: 'inference:held',
|
|
5851
|
+
agentName: agent.name,
|
|
5852
|
+
errorType,
|
|
5853
|
+
error: hold.reason,
|
|
5854
|
+
...(hold.budget !== undefined ? { budget: hold.budget } : {}),
|
|
5855
|
+
...(hold.actual !== undefined ? { actual: hold.actual } : {}),
|
|
5856
|
+
since: hold.startedAt,
|
|
5857
|
+
heldRequests: 1,
|
|
5858
|
+
});
|
|
5859
|
+
if (this.budgetHolds.get(agent.name) !== hold) {
|
|
5860
|
+
endGate();
|
|
5861
|
+
return true;
|
|
5862
|
+
}
|
|
5863
|
+
if (!wait.alerted) {
|
|
5864
|
+
wait.alerted = true;
|
|
5865
|
+
this.opsAlert('context-refusal', agent.name, this.budgetHoldOpenText(hold), {
|
|
5866
|
+
skipLog: true,
|
|
5867
|
+
key: String(hold.wait.since),
|
|
5868
|
+
data: {
|
|
5869
|
+
errorType,
|
|
5870
|
+
since: hold.wait.since,
|
|
5871
|
+
...(hold.budget !== undefined ? { budget: hold.budget } : {}),
|
|
5872
|
+
...(hold.actual !== undefined ? { actual: hold.actual } : {}),
|
|
5873
|
+
},
|
|
5874
|
+
});
|
|
5875
|
+
if (this.budgetHolds.get(agent.name) !== hold) {
|
|
5876
|
+
endGate();
|
|
5877
|
+
return true;
|
|
5878
|
+
}
|
|
5879
|
+
}
|
|
5880
|
+
endGate();
|
|
5881
|
+
if (this.budgetHolds.get(agent.name) !== hold)
|
|
5882
|
+
return true;
|
|
5883
|
+
this.retractUnsentInjections(agent);
|
|
5884
|
+
if (this.budgetHolds.get(agent.name) !== hold)
|
|
5885
|
+
return true;
|
|
5886
|
+
const readiness = this.contextReadiness(agent.getContextManager());
|
|
5887
|
+
const compression = 'pending' in readiness && readiness.pending ? ` Compression: ${boundFailureText(readiness.pending)}` : '';
|
|
5888
|
+
const again = carried ? `, as of a wait that began ${describeWait(startedAt - carried.since)} ago` : '';
|
|
5889
|
+
console.error(`[budget-hold] agent=${agent.name} compile refused (${errorType}): ${hold.reason} \u2014 Held${again}: ` +
|
|
5890
|
+
`the turn waits, idle, and runs once compression makes room.${compression}`);
|
|
5891
|
+
this.driveBudgetHoldSoon(agent);
|
|
5892
|
+
return true;
|
|
5893
|
+
}
|
|
5894
|
+
rowsTurnStandsOn(agent, turn, routing) {
|
|
5895
|
+
if (!continuesTurn(turn)) {
|
|
5896
|
+
const row = this.activeTurnTriggers.get(agent.name)?.readsRow;
|
|
5897
|
+
return row !== undefined ? [row] : [];
|
|
5898
|
+
}
|
|
5899
|
+
const rows = routing.firstRow !== undefined ? [routing.firstRow] : [];
|
|
5900
|
+
const from = routing.start?.sequence;
|
|
5901
|
+
if (from === undefined)
|
|
5902
|
+
return rows;
|
|
5903
|
+
try {
|
|
5904
|
+
const ownCalls = new Set();
|
|
5905
|
+
for (const row of agent.getContextManager().getPresentedMessages()) {
|
|
5906
|
+
if (!ownWindowRow(row, agent, ownCalls) || row.sequence <= from)
|
|
5907
|
+
continue;
|
|
5908
|
+
if (storedInjectionMarkOf(row.metadata) !== undefined || clockStampOf(row.metadata) !== undefined)
|
|
5909
|
+
continue;
|
|
5910
|
+
rows.push(String(row.id));
|
|
5911
|
+
}
|
|
5912
|
+
}
|
|
5913
|
+
catch (error) {
|
|
5914
|
+
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);
|
|
5915
|
+
}
|
|
5916
|
+
return rows;
|
|
5917
|
+
}
|
|
5918
|
+
compressionPending(agent) {
|
|
5919
|
+
try {
|
|
5920
|
+
return !agent.getContextManager().isReady();
|
|
5921
|
+
}
|
|
5922
|
+
catch {
|
|
5923
|
+
return false;
|
|
5924
|
+
}
|
|
5925
|
+
}
|
|
5926
|
+
startBudgetHoldRecheck(agent, hold) {
|
|
5927
|
+
clearInterval(hold.recheck);
|
|
5928
|
+
hold.recheck = undefined;
|
|
5929
|
+
if (this.maintenanceIntervalMs > 0)
|
|
5930
|
+
return;
|
|
5931
|
+
hold.recheck = setInterval(() => this.driveBudgetHold(agent), this.budgetHoldRecheckMs);
|
|
5932
|
+
hold.recheck.unref?.();
|
|
5933
|
+
}
|
|
5934
|
+
driveBudgetHoldSoon(agent) {
|
|
5935
|
+
const kick = setTimeout(() => this.driveBudgetHold(agent), 0);
|
|
5936
|
+
kick.unref?.();
|
|
5937
|
+
}
|
|
5938
|
+
driveBudgetHold(agent) {
|
|
5939
|
+
const hold = this.budgetHolds?.get(agent.name);
|
|
5940
|
+
if (!hold || this.agents.get(agent.name) !== agent || hold.checkQueued)
|
|
5941
|
+
return;
|
|
5942
|
+
if (this.providerGateBlocked(agent.name))
|
|
5943
|
+
return;
|
|
5944
|
+
const kicked = this.kickOverBudgetDrain(agent.name, agent, () => {
|
|
5945
|
+
hold.checkQueued = false;
|
|
5946
|
+
if (this.budgetHolds.get(agent.name) !== hold)
|
|
5947
|
+
return;
|
|
5948
|
+
if (this.budgetHoldMoved(agent, hold))
|
|
5949
|
+
this.releaseBudgetHold(agent.name);
|
|
5950
|
+
else if (kicked === 'already draining')
|
|
5951
|
+
this.driveBudgetHold(agent);
|
|
5952
|
+
}, 'hold', hold.drainKicked);
|
|
5953
|
+
if (kicked === 'kicking' || kicked === 'already draining')
|
|
5954
|
+
hold.checkQueued = true;
|
|
5955
|
+
if (kicked === 'kicking')
|
|
5956
|
+
hold.drainKicked = true;
|
|
5957
|
+
}
|
|
5958
|
+
releaseBudgetHolds() {
|
|
5959
|
+
for (const name of [...(this.budgetHolds?.keys() ?? [])])
|
|
5960
|
+
this.releaseBudgetHold(name);
|
|
5961
|
+
}
|
|
5962
|
+
releaseBudgetHoldsOnEvidence() {
|
|
5963
|
+
for (const name of [...(this.budgetHolds?.keys() ?? [])])
|
|
5964
|
+
this.releaseBudgetHoldOnEvidence(name);
|
|
5965
|
+
}
|
|
5966
|
+
releaseBudgetHoldOnEvidence(agentName) {
|
|
5967
|
+
const hold = this.budgetHolds?.get(agentName);
|
|
5968
|
+
const agent = this.agents.get(agentName);
|
|
5969
|
+
if (hold && agent && this.budgetHoldMoved(agent, hold))
|
|
5970
|
+
this.releaseBudgetHold(agentName);
|
|
5971
|
+
}
|
|
5972
|
+
budgetHoldMoved(agent, hold) {
|
|
5973
|
+
if (!this.compressionPending(agent) || agent.contextSettingsChanges() !== hold.settingsAtHold)
|
|
5974
|
+
return true;
|
|
5975
|
+
const progress = this.compressionProgressOf(agent);
|
|
5976
|
+
if (progress !== undefined && hold.progressAtHold !== undefined)
|
|
5977
|
+
return progress !== hold.progressAtHold;
|
|
5978
|
+
if (progress !== undefined)
|
|
5979
|
+
hold.progressAtHold = progress;
|
|
5980
|
+
return hold.managerCalls !== 0;
|
|
5981
|
+
}
|
|
5982
|
+
compressionProgressOf(agent) {
|
|
5983
|
+
const progress = this.contextProgress(agent.getContextManager());
|
|
5984
|
+
if (!progress)
|
|
5985
|
+
return undefined;
|
|
5986
|
+
try {
|
|
5987
|
+
const { chunksCompressed, summaryCounts } = progress;
|
|
5988
|
+
if (chunksCompressed === undefined && summaryCounts === undefined)
|
|
5989
|
+
return undefined;
|
|
5990
|
+
return JSON.stringify([chunksCompressed, summaryCounts]);
|
|
5991
|
+
}
|
|
5992
|
+
catch (error) {
|
|
5993
|
+
console.error(`[budget-hold] agent=${agent.name}: could not read its compression progress, so it reads as none:`, error);
|
|
5994
|
+
return undefined;
|
|
5995
|
+
}
|
|
5996
|
+
}
|
|
5997
|
+
closureWaits(agentName) {
|
|
5998
|
+
const closure = (r) => r.agentName === agentName
|
|
5999
|
+
&& (mergedBatch(r) ?? [r]).some((m) => m.reason === 'conversation:closure');
|
|
6000
|
+
return this.pendingRequests.some(closure)
|
|
6001
|
+
|| [...(this.passRequests ?? [])].some((pass) => (pass.get(agentName) ?? []).some(closure))
|
|
6002
|
+
|| (this.providerAccelerationCooldowns.get(agentName)?.heldRequests ?? []).some(closure)
|
|
6003
|
+
|| (this.budgetHolds?.get(agentName)?.wakes ?? []).some(closure);
|
|
6004
|
+
}
|
|
6005
|
+
releaseBudgetHold(agentName) {
|
|
6006
|
+
const hold = this.budgetHolds?.get(agentName);
|
|
6007
|
+
const agent = this.agents.get(agentName);
|
|
6008
|
+
if (!hold || !agent || this.quiesced || this.surgeryHold)
|
|
6009
|
+
return;
|
|
6010
|
+
const requests = [hold.turn, ...hold.wakes];
|
|
6011
|
+
hold.wakes = [];
|
|
6012
|
+
this.budgetHolds.delete(agentName);
|
|
6013
|
+
clearInterval(hold.recheck);
|
|
6014
|
+
hold.recheck = undefined;
|
|
6015
|
+
appendAll(this.pendingRequests, requests);
|
|
6016
|
+
console.error(`[budget-hold] agent=${agentName} released after ${Date.now() - hold.startedAt}ms: ` +
|
|
6017
|
+
`${requests.length} request(s) queued, and the start's own compile is the check`);
|
|
6018
|
+
const note = this.storeBudgetHoldNote(agent, hold);
|
|
6019
|
+
if (note !== undefined)
|
|
6020
|
+
this.emitTrace({ type: 'message:added', messageId: note, source: `context-injection:${BUDGET_HOLD_NOTE_NAMESPACE}` });
|
|
6021
|
+
this.finishBudgetHold(agentName, hold, 'released');
|
|
6022
|
+
}
|
|
6023
|
+
storeBudgetHoldNote(agent, hold) {
|
|
6024
|
+
const size = hold.budget !== undefined && hold.actual !== undefined
|
|
6025
|
+
? ` (${hold.actual.toLocaleString('en-US')} tokens against ${hold.budget.toLocaleString('en-US')})`
|
|
6026
|
+
: '';
|
|
6027
|
+
const text = `[budget-hold] Your context didn't fit its budget${size} when a turn came to start. ` +
|
|
6028
|
+
`That turn waited ${describeWait(Date.now() - hold.wait.since)}.`;
|
|
6029
|
+
const row = asStoredRow({ namespace: BUDGET_HOLD_NOTE_NAMESPACE, position: 'afterUser', content: [{ type: 'text', text }] });
|
|
6030
|
+
const mark = { agent: agent.name, namespace: row.namespace, contentHash: unitHash([row]), part: 0, parts: 1 };
|
|
6031
|
+
try {
|
|
6032
|
+
agent.markUnsentPoint();
|
|
6033
|
+
return storeOwnRow(agent, row.participant, row.content, {
|
|
6034
|
+
system: true, kind: STORED_INJECTION_KIND, [STORED_INJECTION_KEY]: mark,
|
|
6035
|
+
}, undefined, !this.logicalTurnAlive(agent));
|
|
6036
|
+
}
|
|
6037
|
+
catch (error) {
|
|
6038
|
+
console.error(`[budget-hold] agent=${agent.name}: the note for its released turn could not be stored:`, error);
|
|
6039
|
+
return undefined;
|
|
6040
|
+
}
|
|
6041
|
+
}
|
|
6042
|
+
finishBudgetHold(agentName, hold, outcome) {
|
|
6043
|
+
if (this.budgetHolds?.get(agentName) === hold)
|
|
6044
|
+
this.budgetHolds.delete(agentName);
|
|
6045
|
+
clearInterval(hold.recheck);
|
|
6046
|
+
hold.recheck = undefined;
|
|
6047
|
+
const waitedMs = Date.now() - hold.startedAt;
|
|
6048
|
+
console.error(`[budget-hold] agent=${agentName} hold ended (${outcome}) after ${waitedMs}ms`);
|
|
6049
|
+
this.emitTrace({ type: 'inference:hold_ended', agentName, outcome, waitedMs });
|
|
6050
|
+
if (outcome === 'released' || !hold.wait.alerted)
|
|
6051
|
+
return;
|
|
6052
|
+
const waitedFor = Date.now() - hold.wait.since;
|
|
6053
|
+
const after = describeWait(waitedFor);
|
|
6054
|
+
const words = outcome === 'removed'
|
|
6055
|
+
? `what the waiting turn was for was removed or taken out of its window after ${after}, so its waiting start doesn't run`
|
|
6056
|
+
: `the waiting turn was stopped after ${after}, so its waiting start doesn't run`;
|
|
6057
|
+
this.opsAlert('context-refusal-clear', agentName, words, {
|
|
6058
|
+
skipLog: true,
|
|
6059
|
+
key: String(hold.wait.since),
|
|
6060
|
+
data: { outcome, waitedMs: waitedFor },
|
|
6061
|
+
});
|
|
6062
|
+
}
|
|
6063
|
+
budgetHoldOpenText(hold) {
|
|
6064
|
+
const head = `compile refused (${hold.errorType}): `;
|
|
6065
|
+
const tail = ' \u2014 the turn waits while compression makes room, and runs once its context fits';
|
|
6066
|
+
return head + boundFailureText(hold.reason, FAILURE_TEXT_MAX_CHARS - head.length - tail.length) + tail;
|
|
6067
|
+
}
|
|
6068
|
+
retractUnsentInjections(agent) {
|
|
6069
|
+
if (!this.presents(agent))
|
|
6070
|
+
return;
|
|
6071
|
+
try {
|
|
6072
|
+
const ids = agent.unsentInjectionRows();
|
|
6073
|
+
if (ids.length === 0)
|
|
6074
|
+
return;
|
|
6075
|
+
agent.getContextManager().retractRows(ids, { undone: true });
|
|
6076
|
+
this.emitTrace({ type: 'presentation:retracted', agentName: agent.name, messageIds: ids, reason: 'unsent-injection' });
|
|
6077
|
+
}
|
|
6078
|
+
catch (error) {
|
|
6079
|
+
console.error(`[budget-hold] agent=${agent.name}: the injection rows its refused start stored could not leave its window:`, error);
|
|
6080
|
+
}
|
|
6081
|
+
}
|
|
6082
|
+
endHeldTurn(agentName, turn, drop) {
|
|
6083
|
+
this.endContinuedTurn(agentName, drop, !continuesTurn(turn));
|
|
6084
|
+
}
|
|
6085
|
+
dropBudgetHold(agentName, drop, wakes = 'gone') {
|
|
6086
|
+
const hold = this.budgetHolds?.get(agentName);
|
|
6087
|
+
if (!hold)
|
|
6088
|
+
return false;
|
|
6089
|
+
this.budgetHolds.delete(agentName);
|
|
6090
|
+
clearInterval(hold.recheck);
|
|
6091
|
+
hold.recheck = undefined;
|
|
6092
|
+
if (wakes === 'back')
|
|
6093
|
+
appendAll(this.pendingRequests, hold.wakes);
|
|
6094
|
+
hold.wakes = [];
|
|
6095
|
+
try {
|
|
6096
|
+
this.endHeldTurn(agentName, hold.turn, drop);
|
|
6097
|
+
}
|
|
6098
|
+
finally {
|
|
6099
|
+
this.finishBudgetHold(agentName, hold, 'stopped');
|
|
6100
|
+
}
|
|
6101
|
+
return true;
|
|
6102
|
+
}
|
|
6103
|
+
heldContinuation(agentName) {
|
|
6104
|
+
const hold = this.budgetHolds?.get(agentName);
|
|
6105
|
+
return hold !== undefined && continuesTurn(hold.turn);
|
|
6106
|
+
}
|
|
6107
|
+
dropHeldTurn(agentName, hold, why) {
|
|
6108
|
+
this.endDetachedHold(agentName, this.detachBudgetHold(agentName, hold), why);
|
|
6109
|
+
}
|
|
6110
|
+
/** Take `hold` out of its agent's wait, with no trace yet. */
|
|
6111
|
+
detachBudgetHold(agentName, hold) {
|
|
6112
|
+
this.budgetHolds.delete(agentName);
|
|
6113
|
+
clearInterval(hold.recheck);
|
|
6114
|
+
hold.recheck = undefined;
|
|
6115
|
+
return hold;
|
|
6116
|
+
}
|
|
6117
|
+
/** End a turn detachBudgetHold took out of its wait: its wakes back in the queue, then its end traced. */
|
|
6118
|
+
endDetachedHold(agentName, hold, why) {
|
|
6119
|
+
appendAll(this.pendingRequests, hold.wakes);
|
|
6120
|
+
hold.wakes = [];
|
|
6121
|
+
try {
|
|
6122
|
+
this.endHeldTurn(agentName, hold.turn, { kind: 'dropped', why, reason: BUDGET_HELD_TURN_REMOVED });
|
|
6123
|
+
}
|
|
6124
|
+
finally {
|
|
6125
|
+
this.finishBudgetHold(agentName, hold, 'removed');
|
|
6126
|
+
}
|
|
6127
|
+
}
|
|
6128
|
+
rowGone(agent, row) {
|
|
6129
|
+
try {
|
|
6130
|
+
return agent.getContextManager().getMessage(row) === null;
|
|
6131
|
+
}
|
|
6132
|
+
catch (error) {
|
|
6133
|
+
console.error(`[budget-hold] agent=${agent.name}: could not read whether row ${row} is still there, so what waits on it stays:`, error);
|
|
6134
|
+
return false;
|
|
6135
|
+
}
|
|
6136
|
+
}
|
|
6137
|
+
rowLeftWindow(agent, row) {
|
|
6138
|
+
if (this.rowGone(agent, row))
|
|
6139
|
+
return true;
|
|
6140
|
+
if (!this.presents(agent))
|
|
6141
|
+
return false;
|
|
6142
|
+
try {
|
|
6143
|
+
return agent.getContextManager().ordinalOf(row) === undefined;
|
|
6144
|
+
}
|
|
6145
|
+
catch (error) {
|
|
6146
|
+
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);
|
|
6147
|
+
return false;
|
|
6148
|
+
}
|
|
6149
|
+
}
|
|
6150
|
+
budgetWaitEnded(agent, trigger) {
|
|
6151
|
+
const row = budgetWaitOf(trigger)?.row;
|
|
6152
|
+
if (row === undefined || !this.rowLeftWindow(agent, row))
|
|
6153
|
+
return false;
|
|
6154
|
+
const batch = mergedBatch(trigger);
|
|
6155
|
+
if (batch)
|
|
6156
|
+
appendAll(this.pendingRequests, batch.filter((r) => budgetWaitOf(r) === undefined));
|
|
6157
|
+
this.endContinuedTurn(agent.name, { kind: 'dropped', why: 'what its held turn stood on was removed', reason: BUDGET_HELD_TURN_REMOVED }, true);
|
|
6158
|
+
return true;
|
|
6159
|
+
}
|
|
6160
|
+
checkBudgetHoldsAfterWindowChange(only, release = true) {
|
|
6161
|
+
for (const [agentName, hold] of [...(this.budgetHolds?.entries() ?? [])]) {
|
|
6162
|
+
const agent = this.agents.get(agentName);
|
|
6163
|
+
if (!agent || (only !== undefined && agentName !== only) || this.budgetHolds?.get(agentName) !== hold)
|
|
6164
|
+
continue;
|
|
6165
|
+
const wakes = hold.wakes.filter((wake) => wake.readsRow === undefined || !this.rowGone(agent, wake.readsRow));
|
|
6166
|
+
if (wakes.length < hold.wakes.length) {
|
|
6167
|
+
console.error(`[budget-hold] agent=${agentName}: ${hold.wakes.length - wakes.length} held wake(s) dropped, their rows removed`);
|
|
6168
|
+
}
|
|
6169
|
+
hold.wakes = wakes;
|
|
6170
|
+
if (hold.standsOn.some((row) => this.rowLeftWindow(agent, row))) {
|
|
6171
|
+
this.dropHeldTurn(agentName, hold, 'a surgery or an undo removed what its held turn stood on');
|
|
6172
|
+
}
|
|
6173
|
+
else if (release) {
|
|
6174
|
+
this.releaseBudgetHold(agentName);
|
|
6175
|
+
}
|
|
6176
|
+
}
|
|
6177
|
+
}
|
|
6178
|
+
kickOverBudgetDrain(agentName, agent, after, by = 'breaker', quiet = false) {
|
|
6179
|
+
// Like a maintenance pass: none starts once stop() has begun, it starts
|
|
6180
|
+
// no tick while a surgery holds the store or once its agent has been
|
|
6181
|
+
// disposed, and a surgery, a switchBranch or stop() waits for the tick
|
|
6182
|
+
// in flight. It joins the drains they wait for before the breaker's alert is
|
|
6183
|
+
// raised and before its body runs, so the alert's listeners find it:
|
|
6184
|
+
// one that begins stop() or a surgery waits for it, and its body then
|
|
6185
|
+
// ticks none and says why. So does a stop() made from inside its first
|
|
6186
|
+
// tick, before that tick's first await.
|
|
6187
|
+
if (this.providerAdmissionClosed)
|
|
6188
|
+
return 'not kicked: the framework is stopping';
|
|
6189
|
+
if (this.surgeryHold)
|
|
6190
|
+
return 'not kicked: a surgery holds the store';
|
|
6191
|
+
if (after) {
|
|
6192
|
+
const waiting = this.overBudgetDrainAfter.get(agentName);
|
|
6193
|
+
if (waiting)
|
|
6194
|
+
waiting.push(after);
|
|
6195
|
+
else
|
|
6196
|
+
this.overBudgetDrainAfter.set(agentName, [after]);
|
|
6197
|
+
}
|
|
6198
|
+
if (this.overBudgetDrainInFlight.has(agentName))
|
|
6199
|
+
return 'already draining';
|
|
6200
|
+
this.overBudgetDrainInFlight.add(agentName);
|
|
6201
|
+
const tag = by === 'hold' ? '[budget-hold]' : '[inference-failed]';
|
|
6202
|
+
const label = by === 'hold' ? 'budget hold' : 'OverBudget breaker';
|
|
6203
|
+
const drain = Promise.resolve().then(async () => {
|
|
6204
|
+
let ticks = 0;
|
|
6205
|
+
let record;
|
|
6206
|
+
try {
|
|
6207
|
+
const cm = agent.getContextManager();
|
|
6208
|
+
if (by === 'hold') {
|
|
6209
|
+
const readiness = this.contextReadiness(cm);
|
|
6210
|
+
const progress = this.contextProgress(cm);
|
|
6211
|
+
record = {
|
|
6212
|
+
agentName, startedAt: Date.now(), ticks: 0, readyBefore: false,
|
|
6213
|
+
...('pending' in readiness && readiness.pending ? { pendingBefore: readiness.pending } : {}),
|
|
6214
|
+
...(progress ? { progressBefore: progress } : {}),
|
|
6215
|
+
};
|
|
6216
|
+
}
|
|
6217
|
+
const holding = () => this.budgetHolds.has(agentName);
|
|
6218
|
+
while ((by === 'hold' ? holding() : ticks < 8) && !this.providerAdmissionClosed && !this.surgeryHold && this.agents.get(agentName) === agent) {
|
|
6219
|
+
const calls = by === 'hold' ? this.budgetHolds.get(agentName)?.managerCalls : undefined;
|
|
6220
|
+
await cm.tick();
|
|
6221
|
+
ticks++;
|
|
6222
|
+
if (by === 'breaker')
|
|
6223
|
+
continue;
|
|
6224
|
+
const held = this.budgetHolds.get(agentName);
|
|
6225
|
+
if (held && this.budgetHoldMoved(agent, held)) {
|
|
6226
|
+
this.releaseBudgetHold(agentName);
|
|
6227
|
+
if (this.quiesced)
|
|
6228
|
+
break;
|
|
6229
|
+
}
|
|
6230
|
+
else if (held?.managerCalls === calls || this.compressionProgressOf(agent) !== undefined) {
|
|
6231
|
+
break;
|
|
6232
|
+
}
|
|
6233
|
+
}
|
|
6234
|
+
if (by === 'hold' && ticks > 0)
|
|
6235
|
+
this.maintenanceFailureReported.delete(agentName);
|
|
6236
|
+
const stopped = by === 'breaker' && ticks >= 8 ? ''
|
|
6237
|
+
: this.agents.get(agentName) !== agent ? ', stopped as the agent was disposed'
|
|
6238
|
+
: this.providerAdmissionClosed ? ', stopped as the framework stops'
|
|
6239
|
+
: this.surgeryHold ? ', stopped while a surgery holds the store'
|
|
6240
|
+
: '';
|
|
6241
|
+
if (!quiet || stopped)
|
|
6242
|
+
console.error(`${tag} drain kicked for ${agentName} (${label}, ${ticks} ticks${stopped})`);
|
|
6243
|
+
}
|
|
6244
|
+
catch (err) {
|
|
6245
|
+
if (err instanceof MaintenanceWithdrawnError) {
|
|
6246
|
+
console.error(`${tag} drain kicked for ${agentName} (${label}, ${ticks} ticks, stopped: ${err.message})`);
|
|
6247
|
+
}
|
|
6248
|
+
else if (record) {
|
|
6249
|
+
record.ticks = ticks;
|
|
6250
|
+
record.finishedAt = Date.now();
|
|
6251
|
+
this.reportMaintenanceFailure(agentName, record, err instanceof Error ? String(err.message) : failureDescription(err));
|
|
6252
|
+
}
|
|
6253
|
+
else {
|
|
6254
|
+
console.error(`${tag} drain kick failed for ${agentName} after ${ticks} ticks: ${describeFailureWithFrames(err)}`);
|
|
6255
|
+
}
|
|
6256
|
+
}
|
|
6257
|
+
finally {
|
|
6258
|
+
this.overBudgetDrainInFlight.delete(agentName);
|
|
6259
|
+
}
|
|
6260
|
+
const waiting = this.overBudgetDrainAfter?.get(agentName);
|
|
6261
|
+
if (waiting) {
|
|
6262
|
+
this.overBudgetDrainAfter.delete(agentName);
|
|
6263
|
+
for (const run of waiting)
|
|
6264
|
+
run();
|
|
6265
|
+
}
|
|
6266
|
+
});
|
|
6267
|
+
this.overBudgetDrains.add(drain);
|
|
6268
|
+
void drain.finally(() => { this.overBudgetDrains.delete(drain); });
|
|
6269
|
+
return 'kicking';
|
|
6270
|
+
}
|
|
5746
6271
|
/** When a hold's timer next fires: at its deadline, stepped past what one
|
|
5747
6272
|
* timer can hold. A hold with no deadline (a wait held until released, or
|
|
5748
6273
|
* the stand-in for unreadable records) asks again as often as unreadable
|
|
@@ -5803,7 +6328,7 @@ export class AgentFramework {
|
|
|
5803
6328
|
}
|
|
5804
6329
|
const requests = cooldown.heldRequests.length > 0 ? cooldown.heldRequests : [{ agentName,
|
|
5805
6330
|
reason: 'provider-acceleration-retry', source: 'framework', timestamp: Date.now() }];
|
|
5806
|
-
this.pendingRequests
|
|
6331
|
+
appendAll(this.pendingRequests, requests);
|
|
5807
6332
|
console.error(cooldown.rearm
|
|
5808
6333
|
? `[inference-rearm] agent=${agentName} turn queued again (${cooldown.rearm.number} of ${cooldown.rearm.of})`
|
|
5809
6334
|
: `[provider-cooldown] agent=${agentName} released after ${Date.now() - cooldown.startedAt}ms — ` +
|
|
@@ -5959,7 +6484,7 @@ export class AgentFramework {
|
|
|
5959
6484
|
// (releaseProviderWait) before anything was held: nothing binds now, so
|
|
5960
6485
|
// the requests start, as the release of an installed hold starts them.
|
|
5961
6486
|
if (report && !this.providerWaits?.active(agent.name, wait.model)) {
|
|
5962
|
-
this.pendingRequests
|
|
6487
|
+
appendAll(this.pendingRequests, requests);
|
|
5963
6488
|
return true;
|
|
5964
6489
|
}
|
|
5965
6490
|
const existing = this.providerAccelerationCooldowns.get(agent.name);
|
|
@@ -6152,6 +6677,7 @@ export class AgentFramework {
|
|
|
6152
6677
|
void pass.finally(() => {
|
|
6153
6678
|
if (this.maintenancePass === pass)
|
|
6154
6679
|
this.maintenancePass = null;
|
|
6680
|
+
this.releaseBudgetHoldsOnEvidence();
|
|
6155
6681
|
});
|
|
6156
6682
|
begin(this.runQueuedMaintenance().catch((error) => {
|
|
6157
6683
|
// The alert cuts its own copies from the whole message, so the
|
|
@@ -6658,6 +7184,9 @@ export class AgentFramework {
|
|
|
6658
7184
|
this.dropContinuations(agentName, { kind: 'caller', reason, why: 'a stop ended the turn' });
|
|
6659
7185
|
return true;
|
|
6660
7186
|
}
|
|
7187
|
+
if (!result && this.budgetHolds?.has(agentName)) {
|
|
7188
|
+
return this.dropBudgetHold(agentName, { kind: 'caller', reason, why: 'a stop ended the turn' }, 'back');
|
|
7189
|
+
}
|
|
6661
7190
|
return !!result;
|
|
6662
7191
|
}
|
|
6663
7192
|
/**
|
|
@@ -6762,6 +7291,7 @@ export class AgentFramework {
|
|
|
6762
7291
|
log({
|
|
6763
7292
|
success: false,
|
|
6764
7293
|
error: `Stream cancelled (${reason})`,
|
|
7294
|
+
errorWords: `Stream cancelled (${reason})`,
|
|
6765
7295
|
request: compiledRequest ?? { note: 'streaming request aborted' },
|
|
6766
7296
|
durationMs,
|
|
6767
7297
|
});
|
|
@@ -6829,6 +7359,7 @@ export class AgentFramework {
|
|
|
6829
7359
|
log({
|
|
6830
7360
|
success: false,
|
|
6831
7361
|
error: `Stream aborted: ${kind}`,
|
|
7362
|
+
errorWords: `Stream aborted: ${kind}`,
|
|
6832
7363
|
request: compiledRequest ?? { note: `streaming request aborted by ${kind}` },
|
|
6833
7364
|
durationMs,
|
|
6834
7365
|
});
|
|
@@ -7477,7 +8008,7 @@ export class AgentFramework {
|
|
|
7477
8008
|
const extKeys = [];
|
|
7478
8009
|
for (const ext of extensions.values()) {
|
|
7479
8010
|
Object.assign(extProperties, ext.properties);
|
|
7480
|
-
extKeys
|
|
8011
|
+
appendAll(extKeys, ext.keys);
|
|
7481
8012
|
}
|
|
7482
8013
|
const settingsProp = baseSchema.properties.settings;
|
|
7483
8014
|
return {
|
|
@@ -7842,7 +8373,8 @@ export class AgentFramework {
|
|
|
7842
8373
|
...this.pendingRequests,
|
|
7843
8374
|
...[...this.passRequests].flatMap((pass) => [...pass.values()].flat()),
|
|
7844
8375
|
...[...this.providerAccelerationCooldowns.values()].flatMap((c) => c.heldRequests),
|
|
7845
|
-
].flatMap((r) => mergedBatch(r) ?? [r]).filter((r) => !passesQuiesce(r)).length
|
|
8376
|
+
].flatMap((r) => mergedBatch(r) ?? [r]).filter((r) => !passesQuiesce(r)).length
|
|
8377
|
+
+ [...(this.budgetHolds?.values() ?? [])].reduce((n, h) => n + 1 + h.wakes.flatMap((r) => mergedBatch(r) ?? [r]).filter((r) => !passesQuiesce(r)).length, 0),
|
|
7846
8378
|
backgroundScripts: this.codeExecution?.scripts.leftRunning() ?? 0,
|
|
7847
8379
|
// Deprecated (#291): no queue is kept. What a live surgery's hold
|
|
7848
8380
|
// keeps in memory, to land at its release, and what waits in memory
|
|
@@ -8207,6 +8739,7 @@ export class AgentFramework {
|
|
|
8207
8739
|
console.error(this.queue?.isClosed
|
|
8208
8740
|
? `[host-mode] resumed — ${releasedRequests} parked wake(s) won't run because the framework is stopping`
|
|
8209
8741
|
: `[host-mode] resumed — ${releasedRequests} parked wake(s) released`);
|
|
8742
|
+
this.releaseBudgetHolds();
|
|
8210
8743
|
}
|
|
8211
8744
|
return this.getHostModeStatus();
|
|
8212
8745
|
}
|
|
@@ -9648,10 +10181,12 @@ export class AgentFramework {
|
|
|
9648
10181
|
store: this.store,
|
|
9649
10182
|
namespace,
|
|
9650
10183
|
isolate: true,
|
|
10184
|
+
viewFilter: (message) => !isReasoningLoss(message.metadata),
|
|
9651
10185
|
strategy: config.strategy ?? new PassthroughStrategy(),
|
|
9652
10186
|
membrane: this.ephemeralAuxiliaryMembraneFor(config.name, release),
|
|
9653
10187
|
debugLogContext: !!process.env.DEBUG_CONTEXT,
|
|
9654
10188
|
});
|
|
10189
|
+
this.storeSees.set(contextManager, (metadata) => !isReasoningLoss(metadata));
|
|
9655
10190
|
ownSlot = { namespace };
|
|
9656
10191
|
}
|
|
9657
10192
|
// Either manager is the run's to release (main's manager release), the derived one included.
|
|
@@ -10035,7 +10570,8 @@ export class AgentFramework {
|
|
|
10035
10570
|
* The returned manager uses a passthrough strategy, so nothing is
|
|
10036
10571
|
* folded: its compile is the agent's window as the agent's view shows it
|
|
10037
10572
|
* (the rows the window holds, through its view filter), and in legacy
|
|
10038
|
-
* mode, where the window is the record, the record so.
|
|
10573
|
+
* mode, where the window is the record, the record so. These compiled
|
|
10574
|
+
* views omit legacy reasoning-loss notices. getAllMessages()
|
|
10039
10575
|
* lists the record. An ended agent's context is the real one — treat it as
|
|
10040
10576
|
* read-only unless you mean to change what a later resume starts from.
|
|
10041
10577
|
*/
|
|
@@ -10154,9 +10690,9 @@ export class AgentFramework {
|
|
|
10154
10690
|
* and what it observes (context-options.ts): a resident's, on the
|
|
10155
10691
|
* residents' slot, or for an isolated agent that watches that slot, the
|
|
10156
10692
|
* residents' view when its edge leaves held traffic out, and the
|
|
10157
|
-
* subconscious's when it shows it.
|
|
10693
|
+
* subconscious's when it shows it. For an isolated agent that
|
|
10158
10694
|
* watches no residents' slot on its branch (a worker, a conversation
|
|
10159
|
-
* fork), whose
|
|
10695
|
+
* fork), whose view excludes only legacy reasoning-loss notices.
|
|
10160
10696
|
*/
|
|
10161
10697
|
liveViewFilterOf(record) {
|
|
10162
10698
|
if (record.ownSlot?.namespace === null)
|
|
@@ -10166,7 +10702,7 @@ export class AgentFramework {
|
|
|
10166
10702
|
return slot !== undefined && slot.branch === undefined && sameSlot(slot, RESIDENTS_SLOT);
|
|
10167
10703
|
});
|
|
10168
10704
|
if (residents.length === 0)
|
|
10169
|
-
return
|
|
10705
|
+
return (message) => !isReasoningLoss(message.metadata);
|
|
10170
10706
|
return residents.some((edge) => edge.includeHeld === true) ? subconsciousViewFilter(record.name) : residentViewFilter(record.name);
|
|
10171
10707
|
}
|
|
10172
10708
|
/**
|
|
@@ -10192,7 +10728,7 @@ export class AgentFramework {
|
|
|
10192
10728
|
* (deriveSharedContext): what the agent its chain of derivations started
|
|
10193
10729
|
* from sees (liveViewFilterOf), and the stored injections of each agent in
|
|
10194
10730
|
* the chain, which carry that agent's name and would otherwise read as
|
|
10195
|
-
* another's.
|
|
10731
|
+
* another's. Legacy notices remain excluded even for an own injection.
|
|
10196
10732
|
*/
|
|
10197
10733
|
derivedViewFilterOf(record) {
|
|
10198
10734
|
const owner = this.viewOwnerOf(record);
|
|
@@ -10211,7 +10747,8 @@ export class AgentFramework {
|
|
|
10211
10747
|
}
|
|
10212
10748
|
if (names.length === 0)
|
|
10213
10749
|
return ownerSees;
|
|
10214
|
-
return (message) =>
|
|
10750
|
+
return (message) => !isReasoningLoss(message.metadata)
|
|
10751
|
+
&& (ownerSees(message) || names.some((name) => isOwnInjection(message.metadata, name)));
|
|
10215
10752
|
}
|
|
10216
10753
|
/** Open a derived agent's context on a branch of its own (see deriveAgent). */
|
|
10217
10754
|
async deriveSharedContext(config, inherit, release) {
|
|
@@ -10243,13 +10780,11 @@ export class AgentFramework {
|
|
|
10243
10780
|
// reads through it, and is recorded beside the manager (storeSees), so
|
|
10244
10781
|
// main's checks of whether an agent sees a row (agentSees: a tool
|
|
10245
10782
|
// boundary's injection, an arrival, a reader of an unread message) ask
|
|
10246
|
-
// what its compile shows.
|
|
10247
|
-
//
|
|
10248
|
-
// sees its own.
|
|
10783
|
+
// what its compile shows. The global legacy-notice exclusion applies
|
|
10784
|
+
// even when the source has no additional view recorded.
|
|
10249
10785
|
const sourceSees = this.storeSees.get(source);
|
|
10250
|
-
const sees =
|
|
10251
|
-
|
|
10252
|
-
: undefined;
|
|
10786
|
+
const sees = (metadata) => !isReasoningLoss(metadata)
|
|
10787
|
+
&& (!sourceSees || sourceSees(metadata) || isOwnInjection(metadata, config.name));
|
|
10253
10788
|
const contextManager = await source.derive({
|
|
10254
10789
|
branch: ownBranch,
|
|
10255
10790
|
strategy: config.strategy ?? new PassthroughStrategy(),
|
|
@@ -10259,11 +10794,10 @@ export class AgentFramework {
|
|
|
10259
10794
|
solve: inherit.solve ?? 'reuse',
|
|
10260
10795
|
inherit: { refusals: inherit.refusals === true },
|
|
10261
10796
|
...(inherit.atSequence !== undefined ? { atSequence: inherit.atSequence } : {}),
|
|
10262
|
-
|
|
10797
|
+
viewFilter: (message) => sees(message.metadata),
|
|
10263
10798
|
debugLogContext: !!process.env.DEBUG_CONTEXT,
|
|
10264
10799
|
});
|
|
10265
|
-
|
|
10266
|
-
this.storeSees.set(contextManager, sees);
|
|
10800
|
+
this.storeSees.set(contextManager, sees);
|
|
10267
10801
|
const derivation = contextManager.getDerivation?.();
|
|
10268
10802
|
return {
|
|
10269
10803
|
contextManager,
|
|
@@ -10781,6 +11315,7 @@ export class AgentFramework {
|
|
|
10781
11315
|
this.closingConversationAgents.delete(name);
|
|
10782
11316
|
// A continuation of its turn still on its way goes with it.
|
|
10783
11317
|
this.dropContinuations(name, { kind: 'dropped', why });
|
|
11318
|
+
this.dropBudgetHold(name, { kind: 'dropped', why });
|
|
10784
11319
|
// A batch its guard still holds is settled as `aborted`: the stream's
|
|
10785
11320
|
// own finally does neither once the agent has gone.
|
|
10786
11321
|
if (agent.toolResultGuard.hasPending) {
|
|
@@ -11484,9 +12019,10 @@ export class AgentFramework {
|
|
|
11484
12019
|
if (outcome.stopReason === 'exhausted') {
|
|
11485
12020
|
// The caller may log or relay the message, which can echo the whole request.
|
|
11486
12021
|
const message = boundFailureText(outcome.error ?? 'Unknown error');
|
|
12022
|
+
const options = outcome.cause !== undefined ? { cause: boundedCause(outcome.cause) } : undefined;
|
|
11487
12023
|
run.settle.reject(outcome.statedWaitMs === undefined
|
|
11488
|
-
? new Error(message)
|
|
11489
|
-
: new ProviderWaitNotSleptError(message, outcome.statedWaitMs));
|
|
12024
|
+
? new Error(message, options)
|
|
12025
|
+
: new ProviderWaitNotSleptError(message, outcome.statedWaitMs, options));
|
|
11490
12026
|
return;
|
|
11491
12027
|
}
|
|
11492
12028
|
run.settle.resolve({
|
|
@@ -11504,7 +12040,19 @@ export class AgentFramework {
|
|
|
11504
12040
|
* Rejects with ProviderWaitNotSleptError when the run ends rather than sleep
|
|
11505
12041
|
* a retryable failure's stated wait longer than the framework's ten-minute
|
|
11506
12042
|
* cap, or one it can't represent, and with a plain Error for any other
|
|
11507
|
-
* ending, a stated wait its error policy declines among them.
|
|
12043
|
+
* ending, a stated wait its error policy declines among them. A failed
|
|
12044
|
+
* turn's rejection never carries a provider's message: a provider's failure
|
|
12045
|
+
* is told by membrane's type and the HTTP status, a safety failure only as
|
|
12046
|
+
* a request that couldn't be completed, a progress bound's cut, a stream's
|
|
12047
|
+
* abort and context-manager's refusal of a compile in their own words, and
|
|
12048
|
+
* any other failure as the turn failing. Where an error ended the turn, the
|
|
12049
|
+
* rejection's `cause` is a bounded copy of it: a plain `Error` with its
|
|
12050
|
+
* `name`, its message and stack cut to 2,000 characters, and, from a
|
|
12051
|
+
* MembraneError, its `type`, `retryable`, `httpStatus`, `providerErrorCode`
|
|
12052
|
+
* and `retryAfterMs`, which a host reads by name. It carries no `rawError`
|
|
12053
|
+
* or `rawRequest`, and `instanceof MembraneError` doesn't hold on it. Its
|
|
12054
|
+
* message and stack are the failure's own, a provider's words included, for
|
|
12055
|
+
* classifying the failure and for logs, and aren't for showing to an agent.
|
|
11508
12056
|
*
|
|
11509
12057
|
* The caller provides a pre-created agent + contextManager (from createEphemeralAgent).
|
|
11510
12058
|
* The task message should already be in the context manager.
|
|
@@ -12518,7 +13066,7 @@ export class AgentFramework {
|
|
|
12518
13066
|
const turnMembers = all.slice(turn.from, turn.to + 1);
|
|
12519
13067
|
if (turnMembers.some((m) => hasBlock(m, 'tool_use'))) {
|
|
12520
13068
|
from = turn.from;
|
|
12521
|
-
removedIds
|
|
13069
|
+
appendAll(removedIds, turnMembers.map((m) => m.id).reverse());
|
|
12522
13070
|
heads.unshift(all[turn.from].id);
|
|
12523
13071
|
}
|
|
12524
13072
|
}
|
|
@@ -12557,16 +13105,9 @@ export class AgentFramework {
|
|
|
12557
13105
|
const cm = agent.getContextManager();
|
|
12558
13106
|
const ep = this.rewindEpisode.get(agent.name);
|
|
12559
13107
|
const count = (ep?.count ?? 0) + 1;
|
|
12560
|
-
const why = cause === 'refusal'
|
|
12561
|
-
? `the model refused on them (content filter: ${failureShown(category)}). Their content is ` +
|
|
12562
|
-
`not reproduced here (so this note can't re-trigger the filter); the ` +
|
|
12563
|
-
`originals remain in the raw record`
|
|
12564
|
-
: `the model API kept refusing the requests that carried them (${failureShown(category)}), ` +
|
|
12565
|
-
`so that the conversation could go on. Their content is not reproduced ` +
|
|
12566
|
-
`here (so this note can't trigger the same refusal); the originals remain ` +
|
|
12567
|
-
`in the raw record`;
|
|
12568
13108
|
const text = `[${NOTICE_TAG.refusalRewind}] The ${count} most recent turn(s) were set aside because ` +
|
|
12569
|
-
|
|
13109
|
+
`the requests that carried them couldn't be completed. Their content isn't shown here, ` +
|
|
13110
|
+
`and the originals remain in the raw record. You are clear to continue — if you ` +
|
|
12570
13111
|
`were mid-task, take a different approach; otherwise carry on with whatever ` +
|
|
12571
13112
|
`is now in front of you, or briefly acknowledge the gap and ask what's next.`;
|
|
12572
13113
|
const blocks = [{ type: 'text', text }];
|
|
@@ -12614,9 +13155,9 @@ export class AgentFramework {
|
|
|
12614
13155
|
: '';
|
|
12615
13156
|
const content = success
|
|
12616
13157
|
? `🔧 Unstuck **${agentName}** — shed ${n} turn(s); the model responded.${list}`
|
|
12617
|
-
: `⚠️ Couldn't unstick **${agentName}** — still
|
|
12618
|
-
|
|
12619
|
-
|
|
13158
|
+
: `⚠️ Couldn't unstick **${agentName}** — its requests still couldn't be completed after ${n} rewind(s).${list}`;
|
|
13159
|
+
console.error(`[unstick] agent=${agentName} ${success ? 'succeeded' : 'gave up'} after ${n} rewind(s)` +
|
|
13160
|
+
`${category ? ` category=${logValue(category)}` : ''}`);
|
|
12620
13161
|
if (s.channelId)
|
|
12621
13162
|
void this.postToChannel(s.serverId, s.channelId, content);
|
|
12622
13163
|
}
|
|
@@ -13227,6 +13768,7 @@ export class AgentFramework {
|
|
|
13227
13768
|
requestId: entry.requestId,
|
|
13228
13769
|
success: entry.success,
|
|
13229
13770
|
error: entry.error,
|
|
13771
|
+
errorWords: entry.errorWords,
|
|
13230
13772
|
durationMs: entry.durationMs,
|
|
13231
13773
|
tokenUsage: entry.tokenUsage,
|
|
13232
13774
|
stopReason: entry.stopReason,
|
|
@@ -13543,7 +14085,7 @@ export class AgentFramework {
|
|
|
13543
14085
|
let failures = [];
|
|
13544
14086
|
try {
|
|
13545
14087
|
this.subconsciousAnchor?.branchMoved(this.tuneOutCoordinator?.activeEpochIds());
|
|
13546
|
-
this.tuneOutCoordinator?.branchMoved();
|
|
14088
|
+
this.tuneOutCoordinator?.branchMoved(sameBranch);
|
|
13547
14089
|
if (!sameBranch)
|
|
13548
14090
|
this.redoStacks.clear();
|
|
13549
14091
|
failures = await this.reinitializeManagers(except);
|
|
@@ -13652,6 +14194,22 @@ export class AgentFramework {
|
|
|
13652
14194
|
* recorded with its error.
|
|
13653
14195
|
*/
|
|
13654
14196
|
undoOnWindow(agent, requester) {
|
|
14197
|
+
// A turn under way that waits for compression stands on what the undo
|
|
14198
|
+
// takes out. It leaves its wait first, so the undo finds the agent idle,
|
|
14199
|
+
// and it ends once the undo has kept its record, so a listener of its
|
|
14200
|
+
// end finds the undo done.
|
|
14201
|
+
const held = this.budgetHolds?.get(agent.name);
|
|
14202
|
+
const taken = held && continuesTurn(held.turn) && !this.hasTurn(agent.name) && !this.turnAlive(agent)
|
|
14203
|
+
? this.detachBudgetHold(agent.name, held) : undefined;
|
|
14204
|
+
try {
|
|
14205
|
+
return this.undoTurnOnWindow(agent, requester);
|
|
14206
|
+
}
|
|
14207
|
+
finally {
|
|
14208
|
+
if (taken)
|
|
14209
|
+
this.endDetachedHold(agent.name, taken, 'an undo took out what its held turn stood on');
|
|
14210
|
+
}
|
|
14211
|
+
}
|
|
14212
|
+
undoTurnOnWindow(agent, requester) {
|
|
13655
14213
|
const agentName = agent.name;
|
|
13656
14214
|
// A turn alive (its start, its stream, its tools, a continuation or a
|
|
13657
14215
|
// provider hold's retry on its way) holds the window: its next rows
|
|
@@ -13695,6 +14253,7 @@ export class AgentFramework {
|
|
|
13695
14253
|
}
|
|
13696
14254
|
this.emitTrace({ type: 'window:undone', agentName, retracted: 0 });
|
|
13697
14255
|
record(window);
|
|
14256
|
+
this.checkBudgetHoldsAfterWindowChange(agentName);
|
|
13698
14257
|
return { undone: true, turnIndex: checkpoint.turnIndex, fromBranch: branch, toBranch: branch, window };
|
|
13699
14258
|
}
|
|
13700
14259
|
const entry = {
|
|
@@ -13727,6 +14286,7 @@ export class AgentFramework {
|
|
|
13727
14286
|
const reason = error instanceof Error ? error.message : String(error);
|
|
13728
14287
|
this.emitTrace({ type: 'window:undone', agentName, retracted: left });
|
|
13729
14288
|
record({ retracted: left, represented: 0 }, reason);
|
|
14289
|
+
this.checkBudgetHoldsAfterWindowChange(agentName, false);
|
|
13730
14290
|
throw new OperatorActionError('failed', `Undo of ${agentName}'s turn ${checkpoint.turnIndex} was cut short: ${left} of the ${tail.length} rows it takes out of the window left it ` +
|
|
13731
14291
|
`(${reason}). redo() puts them back in their order, or another undo finishes it`, { cause: error });
|
|
13732
14292
|
}
|
|
@@ -13747,12 +14307,14 @@ export class AgentFramework {
|
|
|
13747
14307
|
const reason = error instanceof Error ? error.message : String(error);
|
|
13748
14308
|
this.emitTrace({ type: 'window:undone', agentName, retracted: tail.length });
|
|
13749
14309
|
record(window, reason);
|
|
14310
|
+
this.checkBudgetHoldsAfterWindowChange(agentName, false);
|
|
13750
14311
|
const rows = tail.length === 1 ? '1 row' : `${tail.length} rows`;
|
|
13751
14312
|
throw new OperatorActionError('failed', `Undo of ${agentName}'s turn ${checkpoint.turnIndex} took its ${rows} out of the window, and saving its checkpoints ` +
|
|
13752
14313
|
`failed (${reason}). redo() puts ${tail.length === 1 ? 'it' : 'them'} back, or another undo finishes it`, { cause: error });
|
|
13753
14314
|
}
|
|
13754
14315
|
this.emitTrace({ type: 'window:undone', agentName, retracted: tail.length });
|
|
13755
14316
|
record(window);
|
|
14317
|
+
this.checkBudgetHoldsAfterWindowChange(agentName);
|
|
13756
14318
|
return { undone: true, turnIndex: checkpoint.turnIndex, fromBranch: branch, toBranch: branch, window };
|
|
13757
14319
|
}
|
|
13758
14320
|
/**
|
|
@@ -13949,7 +14511,7 @@ export class AgentFramework {
|
|
|
13949
14511
|
this.retakeMarks(agent, cm, entry, back, below);
|
|
13950
14512
|
const ordinals = back.map((id) => cm.ordinalOf(id)).filter((ordinal) => ordinal !== undefined);
|
|
13951
14513
|
const boundary = {
|
|
13952
|
-
indexLengthBefore: ordinals.length > 0 ? Math.min(
|
|
14514
|
+
indexLengthBefore: ordinals.length > 0 ? ordinals.reduce((min, ordinal) => Math.min(min, ordinal), Infinity) : cm.indexLength(),
|
|
13953
14515
|
windowBranch: cm.currentBranch().name,
|
|
13954
14516
|
};
|
|
13955
14517
|
const checkpoints = this.getTurnCheckpoints(agentName);
|
|
@@ -14235,6 +14797,7 @@ export class AgentFramework {
|
|
|
14235
14797
|
this.releaseSurgeryHeldWrites();
|
|
14236
14798
|
for (const name of reserved.keys())
|
|
14237
14799
|
this.presentAtEndOf(this.agents.get(name));
|
|
14800
|
+
this.checkBudgetHoldsAfterWindowChange();
|
|
14238
14801
|
};
|
|
14239
14802
|
}
|
|
14240
14803
|
async reinitializeManagers(except) {
|
|
@@ -14294,7 +14857,7 @@ export class AgentFramework {
|
|
|
14294
14857
|
}
|
|
14295
14858
|
console.error(`[surgery] agent=${agentName}: its context manager is re-initialized, so the ${writes.length} write(s) kept in ` +
|
|
14296
14859
|
"memory for it while it refused its work go to the move's release, ahead of the writes its hold keeps");
|
|
14297
|
-
recovered
|
|
14860
|
+
appendAll(recovered, writes);
|
|
14298
14861
|
}
|
|
14299
14862
|
if (recovered.length === 0)
|
|
14300
14863
|
return;
|
|
@@ -14302,7 +14865,9 @@ export class AgentFramework {
|
|
|
14302
14865
|
// two agents whose slot one record holds (residents) land there in the
|
|
14303
14866
|
// order they came.
|
|
14304
14867
|
recovered.sort((x, y) => x.heldOrder - y.heldOrder);
|
|
14305
|
-
this.surgeryHeldWrites.
|
|
14868
|
+
const later = this.surgeryHeldWrites.splice(0);
|
|
14869
|
+
appendAll(this.surgeryHeldWrites, recovered);
|
|
14870
|
+
appendAll(this.surgeryHeldWrites, later);
|
|
14306
14871
|
if (!this.surgeryHold)
|
|
14307
14872
|
this.releaseSurgeryHeldWrites();
|
|
14308
14873
|
}
|
|
@@ -14365,7 +14930,7 @@ export class AgentFramework {
|
|
|
14365
14930
|
return found;
|
|
14366
14931
|
};
|
|
14367
14932
|
for (const [agentName, writes] of this.heldForRecovery ?? []) {
|
|
14368
|
-
entry(agentName).recovery
|
|
14933
|
+
appendAll(entry(agentName).recovery, writes.filter((write) => !told.has(write)));
|
|
14369
14934
|
}
|
|
14370
14935
|
for (const write of this.surgeryHeldWrites ?? []) {
|
|
14371
14936
|
if (told.has(write))
|
|
@@ -15677,10 +16242,19 @@ export class AgentFramework {
|
|
|
15677
16242
|
const checkpoints = this.getTurnCheckpoints(agentName);
|
|
15678
16243
|
checkpoints.push(checkpoint);
|
|
15679
16244
|
// Trim to max depth
|
|
15680
|
-
|
|
15681
|
-
checkpoints.splice(0, checkpoints.length - MAX_TURN_CHECKPOINTS)
|
|
15682
|
-
|
|
16245
|
+
const evicted = checkpoints.length > MAX_TURN_CHECKPOINTS
|
|
16246
|
+
? checkpoints.splice(0, checkpoints.length - MAX_TURN_CHECKPOINTS)
|
|
16247
|
+
: [];
|
|
15683
16248
|
this.saveTurnCheckpoints(agentName, checkpoints);
|
|
16249
|
+
return { turnIndex, evicted };
|
|
16250
|
+
}
|
|
16251
|
+
dropTurnCheckpoint(agentName, recorded) {
|
|
16252
|
+
const checkpoints = this.getTurnCheckpoints(agentName);
|
|
16253
|
+
if (checkpoints.at(-1)?.turnIndex !== recorded.turnIndex)
|
|
16254
|
+
return;
|
|
16255
|
+
checkpoints.pop();
|
|
16256
|
+
this.saveTurnCheckpoints(agentName, recorded.evicted.concat(checkpoints));
|
|
16257
|
+
this.turnCounters.set(agentName, recorded.turnIndex);
|
|
15684
16258
|
}
|
|
15685
16259
|
/**
|
|
15686
16260
|
* Checkpoints live in one Tree state (TURN_CHECKPOINTS_TREE_ID) keyed by agent
|
|
@@ -16560,7 +17134,7 @@ export class AgentFramework {
|
|
|
16560
17134
|
// reads the time it's written (clock-rows.ts).
|
|
16561
17135
|
const clock = injects ? this.storeDueClock(agent, 'tool-boundary') : undefined;
|
|
16562
17136
|
if (presentsWindow) {
|
|
16563
|
-
heard
|
|
17137
|
+
appendAll(heard, this.presentation.presentAtBoundary(agent, [replyId, resultsId, clock?.id], { injects }).heard);
|
|
16564
17138
|
}
|
|
16565
17139
|
// The clock's store and the boundary's presentations run their
|
|
16566
17140
|
// listeners synchronously too, and one may have stopped the turn,
|
|
@@ -16849,6 +17423,7 @@ export class AgentFramework {
|
|
|
16849
17423
|
timestamp: Date.now(),
|
|
16850
17424
|
suppressProse: this.activeTurnTriggers.get(agent.name)?.suppressProse,
|
|
16851
17425
|
silentHeartbeat: this.activeTurnTriggers.get(agent.name)?.silentHeartbeat,
|
|
17426
|
+
...this.wakeChannelsMark(this.turnRouting.get(agent.name)),
|
|
16852
17427
|
}, currentState.stream ? this.streamCarries.get(currentState.stream)?.() : undefined);
|
|
16853
17428
|
this.pendingRequests.push(continuation);
|
|
16854
17429
|
// Context budget exceeded: break the stream, let compile() compress.
|
|
@@ -16986,6 +17561,7 @@ export class AgentFramework {
|
|
|
16986
17561
|
timestamp: Date.now(),
|
|
16987
17562
|
suppressProse: this.activeTurnTriggers.get(agent.name)?.suppressProse,
|
|
16988
17563
|
silentHeartbeat: this.activeTurnTriggers.get(agent.name)?.silentHeartbeat,
|
|
17564
|
+
...this.wakeChannelsMark(this.turnRouting.get(agent.name)),
|
|
16989
17565
|
});
|
|
16990
17566
|
}
|
|
16991
17567
|
}
|
|
@@ -17759,6 +18335,7 @@ export class AgentFramework {
|
|
|
17759
18335
|
store: this.store,
|
|
17760
18336
|
namespace: `conversations/${name}`,
|
|
17761
18337
|
isolate: true,
|
|
18338
|
+
viewFilter: (message) => !isReasoningLoss(message.metadata),
|
|
17762
18339
|
// Strategy instances are stateful — never share the template's.
|
|
17763
18340
|
strategy: router.strategyFactory?.() ?? new PassthroughStrategy(),
|
|
17764
18341
|
// Provider cooldown ownership belongs to the persistent resident only;
|
|
@@ -17769,6 +18346,7 @@ export class AgentFramework {
|
|
|
17769
18346
|
});
|
|
17770
18347
|
release.manager = contextManager;
|
|
17771
18348
|
this.trackManager(contextManager);
|
|
18349
|
+
this.storeSees.set(contextManager, (metadata) => !isReasoningLoss(metadata));
|
|
17772
18350
|
// Seed with the template's compiled context, renaming the template
|
|
17773
18351
|
// participant so the fork reads its inheritance as its own history.
|
|
17774
18352
|
// Guard: only seed a genuinely fresh namespace. Generation counters are
|
|
@@ -18027,6 +18605,7 @@ export class AgentFramework {
|
|
|
18027
18605
|
this.thinkingLossIncidents.forget(agentName);
|
|
18028
18606
|
// A continuation of its turn still on its way goes with it.
|
|
18029
18607
|
this.dropContinuations(agentName, { kind: 'dropped', why: 'its agent was disposed' });
|
|
18608
|
+
this.dropBudgetHold(agentName, { kind: 'dropped', why: 'its agent was disposed' });
|
|
18030
18609
|
this.turnRouting.delete(agentName);
|
|
18031
18610
|
this.proseSpeechChains.delete(agentName);
|
|
18032
18611
|
this.openLogical.delete(agentName);
|
|
@@ -18125,8 +18704,8 @@ export class AgentFramework {
|
|
|
18125
18704
|
}
|
|
18126
18705
|
// Fallback reaper: a closing fork whose closure inference never ran
|
|
18127
18706
|
// (request dropped as stale, inference policy veto) would otherwise stay
|
|
18128
|
-
// registered forever. No turn, no active stream
|
|
18129
|
-
// done. A turn holds its token from dequeue, through its hooks and
|
|
18707
|
+
// registered forever. No turn, no active stream, no pending request, no
|
|
18708
|
+
// closure wake waiting and no budget hold = done. A turn holds its token from dequeue, through its hooks and
|
|
18130
18709
|
// compile, before its stream registers, so a fork mid-compile is busy;
|
|
18131
18710
|
// and a turn is alive while its continuation or a provider hold's retry
|
|
18132
18711
|
// of it is on its way (hasTurn), so a closure turn held for its retry
|
|
@@ -18134,7 +18713,9 @@ export class AgentFramework {
|
|
|
18134
18713
|
for (const agentName of [...this.closingConversationAgents]) {
|
|
18135
18714
|
if (!this.hasTurn(agentName) &&
|
|
18136
18715
|
!this.activeStreams.has(agentName) &&
|
|
18137
|
-
!this.pendingRequests.some((r) => r.agentName === agentName)
|
|
18716
|
+
!this.pendingRequests.some((r) => r.agentName === agentName) &&
|
|
18717
|
+
!this.closureWaits(agentName) &&
|
|
18718
|
+
!this.budgetHolds?.has(agentName)) {
|
|
18138
18719
|
this.disposeConversationAgent(agentName, 'idle-ttl');
|
|
18139
18720
|
}
|
|
18140
18721
|
}
|
|
@@ -19054,7 +19635,7 @@ export class AgentFramework {
|
|
|
19054
19635
|
eventType: 'mcpl:push-event',
|
|
19055
19636
|
...(occ.tags ? { tags: occ.tags } : {}),
|
|
19056
19637
|
},
|
|
19057
|
-
}, { suppressible, ...remembered });
|
|
19638
|
+
}, { suppressible, ...remembered, folds: true });
|
|
19058
19639
|
// Its batch's row, written later, reads this decision, since a rule's
|
|
19059
19640
|
// script answers once per event. (A copy of the occurrence, as a wake to
|
|
19060
19641
|
// read a row has, was decided before it was copied.)
|
|
@@ -19131,6 +19712,8 @@ export class AgentFramework {
|
|
|
19131
19712
|
unrecord(r);
|
|
19132
19713
|
for (const cooldown of this.providerAccelerationCooldowns.values())
|
|
19133
19714
|
cooldown.heldRequests.forEach(unrecord);
|
|
19715
|
+
for (const hold of this.budgetHolds?.values() ?? [])
|
|
19716
|
+
hold.wakes.forEach(unrecord);
|
|
19134
19717
|
for (const r of this.parkedContinuations ?? [])
|
|
19135
19718
|
unrecord(r);
|
|
19136
19719
|
for (const pass of this.passRequests ?? [])
|
|
@@ -19144,13 +19727,15 @@ export class AgentFramework {
|
|
|
19144
19727
|
if (held > 0 && cooldown.heldRequests.length === 0)
|
|
19145
19728
|
cooldown.emptiedBy = 'withdrawal';
|
|
19146
19729
|
}
|
|
19730
|
+
for (const hold of this.budgetHolds?.values() ?? [])
|
|
19731
|
+
hold.wakes = hold.wakes.filter((r) => !withdrawn(r));
|
|
19147
19732
|
for (const pass of this.passRequests ?? []) {
|
|
19148
19733
|
for (const requests of pass.values()) {
|
|
19149
19734
|
if (!requests.some((r) => emptied.has(r)))
|
|
19150
19735
|
continue;
|
|
19151
19736
|
const left = requests.filter((r) => !emptied.has(r));
|
|
19152
19737
|
requests.length = 0;
|
|
19153
|
-
requests
|
|
19738
|
+
appendAll(requests, left);
|
|
19154
19739
|
}
|
|
19155
19740
|
}
|
|
19156
19741
|
for (const start of emptied) {
|
|
@@ -19695,7 +20280,7 @@ export class AgentFramework {
|
|
|
19695
20280
|
const agent = this.agents.get(agentName);
|
|
19696
20281
|
if (!agent) {
|
|
19697
20282
|
// Agent not found — request is orphaned. Emit warning and drop.
|
|
19698
|
-
const oldest =
|
|
20283
|
+
const oldest = requests.reduce((min, r) => Math.min(min, r.timestamp), Infinity);
|
|
19699
20284
|
this.emitTrace({
|
|
19700
20285
|
type: 'inference:request_dropped',
|
|
19701
20286
|
agentName,
|
|
@@ -19710,6 +20295,12 @@ export class AgentFramework {
|
|
|
19710
20295
|
}
|
|
19711
20296
|
continue;
|
|
19712
20297
|
}
|
|
20298
|
+
const budgetHold = this.budgetHolds?.get(agentName);
|
|
20299
|
+
if (budgetHold) {
|
|
20300
|
+
for (const req of requests)
|
|
20301
|
+
this.holdRequest(budgetHold.wakes, req);
|
|
20302
|
+
continue;
|
|
20303
|
+
}
|
|
19713
20304
|
const providerCooldown = this.providerAccelerationCooldowns?.get(agentName);
|
|
19714
20305
|
if (providerCooldown) {
|
|
19715
20306
|
// A provider wait binds its own model; with a different primary model
|
|
@@ -19746,7 +20337,24 @@ export class AgentFramework {
|
|
|
19746
20337
|
// passes, and the read check and the policy decide on them all; the
|
|
19747
20338
|
// merge below records the batch anew.
|
|
19748
20339
|
if (requests.some((r) => mergedBatch(r) !== undefined)) {
|
|
19749
|
-
requests = requests.flatMap((r) =>
|
|
20340
|
+
requests = requests.flatMap((r) => {
|
|
20341
|
+
const batch = mergedBatch(r);
|
|
20342
|
+
if (!batch?.length)
|
|
20343
|
+
return [r];
|
|
20344
|
+
const wait = budgetWaitOf(r);
|
|
20345
|
+
const waitMarked = wait !== undefined && !batch.some((m) => budgetWaitOf(m) !== undefined);
|
|
20346
|
+
if (waitMarked && !batch.some((m) => this.sameInferenceRequest(m, r))) {
|
|
20347
|
+
const { [MERGED_BATCH]: _batch, ...folded } = r;
|
|
20348
|
+
return [...batch, folded];
|
|
20349
|
+
}
|
|
20350
|
+
const own = r.wakeChannels?.filter((c) => !batch.some((m) => m.wakeChannels?.some((x) => sameChannel(x, c))));
|
|
20351
|
+
const channels = own?.length ? wakeChannelsOf([batch[0], { wakeChannels: own }]) : undefined;
|
|
20352
|
+
const members = channels ? [{ ...batch[0], wakeChannels: channels }, ...batch.slice(1)] : batch;
|
|
20353
|
+
if (!waitMarked)
|
|
20354
|
+
return members;
|
|
20355
|
+
const at = members.findIndex((m) => this.sameInferenceRequest(m, r));
|
|
20356
|
+
return members.map((m, i) => (i === at ? { ...m, [BUDGET_WAIT]: wait } : m));
|
|
20357
|
+
});
|
|
19750
20358
|
}
|
|
19751
20359
|
// A provider's stated wait on this agent's primary model, including one
|
|
19752
20360
|
// recorded before a restart, holds the turn until it passes. An
|
|
@@ -19815,7 +20423,7 @@ export class AgentFramework {
|
|
|
19815
20423
|
// A heartbeat's wake keeps its own key, so a later channel-less wake
|
|
19816
20424
|
// can't absorb it: resumed together, they make a heartbeat's batch
|
|
19817
20425
|
// (heartbeatTurn), as they would have without the quiesce.
|
|
19818
|
-
const key = `${request.reason}|${request.addressed ? 'a' : 'n'}|${isHeartbeatWake(request) ? 'h' : ''}|${request.coalescingSubject ?? ''}|${request.readsRow ?? ''}`;
|
|
20426
|
+
const key = `${request.reason}|${request.addressed ? 'a' : 'n'}|${isHeartbeatWake(request) ? 'h' : ''}|${isUnprompted(request) ? 'u' : ''}|${request.coalescingSubject ?? ''}|${request.readsRow ?? ''}|${budgetWaitOf(request) ? 'waited' : ''}`;
|
|
19819
20427
|
const prev = newestByKey.get(key);
|
|
19820
20428
|
const kept = !prev || request.timestamp >= prev.timestamp ? request : prev;
|
|
19821
20429
|
// The wake it absorbs hands on its lines (WakeLine): each event's
|
|
@@ -19823,16 +20431,20 @@ export class AgentFramework {
|
|
|
19823
20431
|
const lines = prev ? mergeWakeLines([prev, request]) : undefined;
|
|
19824
20432
|
if (lines)
|
|
19825
20433
|
kept.wakeLines = lines;
|
|
20434
|
+
const pair = prev ? [prev, request] : undefined;
|
|
20435
|
+
const channels = pair ? wakeChannelsOf(pair) : undefined;
|
|
20436
|
+
if (channels)
|
|
20437
|
+
kept.wakeChannels = channels;
|
|
19826
20438
|
newestByKey.set(key, kept);
|
|
19827
20439
|
}
|
|
19828
|
-
this.pendingRequests
|
|
20440
|
+
appendAll(this.pendingRequests, [...newestByKey.values()]);
|
|
19829
20441
|
if (passThrough.length === 0)
|
|
19830
20442
|
continue;
|
|
19831
20443
|
// The continuation continues its held turn; its parked siblings must
|
|
19832
20444
|
// NOT be consumed by that turn — they were re-parked above, so narrow
|
|
19833
20445
|
// the batch to the continuation(s) alone before the trigger selection.
|
|
19834
20446
|
requests.length = 0;
|
|
19835
|
-
requests
|
|
20447
|
+
appendAll(requests, passThrough);
|
|
19836
20448
|
}
|
|
19837
20449
|
// A batch that carries a continuation which resumes its logical turn
|
|
19838
20450
|
// (resumesLogicalTurn: a budget restart, a tool-result guard's retry)
|
|
@@ -19849,10 +20461,10 @@ export class AgentFramework {
|
|
|
19849
20461
|
if (requests.some((r) => resumesLogicalTurn(r.reason))) {
|
|
19850
20462
|
const siblings = requests.filter((r) => !isTurnContinuation(r.reason));
|
|
19851
20463
|
if (siblings.length > 0) {
|
|
19852
|
-
this.pendingRequests
|
|
20464
|
+
appendAll(this.pendingRequests, siblings);
|
|
19853
20465
|
const continuations = requests.filter((r) => isTurnContinuation(r.reason));
|
|
19854
20466
|
requests.length = 0;
|
|
19855
|
-
requests
|
|
20467
|
+
appendAll(requests, continuations);
|
|
19856
20468
|
}
|
|
19857
20469
|
}
|
|
19858
20470
|
// Turn-alive includes a continuation of the agent's turn still on its
|
|
@@ -19874,7 +20486,7 @@ export class AgentFramework {
|
|
|
19874
20486
|
// Re-queue requests, but warn if they've been pending too long.
|
|
19875
20487
|
// Suppressed while quiesced: a drain-phase busy requeue is expected,
|
|
19876
20488
|
// not a wedge tell.
|
|
19877
|
-
const oldest =
|
|
20489
|
+
const oldest = requests.reduce((min, r) => Math.min(min, r.timestamp), Infinity);
|
|
19878
20490
|
if (!this.quiesced &&
|
|
19879
20491
|
now - oldest > STALE_REQUEST_MS &&
|
|
19880
20492
|
(this.staleWarnAt.get(agentName) ?? 0) < now - 60_000) {
|
|
@@ -19902,7 +20514,7 @@ export class AgentFramework {
|
|
|
19902
20514
|
console.error(`[inference-stale] agent=${agentName} busy (${shownStatus}) — ` +
|
|
19903
20515
|
`${requests.length} request(s) waiting ${Math.round((now - oldest) / 1000)}s`);
|
|
19904
20516
|
}
|
|
19905
|
-
this.pendingRequests
|
|
20517
|
+
appendAll(this.pendingRequests, requests);
|
|
19906
20518
|
continue;
|
|
19907
20519
|
}
|
|
19908
20520
|
// A wake whose sole cause was a stored row (readsRow): one not
|
|
@@ -19930,7 +20542,7 @@ export class AgentFramework {
|
|
|
19930
20542
|
if (read.length > 0) {
|
|
19931
20543
|
const left = requests.filter((r) => !read.includes(r));
|
|
19932
20544
|
requests.length = 0;
|
|
19933
|
-
requests
|
|
20545
|
+
appendAll(requests, left);
|
|
19934
20546
|
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` +
|
|
19935
20547
|
(requests.length === 0 ? '; no turn starts' : ''));
|
|
19936
20548
|
if (requests.length === 0) {
|
|
@@ -20104,6 +20716,7 @@ export class AgentFramework {
|
|
|
20104
20716
|
// channel is that message's, never one server's id with another's.
|
|
20105
20717
|
channelId: channelReq?.channelId,
|
|
20106
20718
|
channelServerId: channelReq?.channelServerId,
|
|
20719
|
+
wakeChannels: wakeChannelsOf(requests.some(bringsOwnChannels) ? requests.filter(hasOwnCauseForRun) : requests),
|
|
20107
20720
|
// Only the mark this batch decided, never one requests[0] carried.
|
|
20108
20721
|
autoContinueRun: continuationMark,
|
|
20109
20722
|
addressed: addressedReq !== undefined,
|
|
@@ -20128,10 +20741,19 @@ export class AgentFramework {
|
|
|
20128
20741
|
start[TURN_CARRY] = carry;
|
|
20129
20742
|
else
|
|
20130
20743
|
delete start[TURN_CARRY];
|
|
20744
|
+
const wait = requests.map(budgetWaitOf).find((w) => w !== undefined);
|
|
20745
|
+
if (wait)
|
|
20746
|
+
start[BUDGET_WAIT] = wait;
|
|
20747
|
+
else
|
|
20748
|
+
delete start[BUDGET_WAIT];
|
|
20131
20749
|
if (requests.every(isRearmed))
|
|
20132
20750
|
start[REARMED] = true;
|
|
20133
20751
|
else
|
|
20134
20752
|
delete start[REARMED];
|
|
20753
|
+
if (requests.every(isUnprompted))
|
|
20754
|
+
start[UNPROMPTED] = true;
|
|
20755
|
+
else
|
|
20756
|
+
delete start[UNPROMPTED];
|
|
20135
20757
|
// A start merged from more than one request, any of them not the
|
|
20136
20758
|
// turn's, records the batch it was merged from (MERGED_BATCH): until
|
|
20137
20759
|
// it takes its token it stands for them all.
|
|
@@ -20160,7 +20782,7 @@ export class AgentFramework {
|
|
|
20160
20782
|
*/
|
|
20161
20783
|
publishInputWithTurnLocus(input, agentName, callingTurn) {
|
|
20162
20784
|
const given = input?.channelId;
|
|
20163
|
-
if (
|
|
20785
|
+
if (given !== undefined && given !== null)
|
|
20164
20786
|
return input;
|
|
20165
20787
|
// A locked turn's default is its lock, or none: never what the registry
|
|
20166
20788
|
// resolves now, which reads the agent's latest routing (a silent start's).
|
|
@@ -20442,7 +21064,7 @@ export class AgentFramework {
|
|
|
20442
21064
|
}
|
|
20443
21065
|
if (held.length === 0)
|
|
20444
21066
|
return held;
|
|
20445
|
-
logical.drafts
|
|
21067
|
+
appendAll(logical.drafts, held);
|
|
20446
21068
|
console.error(`[drafts] ${agent.name}: held ${held.length} segment(s) as ${held.map((d) => d.id).join(', ')} (${reason})`);
|
|
20447
21069
|
this.emitTrace({
|
|
20448
21070
|
type: 'prose:drafts-held',
|
|
@@ -21143,7 +21765,12 @@ export class AgentFramework {
|
|
|
21143
21765
|
this.holdHeartbeatProse(agent, [text], round);
|
|
21144
21766
|
return null;
|
|
21145
21767
|
}
|
|
21146
|
-
return this.channelRegistry.routeSpeech(agent.name, text, locus);
|
|
21768
|
+
return this.channelRegistry.routeSpeech(agent.name, text, locus, this.cameFrom(round.routing, locus));
|
|
21769
|
+
}
|
|
21770
|
+
cameFrom(routing, locus) {
|
|
21771
|
+
return locus === null && !routing.noChannel && routing.wakeChannels
|
|
21772
|
+
? { cameFrom: routing.wakeChannels.map((c) => ({ channel: c, shown: this.channelShown(c) })) }
|
|
21773
|
+
: undefined;
|
|
21147
21774
|
}
|
|
21148
21775
|
/**
|
|
21149
21776
|
* Hold a locked turn's plain speech as drafts ('no-destination'): the agent
|
|
@@ -21165,6 +21792,17 @@ export class AgentFramework {
|
|
|
21165
21792
|
// A locked turn's locus is its lock, not a channel the heartbeat answered.
|
|
21166
21793
|
return routing.heartbeat && (!routing.locus || routing.locked) ? { heartbeatTurn: true } : {};
|
|
21167
21794
|
}
|
|
21795
|
+
unpromptedTurnMark(routing) {
|
|
21796
|
+
return routing.unprompted && !routing.locus ? { [UNPROMPTED]: true } : {};
|
|
21797
|
+
}
|
|
21798
|
+
wakeChannelsMark(routing) {
|
|
21799
|
+
return routing && !routing.noChannel && routing.wakeChannels ? { wakeChannels: routing.wakeChannels } : {};
|
|
21800
|
+
}
|
|
21801
|
+
turnRetryOf(trigger, routing) {
|
|
21802
|
+
const retry = asTurnRetry(trigger);
|
|
21803
|
+
delete retry.wakeChannels;
|
|
21804
|
+
return Object.assign(retry, this.wakeChannelsMark(routing));
|
|
21805
|
+
}
|
|
21168
21806
|
/** Hold a heartbeat turn's plain speech as drafts ('no-destination'), named by `round`'s notice. */
|
|
21169
21807
|
holdHeartbeatProse(agent, texts, round) {
|
|
21170
21808
|
console.error(`[prose] ${agent.name}: heartbeat turn — ${texts.length} plain-speech segment(s) held as drafts (no destination)`);
|
|
@@ -21805,7 +22443,7 @@ export class AgentFramework {
|
|
|
21805
22443
|
* and no undo or redo is made.
|
|
21806
22444
|
*/
|
|
21807
22445
|
logicalTurnAlive(agent) {
|
|
21808
|
-
return this.hasTurn(agent.name) || this.turnAlive(agent);
|
|
22446
|
+
return this.hasTurn(agent.name) || this.turnAlive(agent) || this.heldContinuation(agent.name);
|
|
21809
22447
|
}
|
|
21810
22448
|
/** Whether `agent` has a turn alive: starting, streaming or waiting on its tools. */
|
|
21811
22449
|
turnAlive(agent) {
|
|
@@ -22548,7 +23186,7 @@ export class AgentFramework {
|
|
|
22548
23186
|
}
|
|
22549
23187
|
agentSees(agent, metadata) {
|
|
22550
23188
|
const sees = this.storeSees.get(agent.getContextManager());
|
|
22551
|
-
return !sees || sees(metadata);
|
|
23189
|
+
return !isReasoningLoss(metadata) && (!sees || sees(metadata));
|
|
22552
23190
|
}
|
|
22553
23191
|
/**
|
|
22554
23192
|
* Record `view`, the filter `contextManager` was opened with
|
|
@@ -23169,6 +23807,8 @@ export class AgentFramework {
|
|
|
23169
23807
|
source: 'framework',
|
|
23170
23808
|
timestamp: Date.now(),
|
|
23171
23809
|
...this.heartbeatTurnMark(routing),
|
|
23810
|
+
...this.unpromptedTurnMark(routing),
|
|
23811
|
+
...this.wakeChannelsMark(routing),
|
|
23172
23812
|
});
|
|
23173
23813
|
}
|
|
23174
23814
|
/**
|
|
@@ -23260,6 +23900,8 @@ export class AgentFramework {
|
|
|
23260
23900
|
...channel,
|
|
23261
23901
|
autoContinueRun: { stop, ...(opts.heldAfterSend ? { heldAfterSend: true } : {}) },
|
|
23262
23902
|
...this.heartbeatTurnMark(routing),
|
|
23903
|
+
...this.unpromptedTurnMark(routing),
|
|
23904
|
+
...this.wakeChannelsMark(routing),
|
|
23263
23905
|
});
|
|
23264
23906
|
}
|
|
23265
23907
|
/** How an autoContinue agent stops a run, as its `[continue]` row says it,
|
|
@@ -23686,7 +24328,7 @@ export class AgentFramework {
|
|
|
23686
24328
|
}
|
|
23687
24329
|
const target = sticky ?? locus;
|
|
23688
24330
|
try {
|
|
23689
|
-
const outcome = await this.channelRegistry.routeSpeech(agent.name, envelope, target);
|
|
24331
|
+
const outcome = await this.channelRegistry.routeSpeech(agent.name, envelope, target, this.cameFrom(routing, target));
|
|
23690
24332
|
this.recordProseDelivery(routing, outcome);
|
|
23691
24333
|
if (outcome?.delivered) {
|
|
23692
24334
|
routing.liveSpeech = true;
|
|
@@ -24041,6 +24683,8 @@ export class AgentFramework {
|
|
|
24041
24683
|
source: 'framework',
|
|
24042
24684
|
timestamp: Date.now(),
|
|
24043
24685
|
...this.heartbeatTurnMark(round.routing),
|
|
24686
|
+
...this.unpromptedTurnMark(round.routing),
|
|
24687
|
+
...this.wakeChannelsMark(round.routing),
|
|
24044
24688
|
});
|
|
24045
24689
|
}
|
|
24046
24690
|
}
|
|
@@ -24174,7 +24818,7 @@ export class AgentFramework {
|
|
|
24174
24818
|
const batch = trigger && mergedBatch(trigger);
|
|
24175
24819
|
if (this.quiesced && batch) {
|
|
24176
24820
|
this.releasePrimaryProviderGate(agent.name);
|
|
24177
|
-
this.pendingRequests
|
|
24821
|
+
appendAll(this.pendingRequests, batch);
|
|
24178
24822
|
console.error(`[provider-admission] ${agent.name}: host quiesced while parked — the merged start goes back as its ${batch.length} requests`);
|
|
24179
24823
|
return;
|
|
24180
24824
|
}
|
|
@@ -24273,8 +24917,9 @@ export class AgentFramework {
|
|
|
24273
24917
|
// one's routing, and an ephemeral run fails with the error rather than
|
|
24274
24918
|
// waiting out its watchdog. A silent start has no part in an open turn.
|
|
24275
24919
|
if (!trigger?.suppressProse) {
|
|
24920
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
24276
24921
|
this.settleAgent(agent.name, {
|
|
24277
|
-
stopReason: 'exhausted', speech: '', error:
|
|
24922
|
+
stopReason: 'exhausted', speech: '', error: failureWords(err), cause: err,
|
|
24278
24923
|
}, this.openLogical.get(agent.name));
|
|
24279
24924
|
}
|
|
24280
24925
|
this.endStatedWaitRun(agent.name);
|
|
@@ -24301,7 +24946,7 @@ export class AgentFramework {
|
|
|
24301
24946
|
this.activeTurnTokens.delete(agent.name);
|
|
24302
24947
|
this.activeTurnTriggers.delete(agent.name);
|
|
24303
24948
|
}
|
|
24304
|
-
if (!tokenHandedOff && ownsProviderGate)
|
|
24949
|
+
if (!tokenHandedOff && ownsProviderGate && !checkWindow.gateReleased)
|
|
24305
24950
|
this.releasePrimaryProviderGate(agent.name);
|
|
24306
24951
|
// A start that ends before its stream exists is its turn's end: what
|
|
24307
24952
|
// arrived during it enters its window, as at a stream's teardown,
|
|
@@ -24336,7 +24981,7 @@ export class AgentFramework {
|
|
|
24336
24981
|
else {
|
|
24337
24982
|
dropped = true;
|
|
24338
24983
|
if (putsBack)
|
|
24339
|
-
back
|
|
24984
|
+
appendAll(back, mergedWakes(r));
|
|
24340
24985
|
}
|
|
24341
24986
|
}
|
|
24342
24987
|
return left;
|
|
@@ -24356,7 +25001,7 @@ export class AgentFramework {
|
|
|
24356
25001
|
this.releasePrimaryProviderGate(agentName);
|
|
24357
25002
|
dropped = true;
|
|
24358
25003
|
if (putsBack)
|
|
24359
|
-
back
|
|
25004
|
+
appendAll(back, mergedWakes(parked));
|
|
24360
25005
|
}
|
|
24361
25006
|
for (const pass of this.passRequests) {
|
|
24362
25007
|
const requests = pass.get(agentName);
|
|
@@ -24364,9 +25009,9 @@ export class AgentFramework {
|
|
|
24364
25009
|
continue;
|
|
24365
25010
|
const left = take(requests);
|
|
24366
25011
|
requests.length = 0;
|
|
24367
|
-
requests
|
|
25012
|
+
appendAll(requests, left);
|
|
24368
25013
|
}
|
|
24369
|
-
this.pendingRequests
|
|
25014
|
+
appendAll(this.pendingRequests, back);
|
|
24370
25015
|
if (!dropped || !drop)
|
|
24371
25016
|
return;
|
|
24372
25017
|
// A primary a provider hold left pending for what it released won't run
|
|
@@ -24388,17 +25033,18 @@ export class AgentFramework {
|
|
|
24388
25033
|
* ends on its own and settles nothing. What was stored for the agent
|
|
24389
25034
|
* meanwhile is presented now (presentAtEndOf), or at that frame's end.
|
|
24390
25035
|
*/
|
|
24391
|
-
endContinuedTurn(agentName, drop) {
|
|
24392
|
-
|
|
25036
|
+
endContinuedTurn(agentName, drop, opensTurn = false) {
|
|
25037
|
+
const ends = opensTurn ? 'its waiting start ends' : 'its turn ends without its continuation';
|
|
25038
|
+
console.error(`[turn-continuation] ${agentName}: ${ends}: ${drop.why}`);
|
|
24393
25039
|
const agent = this.agents.get(agentName);
|
|
24394
25040
|
const startedAt = this.lastInferenceAt.get(agentName)?.startedAt ?? Date.now();
|
|
24395
25041
|
if (agent && drop.kind !== 'dropped') {
|
|
24396
25042
|
const routing = this.openLogical.get(agentName) ?? this.routingOf(agentName);
|
|
24397
|
-
this.endStoppedStart(agent, drop.kind === 'caller' ? { kind: 'caller', reason: drop.reason } : { kind: drop.kind }, routing, startedAt, undefined,
|
|
25043
|
+
this.endStoppedStart(agent, drop.kind === 'caller' ? { kind: 'caller', reason: drop.reason } : { kind: drop.kind }, routing, startedAt, undefined, opensTurn);
|
|
24398
25044
|
}
|
|
24399
25045
|
else {
|
|
24400
25046
|
const owner = this.activeTurnTokens.get(agentName);
|
|
24401
|
-
this.abortDroppedTurn(agentName, owner ?? -1, drop.kind === 'dropped' ? CONTINUATION_DROPPED : drop.kind === 'caller' ? (drop.reason ?? 'user') : drop.kind, startedAt);
|
|
25047
|
+
this.abortDroppedTurn(agentName, owner ?? -1, drop.kind === 'dropped' ? (drop.reason ?? CONTINUATION_DROPPED) : drop.kind === 'caller' ? (drop.reason ?? 'user') : drop.kind, startedAt);
|
|
24402
25048
|
this.channelRegistry?.stopTyping();
|
|
24403
25049
|
if (agent) {
|
|
24404
25050
|
agent.toolResultGuard.recovering = false;
|
|
@@ -24418,7 +25064,7 @@ export class AgentFramework {
|
|
|
24418
25064
|
// A registered ephemeral run's caller waits on the run's settle, which
|
|
24419
25065
|
// no frame gives once its continuation is gone: the turn's end is the
|
|
24420
25066
|
// run's, with the drop's reason.
|
|
24421
|
-
this.settleAgent(agentName, { stopReason: 'exhausted', speech: '', error: `${agentName}'s turn ended without its continuation: ${drop.why}` }, undefined);
|
|
25067
|
+
this.settleAgent(agentName, { stopReason: 'exhausted', speech: '', error: `${agentName}'s ${opensTurn ? 'waiting start ended' : 'turn ended without its continuation'}: ${drop.why}` }, undefined);
|
|
24422
25068
|
}
|
|
24423
25069
|
// The turn's end; a frame that still holds its token presents at its
|
|
24424
25070
|
// own end instead.
|
|
@@ -24639,6 +25285,8 @@ export class AgentFramework {
|
|
|
24639
25285
|
/** This start's window (checkAhead), which its checks read for an
|
|
24640
25286
|
* operator's abandon. */
|
|
24641
25287
|
checkWindow) {
|
|
25288
|
+
if (attempt === 0 && this.budgetWaitEnded(agent, trigger))
|
|
25289
|
+
return false;
|
|
24642
25290
|
const resumesTurn = resumesLogicalTurn(trigger?.reason);
|
|
24643
25291
|
let carry = turnCarryOf(trigger);
|
|
24644
25292
|
const turnProseRouting = trigger?.suppressProse ? 'disabled' : this.proseRoutingOf(agent);
|
|
@@ -24801,8 +25449,10 @@ export class AgentFramework {
|
|
|
24801
25449
|
trigger = own;
|
|
24802
25450
|
}
|
|
24803
25451
|
// Record turn checkpoint before inference (only on first attempt, not retries)
|
|
25452
|
+
let checkpoint;
|
|
25453
|
+
let compileBaseline;
|
|
24804
25454
|
if (attempt === 0 && trigger?.reason !== 'tool_result_guard_retry') {
|
|
24805
|
-
this.recordTurnCheckpoint(agent.name);
|
|
25455
|
+
checkpoint = this.recordTurnCheckpoint(agent.name);
|
|
24806
25456
|
this.redoStacks.delete(agent.name); // new work invalidates redo
|
|
24807
25457
|
}
|
|
24808
25458
|
// This start's routing: its logical turn's (TurnRouting), which holds
|
|
@@ -24845,6 +25495,10 @@ export class AgentFramework {
|
|
|
24845
25495
|
routing.roundsRan = true;
|
|
24846
25496
|
if (continued?.unflushedRounds)
|
|
24847
25497
|
routing.unflushedRounds = continued.unflushedRounds;
|
|
25498
|
+
if (continued?.start)
|
|
25499
|
+
routing.start = continued.start;
|
|
25500
|
+
if (continued?.firstRow !== undefined)
|
|
25501
|
+
routing.firstRow = continued.firstRow;
|
|
24848
25502
|
opensTurn = true;
|
|
24849
25503
|
}
|
|
24850
25504
|
else {
|
|
@@ -24860,8 +25514,11 @@ export class AgentFramework {
|
|
|
24860
25514
|
}
|
|
24861
25515
|
if (opensTurn) {
|
|
24862
25516
|
const head = agent.branchHead();
|
|
24863
|
-
if (head)
|
|
25517
|
+
if (head && !routing.start)
|
|
24864
25518
|
routing.start = head;
|
|
25519
|
+
const firstRow = this.activeTurnTriggers.get(agent.name)?.readsRow;
|
|
25520
|
+
if (firstRow !== undefined)
|
|
25521
|
+
routing.firstRow = firstRow;
|
|
24865
25522
|
// A silent start, and an agent whose configuration turns the pin off
|
|
24866
25523
|
// (turnInputsPin: false), pin nothing, and a pin of the name left from
|
|
24867
25524
|
// before goes.
|
|
@@ -24903,12 +25560,16 @@ export class AgentFramework {
|
|
|
24903
25560
|
// a running turn without ending it, so the notice that writes the history
|
|
24904
25561
|
// no longer holds are still on disk goes in before this start's compile.
|
|
24905
25562
|
this.noticeHeadMoves(agent);
|
|
25563
|
+
const wakeChannels = wakeChannelsOf([routing, trigger]);
|
|
25564
|
+
if (wakeChannels)
|
|
25565
|
+
routing.wakeChannels = wakeChannels;
|
|
24906
25566
|
if (opensTurn) {
|
|
24907
25567
|
// The turn's trigger channel (item-3 redux): the channel of a
|
|
24908
25568
|
// channel/DM-triggered turn, with its server, and none for a
|
|
24909
25569
|
// no-trigger turn, which doesn't inherit a previous turn's channel: a
|
|
24910
25570
|
// heartbeat's takes no locus at all (TurnRouting.heartbeat), and any
|
|
24911
|
-
// other falls back to the global default
|
|
25571
|
+
// other falls back to the global default, with the configured
|
|
25572
|
+
// unprompted channel in it where the turn takes it. A wake whose channel,
|
|
24912
25573
|
// given without its server, names no one registered channel (a host's
|
|
24913
25574
|
// id that two servers' channels answer to, or that none does) has none
|
|
24914
25575
|
// either, and takes no fallback: it was for a channel, and the latest
|
|
@@ -24931,9 +25592,14 @@ export class AgentFramework {
|
|
|
24931
25592
|
routing.heartbeat = true;
|
|
24932
25593
|
else
|
|
24933
25594
|
delete routing.heartbeat;
|
|
25595
|
+
if (isUnprompted(trigger))
|
|
25596
|
+
routing.unprompted = true;
|
|
25597
|
+
else
|
|
25598
|
+
delete routing.unprompted;
|
|
24934
25599
|
// Resolve the turn's outbound locus now: the home channel for forks,
|
|
24935
25600
|
// else the trigger channel, else the global default, which an
|
|
24936
|
-
// ephemeral run never takes (takesGlobalFallback)
|
|
25601
|
+
// ephemeral run never takes (takesGlobalFallback), with the configured
|
|
25602
|
+
// unprompted channel in it where the turn takes it. Ambient mid-turn
|
|
24937
25603
|
// injections must not redirect the agent's plain prose (ambient
|
|
24938
25604
|
// chatter and reactions hijacked it before, 2026-07-21); only an
|
|
24939
25605
|
// addressed message, a follow-up in a channel the turn engaged
|
|
@@ -25019,7 +25685,9 @@ export class AgentFramework {
|
|
|
25019
25685
|
? 'at your budget restart'
|
|
25020
25686
|
: isTurnContinuation(trigger?.reason ?? '')
|
|
25021
25687
|
? 'before this re-inference on your tool results'
|
|
25022
|
-
:
|
|
25688
|
+
: budgetWaitOf(trigger)
|
|
25689
|
+
? 'while your turn waited'
|
|
25690
|
+
: 'while your turn waited on a provider hold';
|
|
25023
25691
|
this.noticeAddressedArrivals(agent, routing, arrivals, when);
|
|
25024
25692
|
}
|
|
25025
25693
|
}
|
|
@@ -25165,13 +25833,16 @@ export class AgentFramework {
|
|
|
25165
25833
|
if (carry && this.heardSince(agent, routing, carry.read))
|
|
25166
25834
|
carry = undefined;
|
|
25167
25835
|
const streamCarry = { silence: carry?.silence ?? null, sends: carry?.sends ?? null, read: lastHeardAt };
|
|
25168
|
-
|
|
25836
|
+
compileBaseline = { progress: this.compressionProgressOf(agent), settings: agent.contextSettingsChanges() };
|
|
25837
|
+
const { stream, request: compiledRequest, takeKvSubmission, drainKvSubmissionIds, continuePrompt, placeDrops, thinkingRecordedAfter, thinkingBranch, noteRequestSent, noteThinkingDropped, carriedBodies, } = await agent.startStreamWithInjections(tools, injections, undefined, compressionTools, {
|
|
25169
25838
|
silentHeartbeat: trigger?.silentHeartbeat,
|
|
25170
25839
|
// This turn's retries are the framework's (errorPolicy below), not
|
|
25171
25840
|
// membrane's inside each attempt.
|
|
25172
25841
|
membraneRetries: false,
|
|
25173
25842
|
inferenceBounds: this.inferenceBounds,
|
|
25174
25843
|
});
|
|
25844
|
+
if (trigger)
|
|
25845
|
+
delete trigger[BUDGET_WAIT];
|
|
25175
25846
|
// Each later round this stream sends is noted as it goes (noteRoundSent).
|
|
25176
25847
|
if (noteRequestSent)
|
|
25177
25848
|
this.streamRequestNotes.set(stream, { note: noteRequestSent, rounds: [], stopped: false });
|
|
@@ -25226,7 +25897,7 @@ export class AgentFramework {
|
|
|
25226
25897
|
if (this.registry.homeChannel(agent.name) !== undefined) {
|
|
25227
25898
|
const wait = this.providerWaits?.active(agent.name, agent.model);
|
|
25228
25899
|
// The turn has begun (inference:started), so the hold keeps its retry.
|
|
25229
|
-
if (wait && this.holdForProviderWait(agent, wait, trigger ? [
|
|
25900
|
+
if (wait && this.holdForProviderWait(agent, wait, trigger ? [this.turnRetryOf(withTurnCarry(trigger, carry), routing)] : [], false, () => {
|
|
25230
25901
|
// The stream the compile made won't be sent: it's cancelled, and
|
|
25231
25902
|
// this start's window closes, before the hold is reported, so a stop
|
|
25232
25903
|
// made while inference:failed is delivered finds neither, and the
|
|
@@ -25279,6 +25950,7 @@ export class AgentFramework {
|
|
|
25279
25950
|
place: placeDrops,
|
|
25280
25951
|
...(thinkingRecordedAfter !== undefined ? { recordedAfter: thinkingRecordedAfter } : {}),
|
|
25281
25952
|
...(thinkingBranch !== undefined ? { branch: thinkingBranch } : {}),
|
|
25953
|
+
...(noteThinkingDropped ? { noteDropped: noteThinkingDropped } : {}),
|
|
25282
25954
|
}
|
|
25283
25955
|
: undefined, streamCarry);
|
|
25284
25956
|
this.activeStreams.set(agent.name, handle);
|
|
@@ -25326,6 +25998,19 @@ export class AgentFramework {
|
|
|
25326
25998
|
}
|
|
25327
25999
|
const retry = trigger && withTurnCarry(trigger, carry);
|
|
25328
26000
|
const asTransient = this.takeAsTransient(agent.name, err, trigger);
|
|
26001
|
+
if (this.holdForBudgetRefusal(agent, err, retry, checkpoint, routing, compileBaseline, () => {
|
|
26002
|
+
if (this.checkAhead.get(agent) === checkWindow)
|
|
26003
|
+
this.checkAhead.delete(agent);
|
|
26004
|
+
if (this.activeTurnTokens.get(agent.name) === turnToken) {
|
|
26005
|
+
this.activeTurnTokens.delete(agent.name);
|
|
26006
|
+
this.activeTurnTriggers.delete(agent.name);
|
|
26007
|
+
}
|
|
26008
|
+
if (ownsProviderGate && !checkWindow.gateReleased) {
|
|
26009
|
+
checkWindow.gateReleased = true;
|
|
26010
|
+
this.releasePrimaryProviderGate(agent.name);
|
|
26011
|
+
}
|
|
26012
|
+
}))
|
|
26013
|
+
return false;
|
|
25329
26014
|
if (ownsProviderGate && this.holdProviderAcceleration(agent, err, retry)) {
|
|
25330
26015
|
// Capacity scheduling is not poisoned history. The cooldown timer owns
|
|
25331
26016
|
// one later fresh compile; no immediate retry or exhaustion marker.
|
|
@@ -25388,7 +26073,7 @@ export class AgentFramework {
|
|
|
25388
26073
|
// drop), not a window nothing will collect.
|
|
25389
26074
|
if (this.checkAhead.get(agent) === checkWindow)
|
|
25390
26075
|
this.checkAhead.delete(agent);
|
|
25391
|
-
await this.startAgentStream(agent, retry &&
|
|
26076
|
+
await this.startAgentStream(agent, retry && this.turnRetryOf(retry, routing), attempt + 1);
|
|
25392
26077
|
}
|
|
25393
26078
|
else {
|
|
25394
26079
|
this.noteFailedTurnEnd(agent, routing);
|
|
@@ -25406,7 +26091,8 @@ export class AgentFramework {
|
|
|
25406
26091
|
this.settleAgent(agent.name, {
|
|
25407
26092
|
stopReason: 'exhausted',
|
|
25408
26093
|
speech: '',
|
|
25409
|
-
...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: err
|
|
26094
|
+
...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: failureWords(err) }),
|
|
26095
|
+
cause: err,
|
|
25410
26096
|
}, routing);
|
|
25411
26097
|
if (isRearmed(trigger) && asTransient)
|
|
25412
26098
|
this.exhaustingRearms.add(agent.name);
|
|
@@ -25414,6 +26100,7 @@ export class AgentFramework {
|
|
|
25414
26100
|
type: 'inference:exhausted',
|
|
25415
26101
|
agentName: agent.name,
|
|
25416
26102
|
error: err.message,
|
|
26103
|
+
...shownReasonOf(err),
|
|
25417
26104
|
// Drives the poison-history breaker (shedMayHelp: an
|
|
25418
26105
|
// `invalid_request` at HTTP 400, 413 or 422 or with no status, or a
|
|
25419
26106
|
// `context_length` at 413, suggests the provider refused the
|
|
@@ -26376,7 +27063,7 @@ export class AgentFramework {
|
|
|
26376
27063
|
this.noteRefusal(agent.name, category, tokenUsage);
|
|
26377
27064
|
countUsage(abandoned);
|
|
26378
27065
|
logStream({
|
|
26379
|
-
success: false, error: 'Tool output withheld by the guard',
|
|
27066
|
+
success: false, error: 'Tool output withheld by the guard', errorWords: 'Tool output withheld by the guard',
|
|
26380
27067
|
request: compiledRequest ?? {}, response, durationMs, tokenUsage, stopReason: 'refusal',
|
|
26381
27068
|
});
|
|
26382
27069
|
console.error(`[tool-result-guard] agent=${agent.name} withheld ${withheld.length} result(s); retrying inference`);
|
|
@@ -26436,10 +27123,12 @@ export class AgentFramework {
|
|
|
26436
27123
|
// a postscript to a message already delivered.
|
|
26437
27124
|
// Restart inference inside this logical turn. No tool is
|
|
26438
27125
|
// executed again and no settle/checkpoint/locus reset occurs.
|
|
26439
|
-
|
|
27126
|
+
const retried = {
|
|
26440
27127
|
...trigger, agentName: agent.name, reason: 'tool_result_guard_retry',
|
|
26441
27128
|
source: trigger?.source ?? 'framework', timestamp: Date.now(),
|
|
26442
|
-
}
|
|
27129
|
+
};
|
|
27130
|
+
delete retried.wakeChannels;
|
|
27131
|
+
await this.startAgentStream(agent, withTurnCarry({ ...retried, ...this.wakeChannelsMark(streamRouting) }, { silence, sends: turnSends, read: carry.read }), attempt);
|
|
26443
27132
|
return;
|
|
26444
27133
|
}
|
|
26445
27134
|
const lastResult = response.content.reduce((last, block, index) => block.type === 'tool_result' ? index : last, -1);
|
|
@@ -26821,6 +27510,7 @@ export class AgentFramework {
|
|
|
26821
27510
|
reason: forced ? 'unstick-retry' : 'refusal-rewind-retry',
|
|
26822
27511
|
source: 'framework',
|
|
26823
27512
|
timestamp: Date.now(),
|
|
27513
|
+
...this.wakeChannelsMark(streamRouting),
|
|
26824
27514
|
});
|
|
26825
27515
|
handledByRewind = true;
|
|
26826
27516
|
};
|
|
@@ -27193,7 +27883,7 @@ export class AgentFramework {
|
|
|
27193
27883
|
});
|
|
27194
27884
|
logStream({
|
|
27195
27885
|
success: false,
|
|
27196
|
-
|
|
27886
|
+
...failedEntry('', err),
|
|
27197
27887
|
request: compiledRequest ?? { note: 'streaming request failed' },
|
|
27198
27888
|
durationMs,
|
|
27199
27889
|
});
|
|
@@ -27261,7 +27951,7 @@ export class AgentFramework {
|
|
|
27261
27951
|
return;
|
|
27262
27952
|
}
|
|
27263
27953
|
// Marked as its turn's retry, as at the start's failure.
|
|
27264
|
-
await this.startAgentStream(agent, retry &&
|
|
27954
|
+
await this.startAgentStream(agent, retry && this.turnRetryOf(retry, streamRouting), attempt + 1);
|
|
27265
27955
|
}
|
|
27266
27956
|
else {
|
|
27267
27957
|
// The turn ends here, and its inference:failed doesn't say so,
|
|
@@ -27286,7 +27976,8 @@ export class AgentFramework {
|
|
|
27286
27976
|
this.settleAgent(agent.name, {
|
|
27287
27977
|
stopReason: 'exhausted',
|
|
27288
27978
|
speech: '',
|
|
27289
|
-
...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: err
|
|
27979
|
+
...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: failureWords(err) }),
|
|
27980
|
+
cause: err,
|
|
27290
27981
|
}, this.agents.get(agent.name) === agent && agent.streamId === myStreamId ? streamRouting : undefined, streamRouting);
|
|
27291
27982
|
if (isRearmed(trigger) && asTransient)
|
|
27292
27983
|
this.exhaustingRearms.add(agent.name);
|
|
@@ -27294,6 +27985,7 @@ export class AgentFramework {
|
|
|
27294
27985
|
type: 'inference:exhausted',
|
|
27295
27986
|
agentName: agent.name,
|
|
27296
27987
|
error: err.message,
|
|
27988
|
+
...shownReasonOf(err),
|
|
27297
27989
|
// Drives the poison-history breaker. `retryable` is kept for
|
|
27298
27990
|
// observability, but the breaker gates on
|
|
27299
27991
|
// shedMayHelp(errorType, httpStatus): only an `invalid_request`
|
|
@@ -27382,6 +28074,7 @@ export class AgentFramework {
|
|
|
27382
28074
|
type: 'inference:exhausted',
|
|
27383
28075
|
agentName: agent.name,
|
|
27384
28076
|
error: terminal,
|
|
28077
|
+
shownReason: terminal,
|
|
27385
28078
|
});
|
|
27386
28079
|
this.rearmExhaustedTurn(agent, new Error(terminal), trigger, ownsProviderGate, streamRouting, false);
|
|
27387
28080
|
// Postmortem 2026-05-28 P2 #7: persist the abort to the
|
|
@@ -27391,6 +28084,7 @@ export class AgentFramework {
|
|
|
27391
28084
|
logStream({
|
|
27392
28085
|
success: false,
|
|
27393
28086
|
error: terminal,
|
|
28087
|
+
errorWords: terminal,
|
|
27394
28088
|
request: compiledRequest ?? { note: 'streaming request aborted' },
|
|
27395
28089
|
durationMs,
|
|
27396
28090
|
});
|
|
@@ -27475,10 +28169,13 @@ export class AgentFramework {
|
|
|
27475
28169
|
// carries what the stream appended, so once a call has dropped
|
|
27476
28170
|
// blocks, none of the stream's later calls is given either. A
|
|
27477
28171
|
// compile whose first call dropped blocks gives no sample, and the
|
|
27478
|
-
// calibration keeps its value.
|
|
27479
|
-
// model's blocks after a model switch
|
|
27480
|
-
// they drop on every call until they
|
|
27481
|
-
// calibration takes up the new model's
|
|
28172
|
+
// calibration keeps its value. Without `binds`, if the provider
|
|
28173
|
+
// drops an earlier model's blocks after a model switch
|
|
28174
|
+
// (`model_binding_mismatch`), they drop on every call until they
|
|
28175
|
+
// leave the context, and the calibration takes up the new model's
|
|
28176
|
+
// tokenizer only after that. With `binds`, the switch changes the
|
|
28177
|
+
// request's identity, so the compile leaves the replies' thinking
|
|
28178
|
+
// out itself, and a drop that does come is noted below.
|
|
27482
28179
|
const thinkingReport = event.round?.thinking;
|
|
27483
28180
|
const thinkingDropped = (thinkingReport?.dropped.length ?? 0) > 0;
|
|
27484
28181
|
if (thinkingDropped)
|
|
@@ -27486,6 +28183,18 @@ export class AgentFramework {
|
|
|
27486
28183
|
// Against the branch's binding record, when the context manager keeps one: a drop it can't account
|
|
27487
28184
|
// for tells the operators (noteThinkingLoss), and a call with no report shows no such drop.
|
|
27488
28185
|
const placedDrops = thinkingDrops && thinkingReport ? thinkingDrops.place(thinkingReport.dropped) : [];
|
|
28186
|
+
// The stored replies' bodies among them, as placed, go to the record, so the next compile sends them
|
|
28187
|
+
// without their thinking and the provider drops none of them again.
|
|
28188
|
+
const droppedBodies = placedDrops.flatMap((drop) => drop.kind === 'reply' ? drop.bodies.map((body) => body.messageId) : []);
|
|
28189
|
+
if (droppedBodies.length > 0 && thinkingDrops?.noteDropped) {
|
|
28190
|
+
try {
|
|
28191
|
+
thinkingDrops.noteDropped(droppedBodies);
|
|
28192
|
+
}
|
|
28193
|
+
catch (error) {
|
|
28194
|
+
console.error(`[thinking-binding] ${agent.name}: couldn't note the stored replies whose thinking the provider dropped, ` +
|
|
28195
|
+
'so the next compile still sends them with it:', error);
|
|
28196
|
+
}
|
|
28197
|
+
}
|
|
27489
28198
|
if (thinkingDrops?.recordedAfter !== undefined) {
|
|
27490
28199
|
this.noteThinkingLoss(agent.name, placedDrops, thinkingDrops.recordedAfter);
|
|
27491
28200
|
}
|
|
@@ -27628,7 +28337,7 @@ export class AgentFramework {
|
|
|
27628
28337
|
// ends here (endFailureAtStop).
|
|
27629
28338
|
this.endFailureAtStop(agent.name, myTurnToken ?? -1, startTime, streamRouting);
|
|
27630
28339
|
logStream({ success: false,
|
|
27631
|
-
|
|
28340
|
+
...failedEntry('Provider acceleration cooldown: ', err),
|
|
27632
28341
|
request: compiledRequest ?? { note: 'streaming request rate-limited' }, durationMs });
|
|
27633
28342
|
this.abandonAgentScripts(agent.name, 'provider acceleration cooldown', streamRouting);
|
|
27634
28343
|
lifecyclePhase = 'failed';
|
|
@@ -27645,7 +28354,7 @@ export class AgentFramework {
|
|
|
27645
28354
|
if (statedWait === 'held') {
|
|
27646
28355
|
this.endFailureAtStop(agent.name, myTurnToken ?? -1, startTime, streamRouting);
|
|
27647
28356
|
logStream({ success: false,
|
|
27648
|
-
|
|
28357
|
+
...failedEntry('Provider wait: ', err),
|
|
27649
28358
|
request: compiledRequest ?? { note: 'streaming request rate-limited' }, durationMs });
|
|
27650
28359
|
this.abandonAgentScripts(agent.name, 'provider wait', streamRouting);
|
|
27651
28360
|
lifecyclePhase = 'failed';
|
|
@@ -27658,7 +28367,8 @@ export class AgentFramework {
|
|
|
27658
28367
|
this.settleAgent(agent.name, {
|
|
27659
28368
|
stopReason: 'exhausted',
|
|
27660
28369
|
speech: '',
|
|
27661
|
-
...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: err
|
|
28370
|
+
...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: failureWords(err) }),
|
|
28371
|
+
cause: err,
|
|
27662
28372
|
}, streamRouting);
|
|
27663
28373
|
if (isRearmed(trigger) && asTransient)
|
|
27664
28374
|
this.exhaustingRearms.add(agent.name);
|
|
@@ -27666,6 +28376,7 @@ export class AgentFramework {
|
|
|
27666
28376
|
type: 'inference:exhausted',
|
|
27667
28377
|
agentName: agent.name,
|
|
27668
28378
|
error: err.message,
|
|
28379
|
+
...shownReasonOf(err),
|
|
27669
28380
|
...this.classifyInferenceError(err),
|
|
27670
28381
|
});
|
|
27671
28382
|
this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, streamRouting, asTransient);
|
|
@@ -27674,7 +28385,7 @@ export class AgentFramework {
|
|
|
27674
28385
|
// Persist to inference log for forensic attribution.
|
|
27675
28386
|
logStream({
|
|
27676
28387
|
success: false,
|
|
27677
|
-
|
|
28388
|
+
...failedEntry('Stream threw: ', err),
|
|
27678
28389
|
request: compiledRequest ?? { note: 'streaming request threw' },
|
|
27679
28390
|
durationMs,
|
|
27680
28391
|
});
|
|
@@ -27700,13 +28411,15 @@ export class AgentFramework {
|
|
|
27700
28411
|
if (countedUsage && !usageLogged) {
|
|
27701
28412
|
const replaced = this.agents.get(agent.name) !== agent;
|
|
27702
28413
|
const superseded = generationLost || agent.streamId !== myStreamId;
|
|
28414
|
+
const ended = replaced
|
|
28415
|
+
? 'Stream abandoned: its agent was disposed or replaced'
|
|
28416
|
+
: superseded
|
|
28417
|
+
? 'Stream superseded: a newer stream of the agent took over'
|
|
28418
|
+
: 'Stream ended without a final response';
|
|
27703
28419
|
logStream({
|
|
27704
28420
|
success: false,
|
|
27705
|
-
error:
|
|
27706
|
-
|
|
27707
|
-
: superseded
|
|
27708
|
-
? 'Stream superseded: a newer stream of the agent took over'
|
|
27709
|
-
: 'Stream ended without a final response',
|
|
28421
|
+
error: ended,
|
|
28422
|
+
errorWords: ended,
|
|
27710
28423
|
request: compiledRequest ?? { note: 'streaming request' },
|
|
27711
28424
|
durationMs: Date.now() - startTime,
|
|
27712
28425
|
tokenUsage: tokenUsageOf(countedUsage),
|
|
@@ -27857,7 +28570,7 @@ export class AgentFramework {
|
|
|
27857
28570
|
// so does one whose failure a provider hold keeps for its retry: the
|
|
27858
28571
|
// turn isn't over (pendingContinuationAgents).
|
|
27859
28572
|
if (frameReachedTerminal && ownsPhysicalStream && this.closingConversationAgents.has(agent.name)
|
|
27860
|
-
&& !this.pendingContinuationAgents().has(agent.name)) {
|
|
28573
|
+
&& !this.pendingContinuationAgents().has(agent.name) && !this.closureWaits(agent.name)) {
|
|
27861
28574
|
this.disposeConversationAgent(agent.name, 'idle-ttl');
|
|
27862
28575
|
}
|
|
27863
28576
|
}
|
|
@@ -27950,7 +28663,7 @@ export class AgentFramework {
|
|
|
27950
28663
|
if (this.surgeryHold) {
|
|
27951
28664
|
throw new Error(`puppet refused: the store is under live ${this.surgeryHold.verb} for ${this.surgeryHold.agentName} — retry when it completes`);
|
|
27952
28665
|
}
|
|
27953
|
-
if (agent.state.status !== 'idle' || this.hasTurn(agentName)) {
|
|
28666
|
+
if (agent.state.status !== 'idle' || this.hasTurn(agentName) || this.heldContinuation(agentName)) {
|
|
27954
28667
|
const shown = agent.state.status === 'idle' ? 'idle+turn-alive' : agent.state.status;
|
|
27955
28668
|
// A turn a provider hold keeps for its retry has no stream: the refusal
|
|
27956
28669
|
// names the hold, and how to go on sooner, as surgery's does.
|
|
@@ -27958,8 +28671,11 @@ export class AgentFramework {
|
|
|
27958
28671
|
throw new Error(held
|
|
27959
28672
|
? `puppet refused: agent ${agentName} is idle+turn-alive (${held.says}; requires idle); ` +
|
|
27960
28673
|
heldTurnWayOn(held.statedWait)
|
|
27961
|
-
:
|
|
27962
|
-
`
|
|
28674
|
+
: agent.state.status === 'idle' && !this.hasTurn(agentName)
|
|
28675
|
+
? `puppet refused: agent ${agentName} is idle+turn-alive (its turn waits for compression to make room; ` +
|
|
28676
|
+
'requires idle); abortInference() ends that turn'
|
|
28677
|
+
: `puppet refused: agent ${agentName} is ${shown} (requires idle — ` +
|
|
28678
|
+
`injecting a turn under an active stream corrupts wire ordering)`);
|
|
27963
28679
|
}
|
|
27964
28680
|
const onSurface = this.availableToolsForPresentation(agentName).some((t) => t.name === toolName);
|
|
27965
28681
|
if (!onSurface) {
|
|
@@ -29742,6 +30458,8 @@ export class AgentFramework {
|
|
|
29742
30458
|
// produced it is already stored separately as a blob.
|
|
29743
30459
|
if (typeof entryToStore.error === 'string')
|
|
29744
30460
|
entryToStore.error = boundFailureText(entryToStore.error);
|
|
30461
|
+
if (typeof entryToStore.errorWords === 'string')
|
|
30462
|
+
entryToStore.errorWords = boundFailureText(entryToStore.errorWords);
|
|
29745
30463
|
// Blob threshold: 10KB - typical context-heavy requests exceed this
|
|
29746
30464
|
const BLOB_THRESHOLD = 10000;
|
|
29747
30465
|
// Both branches persist the JSON view, never the live object: a compiled
|
|
@@ -29959,20 +30677,22 @@ export class AgentFramework {
|
|
|
29959
30677
|
}
|
|
29960
30678
|
/**
|
|
29961
30679
|
* Why this host refuses a skip_reply call, as the model will read it, or
|
|
29962
|
-
* undefined when it doesn't
|
|
29963
|
-
*
|
|
29964
|
-
*
|
|
29965
|
-
*
|
|
29966
|
-
*
|
|
29967
|
-
*
|
|
29968
|
-
*
|
|
29969
|
-
*
|
|
29970
|
-
*
|
|
30680
|
+
* undefined when it doesn't: a reason that isn't a string
|
|
30681
|
+
* (ChannelRegistry.nonStringRefusal), or (skipReplyWakeRefusal) without an
|
|
30682
|
+
* EventGate, any wake_in_seconds, and with one, a wake_in_seconds that isn't
|
|
30683
|
+
* a number of seconds. Only when the channel registry handles skip_reply.
|
|
30684
|
+
* The dispatch that refuses the call reads it here, and so does the turn's
|
|
30685
|
+
* bookkeeping of a refused round (roundCallOf), for the model's calls and a
|
|
30686
|
+
* script's calls that reach the host while a call of the round watches it:
|
|
30687
|
+
* the silence before it, which comes back after its round, and in XML mode
|
|
30688
|
+
* which round's prose stays private. A skip_reply round's own prose stays
|
|
30689
|
+
* private by the tool's name, refused or not.
|
|
29971
30690
|
*/
|
|
29972
30691
|
skipReplyRefusal(call) {
|
|
29973
30692
|
if (call.name !== 'skip_reply' || !this.channelRegistry)
|
|
29974
30693
|
return undefined;
|
|
29975
|
-
return
|
|
30694
|
+
return this.channelRegistry.nonStringRefusal('skip_reply', call.input)?.error
|
|
30695
|
+
?? skipReplyWakeRefusal(readSkipReplyWake(call.input), !!this.eventGate);
|
|
29976
30696
|
}
|
|
29977
30697
|
refusesSelfWake(call) {
|
|
29978
30698
|
return this.skipReplyRefusal(call) !== undefined;
|
|
@@ -29981,8 +30701,8 @@ export class AgentFramework {
|
|
|
29981
30701
|
* A call as its round's routing counts it, made directly, or by a script
|
|
29982
30702
|
* and reaching the host while a run or wait call of the round watches it
|
|
29983
30703
|
* (ScriptRound). A skip_reply refused, because the agent's allowedTools
|
|
29984
|
-
* doesn't name it, because its arguments are refused (markedInputRefusal)
|
|
29985
|
-
* or because this host
|
|
30704
|
+
* doesn't name it, because its arguments are refused (markedInputRefusal),
|
|
30705
|
+
* or because this host refuses its arguments (skipReplyRefusal), is
|
|
29986
30706
|
* marked refused: it keeps its own round private and silences nothing after
|
|
29987
30707
|
* it. A refusal withholds the call's effect, not the model's choice to keep
|
|
29988
30708
|
* its words private. Any other call the list refuses posted nothing, so it
|
|
@@ -30239,7 +30959,7 @@ export class AgentFramework {
|
|
|
30239
30959
|
const error = this.skipReplyRefusal(enrichedCall);
|
|
30240
30960
|
if (error !== undefined) {
|
|
30241
30961
|
const wakeValue = enrichedCall.input?.wake_in_seconds;
|
|
30242
|
-
console.error(`[self-wake] agent=${agentName} wake_in_seconds=${logValue(wakeValue)} skip_reply refused: ${logDetail(
|
|
30962
|
+
console.error(`[self-wake] agent=${agentName} wake_in_seconds=${logValue(wakeValue)} skip_reply refused: ${logDetail(error)}`);
|
|
30243
30963
|
const result = this.refusedResult(agentName, enrichedCall.id, error);
|
|
30244
30964
|
this.emitTrace({ type: 'tool:failed', module: 'channels', tool: enrichedCall.name, callId: enrichedCall.id, error });
|
|
30245
30965
|
this.pushEvent({ type: 'tool-result', callId: enrichedCall.id, toolName: enrichedCall.name, agentName, moduleName: 'channels', result });
|
|
@@ -31803,6 +32523,7 @@ export class AgentFramework {
|
|
|
31803
32523
|
this.noteInferenceExhausted(event.agentName ?? 'unknown', event.error ?? 'unknown error', event.retryable, event.errorType, {
|
|
31804
32524
|
httpStatus: event.httpStatus,
|
|
31805
32525
|
providerErrorCode: event.providerErrorCode,
|
|
32526
|
+
shownReason: event.shownReason,
|
|
31806
32527
|
});
|
|
31807
32528
|
}
|
|
31808
32529
|
else if (event.type === 'inference:completed') {
|
|
@@ -31868,6 +32589,8 @@ export class AgentFramework {
|
|
|
31868
32589
|
bounded.error = boundFailureText(bounded.error);
|
|
31869
32590
|
if (typeof bounded.stack === 'string')
|
|
31870
32591
|
bounded.stack = boundFailureText(bounded.stack);
|
|
32592
|
+
if (typeof bounded.shownReason === 'string')
|
|
32593
|
+
bounded.shownReason = boundFailureText(bounded.shownReason);
|
|
31871
32594
|
if (this.earlyTraces) {
|
|
31872
32595
|
if (this.earlyTraces.length < 1000)
|
|
31873
32596
|
this.earlyTraces.push(traceEvent);
|
|
@@ -31967,6 +32690,20 @@ export class AgentFramework {
|
|
|
31967
32690
|
};
|
|
31968
32691
|
})(),
|
|
31969
32692
|
lastInference: this.lastInferenceAt.get(name) ?? null,
|
|
32693
|
+
budgetHold: (() => {
|
|
32694
|
+
const hold = this.budgetHolds?.get(name);
|
|
32695
|
+
if (!hold)
|
|
32696
|
+
return null;
|
|
32697
|
+
const readiness = this.contextReadiness(agent.getContextManager());
|
|
32698
|
+
return {
|
|
32699
|
+
since: hold.startedAt,
|
|
32700
|
+
pending: 'pending' in readiness && readiness.pending ? boundFailureText(readiness.pending) : null,
|
|
32701
|
+
heldRequests: 1 + hold.wakes.flatMap((r) => mergedBatch(r) ?? [r]).filter((r) => !passesQuiesce(r)).length,
|
|
32702
|
+
errorType: hold.errorType,
|
|
32703
|
+
...(hold.budget !== undefined ? { budget: hold.budget } : {}),
|
|
32704
|
+
...(hold.actual !== undefined ? { actual: hold.actual } : {}),
|
|
32705
|
+
};
|
|
32706
|
+
})(),
|
|
31970
32707
|
refusalStats: this.refusalStats.get(name) ?? null,
|
|
31971
32708
|
...this.upcomingWakesHealth(name),
|
|
31972
32709
|
})),
|
|
@@ -31990,7 +32727,8 @@ export class AgentFramework {
|
|
|
31990
32727
|
}
|
|
31991
32728
|
/**
|
|
31992
32729
|
* Ops alert — the one escalation path for "a human should hear about this":
|
|
31993
|
-
* (1) durable failures.log record (unless the caller already wrote one
|
|
32730
|
+
* (1) durable failures.log record (unless the caller already wrote one, or
|
|
32731
|
+
* the alert keeps none, as a budget hold's open and clear don't),
|
|
31994
32732
|
* (2) an `ops:alert` trace so authorized observers get it on the wire,
|
|
31995
32733
|
* (3) if CONNECTOME_OPS_WEBHOOK is set, a Discord post throttled to one per
|
|
31996
32734
|
* (agent, kind) per cooldown window — a persistent failure re-posts
|
|
@@ -32024,7 +32762,7 @@ export class AgentFramework {
|
|
|
32024
32762
|
notes.stopped = true;
|
|
32025
32763
|
return;
|
|
32026
32764
|
}
|
|
32027
|
-
notes.rounds
|
|
32765
|
+
appendAll(notes.rounds, rows);
|
|
32028
32766
|
this.noteStreamRows(agent, notes);
|
|
32029
32767
|
}
|
|
32030
32768
|
/**
|
|
@@ -32233,17 +32971,8 @@ export class AgentFramework {
|
|
|
32233
32971
|
...(providerErrorCode !== undefined ? { providerErrorCode } : {}),
|
|
32234
32972
|
};
|
|
32235
32973
|
}
|
|
32236
|
-
|
|
32237
|
-
|
|
32238
|
-
}
|
|
32239
|
-
// context-manager's fatal coverage invariant (fix/coverage-invariant-fatal,
|
|
32240
|
-
// 2026-07-26): a compile REFUSES rather than shipping a context with
|
|
32241
|
-
// silently-dropped messages. Recovery is identical to over_budget — kick
|
|
32242
|
-
// the compression drain so summaries cover the un-represented span.
|
|
32243
|
-
if (err instanceof UncoveredDropError || err.name === 'UncoveredDropError') {
|
|
32244
|
-
return { errorType: 'context_refusal' };
|
|
32245
|
-
}
|
|
32246
|
-
return {};
|
|
32974
|
+
const refusal = compileRefusalType(err);
|
|
32975
|
+
return refusal !== undefined ? { errorType: refusal } : {};
|
|
32247
32976
|
}
|
|
32248
32977
|
/**
|
|
32249
32978
|
* Public entry to the ops-alert pipeline (failures.log + ops:alert trace +
|
|
@@ -32266,10 +32995,12 @@ export class AgentFramework {
|
|
|
32266
32995
|
// sink below gets an excerpt cut from the whole reason at its own bound
|
|
32267
32996
|
// (boundFailureText, so its marker states the whole length): the stderr
|
|
32268
32997
|
// line, failures.log and the hard-down alert FAILURE_TEXT_MAX_CHARS, the
|
|
32269
|
-
// [inference-failed] marker FAILURE_MARKER_REASON_MAX_CHARS, the
|
|
32270
|
-
// lastError and the compile-refused alert FAILURE_SUMMARY_MAX_CHARS,
|
|
32271
|
-
// the rewind marker REWIND_MARKER_REASON_MAX_CHARS. The
|
|
32272
|
-
// travels as structured fields beside the text.
|
|
32998
|
+
// [inference-failed] marker's metadata FAILURE_MARKER_REASON_MAX_CHARS, the
|
|
32999
|
+
// status's lastError and the compile-refused alert FAILURE_SUMMARY_MAX_CHARS,
|
|
33000
|
+
// and the rewind marker's metadata REWIND_MARKER_REASON_MAX_CHARS. The
|
|
33001
|
+
// classification travels as structured fields beside the text. The
|
|
33002
|
+
// [inference-failed] marker's text quotes only `shownReason`, which holds
|
|
33003
|
+
// no provider's text, and the rewind marker's quotes no reason.
|
|
32273
33004
|
//
|
|
32274
33005
|
// A reason that isn't a string (the stream paths always pass a message)
|
|
32275
33006
|
// is described rather than allowed to throw before anything is recorded.
|
|
@@ -32311,9 +33042,12 @@ export class AgentFramework {
|
|
|
32311
33042
|
const agent = this.agents.get(agentName);
|
|
32312
33043
|
if (agent && !(rearmed && !ended) && process.env.SUPPRESS_INFERENCE_FAILED_MARKER !== '1') {
|
|
32313
33044
|
const timedOut = errorType === 'timeout';
|
|
32314
|
-
const causeParts = [errorType, httpStatus !== undefined ? `HTTP ${httpStatus}` : undefined]
|
|
33045
|
+
const causeParts = errorType === 'safety' ? [] : [errorType, httpStatus !== undefined ? `HTTP ${httpStatus}` : undefined]
|
|
32315
33046
|
.filter((part) => part !== undefined);
|
|
32316
33047
|
const cause = timedOut && causeParts.length === 1 ? '' : causeParts.join(', ');
|
|
33048
|
+
const shownText = typeof detail?.shownReason === 'string'
|
|
33049
|
+
? failureShown(boundFailureText(detail.shownReason, FAILURE_MARKER_REASON_MAX_CHARS)) : undefined;
|
|
33050
|
+
const shown = shownText !== undefined ? ` Reason: ${shownText}${/[.!?]$/.test(shownText) ? '' : '.'}` : '';
|
|
32317
33051
|
try {
|
|
32318
33052
|
storeOwnRow(agent, 'user', [{
|
|
32319
33053
|
type: 'text',
|
|
@@ -32324,11 +33058,11 @@ export class AgentFramework {
|
|
|
32324
33058
|
`${ended.roundsRan ? ' Its earlier rounds ran, and their tool calls ran as their results show.' : ''}` +
|
|
32325
33059
|
`${ended.callsCutOff ? ` ${CALLS_CUT_OFF}` : ''}` +
|
|
32326
33060
|
`${ended.liveSpeech ? ` ${LIVE_SPEECH_UNRECEIPTED}` : ''}` +
|
|
32327
|
-
`${ended.speechRouting ? ` ${SPEECH_STILL_ROUTING}` : ''}
|
|
33061
|
+
`${ended.speechRouting ? ` ${SPEECH_STILL_ROUTING}` : ''}`
|
|
32328
33062
|
: `[${NOTICE_TAG.inferenceFailed}] Your previous turn did not complete: ` +
|
|
32329
33063
|
`the model call ${timedOut ? 'timed out' : 'failed'}${cause ? ` (${suppliedValue(cause)})` : ''} ` +
|
|
32330
|
-
`and produced no response, so nothing was sent.${madeAgain ? ` ${MADE_AGAIN}` : ''}
|
|
32331
|
-
`${
|
|
33064
|
+
`and produced no response, so nothing was sent.${madeAgain ? ` ${MADE_AGAIN}` : ''}`) +
|
|
33065
|
+
`${shown} ${timedOut ? TIMEOUT_ADVICE : FAILURE_ADVICE}`,
|
|
32332
33066
|
}], { system: true, kind: 'inference-failed', reason: markerReason, consecutive: streak, ...classification, ...cutAt(markerReason) });
|
|
32333
33067
|
}
|
|
32334
33068
|
catch (err) {
|
|
@@ -32369,49 +33103,7 @@ export class AgentFramework {
|
|
|
32369
33103
|
// runUntilIdle() has no maintenance timer, so a drain is its recovery.
|
|
32370
33104
|
const drainState = !overBudget ? undefined
|
|
32371
33105
|
: !agent ? 'unavailable (no agent handle)'
|
|
32372
|
-
: this.
|
|
32373
|
-
: this.surgeryHold ? 'not kicked: a surgery holds the store'
|
|
32374
|
-
: this.overBudgetDrainInFlight.has(agentName) ? 'already draining'
|
|
32375
|
-
: 'kicking';
|
|
32376
|
-
if (agent && drainState === 'kicking') {
|
|
32377
|
-
this.overBudgetDrainInFlight.add(agentName);
|
|
32378
|
-
// Like a maintenance pass: none starts once stop() has begun, it starts
|
|
32379
|
-
// no tick while a surgery holds the store or once its agent has been
|
|
32380
|
-
// disposed, and a surgery, a switchBranch or stop() waits for the tick
|
|
32381
|
-
// in flight. It joins the drains they wait for before the alert is
|
|
32382
|
-
// raised and before its body runs, so the alert's listeners find it:
|
|
32383
|
-
// one that begins stop() or a surgery waits for it, and its body then
|
|
32384
|
-
// ticks none and says why. So does a stop() made from inside its first
|
|
32385
|
-
// tick, before that tick's first await.
|
|
32386
|
-
const drain = Promise.resolve().then(async () => {
|
|
32387
|
-
let ticks = 0;
|
|
32388
|
-
try {
|
|
32389
|
-
const cm = agent.getContextManager();
|
|
32390
|
-
while (ticks < 8 && !this.providerAdmissionClosed && !this.surgeryHold && this.agents.get(agentName) === agent) {
|
|
32391
|
-
await cm.tick();
|
|
32392
|
-
ticks++;
|
|
32393
|
-
}
|
|
32394
|
-
const stopped = ticks >= 8 ? ''
|
|
32395
|
-
: this.agents.get(agentName) !== agent ? ', stopped as the agent was disposed'
|
|
32396
|
-
: this.providerAdmissionClosed ? ', stopped as the framework stops'
|
|
32397
|
-
: ', stopped while a surgery holds the store';
|
|
32398
|
-
console.error(`[inference-failed] drain kicked for ${agentName} (OverBudget breaker, ${ticks} ticks${stopped})`);
|
|
32399
|
-
}
|
|
32400
|
-
catch (err) {
|
|
32401
|
-
if (err instanceof MaintenanceWithdrawnError) {
|
|
32402
|
-
console.error(`[inference-failed] drain kicked for ${agentName} (OverBudget breaker, ${ticks} ticks, stopped: ${err.message})`);
|
|
32403
|
-
}
|
|
32404
|
-
else {
|
|
32405
|
-
console.error(`[inference-failed] drain kick failed for ${agentName} after ${ticks} ticks: ${describeFailureWithFrames(err)}`);
|
|
32406
|
-
}
|
|
32407
|
-
}
|
|
32408
|
-
finally {
|
|
32409
|
-
this.overBudgetDrainInFlight.delete(agentName);
|
|
32410
|
-
}
|
|
32411
|
-
});
|
|
32412
|
-
this.overBudgetDrains.add(drain);
|
|
32413
|
-
void drain.finally(() => { this.overBudgetDrains.delete(drain); });
|
|
32414
|
-
}
|
|
33106
|
+
: this.kickOverBudgetDrain(agentName, agent);
|
|
32415
33107
|
if (overBudget) {
|
|
32416
33108
|
this.opsAlert('context-refusal', agentName, `compile refused (${errorType ?? 'unclassified'}): ${boundFailureText(wholeReason, FAILURE_SUMMARY_MAX_CHARS)} — ` +
|
|
32417
33109
|
`drain breaker ${drainState}; ` +
|
|
@@ -32485,6 +33177,7 @@ export class AgentFramework {
|
|
|
32485
33177
|
reason: 'inference-failure-rewind-retry',
|
|
32486
33178
|
source: 'framework',
|
|
32487
33179
|
timestamp: Date.now(),
|
|
33180
|
+
...this.wakeChannelsMark(this.turnRouting?.get(agent.name)),
|
|
32488
33181
|
});
|
|
32489
33182
|
}
|
|
32490
33183
|
// ==========================================================================
|
|
@@ -32667,7 +33360,8 @@ export class AgentFramework {
|
|
|
32667
33360
|
// a later start of the turn is told where its unprefixed speech lands
|
|
32668
33361
|
// unless a hybrid `>>>` destination takes it. Empty for a no-trigger
|
|
32669
33362
|
// turn until its locus is resolved, so that resolution takes the
|
|
32670
|
-
// global fallback
|
|
33363
|
+
// global fallback, the configured unprompted channel among it (none
|
|
33364
|
+
// for an ephemeral run), and the turn then keeps
|
|
32671
33365
|
// that channel; null for a heartbeat's turn, which takes none
|
|
32672
33366
|
// (TurnRouting.heartbeat).
|
|
32673
33367
|
// A turn with no channel of its own (a heartbeat's, or a wake for a
|
|
@@ -32687,7 +33381,9 @@ export class AgentFramework {
|
|
|
32687
33381
|
// message's channel is nobody's choice for it (#551). By name, so a
|
|
32688
33382
|
// speech dispatch that comes after the run's release still counts.
|
|
32689
33383
|
takesGlobalFallback: (conversationId) => this.usedEphemeralAgentNames.get(conversationId) !== 'run',
|
|
33384
|
+
takesUnpromptedChannel: (conversationId) => this.openLogical.get(conversationId)?.unprompted === true,
|
|
32690
33385
|
eventGatePresent: this.eventGate !== null,
|
|
33386
|
+
unpromptedChannel: this.mcplUnpromptedChannel ?? undefined,
|
|
32691
33387
|
// A channel tool reads the connect and restart marks a tool call's
|
|
32692
33388
|
// "connecting" text reads (mcplConnectInFlight, mcplServerAbsentText).
|
|
32693
33389
|
// This host is connecting to a server while a connect or restart of
|
|
@@ -32708,7 +33404,7 @@ export class AgentFramework {
|
|
|
32708
33404
|
// or wasn't confirmed to. addMessage() alone does not request
|
|
32709
33405
|
// inference, so this never wakes her (matching the
|
|
32710
33406
|
// `discord-send-failed-skip` gate intent: context yes, wake no).
|
|
32711
|
-
onRouteFailure: ({ conversationId, channelId, serverId, reason, textLen, outcome, detail }) => {
|
|
33407
|
+
onRouteFailure: ({ conversationId, channelId, serverId, reason, textLen, outcome, detail, byDesign }) => {
|
|
32712
33408
|
try {
|
|
32713
33409
|
// The marker is `system: true`, so it is never conversational and
|
|
32714
33410
|
// never influences routing. The channel is named by its label when
|
|
@@ -32738,7 +33434,7 @@ export class AgentFramework {
|
|
|
32738
33434
|
// have been posted (in part or whole). Never claim it wasn't.
|
|
32739
33435
|
? `[${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.`
|
|
32740
33436
|
: `[${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.`)
|
|
32741
|
-
: `[${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.`;
|
|
33437
|
+
: `[${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.`;
|
|
32742
33438
|
// A registered speaker's notice is addressed to it (noticeTargetFor).
|
|
32743
33439
|
this.addMessage('user', [{ type: 'text', text: text + (detail !== undefined ? ` Connector detail: ${suppliedValue(detail)}` : '') }], metadata, { forAgent: conversationId });
|
|
32744
33440
|
}
|
|
@@ -35846,7 +36542,7 @@ export class AgentFramework {
|
|
|
35846
36542
|
.filter(([, owner]) => owner === serverId)
|
|
35847
36543
|
.map(([name]) => name);
|
|
35848
36544
|
const known = new Set(names);
|
|
35849
|
-
tools
|
|
36545
|
+
appendAll(tools, (this.mcplTools ?? []).filter((tool) => known.has(tool.name)));
|
|
35850
36546
|
for (const name of names) {
|
|
35851
36547
|
toolServers.set(name, serverId);
|
|
35852
36548
|
const featureSet = this.mcplToolFeatureSets?.get(name);
|
|
@@ -35859,7 +36555,7 @@ export class AgentFramework {
|
|
|
35859
36555
|
kept.set(serverId, names.length);
|
|
35860
36556
|
}
|
|
35861
36557
|
else if (listedServers.has(serverId)) {
|
|
35862
|
-
tools
|
|
36558
|
+
appendAll(tools, staged.tools);
|
|
35863
36559
|
for (const [name, featureSet] of staged.featureSets)
|
|
35864
36560
|
toolFeatureSets.set(name, featureSet);
|
|
35865
36561
|
for (const [name, classes] of staged.classes)
|
|
@@ -35876,7 +36572,7 @@ export class AgentFramework {
|
|
|
35876
36572
|
console.error(`[mcpl] server "${serverId}": tools/list failed (${error}); ${count > 0 ? `keeping its ${count} known tool(s)` : 'none of its tools are offered'}`);
|
|
35877
36573
|
}
|
|
35878
36574
|
}
|
|
35879
|
-
tools
|
|
36575
|
+
appendAll(tools, over.size > 0 ? modernTools.filter((tool) => toolServers.has(tool.name)) : modernTools);
|
|
35880
36576
|
this.mcplToolsRefreshPublished = generation;
|
|
35881
36577
|
this.mcplTools = tools;
|
|
35882
36578
|
this.mcplPublishedConnections = publishedConnections;
|
|
@@ -36810,6 +37506,11 @@ export class AgentFramework {
|
|
|
36810
37506
|
return;
|
|
36811
37507
|
}
|
|
36812
37508
|
if (call.name === 'channel_publish' || call.name === 'channel_close') {
|
|
37509
|
+
const refusal = this.channelRegistry?.nonStringRefusal(call.name, call.input);
|
|
37510
|
+
if (refusal) {
|
|
37511
|
+
reject(refusal.error);
|
|
37512
|
+
return;
|
|
37513
|
+
}
|
|
36813
37514
|
const input = (call.input ?? {});
|
|
36814
37515
|
// An empty serverId is read as none given, as the channel tools and
|
|
36815
37516
|
// channelNamed read it.
|
|
@@ -37342,7 +38043,7 @@ export class AgentFramework {
|
|
|
37342
38043
|
.filter(([field]) => !(field.startsWith('behavior.') && typeof input.behavior === 'object')));
|
|
37343
38044
|
const policy = this.eventGate.addPolicy(rawPolicy, {
|
|
37344
38045
|
...(position ? { position } : {}),
|
|
37345
|
-
onIgnoredFields: (notes) => { ignored
|
|
38046
|
+
onIgnoredFields: (notes) => { appendAll(ignored, notes); },
|
|
37346
38047
|
fieldNames,
|
|
37347
38048
|
});
|
|
37348
38049
|
const after = this.eventGate.probeTable();
|
|
@@ -37667,8 +38368,9 @@ export class AgentFramework {
|
|
|
37667
38368
|
/**
|
|
37668
38369
|
* Whether `agent`'s view leaves suppressed rows out, asked of the view
|
|
37669
38370
|
* itself (storeSees): every resident's, the subconscious's and a view
|
|
37670
|
-
* derived from one does
|
|
37671
|
-
* so a row stamped
|
|
38371
|
+
* derived from one does. An isolated agent's own view excludes legacy
|
|
38372
|
+
* reasoning-loss notices, not suppressed rows, so a suppressed row stamped
|
|
38373
|
+
* there would render.
|
|
37672
38374
|
*/
|
|
37673
38375
|
viewHidesSuppressed(agent) {
|
|
37674
38376
|
if (!agent)
|
|
@@ -37732,6 +38434,7 @@ export class AgentFramework {
|
|
|
37732
38434
|
tags: Array.isArray(e.gateMetadata.tags) ? e.gateMetadata.tags : undefined,
|
|
37733
38435
|
...(e.targetAgents !== undefined ? { recipients: e.targetAgents.filter((name) => this.agents.has(name)) } : {}),
|
|
37734
38436
|
...(asked ? { suppressionAsked: true } : {}),
|
|
38437
|
+
...(opts.folds ? { folds: true } : {}),
|
|
37735
38438
|
});
|
|
37736
38439
|
const decision = admit(env, {
|
|
37737
38440
|
untargetedRecipients: () => this.registry.untargetedRecipients(),
|
|
@@ -38085,7 +38788,7 @@ export class AgentFramework {
|
|
|
38085
38788
|
}
|
|
38086
38789
|
else {
|
|
38087
38790
|
const locus = callingTurn.locus ?? null;
|
|
38088
|
-
registry.routeSpeech(agentName, announcement(locus), locus).then(noted).catch((err) => {
|
|
38791
|
+
registry.routeSpeech(agentName, announcement(locus), locus, this.cameFrom(callingTurn, locus)).then(noted).catch((err) => {
|
|
38089
38792
|
console.error('[sleep] announce failed:', err instanceof Error ? err.message : err);
|
|
38090
38793
|
});
|
|
38091
38794
|
}
|