@animalabs/agent-framework 0.30.0 → 0.32.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 +138 -102
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1133 -752
- 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 +6 -12
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +65 -24
- 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 +1163 -770
- 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 +6 -12
- package/src/types/framework.ts +66 -24
- package/src/types/module.ts +42 -3
- package/src/types/trace.ts +4 -3
- package/dist/src/reasoning-loss-notice.d.ts +0 -23
- package/dist/src/reasoning-loss-notice.d.ts.map +0 -1
- package/dist/src/reasoning-loss-notice.js +0 -52
- package/dist/src/reasoning-loss-notice.js.map +0 -1
- package/dist/src/reasoning-loss.d.ts +0 -24
- package/dist/src/reasoning-loss.d.ts.map +0 -1
- package/dist/src/reasoning-loss.js +0 -102
- package/dist/src/reasoning-loss.js.map +0 -1
- package/src/reasoning-loss-notice.ts +0 -64
- package/src/reasoning-loss.ts +0 -120
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';
|
|
@@ -66,7 +67,6 @@ import { cumulativeDelta } from './usage-accounting.js';
|
|
|
66
67
|
import { stampThinkingTokenEstimates } from './thinking-token-stamp.js';
|
|
67
68
|
import { splitThinkingDrops, ThinkingLossIncidents } from './thinking-drops.js';
|
|
68
69
|
import { asNextRequestCarries, roundRows } from './sent-rows.js';
|
|
69
|
-
import { ReasoningLossTeller } from './reasoning-loss.js';
|
|
70
70
|
import { contentAsStored, nameAsStored } from './text-as-stored.js';
|
|
71
71
|
import { STORED_INJECTION_KEY, STORED_INJECTION_KIND, asStoredRow, hasRenderedContent, isAnotherAgentsInjection, isOwnInjection, isStoredKind, storedInjectionMarkOf, unitHash } from './stored-injections.js';
|
|
72
72
|
/** An MCPL tool call refused because its server isn't in the registry: one
|
|
@@ -627,6 +627,13 @@ export const BUILTIN_TOOLSET_TOOLS = new Set([
|
|
|
627
627
|
'heartbeat--heartbeat_trigger',
|
|
628
628
|
'workspace--status',
|
|
629
629
|
]);
|
|
630
|
+
function agentsOwnVerbRefusal(toolName) {
|
|
631
|
+
return {
|
|
632
|
+
success: false,
|
|
633
|
+
error: `${toolName} is the agent's own, which a module's call can't make for it, so nothing was done.`,
|
|
634
|
+
isError: true,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
630
637
|
const readExtensionSettings = (ext, agentName) => {
|
|
631
638
|
const values = ext.get(agentName);
|
|
632
639
|
return Object.fromEntries(Object.entries(values).map(([key, value]) => [key, value === undefined ? 'undefined' : JSON.stringify(value)]));
|
|
@@ -669,7 +676,7 @@ import { EventGate, MCPL_GATE_EVENT_TYPES, checkToolInput, formatShadowWarning,
|
|
|
669
676
|
import { ReplyJudge } from './gate/reply-judge.js';
|
|
670
677
|
import { HEARTBEAT_CHANGING_TOOLS, Heartbeat, isHeartbeatMcplServer } from './heartbeat/heartbeat.js';
|
|
671
678
|
import { EPHEMERAL_USAGE_NAME, UsageTracker, persistedUsageProblem } from './usage/usage-tracker.js';
|
|
672
|
-
import {
|
|
679
|
+
import { McplRequestError, McplUnreapedLaunchError, stderrTailOf, toolsetDeclarationsOf, withStderr } from './mcpl/server-connection.js';
|
|
673
680
|
import { ToolsetHub, TOOLSETS_MODULE, META_TOOL_NAMES } from './mcpl/toolset-host.js';
|
|
674
681
|
import { HostPresentation, BUILTIN_TOOLSET } from './host-presentation.js';
|
|
675
682
|
import { isEmptyChange } from './mcpl/tool-presence.js';
|
|
@@ -677,7 +684,7 @@ import { ToolsetsModule, resolveAct, withLookHint } from './mcpl/toolsets-module
|
|
|
677
684
|
import { ModernMcpConnection } from './mcpl/modern-connection.js';
|
|
678
685
|
import { MAX_TIMER_MS, WITHHELD_TEXT, asText, credentialRedactor, redactUrl, redactServerText, resolveServerBinding, serverConfigProblems, serverCredentialPatterns, serverTextRedactor } from './mcpl/protocol-family.js';
|
|
679
686
|
import { MALFORMED_CONTENT_NOTICE, normalizeStandardToolResult } from './mcpl/tool-result-normalize.js';
|
|
680
|
-
import { boundedForLog, describeCause } from './mcpl/errors.js';
|
|
687
|
+
import { boundedForLog, describeCause, hostStoppedWhileHandling } from './mcpl/errors.js';
|
|
681
688
|
import { assertServerConfigBooleans, buildWebSocketUrl, isWebSocketTransport } from './mcpl/transport.js';
|
|
682
689
|
import { formatZonedDateTime, resolveTimeZone } from './timezone.js';
|
|
683
690
|
import { CLOCK_ROW_KIND, clockStampOf, planClock, takesClockBefore } from './clock-rows.js';
|
|
@@ -826,8 +833,8 @@ function resumesLogicalTurn(reason) {
|
|
|
826
833
|
*/
|
|
827
834
|
const TURN_RETRY = Symbol('turn-retry');
|
|
828
835
|
/**
|
|
829
|
-
* The batch a start
|
|
830
|
-
*
|
|
836
|
+
* The batch a start was merged from, when the scheduler merged more than
|
|
837
|
+
* one request into it and any of them isn't the turn's: every
|
|
831
838
|
* request it took in, as each was. Until the start takes its turn token it
|
|
832
839
|
* stands for them all. A scheduler pass that takes it back puts the batch in
|
|
833
840
|
* its place, each request with its own identity and flags; a quiesce at the
|
|
@@ -847,14 +854,10 @@ function isRearmed(request) {
|
|
|
847
854
|
function waitAlertsOnItsOwn(wait) {
|
|
848
855
|
return wait.until === null || wait.until - wait.setAt > PROVIDER_WAIT_ALERT_MS;
|
|
849
856
|
}
|
|
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
857
|
function isTransientFailure(error) {
|
|
855
858
|
if (!isMembraneError(error) || error.retryable !== true)
|
|
856
859
|
return false;
|
|
857
|
-
if (error
|
|
860
|
+
if (isDeadlineExpiry(error))
|
|
858
861
|
return false;
|
|
859
862
|
return error.type === 'server' || error.type === 'network' || error.type === 'timeout' || error.type === 'rate_limit';
|
|
860
863
|
}
|
|
@@ -949,6 +952,29 @@ function freshTurnRouting() {
|
|
|
949
952
|
addressedElsewhere: new Set(),
|
|
950
953
|
};
|
|
951
954
|
}
|
|
955
|
+
const TURN_INPUTS_SCAN = 64;
|
|
956
|
+
function turnInputs(cm, floor, agentName, sees) {
|
|
957
|
+
let first;
|
|
958
|
+
let last;
|
|
959
|
+
scan: for (let end = cm.getMessageCount(); end > 0; end -= TURN_INPUTS_SCAN) {
|
|
960
|
+
const { messages } = cm.getMessageWindow(Math.max(0, end - TURN_INPUTS_SCAN), Math.min(TURN_INPUTS_SCAN, end), { resolveBlobs: false });
|
|
961
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
962
|
+
const message = messages[i];
|
|
963
|
+
if (message.sequence <= floor)
|
|
964
|
+
break scan;
|
|
965
|
+
if (!sees(message.metadata))
|
|
966
|
+
continue;
|
|
967
|
+
if (message.participant === agentName)
|
|
968
|
+
break scan;
|
|
969
|
+
last ??= message.id;
|
|
970
|
+
const answers = Array.isArray(message.content) && message.content.length > 0
|
|
971
|
+
&& message.content.every((block) => block.type === 'tool_result');
|
|
972
|
+
if (!answers)
|
|
973
|
+
first = message.id;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return first !== undefined && last !== undefined ? { first, last } : undefined;
|
|
977
|
+
}
|
|
952
978
|
const sameReach = (a, b) => {
|
|
953
979
|
if (!a || !b)
|
|
954
980
|
return a === b;
|
|
@@ -1026,10 +1052,6 @@ const LIVE_SPEECH_UNRECEIPTED = `Plain speech its rounds routed live was handed
|
|
|
1026
1052
|
* had closed admission (endFailureAtStop). Its inference:failed came first,
|
|
1027
1053
|
* and this says no retry follows it. */
|
|
1028
1054
|
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
1055
|
/** The reason on the inference:aborted that ends a turn whose continuation
|
|
1034
1056
|
* was dropped before it started, other than by an abandon or a stop. */
|
|
1035
1057
|
const CONTINUATION_DROPPED = "a turn's continuation was dropped before it started";
|
|
@@ -1075,6 +1097,24 @@ const MAX_TURN_CHECKPOINTS = 20;
|
|
|
1075
1097
|
const DEFAULT_DISCORD_AWARENESS_DEADLINE_MS = 10_000;
|
|
1076
1098
|
const MIN_DISCORD_AWARENESS_DEADLINE_MS = 50;
|
|
1077
1099
|
const MAX_DISCORD_AWARENESS_DEADLINE_MS = 60_000;
|
|
1100
|
+
/** Whether a synthesized tool's nullable field is in use: omission, null and
|
|
1101
|
+
* the empty string all mean it isn't, so a caller whose provider presents
|
|
1102
|
+
* every property as required can still leave one unset. Any other value is
|
|
1103
|
+
* in use, and its field checks it. */
|
|
1104
|
+
function fieldInUse(value) {
|
|
1105
|
+
return value !== undefined && value !== null && value !== '';
|
|
1106
|
+
}
|
|
1107
|
+
/** A synthesized tool's integer field: an integer number, or a string of ASCII
|
|
1108
|
+
* digits once trimmed (a model may write an integer as a string), within the
|
|
1109
|
+
* integers a number holds exactly; NaN for anything else. Number() would read
|
|
1110
|
+
* " ", false and [] as 0, and "2.0" or "+2" as 2, and a longer run of digits
|
|
1111
|
+
* as a different, rounded number. */
|
|
1112
|
+
function integerField(value) {
|
|
1113
|
+
const n = typeof value === 'number' ? value
|
|
1114
|
+
: typeof value === 'string' && /^[0-9]+$/.test(value.trim()) ? Number(value.trim())
|
|
1115
|
+
: Number.NaN;
|
|
1116
|
+
return Number.isSafeInteger(n) ? n : Number.NaN;
|
|
1117
|
+
}
|
|
1078
1118
|
function normalizeDiscordAwarenessDeadline(value) {
|
|
1079
1119
|
if (value === undefined || !Number.isFinite(value) || value <= 0) {
|
|
1080
1120
|
return DEFAULT_DISCORD_AWARENESS_DEADLINE_MS;
|
|
@@ -1084,9 +1124,10 @@ function normalizeDiscordAwarenessDeadline(value) {
|
|
|
1084
1124
|
/**
|
|
1085
1125
|
* Refuse a config whose `subconscious`, `watchdog` or `processLogging` is
|
|
1086
1126
|
* anything but an object, null or false (holdsSettings), or whose switch in
|
|
1087
|
-
* one holds anything but true or false (assertBooleanSetting)
|
|
1088
|
-
*
|
|
1089
|
-
*
|
|
1127
|
+
* one holds anything but true or false (assertBooleanSetting), or whose
|
|
1128
|
+
* subconscious's `thinkingBinding` assertThinkingBinding refuses, whichever
|
|
1129
|
+
* its reader. A subconscious that isn't on has its `allowChannelSpeech` and
|
|
1130
|
+
* `thinkingBinding` left unchecked, since nothing reads them then.
|
|
1090
1131
|
*/
|
|
1091
1132
|
function assertFrameworkSwitches(config) {
|
|
1092
1133
|
if (holdsSettings('subconscious', config.subconscious, '{ enabled: true }')) {
|
|
@@ -1094,6 +1135,7 @@ function assertFrameworkSwitches(config) {
|
|
|
1094
1135
|
'and false or leaving it out doesn\'t');
|
|
1095
1136
|
if (config.subconscious.enabled === true) {
|
|
1096
1137
|
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');
|
|
1138
|
+
assertThinkingBinding('subconscious.thinkingBinding', config.subconscious.thinkingBinding);
|
|
1097
1139
|
}
|
|
1098
1140
|
}
|
|
1099
1141
|
if (holdsSettings('watchdog', config.watchdog, '{ enabled: true }')) {
|
|
@@ -1454,9 +1496,15 @@ function hostCommandRequester(serverId, params) {
|
|
|
1454
1496
|
* schedule, jittered, 10 s doubling to 5 min (by default 13 tries, where
|
|
1455
1497
|
* membrane's settings have 7: about 30 min).
|
|
1456
1498
|
* - rate limited (429): the base schedule with at least 5 tries.
|
|
1457
|
-
* -
|
|
1458
|
-
*
|
|
1459
|
-
*
|
|
1499
|
+
* - a deadline expiry, which membrane leaves to its caller: a progress
|
|
1500
|
+
* bound's cut (FrameworkConfig.inferenceBounds), the adapter's own
|
|
1501
|
+
* watchdogs, the SDK's request timeout or a runtime's fetch timeout. The
|
|
1502
|
+
* deadline schedule: by default 2 tries, 1 s apart, so one fresh call. For
|
|
1503
|
+
* one, the framework's `attempt` counts that call's consecutive expiries,
|
|
1504
|
+
* not the turn's other failures, so each call a deadline cuts gets its
|
|
1505
|
+
* fresh call. Settings with no deadline schedule give it their base one.
|
|
1506
|
+
* - any other retryable failure: the base schedule (by default 3 tries, 1 s
|
|
1507
|
+
* then 2 s).
|
|
1460
1508
|
* A provider's stated wait (`retryAfterMs`) is waited when it is longer than
|
|
1461
1509
|
* the backoff, up to the schedule's ceiling, which by default is at least the
|
|
1462
1510
|
* framework's own cap (PROVIDER_ACCELERATION_MAX_COOLDOWN_MS, ten minutes);
|
|
@@ -1476,6 +1524,10 @@ export class DefaultErrorPolicy {
|
|
|
1476
1524
|
this.maxRetries = Math.max(0, ...Object.values(settings).map((s) => (s?.maxAttempts ?? 1) - 1));
|
|
1477
1525
|
}
|
|
1478
1526
|
onInferenceError(error, _agentName, attempt) {
|
|
1527
|
+
if (isDeadlineExpiry(error)) {
|
|
1528
|
+
const decision = retryDecision(this.settings.deadline ? 'deadline' : 'retryable', attempt + 1, undefined, this.settings);
|
|
1529
|
+
return decision.retry ? { retry: true, delayMs: decision.delayMs } : { retry: false };
|
|
1530
|
+
}
|
|
1479
1531
|
if (!isMembraneError(error)) {
|
|
1480
1532
|
// Not a provider failure membrane classified: retried as any other
|
|
1481
1533
|
// retryable failure, on the base schedule.
|
|
@@ -1485,9 +1537,7 @@ export class DefaultErrorPolicy {
|
|
|
1485
1537
|
if (error.retryable === false)
|
|
1486
1538
|
return { retry: false };
|
|
1487
1539
|
// 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.
|
|
1540
|
+
// copy of membrane, as isMembraneError takes one).
|
|
1491
1541
|
const facts = error;
|
|
1492
1542
|
// A failure that doesn't say is retried as an unknown one is; only false refuses (handled above).
|
|
1493
1543
|
const cls = classifyFailure({ retryable: facts.retryable !== false, type: facts.type, httpStatus: facts.httpStatus, message: facts.message });
|
|
@@ -1516,18 +1566,37 @@ const MAINTENANCE_TICKS_PER_PASS = 8;
|
|
|
1516
1566
|
* together, and then on awareness drains still running, such as its
|
|
1517
1567
|
* startup's delivery. */
|
|
1518
1568
|
const FAILED_CREATE_TEARDOWN_TIMEOUT_MS = 30_000;
|
|
1569
|
+
const ABANDON_GRACE_MS = 10_000;
|
|
1570
|
+
const testAbandonGraces = new WeakMap();
|
|
1571
|
+
/**
|
|
1572
|
+
* For tests only: `framework`'s abandon takes `ms` as its grace in place of
|
|
1573
|
+
* {@link ABANDON_GRACE_MS}. Set it only where the test holds every turn the
|
|
1574
|
+
* abandon reaches past the grace, so no turn can end within it and a short
|
|
1575
|
+
* grace only brings the abandon's report sooner.
|
|
1576
|
+
*/
|
|
1577
|
+
export function setAbandonGraceForTests(framework, ms) {
|
|
1578
|
+
testAbandonGraces.set(framework, ms);
|
|
1579
|
+
}
|
|
1519
1580
|
const PROVIDER_ACCELERATION_DEFAULT_COOLDOWN_MS = 65_000;
|
|
1520
1581
|
/** Cap on waits the framework chooses itself. A provider's stated wait is
|
|
1521
1582
|
* never capped: it is a lower bound (see ProviderWaits). It is also the
|
|
1522
1583
|
* longest stated wait an ephemeral run sleeps in its turn: a longer one ends
|
|
1523
1584
|
* the run instead (nonOwningProviderWait). */
|
|
1524
1585
|
const PROVIDER_ACCELERATION_MAX_COOLDOWN_MS = 10 * 60_000;
|
|
1586
|
+
const FAILURE_ADVICE = 'If this recurs with the same cause, change approach rather than retrying identically ' +
|
|
1587
|
+
'(e.g. drop an oversized attachment or an unsupported setting).';
|
|
1588
|
+
const TIMEOUT_ADVICE = 'If a reply is still owed from that turn, you can give it now.';
|
|
1589
|
+
const MADE_AGAIN = 'The call was made again, and it timed out again.';
|
|
1525
1590
|
const OVERLOADED_TURN_TRIES = 13;
|
|
1526
|
-
const
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1591
|
+
const DEADLINE_TURN_TRIES = 2;
|
|
1592
|
+
const TURN_RETRY_SETTINGS = {
|
|
1593
|
+
...Object.fromEntries(Object.entries(MEMBRANE_RETRY_SETTINGS).map(([cls, s]) => [cls, s && {
|
|
1594
|
+
...s,
|
|
1595
|
+
...(cls === 'overloaded' ? { maxAttempts: OVERLOADED_TURN_TRIES } : {}),
|
|
1596
|
+
statedWaitCeilingMs: s.statedWaitCeilingMs === null ? null : Math.max(s.statedWaitCeilingMs, PROVIDER_ACCELERATION_MAX_COOLDOWN_MS),
|
|
1597
|
+
}])),
|
|
1598
|
+
deadline: { maxAttempts: DEADLINE_TURN_TRIES, firstDelayMs: 1_000, multiplier: 1, maxDelayMs: 1_000, statedWaitCeilingMs: null },
|
|
1599
|
+
};
|
|
1531
1600
|
const PROVIDER_ACCELERATION_JITTER_MS = 5_000;
|
|
1532
1601
|
/** Longest delay one setTimeout can represent (2^31-1 ms, about 24.8 days);
|
|
1533
1602
|
* a longer timer fires almost at once, so longer waits re-arm in steps. */
|
|
@@ -1577,6 +1646,11 @@ async function* turnStreamEvents(stream, agentName) {
|
|
|
1577
1646
|
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
1647
|
return;
|
|
1579
1648
|
}
|
|
1649
|
+
if (event.type === 'aborted' && event.reason === 'timeout') {
|
|
1650
|
+
decidedBy = 'error';
|
|
1651
|
+
yield { type: 'error', error: deadlineCut(event.message) };
|
|
1652
|
+
continue;
|
|
1653
|
+
}
|
|
1580
1654
|
if (event.type === 'complete' || event.type === 'error' || event.type === 'aborted')
|
|
1581
1655
|
decidedBy = event.type;
|
|
1582
1656
|
yield event;
|
|
@@ -1822,6 +1896,30 @@ const isLowSurrogate = (text, index) => {
|
|
|
1822
1896
|
};
|
|
1823
1897
|
/** Whether a cut before `index` would separate the halves of a surrogate pair. */
|
|
1824
1898
|
const splitsPair = (text, index) => index > 0 && index < text.length && isHighSurrogate(text, index - 1) && isLowSurrogate(text, index);
|
|
1899
|
+
/**
|
|
1900
|
+
* Whether an MCPL server's child ended the way a server ends when it's asked
|
|
1901
|
+
* to: exit 0, death by a signal that asks a process to end (SIGTERM, SIGINT,
|
|
1902
|
+
* SIGHUP), or a shell's report of such a death by its convention, 128 plus the
|
|
1903
|
+
* signal's number (143, 130, 129). A service manager's stop signals the host
|
|
1904
|
+
* and its children together, so a child can go before the host's stop()
|
|
1905
|
+
* closes it: its loss is that stop, not a failure, and raises no mcpl-down.
|
|
1906
|
+
* A child the stdio transport ended itself, after a pipe error or a line on
|
|
1907
|
+
* its stdout longer than the bound, was ended by that, whatever its code or
|
|
1908
|
+
* signal. A WebSocket's close codes are 1000 and up, so they never match.
|
|
1909
|
+
*/
|
|
1910
|
+
function endedAsAsked(ended) {
|
|
1911
|
+
if (ended.pipeFailed)
|
|
1912
|
+
return false;
|
|
1913
|
+
const { code, signal } = ended;
|
|
1914
|
+
return code === 0 || code === 143 || code === 130 || code === 129 ||
|
|
1915
|
+
signal === 'SIGTERM' || signal === 'SIGINT' || signal === 'SIGHUP';
|
|
1916
|
+
}
|
|
1917
|
+
/** A lost connection's words for an alert: the transport's reason, then its
|
|
1918
|
+
* exit or close code and signal, each when there is one. */
|
|
1919
|
+
function closeWords(reason, code, signal) {
|
|
1920
|
+
const how = [reason ?? null, code != null ? `code ${code}` : null, signal ? `signal ${signal}` : null].filter((part) => part !== null);
|
|
1921
|
+
return how.length > 0 ? ` (${how.join(', ')})` : '';
|
|
1922
|
+
}
|
|
1825
1923
|
/**
|
|
1826
1924
|
* `text` when it fits within `max` characters; otherwise its head and tail
|
|
1827
1925
|
* around a marker stating how many of how many characters were omitted, the
|
|
@@ -2067,6 +2165,16 @@ export function shedMayHelp(errorType, httpStatus) {
|
|
|
2067
2165
|
return errorType === 'invalid_request' || errorType === 'context_length';
|
|
2068
2166
|
return errorType === 'invalid_request' && (httpStatus === undefined || httpStatus === 400 || httpStatus === 422);
|
|
2069
2167
|
}
|
|
2168
|
+
function responseParts(response) {
|
|
2169
|
+
return [
|
|
2170
|
+
response?.addMessages?.length ? `${response.addMessages.length} message(s) to add` : '',
|
|
2171
|
+
response?.editMessages?.length ? `${response.editMessages.length} edit(s)` : '',
|
|
2172
|
+
response?.removeMessages?.length ? `${response.removeMessages.length} removal(s)` : '',
|
|
2173
|
+
response?.stateUpdate !== undefined ? 'a state update' : '',
|
|
2174
|
+
response?.requestInference ? 'an inference request' : '',
|
|
2175
|
+
response?.toolsChanged ? 'a tools change' : '',
|
|
2176
|
+
].filter(Boolean).join(', ');
|
|
2177
|
+
}
|
|
2070
2178
|
/**
|
|
2071
2179
|
* What a thrown value says, safely and briefly, as boundedErrorMessage gives
|
|
2072
2180
|
* it, with `recorded` applied before the cut, so no part of a credential
|
|
@@ -2168,6 +2276,12 @@ function wakeLineKey(line) {
|
|
|
2168
2276
|
return JSON.stringify([eventType, serverId ?? null, channelId ?? null, messageId ?? null, eventId ?? null]);
|
|
2169
2277
|
}
|
|
2170
2278
|
const SUBCONSCIOUS_MODEL_FIELDS = ['physicalWindowTokens', 'thinkingBinding'];
|
|
2279
|
+
function readerModelFields(cfg) {
|
|
2280
|
+
return {
|
|
2281
|
+
...(cfg.physicalWindowTokens != null ? { physicalWindowTokens: cfg.physicalWindowTokens } : {}),
|
|
2282
|
+
...(holdsSettings('subconscious.thinkingBinding', cfg.thinkingBinding, '{ binds: true, dropBlock: true }') ? { thinkingBinding: cfg.thinkingBinding } : {}),
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2171
2285
|
const JUDGE_PARAMETER = {
|
|
2172
2286
|
type: 'object',
|
|
2173
2287
|
description: 'Ask the reply judge first: batch matching events like debounceMs, then a small, cheap model reads your ' +
|
|
@@ -2233,9 +2347,10 @@ export class AgentFramework {
|
|
|
2233
2347
|
store;
|
|
2234
2348
|
ownsStore;
|
|
2235
2349
|
stopPromise = null;
|
|
2236
|
-
|
|
2350
|
+
// Nothing sets it back to 'none': a stop ends its instance (#315).
|
|
2351
|
+
stopState = 'none';
|
|
2352
|
+
closedStopFailure;
|
|
2237
2353
|
ownedStoreClosed = false;
|
|
2238
|
-
ownedStoreManagersClosed = false;
|
|
2239
2354
|
/** Whether the latest stop() got through its teardown with nothing failed:
|
|
2240
2355
|
* every step and every wait up to and including the one on awareness
|
|
2241
2356
|
* drains, whatever failed after that, in the storage work that finishes
|
|
@@ -2251,8 +2366,8 @@ export class AgentFramework {
|
|
|
2251
2366
|
* promise: a stream it cancelled or an event loop still in a call; and a
|
|
2252
2367
|
* stream whose cancel threw, which stop() doesn't wait for. A later
|
|
2253
2368
|
* 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
|
-
*
|
|
2369
|
+
* since to the entry it was found under (a run's end removes its stream's),
|
|
2370
|
+
* so neither a budget nor a cancel that
|
|
2256
2371
|
* threw lets the store close under work still running. Each leaves once it
|
|
2257
2372
|
* settles. */
|
|
2258
2373
|
leftRunning = new Map();
|
|
@@ -2318,6 +2433,9 @@ export class AgentFramework {
|
|
|
2318
2433
|
providerAccelerationLastRecovery = new Map();
|
|
2319
2434
|
providerAccelerationDefaultCooldownMs = PROVIDER_ACCELERATION_DEFAULT_COOLDOWN_MS;
|
|
2320
2435
|
providerAccelerationJitterMs = PROVIDER_ACCELERATION_JITTER_MS;
|
|
2436
|
+
inferenceBounds = resolveInferenceBounds(undefined);
|
|
2437
|
+
deadlineCuts = new Map();
|
|
2438
|
+
freshCallsCut = new Set();
|
|
2321
2439
|
providerHoldHook;
|
|
2322
2440
|
/** Provider waits (stated retry-after) per (agent, model), kept across restarts. */
|
|
2323
2441
|
providerWaits;
|
|
@@ -2325,8 +2443,6 @@ export class AgentFramework {
|
|
|
2325
2443
|
retryWaitWakers = new Set();
|
|
2326
2444
|
waitingTraceEveryMs = WAITING_TRACE_EVERY_MS;
|
|
2327
2445
|
providerAdmissionClosed = false;
|
|
2328
|
-
/** Invalidates framework turns still assembling when stop begins. */
|
|
2329
|
-
lifecycleGeneration = 0;
|
|
2330
2446
|
/** Last time we reported stale (busy-requeued) inference requests, per agent. */
|
|
2331
2447
|
staleWarnAt = new Map();
|
|
2332
2448
|
/** Per-agent last inference activity (epoch ms), for /healthz + doctor tooling. */
|
|
@@ -2379,7 +2495,6 @@ export class AgentFramework {
|
|
|
2379
2495
|
* next turn.
|
|
2380
2496
|
*/
|
|
2381
2497
|
turnRouting = new Map();
|
|
2382
|
-
reasoningLoss = new ReasoningLossTeller();
|
|
2383
2498
|
/**
|
|
2384
2499
|
* The latest link of each agent's chain of queued prose deliveries. Every
|
|
2385
2500
|
* stream queues its own behind it, a fresh turn's included, so the agent's
|
|
@@ -2467,6 +2582,7 @@ export class AgentFramework {
|
|
|
2467
2582
|
turnRearmMs = DEFAULT_TURN_REARM_MS;
|
|
2468
2583
|
turnRearms = new Map();
|
|
2469
2584
|
exhaustingRearms = new Set();
|
|
2585
|
+
rearmDeadlineCuts = new Set();
|
|
2470
2586
|
statedWaitRuns = new Map();
|
|
2471
2587
|
/** N consecutive failed inferences ⇒ the agent is treated as hard-down and
|
|
2472
2588
|
* escalated loudly to stderr. */
|
|
@@ -2747,7 +2863,7 @@ export class AgentFramework {
|
|
|
2747
2863
|
collapseDerivationNoticesAfter = 10;
|
|
2748
2864
|
/** The host's say over each derivation, ephemeral spawn and resume (config.dendrite.beforeDerive). */
|
|
2749
2865
|
beforeDerive;
|
|
2750
|
-
/** What a source is told of a derivation (config.dendrite.derivationNotice); absent,
|
|
2866
|
+
/** What a source is told of a derivation (config.dendrite.derivationNotice); absent, nothing. */
|
|
2751
2867
|
derivationNoticeHook;
|
|
2752
2868
|
/** Per source agent, per kind: how many derivations, since when, and at which count the last note went. */
|
|
2753
2869
|
derivationTallies = new Map();
|
|
@@ -2999,6 +3115,7 @@ export class AgentFramework {
|
|
|
2999
3115
|
getMessage: (id) => this.getMessage(id),
|
|
3000
3116
|
queryMessages: (filter, opts) => this.queryMessages(filter, opts),
|
|
3001
3117
|
pushEvent: (event, moduleName) => this.pushModuleEvent(event, moduleName, true),
|
|
3118
|
+
dropRetiredPush: (event, moduleName) => this.dropRetiredModuleEvent(event, moduleName),
|
|
3002
3119
|
onTrace: (listener) => this.onTrace(listener),
|
|
3003
3120
|
// The ONLY entry that confers module origin — a private closure the
|
|
3004
3121
|
// framework hands to its own module registry. The public
|
|
@@ -3022,6 +3139,7 @@ export class AgentFramework {
|
|
|
3022
3139
|
// Before anything opens or starts: a refused config must leave nothing behind.
|
|
3023
3140
|
if (config.codeExecution != null)
|
|
3024
3141
|
validateCodeExecutionConfig(config.codeExecution);
|
|
3142
|
+
resolveInferenceBounds(config.inferenceBounds);
|
|
3025
3143
|
assertFrameworkSwitches(config);
|
|
3026
3144
|
for (const agentConfig of config.agents)
|
|
3027
3145
|
assertAgentSwitches(agentConfig);
|
|
@@ -3190,6 +3308,7 @@ export class AgentFramework {
|
|
|
3190
3308
|
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
3309
|
framework.providerHoldHook = config.providerHold;
|
|
3192
3310
|
framework.turnRearmMs = turnRearmDelays(config.exhaustedTurnRearmMs);
|
|
3311
|
+
framework.inferenceBounds = resolveInferenceBounds(config.inferenceBounds);
|
|
3193
3312
|
// Before any delivery.
|
|
3194
3313
|
await framework.reconcileDiscordAwarenessAtStartup();
|
|
3195
3314
|
// Restore persisted usage data (if any) from prior session. The names it
|
|
@@ -3671,9 +3790,7 @@ export class AgentFramework {
|
|
|
3671
3790
|
* maintenance, event loop and operator operations, so a dump shows what
|
|
3672
3791
|
* those waits are on, and before its modules and MCPL servers stop, so a
|
|
3673
3792
|
* 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.
|
|
3793
|
+
* With none in the set, one line says no framework is running.
|
|
3677
3794
|
*
|
|
3678
3795
|
* One listener serves the process, installed by the first create() to
|
|
3679
3796
|
* reach the join and kept: once a process's last SIGUSR2 listener is
|
|
@@ -3717,35 +3834,38 @@ export class AgentFramework {
|
|
|
3717
3834
|
}
|
|
3718
3835
|
/**
|
|
3719
3836
|
* Start the event loop. A start() on a running framework does nothing.
|
|
3720
|
-
*
|
|
3721
|
-
*
|
|
3722
|
-
*
|
|
3723
|
-
*
|
|
3837
|
+
* A stop ends its instance (#315): once a stop() is admitted, start() and
|
|
3838
|
+
* addModule() are refused with an error, for good. While that stop() runs,
|
|
3839
|
+
* the error says the framework is stopping. Once it has settled, the error
|
|
3840
|
+
* says what to do: create a new AgentFramework on the store, and after a
|
|
3841
|
+
* failed stop(), call stop() again first, until one resolves. When a
|
|
3842
|
+
* failed stop() left its owned store closed, so that it can't be tried
|
|
3843
|
+
* again, the error carries that failure as its cause.
|
|
3844
|
+
*
|
|
3845
|
+
* A new instance on the store keeps what the store holds. With the
|
|
3846
|
+
* heartbeat on and the same heartbeat state file (heartbeat.statePath, or
|
|
3847
|
+
* its default), it keeps the heartbeat's schedule as it reads it from that
|
|
3848
|
+
* file: a tick or a reminder that fell due meanwhile fires once, shortly
|
|
3849
|
+
* after it starts. What an instance holds only in memory is lost with it,
|
|
3850
|
+
* as on a process restart: an agent's sleep and its wake, a self-wake, the
|
|
3851
|
+
* inference requests still queued (a budget restart, a heartbeat's wake, a
|
|
3852
|
+
* nudge), a re-armed turn's wait and count, and the redo stack. #627 is
|
|
3853
|
+
* about keeping a sleep and a self-wake across a restart. To pause and
|
|
3854
|
+
* resume one framework in place, use quiesce() and resume().
|
|
3724
3855
|
*/
|
|
3725
3856
|
start() {
|
|
3726
3857
|
// Before the running check: stop() claims its attempt before it clears
|
|
3727
3858
|
// running, so a start() made in its first steps, such as from a trace
|
|
3728
3859
|
// listener, is refused too.
|
|
3729
|
-
this.
|
|
3860
|
+
this.refuseOnceStopped('start()');
|
|
3730
3861
|
if (this.running) {
|
|
3731
3862
|
return;
|
|
3732
3863
|
}
|
|
3733
3864
|
this.running = true;
|
|
3734
|
-
this.providerAdmissionClosed = false;
|
|
3735
3865
|
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
3866
|
for (const config of this.mcplServerConfigs.values()) {
|
|
3743
3867
|
referenceRegistry.setServerAddress(config.id, serverAddressOf(config), this);
|
|
3744
3868
|
}
|
|
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
3869
|
this.heartbeat?.start();
|
|
3750
3870
|
this.loopPromise = this.runLoop();
|
|
3751
3871
|
// Start periodic sync timer (if enabled)
|
|
@@ -3770,11 +3890,14 @@ export class AgentFramework {
|
|
|
3770
3890
|
}
|
|
3771
3891
|
}
|
|
3772
3892
|
/**
|
|
3773
|
-
* Stop the event loop
|
|
3774
|
-
*
|
|
3775
|
-
*
|
|
3776
|
-
*
|
|
3777
|
-
*
|
|
3893
|
+
* Stop the event loop, and with it this framework, for good: once a stop()
|
|
3894
|
+
* is admitted, start() and addModule() are refused, as start() says. A
|
|
3895
|
+
* stop() whose budget is refused, which it rejects with a RangeError, is
|
|
3896
|
+
* not admitted and changes nothing. Each step of the teardown runs
|
|
3897
|
+
* whatever an earlier one did: one that throws, such as a host stream's
|
|
3898
|
+
* cancel(), fails the stop() once the rest has run. When a step or a wait
|
|
3899
|
+
* up to and including the wait on awareness drains fails or times out,
|
|
3900
|
+
* storage stays open and
|
|
3778
3901
|
* stop() can be called again; one that fails after it, in the storage work
|
|
3779
3902
|
* that finishes the stop, fails the stop() without keeping storage open.
|
|
3780
3903
|
* A timed-out wait lets teardown go on while what it waited for may still
|
|
@@ -3782,7 +3905,10 @@ export class AgentFramework {
|
|
|
3782
3905
|
* @param teardownTimeoutMs Optional budget for each of stop()'s waits, in
|
|
3783
3906
|
* turn: on the streams it cancels, on maintenance in flight (a pass or an
|
|
3784
3907
|
* over-budget drain kick), on the event loop, on an operator's operations
|
|
3785
|
-
* already running, on modules and MCPL
|
|
3908
|
+
* already running, on modules and MCPL (its servers and the inference
|
|
3909
|
+
* requests they made) together, where a module whose start() is still
|
|
3910
|
+
* running counts until that start() has settled and its stop() returned,
|
|
3911
|
+
* then on Discord awareness
|
|
3786
3912
|
* drains, which send nothing once stop() has begun, recording the call the
|
|
3787
3913
|
* close ended. With a budget, a stop() can take up to six of them, besides
|
|
3788
3914
|
* the withheld-spill deadline of a guard's write still under way. Omit to
|
|
@@ -3805,9 +3931,7 @@ export class AgentFramework {
|
|
|
3805
3931
|
* dropped, though a write the stream already had under way can still
|
|
3806
3932
|
* land in the run's namespace after them.
|
|
3807
3933
|
* - 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.
|
|
3934
|
+
* that call and the rest of its iteration, and then exits.
|
|
3811
3935
|
* - a later stop() waits again, within its own budget, for any stream or
|
|
3812
3936
|
* loop this one left running, so the store never closes under it.
|
|
3813
3937
|
*
|
|
@@ -3835,11 +3959,14 @@ export class AgentFramework {
|
|
|
3835
3959
|
let settle;
|
|
3836
3960
|
const attempt = new Promise((resolve, reject) => { settle = { resolve, reject }; });
|
|
3837
3961
|
this.stopPromise = attempt;
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3962
|
+
this.stopState = 'stopping';
|
|
3963
|
+
void attempt.then(() => { this.stopState = 'completed'; }, (error) => {
|
|
3964
|
+
this.stopState = 'failed';
|
|
3965
|
+
// Failed cleanup can be retried while its original store is open. A
|
|
3966
|
+
// terminal storage failure after successful close must remain a failure.
|
|
3967
|
+
if (this.ownedStoreClosed)
|
|
3968
|
+
this.closedStopFailure = { error };
|
|
3969
|
+
else if (this.stopPromise === attempt)
|
|
3843
3970
|
this.stopPromise = null;
|
|
3844
3971
|
});
|
|
3845
3972
|
// The body still starts in this call, so its synchronous steps (closing
|
|
@@ -3847,24 +3974,28 @@ export class AgentFramework {
|
|
|
3847
3974
|
this.stopOwned(teardownTimeoutMs).then(settle.resolve, settle.reject);
|
|
3848
3975
|
return attempt;
|
|
3849
3976
|
}
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3977
|
+
refuseOnceStopped(verb) {
|
|
3978
|
+
switch (this.stopState) {
|
|
3979
|
+
case 'none':
|
|
3980
|
+
return;
|
|
3981
|
+
case 'stopping':
|
|
3982
|
+
throw new Error(`${verb} refused: the framework is stopping`);
|
|
3983
|
+
case 'completed':
|
|
3984
|
+
throw new Error(`${verb} refused: this framework has stopped, and a stopped framework doesn't start again. ` +
|
|
3985
|
+
'Create a new AgentFramework on its store to go on, with its storePath if it opened the store itself.');
|
|
3986
|
+
case 'failed': {
|
|
3987
|
+
const closed = this.closedStopFailure;
|
|
3988
|
+
if (closed) {
|
|
3989
|
+
throw new Error(`${verb} refused: this framework's stop() failed and its owned store is closed, so stop() can't be tried again: ` +
|
|
3990
|
+
`it rejects with that same failure, this error's cause: ${shutdownErrorMessage(closed.error)}. ` +
|
|
3991
|
+
"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 });
|
|
3992
|
+
}
|
|
3993
|
+
throw new Error(`${verb} refused: this framework's stop() failed, and a stopped framework doesn't start again. ` +
|
|
3994
|
+
'A failed stop() may have left its cleanup or final persistence unfinished. ' +
|
|
3995
|
+
"Call stop() again: once a stop() of this one resolves, they're done, and only then should a new " +
|
|
3996
|
+
'AgentFramework use its store, with its storePath if this one opened the store itself, ' +
|
|
3997
|
+
'or another instance use anything this one shares.');
|
|
3998
|
+
}
|
|
3868
3999
|
}
|
|
3869
4000
|
}
|
|
3870
4001
|
/** Register before invoking the body, so the set holds the operation for
|
|
@@ -3995,6 +4126,15 @@ export class AgentFramework {
|
|
|
3995
4126
|
// holding the wait for operator operations below until its handshake's
|
|
3996
4127
|
// deadline.
|
|
3997
4128
|
step(() => this.endConnectsInFlight());
|
|
4129
|
+
// Nor does a server lost before or during the stop launch or dial again
|
|
4130
|
+
// on its own: a retry already scheduled starts nothing, and none is
|
|
4131
|
+
// scheduled after.
|
|
4132
|
+
step(() => {
|
|
4133
|
+
for (const connection of this.mcplServerRegistry?.getAllServers() ?? [])
|
|
4134
|
+
connection.stopReconnecting();
|
|
4135
|
+
for (const connection of this.modernServers().values())
|
|
4136
|
+
connection.stopReconnecting();
|
|
4137
|
+
});
|
|
3998
4138
|
step(() => this.pushCoalescer?.suspend());
|
|
3999
4139
|
step(() => this.flushCoalescingSnapshot());
|
|
4000
4140
|
this.running = false;
|
|
@@ -4010,11 +4150,10 @@ export class AgentFramework {
|
|
|
4010
4150
|
this.providerAdmissionClosed = true;
|
|
4011
4151
|
for (const wake of [...this.retryWaitWakers])
|
|
4012
4152
|
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;
|
|
4153
|
+
this.guardSpillsClosed = false;
|
|
4017
4154
|
step(() => this.queue.close());
|
|
4155
|
+
let inferences = [];
|
|
4156
|
+
step(() => { inferences = this.inferenceRouter?.stopAll() ?? []; });
|
|
4018
4157
|
step(() => this.tuneOutCoordinator?.stop());
|
|
4019
4158
|
// Stop running code_execution scripts before cancelling streams: a
|
|
4020
4159
|
// zombie script must not keep firing side-effectful tool calls into a
|
|
@@ -4027,9 +4166,8 @@ export class AgentFramework {
|
|
|
4027
4166
|
// framework must not keep up to the whole ledger budget referenced.
|
|
4028
4167
|
this.toolImageLedgers.clear();
|
|
4029
4168
|
// 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
|
|
4169
|
+
// a turn's start a stop reaches does: the continuation never starts.
|
|
4170
|
+
// Before the holds and the parks go, so the
|
|
4033
4171
|
// continuation leaves them with its turn. Each drop is a step of its
|
|
4034
4172
|
// own, so one that throws leaves the next agent's turn to end all the
|
|
4035
4173
|
// same, as stop()'s other loops do.
|
|
@@ -4040,11 +4178,7 @@ export class AgentFramework {
|
|
|
4040
4178
|
});
|
|
4041
4179
|
// A stopped host must never hang behind its own cooldown.
|
|
4042
4180
|
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.
|
|
4181
|
+
// An open logical turn doesn't survive a stop.
|
|
4048
4182
|
this.openLogical.clear();
|
|
4049
4183
|
// Cancel the stream of every agent whose state holds one: streaming,
|
|
4050
4184
|
// waiting on its tools, or ready at a tool boundary. Membrane reports
|
|
@@ -4152,9 +4286,11 @@ export class AgentFramework {
|
|
|
4152
4286
|
// its idle watchdog. Its end stores its queue, as at any run's end; that
|
|
4153
4287
|
// stream's late events are dropped once the run has left, and a later
|
|
4154
4288
|
// stop() waits for the stream itself (leftRunning). (A start() is refused
|
|
4155
|
-
//
|
|
4289
|
+
// once a stop() is admitted, so no new lifecycle begins beside it.)
|
|
4156
4290
|
for (const [name, run] of this.ephemeralRuns)
|
|
4157
4291
|
run.settle.reject(ephemeralRunStopped(name));
|
|
4292
|
+
for (const agent of this.agents.values())
|
|
4293
|
+
step(() => { agent.unpinTurnInputs(); });
|
|
4158
4294
|
// Admission closed when stop() began. What admitted operations start now,
|
|
4159
4295
|
// such as a surgery's config materialization, joins the set, so wait until
|
|
4160
4296
|
// the set is empty. This comes after the streams and the event loop, which
|
|
@@ -4183,11 +4319,9 @@ export class AgentFramework {
|
|
|
4183
4319
|
// generation and nothing reads the old fork's window again. So each live
|
|
4184
4320
|
// fork ends here as a closing fork's end disposes it, and its release
|
|
4185
4321
|
// 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).
|
|
4322
|
+
// first, as the idle sweep's does. A fork whose spawn is still waiting
|
|
4323
|
+
// for its creation to return has no binding yet, and its spawn binds
|
|
4324
|
+
// nothing once it has ended (spawnConversationAgent).
|
|
4191
4325
|
// Each is a step of its own, and each fork ends whatever its stream or
|
|
4192
4326
|
// turn is doing, as each run does above: its end settles a tool-result
|
|
4193
4327
|
// batch its stream still held and removes the stream's entries
|
|
@@ -4278,16 +4412,16 @@ export class AgentFramework {
|
|
|
4278
4412
|
// workspace: its stub names the audit record. A write that was past its
|
|
4279
4413
|
// deadline isn't waited for here, but the workspace's own stop waits for
|
|
4280
4414
|
// the mount turn it holds, bounded only by teardownTimeoutMs.
|
|
4281
|
-
this.
|
|
4415
|
+
this.guardSpillsClosed = true;
|
|
4282
4416
|
// Stop modules and both engines' MCP servers in parallel
|
|
4283
|
-
const shutdownPromises = [this.moduleRegistry.stopAll(teardownTimeoutMs)];
|
|
4417
|
+
const shutdownPromises = [this.moduleRegistry.stopAll(teardownTimeoutMs), waitFor(inferences)];
|
|
4284
4418
|
if (this.mcplServerRegistry) {
|
|
4285
4419
|
shutdownPromises.push(this.mcplServerRegistry.closeAll(teardownTimeoutMs));
|
|
4286
4420
|
}
|
|
4287
4421
|
shutdownPromises.push(this.closeModernMcpConnections(teardownTimeoutMs));
|
|
4288
4422
|
// A stopped framework's servers' addresses leave the reference registry,
|
|
4289
4423
|
// so no other framework's stub reads them and the registry doesn't keep
|
|
4290
|
-
// this one reachable.
|
|
4424
|
+
// this one reachable.
|
|
4291
4425
|
step(() => { for (const id of this.mcplServerConfigs.keys())
|
|
4292
4426
|
referenceRegistry.forgetServerAddress(id, this); });
|
|
4293
4427
|
// Timed-out or failed teardown retains its context and may still write.
|
|
@@ -4346,8 +4480,8 @@ export class AgentFramework {
|
|
|
4346
4480
|
// No stream is left, unless its wait above ran out or its cancel threw:
|
|
4347
4481
|
// from the start of stop() none registers and no agent turn starts
|
|
4348
4482
|
// (providerAdmissionClosed, checked for every agent), and every stream
|
|
4349
|
-
// registered before was cancelled and awaited above. (A start()
|
|
4350
|
-
//
|
|
4483
|
+
// registered before was cancelled and awaited above. (A start() is
|
|
4484
|
+
// refused once a stop() is admitted, so admission stays closed.)
|
|
4351
4485
|
// What is still deferred (a message held for a turn's end whose flush
|
|
4352
4486
|
// found the target busy again, or one deferred behind its target's own
|
|
4353
4487
|
// state) would wait for a flush that may never come. Land each at its
|
|
@@ -4427,8 +4561,7 @@ export class AgentFramework {
|
|
|
4427
4561
|
shutdownErrors.push(error);
|
|
4428
4562
|
}
|
|
4429
4563
|
if (this.ownsStore && teardownSucceeded) {
|
|
4430
|
-
|
|
4431
|
-
this.ownedStoreManagersClosed = true;
|
|
4564
|
+
this.closeOpenManagers();
|
|
4432
4565
|
try {
|
|
4433
4566
|
this.store.close();
|
|
4434
4567
|
this.ownedStoreClosed = true;
|
|
@@ -4889,7 +5022,6 @@ export class AgentFramework {
|
|
|
4889
5022
|
const alive = [...this.openManagers].flatMap((ref) => ref.deref() ?? []);
|
|
4890
5023
|
for (const contextManager of alive)
|
|
4891
5024
|
this.closeManager(contextManager);
|
|
4892
|
-
return alive.length > 0;
|
|
4893
5025
|
}
|
|
4894
5026
|
/** The framework's membrane, with each complete() call passed through
|
|
4895
5027
|
* `admit` along with the model it names. */
|
|
@@ -4899,8 +5031,17 @@ export class AgentFramework {
|
|
|
4899
5031
|
const value = Reflect.get(obj, prop, receiver);
|
|
4900
5032
|
if (prop === 'complete' && typeof value === 'function') {
|
|
4901
5033
|
return (...args) => {
|
|
4902
|
-
const
|
|
4903
|
-
|
|
5034
|
+
const config = args[0]?.config;
|
|
5035
|
+
const model = config?.model;
|
|
5036
|
+
const options = args[1];
|
|
5037
|
+
const bounds = this.inferenceBounds;
|
|
5038
|
+
const sent = bounds !== null && options?.progressTimeoutMs === undefined
|
|
5039
|
+
? [args[0], {
|
|
5040
|
+
...options,
|
|
5041
|
+
progressTimeoutMs: wholeCallMs(bounds, typeof config?.maxTokens === 'number' ? config.maxTokens : undefined),
|
|
5042
|
+
}, ...args.slice(2)]
|
|
5043
|
+
: args;
|
|
5044
|
+
return admit(() => Reflect.apply(value, this.membrane, sent), typeof model === 'string' ? model : undefined);
|
|
4904
5045
|
};
|
|
4905
5046
|
}
|
|
4906
5047
|
return typeof value === 'function' ? value.bind(this.membrane) : value;
|
|
@@ -5038,7 +5179,7 @@ export class AgentFramework {
|
|
|
5038
5179
|
// length: an operator release of it returns the hold to AF's own pacing.
|
|
5039
5180
|
...(pacing?.stated ? { statedWait: { model: agent.model, ownUntil: now + pacing.ownMs } } : {}),
|
|
5040
5181
|
...(auxiliary && (!existing || existing.auxiliaryOrigin) ? { auxiliaryOrigin: true } : {}),
|
|
5041
|
-
...(existing?.
|
|
5182
|
+
...(existing?.emptiedBy && held.length === 0 ? { emptiedBy: existing.emptiedBy } : {}) });
|
|
5042
5183
|
if (auxiliary && !existing) {
|
|
5043
5184
|
console.error(`[provider-cooldown] agent=${agent.name} host hold (${hostHold.reason}) from an auxiliary call — ` +
|
|
5044
5185
|
`holding primary/auxiliary for ${delayMs}ms`);
|
|
@@ -5055,12 +5196,23 @@ export class AgentFramework {
|
|
|
5055
5196
|
this.noteStatedWaitRun(agent.name, wait, now);
|
|
5056
5197
|
return true;
|
|
5057
5198
|
}
|
|
5058
|
-
|
|
5199
|
+
takeAsTransient(agentName, error, trigger) {
|
|
5200
|
+
if (isTransientFailure(error)) {
|
|
5201
|
+
this.rearmDeadlineCuts.delete(agentName);
|
|
5202
|
+
return true;
|
|
5203
|
+
}
|
|
5204
|
+
if (!isRearmed(trigger) || !isDeadlineExpiry(error))
|
|
5205
|
+
return false;
|
|
5206
|
+
const first = !this.rearmDeadlineCuts.has(agentName);
|
|
5207
|
+
this.rearmDeadlineCuts.add(agentName);
|
|
5208
|
+
return first;
|
|
5209
|
+
}
|
|
5210
|
+
rearmExhaustedTurn(agent, error, trigger, ownsProviderGate, routing, asTransient) {
|
|
5059
5211
|
if (!ownsProviderGate || trigger === undefined || this.providerAdmissionClosed || this.turnRearmMs.length === 0)
|
|
5060
5212
|
return;
|
|
5061
5213
|
const done = this.turnRearms.get(agent.name) ?? 0;
|
|
5062
5214
|
const notSent = (why) => console.error(`[inference-rearm] agent=${agent.name} turn not re-armed: ${why}`);
|
|
5063
|
-
if (!
|
|
5215
|
+
if (!asTransient) {
|
|
5064
5216
|
if (done > 0)
|
|
5065
5217
|
notSent(`its failure isn't transient (${isMembraneError(error) ? error.type : boundFailureText(error.message, FAILURE_SUMMARY_MAX_CHARS)})`);
|
|
5066
5218
|
return;
|
|
@@ -5207,12 +5359,15 @@ export class AgentFramework {
|
|
|
5207
5359
|
? `no longer holds primary turns: the primary model is now ${model} (the wait stays recorded)`
|
|
5208
5360
|
: `primary hold ended after ${Date.now() - cooldown.startedAt}ms`));
|
|
5209
5361
|
}
|
|
5210
|
-
if ((cooldown.auxiliaryOrigin || cooldown.
|
|
5362
|
+
if ((cooldown.auxiliaryOrigin || cooldown.emptiedBy) && cooldown.heldRequests.length === 0) {
|
|
5211
5363
|
// 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
|
-
|
|
5364
|
+
// continuation since dropped with its turn, or wakes since withdrawn:
|
|
5365
|
+
// just reopen the gate. A primary the hold left pending won't run, so
|
|
5366
|
+
// its recovery ends.
|
|
5367
|
+
const which = cooldown.emptiedBy === 'drop' ? 'hold, its held continuation dropped,'
|
|
5368
|
+
: cooldown.emptiedBy === 'withdrawal' ? 'hold, its held wakes withdrawn,' : 'auxiliary hold';
|
|
5369
|
+
console.error(`[provider-cooldown] agent=${agentName} ${which} released after ${Date.now() - cooldown.startedAt}ms`);
|
|
5370
|
+
if (cooldown.emptiedBy)
|
|
5216
5371
|
this.endPendingPrimary(agentName);
|
|
5217
5372
|
this.flushAuxiliaryAdmission(agentName);
|
|
5218
5373
|
return;
|
|
@@ -5862,12 +6017,7 @@ export class AgentFramework {
|
|
|
5862
6017
|
*/
|
|
5863
6018
|
pushEvent(event) {
|
|
5864
6019
|
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}`);
|
|
6020
|
+
console.error(droppedToolResultLine(event, 'after stop'));
|
|
5871
6021
|
return;
|
|
5872
6022
|
}
|
|
5873
6023
|
this.queue.push(event);
|
|
@@ -5897,6 +6047,13 @@ export class AgentFramework {
|
|
|
5897
6047
|
else
|
|
5898
6048
|
this.queue.push(event);
|
|
5899
6049
|
}
|
|
6050
|
+
dropRetiredModuleEvent(event, moduleName) {
|
|
6051
|
+
if (this.queue.isClosed) {
|
|
6052
|
+
this.pushModuleEvent(event, moduleName, true);
|
|
6053
|
+
return;
|
|
6054
|
+
}
|
|
6055
|
+
console.error(retiredPushLine(event, moduleName));
|
|
6056
|
+
}
|
|
5900
6057
|
/**
|
|
5901
6058
|
* Add a trace event listener for observability.
|
|
5902
6059
|
*/
|
|
@@ -5951,12 +6108,17 @@ export class AgentFramework {
|
|
|
5951
6108
|
this.traceListeners.splice(index, 1);
|
|
5952
6109
|
}
|
|
5953
6110
|
/**
|
|
5954
|
-
* Add a module at runtime. Refused, with a rejection,
|
|
5955
|
-
*
|
|
5956
|
-
*
|
|
5957
|
-
* past it
|
|
6111
|
+
* Add a module at runtime. Refused, with a rejection, once a stop() has
|
|
6112
|
+
* been admitted, before anything else is checked: while that stop() runs,
|
|
6113
|
+
* a module added then would either be stopped by it or, once it has taken
|
|
6114
|
+
* its list of modules to stop, stay started past it; once it has settled,
|
|
6115
|
+
* the framework has stopped for good, as start() says. A stop() admitted
|
|
6116
|
+
* while the module's start() runs stops the module once that start() has
|
|
6117
|
+
* settled; if that start() fulfils, this rejects the same way then, with
|
|
6118
|
+
* no `module:added`, and if it rejects, this rejects with its error.
|
|
5958
6119
|
*/
|
|
5959
6120
|
async addModule(module) {
|
|
6121
|
+
this.refuseOnceStopped('addModule()');
|
|
5960
6122
|
// A server's tool prefix and a module's name share one namespace, as
|
|
5961
6123
|
// create() checks at startup: a module isn't added under a prefix a
|
|
5962
6124
|
// server holds, or one a restart is moving its server to.
|
|
@@ -5974,8 +6136,8 @@ export class AgentFramework {
|
|
|
5974
6136
|
throw new Error(`Module "${module.name}" collides with MCPL server "${id}"'s toolPrefix "${module.name}" (a closed tab)`);
|
|
5975
6137
|
}
|
|
5976
6138
|
}
|
|
5977
|
-
this.resetCompletedStop('addModule()');
|
|
5978
6139
|
await this.moduleRegistry.addModule(module);
|
|
6140
|
+
this.refuseOnceStopped('addModule()');
|
|
5979
6141
|
// The workspace judges whether an agent's history holds a message as the
|
|
5980
6142
|
// framework judges its own notices, for its head-move notice
|
|
5981
6143
|
// (noticeHeadMoves) and the writes it weighs against it. A module taken
|
|
@@ -5988,7 +6150,9 @@ export class AgentFramework {
|
|
|
5988
6150
|
this.emitTrace({ type: 'module:added', moduleName: module.name });
|
|
5989
6151
|
}
|
|
5990
6152
|
/**
|
|
5991
|
-
* Remove a module at runtime.
|
|
6153
|
+
* Remove a module at runtime. A module whose start() is still running is
|
|
6154
|
+
* stopped once that start() has settled; this waits for that and for its
|
|
6155
|
+
* stop(), with no time limit.
|
|
5992
6156
|
*/
|
|
5993
6157
|
async removeModule(name) {
|
|
5994
6158
|
await this.moduleRegistry.removeModule(name);
|
|
@@ -6297,7 +6461,7 @@ export class AgentFramework {
|
|
|
6297
6461
|
: []),
|
|
6298
6462
|
];
|
|
6299
6463
|
}
|
|
6300
|
-
/** The registered workspace module, if any (used by
|
|
6464
|
+
/** The registered workspace module, if any (used by save_recent_image). */
|
|
6301
6465
|
getWorkspaceModule() {
|
|
6302
6466
|
const module = this.moduleRegistry.getAllModules().find((m) => m.name === 'workspace');
|
|
6303
6467
|
return module && typeof module.writeBinary === 'function'
|
|
@@ -6770,15 +6934,11 @@ export class AgentFramework {
|
|
|
6770
6934
|
stallSkips = 0;
|
|
6771
6935
|
/** See WITHHELD_SPILL_DEADLINE_MS; an instance field so tests can shorten it. */
|
|
6772
6936
|
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;
|
|
6937
|
+
/** Set once a stop() has let the running annotations finish, and cleared
|
|
6938
|
+
* as each stop() begins: while set, a withheld original is no longer
|
|
6939
|
+
* written to a workspace that is stopping, and its stub names the audit
|
|
6940
|
+
* record. */
|
|
6941
|
+
guardSpillsClosed = false;
|
|
6782
6942
|
/** A new agent's guard: its persisted setting, the tool-results spill its
|
|
6783
6943
|
* withheld originals are written to, and the set stop() waits on
|
|
6784
6944
|
* (agent-framework #277). The setting is read from framework/state's
|
|
@@ -7453,7 +7613,7 @@ export class AgentFramework {
|
|
|
7453
7613
|
// Bounded grace for cancelled streams. Stop cancels streams too; a turn
|
|
7454
7614
|
// with no stream settles on its own. Report the abandon that already ran,
|
|
7455
7615
|
// even if stop cuts its grace short.
|
|
7456
|
-
const grace = Date.now() +
|
|
7616
|
+
const grace = Date.now() + (testAbandonGraces.get(this) ?? ABANDON_GRACE_MS);
|
|
7457
7617
|
while (this.quiesced && !this.queue?.isClosed && this.activeTurnTokens.size > 0 && Date.now() < grace) {
|
|
7458
7618
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
7459
7619
|
}
|
|
@@ -7876,8 +8036,9 @@ export class AgentFramework {
|
|
|
7876
8036
|
* memory-only: they land within the turn, at a tool boundary or at its
|
|
7877
8037
|
* end, and a crash before then loses them. That window runs to the turn's
|
|
7878
8038
|
* end for a message held there and what is queued behind it, and, on a
|
|
7879
|
-
* stream that
|
|
7880
|
-
* prefill path
|
|
8039
|
+
* stream that doesn't declare it carries injected messages (membrane's XML
|
|
8040
|
+
* prefill path declares it can't), for every message deferred during the
|
|
8041
|
+
* turn.
|
|
7881
8042
|
*
|
|
7882
8043
|
* A write that fails, or a queue over the cap, which writes nothing,
|
|
7883
8044
|
* leaves the record as it last held it, except that store only, a write
|
|
@@ -7903,14 +8064,15 @@ export class AgentFramework {
|
|
|
7903
8064
|
catch (err) {
|
|
7904
8065
|
console.error('[host-mode] failed to persist deferred context writes:', err);
|
|
7905
8066
|
this.deferredWritesFellBehind({ error: err });
|
|
7906
|
-
return;
|
|
8067
|
+
return false;
|
|
7907
8068
|
}
|
|
7908
8069
|
if (outcome === 'over-cap') {
|
|
7909
8070
|
this.deferredWritesFellBehind('over-cap');
|
|
8071
|
+
return false;
|
|
7910
8072
|
}
|
|
7911
|
-
|
|
8073
|
+
if (outcome !== 'nothing')
|
|
7912
8074
|
this.deferredWritesCaughtUp();
|
|
7913
|
-
|
|
8075
|
+
return true;
|
|
7914
8076
|
}
|
|
7915
8077
|
/**
|
|
7916
8078
|
* Write the queue to its record as it stands, when the host is quiesced
|
|
@@ -9204,8 +9366,9 @@ export class AgentFramework {
|
|
|
9204
9366
|
* **Inheritance.** `mode: 'shared'` (the default) gives the child the
|
|
9205
9367
|
* parent's messages and fold state through shared storage: nothing is
|
|
9206
9368
|
* copied, the child folds with the parent's memory tree, and neither can
|
|
9207
|
-
* alter what the other reads. It needs
|
|
9208
|
-
*
|
|
9369
|
+
* alter what the other reads. It needs a store that supports derivation
|
|
9370
|
+
* (`ContextManager.supportsDerivation`), and is refused, not silently
|
|
9371
|
+
* downgraded, without one. `mode: 'copy'`
|
|
9209
9372
|
* is the historical mechanism — the parent's compiled context re-added as
|
|
9210
9373
|
* the child's own messages — and works everywhere.
|
|
9211
9374
|
*
|
|
@@ -9756,6 +9919,7 @@ export class AgentFramework {
|
|
|
9756
9919
|
if (run) {
|
|
9757
9920
|
run.endReason ??= 'stopped';
|
|
9758
9921
|
run.endedBy ??= options.by;
|
|
9922
|
+
run.endedFromOutside = true;
|
|
9759
9923
|
// The awaiting caller is told who stopped the job and why, whether or
|
|
9760
9924
|
// not a stream was in flight: settled first, so the aborted stream's own
|
|
9761
9925
|
// settle ('Stream cancelled') finds the run already settled. The caller's
|
|
@@ -9978,13 +10142,14 @@ export class AgentFramework {
|
|
|
9978
10142
|
/**
|
|
9979
10143
|
* The creation ("consent") event: names what kind of agent this is, what
|
|
9980
10144
|
* ends it and what happens to it when its spawner ends. A derived agent's
|
|
9981
|
-
* creation is
|
|
9982
|
-
*
|
|
9983
|
-
* host-framed line in the source's context,
|
|
9984
|
-
*
|
|
9985
|
-
*
|
|
9986
|
-
* fork's branch or copy exists, so a fork never
|
|
9987
|
-
* itself; a source that has ended is told
|
|
10145
|
+
* creation is told to the agent it was derived from only when the host
|
|
10146
|
+
* asks for it with a derivationNotice and that returns text: one
|
|
10147
|
+
* host-framed line in the source's context, with no wake, given the
|
|
10148
|
+
* framework's line built from the same facts as the trace. Without one the
|
|
10149
|
+
* source is told nothing; the trace is emitted either way. Both derivation
|
|
10150
|
+
* paths announce after the fork's branch or copy exists, so a fork never
|
|
10151
|
+
* inherits the notice about itself; a source that has ended is told
|
|
10152
|
+
* nothing (lifecycleNotice).
|
|
9988
10153
|
*/
|
|
9989
10154
|
announceAgentCreated(record) {
|
|
9990
10155
|
this.emitTrace({
|
|
@@ -10001,8 +10166,9 @@ export class AgentFramework {
|
|
|
10001
10166
|
...(record.inherit ? { inherit: record.inherit } : {}),
|
|
10002
10167
|
});
|
|
10003
10168
|
const from = record.inherit?.from;
|
|
10004
|
-
if (!from || from === record.name)
|
|
10169
|
+
if (!from || from === record.name || !this.derivationNoticeHook)
|
|
10005
10170
|
return;
|
|
10171
|
+
const hook = this.derivationNoticeHook;
|
|
10006
10172
|
const detail = {
|
|
10007
10173
|
event: 'derived',
|
|
10008
10174
|
agent: record.name,
|
|
@@ -10013,15 +10179,16 @@ export class AgentFramework {
|
|
|
10013
10179
|
...(record.relationships.resultTo ? { resultTo: record.relationships.resultTo } : {}),
|
|
10014
10180
|
lifetime: record.lifetime,
|
|
10015
10181
|
};
|
|
10016
|
-
//
|
|
10017
|
-
// past the threshold, a reader loop that
|
|
10018
|
-
// would fill its source's context with
|
|
10019
|
-
// the
|
|
10020
|
-
// the latest described; every derivation still emits its
|
|
10182
|
+
// With the host's hook: a resumption is rare and always passed to it. A
|
|
10183
|
+
// first incarnation is tallied: past the threshold, a reader loop that
|
|
10184
|
+
// forks a hundred times a session would fill its source's context with
|
|
10185
|
+
// stamps, so the hook is given a count line every `threshold` forks
|
|
10186
|
+
// instead, with the latest described; every derivation still emits its
|
|
10187
|
+
// trace.
|
|
10021
10188
|
const threshold = this.collapseDerivationNoticesAfter;
|
|
10022
10189
|
const tell = (defaultText, extra = {}) => {
|
|
10023
10190
|
try {
|
|
10024
|
-
const text =
|
|
10191
|
+
const text = hook(record, from, defaultText);
|
|
10025
10192
|
if (text !== null)
|
|
10026
10193
|
this.lifecycleNotice(from, text, { ...detail, ...extra });
|
|
10027
10194
|
}
|
|
@@ -10106,6 +10273,7 @@ export class AgentFramework {
|
|
|
10106
10273
|
if (run) {
|
|
10107
10274
|
run.endReason = reason;
|
|
10108
10275
|
run.endedBy = record.ended?.by;
|
|
10276
|
+
run.endedFromOutside = true;
|
|
10109
10277
|
run.settle.reject(new AgentStoppedError(name, reason, record.ended?.by, detail));
|
|
10110
10278
|
return;
|
|
10111
10279
|
}
|
|
@@ -10114,6 +10282,15 @@ export class AgentFramework {
|
|
|
10114
10282
|
return;
|
|
10115
10283
|
this.releaseAgentRuntime(agent, this.channelOfHome(record.homeChannel), undefined, record.lifetime.kind === 'persistent');
|
|
10116
10284
|
}
|
|
10285
|
+
forgetFailureAccounting(name) {
|
|
10286
|
+
this.consecutiveInferenceFailures.delete(name);
|
|
10287
|
+
this.exhaustionRewinds.delete(name);
|
|
10288
|
+
this.refusalStreak.delete(name);
|
|
10289
|
+
this.refusalRewinds.delete(name);
|
|
10290
|
+
this.rewindEpisode.delete(name);
|
|
10291
|
+
if (this.forcedRewind.delete(name))
|
|
10292
|
+
console.error(`[unstick] agent=${name} its /unstick session ended with its agent`);
|
|
10293
|
+
}
|
|
10117
10294
|
/**
|
|
10118
10295
|
* Everything the process holds for a live, non-ephemeral agent that has
|
|
10119
10296
|
* ended, released once, on every path an agent ends by — stopAgent, the
|
|
@@ -10159,8 +10336,10 @@ export class AgentFramework {
|
|
|
10159
10336
|
this.eventGate?.agentGone(name);
|
|
10160
10337
|
this.agents.delete(name);
|
|
10161
10338
|
this.agentConfigs.delete(name);
|
|
10339
|
+
this.deadlineCuts.delete(name);
|
|
10340
|
+
this.freshCallsCut.delete(name);
|
|
10341
|
+
this.forgetFailureAccounting(name);
|
|
10162
10342
|
this.thinkingLossIncidents.forget(name);
|
|
10163
|
-
this.reasoningLoss.forget(name);
|
|
10164
10343
|
// main's manager release: the context manager an ephemeral run or a fork
|
|
10165
10344
|
// opened is released with the agent (releaseManager).
|
|
10166
10345
|
this.releaseManager(agent);
|
|
@@ -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,8 +11106,11 @@ 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
|
-
this.reasoningLoss.forget(agent.name);
|
|
10902
11114
|
// Its pending self-wake ends with it, as a fork's does.
|
|
10903
11115
|
this.eventGate?.agentGone(agent.name);
|
|
10904
11116
|
this.releaseWindowNowOrAfterResume(agent);
|
|
@@ -11063,6 +11275,14 @@ export class AgentFramework {
|
|
|
11063
11275
|
inputSchema: { type: 'object' },
|
|
11064
11276
|
},
|
|
11065
11277
|
];
|
|
11278
|
+
static WAKE_ADD_RULE_FIELDS = {
|
|
11279
|
+
'behavior.debounce': 'debounceMs',
|
|
11280
|
+
'behavior.rate_limit': 'rateLimit',
|
|
11281
|
+
'behavior.passive_sample': 'passiveSample',
|
|
11282
|
+
'behavior.judge': 'judge',
|
|
11283
|
+
'position.before': 'insertBefore',
|
|
11284
|
+
'position.after': 'insertAfter',
|
|
11285
|
+
};
|
|
11066
11286
|
/** Synthesized wake-rule tools: add/remove a gate.json policy at runtime.
|
|
11067
11287
|
* Present when a gate is wired. They write validated policies into the
|
|
11068
11288
|
* hot-reloaded gate.json (same validation as load), so a rule takes effect
|
|
@@ -11244,9 +11464,9 @@ export class AgentFramework {
|
|
|
11244
11464
|
/** Synthesized drafts tool — present whenever MCPL channels are
|
|
11245
11465
|
* configured. A resident's held drafts (src/prose-drafts.ts): plain speech
|
|
11246
11466
|
* 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". */
|
|
11467
|
+
* kept as that module's retention says. Its optional fields are nullable,
|
|
11468
|
+
* as save_recent_image's selectors are: a caller whose provider presents
|
|
11469
|
+
* every property as required can still say "not in use" (fieldInUse). */
|
|
11250
11470
|
static DRAFTS_TOOL = {
|
|
11251
11471
|
name: 'drafts',
|
|
11252
11472
|
description: 'Your held drafts: plain speech of yours that was NOT sent — held back because an explicit send in its turn ' +
|
|
@@ -11285,10 +11505,10 @@ export class AgentFramework {
|
|
|
11285
11505
|
required: ['action'],
|
|
11286
11506
|
},
|
|
11287
11507
|
};
|
|
11288
|
-
/** Synthesized
|
|
11508
|
+
/** Synthesized save_recent_image tool — present when a workspace module is
|
|
11289
11509
|
* registered. Lets the agent persist an image it has already seen in its
|
|
11290
11510
|
* own context (Discord attachments arrive inlined as base64 and are
|
|
11291
|
-
* otherwise unreachable as files). */
|
|
11511
|
+
* otherwise unreachable as files). Its three selectors are nullable. */
|
|
11292
11512
|
static SAVE_IMAGE_TOOL = {
|
|
11293
11513
|
name: 'save_recent_image',
|
|
11294
11514
|
description: 'Save one or more recent images from your own context to workspace files. ' +
|
|
@@ -11303,7 +11523,8 @@ export class AgentFramework {
|
|
|
11303
11523
|
'restart) the call fails and writes nothing — it never substitutes an older ' +
|
|
11304
11524
|
'image. When other tool calls in the same batch are still running, the save ' +
|
|
11305
11525
|
'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.'
|
|
11526
|
+
'files are visible via workspace tools. Select by `ref` or by ' +
|
|
11527
|
+
'`index`/`count`; a selector you are not using may be omitted or passed as null.',
|
|
11307
11528
|
inputSchema: {
|
|
11308
11529
|
type: 'object',
|
|
11309
11530
|
properties: {
|
|
@@ -11311,18 +11532,24 @@ export class AgentFramework {
|
|
|
11311
11532
|
type: 'string',
|
|
11312
11533
|
description: 'Mount-prefixed destination path, e.g. "project/photos/name.png".',
|
|
11313
11534
|
},
|
|
11535
|
+
// Nullable selectors: a caller whose provider presents every property
|
|
11536
|
+
// as required must still be able to say "not this selector". Omission,
|
|
11537
|
+
// null and "" all mean it isn't used (fieldInUse); any other value is
|
|
11538
|
+
// checked. The descriptions name null, the form to ask for.
|
|
11314
11539
|
index: {
|
|
11315
|
-
type: '
|
|
11316
|
-
description: 'Which image, counting back from the most recent (0 = most recent). Default 0.'
|
|
11540
|
+
type: ['integer', 'null'],
|
|
11541
|
+
description: 'Which image, counting back from the most recent (0 = most recent). Default 0. ' +
|
|
11542
|
+
'null, like omitting it, means the default.',
|
|
11317
11543
|
},
|
|
11318
11544
|
count: {
|
|
11319
|
-
type: '
|
|
11320
|
-
description: 'How many images to save, starting at `index` and going further back. Default 1.'
|
|
11545
|
+
type: ['integer', 'null'],
|
|
11546
|
+
description: 'How many images to save, starting at `index` and going further back. Default 1. ' +
|
|
11547
|
+
'null, like omitting it, means the default.',
|
|
11321
11548
|
},
|
|
11322
11549
|
ref: {
|
|
11323
|
-
type: 'string',
|
|
11550
|
+
type: ['string', 'null'],
|
|
11324
11551
|
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
|
|
11552
|
+
'Mutually exclusive with `index`/`count`: pass null for the selector you are not using. null, like omitting it, means no ref.',
|
|
11326
11553
|
},
|
|
11327
11554
|
},
|
|
11328
11555
|
required: ['path'],
|
|
@@ -13797,8 +14024,9 @@ export class AgentFramework {
|
|
|
13797
14024
|
// idiom): reasoning effort (thinking), provider params, token ceilings,
|
|
13798
14025
|
// caching, refusal handling, timezone — a same-model side-process must
|
|
13799
14026
|
// not silently run under different inference conditions than the
|
|
13800
|
-
// resident it serves.
|
|
13801
|
-
// overridden
|
|
14027
|
+
// resident it serves. Below, identity- and surface-defining fields are
|
|
14028
|
+
// overridden, and the window and thinking binding where the
|
|
14029
|
+
// subconscious's config sets its own.
|
|
13802
14030
|
const model = cfg.model ?? primaryConfig.model;
|
|
13803
14031
|
const inherited = { ...primaryConfig };
|
|
13804
14032
|
if (model !== primaryConfig.model) {
|
|
@@ -13807,6 +14035,7 @@ export class AgentFramework {
|
|
|
13807
14035
|
}
|
|
13808
14036
|
const agentConfig = {
|
|
13809
14037
|
...inherited,
|
|
14038
|
+
...readerModelFields(cfg),
|
|
13810
14039
|
name,
|
|
13811
14040
|
model,
|
|
13812
14041
|
systemPrompt: cfg.systemPrompt,
|
|
@@ -13906,6 +14135,7 @@ export class AgentFramework {
|
|
|
13906
14135
|
config: {
|
|
13907
14136
|
model: cfg.model,
|
|
13908
14137
|
proseRouting: 'disabled',
|
|
14138
|
+
...readerModelFields(cfg),
|
|
13909
14139
|
},
|
|
13910
14140
|
inheritRefusals: true,
|
|
13911
14141
|
onParentEnd: 'end',
|
|
@@ -14019,13 +14249,7 @@ export class AgentFramework {
|
|
|
14019
14249
|
this.addMessage(reader, content, metadata);
|
|
14020
14250
|
}
|
|
14021
14251
|
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) {
|
|
14252
|
+
while (this.running) {
|
|
14029
14253
|
try {
|
|
14030
14254
|
await this.processNextEvent();
|
|
14031
14255
|
}
|
|
@@ -14287,18 +14511,21 @@ export class AgentFramework {
|
|
|
14287
14511
|
// compiled prefix and the live request still match without it
|
|
14288
14512
|
// (#232 gap 5).
|
|
14289
14513
|
//
|
|
14290
|
-
//
|
|
14291
|
-
// (supportsInjectedMessages
|
|
14292
|
-
//
|
|
14293
|
-
//
|
|
14294
|
-
//
|
|
14295
|
-
//
|
|
14514
|
+
// Only a stream that declares it carries injected messages
|
|
14515
|
+
// (supportsInjectedMessages true: membrane's native path) gets
|
|
14516
|
+
// them. Membrane's XML prefill path declares false, and a stream
|
|
14517
|
+
// that doesn't say is taken as one that doesn't, so nothing is
|
|
14518
|
+
// drained for either: everything queued for the agent waits for
|
|
14519
|
+
// the turn-end flush, as a held message does, rather than sitting
|
|
14520
|
+
// in the window between rounds the stream never showed it. A
|
|
14521
|
+
// boundary with no stream, the direct API's, drains: its
|
|
14522
|
+
// re-inference compiles from the store.
|
|
14296
14523
|
const midTurnInjections = [];
|
|
14297
14524
|
// What this boundary stored for the agent to hear, in store order,
|
|
14298
14525
|
// for a boundary that leaves it to the turn's next start as
|
|
14299
14526
|
// arrivals: that start's compile sends it.
|
|
14300
14527
|
const heard = [];
|
|
14301
|
-
if (currentState.supportsInjectedMessages
|
|
14528
|
+
if (currentState.supportsInjectedMessages === true || !currentState.stream) {
|
|
14302
14529
|
// A clock row due (storeDueClock) goes right after the round's
|
|
14303
14530
|
// results, ahead of what is drained below, and joins the stream
|
|
14304
14531
|
// the same way. A boundary that passed during the round can't
|
|
@@ -14397,12 +14624,10 @@ export class AgentFramework {
|
|
|
14397
14624
|
}
|
|
14398
14625
|
// A live stream handed these messages after the results follows
|
|
14399
14626
|
// 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
|
|
14627
|
+
// [Continue]: that round's rows carry no prompt. A boundary that
|
|
14628
|
+
// ends the turn, restarts the stream or falls back to a new start
|
|
14629
|
+
// below hands them to no live stream, and a new start reads its own
|
|
14630
|
+
// prompt from
|
|
14406
14631
|
// what it sends. (The routing notice below rides only an injection
|
|
14407
14632
|
// already here.)
|
|
14408
14633
|
if (midTurnInjections.length > 0) {
|
|
@@ -14645,11 +14870,10 @@ export class AgentFramework {
|
|
|
14645
14870
|
}
|
|
14646
14871
|
// Streaming path: convert results and resume the stream.
|
|
14647
14872
|
// 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
|
|
14873
|
+
// messages. Membrane's native path appends them after the
|
|
14874
|
+
// tool_result envelope, so the next round of THIS turn hears
|
|
14875
|
+
// them. Its XML prefill path declares it can't carry them, so
|
|
14876
|
+
// none are collected for it. Each goes as its participant and content
|
|
14653
14877
|
// alone: a stored row's metadata is the host's record, not input
|
|
14654
14878
|
// for membrane. Each goes with its name and text as the store
|
|
14655
14879
|
// keeps them, whether it's a heard message, rendered or not, or a
|
|
@@ -14679,15 +14903,11 @@ export class AgentFramework {
|
|
|
14679
14903
|
// the watermark of the turn's compile and read as unread, so a
|
|
14680
14904
|
// coalesced replacement could remove a message the agent heard
|
|
14681
14905
|
// 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.
|
|
14906
|
+
// Only a stream that declares it carries them gets them, and so
|
|
14907
|
+
// is marked read (supportsInjectedMessages true: membrane's native
|
|
14908
|
+
// path).
|
|
14909
|
+
// The shown mark moves at every boundary that injects into its
|
|
14910
|
+
// stream.
|
|
14691
14911
|
// The mark is the store's head, a scalar, as a compile's is: it
|
|
14692
14912
|
// also covers what was stored since the compile without being
|
|
14693
14913
|
// carried, such as another slot's messages, and a write that
|
|
@@ -14699,8 +14919,7 @@ export class AgentFramework {
|
|
|
14699
14919
|
// replies after it go without their thinking, whatever the mark
|
|
14700
14920
|
// says; deferring those writers is the fix.
|
|
14701
14921
|
if (midTurnInjections.length > 0) {
|
|
14702
|
-
//
|
|
14703
|
-
// mark (CoalescerHost.isUnshown) counts a doubt as shown.
|
|
14922
|
+
// Shown: the stream carries them (CoalescerHost.isUnshown).
|
|
14704
14923
|
agent.markContextShown();
|
|
14705
14924
|
if (currentState.supportsInjectedMessages === true)
|
|
14706
14925
|
agent.markContextConsumed();
|
|
@@ -14749,15 +14968,23 @@ export class AgentFramework {
|
|
|
14749
14968
|
async dispatchProcessEventToModules(event, startTime = Date.now()) {
|
|
14750
14969
|
// Dispatch to all modules, tracking responses with module names
|
|
14751
14970
|
const responses = [];
|
|
14752
|
-
|
|
14971
|
+
const registrations = new Map();
|
|
14972
|
+
const stillRegistered = (entry) => this.moduleRegistry.registrationOf(entry.moduleName) === registrations.get(entry);
|
|
14973
|
+
const dispatched = this.moduleRegistry.getAllModules()
|
|
14974
|
+
.map((module) => ({ module, registration: this.moduleRegistry.registrationOf(module.name) }));
|
|
14975
|
+
for (const { module, registration } of dispatched) {
|
|
14753
14976
|
// The framework's own toolsets module answers no event; every agent has
|
|
14754
14977
|
// it now, so it isn't listed beside the modules that do.
|
|
14755
14978
|
if (module instanceof ToolsetsModule)
|
|
14756
14979
|
continue;
|
|
14980
|
+
if (this.moduleRegistry.registrationOf(module.name) !== registration)
|
|
14981
|
+
continue;
|
|
14757
14982
|
try {
|
|
14758
14983
|
const processState = this.moduleRegistry.createProcessState(module.name);
|
|
14759
14984
|
const response = await module.onProcess(event, processState);
|
|
14760
|
-
|
|
14985
|
+
const entry = { moduleName: module.name, response };
|
|
14986
|
+
responses.push(entry);
|
|
14987
|
+
registrations.set(entry, registration);
|
|
14761
14988
|
}
|
|
14762
14989
|
catch (error) {
|
|
14763
14990
|
console.error(`Module ${module.name} error handling process event:`, error);
|
|
@@ -14783,7 +15010,16 @@ export class AgentFramework {
|
|
|
14783
15010
|
// responses, a tool call's dispatch and the trace go on, since a module
|
|
14784
15011
|
// can't catch a throw on this path, and one module's mustn't cost the
|
|
14785
15012
|
// event.
|
|
14786
|
-
const
|
|
15013
|
+
const dropUnregistered = (entry) => {
|
|
15014
|
+
if (stillRegistered(entry))
|
|
15015
|
+
return false;
|
|
15016
|
+
const parts = responseParts(entry.response);
|
|
15017
|
+
if (parts) {
|
|
15018
|
+
console.error(`[process] module ${entry.moduleName} stopped before its response to ${event.type} was applied, so none of it was: ${parts}`);
|
|
15019
|
+
}
|
|
15020
|
+
return true;
|
|
15021
|
+
};
|
|
15022
|
+
const applying = responses.filter((entry) => !dropUnregistered(entry)).filter(({ moduleName, response }) => {
|
|
14787
15023
|
const refusal = malformedResponseContent(response);
|
|
14788
15024
|
if (refusal === null)
|
|
14789
15025
|
return true;
|
|
@@ -14804,7 +15040,7 @@ export class AgentFramework {
|
|
|
14804
15040
|
const { moduleName, response } = entry;
|
|
14805
15041
|
const step = { at: 'its start' };
|
|
14806
15042
|
try {
|
|
14807
|
-
|
|
15043
|
+
this.applyProcessResponse(response, event, moduleName, surface, step);
|
|
14808
15044
|
}
|
|
14809
15045
|
catch (error) {
|
|
14810
15046
|
entry.threw = { at: step.at, error: error instanceof Error ? error.message : String(error) };
|
|
@@ -14825,7 +15061,7 @@ export class AgentFramework {
|
|
|
14825
15061
|
this.logProcessEvent(event, applying);
|
|
14826
15062
|
}
|
|
14827
15063
|
}
|
|
14828
|
-
|
|
15064
|
+
applyProcessResponse(response, event, moduleName,
|
|
14829
15065
|
/** The event's surface envelope, when it is console/API input. */
|
|
14830
15066
|
surface,
|
|
14831
15067
|
/** Names the operation under way, for the line when one throws. */
|
|
@@ -15576,8 +15812,10 @@ export class AgentFramework {
|
|
|
15576
15812
|
else {
|
|
15577
15813
|
// Nothing live under the name: only what the process keeps by name.
|
|
15578
15814
|
this.closingConversationAgents.delete(agentName);
|
|
15815
|
+
this.deadlineCuts.delete(agentName);
|
|
15816
|
+
this.freshCallsCut.delete(agentName);
|
|
15817
|
+
this.forgetFailureAccounting(agentName);
|
|
15579
15818
|
this.thinkingLossIncidents.forget(agentName);
|
|
15580
|
-
this.reasoningLoss.forget(agentName);
|
|
15581
15819
|
// A continuation of its turn still on its way goes with it.
|
|
15582
15820
|
this.dropContinuations(agentName, { kind: 'dropped', why: 'its agent was disposed' });
|
|
15583
15821
|
this.turnRouting.delete(agentName);
|
|
@@ -16566,19 +16804,20 @@ export class AgentFramework {
|
|
|
16566
16804
|
});
|
|
16567
16805
|
}
|
|
16568
16806
|
}
|
|
16569
|
-
/** Withdraw unstarted wakes whose sole cause is this subject (§4.2)
|
|
16807
|
+
/** Withdraw unstarted wakes whose sole cause is this subject (§4.2), and
|
|
16808
|
+
* a merged start whose every request is withdrawn. */
|
|
16570
16809
|
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
|
|
16810
|
+
// A wake of the subject's merged into a start
|
|
16576
16811
|
// (MERGED_BATCH) is withdrawn from the start's batch, so no stop puts
|
|
16577
16812
|
// it back and no pass unpacks it.
|
|
16813
|
+
const emptied = new Set();
|
|
16578
16814
|
const unrecord = (r) => {
|
|
16579
16815
|
const batch = mergedBatch(r);
|
|
16580
16816
|
if (batch?.some((w) => w.coalescingSubject === subject)) {
|
|
16581
|
-
|
|
16817
|
+
const left = batch.filter((w) => w.coalescingSubject !== subject);
|
|
16818
|
+
r[MERGED_BATCH] = left;
|
|
16819
|
+
if (left.length === 0)
|
|
16820
|
+
emptied.add(r);
|
|
16582
16821
|
}
|
|
16583
16822
|
};
|
|
16584
16823
|
for (const r of this.pendingRequests)
|
|
@@ -16590,6 +16829,29 @@ export class AgentFramework {
|
|
|
16590
16829
|
for (const pass of this.passRequests ?? [])
|
|
16591
16830
|
for (const requests of pass.values())
|
|
16592
16831
|
requests.forEach(unrecord);
|
|
16832
|
+
const withdrawn = (r) => r.coalescingSubject === subject || emptied.has(r);
|
|
16833
|
+
this.pendingRequests = this.pendingRequests.filter((r) => !withdrawn(r));
|
|
16834
|
+
for (const cooldown of this.providerAccelerationCooldowns.values()) {
|
|
16835
|
+
const held = cooldown.heldRequests.length;
|
|
16836
|
+
cooldown.heldRequests = cooldown.heldRequests.filter((r) => !withdrawn(r));
|
|
16837
|
+
if (held > 0 && cooldown.heldRequests.length === 0)
|
|
16838
|
+
cooldown.emptiedBy = 'withdrawal';
|
|
16839
|
+
}
|
|
16840
|
+
for (const pass of this.passRequests ?? []) {
|
|
16841
|
+
for (const requests of pass.values()) {
|
|
16842
|
+
if (!requests.some((r) => emptied.has(r)))
|
|
16843
|
+
continue;
|
|
16844
|
+
const left = requests.filter((r) => !emptied.has(r));
|
|
16845
|
+
requests.length = 0;
|
|
16846
|
+
requests.push(...left);
|
|
16847
|
+
}
|
|
16848
|
+
}
|
|
16849
|
+
for (const start of emptied) {
|
|
16850
|
+
if (!start.coalescingBatch || start.coalescingSubject !== undefined)
|
|
16851
|
+
continue;
|
|
16852
|
+
console.error(`[coalescing] ${start.agentName}: its deferred batches were withdrawn while its wake waited; turn not started (no wake cause left)`);
|
|
16853
|
+
this.emitTrace({ type: 'mcpl:coalescing', kind: 'turn-withdrawn', agentName: start.agentName, subjects: start.coalescingBatchSubjects ?? [] });
|
|
16854
|
+
}
|
|
16593
16855
|
}
|
|
16594
16856
|
/**
|
|
16595
16857
|
* A placement in the deferred queue is upgraded in place once the flush has
|
|
@@ -17099,7 +17361,7 @@ export class AgentFramework {
|
|
|
17099
17361
|
recovery.releasedAt = now;
|
|
17100
17362
|
requests = [...providerCooldown.heldRequests, ...requests];
|
|
17101
17363
|
}
|
|
17102
|
-
// A start that
|
|
17364
|
+
// A start that came back before it took its
|
|
17103
17365
|
// token stands for the batch it was merged from (MERGED_BATCH),
|
|
17104
17366
|
// including one a hold released just now. The batch takes its place,
|
|
17105
17367
|
// each request as it was, judged one by one from here: a wait holds
|
|
@@ -17501,10 +17763,10 @@ export class AgentFramework {
|
|
|
17501
17763
|
start[REARMED] = true;
|
|
17502
17764
|
else
|
|
17503
17765
|
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 (
|
|
17766
|
+
// A start merged from more than one request, any of them not the
|
|
17767
|
+
// turn's, records the batch it was merged from (MERGED_BATCH): until
|
|
17768
|
+
// it takes its token it stands for them all.
|
|
17769
|
+
if (requests.length > 1 && requests.some((r) => !continuesTurn(r)))
|
|
17508
17770
|
start[MERGED_BATCH] = [...requests];
|
|
17509
17771
|
else
|
|
17510
17772
|
delete start[MERGED_BATCH];
|
|
@@ -18154,15 +18416,13 @@ export class AgentFramework {
|
|
|
18154
18416
|
const refuse = (error) => ({ success: false, error, isError: true });
|
|
18155
18417
|
// Plain text, not a JSON-quoted string: draft text must read exactly as held.
|
|
18156
18418
|
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
18419
|
const action = input.action;
|
|
18160
18420
|
if (action !== 'list' && action !== 'read' && action !== 'resend' && action !== 'dismiss') {
|
|
18161
18421
|
return refuse('action must be one of "list", "read", "resend" or "dismiss".');
|
|
18162
18422
|
}
|
|
18163
18423
|
const unused = (field) => refuse(`${field} is not used by action "${action}" — pass null or omit it.`);
|
|
18164
18424
|
let ids = [];
|
|
18165
|
-
if (
|
|
18425
|
+
if (fieldInUse(input.draftIds)) {
|
|
18166
18426
|
if (action === 'list')
|
|
18167
18427
|
return unused('draftIds');
|
|
18168
18428
|
if (!Array.isArray(input.draftIds) || input.draftIds.some((id) => typeof id !== 'string' || !id.trim())) {
|
|
@@ -18174,17 +18434,20 @@ export class AgentFramework {
|
|
|
18174
18434
|
if (ids.length > 20)
|
|
18175
18435
|
return refuse('draftIds names more than 20 drafts; act on them in smaller groups.');
|
|
18176
18436
|
}
|
|
18177
|
-
if (
|
|
18437
|
+
if (fieldInUse(input.destination) && action !== 'resend')
|
|
18178
18438
|
return unused('destination');
|
|
18179
|
-
|
|
18439
|
+
// false is "don't confirm", which every other action already does, so only
|
|
18440
|
+
// true is a confirmDuplicate those actions refuse.
|
|
18441
|
+
if (fieldInUse(input.confirmDuplicate) && input.confirmDuplicate !== false && action !== 'resend') {
|
|
18180
18442
|
return unused('confirmDuplicate');
|
|
18181
|
-
|
|
18443
|
+
}
|
|
18444
|
+
if (fieldInUse(input.offset) && action !== 'list')
|
|
18182
18445
|
return unused('offset');
|
|
18183
18446
|
if (action === 'list') {
|
|
18184
18447
|
let offset = 0;
|
|
18185
|
-
if (
|
|
18448
|
+
if (fieldInUse(input.offset)) {
|
|
18186
18449
|
const raw = input.offset;
|
|
18187
|
-
const n =
|
|
18450
|
+
const n = integerField(raw);
|
|
18188
18451
|
if (!Number.isInteger(n) || n < 0)
|
|
18189
18452
|
return refuse('offset must be a whole number of drafts to skip (0 or more), or null.');
|
|
18190
18453
|
offset = n;
|
|
@@ -18299,13 +18562,13 @@ export class AgentFramework {
|
|
|
18299
18562
|
return refuse('No channels are configured, so there is nowhere to resend to.');
|
|
18300
18563
|
const home = this.homeOf(agentName);
|
|
18301
18564
|
let spec = typeof input.destination === 'string' ? input.destination.trim() : '';
|
|
18302
|
-
if (
|
|
18565
|
+
if (fieldInUse(input.destination) && typeof input.destination !== 'string') {
|
|
18303
18566
|
return refuse('destination must be "#channel", "@person" or a channel id.');
|
|
18304
18567
|
}
|
|
18305
18568
|
if (!spec && !home)
|
|
18306
18569
|
return refuse('resend needs a destination: "#channel", "@person" or a channel id.');
|
|
18307
18570
|
let confirmDuplicate = false;
|
|
18308
|
-
if (
|
|
18571
|
+
if (fieldInUse(input.confirmDuplicate)) {
|
|
18309
18572
|
if (typeof input.confirmDuplicate !== 'boolean')
|
|
18310
18573
|
return refuse('confirmDuplicate must be true, false or null.');
|
|
18311
18574
|
confirmDuplicate = input.confirmDuplicate;
|
|
@@ -19297,28 +19560,6 @@ export class AgentFramework {
|
|
|
19297
19560
|
return;
|
|
19298
19561
|
this.writeChannelNotices(this.channelOpenedNotices(agentName, serverId, channels, cause), `auto-open notice (${cause})`, 'recordChannelAutoOpenNotice');
|
|
19299
19562
|
}
|
|
19300
|
-
tellReasoningLoss(agent, make) {
|
|
19301
|
-
if (this.agents.get(agent.name) !== agent)
|
|
19302
|
-
return;
|
|
19303
|
-
let line;
|
|
19304
|
-
try {
|
|
19305
|
-
line = make();
|
|
19306
|
-
}
|
|
19307
|
-
catch (err) {
|
|
19308
|
-
console.error(`[reasoning] ${agent.name}: could not tell what reasoning was lost:`, err);
|
|
19309
|
-
return;
|
|
19310
|
-
}
|
|
19311
|
-
if (!line)
|
|
19312
|
-
return;
|
|
19313
|
-
try {
|
|
19314
|
-
this.addMessage('user', [{ type: 'text', text: line }], { system: true, kind: 'reasoning-loss' }, { forAgent: agent.name });
|
|
19315
|
-
}
|
|
19316
|
-
catch (err) {
|
|
19317
|
-
console.error(`[reasoning] ${agent.name}: the line for lost reasoning could not be stored:`, err);
|
|
19318
|
-
return;
|
|
19319
|
-
}
|
|
19320
|
-
console.error(`[reasoning] ${agent.name}: told of reasoning no longer in its context`);
|
|
19321
|
-
}
|
|
19322
19563
|
/**
|
|
19323
19564
|
* Write a builder's channel notices in order, each on its own: one that
|
|
19324
19565
|
* can't be written is logged, and the rest are still written. A reader's
|
|
@@ -20615,6 +20856,8 @@ export class AgentFramework {
|
|
|
20615
20856
|
}
|
|
20616
20857
|
}
|
|
20617
20858
|
async startAgentStream(agent, trigger, attempt = 0, providerGateAlreadyHeld = false) {
|
|
20859
|
+
if (attempt === 0)
|
|
20860
|
+
this.deadlineCuts.delete(agent.name);
|
|
20618
20861
|
const ownsProviderGate = this.registry.ownsProviderScheduling(agent.name);
|
|
20619
20862
|
// A stopping framework starts no turn, whoever owns provider admission
|
|
20620
20863
|
// (conversation agents and ephemeral runs do not take the gate).
|
|
@@ -20684,6 +20927,15 @@ export class AgentFramework {
|
|
|
20684
20927
|
console.error(`[provider-admission] ${agent.name}: host quiesced while parked — the merged start goes back as its ${batch.length} requests`);
|
|
20685
20928
|
return;
|
|
20686
20929
|
}
|
|
20930
|
+
if (this.providerAccelerationCooldowns.has(agent.name)) {
|
|
20931
|
+
this.releasePrimaryProviderGate(agent.name);
|
|
20932
|
+
this.eventGate?.onInferenceEnded(agent.name);
|
|
20933
|
+
this.pendingRequests.push(trigger ?? {
|
|
20934
|
+
agentName: agent.name, reason: 'provider-admission:requeue', source: 'scheduler', timestamp: Date.now(),
|
|
20935
|
+
});
|
|
20936
|
+
console.error(`[provider-admission] ${agent.name}: a provider hold stands — parked wake requeued for the scheduler, not started`);
|
|
20937
|
+
return;
|
|
20938
|
+
}
|
|
20687
20939
|
await this.startAgentStream(agent, trigger, attempt, true);
|
|
20688
20940
|
}).catch((error) => {
|
|
20689
20941
|
this.releasePrimaryProviderGate(agent.name);
|
|
@@ -20850,7 +21102,7 @@ export class AgentFramework {
|
|
|
20850
21102
|
const held = hold.heldRequests.length;
|
|
20851
21103
|
hold.heldRequests = take(hold.heldRequests);
|
|
20852
21104
|
if (held > 0 && hold.heldRequests.length === 0)
|
|
20853
|
-
hold.
|
|
21105
|
+
hold.emptiedBy = 'drop';
|
|
20854
21106
|
}
|
|
20855
21107
|
for (const parked of [...this.parkedContinuations]) {
|
|
20856
21108
|
if (!isContinuation(parked))
|
|
@@ -20967,9 +21219,8 @@ export class AgentFramework {
|
|
|
20967
21219
|
* gets inference:aborted with `reason`, and lastInference records its end
|
|
20968
21220
|
* as driveStream's would, when the record is still this turn's. Nothing of
|
|
20969
21221
|
* the kind when another turn of this agent's name holds the turn token (one
|
|
20970
|
-
* begun
|
|
20971
|
-
*
|
|
20972
|
-
* newer turn instead.
|
|
21222
|
+
* begun by a replacement agent object): the event and the record carry only
|
|
21223
|
+
* the name, so this frame's would end that newer turn instead.
|
|
20973
21224
|
*/
|
|
20974
21225
|
abortDroppedTurn(agentName, turnToken, reason, startedAt) {
|
|
20975
21226
|
const owner = this.activeTurnTokens.get(agentName);
|
|
@@ -21144,7 +21395,6 @@ export class AgentFramework {
|
|
|
21144
21395
|
/** This start's window (checkAhead), which its checks read for an
|
|
21145
21396
|
* operator's abandon. */
|
|
21146
21397
|
checkWindow) {
|
|
21147
|
-
const lifecycle = this.lifecycleGeneration;
|
|
21148
21398
|
const resumesTurn = resumesLogicalTurn(trigger?.reason);
|
|
21149
21399
|
let carry = turnCarryOf(trigger);
|
|
21150
21400
|
const turnProseRouting = trigger?.suppressProse ? 'disabled' : agent.proseRouting;
|
|
@@ -21328,7 +21578,7 @@ export class AgentFramework {
|
|
|
21328
21578
|
// everything the turn has decided about where its speech goes.
|
|
21329
21579
|
// - A resume (a budget restart, a tool-result-guard retry) carries its
|
|
21330
21580
|
// turn's request on and keeps the turn's object as it is, and the
|
|
21331
|
-
// turn stays, or
|
|
21581
|
+
// turn stays, or becomes again, the agent's open turn.
|
|
21332
21582
|
// - A silent start routes no prose. It gets an object of its own, with
|
|
21333
21583
|
// no locus, so it neither takes nor disturbs an open turn's. Its
|
|
21334
21584
|
// object is the agent's live routing while it runs, and the open
|
|
@@ -21353,8 +21603,6 @@ export class AgentFramework {
|
|
|
21353
21603
|
let opensTurn = false;
|
|
21354
21604
|
if (resumesTurn) {
|
|
21355
21605
|
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
21606
|
if (!trigger?.suppressProse && !this.openLogical.has(agent.name))
|
|
21359
21607
|
this.openLogical.set(agent.name, routing);
|
|
21360
21608
|
}
|
|
@@ -21383,6 +21631,10 @@ export class AgentFramework {
|
|
|
21383
21631
|
const head = agent.branchHead();
|
|
21384
21632
|
if (head)
|
|
21385
21633
|
routing.start = head;
|
|
21634
|
+
if (!routing.silent)
|
|
21635
|
+
this.pinTurnInputs(agent, routing);
|
|
21636
|
+
else
|
|
21637
|
+
agent.unpinTurnInputs();
|
|
21386
21638
|
}
|
|
21387
21639
|
this.turnRouting.set(agent.name, routing);
|
|
21388
21640
|
if (opensTurn)
|
|
@@ -21527,11 +21779,11 @@ export class AgentFramework {
|
|
|
21527
21779
|
});
|
|
21528
21780
|
// A budget restart continues the same logical inference window. The
|
|
21529
21781
|
// 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
|
|
21782
|
+
// and the successor takes it again if it ended in between: at a
|
|
21532
21783
|
// provider hold that kept the successor from starting (a stated wait at
|
|
21533
|
-
// its last admission check,
|
|
21534
|
-
//
|
|
21784
|
+
// its last admission check, a hold standing when its park behind an
|
|
21785
|
+
// auxiliary call ended, or a cooldown after its own call failed), whose
|
|
21786
|
+
// end queues it again.
|
|
21535
21787
|
if (!resumesTurn) {
|
|
21536
21788
|
this.eventGate?.onInferenceStarted(agent.name);
|
|
21537
21789
|
}
|
|
@@ -21575,9 +21827,6 @@ export class AgentFramework {
|
|
|
21575
21827
|
const name = agent.name;
|
|
21576
21828
|
agent.onCompiledToSend = (messages) => hub.observeCompile(name, messages);
|
|
21577
21829
|
}
|
|
21578
|
-
if (!agent.onThinkingStrip) {
|
|
21579
|
-
agent.onThinkingStrip = (strip, branch) => this.tellReasoningLoss(agent, () => this.reasoningLoss.forStrip(agent.name, branch, strip, (this.activeTurnTokens.has(agent.name) && this.turnRouting.get(agent.name)) || {}, agent.getContextManager()));
|
|
21580
|
-
}
|
|
21581
21830
|
// Module gatherContext (fail-open, per-module timeout — the module's
|
|
21582
21831
|
// contextTimeoutMs, else the registry default). Injections are
|
|
21583
21832
|
// channel-scoped via scopeInjectionsForAgent, matching the MCPL hook
|
|
@@ -21608,13 +21857,11 @@ export class AgentFramework {
|
|
|
21608
21857
|
// An ephemeral watchdog may dispose this Agent while hooks/compile await,
|
|
21609
21858
|
// and stop() may close provider admission. Do not create a provider
|
|
21610
21859
|
// 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) {
|
|
21860
|
+
// as after the compile: these come before a stop kept for the turn,
|
|
21861
|
+
// which goes with it.
|
|
21862
|
+
if (this.agents.get(agent.name) !== agent || this.providerAdmissionClosed) {
|
|
21615
21863
|
const why = this.agents.get(agent.name) !== agent ? 'the agent was replaced or removed'
|
|
21616
|
-
:
|
|
21617
|
-
: 'the framework was stopped and started again';
|
|
21864
|
+
: 'the framework was stopping';
|
|
21618
21865
|
const reason = `a turn assembled while ${why} is dropped before it sends anything`;
|
|
21619
21866
|
console.error(`[turn] ${agent.name}: ${reason}`);
|
|
21620
21867
|
this.endDroppedTurn(agent.name, routing);
|
|
@@ -21655,6 +21902,7 @@ export class AgentFramework {
|
|
|
21655
21902
|
// This turn's retries are the framework's (errorPolicy below), not
|
|
21656
21903
|
// membrane's inside each attempt.
|
|
21657
21904
|
membraneRetries: false,
|
|
21905
|
+
inferenceBounds: this.inferenceBounds,
|
|
21658
21906
|
});
|
|
21659
21907
|
// Each later round this stream sends is noted as it goes (noteRoundSent).
|
|
21660
21908
|
if (noteRequestSent)
|
|
@@ -21662,18 +21910,16 @@ export class AgentFramework {
|
|
|
21662
21910
|
if (carriedBodies)
|
|
21663
21911
|
this.streamCarried.set(stream, new Set(carriedBodies));
|
|
21664
21912
|
// 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.
|
|
21913
|
+
// deadline), so check ownership and admission again.
|
|
21667
21914
|
// Membrane's yielding stream starts its request when it is first
|
|
21668
21915
|
// iterated, so cancelling it here, before driveStream, sends nothing.
|
|
21669
21916
|
// A turn that was mid-compile when stop() cancelled every stream would
|
|
21670
21917
|
// otherwise register after that pass and outlive stop(). The caller's
|
|
21671
21918
|
// finally releases the turn token.
|
|
21672
21919
|
const stopping = this.providerAdmissionClosed;
|
|
21673
|
-
if (this.agents.get(agent.name) !== agent || stopping
|
|
21920
|
+
if (this.agents.get(agent.name) !== agent || stopping) {
|
|
21674
21921
|
const why = this.agents.get(agent.name) !== agent ? 'the agent was replaced or removed'
|
|
21675
|
-
:
|
|
21676
|
-
: 'the framework was stopped and started again';
|
|
21922
|
+
: 'the framework was stopping';
|
|
21677
21923
|
const reason = `a turn compiled while ${why} is dropped before it sends anything`;
|
|
21678
21924
|
console.error(`[turn] ${agent.name}: ${reason}`);
|
|
21679
21925
|
this.cancelUnsentStream(agent, stream, `compiled while ${why}`);
|
|
@@ -21794,14 +22040,12 @@ export class AgentFramework {
|
|
|
21794
22040
|
// catch never holds a compiled request.) As at the checks after the
|
|
21795
22041
|
// hooks and after the compile, a replaced agent or a stopping framework
|
|
21796
22042
|
// 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.
|
|
22043
|
+
// it. A failed start with no stop goes on below as before.
|
|
21799
22044
|
const stopAtFailure = this.takeStartStop(agent, checkWindow);
|
|
21800
22045
|
if (stopAtFailure) {
|
|
21801
|
-
if (this.agents.get(agent.name) !== agent || this.providerAdmissionClosed
|
|
22046
|
+
if (this.agents.get(agent.name) !== agent || this.providerAdmissionClosed) {
|
|
21802
22047
|
const why = this.agents.get(agent.name) !== agent ? 'the agent was replaced or removed'
|
|
21803
|
-
:
|
|
21804
|
-
: 'the framework was stopped and started again';
|
|
22048
|
+
: 'the framework was stopping';
|
|
21805
22049
|
const reason = `a turn whose request couldn't be built while ${why} is dropped before it sends anything`;
|
|
21806
22050
|
console.error(`[turn] ${agent.name}: ${reason}`);
|
|
21807
22051
|
this.endDroppedTurn(agent.name, routing);
|
|
@@ -21813,6 +22057,7 @@ export class AgentFramework {
|
|
|
21813
22057
|
return false;
|
|
21814
22058
|
}
|
|
21815
22059
|
const retry = trigger && withTurnCarry(trigger, carry);
|
|
22060
|
+
const asTransient = this.takeAsTransient(agent.name, err, trigger);
|
|
21816
22061
|
if (ownsProviderGate && this.holdProviderAcceleration(agent, err, retry)) {
|
|
21817
22062
|
// Capacity scheduling is not poisoned history. The cooldown timer owns
|
|
21818
22063
|
// one later fresh compile; no immediate retry or exhaustion marker.
|
|
@@ -21828,23 +22073,28 @@ export class AgentFramework {
|
|
|
21828
22073
|
this.endHeldStart(agent, checkWindow, turnToken, turnStartedAt, routing);
|
|
21829
22074
|
return false;
|
|
21830
22075
|
}
|
|
21831
|
-
const
|
|
21832
|
-
|
|
22076
|
+
const cuts = isDeadlineExpiry(err) ? this.deadlineCuts.get(agent.name) ?? 0 : undefined;
|
|
22077
|
+
const action = statedWait === 'fail' || (isRearmed(trigger) && (!gotPartway(routing) || (isDeadlineExpiry(err) && !asTransient)))
|
|
22078
|
+
? { retry: false } : this.errorPolicy.onInferenceError(err, agent.name, cuts ?? attempt);
|
|
22079
|
+
if (action.retry && cuts !== undefined)
|
|
22080
|
+
this.deadlineCuts.set(agent.name, cuts + 1);
|
|
22081
|
+
else
|
|
22082
|
+
this.deadlineCuts.delete(agent.name);
|
|
22083
|
+
if (!action.retry && cuts !== undefined && cuts > 0)
|
|
22084
|
+
this.freshCallsCut.add(agent.name);
|
|
21833
22085
|
if (action.retry) {
|
|
21834
22086
|
// A policy's delay can be a provider's retry-after: wait all of it.
|
|
21835
22087
|
await this.waitForRetry(action.delayMs, checkWindow, agent);
|
|
21836
|
-
if (this.providerAdmissionClosed
|
|
22088
|
+
if (this.providerAdmissionClosed) {
|
|
21837
22089
|
// 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
|
|
22090
|
+
// it, and a stop kept for the turn goes with it. The turn ends as
|
|
22091
|
+
// one dropped before it sends anything does: a terminal event
|
|
22092
|
+
// saying why, after the inference:failed above, and its record's
|
|
22093
|
+
// end. Nothing is written
|
|
21844
22094
|
// but the [speech-not-posted] notice; this frame still owns its
|
|
21845
22095
|
// token, since no retry frame replaced it.
|
|
21846
22096
|
this.nameUnpostedSpeech(agent, routing);
|
|
21847
|
-
this.abortDroppedTurn(agent.name, turnToken,
|
|
22097
|
+
this.abortDroppedTurn(agent.name, turnToken, RETRY_CANCELLED_BY_STOP, turnStartedAt);
|
|
21848
22098
|
if (this.activeTurnTokens.get(agent.name) === turnToken) {
|
|
21849
22099
|
this.activeTurnTokens.delete(agent.name);
|
|
21850
22100
|
this.activeTurnTriggers.delete(agent.name);
|
|
@@ -21890,7 +22140,7 @@ export class AgentFramework {
|
|
|
21890
22140
|
speech: '',
|
|
21891
22141
|
...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: err.message }),
|
|
21892
22142
|
}, routing);
|
|
21893
|
-
if (isRearmed(trigger) &&
|
|
22143
|
+
if (isRearmed(trigger) && asTransient)
|
|
21894
22144
|
this.exhaustingRearms.add(agent.name);
|
|
21895
22145
|
this.emitTrace({
|
|
21896
22146
|
type: 'inference:exhausted',
|
|
@@ -21911,7 +22161,7 @@ export class AgentFramework {
|
|
|
21911
22161
|
if (action.emit) {
|
|
21912
22162
|
this.pushEvent(action.emit);
|
|
21913
22163
|
}
|
|
21914
|
-
this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, routing);
|
|
22164
|
+
this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, routing, asTransient);
|
|
21915
22165
|
}
|
|
21916
22166
|
// No handoff on either failure branch: the terminal branch deleted the
|
|
21917
22167
|
// token above; the retry branch's recursive startAgentStream replaced
|
|
@@ -22527,7 +22777,7 @@ export class AgentFramework {
|
|
|
22527
22777
|
assistantBlocks = roundContent.filter((b) => b.type !== 'tool_result');
|
|
22528
22778
|
}
|
|
22529
22779
|
else {
|
|
22530
|
-
//
|
|
22780
|
+
// XML tool mode: this round's text and its calls.
|
|
22531
22781
|
// XML mode: persist the verbatim generation on each tool_use
|
|
22532
22782
|
// (membrane#36 round-trip fidelity). context.rawText is the
|
|
22533
22783
|
// parser's fullMatch — the exact <function_calls> block the
|
|
@@ -22539,14 +22789,12 @@ export class AgentFramework {
|
|
|
22539
22789
|
const rawXml = rawText && /^<(antml:)?function_calls>/.test(rawText)
|
|
22540
22790
|
? rawText
|
|
22541
22791
|
: undefined;
|
|
22542
|
-
// Persist THIS round's text only
|
|
22543
|
-
//
|
|
22544
|
-
//
|
|
22545
|
-
//
|
|
22546
|
-
//
|
|
22547
|
-
|
|
22548
|
-
const roundText = event.context.roundPreamble ??
|
|
22549
|
-
event.context.preamble;
|
|
22792
|
+
// Persist THIS round's text only: `roundPreamble` is the
|
|
22793
|
+
// per-round delta. The `preamble` beside it is CUMULATIVE in XML
|
|
22794
|
+
// mode: persisting it per round would store each round's text N
|
|
22795
|
+
// times and re-persist injected <function_results> as the
|
|
22796
|
+
// agent's own words.
|
|
22797
|
+
const roundText = event.context.roundPreamble;
|
|
22550
22798
|
assistantBlocks = [];
|
|
22551
22799
|
if (roundText) {
|
|
22552
22800
|
assistantBlocks.push({ type: 'text', text: roundText });
|
|
@@ -22570,9 +22818,7 @@ export class AgentFramework {
|
|
|
22570
22818
|
this.roundTurns?.set(assistantBlocks, streamRouting);
|
|
22571
22819
|
// Whether this stream can carry messages injected with the round's
|
|
22572
22820
|
// 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.
|
|
22821
|
+
// prefill path. The tool boundary reads it from the agent's state.
|
|
22576
22822
|
agent.enterWaitingForTools(calls, stream, event.context.supportsInjectedMessages);
|
|
22577
22823
|
// Speak-while-acting: route THIS round's prose to the locus NOW
|
|
22578
22824
|
// instead of batching every segment to the end of the turn. Long
|
|
@@ -22610,12 +22856,10 @@ export class AgentFramework {
|
|
|
22610
22856
|
liveProseRouting = true;
|
|
22611
22857
|
// Whether this round's tool-result batch (and anything queued
|
|
22612
22858
|
// 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
|
|
22859
|
+
// again: the stream declares it (true on membrane's native path).
|
|
22860
|
+
// On the XML path, which declares false, a 'now' notice reaches
|
|
22617
22861
|
// the agent at its next compile instead.
|
|
22618
|
-
const presentsInjections = event.context.supportsInjectedMessages
|
|
22862
|
+
const presentsInjections = event.context.supportsInjectedMessages === true;
|
|
22619
22863
|
// Queued under this turn's routing, whenever it posts, and
|
|
22620
22864
|
// streamed by this stream once published.
|
|
22621
22865
|
// This round's request was answered: what it carried is heard.
|
|
@@ -23348,14 +23592,14 @@ export class AgentFramework {
|
|
|
23348
23592
|
// active (native roundContent), each mid-turn round's prose was
|
|
23349
23593
|
// already delivered when that round yielded — only the TRAILING
|
|
23350
23594
|
// prose (blocks after the last tool round) remains here. On the
|
|
23351
|
-
//
|
|
23352
|
-
//
|
|
23595
|
+
// XML tool path (no roundContent, so nothing was live-routed)
|
|
23596
|
+
// the historical behavior applies:
|
|
23353
23597
|
// route the whole turn's parsed segments, once, now. The global
|
|
23354
23598
|
// speech/thoughts split is left untouched (module/TUI rendering
|
|
23355
23599
|
// unaffected) — this only governs what reaches the channel.
|
|
23356
23600
|
//
|
|
23357
23601
|
// Native round routing uses the live sticky flag, which a fresh
|
|
23358
|
-
// injected message can reset. The
|
|
23602
|
+
// injected message can reset. The XML tool path has no
|
|
23359
23603
|
// reliable round boundaries, so retain its historical turn-wide
|
|
23360
23604
|
// scan to avoid double-posting.
|
|
23361
23605
|
const toolCalls = response.content
|
|
@@ -23582,6 +23826,7 @@ export class AgentFramework {
|
|
|
23582
23826
|
break;
|
|
23583
23827
|
this.abandonAgentScripts(agent.name, 'stream error', streamRouting);
|
|
23584
23828
|
const retry = retryOf(trigger);
|
|
23829
|
+
const asTransient = this.takeAsTransient(agent.name, err, trigger);
|
|
23585
23830
|
// A hold takes the turn's retry, or, once stop() has closed
|
|
23586
23831
|
// admission, the turn ends here (endFailureAtStop).
|
|
23587
23832
|
if (ownsProviderGate && this.holdProviderAcceleration(agent, err, retry)) {
|
|
@@ -23597,8 +23842,15 @@ export class AgentFramework {
|
|
|
23597
23842
|
this.eventGate?.onInferenceEnded(agent.name);
|
|
23598
23843
|
break;
|
|
23599
23844
|
}
|
|
23600
|
-
const
|
|
23601
|
-
|
|
23845
|
+
const cuts = isDeadlineExpiry(err) ? this.deadlineCuts.get(agent.name) ?? 0 : undefined;
|
|
23846
|
+
const action = statedWait === 'fail' || (isRearmed(trigger) && (!gotPartway(streamRouting) || (isDeadlineExpiry(err) && !asTransient)))
|
|
23847
|
+
? { retry: false } : this.errorPolicy.onInferenceError(err, agent.name, cuts ?? attempt);
|
|
23848
|
+
if (action.retry && cuts !== undefined)
|
|
23849
|
+
this.deadlineCuts.set(agent.name, cuts + 1);
|
|
23850
|
+
else
|
|
23851
|
+
this.deadlineCuts.delete(agent.name);
|
|
23852
|
+
if (!action.retry && cuts !== undefined && cuts > 0)
|
|
23853
|
+
this.freshCallsCut.add(agent.name);
|
|
23602
23854
|
if (action.retry) {
|
|
23603
23855
|
// A stop made during the wait reaches no stream: the check after
|
|
23604
23856
|
// the wait ends the turn, as the guard's check after its wait
|
|
@@ -23660,7 +23912,7 @@ export class AgentFramework {
|
|
|
23660
23912
|
speech: '',
|
|
23661
23913
|
...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: err.message }),
|
|
23662
23914
|
}, this.agents.get(agent.name) === agent && agent.streamId === myStreamId ? streamRouting : undefined, streamRouting);
|
|
23663
|
-
if (isRearmed(trigger) &&
|
|
23915
|
+
if (isRearmed(trigger) && asTransient)
|
|
23664
23916
|
this.exhaustingRearms.add(agent.name);
|
|
23665
23917
|
this.emitTrace({
|
|
23666
23918
|
type: 'inference:exhausted',
|
|
@@ -23680,7 +23932,7 @@ export class AgentFramework {
|
|
|
23680
23932
|
if (action.emit) {
|
|
23681
23933
|
this.pushEvent(action.emit);
|
|
23682
23934
|
}
|
|
23683
|
-
this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, streamRouting);
|
|
23935
|
+
this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, streamRouting, asTransient);
|
|
23684
23936
|
}
|
|
23685
23937
|
break;
|
|
23686
23938
|
}
|
|
@@ -23755,7 +24007,7 @@ export class AgentFramework {
|
|
|
23755
24007
|
agentName: agent.name,
|
|
23756
24008
|
error: terminal,
|
|
23757
24009
|
});
|
|
23758
|
-
this.rearmExhaustedTurn(agent, new Error(terminal), trigger, ownsProviderGate, streamRouting);
|
|
24010
|
+
this.rearmExhaustedTurn(agent, new Error(terminal), trigger, ownsProviderGate, streamRouting, false);
|
|
23759
24011
|
// Postmortem 2026-05-28 P2 #7: persist the abort to the
|
|
23760
24012
|
// inference log so future investigations can attribute the
|
|
23761
24013
|
// terminal cause without relying on live in-memory reducer
|
|
@@ -23773,6 +24025,8 @@ export class AgentFramework {
|
|
|
23773
24025
|
break;
|
|
23774
24026
|
}
|
|
23775
24027
|
case 'usage': {
|
|
24028
|
+
this.rearmDeadlineCuts.delete(agent.name);
|
|
24029
|
+
this.deadlineCuts.delete(agent.name);
|
|
23776
24030
|
// Cumulative across the stream's rounds: replace, never sum.
|
|
23777
24031
|
latestUsage = event.usage;
|
|
23778
24032
|
// The stream's accumulated fresh input, which is what the
|
|
@@ -23859,9 +24113,6 @@ export class AgentFramework {
|
|
|
23859
24113
|
if (thinkingDrops?.recordedAfter !== undefined) {
|
|
23860
24114
|
this.noteThinkingLoss(agent.name, placedDrops, thinkingDrops.recordedAfter);
|
|
23861
24115
|
}
|
|
23862
|
-
if (thinkingDrops) {
|
|
23863
|
-
this.tellReasoningLoss(agent, () => this.reasoningLoss.forDrops(agent.name, thinkingDrops.branch ?? '', placedDrops, streamRouting, agent.getContextManager()));
|
|
23864
|
-
}
|
|
23865
24116
|
try {
|
|
23866
24117
|
if (!thinkingDroppedInStream) {
|
|
23867
24118
|
strat?.reportRealInputTokens?.(realTotal, compiledRequest ? { system: compiledRequest.system, tools: compiledRequest.tools } : undefined);
|
|
@@ -23995,6 +24246,7 @@ export class AgentFramework {
|
|
|
23995
24246
|
agent.reset();
|
|
23996
24247
|
this.emitTrace({ type: 'inference:failed', agentName: agent.name, error: err.message, stack: err.stack });
|
|
23997
24248
|
const retry = retryOf(trigger);
|
|
24249
|
+
const asTransient = this.takeAsTransient(agent.name, err, trigger);
|
|
23998
24250
|
if (ownsProviderGate && this.holdProviderAcceleration(agent, err, retry)) {
|
|
23999
24251
|
// Once stop() has closed admission, nothing is held, and the turn
|
|
24000
24252
|
// ends here (endFailureAtStop).
|
|
@@ -24032,7 +24284,7 @@ export class AgentFramework {
|
|
|
24032
24284
|
speech: '',
|
|
24033
24285
|
...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: err.message }),
|
|
24034
24286
|
}, streamRouting);
|
|
24035
|
-
if (isRearmed(trigger) &&
|
|
24287
|
+
if (isRearmed(trigger) && asTransient)
|
|
24036
24288
|
this.exhaustingRearms.add(agent.name);
|
|
24037
24289
|
this.emitTrace({
|
|
24038
24290
|
type: 'inference:exhausted',
|
|
@@ -24040,7 +24292,7 @@ export class AgentFramework {
|
|
|
24040
24292
|
error: err.message,
|
|
24041
24293
|
...this.classifyInferenceError(err),
|
|
24042
24294
|
});
|
|
24043
|
-
this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, streamRouting);
|
|
24295
|
+
this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, streamRouting, asTransient);
|
|
24044
24296
|
// Postmortem 2026-05-28 P2 #7: catch-path failures (stream itself
|
|
24045
24297
|
// threw) were previously only visible via in-memory trace listeners.
|
|
24046
24298
|
// Persist to inference log for forensic attribution.
|
|
@@ -24246,7 +24498,11 @@ export class AgentFramework {
|
|
|
24246
24498
|
}
|
|
24247
24499
|
/**
|
|
24248
24500
|
* Execute a tool call and return the result.
|
|
24249
|
-
* Routes
|
|
24501
|
+
* Routes it where the agent's own call of the name goes: to the
|
|
24502
|
+
* framework's own tools the agent has (agent_settings, the workspace and
|
|
24503
|
+
* gate tools, think, journal, prose_help and the rest), the channel tools,
|
|
24504
|
+
* MCPL or the module registry. skip_reply, which ends the turn it's called
|
|
24505
|
+
* in, is refused, since this call isn't made in one.
|
|
24250
24506
|
* Its one caller in the framework is the operator's puppet
|
|
24251
24507
|
* (puppetToolCall), which puts the agent's inline cap on the call as
|
|
24252
24508
|
* resultMaxChars; a host may call it as well.
|
|
@@ -24292,8 +24548,11 @@ export class AgentFramework {
|
|
|
24292
24548
|
* wire ordering. For its own duration the puppet HOLDS the turn-alive
|
|
24293
24549
|
* marker (wakes requeue, cross-turn writers defer), so a wake landing
|
|
24294
24550
|
* during the tool's execution cannot start a turn underneath the pair.
|
|
24295
|
-
* - Refused for tools outside the agent's own surface (
|
|
24296
|
-
*
|
|
24551
|
+
* - Refused for tools outside the agent's own surface (the tools it may
|
|
24552
|
+
* call, availableToolsForPresentation), and for skip_reply, by name or
|
|
24553
|
+
* through a toolsets--act that reaches it, which ends the turn it's
|
|
24554
|
+
* called in: the stored pair must be an act the agent could genuinely
|
|
24555
|
+
* have taken.
|
|
24297
24556
|
* - The call executes FOR REAL through the shared dispatch (MCPL,
|
|
24298
24557
|
* channel tools, utils, modules) with the agent's provenance — a
|
|
24299
24558
|
* puppeted walk_to actually moves the avatar.
|
|
@@ -24334,12 +24593,17 @@ export class AgentFramework {
|
|
|
24334
24593
|
: `puppet refused: agent ${agentName} is ${shown} (requires idle — ` +
|
|
24335
24594
|
`injecting a turn under an active stream corrupts wire ordering)`);
|
|
24336
24595
|
}
|
|
24337
|
-
const onSurface = this.
|
|
24338
|
-
.some((t) => t.name === toolName && agent.canUseTool(t.name));
|
|
24596
|
+
const onSurface = this.availableToolsForPresentation(agentName).some((t) => t.name === toolName);
|
|
24339
24597
|
if (!onSurface) {
|
|
24340
24598
|
throw new Error(`puppet refused: tool ${toolName} is not on ${agentName}'s surface — ` +
|
|
24341
24599
|
`the stored pair must be an act the agent could genuinely have taken`);
|
|
24342
24600
|
}
|
|
24601
|
+
const route = this.toolRoute({ id: '', name: toolName, input }, agentName);
|
|
24602
|
+
const acting = route.kind === 'host-act' && route.host.kind === 'call' ? route.host.name : toolName;
|
|
24603
|
+
if (acting === 'skip_reply') {
|
|
24604
|
+
throw new Error("puppet refused: skip_reply ends the turn it's called in, and a puppeted call isn't made in one, " +
|
|
24605
|
+
'so nothing was run or stored');
|
|
24606
|
+
}
|
|
24343
24607
|
// Anthropic-shaped id so the stored pair is indistinguishable from a
|
|
24344
24608
|
// provider-issued call.
|
|
24345
24609
|
const alphabet = 'ABCDEFGHJKMNPQRSTVWXYZabcdefghjkmnpqrstvwxyz0123456789';
|
|
@@ -24471,96 +24735,119 @@ export class AgentFramework {
|
|
|
24471
24735
|
this.emitTrace({ type: 'tool:failed', module: 'framework', tool: call.name, callId: call.id, error: marked });
|
|
24472
24736
|
return { success: false, error: marked, isError: true };
|
|
24473
24737
|
}
|
|
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
|
-
|
|
24738
|
+
const subject = origin.kind === 'agent' ? origin.agentName : call.callerAgentName ?? '__ephemeral__';
|
|
24739
|
+
const route = this.toolRoute(call, call.callerAgentName);
|
|
24740
|
+
switch (route.kind) {
|
|
24741
|
+
case 'presentation':
|
|
24742
|
+
return this.editToolPresentation(call.callerAgentName ?? '__ephemeral__', call);
|
|
24743
|
+
// toolsets--act on a host toolset's tool (the built-in one, a server's
|
|
24744
|
+
// tab) goes where a programmatic call of that tool goes, under its name.
|
|
24745
|
+
case 'host-act':
|
|
24746
|
+
if (route.host.kind === 'refused')
|
|
24747
|
+
return failed({ success: false, error: route.host.error, isError: true });
|
|
24748
|
+
return this.executeToolCallFrom({ ...call, name: route.host.name, input: route.host.args }, origin);
|
|
24749
|
+
// RFC-014: toolsets--act, resolved to the dynamic tool it calls, goes
|
|
24750
|
+
// where a programmatic call of that tool goes, with its instance.
|
|
24751
|
+
case 'toolset-act': {
|
|
24752
|
+
const target = this.resolveToolsetAct(call);
|
|
24753
|
+
if (!target.ok)
|
|
24754
|
+
return failed({ success: false, error: target.error, isError: true }, this.everyServerRedactor()(target.error));
|
|
24755
|
+
if (origin.kind === 'agent') {
|
|
24756
|
+
const failure = this.agentToolPermissionFailure(origin.agentName, target.call, false);
|
|
24757
|
+
if (failure)
|
|
24758
|
+
return failed(failure, this.everyServerRedactor()(failure.error ?? ''));
|
|
24759
|
+
}
|
|
24760
|
+
return this.executeMcplToolCall(target.call, target.serverId, target.prefix);
|
|
24761
|
+
}
|
|
24762
|
+
case 'mcpl':
|
|
24763
|
+
return this.executeMcplToolCall(call, route.serverId, route.prefix);
|
|
24764
|
+
// Synthesized channel tools. This route was missing: when
|
|
24765
|
+
// subscription-GC moved off the retired MCPL unsubscribe tool onto the
|
|
24766
|
+
// generic channel_close (discord-mcpl b095a9f), module-originated
|
|
24767
|
+
// closes started failing downstream as "Invalid tool name format" —
|
|
24768
|
+
// the janitor had been silently broken since. `origin` is trusted
|
|
24769
|
+
// dispatch context established at the call boundary above, not
|
|
24770
|
+
// inferred from which method was used: only the ModuleRegistry closure
|
|
24771
|
+
// carries module provenance (see handleToolClose).
|
|
24772
|
+
case 'channel': {
|
|
24773
|
+
const turn = call.name === 'channel_publish' && call.callerAgentName ? this.callingTurnOf(call.callerAgentName, call) : undefined;
|
|
24774
|
+
const input = call.name === 'channel_publish' && call.callerAgentName
|
|
24775
|
+
? this.publishInputWithTurnLocus(call.input, call.callerAgentName, turn)
|
|
24776
|
+
: call.input;
|
|
24777
|
+
return this.channelRegistry.handleChannelToolCall(call.name, input, origin.kind === 'agent' && turn ? { ...origin, inTurn: true } : origin);
|
|
24778
|
+
}
|
|
24779
|
+
case 'agent-settings':
|
|
24780
|
+
return this.awaitedToolAnswer((deliver) => this.dispatchAgentSettingsToolCall(subject, call, deliver));
|
|
24781
|
+
case 'save-image':
|
|
24782
|
+
return this.awaitedToolAnswer((deliver) => this.dispatchSaveImageToolCall(subject, call, deliver));
|
|
24783
|
+
case 'read-image':
|
|
24784
|
+
return this.awaitedToolAnswer((deliver) => this.dispatchReadImageToolCall(subject, call, deliver));
|
|
24785
|
+
case 'fetch-reference':
|
|
24786
|
+
return this.awaitedToolAnswer((deliver) => this.dispatchFetchReferenceToolCall(subject, call, deliver));
|
|
24787
|
+
case 'gate-status':
|
|
24788
|
+
return this.awaitedToolAnswer((deliver) => this.dispatchGateToolCall(subject, call, deliver));
|
|
24789
|
+
// The tune-out tool, in process: the same body the dispatcher runs, its
|
|
24790
|
+
// answer awaited here (it can wait for the tune-out's transport).
|
|
24791
|
+
case 'tune-out': {
|
|
24792
|
+
const agentName = call.callerAgentName ?? this.primaryAgentName ?? '__ephemeral__';
|
|
24793
|
+
const input = (call.input ?? {});
|
|
24794
|
+
// The calling turn's own channels, as the channel tools read them here (origin.own).
|
|
24795
|
+
const own = origin.kind === 'agent' ? origin.own ?? [] : [];
|
|
24796
|
+
const turn = this.callingTurnOf(agentName, call);
|
|
24797
|
+
return call.name === 'focus' ? this.handleFocusTool(agentName, input, own, turn) : this.handleTuneOutTool(agentName, input, own, turn);
|
|
24798
|
+
}
|
|
24799
|
+
case 'subconscious': {
|
|
24800
|
+
const routed = this.readerToolCall(subject, call);
|
|
24801
|
+
return 'refused' in routed ? failed({ success: false, error: `${routed.refused}, so the call didn't run.`, isError: true }) : routed.result;
|
|
24802
|
+
}
|
|
24803
|
+
case 'sleep':
|
|
24804
|
+
return this.awaitedToolAnswer((deliver) => this.dispatchSleepToolCall(subject, call, deliver));
|
|
24805
|
+
// The heartbeat's tools, for callers that await a tool's result (an
|
|
24806
|
+
// in-process call, a puppet, a module's callTool). A module is trusted;
|
|
24807
|
+
// an agent is held to what dispatch holds it to.
|
|
24808
|
+
case 'heartbeat': {
|
|
24809
|
+
const refusal = origin.kind === 'module' ? null : this.heartbeatRefusal(call.callerAgentName ?? '__ephemeral__', call);
|
|
24810
|
+
return refusal ? { success: false, error: refusal, isError: true } : this.runHeartbeatTool(call);
|
|
24811
|
+
}
|
|
24812
|
+
case 'wake-rule':
|
|
24813
|
+
return this.awaitedToolAnswer((deliver) => this.dispatchWakeRuleToolCall(subject, call, deliver));
|
|
24814
|
+
case 'event-tags':
|
|
24815
|
+
return this.awaitedToolAnswer((deliver) => this.dispatchEventTagsToolCall(subject, call, deliver));
|
|
24816
|
+
case 'agent-verb':
|
|
24817
|
+
if (origin.kind === 'module')
|
|
24818
|
+
return failed(agentsOwnVerbRefusal(call.name));
|
|
24819
|
+
if (call.name === 'skip_reply') {
|
|
24820
|
+
return failed({
|
|
24821
|
+
success: false,
|
|
24822
|
+
error: "skip_reply ends the turn it's called in, and this call wasn't made in a turn, so nothing was done.",
|
|
24823
|
+
isError: true,
|
|
24824
|
+
});
|
|
24825
|
+
}
|
|
24826
|
+
return this.channelRegistry.handleChannelToolCall(call.name, call.input, origin);
|
|
24827
|
+
// Client-side programmatic tool calling for callers that await a tool's
|
|
24828
|
+
// result: the puppet and a host through executeToolCall, and a module
|
|
24829
|
+
// through callTool. Keyed by callerAgentName, so each agent's scripts are
|
|
24830
|
+
// its own.
|
|
24831
|
+
case 'code-execution':
|
|
24832
|
+
return this.runCodeExecution(call.callerAgentName ?? '__ephemeral__', call);
|
|
24833
|
+
// drafts: the calling resident's own held drafts. A puppeted call has
|
|
24834
|
+
// agent origin and acts on that resident's drafts, as the model's own
|
|
24835
|
+
// call does through dispatchToolCall, but it isn't made inside the
|
|
24836
|
+
// resident's turn, so it takes none of the turn's channels, engages none,
|
|
24837
|
+
// and isn't noted as the resident's speech (callingTurnOf). A module
|
|
24838
|
+
// holds no drafts.
|
|
24839
|
+
case 'drafts':
|
|
24840
|
+
if (origin.kind === 'module')
|
|
24841
|
+
return failed(agentsOwnVerbRefusal(call.name));
|
|
24842
|
+
return this.draftsToolResult(origin.agentName, call.input);
|
|
24843
|
+
case 'prose-help':
|
|
24844
|
+
return { success: true, data: PROSE_ROUTING_HELP };
|
|
24845
|
+
case 'module':
|
|
24846
|
+
// Module tools. The caller uses the result in process, so it gets the
|
|
24847
|
+
// module's own result, as the module returned it, less a refusal it
|
|
24848
|
+
// claims, which is the dispatcher's to say.
|
|
24849
|
+
return this.moduleRegistry.handleToolCall(call);
|
|
24562
24850
|
}
|
|
24563
|
-
return this.moduleRegistry.handleToolCall(call);
|
|
24564
24851
|
}
|
|
24565
24852
|
/**
|
|
24566
24853
|
* RFC-014: the calls toolsets--act resolved to a server's dynamic tool,
|
|
@@ -25288,7 +25575,7 @@ export class AgentFramework {
|
|
|
25288
25575
|
// this write's own, rather than a timer Node would cut to 1 ms.
|
|
25289
25576
|
if (!Number.isFinite(deadline))
|
|
25290
25577
|
deadline = Date.now() + this.withheldSpillDeadlineMs;
|
|
25291
|
-
if (this.
|
|
25578
|
+
if (this.guardSpillsClosed)
|
|
25292
25579
|
return null;
|
|
25293
25580
|
const place = this.toolResultFilePlace(label);
|
|
25294
25581
|
if (!place)
|
|
@@ -26341,6 +26628,71 @@ export class AgentFramework {
|
|
|
26341
26628
|
const refused = bareToolName(call.name) === 'skip_reply' && (!permitted || this.refusesSelfWake(call));
|
|
26342
26629
|
return { name: call.name, cause: permitted || refused ? silenceCauseOf([call]) : null, refused, permitted };
|
|
26343
26630
|
}
|
|
26631
|
+
toolRoute(call, actor) {
|
|
26632
|
+
const name = call.name;
|
|
26633
|
+
if (isPresentationTool(name))
|
|
26634
|
+
return { kind: 'presentation' };
|
|
26635
|
+
if (name === META_TOOL_NAMES.act) {
|
|
26636
|
+
const host = actor === undefined ? null : this.hostPresentation?.resolveAct(actor, call.input) ?? null;
|
|
26637
|
+
if (host)
|
|
26638
|
+
return { kind: 'host-act', host };
|
|
26639
|
+
if (this.toolsetHub && this.mcplServerRegistry)
|
|
26640
|
+
return { kind: 'toolset-act' };
|
|
26641
|
+
}
|
|
26642
|
+
const mcpl = this.mcplServerRegistry ? this.resolveMcplTool(name) : null;
|
|
26643
|
+
if (mcpl)
|
|
26644
|
+
return { kind: 'mcpl', serverId: mcpl[0], prefix: mcpl[1] };
|
|
26645
|
+
if (name.startsWith('channel_') && this.channelRegistry)
|
|
26646
|
+
return { kind: 'channel' };
|
|
26647
|
+
if (name === 'agent_settings')
|
|
26648
|
+
return { kind: 'agent-settings' };
|
|
26649
|
+
if (name === 'save_recent_image')
|
|
26650
|
+
return { kind: 'save-image' };
|
|
26651
|
+
if (name === 'read_image')
|
|
26652
|
+
return { kind: 'read-image' };
|
|
26653
|
+
if (name === 'fetch_reference')
|
|
26654
|
+
return { kind: 'fetch-reference' };
|
|
26655
|
+
if (name === 'gate_status' && this.eventGate)
|
|
26656
|
+
return { kind: 'gate-status' };
|
|
26657
|
+
if ((name === 'tune_out' || name === 'focus') && this.tuneOutCoordinator)
|
|
26658
|
+
return { kind: 'tune-out' };
|
|
26659
|
+
if (SUBCONSCIOUS_TOOL_NAMES.includes(name))
|
|
26660
|
+
return { kind: 'subconscious' };
|
|
26661
|
+
if ((name === 'sleep' || name === 'wake') && this.eventGate)
|
|
26662
|
+
return { kind: 'sleep' };
|
|
26663
|
+
if (this.heartbeat?.ownsTool(name))
|
|
26664
|
+
return { kind: 'heartbeat' };
|
|
26665
|
+
if ((name === 'wake_add_rule' || name === 'wake_remove_rule') && this.eventGate)
|
|
26666
|
+
return { kind: 'wake-rule' };
|
|
26667
|
+
if (name === 'event_tags' && this.eventGate)
|
|
26668
|
+
return { kind: 'event-tags' };
|
|
26669
|
+
if ((name === 'think' || name === 'journal' || name === 'skip_reply') && this.channelRegistry)
|
|
26670
|
+
return { kind: 'agent-verb' };
|
|
26671
|
+
if (name === CODE_EXECUTION_TOOL_NAME && this.codeExecution)
|
|
26672
|
+
return { kind: 'code-execution' };
|
|
26673
|
+
if (name === 'drafts' && this.channelRegistry)
|
|
26674
|
+
return { kind: 'drafts' };
|
|
26675
|
+
if (name === PROSE_HELP_TOOL.name)
|
|
26676
|
+
return { kind: 'prose-help' };
|
|
26677
|
+
return { kind: 'module' };
|
|
26678
|
+
}
|
|
26679
|
+
pushToolResult(agentName, call, moduleName) {
|
|
26680
|
+
return (result) => this.pushEvent({ type: 'tool-result', callId: call.id, toolName: call.name, agentName, moduleName, result });
|
|
26681
|
+
}
|
|
26682
|
+
awaitedToolAnswer(run) {
|
|
26683
|
+
return new Promise((resolve) => {
|
|
26684
|
+
try {
|
|
26685
|
+
run((result) => {
|
|
26686
|
+
const answer = { ...result };
|
|
26687
|
+
delete answer.refused;
|
|
26688
|
+
resolve(answer);
|
|
26689
|
+
});
|
|
26690
|
+
}
|
|
26691
|
+
catch (error) {
|
|
26692
|
+
resolve({ success: false, error: thrownText(error), isError: true });
|
|
26693
|
+
}
|
|
26694
|
+
});
|
|
26695
|
+
}
|
|
26344
26696
|
/**
|
|
26345
26697
|
* Dispatch a call the agent made. `turnId` is the turn it was made in
|
|
26346
26698
|
* (TurnRouting.turnId), as its caller knows it: a model's call, the turn
|
|
@@ -26392,200 +26744,190 @@ export class AgentFramework {
|
|
|
26392
26744
|
enrichedCall.callerTurnId = turnId;
|
|
26393
26745
|
else
|
|
26394
26746
|
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);
|
|
26747
|
+
const route = this.toolRoute(enrichedCall, agentName);
|
|
26748
|
+
switch (route.kind) {
|
|
26749
|
+
case 'presentation': {
|
|
26750
|
+
const result = this.editToolPresentation(agentName, enrichedCall);
|
|
26751
|
+
this.pushEvent({ type: 'tool-result', callId: call.id, toolName: call.name, agentName, moduleName: 'tool-presentation', result });
|
|
26407
26752
|
return;
|
|
26408
26753
|
}
|
|
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
|
-
|
|
26754
|
+
// toolsets--act on a host toolset's tool (the built-in one, a server's
|
|
26755
|
+
// tab): the call of that tool, under its own name, down the path a
|
|
26756
|
+
// direct call of it takes, permissions included.
|
|
26757
|
+
case 'host-act':
|
|
26758
|
+
this.dispatchHostAct(agentName, call, route.host, turnId);
|
|
26759
|
+
return;
|
|
26760
|
+
// RFC-014: toolsets--act calls a server's dynamic tool, down the same
|
|
26761
|
+
// path as a direct call of it (dispatchToolsetAct).
|
|
26762
|
+
case 'toolset-act': {
|
|
26763
|
+
const scriptWaiter = this.scriptToolWaiters.get(enrichedCall.id);
|
|
26764
|
+
if (scriptWaiter)
|
|
26765
|
+
scriptWaiter.route.toServer = true;
|
|
26766
|
+
this.dispatchToolsetAct(agentName, enrichedCall);
|
|
26767
|
+
return;
|
|
26768
|
+
}
|
|
26769
|
+
case 'mcpl': {
|
|
26770
|
+
// A script's call: its waiter keeps that it went to a server, for the
|
|
26771
|
+
// script lane's exit (scriptToolWaiters).
|
|
26772
|
+
const scriptWaiter = this.scriptToolWaiters.get(enrichedCall.id);
|
|
26773
|
+
if (scriptWaiter)
|
|
26774
|
+
scriptWaiter.route.toServer = true;
|
|
26775
|
+
this.dispatchMcplToolCall(agentName, enrichedCall, route.serverId, route.prefix);
|
|
26776
|
+
return;
|
|
26777
|
+
}
|
|
26778
|
+
case 'channel': {
|
|
26779
|
+
this.dispatchChannelToolCall(agentName, enrichedCall);
|
|
26780
|
+
return;
|
|
26781
|
+
}
|
|
26782
|
+
case 'agent-settings': {
|
|
26783
|
+
this.dispatchAgentSettingsToolCall(agentName, enrichedCall);
|
|
26784
|
+
return;
|
|
26785
|
+
}
|
|
26786
|
+
case 'save-image': {
|
|
26787
|
+
this.dispatchSaveImageToolCall(agentName, enrichedCall);
|
|
26788
|
+
return;
|
|
26789
|
+
}
|
|
26790
|
+
case 'read-image': {
|
|
26791
|
+
this.dispatchReadImageToolCall(agentName, enrichedCall);
|
|
26792
|
+
return;
|
|
26793
|
+
}
|
|
26794
|
+
case 'fetch-reference': {
|
|
26795
|
+
this.dispatchFetchReferenceToolCall(agentName, enrichedCall);
|
|
26796
|
+
return;
|
|
26797
|
+
}
|
|
26798
|
+
case 'gate-status': {
|
|
26799
|
+
this.dispatchGateToolCall(agentName, enrichedCall);
|
|
26800
|
+
return;
|
|
26801
|
+
}
|
|
26802
|
+
case 'tune-out': {
|
|
26803
|
+
this.dispatchTuneOutToolCall(agentName, enrichedCall);
|
|
26804
|
+
return;
|
|
26805
|
+
}
|
|
26806
|
+
case 'subconscious': {
|
|
26807
|
+
const routed = this.readerToolCall(agentName, enrichedCall);
|
|
26808
|
+
if ('refused' in routed) {
|
|
26809
|
+
const error = routed.refused;
|
|
26810
|
+
const result = this.refusedResult(agentName, call.id, error);
|
|
26811
|
+
this.emitTrace({ type: 'tool:failed', module: 'tune-out', tool: call.name, callId: call.id, error });
|
|
26812
|
+
this.pushEvent({
|
|
26813
|
+
type: 'tool-result', callId: call.id, toolName: call.name, agentName, moduleName: 'tune-out', result,
|
|
26814
|
+
});
|
|
26815
|
+
return;
|
|
26816
|
+
}
|
|
26817
|
+
void routed.result
|
|
26818
|
+
.then((result) => {
|
|
26819
|
+
// Through pushEvent, like every other tool's result, so one that
|
|
26820
|
+
// arrives after stop() is dropped rather than rejected unhandled.
|
|
26821
|
+
this.pushEvent({
|
|
26822
|
+
type: 'tool-result',
|
|
26823
|
+
callId: enrichedCall.id,
|
|
26824
|
+
toolName: enrichedCall.name,
|
|
26825
|
+
agentName,
|
|
26826
|
+
moduleName: 'tune-out',
|
|
26827
|
+
result,
|
|
26828
|
+
});
|
|
26469
26829
|
});
|
|
26470
26830
|
return;
|
|
26471
26831
|
}
|
|
26472
|
-
|
|
26473
|
-
.
|
|
26474
|
-
|
|
26475
|
-
|
|
26832
|
+
case 'sleep': {
|
|
26833
|
+
this.dispatchSleepToolCall(agentName, enrichedCall);
|
|
26834
|
+
return;
|
|
26835
|
+
}
|
|
26836
|
+
case 'heartbeat': {
|
|
26837
|
+
this.dispatchHeartbeatToolCall(agentName, enrichedCall);
|
|
26838
|
+
return;
|
|
26839
|
+
}
|
|
26840
|
+
case 'wake-rule': {
|
|
26841
|
+
this.dispatchWakeRuleToolCall(agentName, enrichedCall);
|
|
26842
|
+
return;
|
|
26843
|
+
}
|
|
26844
|
+
case 'event-tags': {
|
|
26845
|
+
this.dispatchEventTagsToolCall(agentName, enrichedCall);
|
|
26846
|
+
return;
|
|
26847
|
+
}
|
|
26848
|
+
case 'agent-verb': {
|
|
26849
|
+
// skip_reply(wake_in_seconds): arm a gate self-wake so "not replying
|
|
26850
|
+
// NOW" can also mean "back in a moment" — ends the turn, then wakes
|
|
26851
|
+
// the agent after N seconds unless something else wakes it first
|
|
26852
|
+
// (any turn start cancels the pending self-wake). Armed here because
|
|
26853
|
+
// the framework owns the EventGate; the registry only words the
|
|
26854
|
+
// confirmation. A wake the host can't set is refused, so the model
|
|
26855
|
+
// learns that its turn remains open and no wake was armed: any
|
|
26856
|
+
// wake_in_seconds without a gate, and one that isn't a number of
|
|
26857
|
+
// seconds with one.
|
|
26858
|
+
if (enrichedCall.name === 'skip_reply') {
|
|
26859
|
+
const error = this.skipReplyRefusal(enrichedCall);
|
|
26860
|
+
if (error !== undefined) {
|
|
26861
|
+
const wakeValue = enrichedCall.input?.wake_in_seconds;
|
|
26862
|
+
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)')}`);
|
|
26863
|
+
const result = this.refusedResult(agentName, enrichedCall.id, error);
|
|
26864
|
+
this.emitTrace({ type: 'tool:failed', module: 'channels', tool: enrichedCall.name, callId: enrichedCall.id, error });
|
|
26865
|
+
this.pushEvent({ type: 'tool-result', callId: enrichedCall.id, toolName: enrichedCall.name, agentName, moduleName: 'channels', result });
|
|
26866
|
+
return;
|
|
26867
|
+
}
|
|
26868
|
+
const skipping = this.callingTurnOf(agentName, enrichedCall);
|
|
26869
|
+
if (skipping) {
|
|
26870
|
+
skipping.privateEnd = true;
|
|
26871
|
+
delete skipping.unpostedSpeech;
|
|
26872
|
+
}
|
|
26873
|
+
const wake = readSkipReplyWake(enrichedCall.input);
|
|
26874
|
+
if (wake.kind === 'wake' && this.eventGate) {
|
|
26875
|
+
const armedAt = Date.now();
|
|
26876
|
+
const { inMs, armed } = this.eventGate.armSelfWake(agentName, wake.seconds, 'skip_reply');
|
|
26877
|
+
// The heartbeat's tick or reminder due before the timer: if it wakes
|
|
26878
|
+
// the agent, its turn cancels the timer, so the confirmation names
|
|
26879
|
+
// it and the time the self-wake would have left.
|
|
26880
|
+
const earlierWake = armed ? this.heartbeatWakeBefore(agentName, armedAt + inMs) : undefined;
|
|
26881
|
+
console.error(armed
|
|
26882
|
+
? `[self-wake] agent=${agentName} armed in ${Math.round(inMs / 1000)}s (skip_reply)`
|
|
26883
|
+
+ (earlierWake ? `; due sooner, in ~${earlierWake.inSeconds}s: ${logDetail(earlierWake.what)}` : '')
|
|
26884
|
+
: `[self-wake] agent=${agentName} not armed: the framework has stopped (skip_reply)`);
|
|
26885
|
+
this.dispatchChannelToolCall(agentName, enrichedCall, earlierWake);
|
|
26886
|
+
return;
|
|
26887
|
+
}
|
|
26888
|
+
}
|
|
26889
|
+
this.dispatchChannelToolCall(agentName, enrichedCall);
|
|
26890
|
+
return;
|
|
26891
|
+
}
|
|
26892
|
+
// Client-side programmatic tool calling: run a script that calls the
|
|
26893
|
+
// agent's other tools in-process (script-inner calls come back through
|
|
26894
|
+
// this very dispatcher with waiter-tracked call IDs).
|
|
26895
|
+
case 'code-execution': {
|
|
26896
|
+
this.dispatchCodeExecutionToolCall(agentName, enrichedCall);
|
|
26897
|
+
return;
|
|
26898
|
+
}
|
|
26899
|
+
// drafts: the resident's own held drafts (list/read/resend/dismiss).
|
|
26900
|
+
// Answered by the framework; resend publishes through the registry's
|
|
26901
|
+
// publish executor, so the result arrives asynchronously.
|
|
26902
|
+
case 'drafts': {
|
|
26903
|
+
const startedAt = Date.now();
|
|
26904
|
+
this.emitTrace({ type: 'tool:started', module: 'framework', tool: 'drafts', callId: enrichedCall.id, input: enrichedCall.input });
|
|
26905
|
+
void this.draftsToolResult(agentName, enrichedCall.input, this.callingTurnOf(agentName, enrichedCall), enrichedCall.id)
|
|
26906
|
+
.then((result) => {
|
|
26907
|
+
this.emitTrace({ type: 'tool:completed', module: 'framework', tool: 'drafts', callId: enrichedCall.id, durationMs: Date.now() - startedAt });
|
|
26908
|
+
this.pushEvent({ type: 'tool-result', callId: enrichedCall.id, toolName: enrichedCall.name, agentName, moduleName: 'framework', result });
|
|
26909
|
+
});
|
|
26910
|
+
return;
|
|
26911
|
+
}
|
|
26912
|
+
// prose_help: on-demand routing reference for explicit-prose agents.
|
|
26913
|
+
// Answered inline — the grammar is a tool RESULT (model-requested), never
|
|
26914
|
+
// ambient context (see PROSE_ROUTING_HELP / teach-by-bounce).
|
|
26915
|
+
case 'prose-help': {
|
|
26916
|
+
this.emitTrace({ type: 'tool:completed', module: 'framework', tool: 'prose_help', callId: enrichedCall.id, durationMs: 0 });
|
|
26476
26917
|
this.pushEvent({
|
|
26477
26918
|
type: 'tool-result',
|
|
26478
26919
|
callId: enrichedCall.id,
|
|
26479
26920
|
toolName: enrichedCall.name,
|
|
26480
26921
|
agentName,
|
|
26481
|
-
moduleName: '
|
|
26482
|
-
result,
|
|
26922
|
+
moduleName: 'framework',
|
|
26923
|
+
result: { success: true, data: PROSE_ROUTING_HELP },
|
|
26483
26924
|
});
|
|
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
|
-
}
|
|
26925
|
+
return;
|
|
26551
26926
|
}
|
|
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;
|
|
26927
|
+
case 'module':
|
|
26928
|
+
break;
|
|
26929
|
+
default:
|
|
26930
|
+
route;
|
|
26589
26931
|
}
|
|
26590
26932
|
const sepIndex = enrichedCall.name.indexOf('--');
|
|
26591
26933
|
const moduleName = sepIndex >= 0 ? enrichedCall.name.substring(0, sepIndex) : 'unknown';
|
|
@@ -26936,6 +27278,7 @@ export class AgentFramework {
|
|
|
26936
27278
|
const agent = this.agents.get(agentName);
|
|
26937
27279
|
if ((agent && !this.agentSees(agent, metadata)) || isOwnScheduleWrite(metadata) || !isConversationalInjection(metadata))
|
|
26938
27280
|
return;
|
|
27281
|
+
this.rearmDeadlineCuts?.delete(agentName);
|
|
26939
27282
|
if (this.turnRearms?.delete(agentName))
|
|
26940
27283
|
console.error(`[inference-rearm] agent=${agentName} re-send count started again: a message was said to it`);
|
|
26941
27284
|
}
|
|
@@ -27656,11 +27999,9 @@ export class AgentFramework {
|
|
|
27656
27999
|
* at a tool boundary whose stream declares it carries the messages
|
|
27657
28000
|
* injected there (markContextConsumed). So a notice such a stream carried
|
|
27658
28001
|
* 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.
|
|
28002
|
+
* a budget restart, the direct API's boundary, which has no stream) is
|
|
28003
|
+
* above it until a compile sends it, and counts as unread. null when the
|
|
28004
|
+
* window can't be read, which is logged.
|
|
27664
28005
|
*/
|
|
27665
28006
|
releasedWindow(agent, held) {
|
|
27666
28007
|
try {
|
|
@@ -28765,6 +29106,7 @@ export class AgentFramework {
|
|
|
28765
29106
|
const ended = this.exhaustedTurns?.get(agentName);
|
|
28766
29107
|
this.exhaustedTurns?.delete(agentName);
|
|
28767
29108
|
const rearmed = this.exhaustingRearms?.delete(agentName) === true;
|
|
29109
|
+
const madeAgain = this.freshCallsCut?.delete(agentName) === true;
|
|
28768
29110
|
// The reason is the failure's own message, and a provider can echo the
|
|
28769
29111
|
// whole rejected request in it: one household's [inference-failed]
|
|
28770
29112
|
// marker carried ~1.7 MB of its own request back into its context. Every
|
|
@@ -28815,24 +29157,25 @@ export class AgentFramework {
|
|
|
28815
29157
|
// (2) Agent-facing chronicle marker (no inference triggered → no loop).
|
|
28816
29158
|
const agent = this.agents.get(agentName);
|
|
28817
29159
|
if (agent && !(rearmed && !ended) && process.env.SUPPRESS_INFERENCE_FAILED_MARKER !== '1') {
|
|
28818
|
-
const
|
|
28819
|
-
|
|
28820
|
-
.
|
|
29160
|
+
const timedOut = errorType === 'timeout';
|
|
29161
|
+
const causeParts = [errorType, httpStatus !== undefined ? `HTTP ${httpStatus}` : undefined]
|
|
29162
|
+
.filter((part) => part !== undefined);
|
|
29163
|
+
const cause = timedOut && causeParts.length === 1 ? '' : causeParts.join(', ');
|
|
28821
29164
|
try {
|
|
28822
29165
|
agent.getContextManager().addMessage('user', [{
|
|
28823
29166
|
type: 'text',
|
|
28824
29167
|
text: (ended
|
|
28825
|
-
? `[${NOTICE_TAG.inferenceFailed}] Your previous turn did not complete:
|
|
28826
|
-
|
|
29168
|
+
? `[${NOTICE_TAG.inferenceFailed}] Your previous turn did not complete: ` +
|
|
29169
|
+
`${timedOut ? 'its last call timed out' : 'it failed'}${cause ? ` (${suppliedValue(cause)})` : ''}.` +
|
|
29170
|
+
`${madeAgain ? ` ${MADE_AGAIN}` : ''}` +
|
|
28827
29171
|
`${ended.roundsRan ? ' Its earlier rounds ran, and their tool calls ran as their results show.' : ''}` +
|
|
28828
29172
|
`${ended.callsCutOff ? ` ${CALLS_CUT_OFF}` : ''}` +
|
|
28829
29173
|
`${ended.liveSpeech ? ` ${LIVE_SPEECH_UNRECEIPTED}` : ''}` +
|
|
28830
29174
|
`${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).`,
|
|
29175
|
+
: `[${NOTICE_TAG.inferenceFailed}] Your previous turn did not complete: ` +
|
|
29176
|
+
`the model call ${timedOut ? 'timed out' : 'failed'}${cause ? ` (${suppliedValue(cause)})` : ''} ` +
|
|
29177
|
+
`and produced no response, so nothing was sent.${madeAgain ? ` ${MADE_AGAIN}` : ''} Reason: `) +
|
|
29178
|
+
`${failureShown(markerReason)}. ${timedOut ? TIMEOUT_ADVICE : FAILURE_ADVICE}`,
|
|
28836
29179
|
}], { system: true, kind: 'inference-failed', reason: markerReason, consecutive: streak, ...classification, ...cutAt(markerReason) });
|
|
28837
29180
|
}
|
|
28838
29181
|
catch (err) {
|
|
@@ -29043,7 +29386,7 @@ export class AgentFramework {
|
|
|
29043
29386
|
return source;
|
|
29044
29387
|
});
|
|
29045
29388
|
// Server-initiated inference router (Step 6)
|
|
29046
|
-
this.inferenceRouter = new InferenceRouter(this.membrane, this.hookOrchestrator, this.featureSetManager, inferenceRouting ?? null, (event) => this.emitTrace(event));
|
|
29389
|
+
this.inferenceRouter = new InferenceRouter(this.membrane, this.hookOrchestrator, this.featureSetManager, inferenceRouting ?? null, (event) => this.emitTrace(event), () => this.inferenceBounds);
|
|
29047
29390
|
// Checkpoint manager (Step 8), built and loaded at the top
|
|
29048
29391
|
this.checkpointManager = checkpoints;
|
|
29049
29392
|
// Channel registry (Step 7)
|
|
@@ -29076,7 +29419,15 @@ export class AgentFramework {
|
|
|
29076
29419
|
const scope = coordinator.getAllExcept();
|
|
29077
29420
|
if (!scope || scope.params.except.serverId !== serverId || scope.params.except.channelId !== channelId)
|
|
29078
29421
|
return undefined;
|
|
29079
|
-
|
|
29422
|
+
let released;
|
|
29423
|
+
try {
|
|
29424
|
+
released = coordinator.cancelAllExcept('channel-close', 'the excepted channel was closed');
|
|
29425
|
+
}
|
|
29426
|
+
catch (err) {
|
|
29427
|
+
throw new Error(`The close wasn't recorded: ${err instanceof Error ? err.message : String(err)}`);
|
|
29428
|
+
}
|
|
29429
|
+
if (!released.ok)
|
|
29430
|
+
throw new Error(`The close wasn't recorded: ${released.error}`);
|
|
29080
29431
|
return 'The focus on it ended all the same, and what the other channels held was delivered.';
|
|
29081
29432
|
},
|
|
29082
29433
|
focusChannelResolver: () => this.tuneOutCoordinator?.focusChannel() ?? null,
|
|
@@ -29248,13 +29599,11 @@ export class AgentFramework {
|
|
|
29248
29599
|
attempt: 0,
|
|
29249
29600
|
willRetry: !!left?.willReconnect,
|
|
29250
29601
|
});
|
|
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}`);
|
|
29602
|
+
// So the server doesn't vanish quietly: nothing connects it again on
|
|
29603
|
+
// its own, so alert at once. One that connected before its setup
|
|
29604
|
+
// threw is still connected, and gets no alert.
|
|
29605
|
+
if (!left?.willReconnect && !left?.isConnected) {
|
|
29606
|
+
this.opsAlert('mcpl-down', config.id, `MCPL server not connected at startup, and this host won't retry it on its own: ${message}`);
|
|
29258
29607
|
}
|
|
29259
29608
|
}
|
|
29260
29609
|
}
|
|
@@ -31068,9 +31417,10 @@ export class AgentFramework {
|
|
|
31068
31417
|
}
|
|
31069
31418
|
/**
|
|
31070
31419
|
* Restart an MCPL server at runtime: disconnect, then reconnect with the
|
|
31071
|
-
* same (or an updated) config. This
|
|
31072
|
-
*
|
|
31073
|
-
*
|
|
31420
|
+
* same (or an updated) config. This respawns a stdio child at once,
|
|
31421
|
+
* whatever its state; `reconnect: true` spawns a fresh child only after
|
|
31422
|
+
* the connection fails or its child exits. Throws if the server has no
|
|
31423
|
+
* stored config and none is given.
|
|
31074
31424
|
* What its connect would refuse is refused before the running server is
|
|
31075
31425
|
* torn down. Until the connect, a connect or a restart started meanwhile
|
|
31076
31426
|
* is refused the id and the tool prefix the restart moves the server to,
|
|
@@ -31470,9 +31820,9 @@ export class AgentFramework {
|
|
|
31470
31820
|
// request turned away at stop gets, so it can send it again to
|
|
31471
31821
|
// the host's next run. Optional chaining: prototype-built
|
|
31472
31822
|
// harnesses leave the queue unset.
|
|
31473
|
-
const
|
|
31823
|
+
const answer = this.queue?.isClosed ? hostStoppedWhileHandling() : { code: -32603, message: 'Internal error' };
|
|
31474
31824
|
try {
|
|
31475
|
-
tracked.respondError?.(
|
|
31825
|
+
tracked.respondError?.(answer.code, answer.message);
|
|
31476
31826
|
}
|
|
31477
31827
|
catch (sendError) {
|
|
31478
31828
|
responseError = recorded(describeCause(sendError));
|
|
@@ -31638,6 +31988,7 @@ export class AgentFramework {
|
|
|
31638
31988
|
// Chunks go out on the transport the request came on, as its
|
|
31639
31989
|
// answer does.
|
|
31640
31990
|
...(responder.sendChunk ? { sendChunk: responder.sendChunk } : {}),
|
|
31991
|
+
...(responder.signal ? { signal: responder.signal } : {}),
|
|
31641
31992
|
});
|
|
31642
31993
|
}
|
|
31643
31994
|
});
|
|
@@ -31905,14 +32256,15 @@ export class AgentFramework {
|
|
|
31905
32256
|
// that next one (see refreshMcplTools).
|
|
31906
32257
|
this.noteMcplServerClosed(connection.id);
|
|
31907
32258
|
this.dropMcplToolClasses(connection.id);
|
|
32259
|
+
// The transport's own words only: a WebSocket server's close text
|
|
32260
|
+
// stays off the trace.
|
|
32261
|
+
const reason = info?.reason !== undefined && !info.reasonFromPeer ? boundFailureText(info.reason) : null;
|
|
31908
32262
|
this.emitTrace({
|
|
31909
32263
|
type: 'mcpl:server-closed',
|
|
31910
32264
|
serverId: connection.id,
|
|
31911
32265
|
code: code ?? null,
|
|
31912
32266
|
signal: signal ?? null,
|
|
31913
|
-
|
|
31914
|
-
// stays off the trace.
|
|
31915
|
-
reason: info?.reason !== undefined && !info.reasonFromPeer ? boundFailureText(info.reason) : null,
|
|
32267
|
+
reason,
|
|
31916
32268
|
willReconnect: connection.willReconnect,
|
|
31917
32269
|
});
|
|
31918
32270
|
this.emitTrace({ type: 'module:removed', moduleName: `mcpl:${connection.id}` });
|
|
@@ -31922,6 +32274,14 @@ export class AgentFramework {
|
|
|
31922
32274
|
// RFC-014: its toolsets go with it; a next connection reads them again.
|
|
31923
32275
|
this.toolsetHub?.get(connection.id)?.detach(connection);
|
|
31924
32276
|
this.handleToolsListChanged(connection.id);
|
|
32277
|
+
// Lost on its own with no reconnect coming: nothing in this host
|
|
32278
|
+
// connects it again on its own. A close the host made itself (stop(),
|
|
32279
|
+
// a disconnect, a restart) is byHost, a loss once stop() has begun is
|
|
32280
|
+
// part of the close the host is making, and a child that ended as
|
|
32281
|
+
// asked was stopped: none of them gets an alert.
|
|
32282
|
+
if (!info?.byHost && !this.stopHasBegun() && !endedAsAsked({ code, signal, pipeFailed: info?.pipeFailed })) {
|
|
32283
|
+
this.opsAlert('mcpl-down', connection.id, `MCPL server closed${closeWords(reason, code, signal)}, and this host won't reconnect it on its own`);
|
|
32284
|
+
}
|
|
31925
32285
|
}
|
|
31926
32286
|
});
|
|
31927
32287
|
}
|
|
@@ -31951,6 +32311,7 @@ export class AgentFramework {
|
|
|
31951
32311
|
type: 'mcpl:server-connect-failed',
|
|
31952
32312
|
serverId: connection.id,
|
|
31953
32313
|
error: recorded(params.error),
|
|
32314
|
+
...(params.stderrTail === undefined ? {} : { stderrTail: recorded(params.stderrTail) }),
|
|
31954
32315
|
attempt: params.attempt,
|
|
31955
32316
|
willRetry: connection.willReconnect,
|
|
31956
32317
|
});
|
|
@@ -31969,7 +32330,7 @@ export class AgentFramework {
|
|
|
31969
32330
|
connection.on('error', (err) => {
|
|
31970
32331
|
this.emitTrace({ type: 'mcpl:server-error', serverId: connection.id, error: recorded(err.message) });
|
|
31971
32332
|
});
|
|
31972
|
-
connection.on('close', () => {
|
|
32333
|
+
connection.on('close', (info) => {
|
|
31973
32334
|
// A closed provider's class hints are stale (RFC-008), as for MCPL,
|
|
31974
32335
|
// and so is a listing already under way.
|
|
31975
32336
|
this.noteMcplServerClosed(connection.id);
|
|
@@ -31977,8 +32338,9 @@ export class AgentFramework {
|
|
|
31977
32338
|
this.emitTrace({
|
|
31978
32339
|
type: 'mcpl:server-closed',
|
|
31979
32340
|
serverId: connection.id,
|
|
31980
|
-
code: null,
|
|
31981
|
-
signal: null,
|
|
32341
|
+
code: info.code ?? null,
|
|
32342
|
+
signal: info.signal ?? null,
|
|
32343
|
+
reason: info.reason,
|
|
31982
32344
|
willReconnect: connection.willReconnect,
|
|
31983
32345
|
});
|
|
31984
32346
|
this.emitTrace({ type: 'module:removed', moduleName: `mcpl:${connection.id}` });
|
|
@@ -31999,6 +32361,13 @@ export class AgentFramework {
|
|
|
31999
32361
|
// Gone for good: its tools leave the agents' list, as a
|
|
32000
32362
|
// disconnect's do.
|
|
32001
32363
|
this.handleToolsListChanged(connection.id);
|
|
32364
|
+
// Lost on its own: nothing in this host connects it again on its
|
|
32365
|
+
// own. A loss once stop() has begun is part of the close the host
|
|
32366
|
+
// is making, and a child that ended as asked was stopped: neither
|
|
32367
|
+
// gets an alert.
|
|
32368
|
+
if (!this.stopHasBegun() && !endedAsAsked(info)) {
|
|
32369
|
+
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`);
|
|
32370
|
+
}
|
|
32002
32371
|
});
|
|
32003
32372
|
}
|
|
32004
32373
|
});
|
|
@@ -33132,7 +33501,7 @@ export class AgentFramework {
|
|
|
33132
33501
|
* (same path as gate.json load), so an invalid rule is surfaced as a tool
|
|
33133
33502
|
* error and nothing is written.
|
|
33134
33503
|
*/
|
|
33135
|
-
dispatchWakeRuleToolCall(agentName, call) {
|
|
33504
|
+
dispatchWakeRuleToolCall(agentName, call, deliver = this.pushToolResult(agentName, call, 'gate')) {
|
|
33136
33505
|
this.emitTrace({ type: 'tool:started', module: 'gate', tool: call.name, callId: call.id, input: call.input });
|
|
33137
33506
|
// What wake_add_rule's input held that does nothing, checkToolInput's notes and
|
|
33138
33507
|
// then the gate's on a rule it saved: a refusal names it too, so the call that
|
|
@@ -33147,13 +33516,22 @@ export class AgentFramework {
|
|
|
33147
33516
|
module: 'gate', tool: call.name, callId: call.id, durationMs: 0,
|
|
33148
33517
|
...(result.isError ? { error: result.error } : {}),
|
|
33149
33518
|
});
|
|
33150
|
-
|
|
33519
|
+
deliver(result);
|
|
33151
33520
|
};
|
|
33152
33521
|
try {
|
|
33153
33522
|
if (call.name === 'wake_remove_rule') {
|
|
33154
33523
|
const input = (call.input ?? {});
|
|
33155
33524
|
if (typeof input.name !== 'string' || !input.name) {
|
|
33156
|
-
|
|
33525
|
+
const edit = this.eventGate.ruleNamesForChange();
|
|
33526
|
+
const names = 'names' in edit ? edit.names.map((name) => quotedValue(name)) : [];
|
|
33527
|
+
finish({
|
|
33528
|
+
success: false,
|
|
33529
|
+
error: `wake_remove_rule: name is ${shownValue(input.name)}, where the name of a rule is needed, so nothing was removed. `
|
|
33530
|
+
+ ('refused' in edit
|
|
33531
|
+
? edit.refused
|
|
33532
|
+
: (names.length > 0 ? `The rules a change edits are ${names.join(', ')}.` : 'There is no rule to remove.') + edit.note),
|
|
33533
|
+
isError: true,
|
|
33534
|
+
});
|
|
33157
33535
|
return;
|
|
33158
33536
|
}
|
|
33159
33537
|
const removed = this.removeGatePolicy(input.name);
|
|
@@ -33183,9 +33561,8 @@ export class AgentFramework {
|
|
|
33183
33561
|
if (behaviorSources.length === 0) {
|
|
33184
33562
|
finish({
|
|
33185
33563
|
success: false,
|
|
33186
|
-
error:
|
|
33187
|
-
? '
|
|
33188
|
-
: 'wake_add_rule: specify exactly one behavior — `behavior` ("always"/"defer"/"skip"), `debounceMs`, `rateLimit`, or `passiveSample`.',
|
|
33564
|
+
error: 'wake_add_rule got no behavior, so nothing was saved. Give one of behavior ("always", "defer" or "skip"), '
|
|
33565
|
+
+ (this.judgeWired ? 'debounceMs, rateLimit, passiveSample or judge.' : 'debounceMs, rateLimit or passiveSample.'),
|
|
33189
33566
|
isError: true,
|
|
33190
33567
|
});
|
|
33191
33568
|
return;
|
|
@@ -33193,7 +33570,7 @@ export class AgentFramework {
|
|
|
33193
33570
|
if (behaviorSources.length > 1) {
|
|
33194
33571
|
finish({
|
|
33195
33572
|
success: false,
|
|
33196
|
-
error: `wake_add_rule
|
|
33573
|
+
error: `wake_add_rule got more than one behavior (${behaviorSources.join(', ')}), so nothing was saved. Give only one of them.`,
|
|
33197
33574
|
isError: true,
|
|
33198
33575
|
});
|
|
33199
33576
|
return;
|
|
@@ -33234,7 +33611,7 @@ export class AgentFramework {
|
|
|
33234
33611
|
if (placementSources.length > 1) {
|
|
33235
33612
|
finish({
|
|
33236
33613
|
success: false,
|
|
33237
|
-
error: `wake_add_rule
|
|
33614
|
+
error: `wake_add_rule got more than one placement (${placementSources.join(', ')}), so nothing was saved. Give only one of them.`,
|
|
33238
33615
|
isError: true,
|
|
33239
33616
|
});
|
|
33240
33617
|
return;
|
|
@@ -33254,10 +33631,18 @@ export class AgentFramework {
|
|
|
33254
33631
|
for (const field of ['insertBefore', 'insertAfter']) {
|
|
33255
33632
|
const anchor = input[field];
|
|
33256
33633
|
if (anchor !== undefined && (typeof anchor !== 'string' || anchor.length === 0)) {
|
|
33634
|
+
const edit = this.eventGate.ruleNamesForChange();
|
|
33635
|
+
const names = 'names' in edit ? edit.names.filter((name) => name !== input.name).map((name) => quotedValue(name)) : [];
|
|
33257
33636
|
finish({
|
|
33258
33637
|
success: false,
|
|
33259
|
-
error: `wake_add_rule: ${field} must be the name of a rule, and it's ${shownValue(anchor)}, so nothing `
|
|
33260
|
-
+
|
|
33638
|
+
error: `wake_add_rule: ${field} must be the name of a rule, and it's ${shownValue(anchor)}, so nothing was saved. `
|
|
33639
|
+
+ ('refused' in edit
|
|
33640
|
+
? edit.refused
|
|
33641
|
+
: (names.length > 0
|
|
33642
|
+
? `The other rules a change edits are ${names.join(', ')}. Give ${field} one of them to place the rule beside it, or leave it out`
|
|
33643
|
+
: `There is no other rule to place it beside, so leave ${field} out`)
|
|
33644
|
+
+ ' to add the rule at the end, or, where a rule already has its name, to replace that rule where it is.'
|
|
33645
|
+
+ edit.note),
|
|
33261
33646
|
isError: true,
|
|
33262
33647
|
});
|
|
33263
33648
|
return;
|
|
@@ -33285,9 +33670,12 @@ export class AgentFramework {
|
|
|
33285
33670
|
// (af#105 — a prepended catch-all sampler silently disabled the
|
|
33286
33671
|
// addressed-message wakes for 12h before anyone could see why).
|
|
33287
33672
|
const before = new Map(this.eventGate.probeTable().map((r) => [r.probe, r]));
|
|
33288
|
-
const
|
|
33673
|
+
const fieldNames = Object.fromEntries(Object.entries(AgentFramework.WAKE_ADD_RULE_FIELDS)
|
|
33674
|
+
.filter(([field]) => !(field.startsWith('behavior.') && typeof input.behavior === 'object')));
|
|
33675
|
+
const policy = this.eventGate.addPolicy(rawPolicy, {
|
|
33289
33676
|
...(position ? { position } : {}),
|
|
33290
33677
|
onIgnoredFields: (notes) => { ignored.push(...notes); },
|
|
33678
|
+
fieldNames,
|
|
33291
33679
|
});
|
|
33292
33680
|
const after = this.eventGate.probeTable();
|
|
33293
33681
|
const probes = after.map((r) => {
|
|
@@ -33589,22 +33977,20 @@ export class AgentFramework {
|
|
|
33589
33977
|
});
|
|
33590
33978
|
return decision;
|
|
33591
33979
|
}
|
|
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
33980
|
commitDeliveries() {
|
|
33981
|
+
let durable = true;
|
|
33598
33982
|
if (this.deferredMessages.length > 0) {
|
|
33599
33983
|
this.deferredWritesPersisted = true;
|
|
33600
|
-
this.persistDeferredWrites();
|
|
33984
|
+
durable = this.persistDeferredWrites();
|
|
33601
33985
|
}
|
|
33602
33986
|
try {
|
|
33603
33987
|
this.store.sync();
|
|
33604
33988
|
}
|
|
33605
33989
|
catch (err) {
|
|
33606
33990
|
console.error('[tune-out] could not sync the store after a release:', err);
|
|
33991
|
+
return false;
|
|
33607
33992
|
}
|
|
33993
|
+
return durable;
|
|
33608
33994
|
}
|
|
33609
33995
|
/** The history-reading tool the primary resident actually has, if any (#232 gap 11). */
|
|
33610
33996
|
residentHistoryTool() {
|
|
@@ -33614,7 +34000,7 @@ export class AgentFramework {
|
|
|
33614
34000
|
const names = this.getAllTools().map((t) => t.name).filter((n) => primary.canUseTool(n));
|
|
33615
34001
|
return names.find((n) => /(^|--)(fetch_history|channel_read|read_history|history)$/.test(n)) ?? null;
|
|
33616
34002
|
}
|
|
33617
|
-
dispatchSleepToolCall(agentName, call) {
|
|
34003
|
+
dispatchSleepToolCall(agentName, call, deliver = this.pushToolResult(agentName, call, 'gate')) {
|
|
33618
34004
|
// The gate's sleep quiets the whole agent, every conversation of it, so a
|
|
33619
34005
|
// conversation fork or an ephemeral run neither sets one nor ends the
|
|
33620
34006
|
// agent's own. Refused before the tool runs, as a fork's channel_open is.
|
|
@@ -33633,7 +34019,7 @@ export class AgentFramework {
|
|
|
33633
34019
|
? `Nothing was set: a sleep quiets the whole agent, not one ${one}.${comeBack}`
|
|
33634
34020
|
: `Nothing was changed: a sleep is the agent's, so this ${one} can't end one.`;
|
|
33635
34021
|
this.emitTrace({ type: 'tool:failed', module: 'gate', tool: call.name, callId: call.id, error: `${agentName}: ${error}` });
|
|
33636
|
-
|
|
34022
|
+
deliver(this.refusedResult(agentName, call.id, error));
|
|
33637
34023
|
return;
|
|
33638
34024
|
}
|
|
33639
34025
|
this.emitTrace({ type: 'tool:started', module: 'gate', tool: call.name, callId: call.id, input: call.input });
|
|
@@ -33641,7 +34027,7 @@ export class AgentFramework {
|
|
|
33641
34027
|
const input = (call.input ?? {});
|
|
33642
34028
|
const finish = (result) => {
|
|
33643
34029
|
this.emitTrace({ type: 'tool:completed', module: 'gate', tool: call.name, callId: call.id, durationMs: 0 });
|
|
33644
|
-
|
|
34030
|
+
deliver(result);
|
|
33645
34031
|
};
|
|
33646
34032
|
if (call.name === 'wake') {
|
|
33647
34033
|
const was = gate.clearSleep();
|
|
@@ -33762,31 +34148,36 @@ export class AgentFramework {
|
|
|
33762
34148
|
*/
|
|
33763
34149
|
/** RFC-005 on-demand dereference: [ref id] → workspace file, via the
|
|
33764
34150
|
* origin-bound, ceiling-bounded, digest-verified host fetcher. */
|
|
33765
|
-
dispatchFetchReferenceToolCall(agentName, call) {
|
|
34151
|
+
dispatchFetchReferenceToolCall(agentName, call, deliver = this.pushToolResult(agentName, call, 'workspace')) {
|
|
33766
34152
|
this.emitTrace({ type: 'tool:started', module: 'workspace', tool: call.name, callId: call.id, input: call.input });
|
|
33767
34153
|
const startTime = Date.now();
|
|
33768
34154
|
void (async () => {
|
|
33769
|
-
const args = (call.input ?? {});
|
|
33770
|
-
const refId = String(args.ref_id ?? '');
|
|
33771
|
-
const record = referenceRegistry.get(refId);
|
|
33772
34155
|
let result;
|
|
33773
|
-
|
|
33774
|
-
|
|
33775
|
-
|
|
33776
|
-
|
|
33777
|
-
|
|
34156
|
+
try {
|
|
34157
|
+
const args = (call.input ?? {});
|
|
34158
|
+
const refId = String(args.ref_id ?? '');
|
|
34159
|
+
const record = referenceRegistry.get(refId);
|
|
34160
|
+
if (!record) {
|
|
34161
|
+
// Vector 20: a stale or unknown id is a defined miss, never a
|
|
34162
|
+
// different record.
|
|
34163
|
+
result = { success: false, isError: true,
|
|
34164
|
+
error: `unknown reference: ${refId || '(missing ref_id)'} — it may have been evicted; ids are never reused` };
|
|
34165
|
+
}
|
|
34166
|
+
else {
|
|
34167
|
+
const maxBytes = typeof args.max_bytes === 'number'
|
|
34168
|
+
? Math.min(args.max_bytes, DEFAULT_FETCH_MAX_BYTES) : undefined;
|
|
34169
|
+
const outcome = await this.getReferenceFetcher().fetch(record, { maxBytes });
|
|
34170
|
+
result = outcome.ok
|
|
34171
|
+
? { success: true, data: JSON.stringify({ path: outcome.path, bytes: outcome.bytes,
|
|
34172
|
+
mime_type: outcome.mimeType, digest_verified: outcome.digestVerified ?? 'no digest claimed' }) }
|
|
34173
|
+
// The fetcher says what happened as a clause about the reference,
|
|
34174
|
+
// and what calling again can do as a sentence of its own.
|
|
34175
|
+
: { success: false, isError: true,
|
|
34176
|
+
error: `fetch_reference couldn't fetch "${refId}": ${outcome.error}. ${outcome.remedy}` };
|
|
34177
|
+
}
|
|
33778
34178
|
}
|
|
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}` };
|
|
34179
|
+
catch (error) {
|
|
34180
|
+
result = { success: false, isError: true, error: `fetch_reference couldn't answer: ${thrownText(error)}` };
|
|
33790
34181
|
}
|
|
33791
34182
|
this.emitTrace({
|
|
33792
34183
|
type: result.isError ? 'tool:failed' : 'tool:completed',
|
|
@@ -33794,13 +34185,10 @@ export class AgentFramework {
|
|
|
33794
34185
|
durationMs: Date.now() - startTime,
|
|
33795
34186
|
...(result.isError ? { error: result.error } : {}),
|
|
33796
34187
|
});
|
|
33797
|
-
|
|
33798
|
-
type: 'tool-result', callId: call.id, toolName: call.name, agentName, moduleName: 'workspace',
|
|
33799
|
-
result: { ...result, isError: result.isError ?? false },
|
|
33800
|
-
});
|
|
34188
|
+
deliver({ ...result, isError: result.isError ?? false });
|
|
33801
34189
|
})();
|
|
33802
34190
|
}
|
|
33803
|
-
dispatchReadImageToolCall(agentName, call) {
|
|
34191
|
+
dispatchReadImageToolCall(agentName, call, deliver = this.pushToolResult(agentName, call, 'workspace')) {
|
|
33804
34192
|
this.emitTrace({ type: 'tool:started', module: 'workspace', tool: call.name, callId: call.id, input: call.input });
|
|
33805
34193
|
const finish = (result) => {
|
|
33806
34194
|
this.emitTrace({
|
|
@@ -33811,7 +34199,7 @@ export class AgentFramework {
|
|
|
33811
34199
|
durationMs: 0,
|
|
33812
34200
|
...(result.isError ? { error: result.error } : {}),
|
|
33813
34201
|
});
|
|
33814
|
-
|
|
34202
|
+
deliver(result);
|
|
33815
34203
|
};
|
|
33816
34204
|
void (async () => {
|
|
33817
34205
|
try {
|
|
@@ -33893,14 +34281,11 @@ export class AgentFramework {
|
|
|
33893
34281
|
* every sibling in the round to settle; if one never does, fail closed
|
|
33894
34282
|
* rather than trust an index the batch may still change. Sibling saves
|
|
33895
34283
|
* 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.
|
|
34284
|
+
* otherwise deadlock into a double timeout). Only the round's own calls
|
|
34285
|
+
* wait: a call that isn't among its pending calls, a script's (`pytc-…`)
|
|
34286
|
+
* or an awaited call's, counts the images the agent has now.
|
|
33900
34287
|
*/
|
|
33901
34288
|
async awaitSiblingToolCalls(agent, call) {
|
|
33902
|
-
if (call.id.startsWith('pytc-'))
|
|
33903
|
-
return;
|
|
33904
34289
|
const BARRIER_MS = 120_000;
|
|
33905
34290
|
const POLL_MS = 20;
|
|
33906
34291
|
const startedAt = Date.now();
|
|
@@ -33909,6 +34294,8 @@ export class AgentFramework {
|
|
|
33909
34294
|
// Idle (puppet path) or the round was torn down: nothing to wait on.
|
|
33910
34295
|
if (state.status !== 'waiting_for_tools')
|
|
33911
34296
|
return;
|
|
34297
|
+
if (!state.pending.has(call.id))
|
|
34298
|
+
return;
|
|
33912
34299
|
const open = [...state.pending.values()].filter((p) => p.id !== call.id && p.name !== 'save_recent_image');
|
|
33913
34300
|
if (open.length === 0)
|
|
33914
34301
|
return;
|
|
@@ -33935,7 +34322,7 @@ export class AgentFramework {
|
|
|
33935
34322
|
* nothing. It never slides to an older image — a filename must not
|
|
33936
34323
|
* authenticate bytes from another surface.
|
|
33937
34324
|
*/
|
|
33938
|
-
dispatchSaveImageToolCall(agentName, call) {
|
|
34325
|
+
dispatchSaveImageToolCall(agentName, call, deliver = this.pushToolResult(agentName, call, 'workspace')) {
|
|
33939
34326
|
this.emitTrace({ type: 'tool:started', module: 'workspace', tool: call.name, callId: call.id, input: call.input });
|
|
33940
34327
|
const finish = (result) => {
|
|
33941
34328
|
this.emitTrace({
|
|
@@ -33946,7 +34333,7 @@ export class AgentFramework {
|
|
|
33946
34333
|
durationMs: 0,
|
|
33947
34334
|
...(result.isError ? { error: result.error } : {}),
|
|
33948
34335
|
});
|
|
33949
|
-
|
|
34336
|
+
deliver(result);
|
|
33950
34337
|
};
|
|
33951
34338
|
void (async () => {
|
|
33952
34339
|
try {
|
|
@@ -34057,21 +34444,36 @@ export class AgentFramework {
|
|
|
34057
34444
|
let count = 1;
|
|
34058
34445
|
/** Direct-ref mode: the one ref to find in the scannable context. */
|
|
34059
34446
|
let wantedRef = null;
|
|
34060
|
-
|
|
34447
|
+
// A selector is in use only when it carries a value (fieldInUse), so a
|
|
34448
|
+
// caller whose provider presents every property as required can still
|
|
34449
|
+
// choose one, whichever of the two it fills the other selector with.
|
|
34450
|
+
// Any other value is checked, never read as absent: a whitespace or
|
|
34451
|
+
// malformed ref, or an index that isn't an integer, is refused rather
|
|
34452
|
+
// than turned into a different image than the one the caller named.
|
|
34453
|
+
// A malformed ref beside an index or count in use says how to leave a
|
|
34454
|
+
// selector unused, since the caller may have meant the positional one, as
|
|
34455
|
+
// the conflict refusal below does for a well-formed ref. No refusal of one
|
|
34456
|
+
// selector on its own says it: a ref alone is the caller's one selector,
|
|
34457
|
+
// and dropping it would save the newest image; an index and a count are
|
|
34458
|
+
// one selector, and dropping either changes which images are saved.
|
|
34459
|
+
if (fieldInUse(input.ref)) {
|
|
34061
34460
|
if (typeof input.ref !== 'string' || !TOOL_IMAGE_REF_RE.test(input.ref)) {
|
|
34062
|
-
|
|
34461
|
+
const positionalInUse = fieldInUse(input.index) || fieldInUse(input.count);
|
|
34462
|
+
throw new Error('save_recent_image: `ref` must look like "img_k7x3q2_7" (copy it from the image placeholder in your context)' +
|
|
34463
|
+
(positionalInUse ? ' (pass null, or leave it out, for a selector you are not using)' : ''));
|
|
34063
34464
|
}
|
|
34064
|
-
if (input.index
|
|
34065
|
-
throw new Error('save_recent_image: `ref` is mutually exclusive with `index`/`count`'
|
|
34465
|
+
if (fieldInUse(input.index) || fieldInUse(input.count)) {
|
|
34466
|
+
throw new Error('save_recent_image: `ref` is mutually exclusive with `index`/`count` — pass null (or omit) ' +
|
|
34467
|
+
'for the selector you are not using');
|
|
34066
34468
|
}
|
|
34067
34469
|
wantedRef = input.ref;
|
|
34068
34470
|
}
|
|
34069
34471
|
else {
|
|
34070
|
-
index = input.index
|
|
34472
|
+
index = fieldInUse(input.index) ? integerField(input.index) : 0;
|
|
34071
34473
|
if (!Number.isInteger(index) || index < 0) {
|
|
34072
34474
|
throw new Error('save_recent_image: `index` must be a non-negative integer');
|
|
34073
34475
|
}
|
|
34074
|
-
count = input.count
|
|
34476
|
+
count = fieldInUse(input.count) ? integerField(input.count) : 1;
|
|
34075
34477
|
const MAX_COUNT = 20;
|
|
34076
34478
|
if (!Number.isInteger(count) || count < 1 || count > MAX_COUNT) {
|
|
34077
34479
|
throw new Error(`save_recent_image: \`count\` must be an integer in 1..${MAX_COUNT}`);
|
|
@@ -34314,7 +34716,7 @@ export class AgentFramework {
|
|
|
34314
34716
|
? 'No setting reads differently after this call.'
|
|
34315
34717
|
: `Changed before the refusal: ${changed.join(', ')}; no other setting reads differently.`;
|
|
34316
34718
|
}
|
|
34317
|
-
dispatchAgentSettingsToolCall(agentName, call) {
|
|
34719
|
+
dispatchAgentSettingsToolCall(agentName, call, deliver = this.pushToolResult(agentName, call, 'agent')) {
|
|
34318
34720
|
this.emitTrace({
|
|
34319
34721
|
type: 'tool:started',
|
|
34320
34722
|
module: 'agent',
|
|
@@ -34502,14 +34904,7 @@ export class AgentFramework {
|
|
|
34502
34904
|
durationMs: 0,
|
|
34503
34905
|
...(result.isError ? { error: result.error } : {}),
|
|
34504
34906
|
});
|
|
34505
|
-
|
|
34506
|
-
type: 'tool-result',
|
|
34507
|
-
callId: call.id,
|
|
34508
|
-
toolName: call.name,
|
|
34509
|
-
agentName,
|
|
34510
|
-
moduleName: 'agent',
|
|
34511
|
-
result,
|
|
34512
|
-
});
|
|
34907
|
+
deliver(result);
|
|
34513
34908
|
}
|
|
34514
34909
|
/** Aggregate the event-tag vocabulary: reserved chat:* core + each connected
|
|
34515
34910
|
* server's declared tag ontology + gate.js status. */
|
|
@@ -34533,7 +34928,7 @@ export class AgentFramework {
|
|
|
34533
34928
|
'in gate.js. Unknown/undeclared tags are tolerated (open ontologies).',
|
|
34534
34929
|
};
|
|
34535
34930
|
}
|
|
34536
|
-
dispatchEventTagsToolCall(agentName, call) {
|
|
34931
|
+
dispatchEventTagsToolCall(agentName, call, deliver = this.pushToolResult(agentName, call, 'gate')) {
|
|
34537
34932
|
this.emitTrace({ type: 'tool:started', module: 'gate', tool: call.name, callId: call.id, input: call.input });
|
|
34538
34933
|
let result;
|
|
34539
34934
|
try {
|
|
@@ -34546,36 +34941,22 @@ export class AgentFramework {
|
|
|
34546
34941
|
this.toolLifecycleEmitter?.markDispatchFailure(agentName, call.id);
|
|
34547
34942
|
result = { success: false, error: err.message, isError: true };
|
|
34548
34943
|
}
|
|
34549
|
-
|
|
34944
|
+
deliver(result);
|
|
34550
34945
|
}
|
|
34551
|
-
dispatchGateToolCall(agentName, call) {
|
|
34946
|
+
dispatchGateToolCall(agentName, call, deliver = this.pushToolResult(agentName, call, 'gate')) {
|
|
34552
34947
|
this.emitTrace({ type: 'tool:started', module: 'gate', tool: call.name, callId: call.id, input: call.input });
|
|
34553
34948
|
const startTime = Date.now();
|
|
34554
34949
|
this.eventGate.handleToolCall()
|
|
34555
34950
|
.then((result) => {
|
|
34556
34951
|
const durationMs = Date.now() - startTime;
|
|
34557
34952
|
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
|
-
});
|
|
34953
|
+
deliver(result);
|
|
34566
34954
|
})
|
|
34567
34955
|
.catch((error) => {
|
|
34568
34956
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
34569
34957
|
this.emitTrace({ type: 'tool:failed', module: 'gate', tool: call.name, callId: call.id, error: err.message });
|
|
34570
34958
|
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
|
-
});
|
|
34959
|
+
deliver({ success: false, error: err.message, isError: true });
|
|
34579
34960
|
});
|
|
34580
34961
|
}
|
|
34581
34962
|
/** The channel context one server is told for an agent's inference
|