@animalabs/agent-framework 0.30.0 → 0.31.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 +71 -8
- package/dist/src/agent.d.ts +29 -9
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +82 -17
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/mcp-server-core.d.ts.map +1 -1
- package/dist/src/api/mcp-server-core.js +10 -3
- package/dist/src/api/mcp-server-core.js.map +1 -1
- package/dist/src/code-execution/isolate/render.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/render.js +7 -6
- package/dist/src/code-execution/isolate/render.js.map +1 -1
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +5 -4
- package/dist/src/code-execution/isolate/scripts.js.map +1 -1
- package/dist/src/framework.d.ts +129 -92
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1115 -704
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +24 -17
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +198 -144
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/recipe-reconcile.d.ts.map +1 -1
- package/dist/src/gate/recipe-reconcile.js +6 -5
- package/dist/src/gate/recipe-reconcile.js.map +1 -1
- package/dist/src/host-presentation.d.ts.map +1 -1
- package/dist/src/host-presentation.js +5 -7
- package/dist/src/host-presentation.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 +37 -0
- package/dist/src/inference-bounds.d.ts.map +1 -0
- package/dist/src/inference-bounds.js +84 -0
- package/dist/src/inference-bounds.js.map +1 -0
- package/dist/src/line-reader.d.ts +7 -1
- package/dist/src/line-reader.d.ts.map +1 -1
- package/dist/src/line-reader.js +17 -6
- package/dist/src/line-reader.js.map +1 -1
- package/dist/src/mcpl/errors.d.ts +1 -0
- package/dist/src/mcpl/errors.d.ts.map +1 -1
- package/dist/src/mcpl/errors.js +3 -0
- package/dist/src/mcpl/errors.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +2 -2
- package/dist/src/mcpl/inbound-source.js +2 -2
- package/dist/src/mcpl/inference-router.d.ts +11 -1
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.js +65 -9
- package/dist/src/mcpl/inference-router.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts +10 -7
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
- package/dist/src/mcpl/modern-connection.js +85 -74
- package/dist/src/mcpl/modern-connection.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts +1 -1
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +45 -3
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +2 -7
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +1 -2
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +3 -3
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +14 -0
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +26 -22
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts +1 -0
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
- package/dist/src/mcpl/tool-signature.js +16 -11
- package/dist/src/mcpl/tool-signature.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +44 -21
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +283 -64
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +17 -1
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +32 -4
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +189 -25
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
- package/dist/src/modules/discord/discord-js-client.js +24 -16
- package/dist/src/modules/discord/discord-js-client.js.map +1 -1
- package/dist/src/modules/sender-name.d.ts +5 -4
- package/dist/src/modules/sender-name.d.ts.map +1 -1
- package/dist/src/modules/sender-name.js +7 -5
- package/dist/src/modules/sender-name.js.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.d.ts +16 -8
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +33 -12
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +6 -3
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +61 -15
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +9 -1
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +50 -10
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts +11 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +19 -8
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +8 -0
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts +7 -0
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +7 -5
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.js +1 -9
- package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
- package/dist/src/supplied-text.d.ts +2 -0
- package/dist/src/supplied-text.d.ts.map +1 -1
- package/dist/src/supplied-text.js +16 -7
- package/dist/src/supplied-text.js.map +1 -1
- package/dist/src/tool-result-guard.js +1 -1
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +2 -20
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +175 -70
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts +14 -2
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/tune-out/tools.js.map +1 -1
- package/dist/src/types/agent.d.ts +2 -1
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +54 -15
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +42 -3
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +4 -3
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agent.ts +90 -25
- package/src/api/mcp-server-core.ts +10 -3
- package/src/code-execution/isolate/render.ts +7 -6
- package/src/code-execution/isolate/scripts.ts +5 -4
- package/src/framework.ts +1145 -723
- package/src/gate/event-gate.ts +220 -145
- package/src/gate/recipe-reconcile.ts +7 -5
- package/src/host-presentation.ts +4 -7
- package/src/index.ts +1 -0
- package/src/inference-bounds.ts +111 -0
- package/src/line-reader.ts +22 -5
- package/src/mcpl/errors.ts +4 -0
- package/src/mcpl/inbound-source.ts +2 -2
- package/src/mcpl/inference-router.ts +67 -10
- package/src/mcpl/modern-connection.ts +83 -68
- package/src/mcpl/protocol-family.ts +37 -4
- package/src/mcpl/push-coalescer.ts +2 -7
- package/src/mcpl/references.ts +3 -4
- package/src/mcpl/server-connection.ts +30 -21
- package/src/mcpl/tool-signature.ts +18 -11
- package/src/mcpl/transport.ts +261 -60
- package/src/mcpl/types.ts +18 -1
- package/src/module-registry.ts +178 -25
- package/src/modules/discord/discord-js-client.ts +33 -29
- package/src/modules/sender-name.ts +8 -5
- package/src/modules/workspace/disk-agreement.ts +39 -15
- package/src/modules/workspace/index.ts +65 -14
- package/src/modules/workspace/observe.ts +51 -10
- package/src/modules/workspace/reconcile.ts +22 -8
- package/src/modules/workspace/types.ts +5 -0
- package/src/modules/workspace/watcher.ts +8 -6
- package/src/recovery/discord-awareness-outbox.ts +1 -11
- package/src/supplied-text.ts +17 -6
- package/src/tool-result-guard.ts +1 -1
- package/src/tune-out/coordinator.ts +187 -85
- package/src/tune-out/tools.ts +14 -2
- package/src/types/agent.ts +2 -1
- package/src/types/framework.ts +55 -15
- package/src/types/module.ts +42 -3
- package/src/types/trace.ts +4 -3
package/dist/src/framework.js
CHANGED
|
@@ -8,6 +8,7 @@ import { appendFileSync, closeSync, fsyncSync, mkdirSync, openSync, readFileSync
|
|
|
8
8
|
import { JsStore } from '@animalabs/chronicle';
|
|
9
9
|
import { MembraneError } from '@animalabs/membrane';
|
|
10
10
|
import { classifyFailure, retryDecision, MEMBRANE_RETRY_SETTINGS } from '@animalabs/membrane/retry';
|
|
11
|
+
import { deadlineCut, isDeadlineExpiry, resolveInferenceBounds, wholeCallMs } from './inference-bounds.js';
|
|
11
12
|
import { ContextManager, PassthroughStrategy, WindowedPassthroughStrategy, OverBudgetError, UncoveredDropError, stripUnpairedToolBlocks, continuesBody, MalformedContentError, malformedContentPlaces, CompressionWithdrawn } from '@animalabs/context-manager';
|
|
12
13
|
import { SILENT_HEARTBEAT_SEPARATOR } from './silent-heartbeat.js';
|
|
13
14
|
import { SUBCONSCIOUS_TOOLS, SUBCONSCIOUS_TOOL_NAMES, defaultReaderFraming, } from './tune-out/tools.js';
|
|
@@ -18,9 +19,9 @@ import { asModuleRow, heldHandle, heldWriteId, matchesMessageQuery } from './hel
|
|
|
18
19
|
import { ProcessQueueImpl } from './queue.js';
|
|
19
20
|
import { DEFAULT_SHUTDOWN_TIMEOUT_MS, validateShutdownTimeout, shutdownErrorMessage, waitForShutdown, ShutdownTimeoutError } from './shutdown.js';
|
|
20
21
|
import { REFUSAL_REACTIONS, REFUSAL_REACTION_FALLBACK, REFUSAL_REACTION_BASELINE } from './refusal-reactions.js';
|
|
21
|
-
import { Agent, assertAgentSwitches, dryCompileStatsOf } from './agent.js';
|
|
22
|
+
import { Agent, assertAgentSwitches, assertThinkingBinding, dryCompileStatsOf } from './agent.js';
|
|
22
23
|
import { assertBooleanSetting, holdsSettings, kindOf } from './config-checks.js';
|
|
23
|
-
import { ModuleRegistry, checkedToolResult, ensureStateRegistered, storableToolResult, thrownResult } from './module-registry.js';
|
|
24
|
+
import { ModuleRegistry, checkedToolResult, droppedToolResultLine, ensureStateRegistered, retiredPushLine, storableToolResult, thrownResult } from './module-registry.js';
|
|
24
25
|
import { McplServerRegistry } from './mcpl/server-registry.js';
|
|
25
26
|
import { prefixesMeet, prefixesMeetError } from './mcpl/prefix-rule.js';
|
|
26
27
|
import { logDetail, logValue } from './mcpl/log-value.js';
|
|
@@ -627,6 +628,13 @@ export const BUILTIN_TOOLSET_TOOLS = new Set([
|
|
|
627
628
|
'heartbeat--heartbeat_trigger',
|
|
628
629
|
'workspace--status',
|
|
629
630
|
]);
|
|
631
|
+
function agentsOwnVerbRefusal(toolName) {
|
|
632
|
+
return {
|
|
633
|
+
success: false,
|
|
634
|
+
error: `${toolName} is the agent's own, which a module's call can't make for it, so nothing was done.`,
|
|
635
|
+
isError: true,
|
|
636
|
+
};
|
|
637
|
+
}
|
|
630
638
|
const readExtensionSettings = (ext, agentName) => {
|
|
631
639
|
const values = ext.get(agentName);
|
|
632
640
|
return Object.fromEntries(Object.entries(values).map(([key, value]) => [key, value === undefined ? 'undefined' : JSON.stringify(value)]));
|
|
@@ -669,7 +677,7 @@ import { EventGate, MCPL_GATE_EVENT_TYPES, checkToolInput, formatShadowWarning,
|
|
|
669
677
|
import { ReplyJudge } from './gate/reply-judge.js';
|
|
670
678
|
import { HEARTBEAT_CHANGING_TOOLS, Heartbeat, isHeartbeatMcplServer } from './heartbeat/heartbeat.js';
|
|
671
679
|
import { EPHEMERAL_USAGE_NAME, UsageTracker, persistedUsageProblem } from './usage/usage-tracker.js';
|
|
672
|
-
import {
|
|
680
|
+
import { McplRequestError, McplUnreapedLaunchError, stderrTailOf, toolsetDeclarationsOf, withStderr } from './mcpl/server-connection.js';
|
|
673
681
|
import { ToolsetHub, TOOLSETS_MODULE, META_TOOL_NAMES } from './mcpl/toolset-host.js';
|
|
674
682
|
import { HostPresentation, BUILTIN_TOOLSET } from './host-presentation.js';
|
|
675
683
|
import { isEmptyChange } from './mcpl/tool-presence.js';
|
|
@@ -677,7 +685,7 @@ import { ToolsetsModule, resolveAct, withLookHint } from './mcpl/toolsets-module
|
|
|
677
685
|
import { ModernMcpConnection } from './mcpl/modern-connection.js';
|
|
678
686
|
import { MAX_TIMER_MS, WITHHELD_TEXT, asText, credentialRedactor, redactUrl, redactServerText, resolveServerBinding, serverConfigProblems, serverCredentialPatterns, serverTextRedactor } from './mcpl/protocol-family.js';
|
|
679
687
|
import { MALFORMED_CONTENT_NOTICE, normalizeStandardToolResult } from './mcpl/tool-result-normalize.js';
|
|
680
|
-
import { boundedForLog, describeCause } from './mcpl/errors.js';
|
|
688
|
+
import { boundedForLog, describeCause, hostStoppedWhileHandling } from './mcpl/errors.js';
|
|
681
689
|
import { assertServerConfigBooleans, buildWebSocketUrl, isWebSocketTransport } from './mcpl/transport.js';
|
|
682
690
|
import { formatZonedDateTime, resolveTimeZone } from './timezone.js';
|
|
683
691
|
import { CLOCK_ROW_KIND, clockStampOf, planClock, takesClockBefore } from './clock-rows.js';
|
|
@@ -826,8 +834,8 @@ function resumesLogicalTurn(reason) {
|
|
|
826
834
|
*/
|
|
827
835
|
const TURN_RETRY = Symbol('turn-retry');
|
|
828
836
|
/**
|
|
829
|
-
* The batch a start
|
|
830
|
-
*
|
|
837
|
+
* The batch a start was merged from, when the scheduler merged more than
|
|
838
|
+
* one request into it and any of them isn't the turn's: every
|
|
831
839
|
* request it took in, as each was. Until the start takes its turn token it
|
|
832
840
|
* stands for them all. A scheduler pass that takes it back puts the batch in
|
|
833
841
|
* its place, each request with its own identity and flags; a quiesce at the
|
|
@@ -847,14 +855,10 @@ function isRearmed(request) {
|
|
|
847
855
|
function waitAlertsOnItsOwn(wait) {
|
|
848
856
|
return wait.until === null || wait.until - wait.setAt > PROVIDER_WAIT_ALERT_MS;
|
|
849
857
|
}
|
|
850
|
-
// membrane 0.8.0 marks a deadline it reports as run out with this registered
|
|
851
|
-
// symbol (on a sent request, or the Anthropic SDK's timeout, which covers its
|
|
852
|
-
// connect timeout too), and doesn't export its reader.
|
|
853
|
-
const MEMBRANE_DEADLINE_EXPIRY = Symbol.for('membrane.deadlineExpiry');
|
|
854
858
|
function isTransientFailure(error) {
|
|
855
859
|
if (!isMembraneError(error) || error.retryable !== true)
|
|
856
860
|
return false;
|
|
857
|
-
if (error
|
|
861
|
+
if (isDeadlineExpiry(error))
|
|
858
862
|
return false;
|
|
859
863
|
return error.type === 'server' || error.type === 'network' || error.type === 'timeout' || error.type === 'rate_limit';
|
|
860
864
|
}
|
|
@@ -949,6 +953,29 @@ function freshTurnRouting() {
|
|
|
949
953
|
addressedElsewhere: new Set(),
|
|
950
954
|
};
|
|
951
955
|
}
|
|
956
|
+
const TURN_INPUTS_SCAN = 64;
|
|
957
|
+
function turnInputs(cm, floor, agentName, sees) {
|
|
958
|
+
let first;
|
|
959
|
+
let last;
|
|
960
|
+
scan: for (let end = cm.getMessageCount(); end > 0; end -= TURN_INPUTS_SCAN) {
|
|
961
|
+
const { messages } = cm.getMessageWindow(Math.max(0, end - TURN_INPUTS_SCAN), Math.min(TURN_INPUTS_SCAN, end), { resolveBlobs: false });
|
|
962
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
963
|
+
const message = messages[i];
|
|
964
|
+
if (message.sequence <= floor)
|
|
965
|
+
break scan;
|
|
966
|
+
if (!sees(message.metadata))
|
|
967
|
+
continue;
|
|
968
|
+
if (message.participant === agentName)
|
|
969
|
+
break scan;
|
|
970
|
+
last ??= message.id;
|
|
971
|
+
const answers = Array.isArray(message.content) && message.content.length > 0
|
|
972
|
+
&& message.content.every((block) => block.type === 'tool_result');
|
|
973
|
+
if (!answers)
|
|
974
|
+
first = message.id;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
return first !== undefined && last !== undefined ? { first, last } : undefined;
|
|
978
|
+
}
|
|
952
979
|
const sameReach = (a, b) => {
|
|
953
980
|
if (!a || !b)
|
|
954
981
|
return a === b;
|
|
@@ -1026,10 +1053,6 @@ const LIVE_SPEECH_UNRECEIPTED = `Plain speech its rounds routed live was handed
|
|
|
1026
1053
|
* had closed admission (endFailureAtStop). Its inference:failed came first,
|
|
1027
1054
|
* and this says no retry follows it. */
|
|
1028
1055
|
const RETRY_CANCELLED_BY_STOP = 'a retry waiting while the framework was stopping is dropped before it sends anything';
|
|
1029
|
-
/** The reason on the inference:aborted that ends a turn whose start waited to
|
|
1030
|
-
* retry a failed compile across a stop() and a start(): the retry would
|
|
1031
|
-
* revive the earlier lifecycle's turn in the next one. */
|
|
1032
|
-
const RETRY_ACROSS_RESTART = 'a retry waiting while the framework was stopped and started again is dropped before it sends anything';
|
|
1033
1056
|
/** The reason on the inference:aborted that ends a turn whose continuation
|
|
1034
1057
|
* was dropped before it started, other than by an abandon or a stop. */
|
|
1035
1058
|
const CONTINUATION_DROPPED = "a turn's continuation was dropped before it started";
|
|
@@ -1075,6 +1098,24 @@ const MAX_TURN_CHECKPOINTS = 20;
|
|
|
1075
1098
|
const DEFAULT_DISCORD_AWARENESS_DEADLINE_MS = 10_000;
|
|
1076
1099
|
const MIN_DISCORD_AWARENESS_DEADLINE_MS = 50;
|
|
1077
1100
|
const MAX_DISCORD_AWARENESS_DEADLINE_MS = 60_000;
|
|
1101
|
+
/** Whether a synthesized tool's nullable field is in use: omission, null and
|
|
1102
|
+
* the empty string all mean it isn't, so a caller whose provider presents
|
|
1103
|
+
* every property as required can still leave one unset. Any other value is
|
|
1104
|
+
* in use, and its field checks it. */
|
|
1105
|
+
function fieldInUse(value) {
|
|
1106
|
+
return value !== undefined && value !== null && value !== '';
|
|
1107
|
+
}
|
|
1108
|
+
/** A synthesized tool's integer field: an integer number, or a string of ASCII
|
|
1109
|
+
* digits once trimmed (a model may write an integer as a string), within the
|
|
1110
|
+
* integers a number holds exactly; NaN for anything else. Number() would read
|
|
1111
|
+
* " ", false and [] as 0, and "2.0" or "+2" as 2, and a longer run of digits
|
|
1112
|
+
* as a different, rounded number. */
|
|
1113
|
+
function integerField(value) {
|
|
1114
|
+
const n = typeof value === 'number' ? value
|
|
1115
|
+
: typeof value === 'string' && /^[0-9]+$/.test(value.trim()) ? Number(value.trim())
|
|
1116
|
+
: Number.NaN;
|
|
1117
|
+
return Number.isSafeInteger(n) ? n : Number.NaN;
|
|
1118
|
+
}
|
|
1078
1119
|
function normalizeDiscordAwarenessDeadline(value) {
|
|
1079
1120
|
if (value === undefined || !Number.isFinite(value) || value <= 0) {
|
|
1080
1121
|
return DEFAULT_DISCORD_AWARENESS_DEADLINE_MS;
|
|
@@ -1084,9 +1125,10 @@ function normalizeDiscordAwarenessDeadline(value) {
|
|
|
1084
1125
|
/**
|
|
1085
1126
|
* Refuse a config whose `subconscious`, `watchdog` or `processLogging` is
|
|
1086
1127
|
* anything but an object, null or false (holdsSettings), or whose switch in
|
|
1087
|
-
* one holds anything but true or false (assertBooleanSetting)
|
|
1088
|
-
*
|
|
1089
|
-
*
|
|
1128
|
+
* one holds anything but true or false (assertBooleanSetting), or whose
|
|
1129
|
+
* subconscious's `thinkingBinding` assertThinkingBinding refuses, whichever
|
|
1130
|
+
* its reader. A subconscious that isn't on has its `allowChannelSpeech` and
|
|
1131
|
+
* `thinkingBinding` left unchecked, since nothing reads them then.
|
|
1090
1132
|
*/
|
|
1091
1133
|
function assertFrameworkSwitches(config) {
|
|
1092
1134
|
if (holdsSettings('subconscious', config.subconscious, '{ enabled: true }')) {
|
|
@@ -1094,6 +1136,7 @@ function assertFrameworkSwitches(config) {
|
|
|
1094
1136
|
'and false or leaving it out doesn\'t');
|
|
1095
1137
|
if (config.subconscious.enabled === true) {
|
|
1096
1138
|
assertBooleanSetting('subconscious.allowChannelSpeech', config.subconscious.allowChannelSpeech, 'true lets the subconscious speak in channels with speak_in_channel, and false or leaving it out refuses it');
|
|
1139
|
+
assertThinkingBinding('subconscious.thinkingBinding', config.subconscious.thinkingBinding);
|
|
1097
1140
|
}
|
|
1098
1141
|
}
|
|
1099
1142
|
if (holdsSettings('watchdog', config.watchdog, '{ enabled: true }')) {
|
|
@@ -1454,9 +1497,15 @@ function hostCommandRequester(serverId, params) {
|
|
|
1454
1497
|
* schedule, jittered, 10 s doubling to 5 min (by default 13 tries, where
|
|
1455
1498
|
* membrane's settings have 7: about 30 min).
|
|
1456
1499
|
* - rate limited (429): the base schedule with at least 5 tries.
|
|
1457
|
-
* -
|
|
1458
|
-
*
|
|
1459
|
-
*
|
|
1500
|
+
* - a deadline expiry, which membrane leaves to its caller: a progress
|
|
1501
|
+
* bound's cut (FrameworkConfig.inferenceBounds), the adapter's own
|
|
1502
|
+
* watchdogs, the SDK's request timeout or a runtime's fetch timeout. The
|
|
1503
|
+
* deadline schedule: by default 2 tries, 1 s apart, so one fresh call. For
|
|
1504
|
+
* one, the framework's `attempt` counts that call's consecutive expiries,
|
|
1505
|
+
* not the turn's other failures, so each call a deadline cuts gets its
|
|
1506
|
+
* fresh call. Settings with no deadline schedule give it their base one.
|
|
1507
|
+
* - any other retryable failure: the base schedule (by default 3 tries, 1 s
|
|
1508
|
+
* then 2 s).
|
|
1460
1509
|
* A provider's stated wait (`retryAfterMs`) is waited when it is longer than
|
|
1461
1510
|
* the backoff, up to the schedule's ceiling, which by default is at least the
|
|
1462
1511
|
* framework's own cap (PROVIDER_ACCELERATION_MAX_COOLDOWN_MS, ten minutes);
|
|
@@ -1476,6 +1525,10 @@ export class DefaultErrorPolicy {
|
|
|
1476
1525
|
this.maxRetries = Math.max(0, ...Object.values(settings).map((s) => (s?.maxAttempts ?? 1) - 1));
|
|
1477
1526
|
}
|
|
1478
1527
|
onInferenceError(error, _agentName, attempt) {
|
|
1528
|
+
if (isDeadlineExpiry(error)) {
|
|
1529
|
+
const decision = retryDecision(this.settings.deadline ? 'deadline' : 'retryable', attempt + 1, undefined, this.settings);
|
|
1530
|
+
return decision.retry ? { retry: true, delayMs: decision.delayMs } : { retry: false };
|
|
1531
|
+
}
|
|
1479
1532
|
if (!isMembraneError(error)) {
|
|
1480
1533
|
// Not a provider failure membrane classified: retried as any other
|
|
1481
1534
|
// retryable failure, on the base schedule.
|
|
@@ -1485,9 +1538,7 @@ export class DefaultErrorPolicy {
|
|
|
1485
1538
|
if (error.retryable === false)
|
|
1486
1539
|
return { retry: false };
|
|
1487
1540
|
// The facts membrane classifies by, read off the error itself (from either
|
|
1488
|
-
// copy of membrane, as isMembraneError takes one).
|
|
1489
|
-
// stream deadline is a retryable timeout here, as it was before membrane
|
|
1490
|
-
// 0.7, where membrane itself gives it no retry.
|
|
1541
|
+
// copy of membrane, as isMembraneError takes one).
|
|
1491
1542
|
const facts = error;
|
|
1492
1543
|
// A failure that doesn't say is retried as an unknown one is; only false refuses (handled above).
|
|
1493
1544
|
const cls = classifyFailure({ retryable: facts.retryable !== false, type: facts.type, httpStatus: facts.httpStatus, message: facts.message });
|
|
@@ -1516,18 +1567,37 @@ const MAINTENANCE_TICKS_PER_PASS = 8;
|
|
|
1516
1567
|
* together, and then on awareness drains still running, such as its
|
|
1517
1568
|
* startup's delivery. */
|
|
1518
1569
|
const FAILED_CREATE_TEARDOWN_TIMEOUT_MS = 30_000;
|
|
1570
|
+
const ABANDON_GRACE_MS = 10_000;
|
|
1571
|
+
const testAbandonGraces = new WeakMap();
|
|
1572
|
+
/**
|
|
1573
|
+
* For tests only: `framework`'s abandon takes `ms` as its grace in place of
|
|
1574
|
+
* {@link ABANDON_GRACE_MS}. Set it only where the test holds every turn the
|
|
1575
|
+
* abandon reaches past the grace, so no turn can end within it and a short
|
|
1576
|
+
* grace only brings the abandon's report sooner.
|
|
1577
|
+
*/
|
|
1578
|
+
export function setAbandonGraceForTests(framework, ms) {
|
|
1579
|
+
testAbandonGraces.set(framework, ms);
|
|
1580
|
+
}
|
|
1519
1581
|
const PROVIDER_ACCELERATION_DEFAULT_COOLDOWN_MS = 65_000;
|
|
1520
1582
|
/** Cap on waits the framework chooses itself. A provider's stated wait is
|
|
1521
1583
|
* never capped: it is a lower bound (see ProviderWaits). It is also the
|
|
1522
1584
|
* longest stated wait an ephemeral run sleeps in its turn: a longer one ends
|
|
1523
1585
|
* the run instead (nonOwningProviderWait). */
|
|
1524
1586
|
const PROVIDER_ACCELERATION_MAX_COOLDOWN_MS = 10 * 60_000;
|
|
1587
|
+
const FAILURE_ADVICE = 'If this recurs with the same cause, change approach rather than retrying identically ' +
|
|
1588
|
+
'(e.g. drop an oversized attachment or an unsupported setting).';
|
|
1589
|
+
const TIMEOUT_ADVICE = 'If a reply is still owed from that turn, you can give it now.';
|
|
1590
|
+
const MADE_AGAIN = 'The call was made again, and it timed out again.';
|
|
1525
1591
|
const OVERLOADED_TURN_TRIES = 13;
|
|
1526
|
-
const
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1592
|
+
const DEADLINE_TURN_TRIES = 2;
|
|
1593
|
+
const TURN_RETRY_SETTINGS = {
|
|
1594
|
+
...Object.fromEntries(Object.entries(MEMBRANE_RETRY_SETTINGS).map(([cls, s]) => [cls, s && {
|
|
1595
|
+
...s,
|
|
1596
|
+
...(cls === 'overloaded' ? { maxAttempts: OVERLOADED_TURN_TRIES } : {}),
|
|
1597
|
+
statedWaitCeilingMs: s.statedWaitCeilingMs === null ? null : Math.max(s.statedWaitCeilingMs, PROVIDER_ACCELERATION_MAX_COOLDOWN_MS),
|
|
1598
|
+
}])),
|
|
1599
|
+
deadline: { maxAttempts: DEADLINE_TURN_TRIES, firstDelayMs: 1_000, multiplier: 1, maxDelayMs: 1_000, statedWaitCeilingMs: null },
|
|
1600
|
+
};
|
|
1531
1601
|
const PROVIDER_ACCELERATION_JITTER_MS = 5_000;
|
|
1532
1602
|
/** Longest delay one setTimeout can represent (2^31-1 ms, about 24.8 days);
|
|
1533
1603
|
* a longer timer fires almost at once, so longer waits re-arm in steps. */
|
|
@@ -1577,6 +1647,11 @@ async function* turnStreamEvents(stream, agentName) {
|
|
|
1577
1647
|
console.error(`[stream] ${agentName}: the stream sent an event (${event.type}) after its ${decidedBy} event, which decided its turn; it and the rest of the stream decide nothing`);
|
|
1578
1648
|
return;
|
|
1579
1649
|
}
|
|
1650
|
+
if (event.type === 'aborted' && event.reason === 'timeout') {
|
|
1651
|
+
decidedBy = 'error';
|
|
1652
|
+
yield { type: 'error', error: deadlineCut(event.message) };
|
|
1653
|
+
continue;
|
|
1654
|
+
}
|
|
1580
1655
|
if (event.type === 'complete' || event.type === 'error' || event.type === 'aborted')
|
|
1581
1656
|
decidedBy = event.type;
|
|
1582
1657
|
yield event;
|
|
@@ -1822,6 +1897,30 @@ const isLowSurrogate = (text, index) => {
|
|
|
1822
1897
|
};
|
|
1823
1898
|
/** Whether a cut before `index` would separate the halves of a surrogate pair. */
|
|
1824
1899
|
const splitsPair = (text, index) => index > 0 && index < text.length && isHighSurrogate(text, index - 1) && isLowSurrogate(text, index);
|
|
1900
|
+
/**
|
|
1901
|
+
* Whether an MCPL server's child ended the way a server ends when it's asked
|
|
1902
|
+
* to: exit 0, death by a signal that asks a process to end (SIGTERM, SIGINT,
|
|
1903
|
+
* SIGHUP), or a shell's report of such a death by its convention, 128 plus the
|
|
1904
|
+
* signal's number (143, 130, 129). A service manager's stop signals the host
|
|
1905
|
+
* and its children together, so a child can go before the host's stop()
|
|
1906
|
+
* closes it: its loss is that stop, not a failure, and raises no mcpl-down.
|
|
1907
|
+
* A child the stdio transport ended itself, after a pipe error or a line on
|
|
1908
|
+
* its stdout longer than the bound, was ended by that, whatever its code or
|
|
1909
|
+
* signal. A WebSocket's close codes are 1000 and up, so they never match.
|
|
1910
|
+
*/
|
|
1911
|
+
function endedAsAsked(ended) {
|
|
1912
|
+
if (ended.pipeFailed)
|
|
1913
|
+
return false;
|
|
1914
|
+
const { code, signal } = ended;
|
|
1915
|
+
return code === 0 || code === 143 || code === 130 || code === 129 ||
|
|
1916
|
+
signal === 'SIGTERM' || signal === 'SIGINT' || signal === 'SIGHUP';
|
|
1917
|
+
}
|
|
1918
|
+
/** A lost connection's words for an alert: the transport's reason, then its
|
|
1919
|
+
* exit or close code and signal, each when there is one. */
|
|
1920
|
+
function closeWords(reason, code, signal) {
|
|
1921
|
+
const how = [reason ?? null, code != null ? `code ${code}` : null, signal ? `signal ${signal}` : null].filter((part) => part !== null);
|
|
1922
|
+
return how.length > 0 ? ` (${how.join(', ')})` : '';
|
|
1923
|
+
}
|
|
1825
1924
|
/**
|
|
1826
1925
|
* `text` when it fits within `max` characters; otherwise its head and tail
|
|
1827
1926
|
* around a marker stating how many of how many characters were omitted, the
|
|
@@ -2067,6 +2166,16 @@ export function shedMayHelp(errorType, httpStatus) {
|
|
|
2067
2166
|
return errorType === 'invalid_request' || errorType === 'context_length';
|
|
2068
2167
|
return errorType === 'invalid_request' && (httpStatus === undefined || httpStatus === 400 || httpStatus === 422);
|
|
2069
2168
|
}
|
|
2169
|
+
function responseParts(response) {
|
|
2170
|
+
return [
|
|
2171
|
+
response?.addMessages?.length ? `${response.addMessages.length} message(s) to add` : '',
|
|
2172
|
+
response?.editMessages?.length ? `${response.editMessages.length} edit(s)` : '',
|
|
2173
|
+
response?.removeMessages?.length ? `${response.removeMessages.length} removal(s)` : '',
|
|
2174
|
+
response?.stateUpdate !== undefined ? 'a state update' : '',
|
|
2175
|
+
response?.requestInference ? 'an inference request' : '',
|
|
2176
|
+
response?.toolsChanged ? 'a tools change' : '',
|
|
2177
|
+
].filter(Boolean).join(', ');
|
|
2178
|
+
}
|
|
2070
2179
|
/**
|
|
2071
2180
|
* What a thrown value says, safely and briefly, as boundedErrorMessage gives
|
|
2072
2181
|
* it, with `recorded` applied before the cut, so no part of a credential
|
|
@@ -2168,6 +2277,12 @@ function wakeLineKey(line) {
|
|
|
2168
2277
|
return JSON.stringify([eventType, serverId ?? null, channelId ?? null, messageId ?? null, eventId ?? null]);
|
|
2169
2278
|
}
|
|
2170
2279
|
const SUBCONSCIOUS_MODEL_FIELDS = ['physicalWindowTokens', 'thinkingBinding'];
|
|
2280
|
+
function readerModelFields(cfg) {
|
|
2281
|
+
return {
|
|
2282
|
+
...(cfg.physicalWindowTokens != null ? { physicalWindowTokens: cfg.physicalWindowTokens } : {}),
|
|
2283
|
+
...(holdsSettings('subconscious.thinkingBinding', cfg.thinkingBinding, '{ binds: true, dropBlock: true }') ? { thinkingBinding: cfg.thinkingBinding } : {}),
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2171
2286
|
const JUDGE_PARAMETER = {
|
|
2172
2287
|
type: 'object',
|
|
2173
2288
|
description: 'Ask the reply judge first: batch matching events like debounceMs, then a small, cheap model reads your ' +
|
|
@@ -2233,9 +2348,10 @@ export class AgentFramework {
|
|
|
2233
2348
|
store;
|
|
2234
2349
|
ownsStore;
|
|
2235
2350
|
stopPromise = null;
|
|
2236
|
-
|
|
2351
|
+
// Nothing sets it back to 'none': a stop ends its instance (#315).
|
|
2352
|
+
stopState = 'none';
|
|
2353
|
+
closedStopFailure;
|
|
2237
2354
|
ownedStoreClosed = false;
|
|
2238
|
-
ownedStoreManagersClosed = false;
|
|
2239
2355
|
/** Whether the latest stop() got through its teardown with nothing failed:
|
|
2240
2356
|
* every step and every wait up to and including the one on awareness
|
|
2241
2357
|
* drains, whatever failed after that, in the storage work that finishes
|
|
@@ -2251,8 +2367,8 @@ export class AgentFramework {
|
|
|
2251
2367
|
* promise: a stream it cancelled or an event loop still in a call; and a
|
|
2252
2368
|
* stream whose cancel threw, which stop() doesn't wait for. A later
|
|
2253
2369
|
* stop() waits for each again, beside what it finds, whatever has happened
|
|
2254
|
-
* since to the entry it was found under (a run's end removes its stream's,
|
|
2255
|
-
*
|
|
2370
|
+
* since to the entry it was found under (a run's end removes its stream's),
|
|
2371
|
+
* so neither a budget nor a cancel that
|
|
2256
2372
|
* threw lets the store close under work still running. Each leaves once it
|
|
2257
2373
|
* settles. */
|
|
2258
2374
|
leftRunning = new Map();
|
|
@@ -2318,6 +2434,9 @@ export class AgentFramework {
|
|
|
2318
2434
|
providerAccelerationLastRecovery = new Map();
|
|
2319
2435
|
providerAccelerationDefaultCooldownMs = PROVIDER_ACCELERATION_DEFAULT_COOLDOWN_MS;
|
|
2320
2436
|
providerAccelerationJitterMs = PROVIDER_ACCELERATION_JITTER_MS;
|
|
2437
|
+
inferenceBounds = resolveInferenceBounds(undefined);
|
|
2438
|
+
deadlineCuts = new Map();
|
|
2439
|
+
freshCallsCut = new Set();
|
|
2321
2440
|
providerHoldHook;
|
|
2322
2441
|
/** Provider waits (stated retry-after) per (agent, model), kept across restarts. */
|
|
2323
2442
|
providerWaits;
|
|
@@ -2325,8 +2444,6 @@ export class AgentFramework {
|
|
|
2325
2444
|
retryWaitWakers = new Set();
|
|
2326
2445
|
waitingTraceEveryMs = WAITING_TRACE_EVERY_MS;
|
|
2327
2446
|
providerAdmissionClosed = false;
|
|
2328
|
-
/** Invalidates framework turns still assembling when stop begins. */
|
|
2329
|
-
lifecycleGeneration = 0;
|
|
2330
2447
|
/** Last time we reported stale (busy-requeued) inference requests, per agent. */
|
|
2331
2448
|
staleWarnAt = new Map();
|
|
2332
2449
|
/** Per-agent last inference activity (epoch ms), for /healthz + doctor tooling. */
|
|
@@ -2467,6 +2584,7 @@ export class AgentFramework {
|
|
|
2467
2584
|
turnRearmMs = DEFAULT_TURN_REARM_MS;
|
|
2468
2585
|
turnRearms = new Map();
|
|
2469
2586
|
exhaustingRearms = new Set();
|
|
2587
|
+
rearmDeadlineCuts = new Set();
|
|
2470
2588
|
statedWaitRuns = new Map();
|
|
2471
2589
|
/** N consecutive failed inferences ⇒ the agent is treated as hard-down and
|
|
2472
2590
|
* escalated loudly to stderr. */
|
|
@@ -2999,6 +3117,7 @@ export class AgentFramework {
|
|
|
2999
3117
|
getMessage: (id) => this.getMessage(id),
|
|
3000
3118
|
queryMessages: (filter, opts) => this.queryMessages(filter, opts),
|
|
3001
3119
|
pushEvent: (event, moduleName) => this.pushModuleEvent(event, moduleName, true),
|
|
3120
|
+
dropRetiredPush: (event, moduleName) => this.dropRetiredModuleEvent(event, moduleName),
|
|
3002
3121
|
onTrace: (listener) => this.onTrace(listener),
|
|
3003
3122
|
// The ONLY entry that confers module origin — a private closure the
|
|
3004
3123
|
// framework hands to its own module registry. The public
|
|
@@ -3022,6 +3141,7 @@ export class AgentFramework {
|
|
|
3022
3141
|
// Before anything opens or starts: a refused config must leave nothing behind.
|
|
3023
3142
|
if (config.codeExecution != null)
|
|
3024
3143
|
validateCodeExecutionConfig(config.codeExecution);
|
|
3144
|
+
resolveInferenceBounds(config.inferenceBounds);
|
|
3025
3145
|
assertFrameworkSwitches(config);
|
|
3026
3146
|
for (const agentConfig of config.agents)
|
|
3027
3147
|
assertAgentSwitches(agentConfig);
|
|
@@ -3190,6 +3310,7 @@ export class AgentFramework {
|
|
|
3190
3310
|
const framework = building.framework = new AgentFramework(store, ownsStore, config.membrane, config.inferencePolicy ?? new DefaultInferencePolicy(), config.errorPolicy ?? new DefaultErrorPolicy(), config.syncIntervalMs ?? DEFAULT_SYNC_INTERVAL_MS, config.maintenanceIntervalMs ?? DEFAULT_MAINTENANCE_INTERVAL_MS, processLoggingPersist, resolveTimeZone(config.timeZone), discordAwarenessOutbox, config.discordAwarenessEmoji ?? DEFAULT_DISCORD_AWARENESS_EMOJI, normalizeDiscordAwarenessDeadline(config.discordAwarenessDeadlineMs), new OperatorLog(operatorLogPath));
|
|
3191
3311
|
framework.providerHoldHook = config.providerHold;
|
|
3192
3312
|
framework.turnRearmMs = turnRearmDelays(config.exhaustedTurnRearmMs);
|
|
3313
|
+
framework.inferenceBounds = resolveInferenceBounds(config.inferenceBounds);
|
|
3193
3314
|
// Before any delivery.
|
|
3194
3315
|
await framework.reconcileDiscordAwarenessAtStartup();
|
|
3195
3316
|
// Restore persisted usage data (if any) from prior session. The names it
|
|
@@ -3671,9 +3792,7 @@ export class AgentFramework {
|
|
|
3671
3792
|
* maintenance, event loop and operator operations, so a dump shows what
|
|
3672
3793
|
* those waits are on, and before its modules and MCPL servers stop, so a
|
|
3673
3794
|
* stop() whose teardown hangs, fails or times out doesn't keep it listed.
|
|
3674
|
-
* With none in the set, one line says no framework is running.
|
|
3675
|
-
* started again after a completed stop() doesn't rejoin; whether that
|
|
3676
|
-
* lifecycle is supported is #315.
|
|
3795
|
+
* With none in the set, one line says no framework is running.
|
|
3677
3796
|
*
|
|
3678
3797
|
* One listener serves the process, installed by the first create() to
|
|
3679
3798
|
* reach the join and kept: once a process's last SIGUSR2 listener is
|
|
@@ -3717,35 +3836,38 @@ export class AgentFramework {
|
|
|
3717
3836
|
}
|
|
3718
3837
|
/**
|
|
3719
3838
|
* Start the event loop. A start() on a running framework does nothing.
|
|
3720
|
-
*
|
|
3721
|
-
*
|
|
3722
|
-
*
|
|
3723
|
-
*
|
|
3839
|
+
* A stop ends its instance (#315): once a stop() is admitted, start() and
|
|
3840
|
+
* addModule() are refused with an error, for good. While that stop() runs,
|
|
3841
|
+
* the error says the framework is stopping. Once it has settled, the error
|
|
3842
|
+
* says what to do: create a new AgentFramework on the store, and after a
|
|
3843
|
+
* failed stop(), call stop() again first, until one resolves. When a
|
|
3844
|
+
* failed stop() left its owned store closed, so that it can't be tried
|
|
3845
|
+
* again, the error carries that failure as its cause.
|
|
3846
|
+
*
|
|
3847
|
+
* A new instance on the store keeps what the store holds. With the
|
|
3848
|
+
* heartbeat on and the same heartbeat state file (heartbeat.statePath, or
|
|
3849
|
+
* its default), it keeps the heartbeat's schedule as it reads it from that
|
|
3850
|
+
* file: a tick or a reminder that fell due meanwhile fires once, shortly
|
|
3851
|
+
* after it starts. What an instance holds only in memory is lost with it,
|
|
3852
|
+
* as on a process restart: an agent's sleep and its wake, a self-wake, the
|
|
3853
|
+
* inference requests still queued (a budget restart, a heartbeat's wake, a
|
|
3854
|
+
* nudge), a re-armed turn's wait and count, and the redo stack. #627 is
|
|
3855
|
+
* about keeping a sleep and a self-wake across a restart. To pause and
|
|
3856
|
+
* resume one framework in place, use quiesce() and resume().
|
|
3724
3857
|
*/
|
|
3725
3858
|
start() {
|
|
3726
3859
|
// Before the running check: stop() claims its attempt before it clears
|
|
3727
3860
|
// running, so a start() made in its first steps, such as from a trace
|
|
3728
3861
|
// listener, is refused too.
|
|
3729
|
-
this.
|
|
3862
|
+
this.refuseOnceStopped('start()');
|
|
3730
3863
|
if (this.running) {
|
|
3731
3864
|
return;
|
|
3732
3865
|
}
|
|
3733
3866
|
this.running = true;
|
|
3734
|
-
this.providerAdmissionClosed = false;
|
|
3735
3867
|
this.earlyTraces = null;
|
|
3736
|
-
// A new lifecycle: a turn compiled before an earlier stop() never drives,
|
|
3737
|
-
// and withheld spills, closed for the generation a stop ended, are open
|
|
3738
|
-
// for this one.
|
|
3739
|
-
this.lifecycleGeneration++;
|
|
3740
|
-
// Its servers' addresses, which an earlier stop() took out of the
|
|
3741
|
-
// reference registry, for its stubs.
|
|
3742
3868
|
for (const config of this.mcplServerConfigs.values()) {
|
|
3743
3869
|
referenceRegistry.setServerAddress(config.id, serverAddressOf(config), this);
|
|
3744
3870
|
}
|
|
3745
|
-
// A sleep kept through a stop() wakes this lifecycle at its end, or now
|
|
3746
|
-
// if it ended while stopped. Before the loop's first pass, so that a wake
|
|
3747
|
-
// owed now joins the requests already pending in one turn.
|
|
3748
|
-
this.eventGate?.resumeSleep();
|
|
3749
3871
|
this.heartbeat?.start();
|
|
3750
3872
|
this.loopPromise = this.runLoop();
|
|
3751
3873
|
// Start periodic sync timer (if enabled)
|
|
@@ -3770,11 +3892,14 @@ export class AgentFramework {
|
|
|
3770
3892
|
}
|
|
3771
3893
|
}
|
|
3772
3894
|
/**
|
|
3773
|
-
* Stop the event loop
|
|
3774
|
-
*
|
|
3775
|
-
*
|
|
3776
|
-
*
|
|
3777
|
-
*
|
|
3895
|
+
* Stop the event loop, and with it this framework, for good: once a stop()
|
|
3896
|
+
* is admitted, start() and addModule() are refused, as start() says. A
|
|
3897
|
+
* stop() whose budget is refused, which it rejects with a RangeError, is
|
|
3898
|
+
* not admitted and changes nothing. Each step of the teardown runs
|
|
3899
|
+
* whatever an earlier one did: one that throws, such as a host stream's
|
|
3900
|
+
* cancel(), fails the stop() once the rest has run. When a step or a wait
|
|
3901
|
+
* up to and including the wait on awareness drains fails or times out,
|
|
3902
|
+
* storage stays open and
|
|
3778
3903
|
* stop() can be called again; one that fails after it, in the storage work
|
|
3779
3904
|
* that finishes the stop, fails the stop() without keeping storage open.
|
|
3780
3905
|
* A timed-out wait lets teardown go on while what it waited for may still
|
|
@@ -3782,7 +3907,10 @@ export class AgentFramework {
|
|
|
3782
3907
|
* @param teardownTimeoutMs Optional budget for each of stop()'s waits, in
|
|
3783
3908
|
* turn: on the streams it cancels, on maintenance in flight (a pass or an
|
|
3784
3909
|
* over-budget drain kick), on the event loop, on an operator's operations
|
|
3785
|
-
* already running, on modules and MCPL
|
|
3910
|
+
* already running, on modules and MCPL (its servers and the inference
|
|
3911
|
+
* requests they made) together, where a module whose start() is still
|
|
3912
|
+
* running counts until that start() has settled and its stop() returned,
|
|
3913
|
+
* then on Discord awareness
|
|
3786
3914
|
* drains, which send nothing once stop() has begun, recording the call the
|
|
3787
3915
|
* close ended. With a budget, a stop() can take up to six of them, besides
|
|
3788
3916
|
* the withheld-spill deadline of a guard's write still under way. Omit to
|
|
@@ -3805,9 +3933,7 @@ export class AgentFramework {
|
|
|
3805
3933
|
* dropped, though a write the stream already had under way can still
|
|
3806
3934
|
* land in the run's namespace after them.
|
|
3807
3935
|
* - an event loop still in a call, such as a module's onProcess, finishes
|
|
3808
|
-
* that call and the rest of its iteration, and then exits.
|
|
3809
|
-
* made once that stop() has failed runs its own loop beside that last
|
|
3810
|
-
* iteration.
|
|
3936
|
+
* that call and the rest of its iteration, and then exits.
|
|
3811
3937
|
* - a later stop() waits again, within its own budget, for any stream or
|
|
3812
3938
|
* loop this one left running, so the store never closes under it.
|
|
3813
3939
|
*
|
|
@@ -3835,11 +3961,14 @@ export class AgentFramework {
|
|
|
3835
3961
|
let settle;
|
|
3836
3962
|
const attempt = new Promise((resolve, reject) => { settle = { resolve, reject }; });
|
|
3837
3963
|
this.stopPromise = attempt;
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3964
|
+
this.stopState = 'stopping';
|
|
3965
|
+
void attempt.then(() => { this.stopState = 'completed'; }, (error) => {
|
|
3966
|
+
this.stopState = 'failed';
|
|
3967
|
+
// Failed cleanup can be retried while its original store is open. A
|
|
3968
|
+
// terminal storage failure after successful close must remain a failure.
|
|
3969
|
+
if (this.ownedStoreClosed)
|
|
3970
|
+
this.closedStopFailure = { error };
|
|
3971
|
+
else if (this.stopPromise === attempt)
|
|
3843
3972
|
this.stopPromise = null;
|
|
3844
3973
|
});
|
|
3845
3974
|
// The body still starts in this call, so its synchronous steps (closing
|
|
@@ -3847,24 +3976,28 @@ export class AgentFramework {
|
|
|
3847
3976
|
this.stopOwned(teardownTimeoutMs).then(settle.resolve, settle.reject);
|
|
3848
3977
|
return attempt;
|
|
3849
3978
|
}
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3979
|
+
refuseOnceStopped(verb) {
|
|
3980
|
+
switch (this.stopState) {
|
|
3981
|
+
case 'none':
|
|
3982
|
+
return;
|
|
3983
|
+
case 'stopping':
|
|
3984
|
+
throw new Error(`${verb} refused: the framework is stopping`);
|
|
3985
|
+
case 'completed':
|
|
3986
|
+
throw new Error(`${verb} refused: this framework has stopped, and a stopped framework doesn't start again. ` +
|
|
3987
|
+
'Create a new AgentFramework on its store to go on, with its storePath if it opened the store itself.');
|
|
3988
|
+
case 'failed': {
|
|
3989
|
+
const closed = this.closedStopFailure;
|
|
3990
|
+
if (closed) {
|
|
3991
|
+
throw new Error(`${verb} refused: this framework's stop() failed and its owned store is closed, so stop() can't be tried again: ` +
|
|
3992
|
+
`it rejects with that same failure, this error's cause: ${shutdownErrorMessage(closed.error)}. ` +
|
|
3993
|
+
"A stopped framework doesn't start again, and a new AgentFramework on its store doesn't finish what that stop() left unfinished.", { cause: closed.error });
|
|
3994
|
+
}
|
|
3995
|
+
throw new Error(`${verb} refused: this framework's stop() failed, and a stopped framework doesn't start again. ` +
|
|
3996
|
+
'A failed stop() may have left its cleanup or final persistence unfinished. ' +
|
|
3997
|
+
"Call stop() again: once a stop() of this one resolves, they're done, and only then should a new " +
|
|
3998
|
+
'AgentFramework use its store, with its storePath if this one opened the store itself, ' +
|
|
3999
|
+
'or another instance use anything this one shares.');
|
|
4000
|
+
}
|
|
3868
4001
|
}
|
|
3869
4002
|
}
|
|
3870
4003
|
/** Register before invoking the body, so the set holds the operation for
|
|
@@ -3995,6 +4128,15 @@ export class AgentFramework {
|
|
|
3995
4128
|
// holding the wait for operator operations below until its handshake's
|
|
3996
4129
|
// deadline.
|
|
3997
4130
|
step(() => this.endConnectsInFlight());
|
|
4131
|
+
// Nor does a server lost before or during the stop launch or dial again
|
|
4132
|
+
// on its own: a retry already scheduled starts nothing, and none is
|
|
4133
|
+
// scheduled after.
|
|
4134
|
+
step(() => {
|
|
4135
|
+
for (const connection of this.mcplServerRegistry?.getAllServers() ?? [])
|
|
4136
|
+
connection.stopReconnecting();
|
|
4137
|
+
for (const connection of this.modernServers().values())
|
|
4138
|
+
connection.stopReconnecting();
|
|
4139
|
+
});
|
|
3998
4140
|
step(() => this.pushCoalescer?.suspend());
|
|
3999
4141
|
step(() => this.flushCoalescingSnapshot());
|
|
4000
4142
|
this.running = false;
|
|
@@ -4010,11 +4152,10 @@ export class AgentFramework {
|
|
|
4010
4152
|
this.providerAdmissionClosed = true;
|
|
4011
4153
|
for (const wake of [...this.retryWaitWakers])
|
|
4012
4154
|
step(wake);
|
|
4013
|
-
this.
|
|
4014
|
-
// The generation this stop ends; withheld spills close for it alone, so a
|
|
4015
|
-
// start() after this stop has settled reopens them for its own.
|
|
4016
|
-
const stoppingGeneration = this.lifecycleGeneration;
|
|
4155
|
+
this.guardSpillsClosed = false;
|
|
4017
4156
|
step(() => this.queue.close());
|
|
4157
|
+
let inferences = [];
|
|
4158
|
+
step(() => { inferences = this.inferenceRouter?.stopAll() ?? []; });
|
|
4018
4159
|
step(() => this.tuneOutCoordinator?.stop());
|
|
4019
4160
|
// Stop running code_execution scripts before cancelling streams: a
|
|
4020
4161
|
// zombie script must not keep firing side-effectful tool calls into a
|
|
@@ -4027,9 +4168,8 @@ export class AgentFramework {
|
|
|
4027
4168
|
// framework must not keep up to the whole ledger budget referenced.
|
|
4028
4169
|
this.toolImageLedgers.clear();
|
|
4029
4170
|
// A turn whose continuation is still on its way ends with the stop, as
|
|
4030
|
-
// a turn's start a stop reaches does: the continuation
|
|
4031
|
-
//
|
|
4032
|
-
// (openLogical, below). Before the holds and the parks go, so the
|
|
4171
|
+
// a turn's start a stop reaches does: the continuation never starts.
|
|
4172
|
+
// Before the holds and the parks go, so the
|
|
4033
4173
|
// continuation leaves them with its turn. Each drop is a step of its
|
|
4034
4174
|
// own, so one that throws leaves the next agent's turn to end all the
|
|
4035
4175
|
// same, as stop()'s other loops do.
|
|
@@ -4040,11 +4180,7 @@ export class AgentFramework {
|
|
|
4040
4180
|
});
|
|
4041
4181
|
// A stopped host must never hang behind its own cooldown.
|
|
4042
4182
|
step(() => this.cancelProviderAdmission());
|
|
4043
|
-
// An open logical turn doesn't survive a stop
|
|
4044
|
-
// on this instance opens a new one rather than continuing the old turn's
|
|
4045
|
-
// routing (openLogical). A resume still continues its turn, with its
|
|
4046
|
-
// routing state (turnRouting), since it carries that turn's own request
|
|
4047
|
-
// on: beginAgentTurn opens the turn again for it.
|
|
4183
|
+
// An open logical turn doesn't survive a stop.
|
|
4048
4184
|
this.openLogical.clear();
|
|
4049
4185
|
// Cancel the stream of every agent whose state holds one: streaming,
|
|
4050
4186
|
// waiting on its tools, or ready at a tool boundary. Membrane reports
|
|
@@ -4152,9 +4288,11 @@ export class AgentFramework {
|
|
|
4152
4288
|
// its idle watchdog. Its end stores its queue, as at any run's end; that
|
|
4153
4289
|
// stream's late events are dropped once the run has left, and a later
|
|
4154
4290
|
// stop() waits for the stream itself (leftRunning). (A start() is refused
|
|
4155
|
-
//
|
|
4291
|
+
// once a stop() is admitted, so no new lifecycle begins beside it.)
|
|
4156
4292
|
for (const [name, run] of this.ephemeralRuns)
|
|
4157
4293
|
run.settle.reject(ephemeralRunStopped(name));
|
|
4294
|
+
for (const agent of this.agents.values())
|
|
4295
|
+
step(() => { agent.unpinTurnInputs(); });
|
|
4158
4296
|
// Admission closed when stop() began. What admitted operations start now,
|
|
4159
4297
|
// such as a surgery's config materialization, joins the set, so wait until
|
|
4160
4298
|
// the set is empty. This comes after the streams and the event loop, which
|
|
@@ -4183,11 +4321,9 @@ export class AgentFramework {
|
|
|
4183
4321
|
// generation and nothing reads the old fork's window again. So each live
|
|
4184
4322
|
// fork ends here as a closing fork's end disposes it, and its release
|
|
4185
4323
|
// passes on what it left unread, as any release does. Its channel unbinds
|
|
4186
|
-
// first, as the idle sweep's does
|
|
4187
|
-
//
|
|
4188
|
-
//
|
|
4189
|
-
// binding yet, and its spawn binds nothing once it has ended
|
|
4190
|
-
// (spawnConversationAgent).
|
|
4324
|
+
// first, as the idle sweep's does. A fork whose spawn is still waiting
|
|
4325
|
+
// for its creation to return has no binding yet, and its spawn binds
|
|
4326
|
+
// nothing once it has ended (spawnConversationAgent).
|
|
4191
4327
|
// Each is a step of its own, and each fork ends whatever its stream or
|
|
4192
4328
|
// turn is doing, as each run does above: its end settles a tool-result
|
|
4193
4329
|
// batch its stream still held and removes the stream's entries
|
|
@@ -4278,16 +4414,16 @@ export class AgentFramework {
|
|
|
4278
4414
|
// workspace: its stub names the audit record. A write that was past its
|
|
4279
4415
|
// deadline isn't waited for here, but the workspace's own stop waits for
|
|
4280
4416
|
// the mount turn it holds, bounded only by teardownTimeoutMs.
|
|
4281
|
-
this.
|
|
4417
|
+
this.guardSpillsClosed = true;
|
|
4282
4418
|
// Stop modules and both engines' MCP servers in parallel
|
|
4283
|
-
const shutdownPromises = [this.moduleRegistry.stopAll(teardownTimeoutMs)];
|
|
4419
|
+
const shutdownPromises = [this.moduleRegistry.stopAll(teardownTimeoutMs), waitFor(inferences)];
|
|
4284
4420
|
if (this.mcplServerRegistry) {
|
|
4285
4421
|
shutdownPromises.push(this.mcplServerRegistry.closeAll(teardownTimeoutMs));
|
|
4286
4422
|
}
|
|
4287
4423
|
shutdownPromises.push(this.closeModernMcpConnections(teardownTimeoutMs));
|
|
4288
4424
|
// A stopped framework's servers' addresses leave the reference registry,
|
|
4289
4425
|
// so no other framework's stub reads them and the registry doesn't keep
|
|
4290
|
-
// this one reachable.
|
|
4426
|
+
// this one reachable.
|
|
4291
4427
|
step(() => { for (const id of this.mcplServerConfigs.keys())
|
|
4292
4428
|
referenceRegistry.forgetServerAddress(id, this); });
|
|
4293
4429
|
// Timed-out or failed teardown retains its context and may still write.
|
|
@@ -4346,8 +4482,8 @@ export class AgentFramework {
|
|
|
4346
4482
|
// No stream is left, unless its wait above ran out or its cancel threw:
|
|
4347
4483
|
// from the start of stop() none registers and no agent turn starts
|
|
4348
4484
|
// (providerAdmissionClosed, checked for every agent), and every stream
|
|
4349
|
-
// registered before was cancelled and awaited above. (A start()
|
|
4350
|
-
//
|
|
4485
|
+
// registered before was cancelled and awaited above. (A start() is
|
|
4486
|
+
// refused once a stop() is admitted, so admission stays closed.)
|
|
4351
4487
|
// What is still deferred (a message held for a turn's end whose flush
|
|
4352
4488
|
// found the target busy again, or one deferred behind its target's own
|
|
4353
4489
|
// state) would wait for a flush that may never come. Land each at its
|
|
@@ -4427,8 +4563,7 @@ export class AgentFramework {
|
|
|
4427
4563
|
shutdownErrors.push(error);
|
|
4428
4564
|
}
|
|
4429
4565
|
if (this.ownsStore && teardownSucceeded) {
|
|
4430
|
-
|
|
4431
|
-
this.ownedStoreManagersClosed = true;
|
|
4566
|
+
this.closeOpenManagers();
|
|
4432
4567
|
try {
|
|
4433
4568
|
this.store.close();
|
|
4434
4569
|
this.ownedStoreClosed = true;
|
|
@@ -4889,7 +5024,6 @@ export class AgentFramework {
|
|
|
4889
5024
|
const alive = [...this.openManagers].flatMap((ref) => ref.deref() ?? []);
|
|
4890
5025
|
for (const contextManager of alive)
|
|
4891
5026
|
this.closeManager(contextManager);
|
|
4892
|
-
return alive.length > 0;
|
|
4893
5027
|
}
|
|
4894
5028
|
/** The framework's membrane, with each complete() call passed through
|
|
4895
5029
|
* `admit` along with the model it names. */
|
|
@@ -4899,8 +5033,17 @@ export class AgentFramework {
|
|
|
4899
5033
|
const value = Reflect.get(obj, prop, receiver);
|
|
4900
5034
|
if (prop === 'complete' && typeof value === 'function') {
|
|
4901
5035
|
return (...args) => {
|
|
4902
|
-
const
|
|
4903
|
-
|
|
5036
|
+
const config = args[0]?.config;
|
|
5037
|
+
const model = config?.model;
|
|
5038
|
+
const options = args[1];
|
|
5039
|
+
const bounds = this.inferenceBounds;
|
|
5040
|
+
const sent = bounds !== null && options?.progressTimeoutMs === undefined
|
|
5041
|
+
? [args[0], {
|
|
5042
|
+
...options,
|
|
5043
|
+
progressTimeoutMs: wholeCallMs(bounds, typeof config?.maxTokens === 'number' ? config.maxTokens : undefined),
|
|
5044
|
+
}, ...args.slice(2)]
|
|
5045
|
+
: args;
|
|
5046
|
+
return admit(() => Reflect.apply(value, this.membrane, sent), typeof model === 'string' ? model : undefined);
|
|
4904
5047
|
};
|
|
4905
5048
|
}
|
|
4906
5049
|
return typeof value === 'function' ? value.bind(this.membrane) : value;
|
|
@@ -5038,7 +5181,7 @@ export class AgentFramework {
|
|
|
5038
5181
|
// length: an operator release of it returns the hold to AF's own pacing.
|
|
5039
5182
|
...(pacing?.stated ? { statedWait: { model: agent.model, ownUntil: now + pacing.ownMs } } : {}),
|
|
5040
5183
|
...(auxiliary && (!existing || existing.auxiliaryOrigin) ? { auxiliaryOrigin: true } : {}),
|
|
5041
|
-
...(existing?.
|
|
5184
|
+
...(existing?.emptiedBy && held.length === 0 ? { emptiedBy: existing.emptiedBy } : {}) });
|
|
5042
5185
|
if (auxiliary && !existing) {
|
|
5043
5186
|
console.error(`[provider-cooldown] agent=${agent.name} host hold (${hostHold.reason}) from an auxiliary call — ` +
|
|
5044
5187
|
`holding primary/auxiliary for ${delayMs}ms`);
|
|
@@ -5055,12 +5198,23 @@ export class AgentFramework {
|
|
|
5055
5198
|
this.noteStatedWaitRun(agent.name, wait, now);
|
|
5056
5199
|
return true;
|
|
5057
5200
|
}
|
|
5058
|
-
|
|
5201
|
+
takeAsTransient(agentName, error, trigger) {
|
|
5202
|
+
if (isTransientFailure(error)) {
|
|
5203
|
+
this.rearmDeadlineCuts.delete(agentName);
|
|
5204
|
+
return true;
|
|
5205
|
+
}
|
|
5206
|
+
if (!isRearmed(trigger) || !isDeadlineExpiry(error))
|
|
5207
|
+
return false;
|
|
5208
|
+
const first = !this.rearmDeadlineCuts.has(agentName);
|
|
5209
|
+
this.rearmDeadlineCuts.add(agentName);
|
|
5210
|
+
return first;
|
|
5211
|
+
}
|
|
5212
|
+
rearmExhaustedTurn(agent, error, trigger, ownsProviderGate, routing, asTransient) {
|
|
5059
5213
|
if (!ownsProviderGate || trigger === undefined || this.providerAdmissionClosed || this.turnRearmMs.length === 0)
|
|
5060
5214
|
return;
|
|
5061
5215
|
const done = this.turnRearms.get(agent.name) ?? 0;
|
|
5062
5216
|
const notSent = (why) => console.error(`[inference-rearm] agent=${agent.name} turn not re-armed: ${why}`);
|
|
5063
|
-
if (!
|
|
5217
|
+
if (!asTransient) {
|
|
5064
5218
|
if (done > 0)
|
|
5065
5219
|
notSent(`its failure isn't transient (${isMembraneError(error) ? error.type : boundFailureText(error.message, FAILURE_SUMMARY_MAX_CHARS)})`);
|
|
5066
5220
|
return;
|
|
@@ -5207,12 +5361,15 @@ export class AgentFramework {
|
|
|
5207
5361
|
? `no longer holds primary turns: the primary model is now ${model} (the wait stays recorded)`
|
|
5208
5362
|
: `primary hold ended after ${Date.now() - cooldown.startedAt}ms`));
|
|
5209
5363
|
}
|
|
5210
|
-
if ((cooldown.auxiliaryOrigin || cooldown.
|
|
5364
|
+
if ((cooldown.auxiliaryOrigin || cooldown.emptiedBy) && cooldown.heldRequests.length === 0) {
|
|
5211
5365
|
// Nobody asked for an inference while parked, or what was held was a
|
|
5212
|
-
// continuation since dropped with its turn
|
|
5213
|
-
// primary the hold left pending won't run, so
|
|
5214
|
-
|
|
5215
|
-
|
|
5366
|
+
// continuation since dropped with its turn, or wakes since withdrawn:
|
|
5367
|
+
// just reopen the gate. A primary the hold left pending won't run, so
|
|
5368
|
+
// its recovery ends.
|
|
5369
|
+
const which = cooldown.emptiedBy === 'drop' ? 'hold, its held continuation dropped,'
|
|
5370
|
+
: cooldown.emptiedBy === 'withdrawal' ? 'hold, its held wakes withdrawn,' : 'auxiliary hold';
|
|
5371
|
+
console.error(`[provider-cooldown] agent=${agentName} ${which} released after ${Date.now() - cooldown.startedAt}ms`);
|
|
5372
|
+
if (cooldown.emptiedBy)
|
|
5216
5373
|
this.endPendingPrimary(agentName);
|
|
5217
5374
|
this.flushAuxiliaryAdmission(agentName);
|
|
5218
5375
|
return;
|
|
@@ -5862,12 +6019,7 @@ export class AgentFramework {
|
|
|
5862
6019
|
*/
|
|
5863
6020
|
pushEvent(event) {
|
|
5864
6021
|
if (event.type === 'tool-result' && this.queue?.isClosed) {
|
|
5865
|
-
|
|
5866
|
-
// may run the tool again): which tool, and whether it succeeded.
|
|
5867
|
-
const outcome = event.result.success && !event.result.isError
|
|
5868
|
-
? 'succeeded'
|
|
5869
|
-
: `failed${event.result.error ? `: ${thrownText(event.result.error)}` : ''}`;
|
|
5870
|
-
console.error(`[framework] ${event.agentName}: result of tool call ${event.callId} (${event.toolName ? `${event.toolName}, ` : ''}${event.moduleName}) arrived after stop — dropped; the tool ${outcome}`);
|
|
6022
|
+
console.error(droppedToolResultLine(event, 'after stop'));
|
|
5871
6023
|
return;
|
|
5872
6024
|
}
|
|
5873
6025
|
this.queue.push(event);
|
|
@@ -5897,6 +6049,13 @@ export class AgentFramework {
|
|
|
5897
6049
|
else
|
|
5898
6050
|
this.queue.push(event);
|
|
5899
6051
|
}
|
|
6052
|
+
dropRetiredModuleEvent(event, moduleName) {
|
|
6053
|
+
if (this.queue.isClosed) {
|
|
6054
|
+
this.pushModuleEvent(event, moduleName, true);
|
|
6055
|
+
return;
|
|
6056
|
+
}
|
|
6057
|
+
console.error(retiredPushLine(event, moduleName));
|
|
6058
|
+
}
|
|
5900
6059
|
/**
|
|
5901
6060
|
* Add a trace event listener for observability.
|
|
5902
6061
|
*/
|
|
@@ -5951,12 +6110,17 @@ export class AgentFramework {
|
|
|
5951
6110
|
this.traceListeners.splice(index, 1);
|
|
5952
6111
|
}
|
|
5953
6112
|
/**
|
|
5954
|
-
* Add a module at runtime. Refused, with a rejection,
|
|
5955
|
-
*
|
|
5956
|
-
*
|
|
5957
|
-
* past it
|
|
6113
|
+
* Add a module at runtime. Refused, with a rejection, once a stop() has
|
|
6114
|
+
* been admitted, before anything else is checked: while that stop() runs,
|
|
6115
|
+
* a module added then would either be stopped by it or, once it has taken
|
|
6116
|
+
* its list of modules to stop, stay started past it; once it has settled,
|
|
6117
|
+
* the framework has stopped for good, as start() says. A stop() admitted
|
|
6118
|
+
* while the module's start() runs stops the module once that start() has
|
|
6119
|
+
* settled; if that start() fulfils, this rejects the same way then, with
|
|
6120
|
+
* no `module:added`, and if it rejects, this rejects with its error.
|
|
5958
6121
|
*/
|
|
5959
6122
|
async addModule(module) {
|
|
6123
|
+
this.refuseOnceStopped('addModule()');
|
|
5960
6124
|
// A server's tool prefix and a module's name share one namespace, as
|
|
5961
6125
|
// create() checks at startup: a module isn't added under a prefix a
|
|
5962
6126
|
// server holds, or one a restart is moving its server to.
|
|
@@ -5974,8 +6138,8 @@ export class AgentFramework {
|
|
|
5974
6138
|
throw new Error(`Module "${module.name}" collides with MCPL server "${id}"'s toolPrefix "${module.name}" (a closed tab)`);
|
|
5975
6139
|
}
|
|
5976
6140
|
}
|
|
5977
|
-
this.resetCompletedStop('addModule()');
|
|
5978
6141
|
await this.moduleRegistry.addModule(module);
|
|
6142
|
+
this.refuseOnceStopped('addModule()');
|
|
5979
6143
|
// The workspace judges whether an agent's history holds a message as the
|
|
5980
6144
|
// framework judges its own notices, for its head-move notice
|
|
5981
6145
|
// (noticeHeadMoves) and the writes it weighs against it. A module taken
|
|
@@ -5988,7 +6152,9 @@ export class AgentFramework {
|
|
|
5988
6152
|
this.emitTrace({ type: 'module:added', moduleName: module.name });
|
|
5989
6153
|
}
|
|
5990
6154
|
/**
|
|
5991
|
-
* Remove a module at runtime.
|
|
6155
|
+
* Remove a module at runtime. A module whose start() is still running is
|
|
6156
|
+
* stopped once that start() has settled; this waits for that and for its
|
|
6157
|
+
* stop(), with no time limit.
|
|
5992
6158
|
*/
|
|
5993
6159
|
async removeModule(name) {
|
|
5994
6160
|
await this.moduleRegistry.removeModule(name);
|
|
@@ -6297,7 +6463,7 @@ export class AgentFramework {
|
|
|
6297
6463
|
: []),
|
|
6298
6464
|
];
|
|
6299
6465
|
}
|
|
6300
|
-
/** The registered workspace module, if any (used by
|
|
6466
|
+
/** The registered workspace module, if any (used by save_recent_image). */
|
|
6301
6467
|
getWorkspaceModule() {
|
|
6302
6468
|
const module = this.moduleRegistry.getAllModules().find((m) => m.name === 'workspace');
|
|
6303
6469
|
return module && typeof module.writeBinary === 'function'
|
|
@@ -6770,15 +6936,11 @@ export class AgentFramework {
|
|
|
6770
6936
|
stallSkips = 0;
|
|
6771
6937
|
/** See WITHHELD_SPILL_DEADLINE_MS; an instance field so tests can shorten it. */
|
|
6772
6938
|
withheldSpillDeadlineMs = WITHHELD_SPILL_DEADLINE_MS;
|
|
6773
|
-
/**
|
|
6774
|
-
*
|
|
6775
|
-
* that is stopping, and its stub names the audit
|
|
6776
|
-
*
|
|
6777
|
-
|
|
6778
|
-
* is still open (start() doesn't restore stopped modules or MCPL
|
|
6779
|
-
* connections, and refuses an owned store that stop() closed). A start()
|
|
6780
|
-
* made while a stop() is still running is refused. */
|
|
6781
|
-
guardSpillsClosedFor = null;
|
|
6939
|
+
/** Set once a stop() has let the running annotations finish, and cleared
|
|
6940
|
+
* as each stop() begins: while set, a withheld original is no longer
|
|
6941
|
+
* written to a workspace that is stopping, and its stub names the audit
|
|
6942
|
+
* record. */
|
|
6943
|
+
guardSpillsClosed = false;
|
|
6782
6944
|
/** A new agent's guard: its persisted setting, the tool-results spill its
|
|
6783
6945
|
* withheld originals are written to, and the set stop() waits on
|
|
6784
6946
|
* (agent-framework #277). The setting is read from framework/state's
|
|
@@ -7453,7 +7615,7 @@ export class AgentFramework {
|
|
|
7453
7615
|
// Bounded grace for cancelled streams. Stop cancels streams too; a turn
|
|
7454
7616
|
// with no stream settles on its own. Report the abandon that already ran,
|
|
7455
7617
|
// even if stop cuts its grace short.
|
|
7456
|
-
const grace = Date.now() +
|
|
7618
|
+
const grace = Date.now() + (testAbandonGraces.get(this) ?? ABANDON_GRACE_MS);
|
|
7457
7619
|
while (this.quiesced && !this.queue?.isClosed && this.activeTurnTokens.size > 0 && Date.now() < grace) {
|
|
7458
7620
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
7459
7621
|
}
|
|
@@ -7876,8 +8038,9 @@ export class AgentFramework {
|
|
|
7876
8038
|
* memory-only: they land within the turn, at a tool boundary or at its
|
|
7877
8039
|
* end, and a crash before then loses them. That window runs to the turn's
|
|
7878
8040
|
* end for a message held there and what is queued behind it, and, on a
|
|
7879
|
-
* stream that
|
|
7880
|
-
* prefill path
|
|
8041
|
+
* stream that doesn't declare it carries injected messages (membrane's XML
|
|
8042
|
+
* prefill path declares it can't), for every message deferred during the
|
|
8043
|
+
* turn.
|
|
7881
8044
|
*
|
|
7882
8045
|
* A write that fails, or a queue over the cap, which writes nothing,
|
|
7883
8046
|
* leaves the record as it last held it, except that store only, a write
|
|
@@ -7903,14 +8066,15 @@ export class AgentFramework {
|
|
|
7903
8066
|
catch (err) {
|
|
7904
8067
|
console.error('[host-mode] failed to persist deferred context writes:', err);
|
|
7905
8068
|
this.deferredWritesFellBehind({ error: err });
|
|
7906
|
-
return;
|
|
8069
|
+
return false;
|
|
7907
8070
|
}
|
|
7908
8071
|
if (outcome === 'over-cap') {
|
|
7909
8072
|
this.deferredWritesFellBehind('over-cap');
|
|
8073
|
+
return false;
|
|
7910
8074
|
}
|
|
7911
|
-
|
|
8075
|
+
if (outcome !== 'nothing')
|
|
7912
8076
|
this.deferredWritesCaughtUp();
|
|
7913
|
-
|
|
8077
|
+
return true;
|
|
7914
8078
|
}
|
|
7915
8079
|
/**
|
|
7916
8080
|
* Write the queue to its record as it stands, when the host is quiesced
|
|
@@ -9204,8 +9368,9 @@ export class AgentFramework {
|
|
|
9204
9368
|
* **Inheritance.** `mode: 'shared'` (the default) gives the child the
|
|
9205
9369
|
* parent's messages and fold state through shared storage: nothing is
|
|
9206
9370
|
* copied, the child folds with the parent's memory tree, and neither can
|
|
9207
|
-
* alter what the other reads. It needs
|
|
9208
|
-
*
|
|
9371
|
+
* alter what the other reads. It needs a store that supports derivation
|
|
9372
|
+
* (`ContextManager.supportsDerivation`), and is refused, not silently
|
|
9373
|
+
* downgraded, without one. `mode: 'copy'`
|
|
9209
9374
|
* is the historical mechanism — the parent's compiled context re-added as
|
|
9210
9375
|
* the child's own messages — and works everywhere.
|
|
9211
9376
|
*
|
|
@@ -9756,6 +9921,7 @@ export class AgentFramework {
|
|
|
9756
9921
|
if (run) {
|
|
9757
9922
|
run.endReason ??= 'stopped';
|
|
9758
9923
|
run.endedBy ??= options.by;
|
|
9924
|
+
run.endedFromOutside = true;
|
|
9759
9925
|
// The awaiting caller is told who stopped the job and why, whether or
|
|
9760
9926
|
// not a stream was in flight: settled first, so the aborted stream's own
|
|
9761
9927
|
// settle ('Stream cancelled') finds the run already settled. The caller's
|
|
@@ -10106,6 +10272,7 @@ export class AgentFramework {
|
|
|
10106
10272
|
if (run) {
|
|
10107
10273
|
run.endReason = reason;
|
|
10108
10274
|
run.endedBy = record.ended?.by;
|
|
10275
|
+
run.endedFromOutside = true;
|
|
10109
10276
|
run.settle.reject(new AgentStoppedError(name, reason, record.ended?.by, detail));
|
|
10110
10277
|
return;
|
|
10111
10278
|
}
|
|
@@ -10114,6 +10281,15 @@ export class AgentFramework {
|
|
|
10114
10281
|
return;
|
|
10115
10282
|
this.releaseAgentRuntime(agent, this.channelOfHome(record.homeChannel), undefined, record.lifetime.kind === 'persistent');
|
|
10116
10283
|
}
|
|
10284
|
+
forgetFailureAccounting(name) {
|
|
10285
|
+
this.consecutiveInferenceFailures.delete(name);
|
|
10286
|
+
this.exhaustionRewinds.delete(name);
|
|
10287
|
+
this.refusalStreak.delete(name);
|
|
10288
|
+
this.refusalRewinds.delete(name);
|
|
10289
|
+
this.rewindEpisode.delete(name);
|
|
10290
|
+
if (this.forcedRewind.delete(name))
|
|
10291
|
+
console.error(`[unstick] agent=${name} its /unstick session ended with its agent`);
|
|
10292
|
+
}
|
|
10117
10293
|
/**
|
|
10118
10294
|
* Everything the process holds for a live, non-ephemeral agent that has
|
|
10119
10295
|
* ended, released once, on every path an agent ends by — stopAgent, the
|
|
@@ -10159,6 +10335,9 @@ export class AgentFramework {
|
|
|
10159
10335
|
this.eventGate?.agentGone(name);
|
|
10160
10336
|
this.agents.delete(name);
|
|
10161
10337
|
this.agentConfigs.delete(name);
|
|
10338
|
+
this.deadlineCuts.delete(name);
|
|
10339
|
+
this.freshCallsCut.delete(name);
|
|
10340
|
+
this.forgetFailureAccounting(name);
|
|
10162
10341
|
this.thinkingLossIncidents.forget(name);
|
|
10163
10342
|
this.reasoningLoss.forget(name);
|
|
10164
10343
|
// main's manager release: the context manager an ephemeral run or a fork
|
|
@@ -10201,6 +10380,7 @@ export class AgentFramework {
|
|
|
10201
10380
|
// waits ends here as at a turn's end, alerted if it ran long
|
|
10202
10381
|
// (endStatedWaitRun).
|
|
10203
10382
|
this.turnRearms.delete(name);
|
|
10383
|
+
this.rearmDeadlineCuts.delete(name);
|
|
10204
10384
|
this.exhaustingRearms.delete(name);
|
|
10205
10385
|
this.exhaustedTurns.delete(name);
|
|
10206
10386
|
this.endStatedWaitRun(name);
|
|
@@ -10593,13 +10773,12 @@ export class AgentFramework {
|
|
|
10593
10773
|
}
|
|
10594
10774
|
/**
|
|
10595
10775
|
* A start dropped before its stream sends anything (its agent was replaced,
|
|
10596
|
-
* or stop() closed admission
|
|
10597
|
-
* ends
|
|
10598
|
-
*
|
|
10599
|
-
*
|
|
10600
|
-
*
|
|
10601
|
-
*
|
|
10602
|
-
* other turn open.
|
|
10776
|
+
* or stop() closed admission) ends the logical turn it was running. stop()
|
|
10777
|
+
* ends every open turn (openLogical), but a fresh start that awaited its
|
|
10778
|
+
* batches' assembly across a stop() reaches its routing after it and opens
|
|
10779
|
+
* its turn. That turn would then stay open, and the next start of the
|
|
10780
|
+
* agent's name would continue the dropped turn's routing. The identity
|
|
10781
|
+
* check leaves any other turn open.
|
|
10603
10782
|
*/
|
|
10604
10783
|
endDroppedTurn(agentName, routing) {
|
|
10605
10784
|
if (this.openLogical.get(agentName) === routing)
|
|
@@ -10610,8 +10789,36 @@ export class AgentFramework {
|
|
|
10610
10789
|
// Its scripts take it as a settle's end of the turn: the start sent
|
|
10611
10790
|
// nothing, so none of its own calls is waiting on a script.
|
|
10612
10791
|
this.settledTurns.add(routing);
|
|
10792
|
+
this.releaseTurnInputs(routing);
|
|
10613
10793
|
this.codeExecution?.scripts.turnEnded(agentName, { id: routing.turnId, live: this.liveTurns(agentName, routing) }, false);
|
|
10614
10794
|
}
|
|
10795
|
+
pinTurnInputs(agent, routing) {
|
|
10796
|
+
let range;
|
|
10797
|
+
try {
|
|
10798
|
+
const cm = agent.getContextManager();
|
|
10799
|
+
const watermark = agent.getConsumedWatermark();
|
|
10800
|
+
if (watermark && watermark.branch === cm.currentBranch().name) {
|
|
10801
|
+
range = turnInputs(cm, watermark.sequence, agent.name, (metadata) => this.agentSees(agent, metadata));
|
|
10802
|
+
}
|
|
10803
|
+
}
|
|
10804
|
+
catch (error) {
|
|
10805
|
+
console.error(`[turn-inputs] ${agent.name}: could not read the turn's inputs, so none is pinned:`, error);
|
|
10806
|
+
}
|
|
10807
|
+
if (!range) {
|
|
10808
|
+
agent.unpinTurnInputs();
|
|
10809
|
+
return;
|
|
10810
|
+
}
|
|
10811
|
+
const id = agent.pinTurnInputs(range.first, range.last);
|
|
10812
|
+
if (id !== undefined)
|
|
10813
|
+
routing.inputsPin = { agent, id };
|
|
10814
|
+
}
|
|
10815
|
+
releaseTurnInputs(routing) {
|
|
10816
|
+
const pin = routing.inputsPin;
|
|
10817
|
+
if (!pin)
|
|
10818
|
+
return;
|
|
10819
|
+
routing.inputsPin = undefined;
|
|
10820
|
+
pin.agent.unpinTurnInputs(pin.id);
|
|
10821
|
+
}
|
|
10615
10822
|
/** A logical turn's end, for every agent: it closes the agent's open
|
|
10616
10823
|
* logical turn when it is the settling stream's own (openLogical), and
|
|
10617
10824
|
* settles an active ephemeral run. The run's tool-call count comes from
|
|
@@ -10641,8 +10848,10 @@ export class AgentFramework {
|
|
|
10641
10848
|
// or of the stream that now holds the agent, are left alone.
|
|
10642
10849
|
if (stream) {
|
|
10643
10850
|
// Ended, unless it's the logical turn this settle left open.
|
|
10644
|
-
if (this.openLogical.get(agentName) !== stream)
|
|
10851
|
+
if (this.openLogical.get(agentName) !== stream) {
|
|
10645
10852
|
this.settledTurns.add(stream);
|
|
10853
|
+
this.releaseTurnInputs(stream);
|
|
10854
|
+
}
|
|
10646
10855
|
this.codeExecution?.scripts.turnEnded(agentName, { id: stream.turnId, live: this.liveTurns(agentName, stream) }, outcome.stopReason === 'exhausted', outcome.error);
|
|
10647
10856
|
}
|
|
10648
10857
|
const run = this.ephemeralRuns.get(agentName);
|
|
@@ -10897,6 +11106,10 @@ export class AgentFramework {
|
|
|
10897
11106
|
// deregistered, so disposal must release EventGate liveness itself.
|
|
10898
11107
|
this.eventGate?.onInferenceEnded(agent.name);
|
|
10899
11108
|
this.agents.delete(agent.name);
|
|
11109
|
+
this.deadlineCuts.delete(agent.name);
|
|
11110
|
+
this.freshCallsCut.delete(agent.name);
|
|
11111
|
+
if (run.endedFromOutside)
|
|
11112
|
+
this.forgetFailureAccounting(agent.name);
|
|
10900
11113
|
this.thinkingLossIncidents.forget(agent.name);
|
|
10901
11114
|
this.reasoningLoss.forget(agent.name);
|
|
10902
11115
|
// Its pending self-wake ends with it, as a fork's does.
|
|
@@ -11063,6 +11276,14 @@ export class AgentFramework {
|
|
|
11063
11276
|
inputSchema: { type: 'object' },
|
|
11064
11277
|
},
|
|
11065
11278
|
];
|
|
11279
|
+
static WAKE_ADD_RULE_FIELDS = {
|
|
11280
|
+
'behavior.debounce': 'debounceMs',
|
|
11281
|
+
'behavior.rate_limit': 'rateLimit',
|
|
11282
|
+
'behavior.passive_sample': 'passiveSample',
|
|
11283
|
+
'behavior.judge': 'judge',
|
|
11284
|
+
'position.before': 'insertBefore',
|
|
11285
|
+
'position.after': 'insertAfter',
|
|
11286
|
+
};
|
|
11066
11287
|
/** Synthesized wake-rule tools: add/remove a gate.json policy at runtime.
|
|
11067
11288
|
* Present when a gate is wired. They write validated policies into the
|
|
11068
11289
|
* hot-reloaded gate.json (same validation as load), so a rule takes effect
|
|
@@ -11244,9 +11465,9 @@ export class AgentFramework {
|
|
|
11244
11465
|
/** Synthesized drafts tool — present whenever MCPL channels are
|
|
11245
11466
|
* configured. A resident's held drafts (src/prose-drafts.ts): plain speech
|
|
11246
11467
|
* that was not sent, never published except by its resident's resend, and
|
|
11247
|
-
* kept as that module's retention says.
|
|
11248
|
-
*
|
|
11249
|
-
* every property as required can still say "not in use". */
|
|
11468
|
+
* kept as that module's retention says. Its optional fields are nullable,
|
|
11469
|
+
* as save_recent_image's selectors are: a caller whose provider presents
|
|
11470
|
+
* every property as required can still say "not in use" (fieldInUse). */
|
|
11250
11471
|
static DRAFTS_TOOL = {
|
|
11251
11472
|
name: 'drafts',
|
|
11252
11473
|
description: 'Your held drafts: plain speech of yours that was NOT sent — held back because an explicit send in its turn ' +
|
|
@@ -11285,10 +11506,10 @@ export class AgentFramework {
|
|
|
11285
11506
|
required: ['action'],
|
|
11286
11507
|
},
|
|
11287
11508
|
};
|
|
11288
|
-
/** Synthesized
|
|
11509
|
+
/** Synthesized save_recent_image tool — present when a workspace module is
|
|
11289
11510
|
* registered. Lets the agent persist an image it has already seen in its
|
|
11290
11511
|
* own context (Discord attachments arrive inlined as base64 and are
|
|
11291
|
-
* otherwise unreachable as files). */
|
|
11512
|
+
* otherwise unreachable as files). Its three selectors are nullable. */
|
|
11292
11513
|
static SAVE_IMAGE_TOOL = {
|
|
11293
11514
|
name: 'save_recent_image',
|
|
11294
11515
|
description: 'Save one or more recent images from your own context to workspace files. ' +
|
|
@@ -11303,7 +11524,8 @@ export class AgentFramework {
|
|
|
11303
11524
|
'restart) the call fails and writes nothing — it never substitutes an older ' +
|
|
11304
11525
|
'image. When other tool calls in the same batch are still running, the save ' +
|
|
11305
11526
|
'waits for them so their images are counted. Receipts carry source tool call, MIME, byte size and SHA-256. Saved ' +
|
|
11306
|
-
'files are visible via workspace tools.'
|
|
11527
|
+
'files are visible via workspace tools. Select by `ref` or by ' +
|
|
11528
|
+
'`index`/`count`; a selector you are not using may be omitted or passed as null.',
|
|
11307
11529
|
inputSchema: {
|
|
11308
11530
|
type: 'object',
|
|
11309
11531
|
properties: {
|
|
@@ -11311,18 +11533,24 @@ export class AgentFramework {
|
|
|
11311
11533
|
type: 'string',
|
|
11312
11534
|
description: 'Mount-prefixed destination path, e.g. "project/photos/name.png".',
|
|
11313
11535
|
},
|
|
11536
|
+
// Nullable selectors: a caller whose provider presents every property
|
|
11537
|
+
// as required must still be able to say "not this selector". Omission,
|
|
11538
|
+
// null and "" all mean it isn't used (fieldInUse); any other value is
|
|
11539
|
+
// checked. The descriptions name null, the form to ask for.
|
|
11314
11540
|
index: {
|
|
11315
|
-
type: '
|
|
11316
|
-
description: 'Which image, counting back from the most recent (0 = most recent). Default 0.'
|
|
11541
|
+
type: ['integer', 'null'],
|
|
11542
|
+
description: 'Which image, counting back from the most recent (0 = most recent). Default 0. ' +
|
|
11543
|
+
'null, like omitting it, means the default.',
|
|
11317
11544
|
},
|
|
11318
11545
|
count: {
|
|
11319
|
-
type: '
|
|
11320
|
-
description: 'How many images to save, starting at `index` and going further back. Default 1.'
|
|
11546
|
+
type: ['integer', 'null'],
|
|
11547
|
+
description: 'How many images to save, starting at `index` and going further back. Default 1. ' +
|
|
11548
|
+
'null, like omitting it, means the default.',
|
|
11321
11549
|
},
|
|
11322
11550
|
ref: {
|
|
11323
|
-
type: 'string',
|
|
11551
|
+
type: ['string', 'null'],
|
|
11324
11552
|
description: 'Save a specific tool-result image by its history ref (e.g. "img_k7x3q2_7", as shown in its placeholder in your context) instead of by position. ' +
|
|
11325
|
-
'Mutually exclusive with `index`/`count
|
|
11553
|
+
'Mutually exclusive with `index`/`count`: pass null for the selector you are not using. null, like omitting it, means no ref.',
|
|
11326
11554
|
},
|
|
11327
11555
|
},
|
|
11328
11556
|
required: ['path'],
|
|
@@ -13797,8 +14025,9 @@ export class AgentFramework {
|
|
|
13797
14025
|
// idiom): reasoning effort (thinking), provider params, token ceilings,
|
|
13798
14026
|
// caching, refusal handling, timezone — a same-model side-process must
|
|
13799
14027
|
// not silently run under different inference conditions than the
|
|
13800
|
-
// resident it serves.
|
|
13801
|
-
// overridden
|
|
14028
|
+
// resident it serves. Below, identity- and surface-defining fields are
|
|
14029
|
+
// overridden, and the window and thinking binding where the
|
|
14030
|
+
// subconscious's config sets its own.
|
|
13802
14031
|
const model = cfg.model ?? primaryConfig.model;
|
|
13803
14032
|
const inherited = { ...primaryConfig };
|
|
13804
14033
|
if (model !== primaryConfig.model) {
|
|
@@ -13807,6 +14036,7 @@ export class AgentFramework {
|
|
|
13807
14036
|
}
|
|
13808
14037
|
const agentConfig = {
|
|
13809
14038
|
...inherited,
|
|
14039
|
+
...readerModelFields(cfg),
|
|
13810
14040
|
name,
|
|
13811
14041
|
model,
|
|
13812
14042
|
systemPrompt: cfg.systemPrompt,
|
|
@@ -13906,6 +14136,7 @@ export class AgentFramework {
|
|
|
13906
14136
|
config: {
|
|
13907
14137
|
model: cfg.model,
|
|
13908
14138
|
proseRouting: 'disabled',
|
|
14139
|
+
...readerModelFields(cfg),
|
|
13909
14140
|
},
|
|
13910
14141
|
inheritRefusals: true,
|
|
13911
14142
|
onParentEnd: 'end',
|
|
@@ -14019,13 +14250,7 @@ export class AgentFramework {
|
|
|
14019
14250
|
this.addMessage(reader, content, metadata);
|
|
14020
14251
|
}
|
|
14021
14252
|
async runLoop() {
|
|
14022
|
-
|
|
14023
|
-
// this loop was in a call (a module's onProcess) returns before the call
|
|
14024
|
-
// does, and a start() after it begins a lifecycle with a loop of its own;
|
|
14025
|
-
// this one finishes its iteration and exits, rather than running beside
|
|
14026
|
-
// that loop for good.
|
|
14027
|
-
const lifecycle = this.lifecycleGeneration;
|
|
14028
|
-
while (this.running && this.lifecycleGeneration === lifecycle) {
|
|
14253
|
+
while (this.running) {
|
|
14029
14254
|
try {
|
|
14030
14255
|
await this.processNextEvent();
|
|
14031
14256
|
}
|
|
@@ -14287,18 +14512,21 @@ export class AgentFramework {
|
|
|
14287
14512
|
// compiled prefix and the live request still match without it
|
|
14288
14513
|
// (#232 gap 5).
|
|
14289
14514
|
//
|
|
14290
|
-
//
|
|
14291
|
-
// (supportsInjectedMessages
|
|
14292
|
-
//
|
|
14293
|
-
//
|
|
14294
|
-
//
|
|
14295
|
-
//
|
|
14515
|
+
// Only a stream that declares it carries injected messages
|
|
14516
|
+
// (supportsInjectedMessages true: membrane's native path) gets
|
|
14517
|
+
// them. Membrane's XML prefill path declares false, and a stream
|
|
14518
|
+
// that doesn't say is taken as one that doesn't, so nothing is
|
|
14519
|
+
// drained for either: everything queued for the agent waits for
|
|
14520
|
+
// the turn-end flush, as a held message does, rather than sitting
|
|
14521
|
+
// in the window between rounds the stream never showed it. A
|
|
14522
|
+
// boundary with no stream, the direct API's, drains: its
|
|
14523
|
+
// re-inference compiles from the store.
|
|
14296
14524
|
const midTurnInjections = [];
|
|
14297
14525
|
// What this boundary stored for the agent to hear, in store order,
|
|
14298
14526
|
// for a boundary that leaves it to the turn's next start as
|
|
14299
14527
|
// arrivals: that start's compile sends it.
|
|
14300
14528
|
const heard = [];
|
|
14301
|
-
if (currentState.supportsInjectedMessages
|
|
14529
|
+
if (currentState.supportsInjectedMessages === true || !currentState.stream) {
|
|
14302
14530
|
// A clock row due (storeDueClock) goes right after the round's
|
|
14303
14531
|
// results, ahead of what is drained below, and joins the stream
|
|
14304
14532
|
// the same way. A boundary that passed during the round can't
|
|
@@ -14397,12 +14625,10 @@ export class AgentFramework {
|
|
|
14397
14625
|
}
|
|
14398
14626
|
// A live stream handed these messages after the results follows
|
|
14399
14627
|
// them in its next round, not the tool loop of a reply to
|
|
14400
|
-
// [Continue]: that round's rows carry no prompt.
|
|
14401
|
-
//
|
|
14402
|
-
//
|
|
14403
|
-
//
|
|
14404
|
-
// restarts the stream or falls back to a new start below hands
|
|
14405
|
-
// them to no live stream, and a new start reads its own prompt from
|
|
14628
|
+
// [Continue]: that round's rows carry no prompt. A boundary that
|
|
14629
|
+
// ends the turn, restarts the stream or falls back to a new start
|
|
14630
|
+
// below hands them to no live stream, and a new start reads its own
|
|
14631
|
+
// prompt from
|
|
14406
14632
|
// what it sends. (The routing notice below rides only an injection
|
|
14407
14633
|
// already here.)
|
|
14408
14634
|
if (midTurnInjections.length > 0) {
|
|
@@ -14645,11 +14871,10 @@ export class AgentFramework {
|
|
|
14645
14871
|
}
|
|
14646
14872
|
// Streaming path: convert results and resume the stream.
|
|
14647
14873
|
// Mid-turn messages collected above ride along as injected user
|
|
14648
|
-
// messages. Membrane's native path
|
|
14649
|
-
//
|
|
14650
|
-
//
|
|
14651
|
-
//
|
|
14652
|
-
// collected for it. Each goes as its participant and content
|
|
14874
|
+
// messages. Membrane's native path appends them after the
|
|
14875
|
+
// tool_result envelope, so the next round of THIS turn hears
|
|
14876
|
+
// them. Its XML prefill path declares it can't carry them, so
|
|
14877
|
+
// none are collected for it. Each goes as its participant and content
|
|
14653
14878
|
// alone: a stored row's metadata is the host's record, not input
|
|
14654
14879
|
// for membrane. Each goes with its name and text as the store
|
|
14655
14880
|
// keeps them, whether it's a heard message, rendered or not, or a
|
|
@@ -14679,15 +14904,11 @@ export class AgentFramework {
|
|
|
14679
14904
|
// the watermark of the turn's compile and read as unread, so a
|
|
14680
14905
|
// coalesced replacement could remove a message the agent heard
|
|
14681
14906
|
// and answered (and that its later replies were minted after).
|
|
14682
|
-
// Only a stream that declares it carries them
|
|
14683
|
-
// (supportsInjectedMessages true: membrane's native
|
|
14684
|
-
//
|
|
14685
|
-
//
|
|
14686
|
-
//
|
|
14687
|
-
// saw it. So without the declaration they stay unread, as before
|
|
14688
|
-
// this mark.
|
|
14689
|
-
// The shown mark moves at every boundary that injects, declared
|
|
14690
|
-
// or not, since there a doubt counts as shown.
|
|
14907
|
+
// Only a stream that declares it carries them gets them, and so
|
|
14908
|
+
// is marked read (supportsInjectedMessages true: membrane's native
|
|
14909
|
+
// path).
|
|
14910
|
+
// The shown mark moves at every boundary that injects into its
|
|
14911
|
+
// stream.
|
|
14691
14912
|
// The mark is the store's head, a scalar, as a compile's is: it
|
|
14692
14913
|
// also covers what was stored since the compile without being
|
|
14693
14914
|
// carried, such as another slot's messages, and a write that
|
|
@@ -14699,8 +14920,7 @@ export class AgentFramework {
|
|
|
14699
14920
|
// replies after it go without their thinking, whatever the mark
|
|
14700
14921
|
// says; deferring those writers is the fix.
|
|
14701
14922
|
if (midTurnInjections.length > 0) {
|
|
14702
|
-
//
|
|
14703
|
-
// mark (CoalescerHost.isUnshown) counts a doubt as shown.
|
|
14923
|
+
// Shown: the stream carries them (CoalescerHost.isUnshown).
|
|
14704
14924
|
agent.markContextShown();
|
|
14705
14925
|
if (currentState.supportsInjectedMessages === true)
|
|
14706
14926
|
agent.markContextConsumed();
|
|
@@ -14749,15 +14969,23 @@ export class AgentFramework {
|
|
|
14749
14969
|
async dispatchProcessEventToModules(event, startTime = Date.now()) {
|
|
14750
14970
|
// Dispatch to all modules, tracking responses with module names
|
|
14751
14971
|
const responses = [];
|
|
14752
|
-
|
|
14972
|
+
const registrations = new Map();
|
|
14973
|
+
const stillRegistered = (entry) => this.moduleRegistry.registrationOf(entry.moduleName) === registrations.get(entry);
|
|
14974
|
+
const dispatched = this.moduleRegistry.getAllModules()
|
|
14975
|
+
.map((module) => ({ module, registration: this.moduleRegistry.registrationOf(module.name) }));
|
|
14976
|
+
for (const { module, registration } of dispatched) {
|
|
14753
14977
|
// The framework's own toolsets module answers no event; every agent has
|
|
14754
14978
|
// it now, so it isn't listed beside the modules that do.
|
|
14755
14979
|
if (module instanceof ToolsetsModule)
|
|
14756
14980
|
continue;
|
|
14981
|
+
if (this.moduleRegistry.registrationOf(module.name) !== registration)
|
|
14982
|
+
continue;
|
|
14757
14983
|
try {
|
|
14758
14984
|
const processState = this.moduleRegistry.createProcessState(module.name);
|
|
14759
14985
|
const response = await module.onProcess(event, processState);
|
|
14760
|
-
|
|
14986
|
+
const entry = { moduleName: module.name, response };
|
|
14987
|
+
responses.push(entry);
|
|
14988
|
+
registrations.set(entry, registration);
|
|
14761
14989
|
}
|
|
14762
14990
|
catch (error) {
|
|
14763
14991
|
console.error(`Module ${module.name} error handling process event:`, error);
|
|
@@ -14783,7 +15011,16 @@ export class AgentFramework {
|
|
|
14783
15011
|
// responses, a tool call's dispatch and the trace go on, since a module
|
|
14784
15012
|
// can't catch a throw on this path, and one module's mustn't cost the
|
|
14785
15013
|
// event.
|
|
14786
|
-
const
|
|
15014
|
+
const dropUnregistered = (entry) => {
|
|
15015
|
+
if (stillRegistered(entry))
|
|
15016
|
+
return false;
|
|
15017
|
+
const parts = responseParts(entry.response);
|
|
15018
|
+
if (parts) {
|
|
15019
|
+
console.error(`[process] module ${entry.moduleName} stopped before its response to ${event.type} was applied, so none of it was: ${parts}`);
|
|
15020
|
+
}
|
|
15021
|
+
return true;
|
|
15022
|
+
};
|
|
15023
|
+
const applying = responses.filter((entry) => !dropUnregistered(entry)).filter(({ moduleName, response }) => {
|
|
14787
15024
|
const refusal = malformedResponseContent(response);
|
|
14788
15025
|
if (refusal === null)
|
|
14789
15026
|
return true;
|
|
@@ -14804,7 +15041,7 @@ export class AgentFramework {
|
|
|
14804
15041
|
const { moduleName, response } = entry;
|
|
14805
15042
|
const step = { at: 'its start' };
|
|
14806
15043
|
try {
|
|
14807
|
-
|
|
15044
|
+
this.applyProcessResponse(response, event, moduleName, surface, step);
|
|
14808
15045
|
}
|
|
14809
15046
|
catch (error) {
|
|
14810
15047
|
entry.threw = { at: step.at, error: error instanceof Error ? error.message : String(error) };
|
|
@@ -14825,7 +15062,7 @@ export class AgentFramework {
|
|
|
14825
15062
|
this.logProcessEvent(event, applying);
|
|
14826
15063
|
}
|
|
14827
15064
|
}
|
|
14828
|
-
|
|
15065
|
+
applyProcessResponse(response, event, moduleName,
|
|
14829
15066
|
/** The event's surface envelope, when it is console/API input. */
|
|
14830
15067
|
surface,
|
|
14831
15068
|
/** Names the operation under way, for the line when one throws. */
|
|
@@ -15576,6 +15813,9 @@ export class AgentFramework {
|
|
|
15576
15813
|
else {
|
|
15577
15814
|
// Nothing live under the name: only what the process keeps by name.
|
|
15578
15815
|
this.closingConversationAgents.delete(agentName);
|
|
15816
|
+
this.deadlineCuts.delete(agentName);
|
|
15817
|
+
this.freshCallsCut.delete(agentName);
|
|
15818
|
+
this.forgetFailureAccounting(agentName);
|
|
15579
15819
|
this.thinkingLossIncidents.forget(agentName);
|
|
15580
15820
|
this.reasoningLoss.forget(agentName);
|
|
15581
15821
|
// A continuation of its turn still on its way goes with it.
|
|
@@ -16566,19 +16806,20 @@ export class AgentFramework {
|
|
|
16566
16806
|
});
|
|
16567
16807
|
}
|
|
16568
16808
|
}
|
|
16569
|
-
/** Withdraw unstarted wakes whose sole cause is this subject (§4.2)
|
|
16809
|
+
/** Withdraw unstarted wakes whose sole cause is this subject (§4.2), and
|
|
16810
|
+
* a merged start whose every request is withdrawn. */
|
|
16570
16811
|
cancelCoalescedWakes(subject) {
|
|
16571
|
-
|
|
16572
|
-
for (const cooldown of this.providerAccelerationCooldowns.values()) {
|
|
16573
|
-
cooldown.heldRequests = cooldown.heldRequests.filter((r) => r.coalescingSubject !== subject);
|
|
16574
|
-
}
|
|
16575
|
-
// A wake of the subject's merged into a start that carries a turn on
|
|
16812
|
+
// A wake of the subject's merged into a start
|
|
16576
16813
|
// (MERGED_BATCH) is withdrawn from the start's batch, so no stop puts
|
|
16577
16814
|
// it back and no pass unpacks it.
|
|
16815
|
+
const emptied = new Set();
|
|
16578
16816
|
const unrecord = (r) => {
|
|
16579
16817
|
const batch = mergedBatch(r);
|
|
16580
16818
|
if (batch?.some((w) => w.coalescingSubject === subject)) {
|
|
16581
|
-
|
|
16819
|
+
const left = batch.filter((w) => w.coalescingSubject !== subject);
|
|
16820
|
+
r[MERGED_BATCH] = left;
|
|
16821
|
+
if (left.length === 0)
|
|
16822
|
+
emptied.add(r);
|
|
16582
16823
|
}
|
|
16583
16824
|
};
|
|
16584
16825
|
for (const r of this.pendingRequests)
|
|
@@ -16590,6 +16831,29 @@ export class AgentFramework {
|
|
|
16590
16831
|
for (const pass of this.passRequests ?? [])
|
|
16591
16832
|
for (const requests of pass.values())
|
|
16592
16833
|
requests.forEach(unrecord);
|
|
16834
|
+
const withdrawn = (r) => r.coalescingSubject === subject || emptied.has(r);
|
|
16835
|
+
this.pendingRequests = this.pendingRequests.filter((r) => !withdrawn(r));
|
|
16836
|
+
for (const cooldown of this.providerAccelerationCooldowns.values()) {
|
|
16837
|
+
const held = cooldown.heldRequests.length;
|
|
16838
|
+
cooldown.heldRequests = cooldown.heldRequests.filter((r) => !withdrawn(r));
|
|
16839
|
+
if (held > 0 && cooldown.heldRequests.length === 0)
|
|
16840
|
+
cooldown.emptiedBy = 'withdrawal';
|
|
16841
|
+
}
|
|
16842
|
+
for (const pass of this.passRequests ?? []) {
|
|
16843
|
+
for (const requests of pass.values()) {
|
|
16844
|
+
if (!requests.some((r) => emptied.has(r)))
|
|
16845
|
+
continue;
|
|
16846
|
+
const left = requests.filter((r) => !emptied.has(r));
|
|
16847
|
+
requests.length = 0;
|
|
16848
|
+
requests.push(...left);
|
|
16849
|
+
}
|
|
16850
|
+
}
|
|
16851
|
+
for (const start of emptied) {
|
|
16852
|
+
if (!start.coalescingBatch || start.coalescingSubject !== undefined)
|
|
16853
|
+
continue;
|
|
16854
|
+
console.error(`[coalescing] ${start.agentName}: its deferred batches were withdrawn while its wake waited; turn not started (no wake cause left)`);
|
|
16855
|
+
this.emitTrace({ type: 'mcpl:coalescing', kind: 'turn-withdrawn', agentName: start.agentName, subjects: start.coalescingBatchSubjects ?? [] });
|
|
16856
|
+
}
|
|
16593
16857
|
}
|
|
16594
16858
|
/**
|
|
16595
16859
|
* A placement in the deferred queue is upgraded in place once the flush has
|
|
@@ -17099,7 +17363,7 @@ export class AgentFramework {
|
|
|
17099
17363
|
recovery.releasedAt = now;
|
|
17100
17364
|
requests = [...providerCooldown.heldRequests, ...requests];
|
|
17101
17365
|
}
|
|
17102
|
-
// A start that
|
|
17366
|
+
// A start that came back before it took its
|
|
17103
17367
|
// token stands for the batch it was merged from (MERGED_BATCH),
|
|
17104
17368
|
// including one a hold released just now. The batch takes its place,
|
|
17105
17369
|
// each request as it was, judged one by one from here: a wait holds
|
|
@@ -17501,10 +17765,10 @@ export class AgentFramework {
|
|
|
17501
17765
|
start[REARMED] = true;
|
|
17502
17766
|
else
|
|
17503
17767
|
delete start[REARMED];
|
|
17504
|
-
// A start
|
|
17505
|
-
// the batch it was merged from (MERGED_BATCH): until
|
|
17506
|
-
// token it stands for them all.
|
|
17507
|
-
if (
|
|
17768
|
+
// A start merged from more than one request, any of them not the
|
|
17769
|
+
// turn's, records the batch it was merged from (MERGED_BATCH): until
|
|
17770
|
+
// it takes its token it stands for them all.
|
|
17771
|
+
if (requests.length > 1 && requests.some((r) => !continuesTurn(r)))
|
|
17508
17772
|
start[MERGED_BATCH] = [...requests];
|
|
17509
17773
|
else
|
|
17510
17774
|
delete start[MERGED_BATCH];
|
|
@@ -18154,15 +18418,13 @@ export class AgentFramework {
|
|
|
18154
18418
|
const refuse = (error) => ({ success: false, error, isError: true });
|
|
18155
18419
|
// Plain text, not a JSON-quoted string: draft text must read exactly as held.
|
|
18156
18420
|
const ok = (text) => ({ success: true, data: [{ type: 'text', text }] });
|
|
18157
|
-
// null and omission mean the same thing: the field is not in use.
|
|
18158
|
-
const present = (v) => v !== undefined && v !== null;
|
|
18159
18421
|
const action = input.action;
|
|
18160
18422
|
if (action !== 'list' && action !== 'read' && action !== 'resend' && action !== 'dismiss') {
|
|
18161
18423
|
return refuse('action must be one of "list", "read", "resend" or "dismiss".');
|
|
18162
18424
|
}
|
|
18163
18425
|
const unused = (field) => refuse(`${field} is not used by action "${action}" — pass null or omit it.`);
|
|
18164
18426
|
let ids = [];
|
|
18165
|
-
if (
|
|
18427
|
+
if (fieldInUse(input.draftIds)) {
|
|
18166
18428
|
if (action === 'list')
|
|
18167
18429
|
return unused('draftIds');
|
|
18168
18430
|
if (!Array.isArray(input.draftIds) || input.draftIds.some((id) => typeof id !== 'string' || !id.trim())) {
|
|
@@ -18174,17 +18436,20 @@ export class AgentFramework {
|
|
|
18174
18436
|
if (ids.length > 20)
|
|
18175
18437
|
return refuse('draftIds names more than 20 drafts; act on them in smaller groups.');
|
|
18176
18438
|
}
|
|
18177
|
-
if (
|
|
18439
|
+
if (fieldInUse(input.destination) && action !== 'resend')
|
|
18178
18440
|
return unused('destination');
|
|
18179
|
-
|
|
18441
|
+
// false is "don't confirm", which every other action already does, so only
|
|
18442
|
+
// true is a confirmDuplicate those actions refuse.
|
|
18443
|
+
if (fieldInUse(input.confirmDuplicate) && input.confirmDuplicate !== false && action !== 'resend') {
|
|
18180
18444
|
return unused('confirmDuplicate');
|
|
18181
|
-
|
|
18445
|
+
}
|
|
18446
|
+
if (fieldInUse(input.offset) && action !== 'list')
|
|
18182
18447
|
return unused('offset');
|
|
18183
18448
|
if (action === 'list') {
|
|
18184
18449
|
let offset = 0;
|
|
18185
|
-
if (
|
|
18450
|
+
if (fieldInUse(input.offset)) {
|
|
18186
18451
|
const raw = input.offset;
|
|
18187
|
-
const n =
|
|
18452
|
+
const n = integerField(raw);
|
|
18188
18453
|
if (!Number.isInteger(n) || n < 0)
|
|
18189
18454
|
return refuse('offset must be a whole number of drafts to skip (0 or more), or null.');
|
|
18190
18455
|
offset = n;
|
|
@@ -18299,13 +18564,13 @@ export class AgentFramework {
|
|
|
18299
18564
|
return refuse('No channels are configured, so there is nowhere to resend to.');
|
|
18300
18565
|
const home = this.homeOf(agentName);
|
|
18301
18566
|
let spec = typeof input.destination === 'string' ? input.destination.trim() : '';
|
|
18302
|
-
if (
|
|
18567
|
+
if (fieldInUse(input.destination) && typeof input.destination !== 'string') {
|
|
18303
18568
|
return refuse('destination must be "#channel", "@person" or a channel id.');
|
|
18304
18569
|
}
|
|
18305
18570
|
if (!spec && !home)
|
|
18306
18571
|
return refuse('resend needs a destination: "#channel", "@person" or a channel id.');
|
|
18307
18572
|
let confirmDuplicate = false;
|
|
18308
|
-
if (
|
|
18573
|
+
if (fieldInUse(input.confirmDuplicate)) {
|
|
18309
18574
|
if (typeof input.confirmDuplicate !== 'boolean')
|
|
18310
18575
|
return refuse('confirmDuplicate must be true, false or null.');
|
|
18311
18576
|
confirmDuplicate = input.confirmDuplicate;
|
|
@@ -20615,6 +20880,8 @@ export class AgentFramework {
|
|
|
20615
20880
|
}
|
|
20616
20881
|
}
|
|
20617
20882
|
async startAgentStream(agent, trigger, attempt = 0, providerGateAlreadyHeld = false) {
|
|
20883
|
+
if (attempt === 0)
|
|
20884
|
+
this.deadlineCuts.delete(agent.name);
|
|
20618
20885
|
const ownsProviderGate = this.registry.ownsProviderScheduling(agent.name);
|
|
20619
20886
|
// A stopping framework starts no turn, whoever owns provider admission
|
|
20620
20887
|
// (conversation agents and ephemeral runs do not take the gate).
|
|
@@ -20684,6 +20951,15 @@ export class AgentFramework {
|
|
|
20684
20951
|
console.error(`[provider-admission] ${agent.name}: host quiesced while parked — the merged start goes back as its ${batch.length} requests`);
|
|
20685
20952
|
return;
|
|
20686
20953
|
}
|
|
20954
|
+
if (this.providerAccelerationCooldowns.has(agent.name)) {
|
|
20955
|
+
this.releasePrimaryProviderGate(agent.name);
|
|
20956
|
+
this.eventGate?.onInferenceEnded(agent.name);
|
|
20957
|
+
this.pendingRequests.push(trigger ?? {
|
|
20958
|
+
agentName: agent.name, reason: 'provider-admission:requeue', source: 'scheduler', timestamp: Date.now(),
|
|
20959
|
+
});
|
|
20960
|
+
console.error(`[provider-admission] ${agent.name}: a provider hold stands — parked wake requeued for the scheduler, not started`);
|
|
20961
|
+
return;
|
|
20962
|
+
}
|
|
20687
20963
|
await this.startAgentStream(agent, trigger, attempt, true);
|
|
20688
20964
|
}).catch((error) => {
|
|
20689
20965
|
this.releasePrimaryProviderGate(agent.name);
|
|
@@ -20850,7 +21126,7 @@ export class AgentFramework {
|
|
|
20850
21126
|
const held = hold.heldRequests.length;
|
|
20851
21127
|
hold.heldRequests = take(hold.heldRequests);
|
|
20852
21128
|
if (held > 0 && hold.heldRequests.length === 0)
|
|
20853
|
-
hold.
|
|
21129
|
+
hold.emptiedBy = 'drop';
|
|
20854
21130
|
}
|
|
20855
21131
|
for (const parked of [...this.parkedContinuations]) {
|
|
20856
21132
|
if (!isContinuation(parked))
|
|
@@ -20967,9 +21243,8 @@ export class AgentFramework {
|
|
|
20967
21243
|
* gets inference:aborted with `reason`, and lastInference records its end
|
|
20968
21244
|
* as driveStream's would, when the record is still this turn's. Nothing of
|
|
20969
21245
|
* the kind when another turn of this agent's name holds the turn token (one
|
|
20970
|
-
* begun
|
|
20971
|
-
*
|
|
20972
|
-
* newer turn instead.
|
|
21246
|
+
* begun by a replacement agent object): the event and the record carry only
|
|
21247
|
+
* the name, so this frame's would end that newer turn instead.
|
|
20973
21248
|
*/
|
|
20974
21249
|
abortDroppedTurn(agentName, turnToken, reason, startedAt) {
|
|
20975
21250
|
const owner = this.activeTurnTokens.get(agentName);
|
|
@@ -21144,7 +21419,6 @@ export class AgentFramework {
|
|
|
21144
21419
|
/** This start's window (checkAhead), which its checks read for an
|
|
21145
21420
|
* operator's abandon. */
|
|
21146
21421
|
checkWindow) {
|
|
21147
|
-
const lifecycle = this.lifecycleGeneration;
|
|
21148
21422
|
const resumesTurn = resumesLogicalTurn(trigger?.reason);
|
|
21149
21423
|
let carry = turnCarryOf(trigger);
|
|
21150
21424
|
const turnProseRouting = trigger?.suppressProse ? 'disabled' : agent.proseRouting;
|
|
@@ -21328,7 +21602,7 @@ export class AgentFramework {
|
|
|
21328
21602
|
// everything the turn has decided about where its speech goes.
|
|
21329
21603
|
// - A resume (a budget restart, a tool-result-guard retry) carries its
|
|
21330
21604
|
// turn's request on and keeps the turn's object as it is, and the
|
|
21331
|
-
// turn stays, or
|
|
21605
|
+
// turn stays, or becomes again, the agent's open turn.
|
|
21332
21606
|
// - A silent start routes no prose. It gets an object of its own, with
|
|
21333
21607
|
// no locus, so it neither takes nor disturbs an open turn's. Its
|
|
21334
21608
|
// object is the agent's live routing while it runs, and the open
|
|
@@ -21353,8 +21627,6 @@ export class AgentFramework {
|
|
|
21353
21627
|
let opensTurn = false;
|
|
21354
21628
|
if (resumesTurn) {
|
|
21355
21629
|
routing = this.routingOf(agent.name);
|
|
21356
|
-
// The turn a resume continues is open, unless a stop() ended every
|
|
21357
|
-
// open turn since: it opens again, so its later starts continue it.
|
|
21358
21630
|
if (!trigger?.suppressProse && !this.openLogical.has(agent.name))
|
|
21359
21631
|
this.openLogical.set(agent.name, routing);
|
|
21360
21632
|
}
|
|
@@ -21383,6 +21655,10 @@ export class AgentFramework {
|
|
|
21383
21655
|
const head = agent.branchHead();
|
|
21384
21656
|
if (head)
|
|
21385
21657
|
routing.start = head;
|
|
21658
|
+
if (!routing.silent)
|
|
21659
|
+
this.pinTurnInputs(agent, routing);
|
|
21660
|
+
else
|
|
21661
|
+
agent.unpinTurnInputs();
|
|
21386
21662
|
}
|
|
21387
21663
|
this.turnRouting.set(agent.name, routing);
|
|
21388
21664
|
if (opensTurn)
|
|
@@ -21527,11 +21803,11 @@ export class AgentFramework {
|
|
|
21527
21803
|
});
|
|
21528
21804
|
// A budget restart continues the same logical inference window. The
|
|
21529
21805
|
// predecessor keeps EventGate liveness until its successor terminates,
|
|
21530
|
-
// and the successor takes it again if it ended in between: at a
|
|
21531
|
-
// (a restart left queued, which a start() then takes up), or at a
|
|
21806
|
+
// and the successor takes it again if it ended in between: at a
|
|
21532
21807
|
// provider hold that kept the successor from starting (a stated wait at
|
|
21533
|
-
// its last admission check,
|
|
21534
|
-
//
|
|
21808
|
+
// its last admission check, a hold standing when its park behind an
|
|
21809
|
+
// auxiliary call ended, or a cooldown after its own call failed), whose
|
|
21810
|
+
// end queues it again.
|
|
21535
21811
|
if (!resumesTurn) {
|
|
21536
21812
|
this.eventGate?.onInferenceStarted(agent.name);
|
|
21537
21813
|
}
|
|
@@ -21608,13 +21884,11 @@ export class AgentFramework {
|
|
|
21608
21884
|
// An ephemeral watchdog may dispose this Agent while hooks/compile await,
|
|
21609
21885
|
// and stop() may close provider admission. Do not create a provider
|
|
21610
21886
|
// stream after its generation lost ownership, or once admission closed,
|
|
21611
|
-
//
|
|
21612
|
-
//
|
|
21613
|
-
|
|
21614
|
-
if (this.agents.get(agent.name) !== agent || this.providerAdmissionClosed || lifecycle !== this.lifecycleGeneration) {
|
|
21887
|
+
// as after the compile: these come before a stop kept for the turn,
|
|
21888
|
+
// which goes with it.
|
|
21889
|
+
if (this.agents.get(agent.name) !== agent || this.providerAdmissionClosed) {
|
|
21615
21890
|
const why = this.agents.get(agent.name) !== agent ? 'the agent was replaced or removed'
|
|
21616
|
-
:
|
|
21617
|
-
: 'the framework was stopped and started again';
|
|
21891
|
+
: 'the framework was stopping';
|
|
21618
21892
|
const reason = `a turn assembled while ${why} is dropped before it sends anything`;
|
|
21619
21893
|
console.error(`[turn] ${agent.name}: ${reason}`);
|
|
21620
21894
|
this.endDroppedTurn(agent.name, routing);
|
|
@@ -21655,6 +21929,7 @@ export class AgentFramework {
|
|
|
21655
21929
|
// This turn's retries are the framework's (errorPolicy below), not
|
|
21656
21930
|
// membrane's inside each attempt.
|
|
21657
21931
|
membraneRetries: false,
|
|
21932
|
+
inferenceBounds: this.inferenceBounds,
|
|
21658
21933
|
});
|
|
21659
21934
|
// Each later round this stream sends is noted as it goes (noteRoundSent).
|
|
21660
21935
|
if (noteRequestSent)
|
|
@@ -21662,18 +21937,16 @@ export class AgentFramework {
|
|
|
21662
21937
|
if (carriedBodies)
|
|
21663
21938
|
this.streamCarried.set(stream, new Set(carriedBodies));
|
|
21664
21939
|
// The compile can wait for annotations (each batch has one spill
|
|
21665
|
-
// deadline), so check ownership
|
|
21666
|
-
// A stop followed by start must not revive this earlier turn.
|
|
21940
|
+
// deadline), so check ownership and admission again.
|
|
21667
21941
|
// Membrane's yielding stream starts its request when it is first
|
|
21668
21942
|
// iterated, so cancelling it here, before driveStream, sends nothing.
|
|
21669
21943
|
// A turn that was mid-compile when stop() cancelled every stream would
|
|
21670
21944
|
// otherwise register after that pass and outlive stop(). The caller's
|
|
21671
21945
|
// finally releases the turn token.
|
|
21672
21946
|
const stopping = this.providerAdmissionClosed;
|
|
21673
|
-
if (this.agents.get(agent.name) !== agent || stopping
|
|
21947
|
+
if (this.agents.get(agent.name) !== agent || stopping) {
|
|
21674
21948
|
const why = this.agents.get(agent.name) !== agent ? 'the agent was replaced or removed'
|
|
21675
|
-
:
|
|
21676
|
-
: 'the framework was stopped and started again';
|
|
21949
|
+
: 'the framework was stopping';
|
|
21677
21950
|
const reason = `a turn compiled while ${why} is dropped before it sends anything`;
|
|
21678
21951
|
console.error(`[turn] ${agent.name}: ${reason}`);
|
|
21679
21952
|
this.cancelUnsentStream(agent, stream, `compiled while ${why}`);
|
|
@@ -21794,14 +22067,12 @@ export class AgentFramework {
|
|
|
21794
22067
|
// catch never holds a compiled request.) As at the checks after the
|
|
21795
22068
|
// hooks and after the compile, a replaced agent or a stopping framework
|
|
21796
22069
|
// comes first: the turn is dropped as theirs, and the stop goes with
|
|
21797
|
-
// it
|
|
21798
|
-
// compile. A failed start with no stop goes on below as before.
|
|
22070
|
+
// it. A failed start with no stop goes on below as before.
|
|
21799
22071
|
const stopAtFailure = this.takeStartStop(agent, checkWindow);
|
|
21800
22072
|
if (stopAtFailure) {
|
|
21801
|
-
if (this.agents.get(agent.name) !== agent || this.providerAdmissionClosed
|
|
22073
|
+
if (this.agents.get(agent.name) !== agent || this.providerAdmissionClosed) {
|
|
21802
22074
|
const why = this.agents.get(agent.name) !== agent ? 'the agent was replaced or removed'
|
|
21803
|
-
:
|
|
21804
|
-
: 'the framework was stopped and started again';
|
|
22075
|
+
: 'the framework was stopping';
|
|
21805
22076
|
const reason = `a turn whose request couldn't be built while ${why} is dropped before it sends anything`;
|
|
21806
22077
|
console.error(`[turn] ${agent.name}: ${reason}`);
|
|
21807
22078
|
this.endDroppedTurn(agent.name, routing);
|
|
@@ -21813,6 +22084,7 @@ export class AgentFramework {
|
|
|
21813
22084
|
return false;
|
|
21814
22085
|
}
|
|
21815
22086
|
const retry = trigger && withTurnCarry(trigger, carry);
|
|
22087
|
+
const asTransient = this.takeAsTransient(agent.name, err, trigger);
|
|
21816
22088
|
if (ownsProviderGate && this.holdProviderAcceleration(agent, err, retry)) {
|
|
21817
22089
|
// Capacity scheduling is not poisoned history. The cooldown timer owns
|
|
21818
22090
|
// one later fresh compile; no immediate retry or exhaustion marker.
|
|
@@ -21828,23 +22100,28 @@ export class AgentFramework {
|
|
|
21828
22100
|
this.endHeldStart(agent, checkWindow, turnToken, turnStartedAt, routing);
|
|
21829
22101
|
return false;
|
|
21830
22102
|
}
|
|
21831
|
-
const
|
|
21832
|
-
|
|
22103
|
+
const cuts = isDeadlineExpiry(err) ? this.deadlineCuts.get(agent.name) ?? 0 : undefined;
|
|
22104
|
+
const action = statedWait === 'fail' || (isRearmed(trigger) && (!gotPartway(routing) || (isDeadlineExpiry(err) && !asTransient)))
|
|
22105
|
+
? { retry: false } : this.errorPolicy.onInferenceError(err, agent.name, cuts ?? attempt);
|
|
22106
|
+
if (action.retry && cuts !== undefined)
|
|
22107
|
+
this.deadlineCuts.set(agent.name, cuts + 1);
|
|
22108
|
+
else
|
|
22109
|
+
this.deadlineCuts.delete(agent.name);
|
|
22110
|
+
if (!action.retry && cuts !== undefined && cuts > 0)
|
|
22111
|
+
this.freshCallsCut.add(agent.name);
|
|
21833
22112
|
if (action.retry) {
|
|
21834
22113
|
// A policy's delay can be a provider's retry-after: wait all of it.
|
|
21835
22114
|
await this.waitForRetry(action.delayMs, checkWindow, agent);
|
|
21836
|
-
if (this.providerAdmissionClosed
|
|
22115
|
+
if (this.providerAdmissionClosed) {
|
|
21837
22116
|
// stop() ended the wait: that cancels the retry, it does not hasten
|
|
21838
|
-
// it
|
|
21839
|
-
//
|
|
21840
|
-
//
|
|
21841
|
-
//
|
|
21842
|
-
// anything does: a terminal event saying why, after the
|
|
21843
|
-
// inference:failed above, and its record's end. Nothing is written
|
|
22117
|
+
// it, and a stop kept for the turn goes with it. The turn ends as
|
|
22118
|
+
// one dropped before it sends anything does: a terminal event
|
|
22119
|
+
// saying why, after the inference:failed above, and its record's
|
|
22120
|
+
// end. Nothing is written
|
|
21844
22121
|
// but the [speech-not-posted] notice; this frame still owns its
|
|
21845
22122
|
// token, since no retry frame replaced it.
|
|
21846
22123
|
this.nameUnpostedSpeech(agent, routing);
|
|
21847
|
-
this.abortDroppedTurn(agent.name, turnToken,
|
|
22124
|
+
this.abortDroppedTurn(agent.name, turnToken, RETRY_CANCELLED_BY_STOP, turnStartedAt);
|
|
21848
22125
|
if (this.activeTurnTokens.get(agent.name) === turnToken) {
|
|
21849
22126
|
this.activeTurnTokens.delete(agent.name);
|
|
21850
22127
|
this.activeTurnTriggers.delete(agent.name);
|
|
@@ -21890,7 +22167,7 @@ export class AgentFramework {
|
|
|
21890
22167
|
speech: '',
|
|
21891
22168
|
...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: err.message }),
|
|
21892
22169
|
}, routing);
|
|
21893
|
-
if (isRearmed(trigger) &&
|
|
22170
|
+
if (isRearmed(trigger) && asTransient)
|
|
21894
22171
|
this.exhaustingRearms.add(agent.name);
|
|
21895
22172
|
this.emitTrace({
|
|
21896
22173
|
type: 'inference:exhausted',
|
|
@@ -21911,7 +22188,7 @@ export class AgentFramework {
|
|
|
21911
22188
|
if (action.emit) {
|
|
21912
22189
|
this.pushEvent(action.emit);
|
|
21913
22190
|
}
|
|
21914
|
-
this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, routing);
|
|
22191
|
+
this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, routing, asTransient);
|
|
21915
22192
|
}
|
|
21916
22193
|
// No handoff on either failure branch: the terminal branch deleted the
|
|
21917
22194
|
// token above; the retry branch's recursive startAgentStream replaced
|
|
@@ -22527,7 +22804,7 @@ export class AgentFramework {
|
|
|
22527
22804
|
assistantBlocks = roundContent.filter((b) => b.type !== 'tool_result');
|
|
22528
22805
|
}
|
|
22529
22806
|
else {
|
|
22530
|
-
//
|
|
22807
|
+
// XML tool mode: this round's text and its calls.
|
|
22531
22808
|
// XML mode: persist the verbatim generation on each tool_use
|
|
22532
22809
|
// (membrane#36 round-trip fidelity). context.rawText is the
|
|
22533
22810
|
// parser's fullMatch — the exact <function_calls> block the
|
|
@@ -22539,14 +22816,12 @@ export class AgentFramework {
|
|
|
22539
22816
|
const rawXml = rawText && /^<(antml:)?function_calls>/.test(rawText)
|
|
22540
22817
|
? rawText
|
|
22541
22818
|
: undefined;
|
|
22542
|
-
// Persist THIS round's text only
|
|
22543
|
-
//
|
|
22544
|
-
//
|
|
22545
|
-
//
|
|
22546
|
-
//
|
|
22547
|
-
|
|
22548
|
-
const roundText = event.context.roundPreamble ??
|
|
22549
|
-
event.context.preamble;
|
|
22819
|
+
// Persist THIS round's text only: `roundPreamble` is the
|
|
22820
|
+
// per-round delta. The `preamble` beside it is CUMULATIVE in XML
|
|
22821
|
+
// mode: persisting it per round would store each round's text N
|
|
22822
|
+
// times and re-persist injected <function_results> as the
|
|
22823
|
+
// agent's own words.
|
|
22824
|
+
const roundText = event.context.roundPreamble;
|
|
22550
22825
|
assistantBlocks = [];
|
|
22551
22826
|
if (roundText) {
|
|
22552
22827
|
assistantBlocks.push({ type: 'text', text: roundText });
|
|
@@ -22570,9 +22845,7 @@ export class AgentFramework {
|
|
|
22570
22845
|
this.roundTurns?.set(assistantBlocks, streamRouting);
|
|
22571
22846
|
// Whether this stream can carry messages injected with the round's
|
|
22572
22847
|
// results: true on membrane's native path and false on its XML
|
|
22573
|
-
// prefill path
|
|
22574
|
-
// doesn't declare it. The tool boundary reads it from the agent's
|
|
22575
|
-
// state.
|
|
22848
|
+
// prefill path. The tool boundary reads it from the agent's state.
|
|
22576
22849
|
agent.enterWaitingForTools(calls, stream, event.context.supportsInjectedMessages);
|
|
22577
22850
|
// Speak-while-acting: route THIS round's prose to the locus NOW
|
|
22578
22851
|
// instead of batching every segment to the end of the turn. Long
|
|
@@ -22610,12 +22883,10 @@ export class AgentFramework {
|
|
|
22610
22883
|
liveProseRouting = true;
|
|
22611
22884
|
// Whether this round's tool-result batch (and anything queued
|
|
22612
22885
|
// with it) will be presented to the model before it speaks
|
|
22613
|
-
// again: the
|
|
22614
|
-
//
|
|
22615
|
-
// presenting it. That holds for 0.5.x's native rounds, while its
|
|
22616
|
-
// XML path drops what's injected, so a 'now' notice there reaches
|
|
22886
|
+
// again: the stream declares it (true on membrane's native path).
|
|
22887
|
+
// On the XML path, which declares false, a 'now' notice reaches
|
|
22617
22888
|
// the agent at its next compile instead.
|
|
22618
|
-
const presentsInjections = event.context.supportsInjectedMessages
|
|
22889
|
+
const presentsInjections = event.context.supportsInjectedMessages === true;
|
|
22619
22890
|
// Queued under this turn's routing, whenever it posts, and
|
|
22620
22891
|
// streamed by this stream once published.
|
|
22621
22892
|
// This round's request was answered: what it carried is heard.
|
|
@@ -23348,14 +23619,14 @@ export class AgentFramework {
|
|
|
23348
23619
|
// active (native roundContent), each mid-turn round's prose was
|
|
23349
23620
|
// already delivered when that round yielded — only the TRAILING
|
|
23350
23621
|
// prose (blocks after the last tool round) remains here. On the
|
|
23351
|
-
//
|
|
23352
|
-
//
|
|
23622
|
+
// XML tool path (no roundContent, so nothing was live-routed)
|
|
23623
|
+
// the historical behavior applies:
|
|
23353
23624
|
// route the whole turn's parsed segments, once, now. The global
|
|
23354
23625
|
// speech/thoughts split is left untouched (module/TUI rendering
|
|
23355
23626
|
// unaffected) — this only governs what reaches the channel.
|
|
23356
23627
|
//
|
|
23357
23628
|
// Native round routing uses the live sticky flag, which a fresh
|
|
23358
|
-
// injected message can reset. The
|
|
23629
|
+
// injected message can reset. The XML tool path has no
|
|
23359
23630
|
// reliable round boundaries, so retain its historical turn-wide
|
|
23360
23631
|
// scan to avoid double-posting.
|
|
23361
23632
|
const toolCalls = response.content
|
|
@@ -23582,6 +23853,7 @@ export class AgentFramework {
|
|
|
23582
23853
|
break;
|
|
23583
23854
|
this.abandonAgentScripts(agent.name, 'stream error', streamRouting);
|
|
23584
23855
|
const retry = retryOf(trigger);
|
|
23856
|
+
const asTransient = this.takeAsTransient(agent.name, err, trigger);
|
|
23585
23857
|
// A hold takes the turn's retry, or, once stop() has closed
|
|
23586
23858
|
// admission, the turn ends here (endFailureAtStop).
|
|
23587
23859
|
if (ownsProviderGate && this.holdProviderAcceleration(agent, err, retry)) {
|
|
@@ -23597,8 +23869,15 @@ export class AgentFramework {
|
|
|
23597
23869
|
this.eventGate?.onInferenceEnded(agent.name);
|
|
23598
23870
|
break;
|
|
23599
23871
|
}
|
|
23600
|
-
const
|
|
23601
|
-
|
|
23872
|
+
const cuts = isDeadlineExpiry(err) ? this.deadlineCuts.get(agent.name) ?? 0 : undefined;
|
|
23873
|
+
const action = statedWait === 'fail' || (isRearmed(trigger) && (!gotPartway(streamRouting) || (isDeadlineExpiry(err) && !asTransient)))
|
|
23874
|
+
? { retry: false } : this.errorPolicy.onInferenceError(err, agent.name, cuts ?? attempt);
|
|
23875
|
+
if (action.retry && cuts !== undefined)
|
|
23876
|
+
this.deadlineCuts.set(agent.name, cuts + 1);
|
|
23877
|
+
else
|
|
23878
|
+
this.deadlineCuts.delete(agent.name);
|
|
23879
|
+
if (!action.retry && cuts !== undefined && cuts > 0)
|
|
23880
|
+
this.freshCallsCut.add(agent.name);
|
|
23602
23881
|
if (action.retry) {
|
|
23603
23882
|
// A stop made during the wait reaches no stream: the check after
|
|
23604
23883
|
// the wait ends the turn, as the guard's check after its wait
|
|
@@ -23660,7 +23939,7 @@ export class AgentFramework {
|
|
|
23660
23939
|
speech: '',
|
|
23661
23940
|
...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: err.message }),
|
|
23662
23941
|
}, this.agents.get(agent.name) === agent && agent.streamId === myStreamId ? streamRouting : undefined, streamRouting);
|
|
23663
|
-
if (isRearmed(trigger) &&
|
|
23942
|
+
if (isRearmed(trigger) && asTransient)
|
|
23664
23943
|
this.exhaustingRearms.add(agent.name);
|
|
23665
23944
|
this.emitTrace({
|
|
23666
23945
|
type: 'inference:exhausted',
|
|
@@ -23680,7 +23959,7 @@ export class AgentFramework {
|
|
|
23680
23959
|
if (action.emit) {
|
|
23681
23960
|
this.pushEvent(action.emit);
|
|
23682
23961
|
}
|
|
23683
|
-
this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, streamRouting);
|
|
23962
|
+
this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, streamRouting, asTransient);
|
|
23684
23963
|
}
|
|
23685
23964
|
break;
|
|
23686
23965
|
}
|
|
@@ -23755,7 +24034,7 @@ export class AgentFramework {
|
|
|
23755
24034
|
agentName: agent.name,
|
|
23756
24035
|
error: terminal,
|
|
23757
24036
|
});
|
|
23758
|
-
this.rearmExhaustedTurn(agent, new Error(terminal), trigger, ownsProviderGate, streamRouting);
|
|
24037
|
+
this.rearmExhaustedTurn(agent, new Error(terminal), trigger, ownsProviderGate, streamRouting, false);
|
|
23759
24038
|
// Postmortem 2026-05-28 P2 #7: persist the abort to the
|
|
23760
24039
|
// inference log so future investigations can attribute the
|
|
23761
24040
|
// terminal cause without relying on live in-memory reducer
|
|
@@ -23773,6 +24052,8 @@ export class AgentFramework {
|
|
|
23773
24052
|
break;
|
|
23774
24053
|
}
|
|
23775
24054
|
case 'usage': {
|
|
24055
|
+
this.rearmDeadlineCuts.delete(agent.name);
|
|
24056
|
+
this.deadlineCuts.delete(agent.name);
|
|
23776
24057
|
// Cumulative across the stream's rounds: replace, never sum.
|
|
23777
24058
|
latestUsage = event.usage;
|
|
23778
24059
|
// The stream's accumulated fresh input, which is what the
|
|
@@ -23995,6 +24276,7 @@ export class AgentFramework {
|
|
|
23995
24276
|
agent.reset();
|
|
23996
24277
|
this.emitTrace({ type: 'inference:failed', agentName: agent.name, error: err.message, stack: err.stack });
|
|
23997
24278
|
const retry = retryOf(trigger);
|
|
24279
|
+
const asTransient = this.takeAsTransient(agent.name, err, trigger);
|
|
23998
24280
|
if (ownsProviderGate && this.holdProviderAcceleration(agent, err, retry)) {
|
|
23999
24281
|
// Once stop() has closed admission, nothing is held, and the turn
|
|
24000
24282
|
// ends here (endFailureAtStop).
|
|
@@ -24032,7 +24314,7 @@ export class AgentFramework {
|
|
|
24032
24314
|
speech: '',
|
|
24033
24315
|
...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: err.message }),
|
|
24034
24316
|
}, streamRouting);
|
|
24035
|
-
if (isRearmed(trigger) &&
|
|
24317
|
+
if (isRearmed(trigger) && asTransient)
|
|
24036
24318
|
this.exhaustingRearms.add(agent.name);
|
|
24037
24319
|
this.emitTrace({
|
|
24038
24320
|
type: 'inference:exhausted',
|
|
@@ -24040,7 +24322,7 @@ export class AgentFramework {
|
|
|
24040
24322
|
error: err.message,
|
|
24041
24323
|
...this.classifyInferenceError(err),
|
|
24042
24324
|
});
|
|
24043
|
-
this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, streamRouting);
|
|
24325
|
+
this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, streamRouting, asTransient);
|
|
24044
24326
|
// Postmortem 2026-05-28 P2 #7: catch-path failures (stream itself
|
|
24045
24327
|
// threw) were previously only visible via in-memory trace listeners.
|
|
24046
24328
|
// Persist to inference log for forensic attribution.
|
|
@@ -24246,7 +24528,11 @@ export class AgentFramework {
|
|
|
24246
24528
|
}
|
|
24247
24529
|
/**
|
|
24248
24530
|
* Execute a tool call and return the result.
|
|
24249
|
-
* Routes
|
|
24531
|
+
* Routes it where the agent's own call of the name goes: to the
|
|
24532
|
+
* framework's own tools the agent has (agent_settings, the workspace and
|
|
24533
|
+
* gate tools, think, journal, prose_help and the rest), the channel tools,
|
|
24534
|
+
* MCPL or the module registry. skip_reply, which ends the turn it's called
|
|
24535
|
+
* in, is refused, since this call isn't made in one.
|
|
24250
24536
|
* Its one caller in the framework is the operator's puppet
|
|
24251
24537
|
* (puppetToolCall), which puts the agent's inline cap on the call as
|
|
24252
24538
|
* resultMaxChars; a host may call it as well.
|
|
@@ -24292,8 +24578,11 @@ export class AgentFramework {
|
|
|
24292
24578
|
* wire ordering. For its own duration the puppet HOLDS the turn-alive
|
|
24293
24579
|
* marker (wakes requeue, cross-turn writers defer), so a wake landing
|
|
24294
24580
|
* during the tool's execution cannot start a turn underneath the pair.
|
|
24295
|
-
* - Refused for tools outside the agent's own surface (
|
|
24296
|
-
*
|
|
24581
|
+
* - Refused for tools outside the agent's own surface (the tools it may
|
|
24582
|
+
* call, availableToolsForPresentation), and for skip_reply, by name or
|
|
24583
|
+
* through a toolsets--act that reaches it, which ends the turn it's
|
|
24584
|
+
* called in: the stored pair must be an act the agent could genuinely
|
|
24585
|
+
* have taken.
|
|
24297
24586
|
* - The call executes FOR REAL through the shared dispatch (MCPL,
|
|
24298
24587
|
* channel tools, utils, modules) with the agent's provenance — a
|
|
24299
24588
|
* puppeted walk_to actually moves the avatar.
|
|
@@ -24334,12 +24623,17 @@ export class AgentFramework {
|
|
|
24334
24623
|
: `puppet refused: agent ${agentName} is ${shown} (requires idle — ` +
|
|
24335
24624
|
`injecting a turn under an active stream corrupts wire ordering)`);
|
|
24336
24625
|
}
|
|
24337
|
-
const onSurface = this.
|
|
24338
|
-
.some((t) => t.name === toolName && agent.canUseTool(t.name));
|
|
24626
|
+
const onSurface = this.availableToolsForPresentation(agentName).some((t) => t.name === toolName);
|
|
24339
24627
|
if (!onSurface) {
|
|
24340
24628
|
throw new Error(`puppet refused: tool ${toolName} is not on ${agentName}'s surface — ` +
|
|
24341
24629
|
`the stored pair must be an act the agent could genuinely have taken`);
|
|
24342
24630
|
}
|
|
24631
|
+
const route = this.toolRoute({ id: '', name: toolName, input }, agentName);
|
|
24632
|
+
const acting = route.kind === 'host-act' && route.host.kind === 'call' ? route.host.name : toolName;
|
|
24633
|
+
if (acting === 'skip_reply') {
|
|
24634
|
+
throw new Error("puppet refused: skip_reply ends the turn it's called in, and a puppeted call isn't made in one, " +
|
|
24635
|
+
'so nothing was run or stored');
|
|
24636
|
+
}
|
|
24343
24637
|
// Anthropic-shaped id so the stored pair is indistinguishable from a
|
|
24344
24638
|
// provider-issued call.
|
|
24345
24639
|
const alphabet = 'ABCDEFGHJKMNPQRSTVWXYZabcdefghjkmnpqrstvwxyz0123456789';
|
|
@@ -24471,96 +24765,119 @@ export class AgentFramework {
|
|
|
24471
24765
|
this.emitTrace({ type: 'tool:failed', module: 'framework', tool: call.name, callId: call.id, error: marked });
|
|
24472
24766
|
return { success: false, error: marked, isError: true };
|
|
24473
24767
|
}
|
|
24474
|
-
|
|
24475
|
-
|
|
24476
|
-
|
|
24477
|
-
|
|
24478
|
-
|
|
24479
|
-
|
|
24480
|
-
|
|
24481
|
-
|
|
24482
|
-
|
|
24483
|
-
|
|
24484
|
-
|
|
24485
|
-
|
|
24486
|
-
|
|
24487
|
-
|
|
24488
|
-
|
|
24489
|
-
|
|
24490
|
-
|
|
24491
|
-
|
|
24492
|
-
|
|
24493
|
-
|
|
24494
|
-
|
|
24495
|
-
|
|
24496
|
-
|
|
24497
|
-
|
|
24498
|
-
|
|
24499
|
-
|
|
24500
|
-
|
|
24501
|
-
|
|
24502
|
-
|
|
24503
|
-
|
|
24504
|
-
|
|
24505
|
-
|
|
24506
|
-
|
|
24507
|
-
|
|
24508
|
-
|
|
24509
|
-
|
|
24510
|
-
|
|
24511
|
-
|
|
24512
|
-
|
|
24513
|
-
|
|
24514
|
-
|
|
24515
|
-
|
|
24516
|
-
|
|
24517
|
-
|
|
24518
|
-
|
|
24519
|
-
|
|
24520
|
-
|
|
24521
|
-
|
|
24522
|
-
|
|
24523
|
-
|
|
24524
|
-
|
|
24525
|
-
|
|
24526
|
-
|
|
24527
|
-
|
|
24528
|
-
|
|
24529
|
-
|
|
24530
|
-
|
|
24531
|
-
|
|
24532
|
-
|
|
24533
|
-
|
|
24534
|
-
|
|
24535
|
-
|
|
24536
|
-
|
|
24537
|
-
|
|
24538
|
-
|
|
24539
|
-
|
|
24540
|
-
|
|
24541
|
-
|
|
24542
|
-
|
|
24543
|
-
|
|
24544
|
-
|
|
24545
|
-
|
|
24546
|
-
|
|
24547
|
-
|
|
24548
|
-
|
|
24549
|
-
|
|
24550
|
-
|
|
24551
|
-
|
|
24552
|
-
|
|
24553
|
-
|
|
24554
|
-
|
|
24555
|
-
|
|
24556
|
-
|
|
24557
|
-
|
|
24558
|
-
|
|
24559
|
-
|
|
24560
|
-
|
|
24561
|
-
|
|
24768
|
+
const subject = origin.kind === 'agent' ? origin.agentName : call.callerAgentName ?? '__ephemeral__';
|
|
24769
|
+
const route = this.toolRoute(call, call.callerAgentName);
|
|
24770
|
+
switch (route.kind) {
|
|
24771
|
+
case 'presentation':
|
|
24772
|
+
return this.editToolPresentation(call.callerAgentName ?? '__ephemeral__', call);
|
|
24773
|
+
// toolsets--act on a host toolset's tool (the built-in one, a server's
|
|
24774
|
+
// tab) goes where a programmatic call of that tool goes, under its name.
|
|
24775
|
+
case 'host-act':
|
|
24776
|
+
if (route.host.kind === 'refused')
|
|
24777
|
+
return failed({ success: false, error: route.host.error, isError: true });
|
|
24778
|
+
return this.executeToolCallFrom({ ...call, name: route.host.name, input: route.host.args }, origin);
|
|
24779
|
+
// RFC-014: toolsets--act, resolved to the dynamic tool it calls, goes
|
|
24780
|
+
// where a programmatic call of that tool goes, with its instance.
|
|
24781
|
+
case 'toolset-act': {
|
|
24782
|
+
const target = this.resolveToolsetAct(call);
|
|
24783
|
+
if (!target.ok)
|
|
24784
|
+
return failed({ success: false, error: target.error, isError: true }, this.everyServerRedactor()(target.error));
|
|
24785
|
+
if (origin.kind === 'agent') {
|
|
24786
|
+
const failure = this.agentToolPermissionFailure(origin.agentName, target.call, false);
|
|
24787
|
+
if (failure)
|
|
24788
|
+
return failed(failure, this.everyServerRedactor()(failure.error ?? ''));
|
|
24789
|
+
}
|
|
24790
|
+
return this.executeMcplToolCall(target.call, target.serverId, target.prefix);
|
|
24791
|
+
}
|
|
24792
|
+
case 'mcpl':
|
|
24793
|
+
return this.executeMcplToolCall(call, route.serverId, route.prefix);
|
|
24794
|
+
// Synthesized channel tools. This route was missing: when
|
|
24795
|
+
// subscription-GC moved off the retired MCPL unsubscribe tool onto the
|
|
24796
|
+
// generic channel_close (discord-mcpl b095a9f), module-originated
|
|
24797
|
+
// closes started failing downstream as "Invalid tool name format" —
|
|
24798
|
+
// the janitor had been silently broken since. `origin` is trusted
|
|
24799
|
+
// dispatch context established at the call boundary above, not
|
|
24800
|
+
// inferred from which method was used: only the ModuleRegistry closure
|
|
24801
|
+
// carries module provenance (see handleToolClose).
|
|
24802
|
+
case 'channel': {
|
|
24803
|
+
const turn = call.name === 'channel_publish' && call.callerAgentName ? this.callingTurnOf(call.callerAgentName, call) : undefined;
|
|
24804
|
+
const input = call.name === 'channel_publish' && call.callerAgentName
|
|
24805
|
+
? this.publishInputWithTurnLocus(call.input, call.callerAgentName, turn)
|
|
24806
|
+
: call.input;
|
|
24807
|
+
return this.channelRegistry.handleChannelToolCall(call.name, input, origin.kind === 'agent' && turn ? { ...origin, inTurn: true } : origin);
|
|
24808
|
+
}
|
|
24809
|
+
case 'agent-settings':
|
|
24810
|
+
return this.awaitedToolAnswer((deliver) => this.dispatchAgentSettingsToolCall(subject, call, deliver));
|
|
24811
|
+
case 'save-image':
|
|
24812
|
+
return this.awaitedToolAnswer((deliver) => this.dispatchSaveImageToolCall(subject, call, deliver));
|
|
24813
|
+
case 'read-image':
|
|
24814
|
+
return this.awaitedToolAnswer((deliver) => this.dispatchReadImageToolCall(subject, call, deliver));
|
|
24815
|
+
case 'fetch-reference':
|
|
24816
|
+
return this.awaitedToolAnswer((deliver) => this.dispatchFetchReferenceToolCall(subject, call, deliver));
|
|
24817
|
+
case 'gate-status':
|
|
24818
|
+
return this.awaitedToolAnswer((deliver) => this.dispatchGateToolCall(subject, call, deliver));
|
|
24819
|
+
// The tune-out tool, in process: the same body the dispatcher runs, its
|
|
24820
|
+
// answer awaited here (it can wait for the tune-out's transport).
|
|
24821
|
+
case 'tune-out': {
|
|
24822
|
+
const agentName = call.callerAgentName ?? this.primaryAgentName ?? '__ephemeral__';
|
|
24823
|
+
const input = (call.input ?? {});
|
|
24824
|
+
// The calling turn's own channels, as the channel tools read them here (origin.own).
|
|
24825
|
+
const own = origin.kind === 'agent' ? origin.own ?? [] : [];
|
|
24826
|
+
const turn = this.callingTurnOf(agentName, call);
|
|
24827
|
+
return call.name === 'focus' ? this.handleFocusTool(agentName, input, own, turn) : this.handleTuneOutTool(agentName, input, own, turn);
|
|
24828
|
+
}
|
|
24829
|
+
case 'subconscious': {
|
|
24830
|
+
const routed = this.readerToolCall(subject, call);
|
|
24831
|
+
return 'refused' in routed ? failed({ success: false, error: `${routed.refused}, so the call didn't run.`, isError: true }) : routed.result;
|
|
24832
|
+
}
|
|
24833
|
+
case 'sleep':
|
|
24834
|
+
return this.awaitedToolAnswer((deliver) => this.dispatchSleepToolCall(subject, call, deliver));
|
|
24835
|
+
// The heartbeat's tools, for callers that await a tool's result (an
|
|
24836
|
+
// in-process call, a puppet, a module's callTool). A module is trusted;
|
|
24837
|
+
// an agent is held to what dispatch holds it to.
|
|
24838
|
+
case 'heartbeat': {
|
|
24839
|
+
const refusal = origin.kind === 'module' ? null : this.heartbeatRefusal(call.callerAgentName ?? '__ephemeral__', call);
|
|
24840
|
+
return refusal ? { success: false, error: refusal, isError: true } : this.runHeartbeatTool(call);
|
|
24841
|
+
}
|
|
24842
|
+
case 'wake-rule':
|
|
24843
|
+
return this.awaitedToolAnswer((deliver) => this.dispatchWakeRuleToolCall(subject, call, deliver));
|
|
24844
|
+
case 'event-tags':
|
|
24845
|
+
return this.awaitedToolAnswer((deliver) => this.dispatchEventTagsToolCall(subject, call, deliver));
|
|
24846
|
+
case 'agent-verb':
|
|
24847
|
+
if (origin.kind === 'module')
|
|
24848
|
+
return failed(agentsOwnVerbRefusal(call.name));
|
|
24849
|
+
if (call.name === 'skip_reply') {
|
|
24850
|
+
return failed({
|
|
24851
|
+
success: false,
|
|
24852
|
+
error: "skip_reply ends the turn it's called in, and this call wasn't made in a turn, so nothing was done.",
|
|
24853
|
+
isError: true,
|
|
24854
|
+
});
|
|
24855
|
+
}
|
|
24856
|
+
return this.channelRegistry.handleChannelToolCall(call.name, call.input, origin);
|
|
24857
|
+
// Client-side programmatic tool calling for callers that await a tool's
|
|
24858
|
+
// result: the puppet and a host through executeToolCall, and a module
|
|
24859
|
+
// through callTool. Keyed by callerAgentName, so each agent's scripts are
|
|
24860
|
+
// its own.
|
|
24861
|
+
case 'code-execution':
|
|
24862
|
+
return this.runCodeExecution(call.callerAgentName ?? '__ephemeral__', call);
|
|
24863
|
+
// drafts: the calling resident's own held drafts. A puppeted call has
|
|
24864
|
+
// agent origin and acts on that resident's drafts, as the model's own
|
|
24865
|
+
// call does through dispatchToolCall, but it isn't made inside the
|
|
24866
|
+
// resident's turn, so it takes none of the turn's channels, engages none,
|
|
24867
|
+
// and isn't noted as the resident's speech (callingTurnOf). A module
|
|
24868
|
+
// holds no drafts.
|
|
24869
|
+
case 'drafts':
|
|
24870
|
+
if (origin.kind === 'module')
|
|
24871
|
+
return failed(agentsOwnVerbRefusal(call.name));
|
|
24872
|
+
return this.draftsToolResult(origin.agentName, call.input);
|
|
24873
|
+
case 'prose-help':
|
|
24874
|
+
return { success: true, data: PROSE_ROUTING_HELP };
|
|
24875
|
+
case 'module':
|
|
24876
|
+
// Module tools. The caller uses the result in process, so it gets the
|
|
24877
|
+
// module's own result, as the module returned it, less a refusal it
|
|
24878
|
+
// claims, which is the dispatcher's to say.
|
|
24879
|
+
return this.moduleRegistry.handleToolCall(call);
|
|
24562
24880
|
}
|
|
24563
|
-
return this.moduleRegistry.handleToolCall(call);
|
|
24564
24881
|
}
|
|
24565
24882
|
/**
|
|
24566
24883
|
* RFC-014: the calls toolsets--act resolved to a server's dynamic tool,
|
|
@@ -25288,7 +25605,7 @@ export class AgentFramework {
|
|
|
25288
25605
|
// this write's own, rather than a timer Node would cut to 1 ms.
|
|
25289
25606
|
if (!Number.isFinite(deadline))
|
|
25290
25607
|
deadline = Date.now() + this.withheldSpillDeadlineMs;
|
|
25291
|
-
if (this.
|
|
25608
|
+
if (this.guardSpillsClosed)
|
|
25292
25609
|
return null;
|
|
25293
25610
|
const place = this.toolResultFilePlace(label);
|
|
25294
25611
|
if (!place)
|
|
@@ -26341,6 +26658,71 @@ export class AgentFramework {
|
|
|
26341
26658
|
const refused = bareToolName(call.name) === 'skip_reply' && (!permitted || this.refusesSelfWake(call));
|
|
26342
26659
|
return { name: call.name, cause: permitted || refused ? silenceCauseOf([call]) : null, refused, permitted };
|
|
26343
26660
|
}
|
|
26661
|
+
toolRoute(call, actor) {
|
|
26662
|
+
const name = call.name;
|
|
26663
|
+
if (isPresentationTool(name))
|
|
26664
|
+
return { kind: 'presentation' };
|
|
26665
|
+
if (name === META_TOOL_NAMES.act) {
|
|
26666
|
+
const host = actor === undefined ? null : this.hostPresentation?.resolveAct(actor, call.input) ?? null;
|
|
26667
|
+
if (host)
|
|
26668
|
+
return { kind: 'host-act', host };
|
|
26669
|
+
if (this.toolsetHub && this.mcplServerRegistry)
|
|
26670
|
+
return { kind: 'toolset-act' };
|
|
26671
|
+
}
|
|
26672
|
+
const mcpl = this.mcplServerRegistry ? this.resolveMcplTool(name) : null;
|
|
26673
|
+
if (mcpl)
|
|
26674
|
+
return { kind: 'mcpl', serverId: mcpl[0], prefix: mcpl[1] };
|
|
26675
|
+
if (name.startsWith('channel_') && this.channelRegistry)
|
|
26676
|
+
return { kind: 'channel' };
|
|
26677
|
+
if (name === 'agent_settings')
|
|
26678
|
+
return { kind: 'agent-settings' };
|
|
26679
|
+
if (name === 'save_recent_image')
|
|
26680
|
+
return { kind: 'save-image' };
|
|
26681
|
+
if (name === 'read_image')
|
|
26682
|
+
return { kind: 'read-image' };
|
|
26683
|
+
if (name === 'fetch_reference')
|
|
26684
|
+
return { kind: 'fetch-reference' };
|
|
26685
|
+
if (name === 'gate_status' && this.eventGate)
|
|
26686
|
+
return { kind: 'gate-status' };
|
|
26687
|
+
if ((name === 'tune_out' || name === 'focus') && this.tuneOutCoordinator)
|
|
26688
|
+
return { kind: 'tune-out' };
|
|
26689
|
+
if (SUBCONSCIOUS_TOOL_NAMES.includes(name))
|
|
26690
|
+
return { kind: 'subconscious' };
|
|
26691
|
+
if ((name === 'sleep' || name === 'wake') && this.eventGate)
|
|
26692
|
+
return { kind: 'sleep' };
|
|
26693
|
+
if (this.heartbeat?.ownsTool(name))
|
|
26694
|
+
return { kind: 'heartbeat' };
|
|
26695
|
+
if ((name === 'wake_add_rule' || name === 'wake_remove_rule') && this.eventGate)
|
|
26696
|
+
return { kind: 'wake-rule' };
|
|
26697
|
+
if (name === 'event_tags' && this.eventGate)
|
|
26698
|
+
return { kind: 'event-tags' };
|
|
26699
|
+
if ((name === 'think' || name === 'journal' || name === 'skip_reply') && this.channelRegistry)
|
|
26700
|
+
return { kind: 'agent-verb' };
|
|
26701
|
+
if (name === CODE_EXECUTION_TOOL_NAME && this.codeExecution)
|
|
26702
|
+
return { kind: 'code-execution' };
|
|
26703
|
+
if (name === 'drafts' && this.channelRegistry)
|
|
26704
|
+
return { kind: 'drafts' };
|
|
26705
|
+
if (name === PROSE_HELP_TOOL.name)
|
|
26706
|
+
return { kind: 'prose-help' };
|
|
26707
|
+
return { kind: 'module' };
|
|
26708
|
+
}
|
|
26709
|
+
pushToolResult(agentName, call, moduleName) {
|
|
26710
|
+
return (result) => this.pushEvent({ type: 'tool-result', callId: call.id, toolName: call.name, agentName, moduleName, result });
|
|
26711
|
+
}
|
|
26712
|
+
awaitedToolAnswer(run) {
|
|
26713
|
+
return new Promise((resolve) => {
|
|
26714
|
+
try {
|
|
26715
|
+
run((result) => {
|
|
26716
|
+
const answer = { ...result };
|
|
26717
|
+
delete answer.refused;
|
|
26718
|
+
resolve(answer);
|
|
26719
|
+
});
|
|
26720
|
+
}
|
|
26721
|
+
catch (error) {
|
|
26722
|
+
resolve({ success: false, error: thrownText(error), isError: true });
|
|
26723
|
+
}
|
|
26724
|
+
});
|
|
26725
|
+
}
|
|
26344
26726
|
/**
|
|
26345
26727
|
* Dispatch a call the agent made. `turnId` is the turn it was made in
|
|
26346
26728
|
* (TurnRouting.turnId), as its caller knows it: a model's call, the turn
|
|
@@ -26392,200 +26774,190 @@ export class AgentFramework {
|
|
|
26392
26774
|
enrichedCall.callerTurnId = turnId;
|
|
26393
26775
|
else
|
|
26394
26776
|
delete enrichedCall.callerTurnId;
|
|
26395
|
-
|
|
26396
|
-
|
|
26397
|
-
|
|
26398
|
-
|
|
26399
|
-
|
|
26400
|
-
// toolsets--act on a host toolset's tool (the built-in one, a server's
|
|
26401
|
-
// tab): the call of that tool, under its own name, down the path a
|
|
26402
|
-
// direct call of it takes, permissions included.
|
|
26403
|
-
if (enrichedCall.name === META_TOOL_NAMES.act) {
|
|
26404
|
-
const host = this.hostPresentation?.resolveAct(agentName, enrichedCall.input) ?? null;
|
|
26405
|
-
if (host) {
|
|
26406
|
-
this.dispatchHostAct(agentName, call, host, turnId);
|
|
26777
|
+
const route = this.toolRoute(enrichedCall, agentName);
|
|
26778
|
+
switch (route.kind) {
|
|
26779
|
+
case 'presentation': {
|
|
26780
|
+
const result = this.editToolPresentation(agentName, enrichedCall);
|
|
26781
|
+
this.pushEvent({ type: 'tool-result', callId: call.id, toolName: call.name, agentName, moduleName: 'tool-presentation', result });
|
|
26407
26782
|
return;
|
|
26408
26783
|
}
|
|
26409
|
-
|
|
26410
|
-
|
|
26411
|
-
|
|
26412
|
-
|
|
26413
|
-
|
|
26414
|
-
|
|
26415
|
-
|
|
26416
|
-
|
|
26417
|
-
|
|
26418
|
-
|
|
26419
|
-
|
|
26420
|
-
|
|
26421
|
-
|
|
26422
|
-
|
|
26423
|
-
|
|
26424
|
-
|
|
26425
|
-
|
|
26426
|
-
|
|
26427
|
-
|
|
26428
|
-
|
|
26429
|
-
|
|
26430
|
-
|
|
26431
|
-
|
|
26432
|
-
|
|
26433
|
-
|
|
26434
|
-
|
|
26435
|
-
|
|
26436
|
-
|
|
26437
|
-
|
|
26438
|
-
|
|
26439
|
-
|
|
26440
|
-
|
|
26441
|
-
|
|
26442
|
-
|
|
26443
|
-
|
|
26444
|
-
|
|
26445
|
-
|
|
26446
|
-
|
|
26447
|
-
|
|
26448
|
-
|
|
26449
|
-
|
|
26450
|
-
|
|
26451
|
-
|
|
26452
|
-
|
|
26453
|
-
|
|
26454
|
-
|
|
26455
|
-
|
|
26456
|
-
|
|
26457
|
-
|
|
26458
|
-
|
|
26459
|
-
|
|
26460
|
-
|
|
26461
|
-
|
|
26462
|
-
|
|
26463
|
-
|
|
26464
|
-
|
|
26465
|
-
|
|
26466
|
-
|
|
26467
|
-
|
|
26468
|
-
|
|
26784
|
+
// toolsets--act on a host toolset's tool (the built-in one, a server's
|
|
26785
|
+
// tab): the call of that tool, under its own name, down the path a
|
|
26786
|
+
// direct call of it takes, permissions included.
|
|
26787
|
+
case 'host-act':
|
|
26788
|
+
this.dispatchHostAct(agentName, call, route.host, turnId);
|
|
26789
|
+
return;
|
|
26790
|
+
// RFC-014: toolsets--act calls a server's dynamic tool, down the same
|
|
26791
|
+
// path as a direct call of it (dispatchToolsetAct).
|
|
26792
|
+
case 'toolset-act': {
|
|
26793
|
+
const scriptWaiter = this.scriptToolWaiters.get(enrichedCall.id);
|
|
26794
|
+
if (scriptWaiter)
|
|
26795
|
+
scriptWaiter.route.toServer = true;
|
|
26796
|
+
this.dispatchToolsetAct(agentName, enrichedCall);
|
|
26797
|
+
return;
|
|
26798
|
+
}
|
|
26799
|
+
case 'mcpl': {
|
|
26800
|
+
// A script's call: its waiter keeps that it went to a server, for the
|
|
26801
|
+
// script lane's exit (scriptToolWaiters).
|
|
26802
|
+
const scriptWaiter = this.scriptToolWaiters.get(enrichedCall.id);
|
|
26803
|
+
if (scriptWaiter)
|
|
26804
|
+
scriptWaiter.route.toServer = true;
|
|
26805
|
+
this.dispatchMcplToolCall(agentName, enrichedCall, route.serverId, route.prefix);
|
|
26806
|
+
return;
|
|
26807
|
+
}
|
|
26808
|
+
case 'channel': {
|
|
26809
|
+
this.dispatchChannelToolCall(agentName, enrichedCall);
|
|
26810
|
+
return;
|
|
26811
|
+
}
|
|
26812
|
+
case 'agent-settings': {
|
|
26813
|
+
this.dispatchAgentSettingsToolCall(agentName, enrichedCall);
|
|
26814
|
+
return;
|
|
26815
|
+
}
|
|
26816
|
+
case 'save-image': {
|
|
26817
|
+
this.dispatchSaveImageToolCall(agentName, enrichedCall);
|
|
26818
|
+
return;
|
|
26819
|
+
}
|
|
26820
|
+
case 'read-image': {
|
|
26821
|
+
this.dispatchReadImageToolCall(agentName, enrichedCall);
|
|
26822
|
+
return;
|
|
26823
|
+
}
|
|
26824
|
+
case 'fetch-reference': {
|
|
26825
|
+
this.dispatchFetchReferenceToolCall(agentName, enrichedCall);
|
|
26826
|
+
return;
|
|
26827
|
+
}
|
|
26828
|
+
case 'gate-status': {
|
|
26829
|
+
this.dispatchGateToolCall(agentName, enrichedCall);
|
|
26830
|
+
return;
|
|
26831
|
+
}
|
|
26832
|
+
case 'tune-out': {
|
|
26833
|
+
this.dispatchTuneOutToolCall(agentName, enrichedCall);
|
|
26834
|
+
return;
|
|
26835
|
+
}
|
|
26836
|
+
case 'subconscious': {
|
|
26837
|
+
const routed = this.readerToolCall(agentName, enrichedCall);
|
|
26838
|
+
if ('refused' in routed) {
|
|
26839
|
+
const error = routed.refused;
|
|
26840
|
+
const result = this.refusedResult(agentName, call.id, error);
|
|
26841
|
+
this.emitTrace({ type: 'tool:failed', module: 'tune-out', tool: call.name, callId: call.id, error });
|
|
26842
|
+
this.pushEvent({
|
|
26843
|
+
type: 'tool-result', callId: call.id, toolName: call.name, agentName, moduleName: 'tune-out', result,
|
|
26844
|
+
});
|
|
26845
|
+
return;
|
|
26846
|
+
}
|
|
26847
|
+
void routed.result
|
|
26848
|
+
.then((result) => {
|
|
26849
|
+
// Through pushEvent, like every other tool's result, so one that
|
|
26850
|
+
// arrives after stop() is dropped rather than rejected unhandled.
|
|
26851
|
+
this.pushEvent({
|
|
26852
|
+
type: 'tool-result',
|
|
26853
|
+
callId: enrichedCall.id,
|
|
26854
|
+
toolName: enrichedCall.name,
|
|
26855
|
+
agentName,
|
|
26856
|
+
moduleName: 'tune-out',
|
|
26857
|
+
result,
|
|
26858
|
+
});
|
|
26469
26859
|
});
|
|
26470
26860
|
return;
|
|
26471
26861
|
}
|
|
26472
|
-
|
|
26473
|
-
.
|
|
26474
|
-
|
|
26475
|
-
|
|
26862
|
+
case 'sleep': {
|
|
26863
|
+
this.dispatchSleepToolCall(agentName, enrichedCall);
|
|
26864
|
+
return;
|
|
26865
|
+
}
|
|
26866
|
+
case 'heartbeat': {
|
|
26867
|
+
this.dispatchHeartbeatToolCall(agentName, enrichedCall);
|
|
26868
|
+
return;
|
|
26869
|
+
}
|
|
26870
|
+
case 'wake-rule': {
|
|
26871
|
+
this.dispatchWakeRuleToolCall(agentName, enrichedCall);
|
|
26872
|
+
return;
|
|
26873
|
+
}
|
|
26874
|
+
case 'event-tags': {
|
|
26875
|
+
this.dispatchEventTagsToolCall(agentName, enrichedCall);
|
|
26876
|
+
return;
|
|
26877
|
+
}
|
|
26878
|
+
case 'agent-verb': {
|
|
26879
|
+
// skip_reply(wake_in_seconds): arm a gate self-wake so "not replying
|
|
26880
|
+
// NOW" can also mean "back in a moment" — ends the turn, then wakes
|
|
26881
|
+
// the agent after N seconds unless something else wakes it first
|
|
26882
|
+
// (any turn start cancels the pending self-wake). Armed here because
|
|
26883
|
+
// the framework owns the EventGate; the registry only words the
|
|
26884
|
+
// confirmation. A wake the host can't set is refused, so the model
|
|
26885
|
+
// learns that its turn remains open and no wake was armed: any
|
|
26886
|
+
// wake_in_seconds without a gate, and one that isn't a number of
|
|
26887
|
+
// seconds with one.
|
|
26888
|
+
if (enrichedCall.name === 'skip_reply') {
|
|
26889
|
+
const error = this.skipReplyRefusal(enrichedCall);
|
|
26890
|
+
if (error !== undefined) {
|
|
26891
|
+
const wakeValue = enrichedCall.input?.wake_in_seconds;
|
|
26892
|
+
console.error(`[self-wake] agent=${agentName} wake_in_seconds=${logValue(wakeValue)} skip_reply refused: ${logDetail(this.eventGate ? "wake_in_seconds isn't a number of seconds" : 'No EventGate configured (FrameworkConfig.gate is unset)')}`);
|
|
26893
|
+
const result = this.refusedResult(agentName, enrichedCall.id, error);
|
|
26894
|
+
this.emitTrace({ type: 'tool:failed', module: 'channels', tool: enrichedCall.name, callId: enrichedCall.id, error });
|
|
26895
|
+
this.pushEvent({ type: 'tool-result', callId: enrichedCall.id, toolName: enrichedCall.name, agentName, moduleName: 'channels', result });
|
|
26896
|
+
return;
|
|
26897
|
+
}
|
|
26898
|
+
const skipping = this.callingTurnOf(agentName, enrichedCall);
|
|
26899
|
+
if (skipping) {
|
|
26900
|
+
skipping.privateEnd = true;
|
|
26901
|
+
delete skipping.unpostedSpeech;
|
|
26902
|
+
}
|
|
26903
|
+
const wake = readSkipReplyWake(enrichedCall.input);
|
|
26904
|
+
if (wake.kind === 'wake' && this.eventGate) {
|
|
26905
|
+
const armedAt = Date.now();
|
|
26906
|
+
const { inMs, armed } = this.eventGate.armSelfWake(agentName, wake.seconds, 'skip_reply');
|
|
26907
|
+
// The heartbeat's tick or reminder due before the timer: if it wakes
|
|
26908
|
+
// the agent, its turn cancels the timer, so the confirmation names
|
|
26909
|
+
// it and the time the self-wake would have left.
|
|
26910
|
+
const earlierWake = armed ? this.heartbeatWakeBefore(agentName, armedAt + inMs) : undefined;
|
|
26911
|
+
console.error(armed
|
|
26912
|
+
? `[self-wake] agent=${agentName} armed in ${Math.round(inMs / 1000)}s (skip_reply)`
|
|
26913
|
+
+ (earlierWake ? `; due sooner, in ~${earlierWake.inSeconds}s: ${logDetail(earlierWake.what)}` : '')
|
|
26914
|
+
: `[self-wake] agent=${agentName} not armed: the framework has stopped (skip_reply)`);
|
|
26915
|
+
this.dispatchChannelToolCall(agentName, enrichedCall, earlierWake);
|
|
26916
|
+
return;
|
|
26917
|
+
}
|
|
26918
|
+
}
|
|
26919
|
+
this.dispatchChannelToolCall(agentName, enrichedCall);
|
|
26920
|
+
return;
|
|
26921
|
+
}
|
|
26922
|
+
// Client-side programmatic tool calling: run a script that calls the
|
|
26923
|
+
// agent's other tools in-process (script-inner calls come back through
|
|
26924
|
+
// this very dispatcher with waiter-tracked call IDs).
|
|
26925
|
+
case 'code-execution': {
|
|
26926
|
+
this.dispatchCodeExecutionToolCall(agentName, enrichedCall);
|
|
26927
|
+
return;
|
|
26928
|
+
}
|
|
26929
|
+
// drafts: the resident's own held drafts (list/read/resend/dismiss).
|
|
26930
|
+
// Answered by the framework; resend publishes through the registry's
|
|
26931
|
+
// publish executor, so the result arrives asynchronously.
|
|
26932
|
+
case 'drafts': {
|
|
26933
|
+
const startedAt = Date.now();
|
|
26934
|
+
this.emitTrace({ type: 'tool:started', module: 'framework', tool: 'drafts', callId: enrichedCall.id, input: enrichedCall.input });
|
|
26935
|
+
void this.draftsToolResult(agentName, enrichedCall.input, this.callingTurnOf(agentName, enrichedCall), enrichedCall.id)
|
|
26936
|
+
.then((result) => {
|
|
26937
|
+
this.emitTrace({ type: 'tool:completed', module: 'framework', tool: 'drafts', callId: enrichedCall.id, durationMs: Date.now() - startedAt });
|
|
26938
|
+
this.pushEvent({ type: 'tool-result', callId: enrichedCall.id, toolName: enrichedCall.name, agentName, moduleName: 'framework', result });
|
|
26939
|
+
});
|
|
26940
|
+
return;
|
|
26941
|
+
}
|
|
26942
|
+
// prose_help: on-demand routing reference for explicit-prose agents.
|
|
26943
|
+
// Answered inline — the grammar is a tool RESULT (model-requested), never
|
|
26944
|
+
// ambient context (see PROSE_ROUTING_HELP / teach-by-bounce).
|
|
26945
|
+
case 'prose-help': {
|
|
26946
|
+
this.emitTrace({ type: 'tool:completed', module: 'framework', tool: 'prose_help', callId: enrichedCall.id, durationMs: 0 });
|
|
26476
26947
|
this.pushEvent({
|
|
26477
26948
|
type: 'tool-result',
|
|
26478
26949
|
callId: enrichedCall.id,
|
|
26479
26950
|
toolName: enrichedCall.name,
|
|
26480
26951
|
agentName,
|
|
26481
|
-
moduleName: '
|
|
26482
|
-
result,
|
|
26952
|
+
moduleName: 'framework',
|
|
26953
|
+
result: { success: true, data: PROSE_ROUTING_HELP },
|
|
26483
26954
|
});
|
|
26484
|
-
|
|
26485
|
-
return;
|
|
26486
|
-
}
|
|
26487
|
-
// Route sleep / wake tools
|
|
26488
|
-
if ((enrichedCall.name === 'sleep' || enrichedCall.name === 'wake') && this.eventGate) {
|
|
26489
|
-
this.dispatchSleepToolCall(agentName, enrichedCall);
|
|
26490
|
-
return;
|
|
26491
|
-
}
|
|
26492
|
-
// Route the heartbeat's tools (heartbeat--…)
|
|
26493
|
-
if (this.heartbeat?.ownsTool(enrichedCall.name)) {
|
|
26494
|
-
this.dispatchHeartbeatToolCall(agentName, enrichedCall);
|
|
26495
|
-
return;
|
|
26496
|
-
}
|
|
26497
|
-
// Route wake-rule tools (runtime gate.json policy add/remove)
|
|
26498
|
-
if ((enrichedCall.name === 'wake_add_rule' || enrichedCall.name === 'wake_remove_rule') && this.eventGate) {
|
|
26499
|
-
this.dispatchWakeRuleToolCall(agentName, enrichedCall);
|
|
26500
|
-
return;
|
|
26501
|
-
}
|
|
26502
|
-
// Route event_tags (tag/ontology discovery)
|
|
26503
|
-
if (enrichedCall.name === 'event_tags' && this.eventGate) {
|
|
26504
|
-
this.dispatchEventTagsToolCall(agentName, enrichedCall);
|
|
26505
|
-
return;
|
|
26506
|
-
}
|
|
26507
|
-
// Route synthesized 'think' (private reasoning), 'journal' (private
|
|
26508
|
-
// long-form notes) and 'skip_reply' (deliberate stay-silent) tools — handled by the channel registry like the other
|
|
26509
|
-
// synthesized channel tools, but they aren't `channel_`-prefixed so they
|
|
26510
|
-
// need an explicit route here.
|
|
26511
|
-
if ((enrichedCall.name === 'think' || enrichedCall.name === 'journal' || enrichedCall.name === 'skip_reply') && this.channelRegistry) {
|
|
26512
|
-
// skip_reply(wake_in_seconds): arm a gate self-wake so "not replying
|
|
26513
|
-
// NOW" can also mean "back in a moment" — ends the turn, then wakes
|
|
26514
|
-
// the agent after N seconds unless something else wakes it first
|
|
26515
|
-
// (any turn start cancels the pending self-wake). Armed here because
|
|
26516
|
-
// the framework owns the EventGate; the registry only words the
|
|
26517
|
-
// confirmation. A wake the host can't set is refused, so the model
|
|
26518
|
-
// learns that its turn remains open and no wake was armed: any
|
|
26519
|
-
// wake_in_seconds without a gate, and one that isn't a number of
|
|
26520
|
-
// seconds with one.
|
|
26521
|
-
if (enrichedCall.name === 'skip_reply') {
|
|
26522
|
-
const error = this.skipReplyRefusal(enrichedCall);
|
|
26523
|
-
if (error !== undefined) {
|
|
26524
|
-
const wakeValue = enrichedCall.input?.wake_in_seconds;
|
|
26525
|
-
console.error(`[self-wake] agent=${agentName} wake_in_seconds=${logValue(wakeValue)} skip_reply refused: ${logDetail(this.eventGate ? "wake_in_seconds isn't a number of seconds" : 'No EventGate configured (FrameworkConfig.gate is unset)')}`);
|
|
26526
|
-
const result = this.refusedResult(agentName, enrichedCall.id, error);
|
|
26527
|
-
this.emitTrace({ type: 'tool:failed', module: 'channels', tool: enrichedCall.name, callId: enrichedCall.id, error });
|
|
26528
|
-
this.pushEvent({ type: 'tool-result', callId: enrichedCall.id, toolName: enrichedCall.name, agentName, moduleName: 'channels', result });
|
|
26529
|
-
return;
|
|
26530
|
-
}
|
|
26531
|
-
const skipping = this.callingTurnOf(agentName, enrichedCall);
|
|
26532
|
-
if (skipping) {
|
|
26533
|
-
skipping.privateEnd = true;
|
|
26534
|
-
delete skipping.unpostedSpeech;
|
|
26535
|
-
}
|
|
26536
|
-
const wake = readSkipReplyWake(enrichedCall.input);
|
|
26537
|
-
if (wake.kind === 'wake' && this.eventGate) {
|
|
26538
|
-
const armedAt = Date.now();
|
|
26539
|
-
const { inMs, armed } = this.eventGate.armSelfWake(agentName, wake.seconds, 'skip_reply');
|
|
26540
|
-
// The heartbeat's tick or reminder due before the timer: if it wakes
|
|
26541
|
-
// the agent, its turn cancels the timer, so the confirmation names
|
|
26542
|
-
// it and the time the self-wake would have left.
|
|
26543
|
-
const earlierWake = armed ? this.heartbeatWakeBefore(agentName, armedAt + inMs) : undefined;
|
|
26544
|
-
console.error(armed
|
|
26545
|
-
? `[self-wake] agent=${agentName} armed in ${Math.round(inMs / 1000)}s (skip_reply)`
|
|
26546
|
-
+ (earlierWake ? `; due sooner, in ~${earlierWake.inSeconds}s: ${logDetail(earlierWake.what)}` : '')
|
|
26547
|
-
: `[self-wake] agent=${agentName} not armed: the framework has stopped (skip_reply)`);
|
|
26548
|
-
this.dispatchChannelToolCall(agentName, enrichedCall, earlierWake);
|
|
26549
|
-
return;
|
|
26550
|
-
}
|
|
26955
|
+
return;
|
|
26551
26956
|
}
|
|
26552
|
-
|
|
26553
|
-
|
|
26554
|
-
|
|
26555
|
-
|
|
26556
|
-
// agent's other tools in-process (script-inner calls come back through
|
|
26557
|
-
// this very dispatcher with waiter-tracked call IDs).
|
|
26558
|
-
if (enrichedCall.name === CODE_EXECUTION_TOOL_NAME && this.codeExecution) {
|
|
26559
|
-
this.dispatchCodeExecutionToolCall(agentName, enrichedCall);
|
|
26560
|
-
return;
|
|
26561
|
-
}
|
|
26562
|
-
// drafts: the resident's own held drafts (list/read/resend/dismiss).
|
|
26563
|
-
// Answered by the framework; resend publishes through the registry's
|
|
26564
|
-
// publish executor, so the result arrives asynchronously.
|
|
26565
|
-
if (enrichedCall.name === 'drafts' && this.channelRegistry) {
|
|
26566
|
-
const startedAt = Date.now();
|
|
26567
|
-
this.emitTrace({ type: 'tool:started', module: 'framework', tool: 'drafts', callId: enrichedCall.id, input: enrichedCall.input });
|
|
26568
|
-
void this.draftsToolResult(agentName, enrichedCall.input, this.callingTurnOf(agentName, enrichedCall), enrichedCall.id)
|
|
26569
|
-
.then((result) => {
|
|
26570
|
-
this.emitTrace({ type: 'tool:completed', module: 'framework', tool: 'drafts', callId: enrichedCall.id, durationMs: Date.now() - startedAt });
|
|
26571
|
-
this.pushEvent({ type: 'tool-result', callId: enrichedCall.id, toolName: enrichedCall.name, agentName, moduleName: 'framework', result });
|
|
26572
|
-
});
|
|
26573
|
-
return;
|
|
26574
|
-
}
|
|
26575
|
-
// prose_help: on-demand routing reference for explicit-prose agents.
|
|
26576
|
-
// Answered inline — the grammar is a tool RESULT (model-requested), never
|
|
26577
|
-
// ambient context (see PROSE_ROUTING_HELP / teach-by-bounce).
|
|
26578
|
-
if (enrichedCall.name === 'prose_help') {
|
|
26579
|
-
this.emitTrace({ type: 'tool:completed', module: 'framework', tool: 'prose_help', callId: enrichedCall.id, durationMs: 0 });
|
|
26580
|
-
this.pushEvent({
|
|
26581
|
-
type: 'tool-result',
|
|
26582
|
-
callId: enrichedCall.id,
|
|
26583
|
-
toolName: enrichedCall.name,
|
|
26584
|
-
agentName,
|
|
26585
|
-
moduleName: 'framework',
|
|
26586
|
-
result: { success: true, data: PROSE_ROUTING_HELP },
|
|
26587
|
-
});
|
|
26588
|
-
return;
|
|
26957
|
+
case 'module':
|
|
26958
|
+
break;
|
|
26959
|
+
default:
|
|
26960
|
+
route;
|
|
26589
26961
|
}
|
|
26590
26962
|
const sepIndex = enrichedCall.name.indexOf('--');
|
|
26591
26963
|
const moduleName = sepIndex >= 0 ? enrichedCall.name.substring(0, sepIndex) : 'unknown';
|
|
@@ -26936,6 +27308,7 @@ export class AgentFramework {
|
|
|
26936
27308
|
const agent = this.agents.get(agentName);
|
|
26937
27309
|
if ((agent && !this.agentSees(agent, metadata)) || isOwnScheduleWrite(metadata) || !isConversationalInjection(metadata))
|
|
26938
27310
|
return;
|
|
27311
|
+
this.rearmDeadlineCuts?.delete(agentName);
|
|
26939
27312
|
if (this.turnRearms?.delete(agentName))
|
|
26940
27313
|
console.error(`[inference-rearm] agent=${agentName} re-send count started again: a message was said to it`);
|
|
26941
27314
|
}
|
|
@@ -27656,11 +28029,9 @@ export class AgentFramework {
|
|
|
27656
28029
|
* at a tool boundary whose stream declares it carries the messages
|
|
27657
28030
|
* injected there (markContextConsumed). So a notice such a stream carried
|
|
27658
28031
|
* is under it. One a boundary stored without sending (a turn a tool ended,
|
|
27659
|
-
* a budget restart
|
|
27660
|
-
*
|
|
27661
|
-
*
|
|
27662
|
-
* rather than to no one where the stream dropped it. null when the window
|
|
27663
|
-
* can't be read, which is logged.
|
|
28032
|
+
* a budget restart, the direct API's boundary, which has no stream) is
|
|
28033
|
+
* above it until a compile sends it, and counts as unread. null when the
|
|
28034
|
+
* window can't be read, which is logged.
|
|
27664
28035
|
*/
|
|
27665
28036
|
releasedWindow(agent, held) {
|
|
27666
28037
|
try {
|
|
@@ -28765,6 +29136,7 @@ export class AgentFramework {
|
|
|
28765
29136
|
const ended = this.exhaustedTurns?.get(agentName);
|
|
28766
29137
|
this.exhaustedTurns?.delete(agentName);
|
|
28767
29138
|
const rearmed = this.exhaustingRearms?.delete(agentName) === true;
|
|
29139
|
+
const madeAgain = this.freshCallsCut?.delete(agentName) === true;
|
|
28768
29140
|
// The reason is the failure's own message, and a provider can echo the
|
|
28769
29141
|
// whole rejected request in it: one household's [inference-failed]
|
|
28770
29142
|
// marker carried ~1.7 MB of its own request back into its context. Every
|
|
@@ -28815,24 +29187,25 @@ export class AgentFramework {
|
|
|
28815
29187
|
// (2) Agent-facing chronicle marker (no inference triggered → no loop).
|
|
28816
29188
|
const agent = this.agents.get(agentName);
|
|
28817
29189
|
if (agent && !(rearmed && !ended) && process.env.SUPPRESS_INFERENCE_FAILED_MARKER !== '1') {
|
|
28818
|
-
const
|
|
28819
|
-
|
|
28820
|
-
.
|
|
29190
|
+
const timedOut = errorType === 'timeout';
|
|
29191
|
+
const causeParts = [errorType, httpStatus !== undefined ? `HTTP ${httpStatus}` : undefined]
|
|
29192
|
+
.filter((part) => part !== undefined);
|
|
29193
|
+
const cause = timedOut && causeParts.length === 1 ? '' : causeParts.join(', ');
|
|
28821
29194
|
try {
|
|
28822
29195
|
agent.getContextManager().addMessage('user', [{
|
|
28823
29196
|
type: 'text',
|
|
28824
29197
|
text: (ended
|
|
28825
|
-
? `[${NOTICE_TAG.inferenceFailed}] Your previous turn did not complete:
|
|
28826
|
-
|
|
29198
|
+
? `[${NOTICE_TAG.inferenceFailed}] Your previous turn did not complete: ` +
|
|
29199
|
+
`${timedOut ? 'its last call timed out' : 'it failed'}${cause ? ` (${suppliedValue(cause)})` : ''}.` +
|
|
29200
|
+
`${madeAgain ? ` ${MADE_AGAIN}` : ''}` +
|
|
28827
29201
|
`${ended.roundsRan ? ' Its earlier rounds ran, and their tool calls ran as their results show.' : ''}` +
|
|
28828
29202
|
`${ended.callsCutOff ? ` ${CALLS_CUT_OFF}` : ''}` +
|
|
28829
29203
|
`${ended.liveSpeech ? ` ${LIVE_SPEECH_UNRECEIPTED}` : ''}` +
|
|
28830
29204
|
`${ended.speechRouting ? ` ${SPEECH_STILL_ROUTING}` : ''} Reason: `
|
|
28831
|
-
: `[${NOTICE_TAG.inferenceFailed}] Your previous turn did not complete:
|
|
28832
|
-
`call failed${cause ? ` (${suppliedValue(cause)})` : ''}
|
|
28833
|
-
|
|
28834
|
-
|
|
28835
|
-
`or an unsupported setting).`,
|
|
29205
|
+
: `[${NOTICE_TAG.inferenceFailed}] Your previous turn did not complete: ` +
|
|
29206
|
+
`the model call ${timedOut ? 'timed out' : 'failed'}${cause ? ` (${suppliedValue(cause)})` : ''} ` +
|
|
29207
|
+
`and produced no response, so nothing was sent.${madeAgain ? ` ${MADE_AGAIN}` : ''} Reason: `) +
|
|
29208
|
+
`${failureShown(markerReason)}. ${timedOut ? TIMEOUT_ADVICE : FAILURE_ADVICE}`,
|
|
28836
29209
|
}], { system: true, kind: 'inference-failed', reason: markerReason, consecutive: streak, ...classification, ...cutAt(markerReason) });
|
|
28837
29210
|
}
|
|
28838
29211
|
catch (err) {
|
|
@@ -29043,7 +29416,7 @@ export class AgentFramework {
|
|
|
29043
29416
|
return source;
|
|
29044
29417
|
});
|
|
29045
29418
|
// Server-initiated inference router (Step 6)
|
|
29046
|
-
this.inferenceRouter = new InferenceRouter(this.membrane, this.hookOrchestrator, this.featureSetManager, inferenceRouting ?? null, (event) => this.emitTrace(event));
|
|
29419
|
+
this.inferenceRouter = new InferenceRouter(this.membrane, this.hookOrchestrator, this.featureSetManager, inferenceRouting ?? null, (event) => this.emitTrace(event), () => this.inferenceBounds);
|
|
29047
29420
|
// Checkpoint manager (Step 8), built and loaded at the top
|
|
29048
29421
|
this.checkpointManager = checkpoints;
|
|
29049
29422
|
// Channel registry (Step 7)
|
|
@@ -29076,7 +29449,15 @@ export class AgentFramework {
|
|
|
29076
29449
|
const scope = coordinator.getAllExcept();
|
|
29077
29450
|
if (!scope || scope.params.except.serverId !== serverId || scope.params.except.channelId !== channelId)
|
|
29078
29451
|
return undefined;
|
|
29079
|
-
|
|
29452
|
+
let released;
|
|
29453
|
+
try {
|
|
29454
|
+
released = coordinator.cancelAllExcept('channel-close', 'the excepted channel was closed');
|
|
29455
|
+
}
|
|
29456
|
+
catch (err) {
|
|
29457
|
+
throw new Error(`The close wasn't recorded: ${err instanceof Error ? err.message : String(err)}`);
|
|
29458
|
+
}
|
|
29459
|
+
if (!released.ok)
|
|
29460
|
+
throw new Error(`The close wasn't recorded: ${released.error}`);
|
|
29080
29461
|
return 'The focus on it ended all the same, and what the other channels held was delivered.';
|
|
29081
29462
|
},
|
|
29082
29463
|
focusChannelResolver: () => this.tuneOutCoordinator?.focusChannel() ?? null,
|
|
@@ -29248,13 +29629,11 @@ export class AgentFramework {
|
|
|
29248
29629
|
attempt: 0,
|
|
29249
29630
|
willRetry: !!left?.willReconnect,
|
|
29250
29631
|
});
|
|
29251
|
-
// So the server doesn't vanish quietly:
|
|
29252
|
-
//
|
|
29253
|
-
//
|
|
29254
|
-
|
|
29255
|
-
|
|
29256
|
-
if (error instanceof McplProtocolVersionError || left?.holdsUnreapedChild) {
|
|
29257
|
-
this.opsAlert('mcpl-down', config.id, `MCPL server unreachable at startup: ${message}`);
|
|
29632
|
+
// So the server doesn't vanish quietly: nothing connects it again on
|
|
29633
|
+
// its own, so alert at once. One that connected before its setup
|
|
29634
|
+
// threw is still connected, and gets no alert.
|
|
29635
|
+
if (!left?.willReconnect && !left?.isConnected) {
|
|
29636
|
+
this.opsAlert('mcpl-down', config.id, `MCPL server not connected at startup, and this host won't retry it on its own: ${message}`);
|
|
29258
29637
|
}
|
|
29259
29638
|
}
|
|
29260
29639
|
}
|
|
@@ -31068,9 +31447,10 @@ export class AgentFramework {
|
|
|
31068
31447
|
}
|
|
31069
31448
|
/**
|
|
31070
31449
|
* Restart an MCPL server at runtime: disconnect, then reconnect with the
|
|
31071
|
-
* same (or an updated) config. This
|
|
31072
|
-
*
|
|
31073
|
-
*
|
|
31450
|
+
* same (or an updated) config. This respawns a stdio child at once,
|
|
31451
|
+
* whatever its state; `reconnect: true` spawns a fresh child only after
|
|
31452
|
+
* the connection fails or its child exits. Throws if the server has no
|
|
31453
|
+
* stored config and none is given.
|
|
31074
31454
|
* What its connect would refuse is refused before the running server is
|
|
31075
31455
|
* torn down. Until the connect, a connect or a restart started meanwhile
|
|
31076
31456
|
* is refused the id and the tool prefix the restart moves the server to,
|
|
@@ -31470,9 +31850,9 @@ export class AgentFramework {
|
|
|
31470
31850
|
// request turned away at stop gets, so it can send it again to
|
|
31471
31851
|
// the host's next run. Optional chaining: prototype-built
|
|
31472
31852
|
// harnesses leave the queue unset.
|
|
31473
|
-
const
|
|
31853
|
+
const answer = this.queue?.isClosed ? hostStoppedWhileHandling() : { code: -32603, message: 'Internal error' };
|
|
31474
31854
|
try {
|
|
31475
|
-
tracked.respondError?.(
|
|
31855
|
+
tracked.respondError?.(answer.code, answer.message);
|
|
31476
31856
|
}
|
|
31477
31857
|
catch (sendError) {
|
|
31478
31858
|
responseError = recorded(describeCause(sendError));
|
|
@@ -31638,6 +32018,7 @@ export class AgentFramework {
|
|
|
31638
32018
|
// Chunks go out on the transport the request came on, as its
|
|
31639
32019
|
// answer does.
|
|
31640
32020
|
...(responder.sendChunk ? { sendChunk: responder.sendChunk } : {}),
|
|
32021
|
+
...(responder.signal ? { signal: responder.signal } : {}),
|
|
31641
32022
|
});
|
|
31642
32023
|
}
|
|
31643
32024
|
});
|
|
@@ -31905,14 +32286,15 @@ export class AgentFramework {
|
|
|
31905
32286
|
// that next one (see refreshMcplTools).
|
|
31906
32287
|
this.noteMcplServerClosed(connection.id);
|
|
31907
32288
|
this.dropMcplToolClasses(connection.id);
|
|
32289
|
+
// The transport's own words only: a WebSocket server's close text
|
|
32290
|
+
// stays off the trace.
|
|
32291
|
+
const reason = info?.reason !== undefined && !info.reasonFromPeer ? boundFailureText(info.reason) : null;
|
|
31908
32292
|
this.emitTrace({
|
|
31909
32293
|
type: 'mcpl:server-closed',
|
|
31910
32294
|
serverId: connection.id,
|
|
31911
32295
|
code: code ?? null,
|
|
31912
32296
|
signal: signal ?? null,
|
|
31913
|
-
|
|
31914
|
-
// stays off the trace.
|
|
31915
|
-
reason: info?.reason !== undefined && !info.reasonFromPeer ? boundFailureText(info.reason) : null,
|
|
32297
|
+
reason,
|
|
31916
32298
|
willReconnect: connection.willReconnect,
|
|
31917
32299
|
});
|
|
31918
32300
|
this.emitTrace({ type: 'module:removed', moduleName: `mcpl:${connection.id}` });
|
|
@@ -31922,6 +32304,14 @@ export class AgentFramework {
|
|
|
31922
32304
|
// RFC-014: its toolsets go with it; a next connection reads them again.
|
|
31923
32305
|
this.toolsetHub?.get(connection.id)?.detach(connection);
|
|
31924
32306
|
this.handleToolsListChanged(connection.id);
|
|
32307
|
+
// Lost on its own with no reconnect coming: nothing in this host
|
|
32308
|
+
// connects it again on its own. A close the host made itself (stop(),
|
|
32309
|
+
// a disconnect, a restart) is byHost, a loss once stop() has begun is
|
|
32310
|
+
// part of the close the host is making, and a child that ended as
|
|
32311
|
+
// asked was stopped: none of them gets an alert.
|
|
32312
|
+
if (!info?.byHost && !this.stopHasBegun() && !endedAsAsked({ code, signal, pipeFailed: info?.pipeFailed })) {
|
|
32313
|
+
this.opsAlert('mcpl-down', connection.id, `MCPL server closed${closeWords(reason, code, signal)}, and this host won't reconnect it on its own`);
|
|
32314
|
+
}
|
|
31925
32315
|
}
|
|
31926
32316
|
});
|
|
31927
32317
|
}
|
|
@@ -31951,6 +32341,7 @@ export class AgentFramework {
|
|
|
31951
32341
|
type: 'mcpl:server-connect-failed',
|
|
31952
32342
|
serverId: connection.id,
|
|
31953
32343
|
error: recorded(params.error),
|
|
32344
|
+
...(params.stderrTail === undefined ? {} : { stderrTail: recorded(params.stderrTail) }),
|
|
31954
32345
|
attempt: params.attempt,
|
|
31955
32346
|
willRetry: connection.willReconnect,
|
|
31956
32347
|
});
|
|
@@ -31969,7 +32360,7 @@ export class AgentFramework {
|
|
|
31969
32360
|
connection.on('error', (err) => {
|
|
31970
32361
|
this.emitTrace({ type: 'mcpl:server-error', serverId: connection.id, error: recorded(err.message) });
|
|
31971
32362
|
});
|
|
31972
|
-
connection.on('close', () => {
|
|
32363
|
+
connection.on('close', (info) => {
|
|
31973
32364
|
// A closed provider's class hints are stale (RFC-008), as for MCPL,
|
|
31974
32365
|
// and so is a listing already under way.
|
|
31975
32366
|
this.noteMcplServerClosed(connection.id);
|
|
@@ -31977,8 +32368,9 @@ export class AgentFramework {
|
|
|
31977
32368
|
this.emitTrace({
|
|
31978
32369
|
type: 'mcpl:server-closed',
|
|
31979
32370
|
serverId: connection.id,
|
|
31980
|
-
code: null,
|
|
31981
|
-
signal: null,
|
|
32371
|
+
code: info.code ?? null,
|
|
32372
|
+
signal: info.signal ?? null,
|
|
32373
|
+
reason: info.reason,
|
|
31982
32374
|
willReconnect: connection.willReconnect,
|
|
31983
32375
|
});
|
|
31984
32376
|
this.emitTrace({ type: 'module:removed', moduleName: `mcpl:${connection.id}` });
|
|
@@ -31999,6 +32391,13 @@ export class AgentFramework {
|
|
|
31999
32391
|
// Gone for good: its tools leave the agents' list, as a
|
|
32000
32392
|
// disconnect's do.
|
|
32001
32393
|
this.handleToolsListChanged(connection.id);
|
|
32394
|
+
// Lost on its own: nothing in this host connects it again on its
|
|
32395
|
+
// own. A loss once stop() has begun is part of the close the host
|
|
32396
|
+
// is making, and a child that ended as asked was stopped: neither
|
|
32397
|
+
// gets an alert.
|
|
32398
|
+
if (!this.stopHasBegun() && !endedAsAsked(info)) {
|
|
32399
|
+
this.opsAlert('mcpl-down', connection.id, `MCP server closed${closeWords(info.reason, info.code, info.signal)}, and this host won't reconnect it on its own`);
|
|
32400
|
+
}
|
|
32002
32401
|
});
|
|
32003
32402
|
}
|
|
32004
32403
|
});
|
|
@@ -33132,7 +33531,7 @@ export class AgentFramework {
|
|
|
33132
33531
|
* (same path as gate.json load), so an invalid rule is surfaced as a tool
|
|
33133
33532
|
* error and nothing is written.
|
|
33134
33533
|
*/
|
|
33135
|
-
dispatchWakeRuleToolCall(agentName, call) {
|
|
33534
|
+
dispatchWakeRuleToolCall(agentName, call, deliver = this.pushToolResult(agentName, call, 'gate')) {
|
|
33136
33535
|
this.emitTrace({ type: 'tool:started', module: 'gate', tool: call.name, callId: call.id, input: call.input });
|
|
33137
33536
|
// What wake_add_rule's input held that does nothing, checkToolInput's notes and
|
|
33138
33537
|
// then the gate's on a rule it saved: a refusal names it too, so the call that
|
|
@@ -33147,13 +33546,22 @@ export class AgentFramework {
|
|
|
33147
33546
|
module: 'gate', tool: call.name, callId: call.id, durationMs: 0,
|
|
33148
33547
|
...(result.isError ? { error: result.error } : {}),
|
|
33149
33548
|
});
|
|
33150
|
-
|
|
33549
|
+
deliver(result);
|
|
33151
33550
|
};
|
|
33152
33551
|
try {
|
|
33153
33552
|
if (call.name === 'wake_remove_rule') {
|
|
33154
33553
|
const input = (call.input ?? {});
|
|
33155
33554
|
if (typeof input.name !== 'string' || !input.name) {
|
|
33156
|
-
|
|
33555
|
+
const edit = this.eventGate.ruleNamesForChange();
|
|
33556
|
+
const names = 'names' in edit ? edit.names.map((name) => quotedValue(name)) : [];
|
|
33557
|
+
finish({
|
|
33558
|
+
success: false,
|
|
33559
|
+
error: `wake_remove_rule: name is ${shownValue(input.name)}, where the name of a rule is needed, so nothing was removed. `
|
|
33560
|
+
+ ('refused' in edit
|
|
33561
|
+
? edit.refused
|
|
33562
|
+
: (names.length > 0 ? `The rules a change edits are ${names.join(', ')}.` : 'There is no rule to remove.') + edit.note),
|
|
33563
|
+
isError: true,
|
|
33564
|
+
});
|
|
33157
33565
|
return;
|
|
33158
33566
|
}
|
|
33159
33567
|
const removed = this.removeGatePolicy(input.name);
|
|
@@ -33183,9 +33591,8 @@ export class AgentFramework {
|
|
|
33183
33591
|
if (behaviorSources.length === 0) {
|
|
33184
33592
|
finish({
|
|
33185
33593
|
success: false,
|
|
33186
|
-
error:
|
|
33187
|
-
? '
|
|
33188
|
-
: 'wake_add_rule: specify exactly one behavior — `behavior` ("always"/"defer"/"skip"), `debounceMs`, `rateLimit`, or `passiveSample`.',
|
|
33594
|
+
error: 'wake_add_rule got no behavior, so nothing was saved. Give one of behavior ("always", "defer" or "skip"), '
|
|
33595
|
+
+ (this.judgeWired ? 'debounceMs, rateLimit, passiveSample or judge.' : 'debounceMs, rateLimit or passiveSample.'),
|
|
33189
33596
|
isError: true,
|
|
33190
33597
|
});
|
|
33191
33598
|
return;
|
|
@@ -33193,7 +33600,7 @@ export class AgentFramework {
|
|
|
33193
33600
|
if (behaviorSources.length > 1) {
|
|
33194
33601
|
finish({
|
|
33195
33602
|
success: false,
|
|
33196
|
-
error: `wake_add_rule
|
|
33603
|
+
error: `wake_add_rule got more than one behavior (${behaviorSources.join(', ')}), so nothing was saved. Give only one of them.`,
|
|
33197
33604
|
isError: true,
|
|
33198
33605
|
});
|
|
33199
33606
|
return;
|
|
@@ -33234,7 +33641,7 @@ export class AgentFramework {
|
|
|
33234
33641
|
if (placementSources.length > 1) {
|
|
33235
33642
|
finish({
|
|
33236
33643
|
success: false,
|
|
33237
|
-
error: `wake_add_rule
|
|
33644
|
+
error: `wake_add_rule got more than one placement (${placementSources.join(', ')}), so nothing was saved. Give only one of them.`,
|
|
33238
33645
|
isError: true,
|
|
33239
33646
|
});
|
|
33240
33647
|
return;
|
|
@@ -33254,10 +33661,18 @@ export class AgentFramework {
|
|
|
33254
33661
|
for (const field of ['insertBefore', 'insertAfter']) {
|
|
33255
33662
|
const anchor = input[field];
|
|
33256
33663
|
if (anchor !== undefined && (typeof anchor !== 'string' || anchor.length === 0)) {
|
|
33664
|
+
const edit = this.eventGate.ruleNamesForChange();
|
|
33665
|
+
const names = 'names' in edit ? edit.names.filter((name) => name !== input.name).map((name) => quotedValue(name)) : [];
|
|
33257
33666
|
finish({
|
|
33258
33667
|
success: false,
|
|
33259
|
-
error: `wake_add_rule: ${field} must be the name of a rule, and it's ${shownValue(anchor)}, so nothing `
|
|
33260
|
-
+
|
|
33668
|
+
error: `wake_add_rule: ${field} must be the name of a rule, and it's ${shownValue(anchor)}, so nothing was saved. `
|
|
33669
|
+
+ ('refused' in edit
|
|
33670
|
+
? edit.refused
|
|
33671
|
+
: (names.length > 0
|
|
33672
|
+
? `The other rules a change edits are ${names.join(', ')}. Give ${field} one of them to place the rule beside it, or leave it out`
|
|
33673
|
+
: `There is no other rule to place it beside, so leave ${field} out`)
|
|
33674
|
+
+ ' to add the rule at the end, or, where a rule already has its name, to replace that rule where it is.'
|
|
33675
|
+
+ edit.note),
|
|
33261
33676
|
isError: true,
|
|
33262
33677
|
});
|
|
33263
33678
|
return;
|
|
@@ -33285,9 +33700,12 @@ export class AgentFramework {
|
|
|
33285
33700
|
// (af#105 — a prepended catch-all sampler silently disabled the
|
|
33286
33701
|
// addressed-message wakes for 12h before anyone could see why).
|
|
33287
33702
|
const before = new Map(this.eventGate.probeTable().map((r) => [r.probe, r]));
|
|
33288
|
-
const
|
|
33703
|
+
const fieldNames = Object.fromEntries(Object.entries(AgentFramework.WAKE_ADD_RULE_FIELDS)
|
|
33704
|
+
.filter(([field]) => !(field.startsWith('behavior.') && typeof input.behavior === 'object')));
|
|
33705
|
+
const policy = this.eventGate.addPolicy(rawPolicy, {
|
|
33289
33706
|
...(position ? { position } : {}),
|
|
33290
33707
|
onIgnoredFields: (notes) => { ignored.push(...notes); },
|
|
33708
|
+
fieldNames,
|
|
33291
33709
|
});
|
|
33292
33710
|
const after = this.eventGate.probeTable();
|
|
33293
33711
|
const probes = after.map((r) => {
|
|
@@ -33589,22 +34007,20 @@ export class AgentFramework {
|
|
|
33589
34007
|
});
|
|
33590
34008
|
return decision;
|
|
33591
34009
|
}
|
|
33592
|
-
/**
|
|
33593
|
-
* Make delivered messages durable now, including those the delivery left
|
|
33594
|
-
* in the deferred queue: a tune-out release flips durable epoch state
|
|
33595
|
-
* first, so its dump must not be lost to a crash in between (#232 gap 6).
|
|
33596
|
-
*/
|
|
33597
34010
|
commitDeliveries() {
|
|
34011
|
+
let durable = true;
|
|
33598
34012
|
if (this.deferredMessages.length > 0) {
|
|
33599
34013
|
this.deferredWritesPersisted = true;
|
|
33600
|
-
this.persistDeferredWrites();
|
|
34014
|
+
durable = this.persistDeferredWrites();
|
|
33601
34015
|
}
|
|
33602
34016
|
try {
|
|
33603
34017
|
this.store.sync();
|
|
33604
34018
|
}
|
|
33605
34019
|
catch (err) {
|
|
33606
34020
|
console.error('[tune-out] could not sync the store after a release:', err);
|
|
34021
|
+
return false;
|
|
33607
34022
|
}
|
|
34023
|
+
return durable;
|
|
33608
34024
|
}
|
|
33609
34025
|
/** The history-reading tool the primary resident actually has, if any (#232 gap 11). */
|
|
33610
34026
|
residentHistoryTool() {
|
|
@@ -33614,7 +34030,7 @@ export class AgentFramework {
|
|
|
33614
34030
|
const names = this.getAllTools().map((t) => t.name).filter((n) => primary.canUseTool(n));
|
|
33615
34031
|
return names.find((n) => /(^|--)(fetch_history|channel_read|read_history|history)$/.test(n)) ?? null;
|
|
33616
34032
|
}
|
|
33617
|
-
dispatchSleepToolCall(agentName, call) {
|
|
34033
|
+
dispatchSleepToolCall(agentName, call, deliver = this.pushToolResult(agentName, call, 'gate')) {
|
|
33618
34034
|
// The gate's sleep quiets the whole agent, every conversation of it, so a
|
|
33619
34035
|
// conversation fork or an ephemeral run neither sets one nor ends the
|
|
33620
34036
|
// agent's own. Refused before the tool runs, as a fork's channel_open is.
|
|
@@ -33633,7 +34049,7 @@ export class AgentFramework {
|
|
|
33633
34049
|
? `Nothing was set: a sleep quiets the whole agent, not one ${one}.${comeBack}`
|
|
33634
34050
|
: `Nothing was changed: a sleep is the agent's, so this ${one} can't end one.`;
|
|
33635
34051
|
this.emitTrace({ type: 'tool:failed', module: 'gate', tool: call.name, callId: call.id, error: `${agentName}: ${error}` });
|
|
33636
|
-
|
|
34052
|
+
deliver(this.refusedResult(agentName, call.id, error));
|
|
33637
34053
|
return;
|
|
33638
34054
|
}
|
|
33639
34055
|
this.emitTrace({ type: 'tool:started', module: 'gate', tool: call.name, callId: call.id, input: call.input });
|
|
@@ -33641,7 +34057,7 @@ export class AgentFramework {
|
|
|
33641
34057
|
const input = (call.input ?? {});
|
|
33642
34058
|
const finish = (result) => {
|
|
33643
34059
|
this.emitTrace({ type: 'tool:completed', module: 'gate', tool: call.name, callId: call.id, durationMs: 0 });
|
|
33644
|
-
|
|
34060
|
+
deliver(result);
|
|
33645
34061
|
};
|
|
33646
34062
|
if (call.name === 'wake') {
|
|
33647
34063
|
const was = gate.clearSleep();
|
|
@@ -33762,31 +34178,36 @@ export class AgentFramework {
|
|
|
33762
34178
|
*/
|
|
33763
34179
|
/** RFC-005 on-demand dereference: [ref id] → workspace file, via the
|
|
33764
34180
|
* origin-bound, ceiling-bounded, digest-verified host fetcher. */
|
|
33765
|
-
dispatchFetchReferenceToolCall(agentName, call) {
|
|
34181
|
+
dispatchFetchReferenceToolCall(agentName, call, deliver = this.pushToolResult(agentName, call, 'workspace')) {
|
|
33766
34182
|
this.emitTrace({ type: 'tool:started', module: 'workspace', tool: call.name, callId: call.id, input: call.input });
|
|
33767
34183
|
const startTime = Date.now();
|
|
33768
34184
|
void (async () => {
|
|
33769
|
-
const args = (call.input ?? {});
|
|
33770
|
-
const refId = String(args.ref_id ?? '');
|
|
33771
|
-
const record = referenceRegistry.get(refId);
|
|
33772
34185
|
let result;
|
|
33773
|
-
|
|
33774
|
-
|
|
33775
|
-
|
|
33776
|
-
|
|
33777
|
-
|
|
34186
|
+
try {
|
|
34187
|
+
const args = (call.input ?? {});
|
|
34188
|
+
const refId = String(args.ref_id ?? '');
|
|
34189
|
+
const record = referenceRegistry.get(refId);
|
|
34190
|
+
if (!record) {
|
|
34191
|
+
// Vector 20: a stale or unknown id is a defined miss, never a
|
|
34192
|
+
// different record.
|
|
34193
|
+
result = { success: false, isError: true,
|
|
34194
|
+
error: `unknown reference: ${refId || '(missing ref_id)'} — it may have been evicted; ids are never reused` };
|
|
34195
|
+
}
|
|
34196
|
+
else {
|
|
34197
|
+
const maxBytes = typeof args.max_bytes === 'number'
|
|
34198
|
+
? Math.min(args.max_bytes, DEFAULT_FETCH_MAX_BYTES) : undefined;
|
|
34199
|
+
const outcome = await this.getReferenceFetcher().fetch(record, { maxBytes });
|
|
34200
|
+
result = outcome.ok
|
|
34201
|
+
? { success: true, data: JSON.stringify({ path: outcome.path, bytes: outcome.bytes,
|
|
34202
|
+
mime_type: outcome.mimeType, digest_verified: outcome.digestVerified ?? 'no digest claimed' }) }
|
|
34203
|
+
// The fetcher says what happened as a clause about the reference,
|
|
34204
|
+
// and what calling again can do as a sentence of its own.
|
|
34205
|
+
: { success: false, isError: true,
|
|
34206
|
+
error: `fetch_reference couldn't fetch "${refId}": ${outcome.error}. ${outcome.remedy}` };
|
|
34207
|
+
}
|
|
33778
34208
|
}
|
|
33779
|
-
|
|
33780
|
-
|
|
33781
|
-
? Math.min(args.max_bytes, DEFAULT_FETCH_MAX_BYTES) : undefined;
|
|
33782
|
-
const outcome = await this.getReferenceFetcher().fetch(record, { maxBytes });
|
|
33783
|
-
result = outcome.ok
|
|
33784
|
-
? { success: true, data: JSON.stringify({ path: outcome.path, bytes: outcome.bytes,
|
|
33785
|
-
mime_type: outcome.mimeType, digest_verified: outcome.digestVerified ?? 'no digest claimed' }) }
|
|
33786
|
-
// The fetcher says what happened as a clause about the reference,
|
|
33787
|
-
// and what calling again can do as a sentence of its own.
|
|
33788
|
-
: { success: false, isError: true,
|
|
33789
|
-
error: `fetch_reference couldn't fetch "${refId}": ${outcome.error}. ${outcome.remedy}` };
|
|
34209
|
+
catch (error) {
|
|
34210
|
+
result = { success: false, isError: true, error: `fetch_reference couldn't answer: ${thrownText(error)}` };
|
|
33790
34211
|
}
|
|
33791
34212
|
this.emitTrace({
|
|
33792
34213
|
type: result.isError ? 'tool:failed' : 'tool:completed',
|
|
@@ -33794,13 +34215,10 @@ export class AgentFramework {
|
|
|
33794
34215
|
durationMs: Date.now() - startTime,
|
|
33795
34216
|
...(result.isError ? { error: result.error } : {}),
|
|
33796
34217
|
});
|
|
33797
|
-
|
|
33798
|
-
type: 'tool-result', callId: call.id, toolName: call.name, agentName, moduleName: 'workspace',
|
|
33799
|
-
result: { ...result, isError: result.isError ?? false },
|
|
33800
|
-
});
|
|
34218
|
+
deliver({ ...result, isError: result.isError ?? false });
|
|
33801
34219
|
})();
|
|
33802
34220
|
}
|
|
33803
|
-
dispatchReadImageToolCall(agentName, call) {
|
|
34221
|
+
dispatchReadImageToolCall(agentName, call, deliver = this.pushToolResult(agentName, call, 'workspace')) {
|
|
33804
34222
|
this.emitTrace({ type: 'tool:started', module: 'workspace', tool: call.name, callId: call.id, input: call.input });
|
|
33805
34223
|
const finish = (result) => {
|
|
33806
34224
|
this.emitTrace({
|
|
@@ -33811,7 +34229,7 @@ export class AgentFramework {
|
|
|
33811
34229
|
durationMs: 0,
|
|
33812
34230
|
...(result.isError ? { error: result.error } : {}),
|
|
33813
34231
|
});
|
|
33814
|
-
|
|
34232
|
+
deliver(result);
|
|
33815
34233
|
};
|
|
33816
34234
|
void (async () => {
|
|
33817
34235
|
try {
|
|
@@ -33893,14 +34311,11 @@ export class AgentFramework {
|
|
|
33893
34311
|
* every sibling in the round to settle; if one never does, fail closed
|
|
33894
34312
|
* rather than trust an index the batch may still change. Sibling saves
|
|
33895
34313
|
* produce no images and are not waited on (two saves in one batch would
|
|
33896
|
-
* otherwise deadlock into a double timeout).
|
|
33897
|
-
*
|
|
33898
|
-
*
|
|
33899
|
-
* on itself.
|
|
34314
|
+
* otherwise deadlock into a double timeout). Only the round's own calls
|
|
34315
|
+
* wait: a call that isn't among its pending calls, a script's (`pytc-…`)
|
|
34316
|
+
* or an awaited call's, counts the images the agent has now.
|
|
33900
34317
|
*/
|
|
33901
34318
|
async awaitSiblingToolCalls(agent, call) {
|
|
33902
|
-
if (call.id.startsWith('pytc-'))
|
|
33903
|
-
return;
|
|
33904
34319
|
const BARRIER_MS = 120_000;
|
|
33905
34320
|
const POLL_MS = 20;
|
|
33906
34321
|
const startedAt = Date.now();
|
|
@@ -33909,6 +34324,8 @@ export class AgentFramework {
|
|
|
33909
34324
|
// Idle (puppet path) or the round was torn down: nothing to wait on.
|
|
33910
34325
|
if (state.status !== 'waiting_for_tools')
|
|
33911
34326
|
return;
|
|
34327
|
+
if (!state.pending.has(call.id))
|
|
34328
|
+
return;
|
|
33912
34329
|
const open = [...state.pending.values()].filter((p) => p.id !== call.id && p.name !== 'save_recent_image');
|
|
33913
34330
|
if (open.length === 0)
|
|
33914
34331
|
return;
|
|
@@ -33935,7 +34352,7 @@ export class AgentFramework {
|
|
|
33935
34352
|
* nothing. It never slides to an older image — a filename must not
|
|
33936
34353
|
* authenticate bytes from another surface.
|
|
33937
34354
|
*/
|
|
33938
|
-
dispatchSaveImageToolCall(agentName, call) {
|
|
34355
|
+
dispatchSaveImageToolCall(agentName, call, deliver = this.pushToolResult(agentName, call, 'workspace')) {
|
|
33939
34356
|
this.emitTrace({ type: 'tool:started', module: 'workspace', tool: call.name, callId: call.id, input: call.input });
|
|
33940
34357
|
const finish = (result) => {
|
|
33941
34358
|
this.emitTrace({
|
|
@@ -33946,7 +34363,7 @@ export class AgentFramework {
|
|
|
33946
34363
|
durationMs: 0,
|
|
33947
34364
|
...(result.isError ? { error: result.error } : {}),
|
|
33948
34365
|
});
|
|
33949
|
-
|
|
34366
|
+
deliver(result);
|
|
33950
34367
|
};
|
|
33951
34368
|
void (async () => {
|
|
33952
34369
|
try {
|
|
@@ -34057,21 +34474,36 @@ export class AgentFramework {
|
|
|
34057
34474
|
let count = 1;
|
|
34058
34475
|
/** Direct-ref mode: the one ref to find in the scannable context. */
|
|
34059
34476
|
let wantedRef = null;
|
|
34060
|
-
|
|
34477
|
+
// A selector is in use only when it carries a value (fieldInUse), so a
|
|
34478
|
+
// caller whose provider presents every property as required can still
|
|
34479
|
+
// choose one, whichever of the two it fills the other selector with.
|
|
34480
|
+
// Any other value is checked, never read as absent: a whitespace or
|
|
34481
|
+
// malformed ref, or an index that isn't an integer, is refused rather
|
|
34482
|
+
// than turned into a different image than the one the caller named.
|
|
34483
|
+
// A malformed ref beside an index or count in use says how to leave a
|
|
34484
|
+
// selector unused, since the caller may have meant the positional one, as
|
|
34485
|
+
// the conflict refusal below does for a well-formed ref. No refusal of one
|
|
34486
|
+
// selector on its own says it: a ref alone is the caller's one selector,
|
|
34487
|
+
// and dropping it would save the newest image; an index and a count are
|
|
34488
|
+
// one selector, and dropping either changes which images are saved.
|
|
34489
|
+
if (fieldInUse(input.ref)) {
|
|
34061
34490
|
if (typeof input.ref !== 'string' || !TOOL_IMAGE_REF_RE.test(input.ref)) {
|
|
34062
|
-
|
|
34491
|
+
const positionalInUse = fieldInUse(input.index) || fieldInUse(input.count);
|
|
34492
|
+
throw new Error('save_recent_image: `ref` must look like "img_k7x3q2_7" (copy it from the image placeholder in your context)' +
|
|
34493
|
+
(positionalInUse ? ' (pass null, or leave it out, for a selector you are not using)' : ''));
|
|
34063
34494
|
}
|
|
34064
|
-
if (input.index
|
|
34065
|
-
throw new Error('save_recent_image: `ref` is mutually exclusive with `index`/`count`'
|
|
34495
|
+
if (fieldInUse(input.index) || fieldInUse(input.count)) {
|
|
34496
|
+
throw new Error('save_recent_image: `ref` is mutually exclusive with `index`/`count` — pass null (or omit) ' +
|
|
34497
|
+
'for the selector you are not using');
|
|
34066
34498
|
}
|
|
34067
34499
|
wantedRef = input.ref;
|
|
34068
34500
|
}
|
|
34069
34501
|
else {
|
|
34070
|
-
index = input.index
|
|
34502
|
+
index = fieldInUse(input.index) ? integerField(input.index) : 0;
|
|
34071
34503
|
if (!Number.isInteger(index) || index < 0) {
|
|
34072
34504
|
throw new Error('save_recent_image: `index` must be a non-negative integer');
|
|
34073
34505
|
}
|
|
34074
|
-
count = input.count
|
|
34506
|
+
count = fieldInUse(input.count) ? integerField(input.count) : 1;
|
|
34075
34507
|
const MAX_COUNT = 20;
|
|
34076
34508
|
if (!Number.isInteger(count) || count < 1 || count > MAX_COUNT) {
|
|
34077
34509
|
throw new Error(`save_recent_image: \`count\` must be an integer in 1..${MAX_COUNT}`);
|
|
@@ -34314,7 +34746,7 @@ export class AgentFramework {
|
|
|
34314
34746
|
? 'No setting reads differently after this call.'
|
|
34315
34747
|
: `Changed before the refusal: ${changed.join(', ')}; no other setting reads differently.`;
|
|
34316
34748
|
}
|
|
34317
|
-
dispatchAgentSettingsToolCall(agentName, call) {
|
|
34749
|
+
dispatchAgentSettingsToolCall(agentName, call, deliver = this.pushToolResult(agentName, call, 'agent')) {
|
|
34318
34750
|
this.emitTrace({
|
|
34319
34751
|
type: 'tool:started',
|
|
34320
34752
|
module: 'agent',
|
|
@@ -34502,14 +34934,7 @@ export class AgentFramework {
|
|
|
34502
34934
|
durationMs: 0,
|
|
34503
34935
|
...(result.isError ? { error: result.error } : {}),
|
|
34504
34936
|
});
|
|
34505
|
-
|
|
34506
|
-
type: 'tool-result',
|
|
34507
|
-
callId: call.id,
|
|
34508
|
-
toolName: call.name,
|
|
34509
|
-
agentName,
|
|
34510
|
-
moduleName: 'agent',
|
|
34511
|
-
result,
|
|
34512
|
-
});
|
|
34937
|
+
deliver(result);
|
|
34513
34938
|
}
|
|
34514
34939
|
/** Aggregate the event-tag vocabulary: reserved chat:* core + each connected
|
|
34515
34940
|
* server's declared tag ontology + gate.js status. */
|
|
@@ -34533,7 +34958,7 @@ export class AgentFramework {
|
|
|
34533
34958
|
'in gate.js. Unknown/undeclared tags are tolerated (open ontologies).',
|
|
34534
34959
|
};
|
|
34535
34960
|
}
|
|
34536
|
-
dispatchEventTagsToolCall(agentName, call) {
|
|
34961
|
+
dispatchEventTagsToolCall(agentName, call, deliver = this.pushToolResult(agentName, call, 'gate')) {
|
|
34537
34962
|
this.emitTrace({ type: 'tool:started', module: 'gate', tool: call.name, callId: call.id, input: call.input });
|
|
34538
34963
|
let result;
|
|
34539
34964
|
try {
|
|
@@ -34546,36 +34971,22 @@ export class AgentFramework {
|
|
|
34546
34971
|
this.toolLifecycleEmitter?.markDispatchFailure(agentName, call.id);
|
|
34547
34972
|
result = { success: false, error: err.message, isError: true };
|
|
34548
34973
|
}
|
|
34549
|
-
|
|
34974
|
+
deliver(result);
|
|
34550
34975
|
}
|
|
34551
|
-
dispatchGateToolCall(agentName, call) {
|
|
34976
|
+
dispatchGateToolCall(agentName, call, deliver = this.pushToolResult(agentName, call, 'gate')) {
|
|
34552
34977
|
this.emitTrace({ type: 'tool:started', module: 'gate', tool: call.name, callId: call.id, input: call.input });
|
|
34553
34978
|
const startTime = Date.now();
|
|
34554
34979
|
this.eventGate.handleToolCall()
|
|
34555
34980
|
.then((result) => {
|
|
34556
34981
|
const durationMs = Date.now() - startTime;
|
|
34557
34982
|
this.emitTrace({ type: 'tool:completed', module: 'gate', tool: call.name, callId: call.id, durationMs });
|
|
34558
|
-
|
|
34559
|
-
type: 'tool-result',
|
|
34560
|
-
callId: call.id,
|
|
34561
|
-
toolName: call.name,
|
|
34562
|
-
agentName,
|
|
34563
|
-
moduleName: 'gate',
|
|
34564
|
-
result,
|
|
34565
|
-
});
|
|
34983
|
+
deliver(result);
|
|
34566
34984
|
})
|
|
34567
34985
|
.catch((error) => {
|
|
34568
34986
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
34569
34987
|
this.emitTrace({ type: 'tool:failed', module: 'gate', tool: call.name, callId: call.id, error: err.message });
|
|
34570
34988
|
this.toolLifecycleEmitter?.markDispatchFailure(agentName, call.id);
|
|
34571
|
-
|
|
34572
|
-
type: 'tool-result',
|
|
34573
|
-
callId: call.id,
|
|
34574
|
-
toolName: call.name,
|
|
34575
|
-
agentName,
|
|
34576
|
-
moduleName: 'gate',
|
|
34577
|
-
result: { success: false, error: err.message, isError: true },
|
|
34578
|
-
});
|
|
34989
|
+
deliver({ success: false, error: err.message, isError: true });
|
|
34579
34990
|
});
|
|
34580
34991
|
}
|
|
34581
34992
|
/** The channel context one server is told for an agent's inference
|