@animalabs/agent-framework 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -8
- package/dist/src/agent.d.ts +29 -9
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +82 -17
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/mcp-server-core.d.ts.map +1 -1
- package/dist/src/api/mcp-server-core.js +10 -3
- package/dist/src/api/mcp-server-core.js.map +1 -1
- package/dist/src/code-execution/isolate/render.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/render.js +7 -6
- package/dist/src/code-execution/isolate/render.js.map +1 -1
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +5 -4
- package/dist/src/code-execution/isolate/scripts.js.map +1 -1
- package/dist/src/framework.d.ts +129 -92
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1115 -704
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +24 -17
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +198 -144
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/recipe-reconcile.d.ts.map +1 -1
- package/dist/src/gate/recipe-reconcile.js +6 -5
- package/dist/src/gate/recipe-reconcile.js.map +1 -1
- package/dist/src/host-presentation.d.ts.map +1 -1
- package/dist/src/host-presentation.js +5 -7
- package/dist/src/host-presentation.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/inference-bounds.d.ts +37 -0
- package/dist/src/inference-bounds.d.ts.map +1 -0
- package/dist/src/inference-bounds.js +84 -0
- package/dist/src/inference-bounds.js.map +1 -0
- package/dist/src/line-reader.d.ts +7 -1
- package/dist/src/line-reader.d.ts.map +1 -1
- package/dist/src/line-reader.js +17 -6
- package/dist/src/line-reader.js.map +1 -1
- package/dist/src/mcpl/errors.d.ts +1 -0
- package/dist/src/mcpl/errors.d.ts.map +1 -1
- package/dist/src/mcpl/errors.js +3 -0
- package/dist/src/mcpl/errors.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +2 -2
- package/dist/src/mcpl/inbound-source.js +2 -2
- package/dist/src/mcpl/inference-router.d.ts +11 -1
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.js +65 -9
- package/dist/src/mcpl/inference-router.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts +10 -7
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
- package/dist/src/mcpl/modern-connection.js +85 -74
- package/dist/src/mcpl/modern-connection.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts +1 -1
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +45 -3
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +2 -7
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +1 -2
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +3 -3
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +14 -0
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +26 -22
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts +1 -0
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
- package/dist/src/mcpl/tool-signature.js +16 -11
- package/dist/src/mcpl/tool-signature.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +44 -21
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +283 -64
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +17 -1
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +32 -4
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +189 -25
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
- package/dist/src/modules/discord/discord-js-client.js +24 -16
- package/dist/src/modules/discord/discord-js-client.js.map +1 -1
- package/dist/src/modules/sender-name.d.ts +5 -4
- package/dist/src/modules/sender-name.d.ts.map +1 -1
- package/dist/src/modules/sender-name.js +7 -5
- package/dist/src/modules/sender-name.js.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.d.ts +16 -8
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +33 -12
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +6 -3
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +61 -15
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +9 -1
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +50 -10
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts +11 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +19 -8
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +8 -0
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts +7 -0
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +7 -5
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.js +1 -9
- package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
- package/dist/src/supplied-text.d.ts +2 -0
- package/dist/src/supplied-text.d.ts.map +1 -1
- package/dist/src/supplied-text.js +16 -7
- package/dist/src/supplied-text.js.map +1 -1
- package/dist/src/tool-result-guard.js +1 -1
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +2 -20
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +175 -70
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts +14 -2
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/tune-out/tools.js.map +1 -1
- package/dist/src/types/agent.d.ts +2 -1
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +54 -15
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +42 -3
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +4 -3
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agent.ts +90 -25
- package/src/api/mcp-server-core.ts +10 -3
- package/src/code-execution/isolate/render.ts +7 -6
- package/src/code-execution/isolate/scripts.ts +5 -4
- package/src/framework.ts +1145 -723
- package/src/gate/event-gate.ts +220 -145
- package/src/gate/recipe-reconcile.ts +7 -5
- package/src/host-presentation.ts +4 -7
- package/src/index.ts +1 -0
- package/src/inference-bounds.ts +111 -0
- package/src/line-reader.ts +22 -5
- package/src/mcpl/errors.ts +4 -0
- package/src/mcpl/inbound-source.ts +2 -2
- package/src/mcpl/inference-router.ts +67 -10
- package/src/mcpl/modern-connection.ts +83 -68
- package/src/mcpl/protocol-family.ts +37 -4
- package/src/mcpl/push-coalescer.ts +2 -7
- package/src/mcpl/references.ts +3 -4
- package/src/mcpl/server-connection.ts +30 -21
- package/src/mcpl/tool-signature.ts +18 -11
- package/src/mcpl/transport.ts +261 -60
- package/src/mcpl/types.ts +18 -1
- package/src/module-registry.ts +178 -25
- package/src/modules/discord/discord-js-client.ts +33 -29
- package/src/modules/sender-name.ts +8 -5
- package/src/modules/workspace/disk-agreement.ts +39 -15
- package/src/modules/workspace/index.ts +65 -14
- package/src/modules/workspace/observe.ts +51 -10
- package/src/modules/workspace/reconcile.ts +22 -8
- package/src/modules/workspace/types.ts +5 -0
- package/src/modules/workspace/watcher.ts +8 -6
- package/src/recovery/discord-awareness-outbox.ts +1 -11
- package/src/supplied-text.ts +17 -6
- package/src/tool-result-guard.ts +1 -1
- package/src/tune-out/coordinator.ts +187 -85
- package/src/tune-out/tools.ts +14 -2
- package/src/types/agent.ts +2 -1
- package/src/types/framework.ts +55 -15
- package/src/types/module.ts +42 -3
- package/src/types/trace.ts +4 -3
package/src/framework.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { JsStore } from '@animalabs/chronicle';
|
|
|
12
12
|
import type { Membrane, ContentBlock, NormalizedRequest, NormalizedResponse, YieldingStream, DetailedUsage, ToolParameter, ToolResult as MembraneToolResult, ToolResultContentBlock, StreamEvent } from '@animalabs/membrane';
|
|
13
13
|
import { MembraneError } from '@animalabs/membrane';
|
|
14
14
|
import { classifyFailure, retryDecision, MEMBRANE_RETRY_SETTINGS, type RetrySettings } from '@animalabs/membrane/retry';
|
|
15
|
+
import { deadlineCut, isDeadlineExpiry, resolveInferenceBounds, wholeCallMs, type InferenceBounds } from './inference-bounds.js';
|
|
15
16
|
import { ContextManager, PassthroughStrategy, WindowedPassthroughStrategy, OverBudgetError, UncoveredDropError, stripUnpairedToolBlocks, continuesBody, MalformedContentError, malformedContentPlaces, CompressionWithdrawn } from '@animalabs/context-manager';
|
|
16
17
|
import type { CacheWireReceipt } from './kv-unified-wire.js';
|
|
17
18
|
import { SILENT_HEARTBEAT_SEPARATOR, type RequestOnlyPrompt, type RequestOnlyStamps } from './silent-heartbeat.js';
|
|
@@ -85,9 +86,9 @@ import { asModuleRow, heldHandle, heldWriteId, matchesMessageQuery } from './hel
|
|
|
85
86
|
import { ProcessQueueImpl } from './queue.js';
|
|
86
87
|
import { DEFAULT_SHUTDOWN_TIMEOUT_MS, validateShutdownTimeout, shutdownErrorMessage, waitForShutdown, ShutdownTimeoutError } from './shutdown.js';
|
|
87
88
|
import { REFUSAL_REACTIONS, REFUSAL_REACTION_FALLBACK, REFUSAL_REACTION_BASELINE } from './refusal-reactions.js';
|
|
88
|
-
import { Agent, assertAgentSwitches, dryCompileStatsOf, type RenderStats } from './agent.js';
|
|
89
|
+
import { Agent, assertAgentSwitches, assertThinkingBinding, dryCompileStatsOf, type RenderStats } from './agent.js';
|
|
89
90
|
import { assertBooleanSetting, holdsSettings, kindOf } from './config-checks.js';
|
|
90
|
-
import { ModuleRegistry, checkedToolResult, ensureStateRegistered, storableToolResult, thrownResult } from './module-registry.js';
|
|
91
|
+
import { ModuleRegistry, checkedToolResult, droppedToolResultLine, ensureStateRegistered, retiredPushLine, storableToolResult, thrownResult } from './module-registry.js';
|
|
91
92
|
import { McplServerRegistry } from './mcpl/server-registry.js';
|
|
92
93
|
import { prefixesMeet, prefixesMeetError } from './mcpl/prefix-rule.js';
|
|
93
94
|
import { logDetail, logValue } from './mcpl/log-value.js';
|
|
@@ -815,6 +816,37 @@ export const BUILTIN_TOOLSET_TOOLS: ReadonlySet<string> = new Set([
|
|
|
815
816
|
'workspace--status',
|
|
816
817
|
]);
|
|
817
818
|
|
|
819
|
+
type ToolRoute =
|
|
820
|
+
| { kind: 'presentation' }
|
|
821
|
+
| { kind: 'host-act'; host: HostActTarget }
|
|
822
|
+
| { kind: 'toolset-act' }
|
|
823
|
+
| { kind: 'mcpl'; serverId: string; prefix: string }
|
|
824
|
+
| { kind: 'channel' }
|
|
825
|
+
| { kind: 'agent-settings' }
|
|
826
|
+
| { kind: 'save-image' }
|
|
827
|
+
| { kind: 'read-image' }
|
|
828
|
+
| { kind: 'fetch-reference' }
|
|
829
|
+
| { kind: 'gate-status' }
|
|
830
|
+
| { kind: 'tune-out' }
|
|
831
|
+
| { kind: 'subconscious' }
|
|
832
|
+
| { kind: 'sleep' }
|
|
833
|
+
| { kind: 'heartbeat' }
|
|
834
|
+
| { kind: 'wake-rule' }
|
|
835
|
+
| { kind: 'event-tags' }
|
|
836
|
+
| { kind: 'agent-verb' }
|
|
837
|
+
| { kind: 'code-execution' }
|
|
838
|
+
| { kind: 'drafts' }
|
|
839
|
+
| { kind: 'prose-help' }
|
|
840
|
+
| { kind: 'module' };
|
|
841
|
+
|
|
842
|
+
function agentsOwnVerbRefusal(toolName: string): ToolResult {
|
|
843
|
+
return {
|
|
844
|
+
success: false,
|
|
845
|
+
error: `${toolName} is the agent's own, which a module's call can't make for it, so nothing was done.`,
|
|
846
|
+
isError: true,
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
|
|
818
850
|
type SettingsReading = {
|
|
819
851
|
core: Record<string, string>;
|
|
820
852
|
exts: Map<AgentSettingsExtension, { touched: readonly string[]; fields: Record<string, string> }>;
|
|
@@ -867,7 +899,7 @@ import { HEARTBEAT_CHANGING_TOOLS, Heartbeat, isHeartbeatMcplServer, type Heartb
|
|
|
867
899
|
import type { GateEntryRef, GateEventInfo, WakeLine } from './gate/types.js';
|
|
868
900
|
import { EPHEMERAL_USAGE_NAME, UsageTracker, persistedUsageProblem, type PersistedUsageState } from './usage/usage-tracker.js';
|
|
869
901
|
import type { SessionUsageSnapshot, UsageUpdatedEvent } from './usage/types.js';
|
|
870
|
-
import {
|
|
902
|
+
import { McplRequestError, McplUnreapedLaunchError, stderrTailOf, toolsetDeclarationsOf, withStderr, type McplConnectionCloseInfo, type McplServerConnection } from './mcpl/server-connection.js';
|
|
871
903
|
import { ToolsetHub, TOOLSETS_MODULE, META_TOOL_NAMES, type ToolsetController } from './mcpl/toolset-host.js';
|
|
872
904
|
import { HostPresentation, BUILTIN_TOOLSET, type HostActTarget } from './host-presentation.js';
|
|
873
905
|
import { isEmptyChange, type Presence } from './mcpl/tool-presence.js';
|
|
@@ -875,7 +907,7 @@ import { ToolsetsModule, resolveAct, withLookHint, type TakenChange } from './mc
|
|
|
875
907
|
import { ModernMcpConnection } from './mcpl/modern-connection.js';
|
|
876
908
|
import { MAX_TIMER_MS, WITHHELD_TEXT, asText, credentialRedactor, redactUrl, redactServerText, resolveServerBinding, serverConfigProblems, serverCredentialPatterns, serverTextRedactor, type CredentialPattern } from './mcpl/protocol-family.js';
|
|
877
909
|
import { MALFORMED_CONTENT_NOTICE, normalizeStandardToolResult } from './mcpl/tool-result-normalize.js';
|
|
878
|
-
import { boundedForLog, describeCause } from './mcpl/errors.js';
|
|
910
|
+
import { boundedForLog, describeCause, hostStoppedWhileHandling } from './mcpl/errors.js';
|
|
879
911
|
import { assertServerConfigBooleans, buildWebSocketUrl, isWebSocketTransport } from './mcpl/transport.js';
|
|
880
912
|
import type {
|
|
881
913
|
McplServerConfig,
|
|
@@ -1198,8 +1230,8 @@ function resumesLogicalTurn(reason: string | undefined): boolean {
|
|
|
1198
1230
|
*/
|
|
1199
1231
|
const TURN_RETRY: unique symbol = Symbol('turn-retry');
|
|
1200
1232
|
/**
|
|
1201
|
-
* The batch a start
|
|
1202
|
-
*
|
|
1233
|
+
* The batch a start was merged from, when the scheduler merged more than
|
|
1234
|
+
* one request into it and any of them isn't the turn's: every
|
|
1203
1235
|
* request it took in, as each was. Until the start takes its turn token it
|
|
1204
1236
|
* stands for them all. A scheduler pass that takes it back puts the batch in
|
|
1205
1237
|
* its place, each request with its own identity and flags; a quiesce at the
|
|
@@ -1230,14 +1262,9 @@ function waitAlertsOnItsOwn(wait: ProviderWait): boolean {
|
|
|
1230
1262
|
return wait.until === null || wait.until - wait.setAt > PROVIDER_WAIT_ALERT_MS;
|
|
1231
1263
|
}
|
|
1232
1264
|
|
|
1233
|
-
// membrane 0.8.0 marks a deadline it reports as run out with this registered
|
|
1234
|
-
// symbol (on a sent request, or the Anthropic SDK's timeout, which covers its
|
|
1235
|
-
// connect timeout too), and doesn't export its reader.
|
|
1236
|
-
const MEMBRANE_DEADLINE_EXPIRY: unique symbol = Symbol.for('membrane.deadlineExpiry');
|
|
1237
|
-
|
|
1238
1265
|
function isTransientFailure(error: Error): boolean {
|
|
1239
1266
|
if (!isMembraneError(error) || error.retryable !== true) return false;
|
|
1240
|
-
if ((error
|
|
1267
|
+
if (isDeadlineExpiry(error)) return false;
|
|
1241
1268
|
return error.type === 'server' || error.type === 'network' || error.type === 'timeout' || error.type === 'rate_limit';
|
|
1242
1269
|
}
|
|
1243
1270
|
|
|
@@ -1492,6 +1519,7 @@ interface TurnRouting {
|
|
|
1492
1519
|
*/
|
|
1493
1520
|
addressedElsewhere: Set<string>;
|
|
1494
1521
|
heard?: Map<MessageId, number>;
|
|
1522
|
+
inputsPin?: { agent: Agent; id: string };
|
|
1495
1523
|
}
|
|
1496
1524
|
|
|
1497
1525
|
/** A message that would re-pin its turn's locus (TurnRouting.arrivals). */
|
|
@@ -1522,6 +1550,29 @@ function freshTurnRouting(): TurnRouting {
|
|
|
1522
1550
|
};
|
|
1523
1551
|
}
|
|
1524
1552
|
|
|
1553
|
+
const TURN_INPUTS_SCAN = 64;
|
|
1554
|
+
|
|
1555
|
+
function turnInputs(
|
|
1556
|
+
cm: ContextManager, floor: number, agentName: string, sees: (metadata: unknown) => boolean,
|
|
1557
|
+
): { first: MessageId; last: MessageId } | undefined {
|
|
1558
|
+
let first: MessageId | undefined;
|
|
1559
|
+
let last: MessageId | undefined;
|
|
1560
|
+
scan: for (let end = cm.getMessageCount(); end > 0; end -= TURN_INPUTS_SCAN) {
|
|
1561
|
+
const { messages } = cm.getMessageWindow(Math.max(0, end - TURN_INPUTS_SCAN), Math.min(TURN_INPUTS_SCAN, end), { resolveBlobs: false });
|
|
1562
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1563
|
+
const message = messages[i];
|
|
1564
|
+
if (message.sequence <= floor) break scan;
|
|
1565
|
+
if (!sees(message.metadata)) continue;
|
|
1566
|
+
if (message.participant === agentName) break scan;
|
|
1567
|
+
last ??= message.id;
|
|
1568
|
+
const answers = Array.isArray(message.content) && message.content.length > 0
|
|
1569
|
+
&& message.content.every((block) => block.type === 'tool_result');
|
|
1570
|
+
if (!answers) first = message.id;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
return first !== undefined && last !== undefined ? { first, last } : undefined;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1525
1576
|
/**
|
|
1526
1577
|
* What a prose delivery tells its stream once a publish succeeds: the
|
|
1527
1578
|
* destination, its server and channel, and the text it published. The
|
|
@@ -1727,11 +1778,6 @@ const LIVE_SPEECH_UNRECEIPTED = `Plain speech its rounds routed live was handed
|
|
|
1727
1778
|
* and this says no retry follows it. */
|
|
1728
1779
|
const RETRY_CANCELLED_BY_STOP = 'a retry waiting while the framework was stopping is dropped before it sends anything';
|
|
1729
1780
|
|
|
1730
|
-
/** The reason on the inference:aborted that ends a turn whose start waited to
|
|
1731
|
-
* retry a failed compile across a stop() and a start(): the retry would
|
|
1732
|
-
* revive the earlier lifecycle's turn in the next one. */
|
|
1733
|
-
const RETRY_ACROSS_RESTART = 'a retry waiting while the framework was stopped and started again is dropped before it sends anything';
|
|
1734
|
-
|
|
1735
1781
|
/** The reason on the inference:aborted that ends a turn whose continuation
|
|
1736
1782
|
* was dropped before it started, other than by an abandon or a stop. */
|
|
1737
1783
|
const CONTINUATION_DROPPED = "a turn's continuation was dropped before it started";
|
|
@@ -1822,6 +1868,26 @@ interface DiscordAwarenessDrainPass {
|
|
|
1822
1868
|
drain?: Promise<DiscordAwarenessDrainOutcome>;
|
|
1823
1869
|
}
|
|
1824
1870
|
|
|
1871
|
+
/** Whether a synthesized tool's nullable field is in use: omission, null and
|
|
1872
|
+
* the empty string all mean it isn't, so a caller whose provider presents
|
|
1873
|
+
* every property as required can still leave one unset. Any other value is
|
|
1874
|
+
* in use, and its field checks it. */
|
|
1875
|
+
function fieldInUse(value: unknown): boolean {
|
|
1876
|
+
return value !== undefined && value !== null && value !== '';
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
/** A synthesized tool's integer field: an integer number, or a string of ASCII
|
|
1880
|
+
* digits once trimmed (a model may write an integer as a string), within the
|
|
1881
|
+
* integers a number holds exactly; NaN for anything else. Number() would read
|
|
1882
|
+
* " ", false and [] as 0, and "2.0" or "+2" as 2, and a longer run of digits
|
|
1883
|
+
* as a different, rounded number. */
|
|
1884
|
+
function integerField(value: unknown): number {
|
|
1885
|
+
const n = typeof value === 'number' ? value
|
|
1886
|
+
: typeof value === 'string' && /^[0-9]+$/.test(value.trim()) ? Number(value.trim())
|
|
1887
|
+
: Number.NaN;
|
|
1888
|
+
return Number.isSafeInteger(n) ? n : Number.NaN;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1825
1891
|
function normalizeDiscordAwarenessDeadline(value: number | undefined): number {
|
|
1826
1892
|
if (value === undefined || !Number.isFinite(value) || value <= 0) {
|
|
1827
1893
|
return DEFAULT_DISCORD_AWARENESS_DEADLINE_MS;
|
|
@@ -1835,9 +1901,10 @@ function normalizeDiscordAwarenessDeadline(value: number | undefined): number {
|
|
|
1835
1901
|
/**
|
|
1836
1902
|
* Refuse a config whose `subconscious`, `watchdog` or `processLogging` is
|
|
1837
1903
|
* anything but an object, null or false (holdsSettings), or whose switch in
|
|
1838
|
-
* one holds anything but true or false (assertBooleanSetting)
|
|
1839
|
-
*
|
|
1840
|
-
*
|
|
1904
|
+
* one holds anything but true or false (assertBooleanSetting), or whose
|
|
1905
|
+
* subconscious's `thinkingBinding` assertThinkingBinding refuses, whichever
|
|
1906
|
+
* its reader. A subconscious that isn't on has its `allowChannelSpeech` and
|
|
1907
|
+
* `thinkingBinding` left unchecked, since nothing reads them then.
|
|
1841
1908
|
*/
|
|
1842
1909
|
function assertFrameworkSwitches(config: FrameworkConfig): void {
|
|
1843
1910
|
if (holdsSettings('subconscious', config.subconscious, '{ enabled: true }')) {
|
|
@@ -1853,6 +1920,7 @@ function assertFrameworkSwitches(config: FrameworkConfig): void {
|
|
|
1853
1920
|
config.subconscious.allowChannelSpeech,
|
|
1854
1921
|
'true lets the subconscious speak in channels with speak_in_channel, and false or leaving it out refuses it',
|
|
1855
1922
|
);
|
|
1923
|
+
assertThinkingBinding('subconscious.thinkingBinding', config.subconscious.thinkingBinding);
|
|
1856
1924
|
}
|
|
1857
1925
|
}
|
|
1858
1926
|
if (holdsSettings('watchdog', config.watchdog, '{ enabled: true }')) {
|
|
@@ -2180,15 +2248,16 @@ export interface HostModeStatus {
|
|
|
2180
2248
|
activeTurnAgents: string[];
|
|
2181
2249
|
/** Wakes that own provider admission while waiting for an in-flight
|
|
2182
2250
|
* auxiliary call — no turn token yet, but a turn is one settle away. The
|
|
2183
|
-
* admission continuation rechecks quiesce and
|
|
2184
|
-
* is false until it has. A
|
|
2185
|
-
* and, as a turn alive, in
|
|
2251
|
+
* admission continuation rechecks quiesce and provider holds, and requeues
|
|
2252
|
+
* a wake either keeps from starting; `drained` is false until it has. A
|
|
2253
|
+
* parked continuation of a turn is counted here and, as a turn alive, in
|
|
2254
|
+
* `activeTurns` and `activeTurnAgents`. */
|
|
2186
2255
|
parkedAdmissions: number;
|
|
2187
2256
|
/** Inference requests waiting to start: queued (parked by the quiesce wake
|
|
2188
2257
|
* gate while quiesced, or, while serving, until the scheduler's next pass
|
|
2189
2258
|
* takes them), in a scheduler pass's hands, or held by a provider hold (an
|
|
2190
2259
|
* acceleration or host hold, or a provider's stated wait). There, a start
|
|
2191
|
-
*
|
|
2260
|
+
* merged from several requests counts as those it took in. A start parked
|
|
2192
2261
|
* on provider admission behind an auxiliary call counts once in
|
|
2193
2262
|
* `parkedAdmissions` instead, however many wakes are merged into it.
|
|
2194
2263
|
* Continuations of a turn (budget restart, tool results) and a held turn's
|
|
@@ -2356,9 +2425,15 @@ function hostCommandRequester(serverId: string, params: HostCommandParams): Oper
|
|
|
2356
2425
|
* schedule, jittered, 10 s doubling to 5 min (by default 13 tries, where
|
|
2357
2426
|
* membrane's settings have 7: about 30 min).
|
|
2358
2427
|
* - rate limited (429): the base schedule with at least 5 tries.
|
|
2359
|
-
* -
|
|
2360
|
-
*
|
|
2361
|
-
*
|
|
2428
|
+
* - a deadline expiry, which membrane leaves to its caller: a progress
|
|
2429
|
+
* bound's cut (FrameworkConfig.inferenceBounds), the adapter's own
|
|
2430
|
+
* watchdogs, the SDK's request timeout or a runtime's fetch timeout. The
|
|
2431
|
+
* deadline schedule: by default 2 tries, 1 s apart, so one fresh call. For
|
|
2432
|
+
* one, the framework's `attempt` counts that call's consecutive expiries,
|
|
2433
|
+
* not the turn's other failures, so each call a deadline cuts gets its
|
|
2434
|
+
* fresh call. Settings with no deadline schedule give it their base one.
|
|
2435
|
+
* - any other retryable failure: the base schedule (by default 3 tries, 1 s
|
|
2436
|
+
* then 2 s).
|
|
2362
2437
|
* A provider's stated wait (`retryAfterMs`) is waited when it is longer than
|
|
2363
2438
|
* the backoff, up to the schedule's ceiling, which by default is at least the
|
|
2364
2439
|
* framework's own cap (PROVIDER_ACCELERATION_MAX_COOLDOWN_MS, ten minutes);
|
|
@@ -2380,6 +2455,10 @@ export class DefaultErrorPolicy implements ErrorPolicy {
|
|
|
2380
2455
|
}
|
|
2381
2456
|
|
|
2382
2457
|
onInferenceError(error: Error, _agentName: string, attempt: number): ErrorAction {
|
|
2458
|
+
if (isDeadlineExpiry(error)) {
|
|
2459
|
+
const decision = retryDecision(this.settings.deadline ? 'deadline' : 'retryable', attempt + 1, undefined, this.settings);
|
|
2460
|
+
return decision.retry ? { retry: true, delayMs: decision.delayMs } : { retry: false };
|
|
2461
|
+
}
|
|
2383
2462
|
if (!isMembraneError(error)) {
|
|
2384
2463
|
// Not a provider failure membrane classified: retried as any other
|
|
2385
2464
|
// retryable failure, on the base schedule.
|
|
@@ -2388,9 +2467,7 @@ export class DefaultErrorPolicy implements ErrorPolicy {
|
|
|
2388
2467
|
}
|
|
2389
2468
|
if (error.retryable === false) return { retry: false };
|
|
2390
2469
|
// The facts membrane classifies by, read off the error itself (from either
|
|
2391
|
-
// copy of membrane, as isMembraneError takes one).
|
|
2392
|
-
// stream deadline is a retryable timeout here, as it was before membrane
|
|
2393
|
-
// 0.7, where membrane itself gives it no retry.
|
|
2470
|
+
// copy of membrane, as isMembraneError takes one).
|
|
2394
2471
|
const facts = error as { retryable?: boolean; type?: string; httpStatus?: number; message?: string };
|
|
2395
2472
|
// A failure that doesn't say is retried as an unknown one is; only false refuses (handled above).
|
|
2396
2473
|
const cls = classifyFailure({ retryable: facts.retryable !== false, type: facts.type, httpStatus: facts.httpStatus, message: facts.message });
|
|
@@ -2420,6 +2497,19 @@ const MAINTENANCE_TICKS_PER_PASS = 8;
|
|
|
2420
2497
|
* together, and then on awareness drains still running, such as its
|
|
2421
2498
|
* startup's delivery. */
|
|
2422
2499
|
const FAILED_CREATE_TEARDOWN_TIMEOUT_MS = 30_000;
|
|
2500
|
+
const ABANDON_GRACE_MS = 10_000;
|
|
2501
|
+
|
|
2502
|
+
const testAbandonGraces = new WeakMap<AgentFramework, number>();
|
|
2503
|
+
|
|
2504
|
+
/**
|
|
2505
|
+
* For tests only: `framework`'s abandon takes `ms` as its grace in place of
|
|
2506
|
+
* {@link ABANDON_GRACE_MS}. Set it only where the test holds every turn the
|
|
2507
|
+
* abandon reaches past the grace, so no turn can end within it and a short
|
|
2508
|
+
* grace only brings the abandon's report sooner.
|
|
2509
|
+
*/
|
|
2510
|
+
export function setAbandonGraceForTests(framework: AgentFramework, ms: number): void {
|
|
2511
|
+
testAbandonGraces.set(framework, ms);
|
|
2512
|
+
}
|
|
2423
2513
|
|
|
2424
2514
|
/** Local per-residence provider admission (AF #114 bounded first slice). */
|
|
2425
2515
|
interface LocalProviderGate {
|
|
@@ -2443,9 +2533,10 @@ interface ProviderAccelerationCooldown {
|
|
|
2443
2533
|
/** Armed by an auxiliary (compression) failure with no primary involved:
|
|
2444
2534
|
* release reopens auxiliary admission without synthesising an inference. */
|
|
2445
2535
|
auxiliaryOrigin?: boolean;
|
|
2446
|
-
/** Its held requests were taken out by dropContinuations
|
|
2447
|
-
*
|
|
2448
|
-
|
|
2536
|
+
/** Its held requests were all taken out, by dropContinuations ('drop') or
|
|
2537
|
+
* by a withdrawal of their coalescing subject ('withdrawal'): release
|
|
2538
|
+
* reopens admission without synthesising an inference in their place. */
|
|
2539
|
+
emptiedBy?: 'drop' | 'withdrawal';
|
|
2449
2540
|
rearm?: { number: number; of: number };
|
|
2450
2541
|
/** Set when this hold is a provider wait (a stated retry-after) for the
|
|
2451
2542
|
* agent's primary model: it holds primary turns only. Auxiliary calls are
|
|
@@ -2484,13 +2575,21 @@ const PROVIDER_ACCELERATION_DEFAULT_COOLDOWN_MS = 65_000;
|
|
|
2484
2575
|
* longest stated wait an ephemeral run sleeps in its turn: a longer one ends
|
|
2485
2576
|
* the run instead (nonOwningProviderWait). */
|
|
2486
2577
|
const PROVIDER_ACCELERATION_MAX_COOLDOWN_MS = 10 * 60_000;
|
|
2578
|
+
const FAILURE_ADVICE = 'If this recurs with the same cause, change approach rather than retrying identically ' +
|
|
2579
|
+
'(e.g. drop an oversized attachment or an unsupported setting).';
|
|
2580
|
+
const TIMEOUT_ADVICE = 'If a reply is still owed from that turn, you can give it now.';
|
|
2581
|
+
const MADE_AGAIN = 'The call was made again, and it timed out again.';
|
|
2487
2582
|
const OVERLOADED_TURN_TRIES = 13;
|
|
2488
|
-
const
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2583
|
+
const DEADLINE_TURN_TRIES = 2;
|
|
2584
|
+
const TURN_RETRY_SETTINGS: RetrySettings = {
|
|
2585
|
+
...Object.fromEntries(
|
|
2586
|
+
Object.entries(MEMBRANE_RETRY_SETTINGS as RetrySettings).map(([cls, s]) => [cls, s && {
|
|
2587
|
+
...s,
|
|
2588
|
+
...(cls === 'overloaded' ? { maxAttempts: OVERLOADED_TURN_TRIES } : {}),
|
|
2589
|
+
statedWaitCeilingMs: s.statedWaitCeilingMs === null ? null : Math.max(s.statedWaitCeilingMs, PROVIDER_ACCELERATION_MAX_COOLDOWN_MS),
|
|
2590
|
+
}])),
|
|
2591
|
+
deadline: { maxAttempts: DEADLINE_TURN_TRIES, firstDelayMs: 1_000, multiplier: 1, maxDelayMs: 1_000, statedWaitCeilingMs: null },
|
|
2592
|
+
} as RetrySettings;
|
|
2494
2593
|
const PROVIDER_ACCELERATION_JITTER_MS = 5_000;
|
|
2495
2594
|
/** Longest delay one setTimeout can represent (2^31-1 ms, about 24.8 days);
|
|
2496
2595
|
* a longer timer fires almost at once, so longer waits re-arm in steps. */
|
|
@@ -2540,6 +2639,11 @@ async function* turnStreamEvents(stream: YieldingStream, agentName: string): Asy
|
|
|
2540
2639
|
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`);
|
|
2541
2640
|
return;
|
|
2542
2641
|
}
|
|
2642
|
+
if (event.type === 'aborted' && event.reason === 'timeout') {
|
|
2643
|
+
decidedBy = 'error';
|
|
2644
|
+
yield { type: 'error', error: deadlineCut(event.message) } as StreamEvent;
|
|
2645
|
+
continue;
|
|
2646
|
+
}
|
|
2543
2647
|
if (event.type === 'complete' || event.type === 'error' || event.type === 'aborted') decidedBy = event.type;
|
|
2544
2648
|
yield event;
|
|
2545
2649
|
}
|
|
@@ -2684,6 +2788,7 @@ interface EphemeralRun {
|
|
|
2684
2788
|
endReason?: AgentEndReason;
|
|
2685
2789
|
/** Who ended it, when it was stopped from outside. */
|
|
2686
2790
|
endedBy?: string;
|
|
2791
|
+
endedFromOutside?: true;
|
|
2687
2792
|
}
|
|
2688
2793
|
|
|
2689
2794
|
/**
|
|
@@ -3026,6 +3131,31 @@ const isLowSurrogate = (text: string, index: number): boolean => {
|
|
|
3026
3131
|
const splitsPair = (text: string, index: number): boolean =>
|
|
3027
3132
|
index > 0 && index < text.length && isHighSurrogate(text, index - 1) && isLowSurrogate(text, index);
|
|
3028
3133
|
|
|
3134
|
+
/**
|
|
3135
|
+
* Whether an MCPL server's child ended the way a server ends when it's asked
|
|
3136
|
+
* to: exit 0, death by a signal that asks a process to end (SIGTERM, SIGINT,
|
|
3137
|
+
* SIGHUP), or a shell's report of such a death by its convention, 128 plus the
|
|
3138
|
+
* signal's number (143, 130, 129). A service manager's stop signals the host
|
|
3139
|
+
* and its children together, so a child can go before the host's stop()
|
|
3140
|
+
* closes it: its loss is that stop, not a failure, and raises no mcpl-down.
|
|
3141
|
+
* A child the stdio transport ended itself, after a pipe error or a line on
|
|
3142
|
+
* its stdout longer than the bound, was ended by that, whatever its code or
|
|
3143
|
+
* signal. A WebSocket's close codes are 1000 and up, so they never match.
|
|
3144
|
+
*/
|
|
3145
|
+
function endedAsAsked(ended: { code?: number | null; signal?: string | null; pipeFailed?: boolean }): boolean {
|
|
3146
|
+
if (ended.pipeFailed) return false;
|
|
3147
|
+
const { code, signal } = ended;
|
|
3148
|
+
return code === 0 || code === 143 || code === 130 || code === 129 ||
|
|
3149
|
+
signal === 'SIGTERM' || signal === 'SIGINT' || signal === 'SIGHUP';
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
/** A lost connection's words for an alert: the transport's reason, then its
|
|
3153
|
+
* exit or close code and signal, each when there is one. */
|
|
3154
|
+
function closeWords(reason: string | null | undefined, code: number | null | undefined, signal: string | null | undefined): string {
|
|
3155
|
+
const how = [reason ?? null, code != null ? `code ${code}` : null, signal ? `signal ${signal}` : null].filter((part) => part !== null);
|
|
3156
|
+
return how.length > 0 ? ` (${how.join(', ')})` : '';
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3029
3159
|
/**
|
|
3030
3160
|
* `text` when it fits within `max` characters; otherwise its head and tail
|
|
3031
3161
|
* around a marker stating how many of how many characters were omitted, the
|
|
@@ -3261,6 +3391,17 @@ export function shedMayHelp(errorType: string | undefined, httpStatus: number |
|
|
|
3261
3391
|
return errorType === 'invalid_request' && (httpStatus === undefined || httpStatus === 400 || httpStatus === 422);
|
|
3262
3392
|
}
|
|
3263
3393
|
|
|
3394
|
+
function responseParts(response: EventResponse | undefined): string {
|
|
3395
|
+
return [
|
|
3396
|
+
response?.addMessages?.length ? `${response.addMessages.length} message(s) to add` : '',
|
|
3397
|
+
response?.editMessages?.length ? `${response.editMessages.length} edit(s)` : '',
|
|
3398
|
+
response?.removeMessages?.length ? `${response.removeMessages.length} removal(s)` : '',
|
|
3399
|
+
response?.stateUpdate !== undefined ? 'a state update' : '',
|
|
3400
|
+
response?.requestInference ? 'an inference request' : '',
|
|
3401
|
+
response?.toolsChanged ? 'a tools change' : '',
|
|
3402
|
+
].filter(Boolean).join(', ');
|
|
3403
|
+
}
|
|
3404
|
+
|
|
3264
3405
|
/**
|
|
3265
3406
|
* What a thrown value says, safely and briefly, as boundedErrorMessage gives
|
|
3266
3407
|
* it, with `recorded` applied before the cut, so no part of a credential
|
|
@@ -3423,6 +3564,13 @@ interface StreamRequestNotes {
|
|
|
3423
3564
|
|
|
3424
3565
|
const SUBCONSCIOUS_MODEL_FIELDS = ['physicalWindowTokens', 'thinkingBinding'] as const satisfies ReadonlyArray<keyof AgentConfig>;
|
|
3425
3566
|
|
|
3567
|
+
function readerModelFields(cfg: SubconsciousConfig): Pick<AgentConfig, (typeof SUBCONSCIOUS_MODEL_FIELDS)[number]> {
|
|
3568
|
+
return {
|
|
3569
|
+
...(cfg.physicalWindowTokens != null ? { physicalWindowTokens: cfg.physicalWindowTokens } : {}),
|
|
3570
|
+
...(holdsSettings('subconscious.thinkingBinding', cfg.thinkingBinding, '{ binds: true, dropBlock: true }') ? { thinkingBinding: cfg.thinkingBinding } : {}),
|
|
3571
|
+
};
|
|
3572
|
+
}
|
|
3573
|
+
|
|
3426
3574
|
const JUDGE_PARAMETER: ToolParameter = {
|
|
3427
3575
|
type: 'object',
|
|
3428
3576
|
description:
|
|
@@ -3491,9 +3639,10 @@ export class AgentFramework {
|
|
|
3491
3639
|
private store: JsStore;
|
|
3492
3640
|
private ownsStore: boolean;
|
|
3493
3641
|
private stopPromise: Promise<void> | null = null;
|
|
3494
|
-
|
|
3642
|
+
// Nothing sets it back to 'none': a stop ends its instance (#315).
|
|
3643
|
+
private stopState: 'none' | 'stopping' | 'completed' | 'failed' = 'none';
|
|
3644
|
+
private closedStopFailure: { error: unknown } | undefined;
|
|
3495
3645
|
private ownedStoreClosed = false;
|
|
3496
|
-
private ownedStoreManagersClosed = false;
|
|
3497
3646
|
/** Whether the latest stop() got through its teardown with nothing failed:
|
|
3498
3647
|
* every step and every wait up to and including the one on awareness
|
|
3499
3648
|
* drains, whatever failed after that, in the storage work that finishes
|
|
@@ -3509,8 +3658,8 @@ export class AgentFramework {
|
|
|
3509
3658
|
* promise: a stream it cancelled or an event loop still in a call; and a
|
|
3510
3659
|
* stream whose cancel threw, which stop() doesn't wait for. A later
|
|
3511
3660
|
* stop() waits for each again, beside what it finds, whatever has happened
|
|
3512
|
-
* since to the entry it was found under (a run's end removes its stream's,
|
|
3513
|
-
*
|
|
3661
|
+
* since to the entry it was found under (a run's end removes its stream's),
|
|
3662
|
+
* so neither a budget nor a cancel that
|
|
3514
3663
|
* threw lets the store close under work still running. Each leaves once it
|
|
3515
3664
|
* settles. */
|
|
3516
3665
|
private readonly leftRunning = new Map<Promise<unknown>, { label: string; kind: 'stream' | 'loop' }>();
|
|
@@ -3576,6 +3725,9 @@ export class AgentFramework {
|
|
|
3576
3725
|
private providerAccelerationLastRecovery: Map<string, ProviderAccelerationReceipt> = new Map();
|
|
3577
3726
|
private providerAccelerationDefaultCooldownMs = PROVIDER_ACCELERATION_DEFAULT_COOLDOWN_MS;
|
|
3578
3727
|
private providerAccelerationJitterMs = PROVIDER_ACCELERATION_JITTER_MS;
|
|
3728
|
+
private inferenceBounds: InferenceBounds | null = resolveInferenceBounds(undefined);
|
|
3729
|
+
private readonly deadlineCuts = new Map<string, number>();
|
|
3730
|
+
private readonly freshCallsCut = new Set<string>();
|
|
3579
3731
|
private providerHoldHook: ProviderHoldHook | undefined;
|
|
3580
3732
|
/** Provider waits (stated retry-after) per (agent, model), kept across restarts. */
|
|
3581
3733
|
private providerWaits: ProviderWaits | undefined;
|
|
@@ -3583,8 +3735,6 @@ export class AgentFramework {
|
|
|
3583
3735
|
private retryWaitWakers = new Set<() => void>();
|
|
3584
3736
|
private waitingTraceEveryMs = WAITING_TRACE_EVERY_MS;
|
|
3585
3737
|
private providerAdmissionClosed = false;
|
|
3586
|
-
/** Invalidates framework turns still assembling when stop begins. */
|
|
3587
|
-
private lifecycleGeneration = 0;
|
|
3588
3738
|
/** Last time we reported stale (busy-requeued) inference requests, per agent. */
|
|
3589
3739
|
private staleWarnAt = new Map<string, number>();
|
|
3590
3740
|
/** Per-agent last inference activity (epoch ms), for /healthz + doctor tooling. */
|
|
@@ -3736,6 +3886,7 @@ export class AgentFramework {
|
|
|
3736
3886
|
private turnRearmMs: readonly number[] = DEFAULT_TURN_REARM_MS;
|
|
3737
3887
|
private turnRearms: Map<string, number> = new Map();
|
|
3738
3888
|
private exhaustingRearms: Set<string> = new Set();
|
|
3889
|
+
private rearmDeadlineCuts: Set<string> = new Set();
|
|
3739
3890
|
private statedWaitRuns: Map<string, StatedWaitRun> = new Map();
|
|
3740
3891
|
/** N consecutive failed inferences ⇒ the agent is treated as hard-down and
|
|
3741
3892
|
* escalated loudly to stderr. */
|
|
@@ -4314,6 +4465,7 @@ export class AgentFramework {
|
|
|
4314
4465
|
getMessage: (id) => this.getMessage(id),
|
|
4315
4466
|
queryMessages: (filter, opts) => this.queryMessages(filter, opts),
|
|
4316
4467
|
pushEvent: (event, moduleName) => this.pushModuleEvent(event, moduleName, true),
|
|
4468
|
+
dropRetiredPush: (event, moduleName) => this.dropRetiredModuleEvent(event, moduleName),
|
|
4317
4469
|
onTrace: (listener) => this.onTrace(listener),
|
|
4318
4470
|
// The ONLY entry that confers module origin — a private closure the
|
|
4319
4471
|
// framework hands to its own module registry. The public
|
|
@@ -4337,6 +4489,7 @@ export class AgentFramework {
|
|
|
4337
4489
|
static async create(config: FrameworkConfig): Promise<AgentFramework> {
|
|
4338
4490
|
// Before anything opens or starts: a refused config must leave nothing behind.
|
|
4339
4491
|
if (config.codeExecution != null) validateCodeExecutionConfig(config.codeExecution);
|
|
4492
|
+
resolveInferenceBounds(config.inferenceBounds);
|
|
4340
4493
|
assertFrameworkSwitches(config);
|
|
4341
4494
|
for (const agentConfig of config.agents) assertAgentSwitches(agentConfig);
|
|
4342
4495
|
assertAgentNamesUnique(config.agents);
|
|
@@ -4538,6 +4691,7 @@ export class AgentFramework {
|
|
|
4538
4691
|
);
|
|
4539
4692
|
framework.providerHoldHook = config.providerHold;
|
|
4540
4693
|
framework.turnRearmMs = turnRearmDelays(config.exhaustedTurnRearmMs);
|
|
4694
|
+
framework.inferenceBounds = resolveInferenceBounds(config.inferenceBounds);
|
|
4541
4695
|
|
|
4542
4696
|
// Before any delivery.
|
|
4543
4697
|
await framework.reconcileDiscordAwarenessAtStartup();
|
|
@@ -5062,9 +5216,7 @@ export class AgentFramework {
|
|
|
5062
5216
|
* maintenance, event loop and operator operations, so a dump shows what
|
|
5063
5217
|
* those waits are on, and before its modules and MCPL servers stop, so a
|
|
5064
5218
|
* stop() whose teardown hangs, fails or times out doesn't keep it listed.
|
|
5065
|
-
* With none in the set, one line says no framework is running.
|
|
5066
|
-
* started again after a completed stop() doesn't rejoin; whether that
|
|
5067
|
-
* lifecycle is supported is #315.
|
|
5219
|
+
* With none in the set, one line says no framework is running.
|
|
5068
5220
|
*
|
|
5069
5221
|
* One listener serves the process, installed by the first create() to
|
|
5070
5222
|
* reach the join and kept: once a process's last SIGUSR2 listener is
|
|
@@ -5108,36 +5260,39 @@ export class AgentFramework {
|
|
|
5108
5260
|
|
|
5109
5261
|
/**
|
|
5110
5262
|
* Start the event loop. A start() on a running framework does nothing.
|
|
5111
|
-
*
|
|
5112
|
-
*
|
|
5113
|
-
*
|
|
5114
|
-
*
|
|
5263
|
+
* A stop ends its instance (#315): once a stop() is admitted, start() and
|
|
5264
|
+
* addModule() are refused with an error, for good. While that stop() runs,
|
|
5265
|
+
* the error says the framework is stopping. Once it has settled, the error
|
|
5266
|
+
* says what to do: create a new AgentFramework on the store, and after a
|
|
5267
|
+
* failed stop(), call stop() again first, until one resolves. When a
|
|
5268
|
+
* failed stop() left its owned store closed, so that it can't be tried
|
|
5269
|
+
* again, the error carries that failure as its cause.
|
|
5270
|
+
*
|
|
5271
|
+
* A new instance on the store keeps what the store holds. With the
|
|
5272
|
+
* heartbeat on and the same heartbeat state file (heartbeat.statePath, or
|
|
5273
|
+
* its default), it keeps the heartbeat's schedule as it reads it from that
|
|
5274
|
+
* file: a tick or a reminder that fell due meanwhile fires once, shortly
|
|
5275
|
+
* after it starts. What an instance holds only in memory is lost with it,
|
|
5276
|
+
* as on a process restart: an agent's sleep and its wake, a self-wake, the
|
|
5277
|
+
* inference requests still queued (a budget restart, a heartbeat's wake, a
|
|
5278
|
+
* nudge), a re-armed turn's wait and count, and the redo stack. #627 is
|
|
5279
|
+
* about keeping a sleep and a self-wake across a restart. To pause and
|
|
5280
|
+
* resume one framework in place, use quiesce() and resume().
|
|
5115
5281
|
*/
|
|
5116
5282
|
start(): void {
|
|
5117
5283
|
// Before the running check: stop() claims its attempt before it clears
|
|
5118
5284
|
// running, so a start() made in its first steps, such as from a trace
|
|
5119
5285
|
// listener, is refused too.
|
|
5120
|
-
this.
|
|
5286
|
+
this.refuseOnceStopped('start()');
|
|
5121
5287
|
if (this.running) {
|
|
5122
5288
|
return;
|
|
5123
5289
|
}
|
|
5124
5290
|
|
|
5125
5291
|
this.running = true;
|
|
5126
|
-
this.providerAdmissionClosed = false;
|
|
5127
5292
|
this.earlyTraces = null;
|
|
5128
|
-
// A new lifecycle: a turn compiled before an earlier stop() never drives,
|
|
5129
|
-
// and withheld spills, closed for the generation a stop ended, are open
|
|
5130
|
-
// for this one.
|
|
5131
|
-
this.lifecycleGeneration++;
|
|
5132
|
-
// Its servers' addresses, which an earlier stop() took out of the
|
|
5133
|
-
// reference registry, for its stubs.
|
|
5134
5293
|
for (const config of this.mcplServerConfigs.values()) {
|
|
5135
5294
|
referenceRegistry.setServerAddress(config.id, serverAddressOf(config), this);
|
|
5136
5295
|
}
|
|
5137
|
-
// A sleep kept through a stop() wakes this lifecycle at its end, or now
|
|
5138
|
-
// if it ended while stopped. Before the loop's first pass, so that a wake
|
|
5139
|
-
// owed now joins the requests already pending in one turn.
|
|
5140
|
-
this.eventGate?.resumeSleep();
|
|
5141
5296
|
this.heartbeat?.start();
|
|
5142
5297
|
this.loopPromise = this.runLoop();
|
|
5143
5298
|
|
|
@@ -5164,11 +5319,14 @@ export class AgentFramework {
|
|
|
5164
5319
|
}
|
|
5165
5320
|
|
|
5166
5321
|
/**
|
|
5167
|
-
* Stop the event loop
|
|
5168
|
-
*
|
|
5169
|
-
*
|
|
5170
|
-
*
|
|
5171
|
-
*
|
|
5322
|
+
* Stop the event loop, and with it this framework, for good: once a stop()
|
|
5323
|
+
* is admitted, start() and addModule() are refused, as start() says. A
|
|
5324
|
+
* stop() whose budget is refused, which it rejects with a RangeError, is
|
|
5325
|
+
* not admitted and changes nothing. Each step of the teardown runs
|
|
5326
|
+
* whatever an earlier one did: one that throws, such as a host stream's
|
|
5327
|
+
* cancel(), fails the stop() once the rest has run. When a step or a wait
|
|
5328
|
+
* up to and including the wait on awareness drains fails or times out,
|
|
5329
|
+
* storage stays open and
|
|
5172
5330
|
* stop() can be called again; one that fails after it, in the storage work
|
|
5173
5331
|
* that finishes the stop, fails the stop() without keeping storage open.
|
|
5174
5332
|
* A timed-out wait lets teardown go on while what it waited for may still
|
|
@@ -5176,7 +5334,10 @@ export class AgentFramework {
|
|
|
5176
5334
|
* @param teardownTimeoutMs Optional budget for each of stop()'s waits, in
|
|
5177
5335
|
* turn: on the streams it cancels, on maintenance in flight (a pass or an
|
|
5178
5336
|
* over-budget drain kick), on the event loop, on an operator's operations
|
|
5179
|
-
* already running, on modules and MCPL
|
|
5337
|
+
* already running, on modules and MCPL (its servers and the inference
|
|
5338
|
+
* requests they made) together, where a module whose start() is still
|
|
5339
|
+
* running counts until that start() has settled and its stop() returned,
|
|
5340
|
+
* then on Discord awareness
|
|
5180
5341
|
* drains, which send nothing once stop() has begun, recording the call the
|
|
5181
5342
|
* close ended. With a budget, a stop() can take up to six of them, besides
|
|
5182
5343
|
* the withheld-spill deadline of a guard's write still under way. Omit to
|
|
@@ -5199,9 +5360,7 @@ export class AgentFramework {
|
|
|
5199
5360
|
* dropped, though a write the stream already had under way can still
|
|
5200
5361
|
* land in the run's namespace after them.
|
|
5201
5362
|
* - an event loop still in a call, such as a module's onProcess, finishes
|
|
5202
|
-
* that call and the rest of its iteration, and then exits.
|
|
5203
|
-
* made once that stop() has failed runs its own loop beside that last
|
|
5204
|
-
* iteration.
|
|
5363
|
+
* that call and the rest of its iteration, and then exits.
|
|
5205
5364
|
* - a later stop() waits again, within its own budget, for any stream or
|
|
5206
5365
|
* loop this one left running, so the store never closes under it.
|
|
5207
5366
|
*
|
|
@@ -5224,11 +5383,13 @@ export class AgentFramework {
|
|
|
5224
5383
|
let settle!: { resolve: () => void; reject: (error: unknown) => void };
|
|
5225
5384
|
const attempt = new Promise<void>((resolve, reject) => { settle = { resolve, reject }; });
|
|
5226
5385
|
this.stopPromise = attempt;
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5386
|
+
this.stopState = 'stopping';
|
|
5387
|
+
void attempt.then(() => { this.stopState = 'completed'; }, (error: unknown) => {
|
|
5388
|
+
this.stopState = 'failed';
|
|
5389
|
+
// Failed cleanup can be retried while its original store is open. A
|
|
5390
|
+
// terminal storage failure after successful close must remain a failure.
|
|
5391
|
+
if (this.ownedStoreClosed) this.closedStopFailure = { error };
|
|
5392
|
+
else if (this.stopPromise === attempt) this.stopPromise = null;
|
|
5232
5393
|
});
|
|
5233
5394
|
// The body still starts in this call, so its synchronous steps (closing
|
|
5234
5395
|
// the queue among them) happen before stop() returns.
|
|
@@ -5236,22 +5397,29 @@ export class AgentFramework {
|
|
|
5236
5397
|
return attempt;
|
|
5237
5398
|
}
|
|
5238
5399
|
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5400
|
+
private refuseOnceStopped(verb: 'start()' | 'addModule()'): void {
|
|
5401
|
+
switch (this.stopState) {
|
|
5402
|
+
case 'none':
|
|
5403
|
+
return;
|
|
5404
|
+
case 'stopping':
|
|
5405
|
+
throw new Error(`${verb} refused: the framework is stopping`);
|
|
5406
|
+
case 'completed':
|
|
5407
|
+
throw new Error(`${verb} refused: this framework has stopped, and a stopped framework doesn't start again. ` +
|
|
5408
|
+
'Create a new AgentFramework on its store to go on, with its storePath if it opened the store itself.');
|
|
5409
|
+
case 'failed': {
|
|
5410
|
+
const closed = this.closedStopFailure;
|
|
5411
|
+
if (closed) {
|
|
5412
|
+
throw new Error(`${verb} refused: this framework's stop() failed and its owned store is closed, so stop() can't be tried again: ` +
|
|
5413
|
+
`it rejects with that same failure, this error's cause: ${shutdownErrorMessage(closed.error)}. ` +
|
|
5414
|
+
"A stopped framework doesn't start again, and a new AgentFramework on its store doesn't finish what that stop() left unfinished.",
|
|
5415
|
+
{ cause: closed.error });
|
|
5416
|
+
}
|
|
5417
|
+
throw new Error(`${verb} refused: this framework's stop() failed, and a stopped framework doesn't start again. ` +
|
|
5418
|
+
'A failed stop() may have left its cleanup or final persistence unfinished. ' +
|
|
5419
|
+
"Call stop() again: once a stop() of this one resolves, they're done, and only then should a new " +
|
|
5420
|
+
'AgentFramework use its store, with its storePath if this one opened the store itself, ' +
|
|
5421
|
+
'or another instance use anything this one shares.');
|
|
5422
|
+
}
|
|
5255
5423
|
}
|
|
5256
5424
|
}
|
|
5257
5425
|
|
|
@@ -5373,6 +5541,13 @@ export class AgentFramework {
|
|
|
5373
5541
|
// holding the wait for operator operations below until its handshake's
|
|
5374
5542
|
// deadline.
|
|
5375
5543
|
step(() => this.endConnectsInFlight());
|
|
5544
|
+
// Nor does a server lost before or during the stop launch or dial again
|
|
5545
|
+
// on its own: a retry already scheduled starts nothing, and none is
|
|
5546
|
+
// scheduled after.
|
|
5547
|
+
step(() => {
|
|
5548
|
+
for (const connection of this.mcplServerRegistry?.getAllServers() ?? []) connection.stopReconnecting();
|
|
5549
|
+
for (const connection of this.modernServers().values()) connection.stopReconnecting();
|
|
5550
|
+
});
|
|
5376
5551
|
step(() => this.pushCoalescer?.suspend());
|
|
5377
5552
|
step(() => this.flushCoalescingSnapshot());
|
|
5378
5553
|
this.running = false;
|
|
@@ -5386,11 +5561,10 @@ export class AgentFramework {
|
|
|
5386
5561
|
step(() => this.ackDeferredWrites());
|
|
5387
5562
|
this.providerAdmissionClosed = true;
|
|
5388
5563
|
for (const wake of [...this.retryWaitWakers]) step(wake);
|
|
5389
|
-
this.
|
|
5390
|
-
// The generation this stop ends; withheld spills close for it alone, so a
|
|
5391
|
-
// start() after this stop has settled reopens them for its own.
|
|
5392
|
-
const stoppingGeneration = this.lifecycleGeneration;
|
|
5564
|
+
this.guardSpillsClosed = false;
|
|
5393
5565
|
step(() => this.queue.close());
|
|
5566
|
+
let inferences: Array<{ label: string; promise: Promise<void> }> = [];
|
|
5567
|
+
step(() => { inferences = this.inferenceRouter?.stopAll() ?? []; });
|
|
5394
5568
|
step(() => this.tuneOutCoordinator?.stop());
|
|
5395
5569
|
|
|
5396
5570
|
// Stop running code_execution scripts before cancelling streams: a
|
|
@@ -5405,9 +5579,8 @@ export class AgentFramework {
|
|
|
5405
5579
|
this.toolImageLedgers.clear();
|
|
5406
5580
|
|
|
5407
5581
|
// A turn whose continuation is still on its way ends with the stop, as
|
|
5408
|
-
// a turn's start a stop reaches does: the continuation
|
|
5409
|
-
//
|
|
5410
|
-
// (openLogical, below). Before the holds and the parks go, so the
|
|
5582
|
+
// a turn's start a stop reaches does: the continuation never starts.
|
|
5583
|
+
// Before the holds and the parks go, so the
|
|
5411
5584
|
// continuation leaves them with its turn. Each drop is a step of its
|
|
5412
5585
|
// own, so one that throws leaves the next agent's turn to end all the
|
|
5413
5586
|
// same, as stop()'s other loops do.
|
|
@@ -5418,11 +5591,7 @@ export class AgentFramework {
|
|
|
5418
5591
|
});
|
|
5419
5592
|
// A stopped host must never hang behind its own cooldown.
|
|
5420
5593
|
step(() => this.cancelProviderAdmission());
|
|
5421
|
-
// An open logical turn doesn't survive a stop
|
|
5422
|
-
// on this instance opens a new one rather than continuing the old turn's
|
|
5423
|
-
// routing (openLogical). A resume still continues its turn, with its
|
|
5424
|
-
// routing state (turnRouting), since it carries that turn's own request
|
|
5425
|
-
// on: beginAgentTurn opens the turn again for it.
|
|
5594
|
+
// An open logical turn doesn't survive a stop.
|
|
5426
5595
|
this.openLogical.clear();
|
|
5427
5596
|
|
|
5428
5597
|
// Cancel the stream of every agent whose state holds one: streaming,
|
|
@@ -5541,8 +5710,9 @@ export class AgentFramework {
|
|
|
5541
5710
|
// its idle watchdog. Its end stores its queue, as at any run's end; that
|
|
5542
5711
|
// stream's late events are dropped once the run has left, and a later
|
|
5543
5712
|
// stop() waits for the stream itself (leftRunning). (A start() is refused
|
|
5544
|
-
//
|
|
5713
|
+
// once a stop() is admitted, so no new lifecycle begins beside it.)
|
|
5545
5714
|
for (const [name, run] of this.ephemeralRuns) run.settle.reject(ephemeralRunStopped(name));
|
|
5715
|
+
for (const agent of this.agents.values()) step(() => { agent.unpinTurnInputs(); });
|
|
5546
5716
|
|
|
5547
5717
|
// Admission closed when stop() began. What admitted operations start now,
|
|
5548
5718
|
// such as a surgery's config materialization, joins the set, so wait until
|
|
@@ -5572,11 +5742,9 @@ export class AgentFramework {
|
|
|
5572
5742
|
// generation and nothing reads the old fork's window again. So each live
|
|
5573
5743
|
// fork ends here as a closing fork's end disposes it, and its release
|
|
5574
5744
|
// passes on what it left unread, as any release does. Its channel unbinds
|
|
5575
|
-
// first, as the idle sweep's does
|
|
5576
|
-
//
|
|
5577
|
-
//
|
|
5578
|
-
// binding yet, and its spawn binds nothing once it has ended
|
|
5579
|
-
// (spawnConversationAgent).
|
|
5745
|
+
// first, as the idle sweep's does. A fork whose spawn is still waiting
|
|
5746
|
+
// for its creation to return has no binding yet, and its spawn binds
|
|
5747
|
+
// nothing once it has ended (spawnConversationAgent).
|
|
5580
5748
|
// Each is a step of its own, and each fork ends whatever its stream or
|
|
5581
5749
|
// turn is doing, as each run does above: its end settles a tool-result
|
|
5582
5750
|
// batch its stream still held and removes the stream's entries
|
|
@@ -5664,17 +5832,17 @@ export class AgentFramework {
|
|
|
5664
5832
|
// workspace: its stub names the audit record. A write that was past its
|
|
5665
5833
|
// deadline isn't waited for here, but the workspace's own stop waits for
|
|
5666
5834
|
// the mount turn it holds, bounded only by teardownTimeoutMs.
|
|
5667
|
-
this.
|
|
5835
|
+
this.guardSpillsClosed = true;
|
|
5668
5836
|
|
|
5669
5837
|
// Stop modules and both engines' MCP servers in parallel
|
|
5670
|
-
const shutdownPromises: Promise<void>[] = [this.moduleRegistry.stopAll(teardownTimeoutMs)];
|
|
5838
|
+
const shutdownPromises: Promise<void>[] = [this.moduleRegistry.stopAll(teardownTimeoutMs), waitFor(inferences)];
|
|
5671
5839
|
if (this.mcplServerRegistry) {
|
|
5672
5840
|
shutdownPromises.push(this.mcplServerRegistry.closeAll(teardownTimeoutMs));
|
|
5673
5841
|
}
|
|
5674
5842
|
shutdownPromises.push(this.closeModernMcpConnections(teardownTimeoutMs));
|
|
5675
5843
|
// A stopped framework's servers' addresses leave the reference registry,
|
|
5676
5844
|
// so no other framework's stub reads them and the registry doesn't keep
|
|
5677
|
-
// this one reachable.
|
|
5845
|
+
// this one reachable.
|
|
5678
5846
|
step(() => { for (const id of this.mcplServerConfigs.keys()) referenceRegistry.forgetServerAddress(id, this); });
|
|
5679
5847
|
// Timed-out or failed teardown retains its context and may still write.
|
|
5680
5848
|
// Sync available state best-effort, but leave its store open for a retry.
|
|
@@ -5734,8 +5902,8 @@ export class AgentFramework {
|
|
|
5734
5902
|
// No stream is left, unless its wait above ran out or its cancel threw:
|
|
5735
5903
|
// from the start of stop() none registers and no agent turn starts
|
|
5736
5904
|
// (providerAdmissionClosed, checked for every agent), and every stream
|
|
5737
|
-
// registered before was cancelled and awaited above. (A start()
|
|
5738
|
-
//
|
|
5905
|
+
// registered before was cancelled and awaited above. (A start() is
|
|
5906
|
+
// refused once a stop() is admitted, so admission stays closed.)
|
|
5739
5907
|
// What is still deferred (a message held for a turn's end whose flush
|
|
5740
5908
|
// found the target busy again, or one deferred behind its target's own
|
|
5741
5909
|
// state) would wait for a flush that may never come. Land each at its
|
|
@@ -5820,7 +5988,7 @@ export class AgentFramework {
|
|
|
5820
5988
|
}
|
|
5821
5989
|
|
|
5822
5990
|
if (this.ownsStore && teardownSucceeded) {
|
|
5823
|
-
|
|
5991
|
+
this.closeOpenManagers();
|
|
5824
5992
|
try {
|
|
5825
5993
|
this.store.close();
|
|
5826
5994
|
this.ownedStoreClosed = true;
|
|
@@ -6253,10 +6421,9 @@ export class AgentFramework {
|
|
|
6253
6421
|
}
|
|
6254
6422
|
contextManager.close();
|
|
6255
6423
|
}
|
|
6256
|
-
private closeOpenManagers():
|
|
6424
|
+
private closeOpenManagers(): void {
|
|
6257
6425
|
const alive = [...this.openManagers].flatMap((ref) => ref.deref() ?? []);
|
|
6258
6426
|
for (const contextManager of alive) this.closeManager(contextManager);
|
|
6259
|
-
return alive.length > 0;
|
|
6260
6427
|
}
|
|
6261
6428
|
/** The framework's membrane, with each complete() call passed through
|
|
6262
6429
|
* `admit` along with the model it names. */
|
|
@@ -6266,8 +6433,17 @@ export class AgentFramework {
|
|
|
6266
6433
|
const value = Reflect.get(obj, prop, receiver);
|
|
6267
6434
|
if (prop === 'complete' && typeof value === 'function') {
|
|
6268
6435
|
return (...args: unknown[]) => {
|
|
6269
|
-
const
|
|
6270
|
-
|
|
6436
|
+
const config = (args[0] as { config?: { model?: unknown; maxTokens?: unknown } } | undefined)?.config;
|
|
6437
|
+
const model = config?.model;
|
|
6438
|
+
const options = args[1] as { progressTimeoutMs?: unknown } | undefined;
|
|
6439
|
+
const bounds = this.inferenceBounds;
|
|
6440
|
+
const sent = bounds !== null && options?.progressTimeoutMs === undefined
|
|
6441
|
+
? [args[0], {
|
|
6442
|
+
...options,
|
|
6443
|
+
progressTimeoutMs: wholeCallMs(bounds, typeof config?.maxTokens === 'number' ? config.maxTokens : undefined),
|
|
6444
|
+
}, ...args.slice(2)]
|
|
6445
|
+
: args;
|
|
6446
|
+
return admit(() => Reflect.apply(value, this.membrane, sent) as Promise<unknown>,
|
|
6271
6447
|
typeof model === 'string' ? model : undefined);
|
|
6272
6448
|
};
|
|
6273
6449
|
}
|
|
@@ -6392,7 +6568,7 @@ export class AgentFramework {
|
|
|
6392
6568
|
// length: an operator release of it returns the hold to AF's own pacing.
|
|
6393
6569
|
...(pacing?.stated ? { statedWait: { model: agent.model, ownUntil: now + pacing.ownMs } } : {}),
|
|
6394
6570
|
...(auxiliary && (!existing || existing.auxiliaryOrigin) ? { auxiliaryOrigin: true } : {}),
|
|
6395
|
-
...(existing?.
|
|
6571
|
+
...(existing?.emptiedBy && held.length === 0 ? { emptiedBy: existing.emptiedBy } : {}) });
|
|
6396
6572
|
if (auxiliary && !existing) {
|
|
6397
6573
|
console.error(`[provider-cooldown] agent=${agent.name} host hold (${hostHold!.reason}) from an auxiliary call — ` +
|
|
6398
6574
|
`holding primary/auxiliary for ${delayMs}ms`);
|
|
@@ -6407,11 +6583,22 @@ export class AgentFramework {
|
|
|
6407
6583
|
if (wait && trigger && (wait.until === null || wait.until > now)) this.noteStatedWaitRun(agent.name, wait, now);
|
|
6408
6584
|
return true;
|
|
6409
6585
|
}
|
|
6410
|
-
private
|
|
6586
|
+
private takeAsTransient(agentName: string, error: Error, trigger: InferenceRequest | undefined): boolean {
|
|
6587
|
+
if (isTransientFailure(error)) {
|
|
6588
|
+
this.rearmDeadlineCuts.delete(agentName);
|
|
6589
|
+
return true;
|
|
6590
|
+
}
|
|
6591
|
+
if (!isRearmed(trigger) || !isDeadlineExpiry(error)) return false;
|
|
6592
|
+
const first = !this.rearmDeadlineCuts.has(agentName);
|
|
6593
|
+
this.rearmDeadlineCuts.add(agentName);
|
|
6594
|
+
return first;
|
|
6595
|
+
}
|
|
6596
|
+
|
|
6597
|
+
private rearmExhaustedTurn(agent: Agent, error: Error, trigger: InferenceRequest | undefined, ownsProviderGate: boolean, routing: TurnRouting | undefined, asTransient: boolean): void {
|
|
6411
6598
|
if (!ownsProviderGate || trigger === undefined || this.providerAdmissionClosed || this.turnRearmMs.length === 0) return;
|
|
6412
6599
|
const done = this.turnRearms.get(agent.name) ?? 0;
|
|
6413
6600
|
const notSent = (why: string) => console.error(`[inference-rearm] agent=${agent.name} turn not re-armed: ${why}`);
|
|
6414
|
-
if (!
|
|
6601
|
+
if (!asTransient) {
|
|
6415
6602
|
if (done > 0) notSent(`its failure isn't transient (${isMembraneError(error) ? error.type : boundFailureText(error.message, FAILURE_SUMMARY_MAX_CHARS)})`);
|
|
6416
6603
|
return;
|
|
6417
6604
|
}
|
|
@@ -6551,12 +6738,15 @@ export class AgentFramework {
|
|
|
6551
6738
|
? `no longer holds primary turns: the primary model is now ${model} (the wait stays recorded)`
|
|
6552
6739
|
: `primary hold ended after ${Date.now() - cooldown.startedAt}ms`));
|
|
6553
6740
|
}
|
|
6554
|
-
if ((cooldown.auxiliaryOrigin || cooldown.
|
|
6741
|
+
if ((cooldown.auxiliaryOrigin || cooldown.emptiedBy) && cooldown.heldRequests.length === 0) {
|
|
6555
6742
|
// Nobody asked for an inference while parked, or what was held was a
|
|
6556
|
-
// continuation since dropped with its turn
|
|
6557
|
-
// primary the hold left pending won't run, so
|
|
6558
|
-
|
|
6559
|
-
|
|
6743
|
+
// continuation since dropped with its turn, or wakes since withdrawn:
|
|
6744
|
+
// just reopen the gate. A primary the hold left pending won't run, so
|
|
6745
|
+
// its recovery ends.
|
|
6746
|
+
const which = cooldown.emptiedBy === 'drop' ? 'hold, its held continuation dropped,'
|
|
6747
|
+
: cooldown.emptiedBy === 'withdrawal' ? 'hold, its held wakes withdrawn,' : 'auxiliary hold';
|
|
6748
|
+
console.error(`[provider-cooldown] agent=${agentName} ${which} released after ${Date.now() - cooldown.startedAt}ms`);
|
|
6749
|
+
if (cooldown.emptiedBy) this.endPendingPrimary(agentName);
|
|
6560
6750
|
this.flushAuxiliaryAdmission(agentName);
|
|
6561
6751
|
return;
|
|
6562
6752
|
}
|
|
@@ -7212,12 +7402,7 @@ export class AgentFramework {
|
|
|
7212
7402
|
*/
|
|
7213
7403
|
pushEvent(event: ProcessEvent): void {
|
|
7214
7404
|
if (event.type === 'tool-result' && this.queue?.isClosed) {
|
|
7215
|
-
|
|
7216
|
-
// may run the tool again): which tool, and whether it succeeded.
|
|
7217
|
-
const outcome = event.result.success && !event.result.isError
|
|
7218
|
-
? 'succeeded'
|
|
7219
|
-
: `failed${event.result.error ? `: ${thrownText(event.result.error)}` : ''}`;
|
|
7220
|
-
console.error(`[framework] ${event.agentName}: result of tool call ${event.callId} (${event.toolName ? `${event.toolName}, ` : ''}${event.moduleName}) arrived after stop — dropped; the tool ${outcome}`);
|
|
7405
|
+
console.error(droppedToolResultLine(event, 'after stop'));
|
|
7221
7406
|
return;
|
|
7222
7407
|
}
|
|
7223
7408
|
this.queue.push(event);
|
|
@@ -7247,6 +7432,14 @@ export class AgentFramework {
|
|
|
7247
7432
|
else this.queue.push(event);
|
|
7248
7433
|
}
|
|
7249
7434
|
|
|
7435
|
+
private dropRetiredModuleEvent(event: ProcessEvent, moduleName: string): void {
|
|
7436
|
+
if (this.queue.isClosed) {
|
|
7437
|
+
this.pushModuleEvent(event, moduleName, true);
|
|
7438
|
+
return;
|
|
7439
|
+
}
|
|
7440
|
+
console.error(retiredPushLine(event, moduleName));
|
|
7441
|
+
}
|
|
7442
|
+
|
|
7250
7443
|
/**
|
|
7251
7444
|
* Add a trace event listener for observability.
|
|
7252
7445
|
*/
|
|
@@ -7298,12 +7491,17 @@ export class AgentFramework {
|
|
|
7298
7491
|
}
|
|
7299
7492
|
|
|
7300
7493
|
/**
|
|
7301
|
-
* Add a module at runtime. Refused, with a rejection,
|
|
7302
|
-
*
|
|
7303
|
-
*
|
|
7304
|
-
* past it
|
|
7494
|
+
* Add a module at runtime. Refused, with a rejection, once a stop() has
|
|
7495
|
+
* been admitted, before anything else is checked: while that stop() runs,
|
|
7496
|
+
* a module added then would either be stopped by it or, once it has taken
|
|
7497
|
+
* its list of modules to stop, stay started past it; once it has settled,
|
|
7498
|
+
* the framework has stopped for good, as start() says. A stop() admitted
|
|
7499
|
+
* while the module's start() runs stops the module once that start() has
|
|
7500
|
+
* settled; if that start() fulfils, this rejects the same way then, with
|
|
7501
|
+
* no `module:added`, and if it rejects, this rejects with its error.
|
|
7305
7502
|
*/
|
|
7306
7503
|
async addModule(module: Module): Promise<void> {
|
|
7504
|
+
this.refuseOnceStopped('addModule()');
|
|
7307
7505
|
// A server's tool prefix and a module's name share one namespace, as
|
|
7308
7506
|
// create() checks at startup: a module isn't added under a prefix a
|
|
7309
7507
|
// server holds, or one a restart is moving its server to.
|
|
@@ -7321,8 +7519,8 @@ export class AgentFramework {
|
|
|
7321
7519
|
throw new Error(`Module "${module.name}" collides with MCPL server "${id}"'s toolPrefix "${module.name}" (a closed tab)`);
|
|
7322
7520
|
}
|
|
7323
7521
|
}
|
|
7324
|
-
this.resetCompletedStop('addModule()');
|
|
7325
7522
|
await this.moduleRegistry.addModule(module);
|
|
7523
|
+
this.refuseOnceStopped('addModule()');
|
|
7326
7524
|
// The workspace judges whether an agent's history holds a message as the
|
|
7327
7525
|
// framework judges its own notices, for its head-move notice
|
|
7328
7526
|
// (noticeHeadMoves) and the writes it weighs against it. A module taken
|
|
@@ -7337,7 +7535,9 @@ export class AgentFramework {
|
|
|
7337
7535
|
}
|
|
7338
7536
|
|
|
7339
7537
|
/**
|
|
7340
|
-
* Remove a module at runtime.
|
|
7538
|
+
* Remove a module at runtime. A module whose start() is still running is
|
|
7539
|
+
* stopped once that start() has settled; this waits for that and for its
|
|
7540
|
+
* stop(), with no time limit.
|
|
7341
7541
|
*/
|
|
7342
7542
|
async removeModule(name: string): Promise<void> {
|
|
7343
7543
|
await this.moduleRegistry.removeModule(name);
|
|
@@ -7686,7 +7886,7 @@ export class AgentFramework {
|
|
|
7686
7886
|
];
|
|
7687
7887
|
}
|
|
7688
7888
|
|
|
7689
|
-
/** The registered workspace module, if any (used by
|
|
7889
|
+
/** The registered workspace module, if any (used by save_recent_image). */
|
|
7690
7890
|
private getWorkspaceModule(): WorkspaceModule | undefined {
|
|
7691
7891
|
const module = this.moduleRegistry.getAllModules().find((m) => m.name === 'workspace');
|
|
7692
7892
|
return module && typeof (module as WorkspaceModule).writeBinary === 'function'
|
|
@@ -8165,15 +8365,11 @@ export class AgentFramework {
|
|
|
8165
8365
|
private stallSkips = 0;
|
|
8166
8366
|
/** See WITHHELD_SPILL_DEADLINE_MS; an instance field so tests can shorten it. */
|
|
8167
8367
|
private withheldSpillDeadlineMs = WITHHELD_SPILL_DEADLINE_MS;
|
|
8168
|
-
/**
|
|
8169
|
-
*
|
|
8170
|
-
* that is stopping, and its stub names the audit
|
|
8171
|
-
*
|
|
8172
|
-
|
|
8173
|
-
* is still open (start() doesn't restore stopped modules or MCPL
|
|
8174
|
-
* connections, and refuses an owned store that stop() closed). A start()
|
|
8175
|
-
* made while a stop() is still running is refused. */
|
|
8176
|
-
private guardSpillsClosedFor: number | null = null;
|
|
8368
|
+
/** Set once a stop() has let the running annotations finish, and cleared
|
|
8369
|
+
* as each stop() begins: while set, a withheld original is no longer
|
|
8370
|
+
* written to a workspace that is stopping, and its stub names the audit
|
|
8371
|
+
* record. */
|
|
8372
|
+
private guardSpillsClosed = false;
|
|
8177
8373
|
|
|
8178
8374
|
/** A new agent's guard: its persisted setting, the tool-results spill its
|
|
8179
8375
|
* withheld originals are written to, and the set stop() waits on
|
|
@@ -8904,7 +9100,7 @@ export class AgentFramework {
|
|
|
8904
9100
|
// Bounded grace for cancelled streams. Stop cancels streams too; a turn
|
|
8905
9101
|
// with no stream settles on its own. Report the abandon that already ran,
|
|
8906
9102
|
// even if stop cuts its grace short.
|
|
8907
|
-
const grace = Date.now() +
|
|
9103
|
+
const grace = Date.now() + (testAbandonGraces.get(this) ?? ABANDON_GRACE_MS);
|
|
8908
9104
|
while (this.quiesced && !this.queue?.isClosed && this.activeTurnTokens.size > 0 && Date.now() < grace) {
|
|
8909
9105
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
8910
9106
|
}
|
|
@@ -9334,8 +9530,9 @@ export class AgentFramework {
|
|
|
9334
9530
|
* memory-only: they land within the turn, at a tool boundary or at its
|
|
9335
9531
|
* end, and a crash before then loses them. That window runs to the turn's
|
|
9336
9532
|
* end for a message held there and what is queued behind it, and, on a
|
|
9337
|
-
* stream that
|
|
9338
|
-
* prefill path
|
|
9533
|
+
* stream that doesn't declare it carries injected messages (membrane's XML
|
|
9534
|
+
* prefill path declares it can't), for every message deferred during the
|
|
9535
|
+
* turn.
|
|
9339
9536
|
*
|
|
9340
9537
|
* A write that fails, or a queue over the cap, which writes nothing,
|
|
9341
9538
|
* leaves the record as it last held it, except that store only, a write
|
|
@@ -9353,20 +9550,21 @@ export class AgentFramework {
|
|
|
9353
9550
|
* itself before it changes the host's mode, and refuses instead, unless
|
|
9354
9551
|
* the call has `abandon` (writeDeferredWritesForQuiesce).
|
|
9355
9552
|
*/
|
|
9356
|
-
private persistDeferredWrites():
|
|
9553
|
+
private persistDeferredWrites(): boolean {
|
|
9357
9554
|
let outcome: DeferredWritesOutcome;
|
|
9358
9555
|
try {
|
|
9359
9556
|
outcome = this.writeDeferredWrites(this.quiesced);
|
|
9360
9557
|
} catch (err) {
|
|
9361
9558
|
console.error('[host-mode] failed to persist deferred context writes:', err);
|
|
9362
9559
|
this.deferredWritesFellBehind({ error: err });
|
|
9363
|
-
return;
|
|
9560
|
+
return false;
|
|
9364
9561
|
}
|
|
9365
9562
|
if (outcome === 'over-cap') {
|
|
9366
9563
|
this.deferredWritesFellBehind('over-cap');
|
|
9367
|
-
|
|
9368
|
-
this.deferredWritesCaughtUp();
|
|
9564
|
+
return false;
|
|
9369
9565
|
}
|
|
9566
|
+
if (outcome !== 'nothing') this.deferredWritesCaughtUp();
|
|
9567
|
+
return true;
|
|
9370
9568
|
}
|
|
9371
9569
|
|
|
9372
9570
|
/**
|
|
@@ -10757,8 +10955,9 @@ export class AgentFramework {
|
|
|
10757
10955
|
* **Inheritance.** `mode: 'shared'` (the default) gives the child the
|
|
10758
10956
|
* parent's messages and fold state through shared storage: nothing is
|
|
10759
10957
|
* copied, the child folds with the parent's memory tree, and neither can
|
|
10760
|
-
* alter what the other reads. It needs
|
|
10761
|
-
*
|
|
10958
|
+
* alter what the other reads. It needs a store that supports derivation
|
|
10959
|
+
* (`ContextManager.supportsDerivation`), and is refused, not silently
|
|
10960
|
+
* downgraded, without one. `mode: 'copy'`
|
|
10762
10961
|
* is the historical mechanism — the parent's compiled context re-added as
|
|
10763
10962
|
* the child's own messages — and works everywhere.
|
|
10764
10963
|
*
|
|
@@ -11347,6 +11546,7 @@ export class AgentFramework {
|
|
|
11347
11546
|
if (run) {
|
|
11348
11547
|
run.endReason ??= 'stopped';
|
|
11349
11548
|
run.endedBy ??= options.by;
|
|
11549
|
+
run.endedFromOutside = true;
|
|
11350
11550
|
// The awaiting caller is told who stopped the job and why, whether or
|
|
11351
11551
|
// not a stream was in flight: settled first, so the aborted stream's own
|
|
11352
11552
|
// settle ('Stream cancelled') finds the run already settled. The caller's
|
|
@@ -11725,6 +11925,7 @@ export class AgentFramework {
|
|
|
11725
11925
|
if (run) {
|
|
11726
11926
|
run.endReason = reason;
|
|
11727
11927
|
run.endedBy = record.ended?.by;
|
|
11928
|
+
run.endedFromOutside = true;
|
|
11728
11929
|
run.settle.reject(new AgentStoppedError(name, reason, record.ended?.by, detail));
|
|
11729
11930
|
return;
|
|
11730
11931
|
}
|
|
@@ -11733,6 +11934,15 @@ export class AgentFramework {
|
|
|
11733
11934
|
this.releaseAgentRuntime(agent, this.channelOfHome(record.homeChannel), undefined, record.lifetime.kind === 'persistent');
|
|
11734
11935
|
}
|
|
11735
11936
|
|
|
11937
|
+
private forgetFailureAccounting(name: string): void {
|
|
11938
|
+
this.consecutiveInferenceFailures.delete(name);
|
|
11939
|
+
this.exhaustionRewinds.delete(name);
|
|
11940
|
+
this.refusalStreak.delete(name);
|
|
11941
|
+
this.refusalRewinds.delete(name);
|
|
11942
|
+
this.rewindEpisode.delete(name);
|
|
11943
|
+
if (this.forcedRewind.delete(name)) console.error(`[unstick] agent=${name} its /unstick session ended with its agent`);
|
|
11944
|
+
}
|
|
11945
|
+
|
|
11736
11946
|
/**
|
|
11737
11947
|
* Everything the process holds for a live, non-ephemeral agent that has
|
|
11738
11948
|
* ended, released once, on every path an agent ends by — stopAgent, the
|
|
@@ -11776,6 +11986,9 @@ export class AgentFramework {
|
|
|
11776
11986
|
this.eventGate?.agentGone(name);
|
|
11777
11987
|
this.agents.delete(name);
|
|
11778
11988
|
this.agentConfigs.delete(name);
|
|
11989
|
+
this.deadlineCuts.delete(name);
|
|
11990
|
+
this.freshCallsCut.delete(name);
|
|
11991
|
+
this.forgetFailureAccounting(name);
|
|
11779
11992
|
this.thinkingLossIncidents.forget(name);
|
|
11780
11993
|
this.reasoningLoss.forget(name);
|
|
11781
11994
|
// main's manager release: the context manager an ephemeral run or a fork
|
|
@@ -11817,6 +12030,7 @@ export class AgentFramework {
|
|
|
11817
12030
|
// waits ends here as at a turn's end, alerted if it ran long
|
|
11818
12031
|
// (endStatedWaitRun).
|
|
11819
12032
|
this.turnRearms.delete(name);
|
|
12033
|
+
this.rearmDeadlineCuts.delete(name);
|
|
11820
12034
|
this.exhaustingRearms.delete(name);
|
|
11821
12035
|
this.exhaustedTurns.delete(name);
|
|
11822
12036
|
this.endStatedWaitRun(name);
|
|
@@ -12221,13 +12435,12 @@ export class AgentFramework {
|
|
|
12221
12435
|
|
|
12222
12436
|
/**
|
|
12223
12437
|
* A start dropped before its stream sends anything (its agent was replaced,
|
|
12224
|
-
* or stop() closed admission
|
|
12225
|
-
* ends
|
|
12226
|
-
*
|
|
12227
|
-
*
|
|
12228
|
-
*
|
|
12229
|
-
*
|
|
12230
|
-
* other turn open.
|
|
12438
|
+
* or stop() closed admission) ends the logical turn it was running. stop()
|
|
12439
|
+
* ends every open turn (openLogical), but a fresh start that awaited its
|
|
12440
|
+
* batches' assembly across a stop() reaches its routing after it and opens
|
|
12441
|
+
* its turn. That turn would then stay open, and the next start of the
|
|
12442
|
+
* agent's name would continue the dropped turn's routing. The identity
|
|
12443
|
+
* check leaves any other turn open.
|
|
12231
12444
|
*/
|
|
12232
12445
|
private endDroppedTurn(agentName: string, routing: TurnRouting): void {
|
|
12233
12446
|
if (this.openLogical.get(agentName) === routing) this.openLogical.delete(agentName);
|
|
@@ -12236,9 +12449,36 @@ export class AgentFramework {
|
|
|
12236
12449
|
// Its scripts take it as a settle's end of the turn: the start sent
|
|
12237
12450
|
// nothing, so none of its own calls is waiting on a script.
|
|
12238
12451
|
this.settledTurns.add(routing);
|
|
12452
|
+
this.releaseTurnInputs(routing);
|
|
12239
12453
|
this.codeExecution?.scripts.turnEnded(agentName, { id: routing.turnId, live: this.liveTurns(agentName, routing) }, false);
|
|
12240
12454
|
}
|
|
12241
12455
|
|
|
12456
|
+
private pinTurnInputs(agent: Agent, routing: TurnRouting): void {
|
|
12457
|
+
let range: { first: MessageId; last: MessageId } | undefined;
|
|
12458
|
+
try {
|
|
12459
|
+
const cm = agent.getContextManager();
|
|
12460
|
+
const watermark = agent.getConsumedWatermark();
|
|
12461
|
+
if (watermark && watermark.branch === cm.currentBranch().name) {
|
|
12462
|
+
range = turnInputs(cm, watermark.sequence, agent.name, (metadata) => this.agentSees(agent, metadata));
|
|
12463
|
+
}
|
|
12464
|
+
} catch (error) {
|
|
12465
|
+
console.error(`[turn-inputs] ${agent.name}: could not read the turn's inputs, so none is pinned:`, error);
|
|
12466
|
+
}
|
|
12467
|
+
if (!range) {
|
|
12468
|
+
agent.unpinTurnInputs();
|
|
12469
|
+
return;
|
|
12470
|
+
}
|
|
12471
|
+
const id = agent.pinTurnInputs(range.first, range.last);
|
|
12472
|
+
if (id !== undefined) routing.inputsPin = { agent, id };
|
|
12473
|
+
}
|
|
12474
|
+
|
|
12475
|
+
private releaseTurnInputs(routing: TurnRouting): void {
|
|
12476
|
+
const pin = routing.inputsPin;
|
|
12477
|
+
if (!pin) return;
|
|
12478
|
+
routing.inputsPin = undefined;
|
|
12479
|
+
pin.agent.unpinTurnInputs(pin.id);
|
|
12480
|
+
}
|
|
12481
|
+
|
|
12242
12482
|
/** A logical turn's end, for every agent: it closes the agent's open
|
|
12243
12483
|
* logical turn when it is the settling stream's own (openLogical), and
|
|
12244
12484
|
* settles an active ephemeral run. The run's tool-call count comes from
|
|
@@ -12276,7 +12516,10 @@ export class AgentFramework {
|
|
|
12276
12516
|
// or of the stream that now holds the agent, are left alone.
|
|
12277
12517
|
if (stream) {
|
|
12278
12518
|
// Ended, unless it's the logical turn this settle left open.
|
|
12279
|
-
if (this.openLogical.get(agentName) !== stream)
|
|
12519
|
+
if (this.openLogical.get(agentName) !== stream) {
|
|
12520
|
+
this.settledTurns.add(stream);
|
|
12521
|
+
this.releaseTurnInputs(stream);
|
|
12522
|
+
}
|
|
12280
12523
|
this.codeExecution?.scripts.turnEnded(
|
|
12281
12524
|
agentName,
|
|
12282
12525
|
{ id: stream.turnId, live: this.liveTurns(agentName, stream) },
|
|
@@ -12543,6 +12786,9 @@ export class AgentFramework {
|
|
|
12543
12786
|
// deregistered, so disposal must release EventGate liveness itself.
|
|
12544
12787
|
this.eventGate?.onInferenceEnded(agent.name);
|
|
12545
12788
|
this.agents.delete(agent.name);
|
|
12789
|
+
this.deadlineCuts.delete(agent.name);
|
|
12790
|
+
this.freshCallsCut.delete(agent.name);
|
|
12791
|
+
if (run.endedFromOutside) this.forgetFailureAccounting(agent.name);
|
|
12546
12792
|
this.thinkingLossIncidents.forget(agent.name);
|
|
12547
12793
|
this.reasoningLoss.forget(agent.name);
|
|
12548
12794
|
// Its pending self-wake ends with it, as a fork's does.
|
|
@@ -12717,6 +12963,15 @@ export class AgentFramework {
|
|
|
12717
12963
|
},
|
|
12718
12964
|
];
|
|
12719
12965
|
|
|
12966
|
+
private static readonly WAKE_ADD_RULE_FIELDS: Readonly<Record<string, string>> = {
|
|
12967
|
+
'behavior.debounce': 'debounceMs',
|
|
12968
|
+
'behavior.rate_limit': 'rateLimit',
|
|
12969
|
+
'behavior.passive_sample': 'passiveSample',
|
|
12970
|
+
'behavior.judge': 'judge',
|
|
12971
|
+
'position.before': 'insertBefore',
|
|
12972
|
+
'position.after': 'insertAfter',
|
|
12973
|
+
};
|
|
12974
|
+
|
|
12720
12975
|
/** Synthesized wake-rule tools: add/remove a gate.json policy at runtime.
|
|
12721
12976
|
* Present when a gate is wired. They write validated policies into the
|
|
12722
12977
|
* hot-reloaded gate.json (same validation as load), so a rule takes effect
|
|
@@ -12909,10 +13164,10 @@ export class AgentFramework {
|
|
|
12909
13164
|
/** Synthesized drafts tool — present whenever MCPL channels are
|
|
12910
13165
|
* configured. A resident's held drafts (src/prose-drafts.ts): plain speech
|
|
12911
13166
|
* that was not sent, never published except by its resident's resend, and
|
|
12912
|
-
* kept as that module's retention says.
|
|
12913
|
-
*
|
|
12914
|
-
* every property as required can still say "not in use". */
|
|
12915
|
-
private static readonly DRAFTS_TOOL = {
|
|
13167
|
+
* kept as that module's retention says. Its optional fields are nullable,
|
|
13168
|
+
* as save_recent_image's selectors are: a caller whose provider presents
|
|
13169
|
+
* every property as required can still say "not in use" (fieldInUse). */
|
|
13170
|
+
private static readonly DRAFTS_TOOL: import('./types/index.js').ToolDefinition = {
|
|
12916
13171
|
name: 'drafts',
|
|
12917
13172
|
description:
|
|
12918
13173
|
'Your held drafts: plain speech of yours that was NOT sent — held back because an explicit send in its turn ' +
|
|
@@ -12951,12 +13206,12 @@ export class AgentFramework {
|
|
|
12951
13206
|
},
|
|
12952
13207
|
required: ['action'],
|
|
12953
13208
|
},
|
|
12954
|
-
}
|
|
13209
|
+
};
|
|
12955
13210
|
|
|
12956
|
-
/** Synthesized
|
|
13211
|
+
/** Synthesized save_recent_image tool — present when a workspace module is
|
|
12957
13212
|
* registered. Lets the agent persist an image it has already seen in its
|
|
12958
13213
|
* own context (Discord attachments arrive inlined as base64 and are
|
|
12959
|
-
* otherwise unreachable as files). */
|
|
13214
|
+
* otherwise unreachable as files). Its three selectors are nullable. */
|
|
12960
13215
|
private static readonly SAVE_IMAGE_TOOL: import('./types/index.js').ToolDefinition = {
|
|
12961
13216
|
name: 'save_recent_image',
|
|
12962
13217
|
description:
|
|
@@ -12972,7 +13227,8 @@ export class AgentFramework {
|
|
|
12972
13227
|
'restart) the call fails and writes nothing — it never substitutes an older ' +
|
|
12973
13228
|
'image. When other tool calls in the same batch are still running, the save ' +
|
|
12974
13229
|
'waits for them so their images are counted. Receipts carry source tool call, MIME, byte size and SHA-256. Saved ' +
|
|
12975
|
-
'files are visible via workspace tools.'
|
|
13230
|
+
'files are visible via workspace tools. Select by `ref` or by ' +
|
|
13231
|
+
'`index`/`count`; a selector you are not using may be omitted or passed as null.',
|
|
12976
13232
|
inputSchema: {
|
|
12977
13233
|
type: 'object',
|
|
12978
13234
|
properties: {
|
|
@@ -12980,19 +13236,27 @@ export class AgentFramework {
|
|
|
12980
13236
|
type: 'string',
|
|
12981
13237
|
description: 'Mount-prefixed destination path, e.g. "project/photos/name.png".',
|
|
12982
13238
|
},
|
|
13239
|
+
// Nullable selectors: a caller whose provider presents every property
|
|
13240
|
+
// as required must still be able to say "not this selector". Omission,
|
|
13241
|
+
// null and "" all mean it isn't used (fieldInUse); any other value is
|
|
13242
|
+
// checked. The descriptions name null, the form to ask for.
|
|
12983
13243
|
index: {
|
|
12984
|
-
type: '
|
|
12985
|
-
description:
|
|
13244
|
+
type: ['integer', 'null'],
|
|
13245
|
+
description:
|
|
13246
|
+
'Which image, counting back from the most recent (0 = most recent). Default 0. ' +
|
|
13247
|
+
'null, like omitting it, means the default.',
|
|
12986
13248
|
},
|
|
12987
13249
|
count: {
|
|
12988
|
-
type: '
|
|
12989
|
-
description:
|
|
13250
|
+
type: ['integer', 'null'],
|
|
13251
|
+
description:
|
|
13252
|
+
'How many images to save, starting at `index` and going further back. Default 1. ' +
|
|
13253
|
+
'null, like omitting it, means the default.',
|
|
12990
13254
|
},
|
|
12991
13255
|
ref: {
|
|
12992
|
-
type: 'string',
|
|
13256
|
+
type: ['string', 'null'],
|
|
12993
13257
|
description:
|
|
12994
13258
|
'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. ' +
|
|
12995
|
-
'Mutually exclusive with `index`/`count
|
|
13259
|
+
'Mutually exclusive with `index`/`count`: pass null for the selector you are not using. null, like omitting it, means no ref.',
|
|
12996
13260
|
},
|
|
12997
13261
|
},
|
|
12998
13262
|
required: ['path'],
|
|
@@ -15808,8 +16072,9 @@ export class AgentFramework {
|
|
|
15808
16072
|
// idiom): reasoning effort (thinking), provider params, token ceilings,
|
|
15809
16073
|
// caching, refusal handling, timezone — a same-model side-process must
|
|
15810
16074
|
// not silently run under different inference conditions than the
|
|
15811
|
-
// resident it serves.
|
|
15812
|
-
// overridden
|
|
16075
|
+
// resident it serves. Below, identity- and surface-defining fields are
|
|
16076
|
+
// overridden, and the window and thinking binding where the
|
|
16077
|
+
// subconscious's config sets its own.
|
|
15813
16078
|
const model = cfg.model ?? primaryConfig.model;
|
|
15814
16079
|
const inherited: AgentConfig = { ...primaryConfig };
|
|
15815
16080
|
if (model !== primaryConfig.model) {
|
|
@@ -15817,6 +16082,7 @@ export class AgentFramework {
|
|
|
15817
16082
|
}
|
|
15818
16083
|
const agentConfig: AgentConfig = {
|
|
15819
16084
|
...inherited,
|
|
16085
|
+
...readerModelFields(cfg),
|
|
15820
16086
|
name,
|
|
15821
16087
|
model,
|
|
15822
16088
|
systemPrompt: cfg.systemPrompt,
|
|
@@ -15924,6 +16190,7 @@ export class AgentFramework {
|
|
|
15924
16190
|
config: {
|
|
15925
16191
|
model: cfg.model,
|
|
15926
16192
|
proseRouting: 'disabled',
|
|
16193
|
+
...readerModelFields(cfg),
|
|
15927
16194
|
},
|
|
15928
16195
|
inheritRefusals: true,
|
|
15929
16196
|
onParentEnd: 'end',
|
|
@@ -16037,13 +16304,7 @@ export class AgentFramework {
|
|
|
16037
16304
|
}
|
|
16038
16305
|
|
|
16039
16306
|
private async runLoop(): Promise<void> {
|
|
16040
|
-
|
|
16041
|
-
// this loop was in a call (a module's onProcess) returns before the call
|
|
16042
|
-
// does, and a start() after it begins a lifecycle with a loop of its own;
|
|
16043
|
-
// this one finishes its iteration and exits, rather than running beside
|
|
16044
|
-
// that loop for good.
|
|
16045
|
-
const lifecycle = this.lifecycleGeneration;
|
|
16046
|
-
while (this.running && this.lifecycleGeneration === lifecycle) {
|
|
16307
|
+
while (this.running) {
|
|
16047
16308
|
try {
|
|
16048
16309
|
await this.processNextEvent();
|
|
16049
16310
|
} catch (error) {
|
|
@@ -16331,18 +16592,21 @@ export class AgentFramework {
|
|
|
16331
16592
|
// compiled prefix and the live request still match without it
|
|
16332
16593
|
// (#232 gap 5).
|
|
16333
16594
|
//
|
|
16334
|
-
//
|
|
16335
|
-
// (supportsInjectedMessages
|
|
16336
|
-
//
|
|
16337
|
-
//
|
|
16338
|
-
//
|
|
16339
|
-
//
|
|
16595
|
+
// Only a stream that declares it carries injected messages
|
|
16596
|
+
// (supportsInjectedMessages true: membrane's native path) gets
|
|
16597
|
+
// them. Membrane's XML prefill path declares false, and a stream
|
|
16598
|
+
// that doesn't say is taken as one that doesn't, so nothing is
|
|
16599
|
+
// drained for either: everything queued for the agent waits for
|
|
16600
|
+
// the turn-end flush, as a held message does, rather than sitting
|
|
16601
|
+
// in the window between rounds the stream never showed it. A
|
|
16602
|
+
// boundary with no stream, the direct API's, drains: its
|
|
16603
|
+
// re-inference compiles from the store.
|
|
16340
16604
|
const midTurnInjections: Array<{ participant: string; content: ContentBlock[]; metadata?: MessageMetadata; id?: MessageId }> = [];
|
|
16341
16605
|
// What this boundary stored for the agent to hear, in store order,
|
|
16342
16606
|
// for a boundary that leaves it to the turn's next start as
|
|
16343
16607
|
// arrivals: that start's compile sends it.
|
|
16344
16608
|
const heard: Array<{ msg: DeferredWrite; id: MessageId; content: ContentBlock[] }> = [];
|
|
16345
|
-
if (currentState.supportsInjectedMessages
|
|
16609
|
+
if (currentState.supportsInjectedMessages === true || !currentState.stream) {
|
|
16346
16610
|
// A clock row due (storeDueClock) goes right after the round's
|
|
16347
16611
|
// results, ahead of what is drained below, and joins the stream
|
|
16348
16612
|
// the same way. A boundary that passed during the round can't
|
|
@@ -16443,12 +16707,10 @@ export class AgentFramework {
|
|
|
16443
16707
|
|
|
16444
16708
|
// A live stream handed these messages after the results follows
|
|
16445
16709
|
// them in its next round, not the tool loop of a reply to
|
|
16446
|
-
// [Continue]: that round's rows carry no prompt.
|
|
16447
|
-
//
|
|
16448
|
-
//
|
|
16449
|
-
//
|
|
16450
|
-
// restarts the stream or falls back to a new start below hands
|
|
16451
|
-
// them to no live stream, and a new start reads its own prompt from
|
|
16710
|
+
// [Continue]: that round's rows carry no prompt. A boundary that
|
|
16711
|
+
// ends the turn, restarts the stream or falls back to a new start
|
|
16712
|
+
// below hands them to no live stream, and a new start reads its own
|
|
16713
|
+
// prompt from
|
|
16452
16714
|
// what it sends. (The routing notice below rides only an injection
|
|
16453
16715
|
// already here.)
|
|
16454
16716
|
if (midTurnInjections.length > 0) {
|
|
@@ -16683,11 +16945,10 @@ export class AgentFramework {
|
|
|
16683
16945
|
}
|
|
16684
16946
|
// Streaming path: convert results and resume the stream.
|
|
16685
16947
|
// Mid-turn messages collected above ride along as injected user
|
|
16686
|
-
// messages. Membrane's native path
|
|
16687
|
-
//
|
|
16688
|
-
//
|
|
16689
|
-
//
|
|
16690
|
-
// collected for it. Each goes as its participant and content
|
|
16948
|
+
// messages. Membrane's native path appends them after the
|
|
16949
|
+
// tool_result envelope, so the next round of THIS turn hears
|
|
16950
|
+
// them. Its XML prefill path declares it can't carry them, so
|
|
16951
|
+
// none are collected for it. Each goes as its participant and content
|
|
16691
16952
|
// alone: a stored row's metadata is the host's record, not input
|
|
16692
16953
|
// for membrane. Each goes with its name and text as the store
|
|
16693
16954
|
// keeps them, whether it's a heard message, rendered or not, or a
|
|
@@ -16718,15 +16979,11 @@ export class AgentFramework {
|
|
|
16718
16979
|
// the watermark of the turn's compile and read as unread, so a
|
|
16719
16980
|
// coalesced replacement could remove a message the agent heard
|
|
16720
16981
|
// and answered (and that its later replies were minted after).
|
|
16721
|
-
// Only a stream that declares it carries them
|
|
16722
|
-
// (supportsInjectedMessages true: membrane's native
|
|
16723
|
-
//
|
|
16724
|
-
//
|
|
16725
|
-
//
|
|
16726
|
-
// saw it. So without the declaration they stay unread, as before
|
|
16727
|
-
// this mark.
|
|
16728
|
-
// The shown mark moves at every boundary that injects, declared
|
|
16729
|
-
// or not, since there a doubt counts as shown.
|
|
16982
|
+
// Only a stream that declares it carries them gets them, and so
|
|
16983
|
+
// is marked read (supportsInjectedMessages true: membrane's native
|
|
16984
|
+
// path).
|
|
16985
|
+
// The shown mark moves at every boundary that injects into its
|
|
16986
|
+
// stream.
|
|
16730
16987
|
// The mark is the store's head, a scalar, as a compile's is: it
|
|
16731
16988
|
// also covers what was stored since the compile without being
|
|
16732
16989
|
// carried, such as another slot's messages, and a write that
|
|
@@ -16738,8 +16995,7 @@ export class AgentFramework {
|
|
|
16738
16995
|
// replies after it go without their thinking, whatever the mark
|
|
16739
16996
|
// says; deferring those writers is the fix.
|
|
16740
16997
|
if (midTurnInjections.length > 0) {
|
|
16741
|
-
//
|
|
16742
|
-
// mark (CoalescerHost.isUnshown) counts a doubt as shown.
|
|
16998
|
+
// Shown: the stream carries them (CoalescerHost.isUnshown).
|
|
16743
16999
|
agent.markContextShown();
|
|
16744
17000
|
if (currentState.supportsInjectedMessages === true) agent.markContextConsumed();
|
|
16745
17001
|
}
|
|
@@ -16799,14 +17055,22 @@ export class AgentFramework {
|
|
|
16799
17055
|
private async dispatchProcessEventToModules(event: ProcessEvent, startTime = Date.now()): Promise<void> {
|
|
16800
17056
|
// Dispatch to all modules, tracking responses with module names
|
|
16801
17057
|
const responses: ModuleProcessResponse[] = [];
|
|
16802
|
-
|
|
17058
|
+
const registrations = new Map<ModuleProcessResponse, object | undefined>();
|
|
17059
|
+
const stillRegistered = (entry: ModuleProcessResponse): boolean =>
|
|
17060
|
+
this.moduleRegistry.registrationOf(entry.moduleName) === registrations.get(entry);
|
|
17061
|
+
const dispatched = this.moduleRegistry.getAllModules()
|
|
17062
|
+
.map((module) => ({ module, registration: this.moduleRegistry.registrationOf(module.name) }));
|
|
17063
|
+
for (const { module, registration } of dispatched) {
|
|
16803
17064
|
// The framework's own toolsets module answers no event; every agent has
|
|
16804
17065
|
// it now, so it isn't listed beside the modules that do.
|
|
16805
17066
|
if (module instanceof ToolsetsModule) continue;
|
|
17067
|
+
if (this.moduleRegistry.registrationOf(module.name) !== registration) continue;
|
|
16806
17068
|
try {
|
|
16807
17069
|
const processState = this.moduleRegistry.createProcessState(module.name);
|
|
16808
17070
|
const response = await module.onProcess(event, processState);
|
|
16809
|
-
|
|
17071
|
+
const entry = { moduleName: module.name, response };
|
|
17072
|
+
responses.push(entry);
|
|
17073
|
+
registrations.set(entry, registration);
|
|
16810
17074
|
} catch (error) {
|
|
16811
17075
|
console.error(`Module ${module.name} error handling process event:`, error);
|
|
16812
17076
|
}
|
|
@@ -16832,7 +17096,17 @@ export class AgentFramework {
|
|
|
16832
17096
|
// responses, a tool call's dispatch and the trace go on, since a module
|
|
16833
17097
|
// can't catch a throw on this path, and one module's mustn't cost the
|
|
16834
17098
|
// event.
|
|
16835
|
-
const
|
|
17099
|
+
const dropUnregistered = (entry: ModuleProcessResponse): boolean => {
|
|
17100
|
+
if (stillRegistered(entry)) return false;
|
|
17101
|
+
const parts = responseParts(entry.response);
|
|
17102
|
+
if (parts) {
|
|
17103
|
+
console.error(
|
|
17104
|
+
`[process] module ${entry.moduleName} stopped before its response to ${event.type} was applied, so none of it was: ${parts}`,
|
|
17105
|
+
);
|
|
17106
|
+
}
|
|
17107
|
+
return true;
|
|
17108
|
+
};
|
|
17109
|
+
const applying = responses.filter((entry) => !dropUnregistered(entry)).filter(({ moduleName, response }) => {
|
|
16836
17110
|
const refusal = malformedResponseContent(response);
|
|
16837
17111
|
if (refusal === null) return true;
|
|
16838
17112
|
const { where, places } = refusal;
|
|
@@ -16855,7 +17129,7 @@ export class AgentFramework {
|
|
|
16855
17129
|
const { moduleName, response } = entry;
|
|
16856
17130
|
const step = { at: 'its start' };
|
|
16857
17131
|
try {
|
|
16858
|
-
|
|
17132
|
+
this.applyProcessResponse(response, event, moduleName, surface, step);
|
|
16859
17133
|
} catch (error) {
|
|
16860
17134
|
entry.threw = { at: step.at, error: error instanceof Error ? error.message : String(error) };
|
|
16861
17135
|
console.error(
|
|
@@ -16883,7 +17157,7 @@ export class AgentFramework {
|
|
|
16883
17157
|
}
|
|
16884
17158
|
}
|
|
16885
17159
|
|
|
16886
|
-
private
|
|
17160
|
+
private applyProcessResponse(
|
|
16887
17161
|
response: EventResponse,
|
|
16888
17162
|
event: ProcessEvent,
|
|
16889
17163
|
moduleName: string,
|
|
@@ -16891,7 +17165,7 @@ export class AgentFramework {
|
|
|
16891
17165
|
surface: InboundSource | undefined,
|
|
16892
17166
|
/** Names the operation under way, for the line when one throws. */
|
|
16893
17167
|
step: { at: string },
|
|
16894
|
-
):
|
|
17168
|
+
): void {
|
|
16895
17169
|
// Add messages. Only the framework writes the inbound envelope: one a
|
|
16896
17170
|
// module's metadata carries (echoed from a server's event, say) is
|
|
16897
17171
|
// dropped, and a surface event's messages carry the framework's own.
|
|
@@ -17695,6 +17969,9 @@ export class AgentFramework {
|
|
|
17695
17969
|
} else {
|
|
17696
17970
|
// Nothing live under the name: only what the process keeps by name.
|
|
17697
17971
|
this.closingConversationAgents.delete(agentName);
|
|
17972
|
+
this.deadlineCuts.delete(agentName);
|
|
17973
|
+
this.freshCallsCut.delete(agentName);
|
|
17974
|
+
this.forgetFailureAccounting(agentName);
|
|
17698
17975
|
this.thinkingLossIncidents.forget(agentName);
|
|
17699
17976
|
this.reasoningLoss.forget(agentName);
|
|
17700
17977
|
// A continuation of its turn still on its way goes with it.
|
|
@@ -18728,25 +19005,45 @@ export class AgentFramework {
|
|
|
18728
19005
|
}
|
|
18729
19006
|
}
|
|
18730
19007
|
|
|
18731
|
-
/** Withdraw unstarted wakes whose sole cause is this subject (§4.2)
|
|
19008
|
+
/** Withdraw unstarted wakes whose sole cause is this subject (§4.2), and
|
|
19009
|
+
* a merged start whose every request is withdrawn. */
|
|
18732
19010
|
private cancelCoalescedWakes(subject: string): void {
|
|
18733
|
-
|
|
18734
|
-
for (const cooldown of this.providerAccelerationCooldowns.values()) {
|
|
18735
|
-
cooldown.heldRequests = cooldown.heldRequests.filter((r) => r.coalescingSubject !== subject);
|
|
18736
|
-
}
|
|
18737
|
-
// A wake of the subject's merged into a start that carries a turn on
|
|
19011
|
+
// A wake of the subject's merged into a start
|
|
18738
19012
|
// (MERGED_BATCH) is withdrawn from the start's batch, so no stop puts
|
|
18739
19013
|
// it back and no pass unpacks it.
|
|
19014
|
+
const emptied = new Set<InferenceRequest>();
|
|
18740
19015
|
const unrecord = (r: InferenceRequest): void => {
|
|
18741
19016
|
const batch = mergedBatch(r);
|
|
18742
19017
|
if (batch?.some((w) => w.coalescingSubject === subject)) {
|
|
18743
|
-
|
|
19018
|
+
const left = batch.filter((w) => w.coalescingSubject !== subject);
|
|
19019
|
+
(r as MarkedRequest)[MERGED_BATCH] = left;
|
|
19020
|
+
if (left.length === 0) emptied.add(r);
|
|
18744
19021
|
}
|
|
18745
19022
|
};
|
|
18746
19023
|
for (const r of this.pendingRequests) unrecord(r);
|
|
18747
19024
|
for (const cooldown of this.providerAccelerationCooldowns.values()) cooldown.heldRequests.forEach(unrecord);
|
|
18748
19025
|
for (const r of this.parkedContinuations ?? []) unrecord(r);
|
|
18749
19026
|
for (const pass of this.passRequests ?? []) for (const requests of pass.values()) requests.forEach(unrecord);
|
|
19027
|
+
const withdrawn = (r: InferenceRequest): boolean => r.coalescingSubject === subject || emptied.has(r);
|
|
19028
|
+
this.pendingRequests = this.pendingRequests.filter((r) => !withdrawn(r));
|
|
19029
|
+
for (const cooldown of this.providerAccelerationCooldowns.values()) {
|
|
19030
|
+
const held = cooldown.heldRequests.length;
|
|
19031
|
+
cooldown.heldRequests = cooldown.heldRequests.filter((r) => !withdrawn(r));
|
|
19032
|
+
if (held > 0 && cooldown.heldRequests.length === 0) cooldown.emptiedBy = 'withdrawal';
|
|
19033
|
+
}
|
|
19034
|
+
for (const pass of this.passRequests ?? []) {
|
|
19035
|
+
for (const requests of pass.values()) {
|
|
19036
|
+
if (!requests.some((r) => emptied.has(r))) continue;
|
|
19037
|
+
const left = requests.filter((r) => !emptied.has(r));
|
|
19038
|
+
requests.length = 0;
|
|
19039
|
+
requests.push(...left);
|
|
19040
|
+
}
|
|
19041
|
+
}
|
|
19042
|
+
for (const start of emptied) {
|
|
19043
|
+
if (!start.coalescingBatch || start.coalescingSubject !== undefined) continue;
|
|
19044
|
+
console.error(`[coalescing] ${start.agentName}: its deferred batches were withdrawn while its wake waited; turn not started (no wake cause left)`);
|
|
19045
|
+
this.emitTrace({ type: 'mcpl:coalescing', kind: 'turn-withdrawn', agentName: start.agentName, subjects: start.coalescingBatchSubjects ?? [] });
|
|
19046
|
+
}
|
|
18750
19047
|
}
|
|
18751
19048
|
|
|
18752
19049
|
/**
|
|
@@ -19261,7 +19558,7 @@ export class AgentFramework {
|
|
|
19261
19558
|
if (recovery) recovery.releasedAt = now;
|
|
19262
19559
|
requests = [...providerCooldown.heldRequests, ...requests];
|
|
19263
19560
|
}
|
|
19264
|
-
// A start that
|
|
19561
|
+
// A start that came back before it took its
|
|
19265
19562
|
// token stands for the batch it was merged from (MERGED_BATCH),
|
|
19266
19563
|
// including one a hold released just now. The batch takes its place,
|
|
19267
19564
|
// each request as it was, judged one by one from here: a wait holds
|
|
@@ -19662,10 +19959,10 @@ export class AgentFramework {
|
|
|
19662
19959
|
else delete start[TURN_CARRY];
|
|
19663
19960
|
if (requests.every(isRearmed)) start[REARMED] = true;
|
|
19664
19961
|
else delete start[REARMED];
|
|
19665
|
-
// A start
|
|
19666
|
-
// the batch it was merged from (MERGED_BATCH): until
|
|
19667
|
-
// token it stands for them all.
|
|
19668
|
-
if (
|
|
19962
|
+
// A start merged from more than one request, any of them not the
|
|
19963
|
+
// turn's, records the batch it was merged from (MERGED_BATCH): until
|
|
19964
|
+
// it takes its token it stands for them all.
|
|
19965
|
+
if (requests.length > 1 && requests.some((r) => !continuesTurn(r))) start[MERGED_BATCH] = [...requests];
|
|
19669
19966
|
else delete start[MERGED_BATCH];
|
|
19670
19967
|
await this.startAgentStream(agent, start);
|
|
19671
19968
|
}
|
|
@@ -20345,8 +20642,6 @@ export class AgentFramework {
|
|
|
20345
20642
|
const refuse = (error: string): ToolResult => ({ success: false, error, isError: true });
|
|
20346
20643
|
// Plain text, not a JSON-quoted string: draft text must read exactly as held.
|
|
20347
20644
|
const ok = (text: string): ToolResult => ({ success: true, data: [{ type: 'text', text }] });
|
|
20348
|
-
// null and omission mean the same thing: the field is not in use.
|
|
20349
|
-
const present = (v: unknown): boolean => v !== undefined && v !== null;
|
|
20350
20645
|
const action = input.action;
|
|
20351
20646
|
if (action !== 'list' && action !== 'read' && action !== 'resend' && action !== 'dismiss') {
|
|
20352
20647
|
return refuse('action must be one of "list", "read", "resend" or "dismiss".');
|
|
@@ -20355,7 +20650,7 @@ export class AgentFramework {
|
|
|
20355
20650
|
refuse(`${field} is not used by action "${action}" — pass null or omit it.`);
|
|
20356
20651
|
|
|
20357
20652
|
let ids: string[] = [];
|
|
20358
|
-
if (
|
|
20653
|
+
if (fieldInUse(input.draftIds)) {
|
|
20359
20654
|
if (action === 'list') return unused('draftIds');
|
|
20360
20655
|
if (!Array.isArray(input.draftIds) || input.draftIds.some((id) => typeof id !== 'string' || !id.trim())) {
|
|
20361
20656
|
return refuse('draftIds must be a list of draft ids such as "d-k7x3qm2", or null.');
|
|
@@ -20364,15 +20659,19 @@ export class AgentFramework {
|
|
|
20364
20659
|
if (new Set(ids).size !== ids.length) return refuse('draftIds names a draft more than once.');
|
|
20365
20660
|
if (ids.length > 20) return refuse('draftIds names more than 20 drafts; act on them in smaller groups.');
|
|
20366
20661
|
}
|
|
20367
|
-
if (
|
|
20368
|
-
|
|
20369
|
-
|
|
20662
|
+
if (fieldInUse(input.destination) && action !== 'resend') return unused('destination');
|
|
20663
|
+
// false is "don't confirm", which every other action already does, so only
|
|
20664
|
+
// true is a confirmDuplicate those actions refuse.
|
|
20665
|
+
if (fieldInUse(input.confirmDuplicate) && input.confirmDuplicate !== false && action !== 'resend') {
|
|
20666
|
+
return unused('confirmDuplicate');
|
|
20667
|
+
}
|
|
20668
|
+
if (fieldInUse(input.offset) && action !== 'list') return unused('offset');
|
|
20370
20669
|
|
|
20371
20670
|
if (action === 'list') {
|
|
20372
20671
|
let offset = 0;
|
|
20373
|
-
if (
|
|
20672
|
+
if (fieldInUse(input.offset)) {
|
|
20374
20673
|
const raw = input.offset;
|
|
20375
|
-
const n =
|
|
20674
|
+
const n = integerField(raw);
|
|
20376
20675
|
if (!Number.isInteger(n) || n < 0) return refuse('offset must be a whole number of drafts to skip (0 or more), or null.');
|
|
20377
20676
|
offset = n;
|
|
20378
20677
|
}
|
|
@@ -20489,12 +20788,12 @@ export class AgentFramework {
|
|
|
20489
20788
|
if (!registry) return refuse('No channels are configured, so there is nowhere to resend to.');
|
|
20490
20789
|
const home = this.homeOf(agentName);
|
|
20491
20790
|
let spec = typeof input.destination === 'string' ? input.destination.trim() : '';
|
|
20492
|
-
if (
|
|
20791
|
+
if (fieldInUse(input.destination) && typeof input.destination !== 'string') {
|
|
20493
20792
|
return refuse('destination must be "#channel", "@person" or a channel id.');
|
|
20494
20793
|
}
|
|
20495
20794
|
if (!spec && !home) return refuse('resend needs a destination: "#channel", "@person" or a channel id.');
|
|
20496
20795
|
let confirmDuplicate = false;
|
|
20497
|
-
if (
|
|
20796
|
+
if (fieldInUse(input.confirmDuplicate)) {
|
|
20498
20797
|
if (typeof input.confirmDuplicate !== 'boolean') return refuse('confirmDuplicate must be true, false or null.');
|
|
20499
20798
|
confirmDuplicate = input.confirmDuplicate;
|
|
20500
20799
|
}
|
|
@@ -22923,6 +23222,7 @@ export class AgentFramework {
|
|
|
22923
23222
|
attempt = 0,
|
|
22924
23223
|
providerGateAlreadyHeld = false,
|
|
22925
23224
|
): Promise<void> {
|
|
23225
|
+
if (attempt === 0) this.deadlineCuts.delete(agent.name);
|
|
22926
23226
|
const ownsProviderGate = this.registry.ownsProviderScheduling(agent.name);
|
|
22927
23227
|
// A stopping framework starts no turn, whoever owns provider admission
|
|
22928
23228
|
// (conversation agents and ephemeral runs do not take the gate).
|
|
@@ -22989,6 +23289,15 @@ export class AgentFramework {
|
|
|
22989
23289
|
console.error(`[provider-admission] ${agent.name}: host quiesced while parked — the merged start goes back as its ${batch.length} requests`);
|
|
22990
23290
|
return;
|
|
22991
23291
|
}
|
|
23292
|
+
if (this.providerAccelerationCooldowns.has(agent.name)) {
|
|
23293
|
+
this.releasePrimaryProviderGate(agent.name);
|
|
23294
|
+
this.eventGate?.onInferenceEnded(agent.name);
|
|
23295
|
+
this.pendingRequests.push(trigger ?? {
|
|
23296
|
+
agentName: agent.name, reason: 'provider-admission:requeue', source: 'scheduler', timestamp: Date.now(),
|
|
23297
|
+
});
|
|
23298
|
+
console.error(`[provider-admission] ${agent.name}: a provider hold stands — parked wake requeued for the scheduler, not started`);
|
|
23299
|
+
return;
|
|
23300
|
+
}
|
|
22992
23301
|
await this.startAgentStream(agent, trigger, attempt, true);
|
|
22993
23302
|
}).catch((error) => {
|
|
22994
23303
|
this.releasePrimaryProviderGate(agent.name);
|
|
@@ -23147,7 +23456,7 @@ export class AgentFramework {
|
|
|
23147
23456
|
if (hold) {
|
|
23148
23457
|
const held = hold.heldRequests.length;
|
|
23149
23458
|
hold.heldRequests = take(hold.heldRequests);
|
|
23150
|
-
if (held > 0 && hold.heldRequests.length === 0) hold.
|
|
23459
|
+
if (held > 0 && hold.heldRequests.length === 0) hold.emptiedBy = 'drop';
|
|
23151
23460
|
}
|
|
23152
23461
|
for (const parked of [...this.parkedContinuations]) {
|
|
23153
23462
|
if (!isContinuation(parked)) continue;
|
|
@@ -23260,9 +23569,8 @@ export class AgentFramework {
|
|
|
23260
23569
|
* gets inference:aborted with `reason`, and lastInference records its end
|
|
23261
23570
|
* as driveStream's would, when the record is still this turn's. Nothing of
|
|
23262
23571
|
* the kind when another turn of this agent's name holds the turn token (one
|
|
23263
|
-
* begun
|
|
23264
|
-
*
|
|
23265
|
-
* newer turn instead.
|
|
23572
|
+
* begun by a replacement agent object): the event and the record carry only
|
|
23573
|
+
* the name, so this frame's would end that newer turn instead.
|
|
23266
23574
|
*/
|
|
23267
23575
|
private abortDroppedTurn(agentName: string, turnToken: number, reason: string, startedAt: number): void {
|
|
23268
23576
|
const owner = this.activeTurnTokens.get(agentName);
|
|
@@ -23451,7 +23759,6 @@ export class AgentFramework {
|
|
|
23451
23759
|
* operator's abandon. */
|
|
23452
23760
|
checkWindow: CheckWindow,
|
|
23453
23761
|
): Promise<boolean> {
|
|
23454
|
-
const lifecycle = this.lifecycleGeneration;
|
|
23455
23762
|
const resumesTurn = resumesLogicalTurn(trigger?.reason);
|
|
23456
23763
|
let carry = turnCarryOf(trigger);
|
|
23457
23764
|
const turnProseRouting = trigger?.suppressProse ? 'disabled' : agent.proseRouting;
|
|
@@ -23639,7 +23946,7 @@ export class AgentFramework {
|
|
|
23639
23946
|
// everything the turn has decided about where its speech goes.
|
|
23640
23947
|
// - A resume (a budget restart, a tool-result-guard retry) carries its
|
|
23641
23948
|
// turn's request on and keeps the turn's object as it is, and the
|
|
23642
|
-
// turn stays, or
|
|
23949
|
+
// turn stays, or becomes again, the agent's open turn.
|
|
23643
23950
|
// - A silent start routes no prose. It gets an object of its own, with
|
|
23644
23951
|
// no locus, so it neither takes nor disturbs an open turn's. Its
|
|
23645
23952
|
// object is the agent's live routing while it runs, and the open
|
|
@@ -23664,8 +23971,6 @@ export class AgentFramework {
|
|
|
23664
23971
|
let opensTurn = false;
|
|
23665
23972
|
if (resumesTurn) {
|
|
23666
23973
|
routing = this.routingOf(agent.name);
|
|
23667
|
-
// The turn a resume continues is open, unless a stop() ended every
|
|
23668
|
-
// open turn since: it opens again, so its later starts continue it.
|
|
23669
23974
|
if (!trigger?.suppressProse && !this.openLogical.has(agent.name)) this.openLogical.set(agent.name, routing);
|
|
23670
23975
|
} else if (trigger?.suppressProse) {
|
|
23671
23976
|
const continued = continuesTurn(trigger) ? this.turnRouting.get(agent.name) : undefined;
|
|
@@ -23687,6 +23992,8 @@ export class AgentFramework {
|
|
|
23687
23992
|
if (opensTurn) {
|
|
23688
23993
|
const head = agent.branchHead();
|
|
23689
23994
|
if (head) routing.start = head;
|
|
23995
|
+
if (!routing.silent) this.pinTurnInputs(agent, routing);
|
|
23996
|
+
else agent.unpinTurnInputs();
|
|
23690
23997
|
}
|
|
23691
23998
|
this.turnRouting.set(agent.name, routing);
|
|
23692
23999
|
if (opensTurn) carry = undefined;
|
|
@@ -23821,11 +24128,11 @@ export class AgentFramework {
|
|
|
23821
24128
|
});
|
|
23822
24129
|
// A budget restart continues the same logical inference window. The
|
|
23823
24130
|
// predecessor keeps EventGate liveness until its successor terminates,
|
|
23824
|
-
// and the successor takes it again if it ended in between: at a
|
|
23825
|
-
// (a restart left queued, which a start() then takes up), or at a
|
|
24131
|
+
// and the successor takes it again if it ended in between: at a
|
|
23826
24132
|
// provider hold that kept the successor from starting (a stated wait at
|
|
23827
|
-
// its last admission check,
|
|
23828
|
-
//
|
|
24133
|
+
// its last admission check, a hold standing when its park behind an
|
|
24134
|
+
// auxiliary call ended, or a cooldown after its own call failed), whose
|
|
24135
|
+
// end queues it again.
|
|
23829
24136
|
if (!resumesTurn) {
|
|
23830
24137
|
this.eventGate?.onInferenceStarted(agent.name);
|
|
23831
24138
|
} else {
|
|
@@ -23913,13 +24220,11 @@ export class AgentFramework {
|
|
|
23913
24220
|
// An ephemeral watchdog may dispose this Agent while hooks/compile await,
|
|
23914
24221
|
// and stop() may close provider admission. Do not create a provider
|
|
23915
24222
|
// stream after its generation lost ownership, or once admission closed,
|
|
23916
|
-
//
|
|
23917
|
-
//
|
|
23918
|
-
|
|
23919
|
-
if (this.agents.get(agent.name) !== agent || this.providerAdmissionClosed || lifecycle !== this.lifecycleGeneration) {
|
|
24223
|
+
// as after the compile: these come before a stop kept for the turn,
|
|
24224
|
+
// which goes with it.
|
|
24225
|
+
if (this.agents.get(agent.name) !== agent || this.providerAdmissionClosed) {
|
|
23920
24226
|
const why = this.agents.get(agent.name) !== agent ? 'the agent was replaced or removed'
|
|
23921
|
-
:
|
|
23922
|
-
: 'the framework was stopped and started again';
|
|
24227
|
+
: 'the framework was stopping';
|
|
23923
24228
|
const reason = `a turn assembled while ${why} is dropped before it sends anything`;
|
|
23924
24229
|
console.error(`[turn] ${agent.name}: ${reason}`);
|
|
23925
24230
|
this.endDroppedTurn(agent.name, routing);
|
|
@@ -23973,23 +24278,22 @@ export class AgentFramework {
|
|
|
23973
24278
|
// This turn's retries are the framework's (errorPolicy below), not
|
|
23974
24279
|
// membrane's inside each attempt.
|
|
23975
24280
|
membraneRetries: false,
|
|
24281
|
+
inferenceBounds: this.inferenceBounds,
|
|
23976
24282
|
});
|
|
23977
24283
|
// Each later round this stream sends is noted as it goes (noteRoundSent).
|
|
23978
24284
|
if (noteRequestSent) this.streamRequestNotes.set(stream, { note: noteRequestSent, rounds: [], stopped: false });
|
|
23979
24285
|
if (carriedBodies) this.streamCarried.set(stream, new Set(carriedBodies));
|
|
23980
24286
|
// The compile can wait for annotations (each batch has one spill
|
|
23981
|
-
// deadline), so check ownership
|
|
23982
|
-
// A stop followed by start must not revive this earlier turn.
|
|
24287
|
+
// deadline), so check ownership and admission again.
|
|
23983
24288
|
// Membrane's yielding stream starts its request when it is first
|
|
23984
24289
|
// iterated, so cancelling it here, before driveStream, sends nothing.
|
|
23985
24290
|
// A turn that was mid-compile when stop() cancelled every stream would
|
|
23986
24291
|
// otherwise register after that pass and outlive stop(). The caller's
|
|
23987
24292
|
// finally releases the turn token.
|
|
23988
24293
|
const stopping = this.providerAdmissionClosed;
|
|
23989
|
-
if (this.agents.get(agent.name) !== agent || stopping
|
|
24294
|
+
if (this.agents.get(agent.name) !== agent || stopping) {
|
|
23990
24295
|
const why = this.agents.get(agent.name) !== agent ? 'the agent was replaced or removed'
|
|
23991
|
-
:
|
|
23992
|
-
: 'the framework was stopped and started again';
|
|
24296
|
+
: 'the framework was stopping';
|
|
23993
24297
|
const reason = `a turn compiled while ${why} is dropped before it sends anything`;
|
|
23994
24298
|
console.error(`[turn] ${agent.name}: ${reason}`);
|
|
23995
24299
|
this.cancelUnsentStream(agent, stream, `compiled while ${why}`);
|
|
@@ -24120,14 +24424,12 @@ export class AgentFramework {
|
|
|
24120
24424
|
// catch never holds a compiled request.) As at the checks after the
|
|
24121
24425
|
// hooks and after the compile, a replaced agent or a stopping framework
|
|
24122
24426
|
// comes first: the turn is dropped as theirs, and the stop goes with
|
|
24123
|
-
// it
|
|
24124
|
-
// compile. A failed start with no stop goes on below as before.
|
|
24427
|
+
// it. A failed start with no stop goes on below as before.
|
|
24125
24428
|
const stopAtFailure = this.takeStartStop(agent, checkWindow);
|
|
24126
24429
|
if (stopAtFailure) {
|
|
24127
|
-
if (this.agents.get(agent.name) !== agent || this.providerAdmissionClosed
|
|
24430
|
+
if (this.agents.get(agent.name) !== agent || this.providerAdmissionClosed) {
|
|
24128
24431
|
const why = this.agents.get(agent.name) !== agent ? 'the agent was replaced or removed'
|
|
24129
|
-
:
|
|
24130
|
-
: 'the framework was stopped and started again';
|
|
24432
|
+
: 'the framework was stopping';
|
|
24131
24433
|
const reason = `a turn whose request couldn't be built while ${why} is dropped before it sends anything`;
|
|
24132
24434
|
console.error(`[turn] ${agent.name}: ${reason}`);
|
|
24133
24435
|
this.endDroppedTurn(agent.name, routing);
|
|
@@ -24140,6 +24442,7 @@ export class AgentFramework {
|
|
|
24140
24442
|
}
|
|
24141
24443
|
|
|
24142
24444
|
const retry = trigger && withTurnCarry(trigger, carry);
|
|
24445
|
+
const asTransient = this.takeAsTransient(agent.name, err, trigger);
|
|
24143
24446
|
if (ownsProviderGate && this.holdProviderAcceleration(agent, err, retry)) {
|
|
24144
24447
|
// Capacity scheduling is not poisoned history. The cooldown timer owns
|
|
24145
24448
|
// one later fresh compile; no immediate retry or exhaustion marker.
|
|
@@ -24156,24 +24459,25 @@ export class AgentFramework {
|
|
|
24156
24459
|
return false;
|
|
24157
24460
|
}
|
|
24158
24461
|
|
|
24159
|
-
const
|
|
24160
|
-
|
|
24462
|
+
const cuts = isDeadlineExpiry(err) ? this.deadlineCuts.get(agent.name) ?? 0 : undefined;
|
|
24463
|
+
const action: ErrorAction = statedWait === 'fail' || (isRearmed(trigger) && (!gotPartway(routing) || (isDeadlineExpiry(err) && !asTransient)))
|
|
24464
|
+
? { retry: false } : this.errorPolicy.onInferenceError(err, agent.name, cuts ?? attempt);
|
|
24465
|
+
if (action.retry && cuts !== undefined) this.deadlineCuts.set(agent.name, cuts + 1);
|
|
24466
|
+
else this.deadlineCuts.delete(agent.name);
|
|
24467
|
+
if (!action.retry && cuts !== undefined && cuts > 0) this.freshCallsCut.add(agent.name);
|
|
24161
24468
|
if (action.retry) {
|
|
24162
24469
|
// A policy's delay can be a provider's retry-after: wait all of it.
|
|
24163
24470
|
await this.waitForRetry(action.delayMs, checkWindow, agent);
|
|
24164
|
-
if (this.providerAdmissionClosed
|
|
24471
|
+
if (this.providerAdmissionClosed) {
|
|
24165
24472
|
// stop() ended the wait: that cancels the retry, it does not hasten
|
|
24166
|
-
// it
|
|
24167
|
-
//
|
|
24168
|
-
//
|
|
24169
|
-
//
|
|
24170
|
-
// anything does: a terminal event saying why, after the
|
|
24171
|
-
// inference:failed above, and its record's end. Nothing is written
|
|
24473
|
+
// it, and a stop kept for the turn goes with it. The turn ends as
|
|
24474
|
+
// one dropped before it sends anything does: a terminal event
|
|
24475
|
+
// saying why, after the inference:failed above, and its record's
|
|
24476
|
+
// end. Nothing is written
|
|
24172
24477
|
// but the [speech-not-posted] notice; this frame still owns its
|
|
24173
24478
|
// token, since no retry frame replaced it.
|
|
24174
24479
|
this.nameUnpostedSpeech(agent, routing);
|
|
24175
|
-
this.abortDroppedTurn(agent.name, turnToken,
|
|
24176
|
-
this.providerAdmissionClosed ? RETRY_CANCELLED_BY_STOP : RETRY_ACROSS_RESTART, turnStartedAt);
|
|
24480
|
+
this.abortDroppedTurn(agent.name, turnToken, RETRY_CANCELLED_BY_STOP, turnStartedAt);
|
|
24177
24481
|
if (this.activeTurnTokens.get(agent.name) === turnToken) {
|
|
24178
24482
|
this.activeTurnTokens.delete(agent.name);
|
|
24179
24483
|
this.activeTurnTriggers.delete(agent.name);
|
|
@@ -24217,7 +24521,7 @@ export class AgentFramework {
|
|
|
24217
24521
|
speech: '',
|
|
24218
24522
|
...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: err.message }),
|
|
24219
24523
|
}, routing);
|
|
24220
|
-
if (isRearmed(trigger) &&
|
|
24524
|
+
if (isRearmed(trigger) && asTransient) this.exhaustingRearms.add(agent.name);
|
|
24221
24525
|
this.emitTrace({
|
|
24222
24526
|
type: 'inference:exhausted',
|
|
24223
24527
|
agentName: agent.name,
|
|
@@ -24237,7 +24541,7 @@ export class AgentFramework {
|
|
|
24237
24541
|
if (action.emit) {
|
|
24238
24542
|
this.pushEvent(action.emit);
|
|
24239
24543
|
}
|
|
24240
|
-
this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, routing);
|
|
24544
|
+
this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, routing, asTransient);
|
|
24241
24545
|
}
|
|
24242
24546
|
// No handoff on either failure branch: the terminal branch deleted the
|
|
24243
24547
|
// token above; the retry branch's recursive startAgentStream replaced
|
|
@@ -24869,7 +25173,7 @@ export class AgentFramework {
|
|
|
24869
25173
|
(b) => b.type !== 'tool_result'
|
|
24870
25174
|
);
|
|
24871
25175
|
} else {
|
|
24872
|
-
//
|
|
25176
|
+
// XML tool mode: this round's text and its calls.
|
|
24873
25177
|
// XML mode: persist the verbatim generation on each tool_use
|
|
24874
25178
|
// (membrane#36 round-trip fidelity). context.rawText is the
|
|
24875
25179
|
// parser's fullMatch — the exact <function_calls> block the
|
|
@@ -24881,15 +25185,12 @@ export class AgentFramework {
|
|
|
24881
25185
|
const rawXml = rawText && /^<(antml:)?function_calls>/.test(rawText)
|
|
24882
25186
|
? rawText
|
|
24883
25187
|
: undefined;
|
|
24884
|
-
// Persist THIS round's text only
|
|
24885
|
-
//
|
|
24886
|
-
//
|
|
24887
|
-
//
|
|
24888
|
-
//
|
|
24889
|
-
|
|
24890
|
-
const roundText =
|
|
24891
|
-
(event.context as { roundPreamble?: string }).roundPreamble ??
|
|
24892
|
-
event.context.preamble;
|
|
25188
|
+
// Persist THIS round's text only: `roundPreamble` is the
|
|
25189
|
+
// per-round delta. The `preamble` beside it is CUMULATIVE in XML
|
|
25190
|
+
// mode: persisting it per round would store each round's text N
|
|
25191
|
+
// times and re-persist injected <function_results> as the
|
|
25192
|
+
// agent's own words.
|
|
25193
|
+
const roundText = (event.context as { roundPreamble?: string }).roundPreamble;
|
|
24893
25194
|
assistantBlocks = [];
|
|
24894
25195
|
if (roundText) {
|
|
24895
25196
|
assistantBlocks.push({ type: 'text', text: roundText });
|
|
@@ -24914,9 +25215,7 @@ export class AgentFramework {
|
|
|
24914
25215
|
|
|
24915
25216
|
// Whether this stream can carry messages injected with the round's
|
|
24916
25217
|
// results: true on membrane's native path and false on its XML
|
|
24917
|
-
// prefill path
|
|
24918
|
-
// doesn't declare it. The tool boundary reads it from the agent's
|
|
24919
|
-
// state.
|
|
25218
|
+
// prefill path. The tool boundary reads it from the agent's state.
|
|
24920
25219
|
agent.enterWaitingForTools(
|
|
24921
25220
|
calls,
|
|
24922
25221
|
stream,
|
|
@@ -24958,13 +25257,11 @@ export class AgentFramework {
|
|
|
24958
25257
|
if (liveRound) liveProseRouting = true;
|
|
24959
25258
|
// Whether this round's tool-result batch (and anything queued
|
|
24960
25259
|
// with it) will be presented to the model before it speaks
|
|
24961
|
-
// again: the
|
|
24962
|
-
//
|
|
24963
|
-
// presenting it. That holds for 0.5.x's native rounds, while its
|
|
24964
|
-
// XML path drops what's injected, so a 'now' notice there reaches
|
|
25260
|
+
// again: the stream declares it (true on membrane's native path).
|
|
25261
|
+
// On the XML path, which declares false, a 'now' notice reaches
|
|
24965
25262
|
// the agent at its next compile instead.
|
|
24966
25263
|
const presentsInjections =
|
|
24967
|
-
(event.context as { supportsInjectedMessages?: boolean }).supportsInjectedMessages
|
|
25264
|
+
(event.context as { supportsInjectedMessages?: boolean }).supportsInjectedMessages === true;
|
|
24968
25265
|
// Queued under this turn's routing, whenever it posts, and
|
|
24969
25266
|
// streamed by this stream once published.
|
|
24970
25267
|
// This round's request was answered: what it carried is heard.
|
|
@@ -25742,14 +26039,14 @@ export class AgentFramework {
|
|
|
25742
26039
|
// active (native roundContent), each mid-turn round's prose was
|
|
25743
26040
|
// already delivered when that round yielded — only the TRAILING
|
|
25744
26041
|
// prose (blocks after the last tool round) remains here. On the
|
|
25745
|
-
//
|
|
25746
|
-
//
|
|
26042
|
+
// XML tool path (no roundContent, so nothing was live-routed)
|
|
26043
|
+
// the historical behavior applies:
|
|
25747
26044
|
// route the whole turn's parsed segments, once, now. The global
|
|
25748
26045
|
// speech/thoughts split is left untouched (module/TUI rendering
|
|
25749
26046
|
// unaffected) — this only governs what reaches the channel.
|
|
25750
26047
|
//
|
|
25751
26048
|
// Native round routing uses the live sticky flag, which a fresh
|
|
25752
|
-
// injected message can reset. The
|
|
26049
|
+
// injected message can reset. The XML tool path has no
|
|
25753
26050
|
// reliable round boundaries, so retain its historical turn-wide
|
|
25754
26051
|
// scan to avoid double-posting.
|
|
25755
26052
|
const toolCalls = response.content
|
|
@@ -25985,6 +26282,7 @@ export class AgentFramework {
|
|
|
25985
26282
|
|
|
25986
26283
|
this.abandonAgentScripts(agent.name, 'stream error', streamRouting);
|
|
25987
26284
|
const retry = retryOf(trigger);
|
|
26285
|
+
const asTransient = this.takeAsTransient(agent.name, err, trigger);
|
|
25988
26286
|
|
|
25989
26287
|
// A hold takes the turn's retry, or, once stop() has closed
|
|
25990
26288
|
// admission, the turn ends here (endFailureAtStop).
|
|
@@ -26000,8 +26298,12 @@ export class AgentFramework {
|
|
|
26000
26298
|
break;
|
|
26001
26299
|
}
|
|
26002
26300
|
|
|
26003
|
-
const
|
|
26004
|
-
|
|
26301
|
+
const cuts = isDeadlineExpiry(err) ? this.deadlineCuts.get(agent.name) ?? 0 : undefined;
|
|
26302
|
+
const action: ErrorAction = statedWait === 'fail' || (isRearmed(trigger) && (!gotPartway(streamRouting) || (isDeadlineExpiry(err) && !asTransient)))
|
|
26303
|
+
? { retry: false } : this.errorPolicy.onInferenceError(err, agent.name, cuts ?? attempt);
|
|
26304
|
+
if (action.retry && cuts !== undefined) this.deadlineCuts.set(agent.name, cuts + 1);
|
|
26305
|
+
else this.deadlineCuts.delete(agent.name);
|
|
26306
|
+
if (!action.retry && cuts !== undefined && cuts > 0) this.freshCallsCut.add(agent.name);
|
|
26005
26307
|
if (action.retry) {
|
|
26006
26308
|
// A stop made during the wait reaches no stream: the check after
|
|
26007
26309
|
// the wait ends the turn, as the guard's check after its wait
|
|
@@ -26058,7 +26360,7 @@ export class AgentFramework {
|
|
|
26058
26360
|
speech: '',
|
|
26059
26361
|
...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: err.message }),
|
|
26060
26362
|
}, this.agents.get(agent.name) === agent && agent.streamId === myStreamId ? streamRouting : undefined, streamRouting);
|
|
26061
|
-
if (isRearmed(trigger) &&
|
|
26363
|
+
if (isRearmed(trigger) && asTransient) this.exhaustingRearms.add(agent.name);
|
|
26062
26364
|
this.emitTrace({
|
|
26063
26365
|
type: 'inference:exhausted',
|
|
26064
26366
|
agentName: agent.name,
|
|
@@ -26076,7 +26378,7 @@ export class AgentFramework {
|
|
|
26076
26378
|
if (action.emit) {
|
|
26077
26379
|
this.pushEvent(action.emit);
|
|
26078
26380
|
}
|
|
26079
|
-
this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, streamRouting);
|
|
26381
|
+
this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, streamRouting, asTransient);
|
|
26080
26382
|
}
|
|
26081
26383
|
break;
|
|
26082
26384
|
}
|
|
@@ -26151,7 +26453,7 @@ export class AgentFramework {
|
|
|
26151
26453
|
agentName: agent.name,
|
|
26152
26454
|
error: terminal,
|
|
26153
26455
|
});
|
|
26154
|
-
this.rearmExhaustedTurn(agent, new Error(terminal), trigger, ownsProviderGate, streamRouting);
|
|
26456
|
+
this.rearmExhaustedTurn(agent, new Error(terminal), trigger, ownsProviderGate, streamRouting, false);
|
|
26155
26457
|
// Postmortem 2026-05-28 P2 #7: persist the abort to the
|
|
26156
26458
|
// inference log so future investigations can attribute the
|
|
26157
26459
|
// terminal cause without relying on live in-memory reducer
|
|
@@ -26169,6 +26471,8 @@ export class AgentFramework {
|
|
|
26169
26471
|
}
|
|
26170
26472
|
|
|
26171
26473
|
case 'usage': {
|
|
26474
|
+
this.rearmDeadlineCuts.delete(agent.name);
|
|
26475
|
+
this.deadlineCuts.delete(agent.name);
|
|
26172
26476
|
// Cumulative across the stream's rounds: replace, never sum.
|
|
26173
26477
|
latestUsage = event.usage;
|
|
26174
26478
|
// The stream's accumulated fresh input, which is what the
|
|
@@ -26411,6 +26715,7 @@ export class AgentFramework {
|
|
|
26411
26715
|
agent.reset();
|
|
26412
26716
|
this.emitTrace({ type: 'inference:failed', agentName: agent.name, error: err.message, stack: err.stack });
|
|
26413
26717
|
const retry = retryOf(trigger);
|
|
26718
|
+
const asTransient = this.takeAsTransient(agent.name, err, trigger);
|
|
26414
26719
|
if (ownsProviderGate && this.holdProviderAcceleration(agent, err, retry)) {
|
|
26415
26720
|
// Once stop() has closed admission, nothing is held, and the turn
|
|
26416
26721
|
// ends here (endFailureAtStop).
|
|
@@ -26442,14 +26747,14 @@ export class AgentFramework {
|
|
|
26442
26747
|
speech: '',
|
|
26443
26748
|
...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: err.message }),
|
|
26444
26749
|
}, streamRouting);
|
|
26445
|
-
if (isRearmed(trigger) &&
|
|
26750
|
+
if (isRearmed(trigger) && asTransient) this.exhaustingRearms.add(agent.name);
|
|
26446
26751
|
this.emitTrace({
|
|
26447
26752
|
type: 'inference:exhausted',
|
|
26448
26753
|
agentName: agent.name,
|
|
26449
26754
|
error: err.message,
|
|
26450
26755
|
...this.classifyInferenceError(err),
|
|
26451
26756
|
});
|
|
26452
|
-
this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, streamRouting);
|
|
26757
|
+
this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, streamRouting, asTransient);
|
|
26453
26758
|
// Postmortem 2026-05-28 P2 #7: catch-path failures (stream itself
|
|
26454
26759
|
// threw) were previously only visible via in-memory trace listeners.
|
|
26455
26760
|
// Persist to inference log for forensic attribution.
|
|
@@ -26660,7 +26965,11 @@ export class AgentFramework {
|
|
|
26660
26965
|
|
|
26661
26966
|
/**
|
|
26662
26967
|
* Execute a tool call and return the result.
|
|
26663
|
-
* Routes
|
|
26968
|
+
* Routes it where the agent's own call of the name goes: to the
|
|
26969
|
+
* framework's own tools the agent has (agent_settings, the workspace and
|
|
26970
|
+
* gate tools, think, journal, prose_help and the rest), the channel tools,
|
|
26971
|
+
* MCPL or the module registry. skip_reply, which ends the turn it's called
|
|
26972
|
+
* in, is refused, since this call isn't made in one.
|
|
26664
26973
|
* Its one caller in the framework is the operator's puppet
|
|
26665
26974
|
* (puppetToolCall), which puts the agent's inline cap on the call as
|
|
26666
26975
|
* resultMaxChars; a host may call it as well.
|
|
@@ -26707,8 +27016,11 @@ export class AgentFramework {
|
|
|
26707
27016
|
* wire ordering. For its own duration the puppet HOLDS the turn-alive
|
|
26708
27017
|
* marker (wakes requeue, cross-turn writers defer), so a wake landing
|
|
26709
27018
|
* during the tool's execution cannot start a turn underneath the pair.
|
|
26710
|
-
* - Refused for tools outside the agent's own surface (
|
|
26711
|
-
*
|
|
27019
|
+
* - Refused for tools outside the agent's own surface (the tools it may
|
|
27020
|
+
* call, availableToolsForPresentation), and for skip_reply, by name or
|
|
27021
|
+
* through a toolsets--act that reaches it, which ends the turn it's
|
|
27022
|
+
* called in: the stored pair must be an act the agent could genuinely
|
|
27023
|
+
* have taken.
|
|
26712
27024
|
* - The call executes FOR REAL through the shared dispatch (MCPL,
|
|
26713
27025
|
* channel tools, utils, modules) with the agent's provenance — a
|
|
26714
27026
|
* puppeted walk_to actually moves the avatar.
|
|
@@ -26760,14 +27072,21 @@ export class AgentFramework {
|
|
|
26760
27072
|
`injecting a turn under an active stream corrupts wire ordering)`,
|
|
26761
27073
|
);
|
|
26762
27074
|
}
|
|
26763
|
-
const onSurface = this.
|
|
26764
|
-
.some((t) => t.name === toolName && agent.canUseTool(t.name));
|
|
27075
|
+
const onSurface = this.availableToolsForPresentation(agentName).some((t) => t.name === toolName);
|
|
26765
27076
|
if (!onSurface) {
|
|
26766
27077
|
throw new Error(
|
|
26767
27078
|
`puppet refused: tool ${toolName} is not on ${agentName}'s surface — ` +
|
|
26768
27079
|
`the stored pair must be an act the agent could genuinely have taken`,
|
|
26769
27080
|
);
|
|
26770
27081
|
}
|
|
27082
|
+
const route = this.toolRoute({ id: '', name: toolName, input }, agentName);
|
|
27083
|
+
const acting = route.kind === 'host-act' && route.host.kind === 'call' ? route.host.name : toolName;
|
|
27084
|
+
if (acting === 'skip_reply') {
|
|
27085
|
+
throw new Error(
|
|
27086
|
+
"puppet refused: skip_reply ends the turn it's called in, and a puppeted call isn't made in one, " +
|
|
27087
|
+
'so nothing was run or stored',
|
|
27088
|
+
);
|
|
27089
|
+
}
|
|
26771
27090
|
|
|
26772
27091
|
// Anthropic-shaped id so the stored pair is indistinguishable from a
|
|
26773
27092
|
// provider-issued call.
|
|
@@ -26906,96 +27225,123 @@ export class AgentFramework {
|
|
|
26906
27225
|
return { success: false, error: marked, isError: true };
|
|
26907
27226
|
}
|
|
26908
27227
|
|
|
26909
|
-
|
|
26910
|
-
|
|
26911
|
-
|
|
26912
|
-
|
|
26913
|
-
|
|
26914
|
-
|
|
26915
|
-
|
|
26916
|
-
|
|
26917
|
-
|
|
26918
|
-
|
|
26919
|
-
|
|
26920
|
-
|
|
26921
|
-
|
|
26922
|
-
|
|
26923
|
-
|
|
26924
|
-
|
|
26925
|
-
|
|
26926
|
-
|
|
26927
|
-
|
|
26928
|
-
|
|
26929
|
-
|
|
26930
|
-
|
|
26931
|
-
|
|
26932
|
-
|
|
26933
|
-
|
|
26934
|
-
|
|
26935
|
-
|
|
27228
|
+
const subject = origin.kind === 'agent' ? origin.agentName : call.callerAgentName ?? '__ephemeral__';
|
|
27229
|
+
const route = this.toolRoute(call, call.callerAgentName);
|
|
27230
|
+
switch (route.kind) {
|
|
27231
|
+
case 'presentation':
|
|
27232
|
+
return this.editToolPresentation(call.callerAgentName ?? '__ephemeral__', call);
|
|
27233
|
+
|
|
27234
|
+
// toolsets--act on a host toolset's tool (the built-in one, a server's
|
|
27235
|
+
// tab) goes where a programmatic call of that tool goes, under its name.
|
|
27236
|
+
case 'host-act':
|
|
27237
|
+
if (route.host.kind === 'refused') return failed({ success: false, error: route.host.error, isError: true });
|
|
27238
|
+
return this.executeToolCallFrom({ ...call, name: route.host.name, input: route.host.args }, origin);
|
|
27239
|
+
|
|
27240
|
+
// RFC-014: toolsets--act, resolved to the dynamic tool it calls, goes
|
|
27241
|
+
// where a programmatic call of that tool goes, with its instance.
|
|
27242
|
+
case 'toolset-act': {
|
|
27243
|
+
const target = this.resolveToolsetAct(call);
|
|
27244
|
+
if (!target.ok) return failed({ success: false, error: target.error, isError: true }, this.everyServerRedactor()(target.error));
|
|
27245
|
+
if (origin.kind === 'agent') {
|
|
27246
|
+
const failure = this.agentToolPermissionFailure(origin.agentName, target.call, false);
|
|
27247
|
+
if (failure) return failed(failure, this.everyServerRedactor()(failure.error ?? ''));
|
|
27248
|
+
}
|
|
27249
|
+
return this.executeMcplToolCall(target.call, target.serverId, target.prefix);
|
|
27250
|
+
}
|
|
27251
|
+
|
|
27252
|
+
case 'mcpl':
|
|
27253
|
+
return this.executeMcplToolCall(call, route.serverId, route.prefix);
|
|
27254
|
+
|
|
27255
|
+
// Synthesized channel tools. This route was missing: when
|
|
27256
|
+
// subscription-GC moved off the retired MCPL unsubscribe tool onto the
|
|
27257
|
+
// generic channel_close (discord-mcpl b095a9f), module-originated
|
|
27258
|
+
// closes started failing downstream as "Invalid tool name format" —
|
|
27259
|
+
// the janitor had been silently broken since. `origin` is trusted
|
|
27260
|
+
// dispatch context established at the call boundary above, not
|
|
27261
|
+
// inferred from which method was used: only the ModuleRegistry closure
|
|
27262
|
+
// carries module provenance (see handleToolClose).
|
|
27263
|
+
case 'channel': {
|
|
27264
|
+
const turn = call.name === 'channel_publish' && call.callerAgentName ? this.callingTurnOf(call.callerAgentName, call) : undefined;
|
|
27265
|
+
const input = call.name === 'channel_publish' && call.callerAgentName
|
|
27266
|
+
? this.publishInputWithTurnLocus(call.input, call.callerAgentName, turn)
|
|
27267
|
+
: call.input;
|
|
27268
|
+
return this.channelRegistry!.handleChannelToolCall(call.name, input, origin.kind === 'agent' && turn ? { ...origin, inTurn: true } : origin);
|
|
27269
|
+
}
|
|
27270
|
+
|
|
27271
|
+
case 'agent-settings':
|
|
27272
|
+
return this.awaitedToolAnswer((deliver) => this.dispatchAgentSettingsToolCall(subject, call, deliver));
|
|
27273
|
+
case 'save-image':
|
|
27274
|
+
return this.awaitedToolAnswer((deliver) => this.dispatchSaveImageToolCall(subject, call, deliver));
|
|
27275
|
+
case 'read-image':
|
|
27276
|
+
return this.awaitedToolAnswer((deliver) => this.dispatchReadImageToolCall(subject, call, deliver));
|
|
27277
|
+
case 'fetch-reference':
|
|
27278
|
+
return this.awaitedToolAnswer((deliver) => this.dispatchFetchReferenceToolCall(subject, call, deliver));
|
|
27279
|
+
case 'gate-status':
|
|
27280
|
+
return this.awaitedToolAnswer((deliver) => this.dispatchGateToolCall(subject, call, deliver));
|
|
27281
|
+
|
|
27282
|
+
// The tune-out tool, in process: the same body the dispatcher runs, its
|
|
27283
|
+
// answer awaited here (it can wait for the tune-out's transport).
|
|
27284
|
+
case 'tune-out': {
|
|
27285
|
+
const agentName = call.callerAgentName ?? this.primaryAgentName ?? '__ephemeral__';
|
|
27286
|
+
const input = (call.input ?? {}) as Record<string, unknown>;
|
|
27287
|
+
// The calling turn's own channels, as the channel tools read them here (origin.own).
|
|
27288
|
+
const own = origin.kind === 'agent' ? origin.own ?? [] : [];
|
|
27289
|
+
const turn = this.callingTurnOf(agentName, call);
|
|
27290
|
+
return call.name === 'focus' ? this.handleFocusTool(agentName, input, own, turn) : this.handleTuneOutTool(agentName, input, own, turn);
|
|
27291
|
+
}
|
|
27292
|
+
case 'subconscious': {
|
|
27293
|
+
const routed = this.readerToolCall(subject, call);
|
|
27294
|
+
return 'refused' in routed ? failed({ success: false, error: `${routed.refused}, so the call didn't run.`, isError: true }) : routed.result;
|
|
27295
|
+
}
|
|
27296
|
+
case 'sleep':
|
|
27297
|
+
return this.awaitedToolAnswer((deliver) => this.dispatchSleepToolCall(subject, call, deliver));
|
|
27298
|
+
|
|
27299
|
+
// The heartbeat's tools, for callers that await a tool's result (an
|
|
27300
|
+
// in-process call, a puppet, a module's callTool). A module is trusted;
|
|
27301
|
+
// an agent is held to what dispatch holds it to.
|
|
27302
|
+
case 'heartbeat': {
|
|
27303
|
+
const refusal = origin.kind === 'module' ? null : this.heartbeatRefusal(call.callerAgentName ?? '__ephemeral__', call);
|
|
27304
|
+
return refusal ? { success: false, error: refusal, isError: true } : this.runHeartbeatTool(call);
|
|
27305
|
+
}
|
|
27306
|
+
case 'wake-rule':
|
|
27307
|
+
return this.awaitedToolAnswer((deliver) => this.dispatchWakeRuleToolCall(subject, call, deliver));
|
|
27308
|
+
case 'event-tags':
|
|
27309
|
+
return this.awaitedToolAnswer((deliver) => this.dispatchEventTagsToolCall(subject, call, deliver));
|
|
27310
|
+
case 'agent-verb':
|
|
27311
|
+
if (origin.kind === 'module') return failed(agentsOwnVerbRefusal(call.name));
|
|
27312
|
+
if (call.name === 'skip_reply') {
|
|
27313
|
+
return failed({
|
|
27314
|
+
success: false,
|
|
27315
|
+
error: "skip_reply ends the turn it's called in, and this call wasn't made in a turn, so nothing was done.",
|
|
27316
|
+
isError: true,
|
|
27317
|
+
});
|
|
27318
|
+
}
|
|
27319
|
+
return this.channelRegistry!.handleChannelToolCall(call.name, call.input, origin);
|
|
27320
|
+
|
|
27321
|
+
// Client-side programmatic tool calling for callers that await a tool's
|
|
27322
|
+
// result: the puppet and a host through executeToolCall, and a module
|
|
27323
|
+
// through callTool. Keyed by callerAgentName, so each agent's scripts are
|
|
27324
|
+
// its own.
|
|
27325
|
+
case 'code-execution':
|
|
27326
|
+
return this.runCodeExecution(call.callerAgentName ?? '__ephemeral__', call);
|
|
27327
|
+
|
|
27328
|
+
// drafts: the calling resident's own held drafts. A puppeted call has
|
|
27329
|
+
// agent origin and acts on that resident's drafts, as the model's own
|
|
27330
|
+
// call does through dispatchToolCall, but it isn't made inside the
|
|
27331
|
+
// resident's turn, so it takes none of the turn's channels, engages none,
|
|
27332
|
+
// and isn't noted as the resident's speech (callingTurnOf). A module
|
|
27333
|
+
// holds no drafts.
|
|
27334
|
+
case 'drafts':
|
|
27335
|
+
if (origin.kind === 'module') return failed(agentsOwnVerbRefusal(call.name));
|
|
27336
|
+
return this.draftsToolResult(origin.agentName, call.input);
|
|
27337
|
+
case 'prose-help':
|
|
27338
|
+
return { success: true, data: PROSE_ROUTING_HELP };
|
|
27339
|
+
case 'module':
|
|
27340
|
+
// Module tools. The caller uses the result in process, so it gets the
|
|
27341
|
+
// module's own result, as the module returned it, less a refusal it
|
|
27342
|
+
// claims, which is the dispatcher's to say.
|
|
27343
|
+
return this.moduleRegistry.handleToolCall(call);
|
|
26936
27344
|
}
|
|
26937
|
-
|
|
26938
|
-
// MCPL tools go where the model's own call of the name goes, by the same
|
|
26939
|
-
// resolver and with the prefix it matched: to the server whose listing
|
|
26940
|
-
// has the name, else the longest matching prefix.
|
|
26941
|
-
if (this.mcplServerRegistry) {
|
|
26942
|
-
const mcpl = this.resolveMcplTool(call.name);
|
|
26943
|
-
if (mcpl) return this.executeMcplToolCall(call, mcpl[0], mcpl[1]);
|
|
26944
|
-
}
|
|
26945
|
-
|
|
26946
|
-
// Synthesized channel tools. This route was missing: when
|
|
26947
|
-
// subscription-GC moved off the retired MCPL unsubscribe tool onto the
|
|
26948
|
-
// generic channel_close (discord-mcpl b095a9f), module-originated
|
|
26949
|
-
// closes started failing downstream as "Invalid tool name format" —
|
|
26950
|
-
// the janitor had been silently broken since. `origin` is trusted
|
|
26951
|
-
// dispatch context established at the call boundary above, not
|
|
26952
|
-
// inferred from which method was used: only the ModuleRegistry closure
|
|
26953
|
-
// carries module provenance (see handleToolClose). The agent-only
|
|
26954
|
-
// synthesized verbs (think, skip_reply) are deliberately not routed
|
|
26955
|
-
// here.
|
|
26956
|
-
if (call.name.startsWith('channel_') && this.channelRegistry) {
|
|
26957
|
-
const turn = call.name === 'channel_publish' && call.callerAgentName ? this.callingTurnOf(call.callerAgentName, call) : undefined;
|
|
26958
|
-
const input = call.name === 'channel_publish' && call.callerAgentName
|
|
26959
|
-
? this.publishInputWithTurnLocus(call.input, call.callerAgentName, turn)
|
|
26960
|
-
: call.input;
|
|
26961
|
-
return this.channelRegistry.handleChannelToolCall(call.name, input, origin.kind === 'agent' && turn ? { ...origin, inTurn: true } : origin);
|
|
26962
|
-
}
|
|
26963
|
-
|
|
26964
|
-
// drafts: the calling resident's own held drafts. A puppeted call has
|
|
26965
|
-
// agent origin and acts on that resident's drafts, as the model's own
|
|
26966
|
-
// call does through dispatchToolCall, but it isn't made inside the
|
|
26967
|
-
// resident's turn, so it takes none of the turn's channels, engages none,
|
|
26968
|
-
// and isn't noted as the resident's speech (callingTurnOf). A module
|
|
26969
|
-
// holds no drafts.
|
|
26970
|
-
if (call.name === 'drafts' && this.channelRegistry && origin.kind === 'agent') {
|
|
26971
|
-
return this.draftsToolResult(origin.agentName, call.input);
|
|
26972
|
-
}
|
|
26973
|
-
|
|
26974
|
-
// The heartbeat's tools, for callers that await a tool's result (an
|
|
26975
|
-
// in-process call, a puppet, a module's callTool). A module is trusted;
|
|
26976
|
-
// an agent is held to what dispatch holds it to.
|
|
26977
|
-
if (this.heartbeat?.ownsTool(call.name)) {
|
|
26978
|
-
const refusal = origin.kind === 'module' ? null : this.heartbeatRefusal(call.callerAgentName ?? '__ephemeral__', call);
|
|
26979
|
-
return refusal ? { success: false, error: refusal, isError: true } : this.runHeartbeatTool(call);
|
|
26980
|
-
}
|
|
26981
|
-
// Module tools. The caller uses the result in process, so it gets the
|
|
26982
|
-
// module's own result, as the module returned it, less a refusal it
|
|
26983
|
-
// claims, which is the dispatcher's to say.
|
|
26984
|
-
// The tune-out tool, in process: the same body the dispatcher runs, its
|
|
26985
|
-
// answer awaited here (it can wait for the tune-out's transport).
|
|
26986
|
-
if ((call.name === 'tune_out' || call.name === 'focus') && this.tuneOutCoordinator) {
|
|
26987
|
-
const agentName = call.callerAgentName ?? this.primaryAgentName ?? '__ephemeral__';
|
|
26988
|
-
const input = (call.input ?? {}) as Record<string, unknown>;
|
|
26989
|
-
// The calling turn's own channels, as the channel tools read them here (origin.own).
|
|
26990
|
-
const own = origin.kind === 'agent' ? origin.own ?? [] : [];
|
|
26991
|
-
const turn = this.callingTurnOf(agentName, call);
|
|
26992
|
-
return call.name === 'focus' ? this.handleFocusTool(agentName, input, own, turn) : this.handleTuneOutTool(agentName, input, own, turn);
|
|
26993
|
-
}
|
|
26994
|
-
if ((SUBCONSCIOUS_TOOL_NAMES as readonly string[]).includes(call.name)) {
|
|
26995
|
-
const routed = this.readerToolCall(origin.kind === 'agent' ? origin.agentName : call.callerAgentName ?? '__ephemeral__', call);
|
|
26996
|
-
return 'refused' in routed ? failed({ success: false, error: `${routed.refused}, so the call didn't run.`, isError: true }) : routed.result;
|
|
26997
|
-
}
|
|
26998
|
-
return this.moduleRegistry.handleToolCall(call);
|
|
26999
27345
|
}
|
|
27000
27346
|
|
|
27001
27347
|
/**
|
|
@@ -27739,7 +28085,7 @@ export class AgentFramework {
|
|
|
27739
28085
|
// A batch with no finite deadline (a host whose deadline() threw) gets
|
|
27740
28086
|
// this write's own, rather than a timer Node would cut to 1 ms.
|
|
27741
28087
|
if (!Number.isFinite(deadline)) deadline = Date.now() + this.withheldSpillDeadlineMs;
|
|
27742
|
-
if (this.
|
|
28088
|
+
if (this.guardSpillsClosed) return null;
|
|
27743
28089
|
const place = this.toolResultFilePlace(label);
|
|
27744
28090
|
if (!place) return null;
|
|
27745
28091
|
if (this.stalledSpills.size > 0) {
|
|
@@ -28842,6 +29188,53 @@ export class AgentFramework {
|
|
|
28842
29188
|
return { name: call.name, cause: permitted || refused ? silenceCauseOf([call]) : null, refused, permitted };
|
|
28843
29189
|
}
|
|
28844
29190
|
|
|
29191
|
+
private toolRoute(call: ToolCall, actor: string | undefined): ToolRoute {
|
|
29192
|
+
const name = call.name;
|
|
29193
|
+
if (isPresentationTool(name)) return { kind: 'presentation' };
|
|
29194
|
+
if (name === META_TOOL_NAMES.act) {
|
|
29195
|
+
const host = actor === undefined ? null : this.hostPresentation?.resolveAct(actor, call.input) ?? null;
|
|
29196
|
+
if (host) return { kind: 'host-act', host };
|
|
29197
|
+
if (this.toolsetHub && this.mcplServerRegistry) return { kind: 'toolset-act' };
|
|
29198
|
+
}
|
|
29199
|
+
const mcpl = this.mcplServerRegistry ? this.resolveMcplTool(name) : null;
|
|
29200
|
+
if (mcpl) return { kind: 'mcpl', serverId: mcpl[0], prefix: mcpl[1] };
|
|
29201
|
+
if (name.startsWith('channel_') && this.channelRegistry) return { kind: 'channel' };
|
|
29202
|
+
if (name === 'agent_settings') return { kind: 'agent-settings' };
|
|
29203
|
+
if (name === 'save_recent_image') return { kind: 'save-image' };
|
|
29204
|
+
if (name === 'read_image') return { kind: 'read-image' };
|
|
29205
|
+
if (name === 'fetch_reference') return { kind: 'fetch-reference' };
|
|
29206
|
+
if (name === 'gate_status' && this.eventGate) return { kind: 'gate-status' };
|
|
29207
|
+
if ((name === 'tune_out' || name === 'focus') && this.tuneOutCoordinator) return { kind: 'tune-out' };
|
|
29208
|
+
if ((SUBCONSCIOUS_TOOL_NAMES as readonly string[]).includes(name)) return { kind: 'subconscious' };
|
|
29209
|
+
if ((name === 'sleep' || name === 'wake') && this.eventGate) return { kind: 'sleep' };
|
|
29210
|
+
if (this.heartbeat?.ownsTool(name)) return { kind: 'heartbeat' };
|
|
29211
|
+
if ((name === 'wake_add_rule' || name === 'wake_remove_rule') && this.eventGate) return { kind: 'wake-rule' };
|
|
29212
|
+
if (name === 'event_tags' && this.eventGate) return { kind: 'event-tags' };
|
|
29213
|
+
if ((name === 'think' || name === 'journal' || name === 'skip_reply') && this.channelRegistry) return { kind: 'agent-verb' };
|
|
29214
|
+
if (name === CODE_EXECUTION_TOOL_NAME && this.codeExecution) return { kind: 'code-execution' };
|
|
29215
|
+
if (name === 'drafts' && this.channelRegistry) return { kind: 'drafts' };
|
|
29216
|
+
if (name === PROSE_HELP_TOOL.name) return { kind: 'prose-help' };
|
|
29217
|
+
return { kind: 'module' };
|
|
29218
|
+
}
|
|
29219
|
+
|
|
29220
|
+
private pushToolResult(agentName: string, call: ToolCall, moduleName: string): (result: ToolResult) => void {
|
|
29221
|
+
return (result) => this.pushEvent({ type: 'tool-result', callId: call.id, toolName: call.name, agentName, moduleName, result });
|
|
29222
|
+
}
|
|
29223
|
+
|
|
29224
|
+
private awaitedToolAnswer(run: (deliver: (result: ToolResult) => void) => void): Promise<ToolResult> {
|
|
29225
|
+
return new Promise((resolve) => {
|
|
29226
|
+
try {
|
|
29227
|
+
run((result) => {
|
|
29228
|
+
const answer = { ...result };
|
|
29229
|
+
delete answer.refused;
|
|
29230
|
+
resolve(answer);
|
|
29231
|
+
});
|
|
29232
|
+
} catch (error) {
|
|
29233
|
+
resolve({ success: false, error: thrownText(error), isError: true });
|
|
29234
|
+
}
|
|
29235
|
+
});
|
|
29236
|
+
}
|
|
29237
|
+
|
|
28845
29238
|
/**
|
|
28846
29239
|
* Dispatch a call the agent made. `turnId` is the turn it was made in
|
|
28847
29240
|
* (TurnRouting.turnId), as its caller knows it: a model's call, the turn
|
|
@@ -28892,217 +29285,207 @@ export class AgentFramework {
|
|
|
28892
29285
|
const enrichedCall: ToolCall = { ...call, callerAgentName: agentName };
|
|
28893
29286
|
if (turnId !== undefined) enrichedCall.callerTurnId = turnId;
|
|
28894
29287
|
else delete enrichedCall.callerTurnId;
|
|
28895
|
-
|
|
28896
|
-
|
|
28897
|
-
|
|
28898
|
-
|
|
28899
|
-
|
|
29288
|
+
const route = this.toolRoute(enrichedCall, agentName);
|
|
29289
|
+
switch (route.kind) {
|
|
29290
|
+
case 'presentation': {
|
|
29291
|
+
const result = this.editToolPresentation(agentName, enrichedCall);
|
|
29292
|
+
this.pushEvent({type:'tool-result',callId:call.id,toolName:call.name,agentName,moduleName:'tool-presentation',result});
|
|
29293
|
+
return;
|
|
29294
|
+
}
|
|
28900
29295
|
|
|
29296
|
+
// toolsets--act on a host toolset's tool (the built-in one, a server's
|
|
29297
|
+
// tab): the call of that tool, under its own name, down the path a
|
|
29298
|
+
// direct call of it takes, permissions included.
|
|
29299
|
+
case 'host-act':
|
|
29300
|
+
this.dispatchHostAct(agentName, call, route.host, turnId);
|
|
29301
|
+
return;
|
|
28901
29302
|
|
|
28902
|
-
|
|
28903
|
-
|
|
28904
|
-
|
|
28905
|
-
|
|
28906
|
-
|
|
28907
|
-
|
|
28908
|
-
this.dispatchHostAct(agentName, call, host, turnId);
|
|
29303
|
+
// RFC-014: toolsets--act calls a server's dynamic tool, down the same
|
|
29304
|
+
// path as a direct call of it (dispatchToolsetAct).
|
|
29305
|
+
case 'toolset-act': {
|
|
29306
|
+
const scriptWaiter = this.scriptToolWaiters.get(enrichedCall.id);
|
|
29307
|
+
if (scriptWaiter) scriptWaiter.route.toServer = true;
|
|
29308
|
+
this.dispatchToolsetAct(agentName, enrichedCall);
|
|
28909
29309
|
return;
|
|
28910
29310
|
}
|
|
28911
|
-
}
|
|
28912
29311
|
|
|
28913
|
-
|
|
28914
|
-
|
|
28915
|
-
|
|
28916
|
-
|
|
28917
|
-
|
|
28918
|
-
|
|
28919
|
-
|
|
28920
|
-
|
|
28921
|
-
|
|
28922
|
-
// Route MCPL tool calls to the appropriate server via prefix map
|
|
28923
|
-
const mcplMatch = this.resolveMcplTool(enrichedCall.name);
|
|
28924
|
-
if (mcplMatch && this.mcplServerRegistry) {
|
|
28925
|
-
// A script's call: its waiter keeps that it went to a server, for the
|
|
28926
|
-
// script lane's exit (scriptToolWaiters).
|
|
28927
|
-
const scriptWaiter = this.scriptToolWaiters.get(enrichedCall.id);
|
|
28928
|
-
if (scriptWaiter) scriptWaiter.route.toServer = true;
|
|
28929
|
-
this.dispatchMcplToolCall(agentName, enrichedCall, mcplMatch[0], mcplMatch[1]);
|
|
28930
|
-
return;
|
|
28931
|
-
}
|
|
29312
|
+
case 'mcpl': {
|
|
29313
|
+
// A script's call: its waiter keeps that it went to a server, for the
|
|
29314
|
+
// script lane's exit (scriptToolWaiters).
|
|
29315
|
+
const scriptWaiter = this.scriptToolWaiters.get(enrichedCall.id);
|
|
29316
|
+
if (scriptWaiter) scriptWaiter.route.toServer = true;
|
|
29317
|
+
this.dispatchMcplToolCall(agentName, enrichedCall, route.serverId, route.prefix);
|
|
29318
|
+
return;
|
|
29319
|
+
}
|
|
28932
29320
|
|
|
28933
|
-
|
|
28934
|
-
|
|
28935
|
-
|
|
28936
|
-
|
|
28937
|
-
}
|
|
29321
|
+
case 'channel': {
|
|
29322
|
+
this.dispatchChannelToolCall(agentName, enrichedCall);
|
|
29323
|
+
return;
|
|
29324
|
+
}
|
|
28938
29325
|
|
|
28939
|
-
|
|
28940
|
-
|
|
28941
|
-
|
|
28942
|
-
|
|
29326
|
+
case 'agent-settings': {
|
|
29327
|
+
this.dispatchAgentSettingsToolCall(agentName, enrichedCall);
|
|
29328
|
+
return;
|
|
29329
|
+
}
|
|
28943
29330
|
|
|
28944
|
-
|
|
28945
|
-
|
|
28946
|
-
|
|
28947
|
-
|
|
29331
|
+
case 'save-image': {
|
|
29332
|
+
this.dispatchSaveImageToolCall(agentName, enrichedCall);
|
|
29333
|
+
return;
|
|
29334
|
+
}
|
|
28948
29335
|
|
|
28949
|
-
|
|
28950
|
-
|
|
28951
|
-
|
|
28952
|
-
|
|
29336
|
+
case 'read-image': {
|
|
29337
|
+
this.dispatchReadImageToolCall(agentName, enrichedCall);
|
|
29338
|
+
return;
|
|
29339
|
+
}
|
|
28953
29340
|
|
|
28954
|
-
|
|
28955
|
-
|
|
28956
|
-
|
|
28957
|
-
|
|
29341
|
+
case 'fetch-reference': {
|
|
29342
|
+
this.dispatchFetchReferenceToolCall(agentName, enrichedCall);
|
|
29343
|
+
return;
|
|
29344
|
+
}
|
|
28958
29345
|
|
|
28959
|
-
|
|
28960
|
-
|
|
28961
|
-
|
|
28962
|
-
|
|
28963
|
-
}
|
|
29346
|
+
case 'gate-status': {
|
|
29347
|
+
this.dispatchGateToolCall(agentName, enrichedCall);
|
|
29348
|
+
return;
|
|
29349
|
+
}
|
|
28964
29350
|
|
|
28965
|
-
|
|
28966
|
-
|
|
28967
|
-
this.dispatchTuneOutToolCall(agentName, enrichedCall);
|
|
28968
|
-
return;
|
|
28969
|
-
}
|
|
28970
|
-
if ((SUBCONSCIOUS_TOOL_NAMES as readonly string[]).includes(enrichedCall.name)) {
|
|
28971
|
-
const routed = this.readerToolCall(agentName, enrichedCall);
|
|
28972
|
-
if ('refused' in routed) {
|
|
28973
|
-
const error = routed.refused;
|
|
28974
|
-
const result = this.refusedResult(agentName, call.id, error);
|
|
28975
|
-
this.emitTrace({ type: 'tool:failed', module: 'tune-out', tool: call.name, callId: call.id, error });
|
|
28976
|
-
this.pushEvent({
|
|
28977
|
-
type: 'tool-result', callId: call.id, toolName: call.name, agentName, moduleName: 'tune-out', result,
|
|
28978
|
-
});
|
|
29351
|
+
case 'tune-out': {
|
|
29352
|
+
this.dispatchTuneOutToolCall(agentName, enrichedCall);
|
|
28979
29353
|
return;
|
|
28980
29354
|
}
|
|
28981
|
-
|
|
28982
|
-
.
|
|
28983
|
-
|
|
28984
|
-
|
|
29355
|
+
case 'subconscious': {
|
|
29356
|
+
const routed = this.readerToolCall(agentName, enrichedCall);
|
|
29357
|
+
if ('refused' in routed) {
|
|
29358
|
+
const error = routed.refused;
|
|
29359
|
+
const result = this.refusedResult(agentName, call.id, error);
|
|
29360
|
+
this.emitTrace({ type: 'tool:failed', module: 'tune-out', tool: call.name, callId: call.id, error });
|
|
28985
29361
|
this.pushEvent({
|
|
28986
|
-
type: 'tool-result',
|
|
28987
|
-
callId: enrichedCall.id,
|
|
28988
|
-
toolName: enrichedCall.name,
|
|
28989
|
-
agentName,
|
|
28990
|
-
moduleName: 'tune-out',
|
|
28991
|
-
result,
|
|
29362
|
+
type: 'tool-result', callId: call.id, toolName: call.name, agentName, moduleName: 'tune-out', result,
|
|
28992
29363
|
});
|
|
28993
|
-
|
|
28994
|
-
|
|
28995
|
-
|
|
29364
|
+
return;
|
|
29365
|
+
}
|
|
29366
|
+
void routed.result
|
|
29367
|
+
.then((result) => {
|
|
29368
|
+
// Through pushEvent, like every other tool's result, so one that
|
|
29369
|
+
// arrives after stop() is dropped rather than rejected unhandled.
|
|
29370
|
+
this.pushEvent({
|
|
29371
|
+
type: 'tool-result',
|
|
29372
|
+
callId: enrichedCall.id,
|
|
29373
|
+
toolName: enrichedCall.name,
|
|
29374
|
+
agentName,
|
|
29375
|
+
moduleName: 'tune-out',
|
|
29376
|
+
result,
|
|
29377
|
+
});
|
|
29378
|
+
});
|
|
29379
|
+
return;
|
|
29380
|
+
}
|
|
28996
29381
|
|
|
28997
|
-
|
|
28998
|
-
|
|
28999
|
-
|
|
29000
|
-
|
|
29001
|
-
}
|
|
29382
|
+
case 'sleep': {
|
|
29383
|
+
this.dispatchSleepToolCall(agentName, enrichedCall);
|
|
29384
|
+
return;
|
|
29385
|
+
}
|
|
29002
29386
|
|
|
29003
|
-
|
|
29004
|
-
|
|
29005
|
-
|
|
29006
|
-
|
|
29007
|
-
}
|
|
29387
|
+
case 'heartbeat': {
|
|
29388
|
+
this.dispatchHeartbeatToolCall(agentName, enrichedCall);
|
|
29389
|
+
return;
|
|
29390
|
+
}
|
|
29008
29391
|
|
|
29009
|
-
|
|
29010
|
-
|
|
29011
|
-
|
|
29012
|
-
|
|
29013
|
-
}
|
|
29392
|
+
case 'wake-rule': {
|
|
29393
|
+
this.dispatchWakeRuleToolCall(agentName, enrichedCall);
|
|
29394
|
+
return;
|
|
29395
|
+
}
|
|
29014
29396
|
|
|
29015
|
-
|
|
29016
|
-
|
|
29017
|
-
|
|
29018
|
-
|
|
29019
|
-
}
|
|
29397
|
+
case 'event-tags': {
|
|
29398
|
+
this.dispatchEventTagsToolCall(agentName, enrichedCall);
|
|
29399
|
+
return;
|
|
29400
|
+
}
|
|
29020
29401
|
|
|
29021
|
-
|
|
29022
|
-
|
|
29023
|
-
|
|
29024
|
-
|
|
29025
|
-
|
|
29026
|
-
|
|
29027
|
-
|
|
29028
|
-
|
|
29029
|
-
|
|
29030
|
-
|
|
29031
|
-
|
|
29032
|
-
|
|
29033
|
-
|
|
29034
|
-
|
|
29035
|
-
|
|
29036
|
-
|
|
29037
|
-
|
|
29038
|
-
|
|
29039
|
-
|
|
29040
|
-
|
|
29041
|
-
|
|
29042
|
-
|
|
29043
|
-
|
|
29044
|
-
|
|
29045
|
-
|
|
29046
|
-
|
|
29047
|
-
|
|
29048
|
-
|
|
29049
|
-
|
|
29050
|
-
|
|
29051
|
-
|
|
29052
|
-
|
|
29053
|
-
|
|
29054
|
-
|
|
29055
|
-
|
|
29056
|
-
|
|
29057
|
-
|
|
29058
|
-
|
|
29059
|
-
|
|
29060
|
-
|
|
29061
|
-
: `[self-wake] agent=${agentName} not armed: the framework has stopped (skip_reply)`);
|
|
29062
|
-
this.dispatchChannelToolCall(agentName, enrichedCall, earlierWake);
|
|
29063
|
-
return;
|
|
29402
|
+
case 'agent-verb': {
|
|
29403
|
+
// skip_reply(wake_in_seconds): arm a gate self-wake so "not replying
|
|
29404
|
+
// NOW" can also mean "back in a moment" — ends the turn, then wakes
|
|
29405
|
+
// the agent after N seconds unless something else wakes it first
|
|
29406
|
+
// (any turn start cancels the pending self-wake). Armed here because
|
|
29407
|
+
// the framework owns the EventGate; the registry only words the
|
|
29408
|
+
// confirmation. A wake the host can't set is refused, so the model
|
|
29409
|
+
// learns that its turn remains open and no wake was armed: any
|
|
29410
|
+
// wake_in_seconds without a gate, and one that isn't a number of
|
|
29411
|
+
// seconds with one.
|
|
29412
|
+
if (enrichedCall.name === 'skip_reply') {
|
|
29413
|
+
const error = this.skipReplyRefusal(enrichedCall);
|
|
29414
|
+
if (error !== undefined) {
|
|
29415
|
+
const wakeValue = (enrichedCall.input as { wake_in_seconds?: unknown } | null | undefined)?.wake_in_seconds;
|
|
29416
|
+
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)')}`);
|
|
29417
|
+
const result = this.refusedResult(agentName, enrichedCall.id, error);
|
|
29418
|
+
this.emitTrace({ type: 'tool:failed', module: 'channels', tool: enrichedCall.name, callId: enrichedCall.id, error });
|
|
29419
|
+
this.pushEvent({ type: 'tool-result', callId: enrichedCall.id, toolName: enrichedCall.name, agentName, moduleName: 'channels', result });
|
|
29420
|
+
return;
|
|
29421
|
+
}
|
|
29422
|
+
const skipping = this.callingTurnOf(agentName, enrichedCall);
|
|
29423
|
+
if (skipping) {
|
|
29424
|
+
skipping.privateEnd = true;
|
|
29425
|
+
delete skipping.unpostedSpeech;
|
|
29426
|
+
}
|
|
29427
|
+
const wake = readSkipReplyWake(enrichedCall.input);
|
|
29428
|
+
if (wake.kind === 'wake' && this.eventGate) {
|
|
29429
|
+
const armedAt = Date.now();
|
|
29430
|
+
const { inMs, armed } = this.eventGate.armSelfWake(agentName, wake.seconds, 'skip_reply');
|
|
29431
|
+
// The heartbeat's tick or reminder due before the timer: if it wakes
|
|
29432
|
+
// the agent, its turn cancels the timer, so the confirmation names
|
|
29433
|
+
// it and the time the self-wake would have left.
|
|
29434
|
+
const earlierWake = armed ? this.heartbeatWakeBefore(agentName, armedAt + inMs) : undefined;
|
|
29435
|
+
console.error(armed
|
|
29436
|
+
? `[self-wake] agent=${agentName} armed in ${Math.round(inMs / 1000)}s (skip_reply)`
|
|
29437
|
+
+ (earlierWake ? `; due sooner, in ~${earlierWake.inSeconds}s: ${logDetail(earlierWake.what)}` : '')
|
|
29438
|
+
: `[self-wake] agent=${agentName} not armed: the framework has stopped (skip_reply)`);
|
|
29439
|
+
this.dispatchChannelToolCall(agentName, enrichedCall, earlierWake);
|
|
29440
|
+
return;
|
|
29441
|
+
}
|
|
29064
29442
|
}
|
|
29443
|
+
this.dispatchChannelToolCall(agentName, enrichedCall);
|
|
29444
|
+
return;
|
|
29065
29445
|
}
|
|
29066
|
-
this.dispatchChannelToolCall(agentName, enrichedCall);
|
|
29067
|
-
return;
|
|
29068
|
-
}
|
|
29069
29446
|
|
|
29070
|
-
|
|
29071
|
-
|
|
29072
|
-
|
|
29073
|
-
|
|
29074
|
-
|
|
29075
|
-
|
|
29076
|
-
|
|
29447
|
+
// Client-side programmatic tool calling: run a script that calls the
|
|
29448
|
+
// agent's other tools in-process (script-inner calls come back through
|
|
29449
|
+
// this very dispatcher with waiter-tracked call IDs).
|
|
29450
|
+
case 'code-execution': {
|
|
29451
|
+
this.dispatchCodeExecutionToolCall(agentName, enrichedCall);
|
|
29452
|
+
return;
|
|
29453
|
+
}
|
|
29077
29454
|
|
|
29078
|
-
|
|
29079
|
-
|
|
29080
|
-
|
|
29081
|
-
|
|
29082
|
-
|
|
29083
|
-
|
|
29084
|
-
|
|
29085
|
-
|
|
29086
|
-
|
|
29087
|
-
|
|
29455
|
+
// drafts: the resident's own held drafts (list/read/resend/dismiss).
|
|
29456
|
+
// Answered by the framework; resend publishes through the registry's
|
|
29457
|
+
// publish executor, so the result arrives asynchronously.
|
|
29458
|
+
case 'drafts': {
|
|
29459
|
+
const startedAt = Date.now();
|
|
29460
|
+
this.emitTrace({ type: 'tool:started', module: 'framework', tool: 'drafts', callId: enrichedCall.id, input: enrichedCall.input });
|
|
29461
|
+
void this.draftsToolResult(agentName, enrichedCall.input, this.callingTurnOf(agentName, enrichedCall), enrichedCall.id)
|
|
29462
|
+
.then((result) => {
|
|
29463
|
+
this.emitTrace({ type: 'tool:completed', module: 'framework', tool: 'drafts', callId: enrichedCall.id, durationMs: Date.now() - startedAt });
|
|
29464
|
+
this.pushEvent({ type: 'tool-result', callId: enrichedCall.id, toolName: enrichedCall.name, agentName, moduleName: 'framework', result });
|
|
29465
|
+
});
|
|
29466
|
+
return;
|
|
29467
|
+
}
|
|
29468
|
+
|
|
29469
|
+
// prose_help: on-demand routing reference for explicit-prose agents.
|
|
29470
|
+
// Answered inline — the grammar is a tool RESULT (model-requested), never
|
|
29471
|
+
// ambient context (see PROSE_ROUTING_HELP / teach-by-bounce).
|
|
29472
|
+
case 'prose-help': {
|
|
29473
|
+
this.emitTrace({ type: 'tool:completed', module: 'framework', tool: 'prose_help', callId: enrichedCall.id, durationMs: 0 });
|
|
29474
|
+
this.pushEvent({
|
|
29475
|
+
type: 'tool-result',
|
|
29476
|
+
callId: enrichedCall.id,
|
|
29477
|
+
toolName: enrichedCall.name,
|
|
29478
|
+
agentName,
|
|
29479
|
+
moduleName: 'framework',
|
|
29480
|
+
result: { success: true, data: PROSE_ROUTING_HELP },
|
|
29088
29481
|
});
|
|
29089
|
-
|
|
29090
|
-
|
|
29482
|
+
return;
|
|
29483
|
+
}
|
|
29091
29484
|
|
|
29092
|
-
|
|
29093
|
-
|
|
29094
|
-
|
|
29095
|
-
|
|
29096
|
-
this.emitTrace({ type: 'tool:completed', module: 'framework', tool: 'prose_help', callId: enrichedCall.id, durationMs: 0 });
|
|
29097
|
-
this.pushEvent({
|
|
29098
|
-
type: 'tool-result',
|
|
29099
|
-
callId: enrichedCall.id,
|
|
29100
|
-
toolName: enrichedCall.name,
|
|
29101
|
-
agentName,
|
|
29102
|
-
moduleName: 'framework',
|
|
29103
|
-
result: { success: true, data: PROSE_ROUTING_HELP },
|
|
29104
|
-
});
|
|
29105
|
-
return;
|
|
29485
|
+
case 'module':
|
|
29486
|
+
break;
|
|
29487
|
+
default:
|
|
29488
|
+
route satisfies never;
|
|
29106
29489
|
}
|
|
29107
29490
|
|
|
29108
29491
|
const sepIndex = enrichedCall.name.indexOf('--');
|
|
@@ -29502,6 +29885,7 @@ export class AgentFramework {
|
|
|
29502
29885
|
private noteSpokenTo(agentName: string, metadata: MessageMetadata | undefined): void {
|
|
29503
29886
|
const agent = this.agents.get(agentName);
|
|
29504
29887
|
if ((agent && !this.agentSees(agent, metadata)) || isOwnScheduleWrite(metadata) || !isConversationalInjection(metadata)) return;
|
|
29888
|
+
this.rearmDeadlineCuts?.delete(agentName);
|
|
29505
29889
|
if (this.turnRearms?.delete(agentName)) console.error(`[inference-rearm] agent=${agentName} re-send count started again: a message was said to it`);
|
|
29506
29890
|
}
|
|
29507
29891
|
|
|
@@ -30233,11 +30617,9 @@ export class AgentFramework {
|
|
|
30233
30617
|
* at a tool boundary whose stream declares it carries the messages
|
|
30234
30618
|
* injected there (markContextConsumed). So a notice such a stream carried
|
|
30235
30619
|
* is under it. One a boundary stored without sending (a turn a tool ended,
|
|
30236
|
-
* a budget restart
|
|
30237
|
-
*
|
|
30238
|
-
*
|
|
30239
|
-
* rather than to no one where the stream dropped it. null when the window
|
|
30240
|
-
* can't be read, which is logged.
|
|
30620
|
+
* a budget restart, the direct API's boundary, which has no stream) is
|
|
30621
|
+
* above it until a compile sends it, and counts as unread. null when the
|
|
30622
|
+
* window can't be read, which is logged.
|
|
30241
30623
|
*/
|
|
30242
30624
|
private releasedWindow(agent: Agent, held: ReadonlySet<string>): ReleasedWindow | null {
|
|
30243
30625
|
try {
|
|
@@ -31395,6 +31777,7 @@ export class AgentFramework {
|
|
|
31395
31777
|
const ended = this.exhaustedTurns?.get(agentName);
|
|
31396
31778
|
this.exhaustedTurns?.delete(agentName);
|
|
31397
31779
|
const rearmed = this.exhaustingRearms?.delete(agentName) === true;
|
|
31780
|
+
const madeAgain = this.freshCallsCut?.delete(agentName) === true;
|
|
31398
31781
|
// The reason is the failure's own message, and a provider can echo the
|
|
31399
31782
|
// whole rejected request in it: one household's [inference-failed]
|
|
31400
31783
|
// marker carried ~1.7 MB of its own request back into its context. Every
|
|
@@ -31451,9 +31834,10 @@ export class AgentFramework {
|
|
|
31451
31834
|
// (2) Agent-facing chronicle marker (no inference triggered → no loop).
|
|
31452
31835
|
const agent = this.agents.get(agentName);
|
|
31453
31836
|
if (agent && !(rearmed && !ended) && process.env.SUPPRESS_INFERENCE_FAILED_MARKER !== '1') {
|
|
31454
|
-
const
|
|
31455
|
-
|
|
31456
|
-
.
|
|
31837
|
+
const timedOut = errorType === 'timeout';
|
|
31838
|
+
const causeParts = [errorType, httpStatus !== undefined ? `HTTP ${httpStatus}` : undefined]
|
|
31839
|
+
.filter((part): part is string => part !== undefined);
|
|
31840
|
+
const cause = timedOut && causeParts.length === 1 ? '' : causeParts.join(', ');
|
|
31457
31841
|
try {
|
|
31458
31842
|
agent.getContextManager().addMessage(
|
|
31459
31843
|
'user',
|
|
@@ -31461,17 +31845,17 @@ export class AgentFramework {
|
|
|
31461
31845
|
type: 'text',
|
|
31462
31846
|
text:
|
|
31463
31847
|
(ended
|
|
31464
|
-
? `[${NOTICE_TAG.inferenceFailed}] Your previous turn did not complete:
|
|
31465
|
-
|
|
31848
|
+
? `[${NOTICE_TAG.inferenceFailed}] Your previous turn did not complete: ` +
|
|
31849
|
+
`${timedOut ? 'its last call timed out' : 'it failed'}${cause ? ` (${suppliedValue(cause)})` : ''}.` +
|
|
31850
|
+
`${madeAgain ? ` ${MADE_AGAIN}` : ''}` +
|
|
31466
31851
|
`${ended.roundsRan ? ' Its earlier rounds ran, and their tool calls ran as their results show.' : ''}` +
|
|
31467
31852
|
`${ended.callsCutOff ? ` ${CALLS_CUT_OFF}` : ''}` +
|
|
31468
31853
|
`${ended.liveSpeech ? ` ${LIVE_SPEECH_UNRECEIPTED}` : ''}` +
|
|
31469
31854
|
`${ended.speechRouting ? ` ${SPEECH_STILL_ROUTING}` : ''} Reason: `
|
|
31470
|
-
: `[${NOTICE_TAG.inferenceFailed}] Your previous turn did not complete:
|
|
31471
|
-
`call failed${cause ? ` (${suppliedValue(cause)})` : ''}
|
|
31472
|
-
|
|
31473
|
-
|
|
31474
|
-
`or an unsupported setting).`,
|
|
31855
|
+
: `[${NOTICE_TAG.inferenceFailed}] Your previous turn did not complete: ` +
|
|
31856
|
+
`the model call ${timedOut ? 'timed out' : 'failed'}${cause ? ` (${suppliedValue(cause)})` : ''} ` +
|
|
31857
|
+
`and produced no response, so nothing was sent.${madeAgain ? ` ${MADE_AGAIN}` : ''} Reason: `) +
|
|
31858
|
+
`${failureShown(markerReason)}. ${timedOut ? TIMEOUT_ADVICE : FAILURE_ADVICE}`,
|
|
31475
31859
|
}],
|
|
31476
31860
|
{ system: true, kind: 'inference-failed', reason: markerReason, consecutive: streak, ...classification, ...cutAt(markerReason) },
|
|
31477
31861
|
);
|
|
@@ -31732,6 +32116,7 @@ export class AgentFramework {
|
|
|
31732
32116
|
this.featureSetManager,
|
|
31733
32117
|
inferenceRouting ?? null,
|
|
31734
32118
|
(event) => this.emitTrace(event as { type: TraceEvent['type']; [key: string]: unknown }),
|
|
32119
|
+
() => this.inferenceBounds,
|
|
31735
32120
|
);
|
|
31736
32121
|
|
|
31737
32122
|
// Checkpoint manager (Step 8), built and loaded at the top
|
|
@@ -31771,7 +32156,13 @@ export class AgentFramework {
|
|
|
31771
32156
|
if (!coordinator) return undefined;
|
|
31772
32157
|
const scope = coordinator.getAllExcept();
|
|
31773
32158
|
if (!scope || scope.params.except.serverId !== serverId || scope.params.except.channelId !== channelId) return undefined;
|
|
31774
|
-
coordinator.cancelAllExcept
|
|
32159
|
+
let released: ReturnType<typeof coordinator.cancelAllExcept>;
|
|
32160
|
+
try {
|
|
32161
|
+
released = coordinator.cancelAllExcept('channel-close', 'the excepted channel was closed');
|
|
32162
|
+
} catch (err) {
|
|
32163
|
+
throw new Error(`The close wasn't recorded: ${err instanceof Error ? err.message : String(err)}`);
|
|
32164
|
+
}
|
|
32165
|
+
if (!released.ok) throw new Error(`The close wasn't recorded: ${released.error}`);
|
|
31775
32166
|
return 'The focus on it ended all the same, and what the other channels held was delivered.';
|
|
31776
32167
|
},
|
|
31777
32168
|
focusChannelResolver: () => this.tuneOutCoordinator?.focusChannel() ?? null,
|
|
@@ -31948,13 +32339,11 @@ export class AgentFramework {
|
|
|
31948
32339
|
attempt: 0,
|
|
31949
32340
|
willRetry: !!left?.willReconnect,
|
|
31950
32341
|
});
|
|
31951
|
-
// So the server doesn't vanish quietly:
|
|
31952
|
-
//
|
|
31953
|
-
//
|
|
31954
|
-
|
|
31955
|
-
|
|
31956
|
-
if (error instanceof McplProtocolVersionError || left?.holdsUnreapedChild) {
|
|
31957
|
-
this.opsAlert('mcpl-down', config.id, `MCPL server unreachable at startup: ${message}`);
|
|
32342
|
+
// So the server doesn't vanish quietly: nothing connects it again on
|
|
32343
|
+
// its own, so alert at once. One that connected before its setup
|
|
32344
|
+
// threw is still connected, and gets no alert.
|
|
32345
|
+
if (!left?.willReconnect && !left?.isConnected) {
|
|
32346
|
+
this.opsAlert('mcpl-down', config.id, `MCPL server not connected at startup, and this host won't retry it on its own: ${message}`);
|
|
31958
32347
|
}
|
|
31959
32348
|
}
|
|
31960
32349
|
}
|
|
@@ -33892,9 +34281,10 @@ export class AgentFramework {
|
|
|
33892
34281
|
|
|
33893
34282
|
/**
|
|
33894
34283
|
* Restart an MCPL server at runtime: disconnect, then reconnect with the
|
|
33895
|
-
* same (or an updated) config. This
|
|
33896
|
-
*
|
|
33897
|
-
*
|
|
34284
|
+
* same (or an updated) config. This respawns a stdio child at once,
|
|
34285
|
+
* whatever its state; `reconnect: true` spawns a fresh child only after
|
|
34286
|
+
* the connection fails or its child exits. Throws if the server has no
|
|
34287
|
+
* stored config and none is given.
|
|
33898
34288
|
* What its connect would refuse is refused before the running server is
|
|
33899
34289
|
* torn down. Until the connect, a connect or a restart started meanwhile
|
|
33900
34290
|
* is refused the id and the tool prefix the restart moves the server to,
|
|
@@ -34376,12 +34766,9 @@ export class AgentFramework {
|
|
|
34376
34766
|
// request turned away at stop gets, so it can send it again to
|
|
34377
34767
|
// the host's next run. Optional chaining: prototype-built
|
|
34378
34768
|
// harnesses leave the queue unset.
|
|
34379
|
-
const
|
|
34769
|
+
const answer = this.queue?.isClosed ? hostStoppedWhileHandling() : { code: -32603, message: 'Internal error' };
|
|
34380
34770
|
try {
|
|
34381
|
-
tracked.respondError?.(
|
|
34382
|
-
stopping ? -32000 : -32603,
|
|
34383
|
-
stopping ? 'the host stopped while handling this request' : 'Internal error',
|
|
34384
|
-
);
|
|
34771
|
+
tracked.respondError?.(answer.code, answer.message);
|
|
34385
34772
|
} catch (sendError) {
|
|
34386
34773
|
responseError = recorded(describeCause(sendError));
|
|
34387
34774
|
}
|
|
@@ -34538,6 +34925,7 @@ export class AgentFramework {
|
|
|
34538
34925
|
respond: (result: unknown) => void;
|
|
34539
34926
|
respondError: (code: number, message: string) => void;
|
|
34540
34927
|
sendChunk?: (chunk: InferenceChunkParams) => void;
|
|
34928
|
+
signal?: AbortSignal;
|
|
34541
34929
|
},
|
|
34542
34930
|
) => {
|
|
34543
34931
|
// RFC-006 §5.3 / SPEC §10.7: no inference from inside a render.
|
|
@@ -34563,6 +34951,7 @@ export class AgentFramework {
|
|
|
34563
34951
|
// Chunks go out on the transport the request came on, as its
|
|
34564
34952
|
// answer does.
|
|
34565
34953
|
...(responder.sendChunk ? { sendChunk: responder.sendChunk } : {}),
|
|
34954
|
+
...(responder.signal ? { signal: responder.signal } : {}),
|
|
34566
34955
|
});
|
|
34567
34956
|
}
|
|
34568
34957
|
});
|
|
@@ -34878,14 +35267,15 @@ export class AgentFramework {
|
|
|
34878
35267
|
// that next one (see refreshMcplTools).
|
|
34879
35268
|
this.noteMcplServerClosed(connection.id);
|
|
34880
35269
|
this.dropMcplToolClasses(connection.id);
|
|
35270
|
+
// The transport's own words only: a WebSocket server's close text
|
|
35271
|
+
// stays off the trace.
|
|
35272
|
+
const reason = info?.reason !== undefined && !info.reasonFromPeer ? boundFailureText(info.reason) : null;
|
|
34881
35273
|
this.emitTrace({
|
|
34882
35274
|
type: 'mcpl:server-closed',
|
|
34883
35275
|
serverId: connection.id,
|
|
34884
35276
|
code: code ?? null,
|
|
34885
35277
|
signal: signal ?? null,
|
|
34886
|
-
|
|
34887
|
-
// stays off the trace.
|
|
34888
|
-
reason: info?.reason !== undefined && !info.reasonFromPeer ? boundFailureText(info.reason) : null,
|
|
35278
|
+
reason,
|
|
34889
35279
|
willReconnect: connection.willReconnect,
|
|
34890
35280
|
});
|
|
34891
35281
|
this.emitTrace({ type: 'module:removed', moduleName: `mcpl:${connection.id}` });
|
|
@@ -34895,6 +35285,14 @@ export class AgentFramework {
|
|
|
34895
35285
|
// RFC-014: its toolsets go with it; a next connection reads them again.
|
|
34896
35286
|
this.toolsetHub?.get(connection.id)?.detach(connection);
|
|
34897
35287
|
this.handleToolsListChanged(connection.id);
|
|
35288
|
+
// Lost on its own with no reconnect coming: nothing in this host
|
|
35289
|
+
// connects it again on its own. A close the host made itself (stop(),
|
|
35290
|
+
// a disconnect, a restart) is byHost, a loss once stop() has begun is
|
|
35291
|
+
// part of the close the host is making, and a child that ended as
|
|
35292
|
+
// asked was stopped: none of them gets an alert.
|
|
35293
|
+
if (!info?.byHost && !this.stopHasBegun() && !endedAsAsked({ code, signal, pipeFailed: info?.pipeFailed })) {
|
|
35294
|
+
this.opsAlert('mcpl-down', connection.id, `MCPL server closed${closeWords(reason, code, signal)}, and this host won't reconnect it on its own`);
|
|
35295
|
+
}
|
|
34898
35296
|
}
|
|
34899
35297
|
});
|
|
34900
35298
|
}
|
|
@@ -34920,11 +35318,12 @@ export class AgentFramework {
|
|
|
34920
35318
|
this.emitTrace({ type: 'module:added', moduleName: `mcpl:${connection.id}` });
|
|
34921
35319
|
this.handleToolsListChanged(connection.id);
|
|
34922
35320
|
});
|
|
34923
|
-
const failed = (params: { error: string; attempt: number; permanent?: boolean }) => {
|
|
35321
|
+
const failed = (params: { error: string; stderrTail?: string; attempt: number; permanent?: boolean }) => {
|
|
34924
35322
|
this.emitTrace({
|
|
34925
35323
|
type: 'mcpl:server-connect-failed',
|
|
34926
35324
|
serverId: connection.id,
|
|
34927
35325
|
error: recorded(params.error),
|
|
35326
|
+
...(params.stderrTail === undefined ? {} : { stderrTail: recorded(params.stderrTail) }),
|
|
34928
35327
|
attempt: params.attempt,
|
|
34929
35328
|
willRetry: connection.willReconnect,
|
|
34930
35329
|
});
|
|
@@ -34942,7 +35341,7 @@ export class AgentFramework {
|
|
|
34942
35341
|
connection.on('error', (err: Error) => {
|
|
34943
35342
|
this.emitTrace({ type: 'mcpl:server-error', serverId: connection.id, error: recorded(err.message) });
|
|
34944
35343
|
});
|
|
34945
|
-
connection.on('close', () => {
|
|
35344
|
+
connection.on('close', (info: { reason: string; code?: number | null; signal?: NodeJS.Signals | null; pipeFailed?: boolean }) => {
|
|
34946
35345
|
// A closed provider's class hints are stale (RFC-008), as for MCPL,
|
|
34947
35346
|
// and so is a listing already under way.
|
|
34948
35347
|
this.noteMcplServerClosed(connection.id);
|
|
@@ -34950,8 +35349,9 @@ export class AgentFramework {
|
|
|
34950
35349
|
this.emitTrace({
|
|
34951
35350
|
type: 'mcpl:server-closed',
|
|
34952
35351
|
serverId: connection.id,
|
|
34953
|
-
code: null,
|
|
34954
|
-
signal: null,
|
|
35352
|
+
code: info.code ?? null,
|
|
35353
|
+
signal: info.signal ?? null,
|
|
35354
|
+
reason: info.reason,
|
|
34955
35355
|
willReconnect: connection.willReconnect,
|
|
34956
35356
|
});
|
|
34957
35357
|
this.emitTrace({ type: 'module:removed', moduleName: `mcpl:${connection.id}` });
|
|
@@ -34971,6 +35371,13 @@ export class AgentFramework {
|
|
|
34971
35371
|
// Gone for good: its tools leave the agents' list, as a
|
|
34972
35372
|
// disconnect's do.
|
|
34973
35373
|
this.handleToolsListChanged(connection.id);
|
|
35374
|
+
// Lost on its own: nothing in this host connects it again on its
|
|
35375
|
+
// own. A loss once stop() has begun is part of the close the host
|
|
35376
|
+
// is making, and a child that ended as asked was stopped: neither
|
|
35377
|
+
// gets an alert.
|
|
35378
|
+
if (!this.stopHasBegun() && !endedAsAsked(info)) {
|
|
35379
|
+
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`);
|
|
35380
|
+
}
|
|
34974
35381
|
});
|
|
34975
35382
|
}
|
|
34976
35383
|
});
|
|
@@ -36135,7 +36542,7 @@ export class AgentFramework {
|
|
|
36135
36542
|
* (same path as gate.json load), so an invalid rule is surfaced as a tool
|
|
36136
36543
|
* error and nothing is written.
|
|
36137
36544
|
*/
|
|
36138
|
-
private dispatchWakeRuleToolCall(agentName: string, call: ToolCall): void {
|
|
36545
|
+
private dispatchWakeRuleToolCall(agentName: string, call: ToolCall, deliver = this.pushToolResult(agentName, call, 'gate')): void {
|
|
36139
36546
|
this.emitTrace({ type: 'tool:started', module: 'gate', tool: call.name, callId: call.id, input: call.input });
|
|
36140
36547
|
// What wake_add_rule's input held that does nothing, checkToolInput's notes and
|
|
36141
36548
|
// then the gate's on a rule it saved: a refusal names it too, so the call that
|
|
@@ -36150,14 +36557,23 @@ export class AgentFramework {
|
|
|
36150
36557
|
module: 'gate', tool: call.name, callId: call.id, durationMs: 0,
|
|
36151
36558
|
...(result.isError ? { error: result.error } : {}),
|
|
36152
36559
|
});
|
|
36153
|
-
|
|
36560
|
+
deliver(result);
|
|
36154
36561
|
};
|
|
36155
36562
|
|
|
36156
36563
|
try {
|
|
36157
36564
|
if (call.name === 'wake_remove_rule') {
|
|
36158
36565
|
const input = (call.input ?? {}) as { name?: unknown };
|
|
36159
36566
|
if (typeof input.name !== 'string' || !input.name) {
|
|
36160
|
-
|
|
36567
|
+
const edit = this.eventGate!.ruleNamesForChange();
|
|
36568
|
+
const names = 'names' in edit ? edit.names.map((name) => quotedValue(name)) : [];
|
|
36569
|
+
finish({
|
|
36570
|
+
success: false,
|
|
36571
|
+
error: `wake_remove_rule: name is ${shownValue(input.name)}, where the name of a rule is needed, so nothing was removed. `
|
|
36572
|
+
+ ('refused' in edit
|
|
36573
|
+
? edit.refused
|
|
36574
|
+
: (names.length > 0 ? `The rules a change edits are ${names.join(', ')}.` : 'There is no rule to remove.') + edit.note),
|
|
36575
|
+
isError: true,
|
|
36576
|
+
});
|
|
36161
36577
|
return;
|
|
36162
36578
|
}
|
|
36163
36579
|
const removed = this.removeGatePolicy(input.name);
|
|
@@ -36194,9 +36610,8 @@ export class AgentFramework {
|
|
|
36194
36610
|
if (behaviorSources.length === 0) {
|
|
36195
36611
|
finish({
|
|
36196
36612
|
success: false,
|
|
36197
|
-
error:
|
|
36198
|
-
? '
|
|
36199
|
-
: 'wake_add_rule: specify exactly one behavior — `behavior` ("always"/"defer"/"skip"), `debounceMs`, `rateLimit`, or `passiveSample`.',
|
|
36613
|
+
error: 'wake_add_rule got no behavior, so nothing was saved. Give one of behavior ("always", "defer" or "skip"), '
|
|
36614
|
+
+ (this.judgeWired ? 'debounceMs, rateLimit, passiveSample or judge.' : 'debounceMs, rateLimit or passiveSample.'),
|
|
36200
36615
|
isError: true,
|
|
36201
36616
|
});
|
|
36202
36617
|
return;
|
|
@@ -36204,7 +36619,7 @@ export class AgentFramework {
|
|
|
36204
36619
|
if (behaviorSources.length > 1) {
|
|
36205
36620
|
finish({
|
|
36206
36621
|
success: false,
|
|
36207
|
-
error: `wake_add_rule
|
|
36622
|
+
error: `wake_add_rule got more than one behavior (${behaviorSources.join(', ')}), so nothing was saved. Give only one of them.`,
|
|
36208
36623
|
isError: true,
|
|
36209
36624
|
});
|
|
36210
36625
|
return;
|
|
@@ -36247,7 +36662,7 @@ export class AgentFramework {
|
|
|
36247
36662
|
if (placementSources.length > 1) {
|
|
36248
36663
|
finish({
|
|
36249
36664
|
success: false,
|
|
36250
|
-
error: `wake_add_rule
|
|
36665
|
+
error: `wake_add_rule got more than one placement (${placementSources.join(', ')}), so nothing was saved. Give only one of them.`,
|
|
36251
36666
|
isError: true,
|
|
36252
36667
|
});
|
|
36253
36668
|
return;
|
|
@@ -36267,10 +36682,18 @@ export class AgentFramework {
|
|
|
36267
36682
|
for (const field of ['insertBefore', 'insertAfter'] as const) {
|
|
36268
36683
|
const anchor = input[field];
|
|
36269
36684
|
if (anchor !== undefined && (typeof anchor !== 'string' || anchor.length === 0)) {
|
|
36685
|
+
const edit = this.eventGate!.ruleNamesForChange();
|
|
36686
|
+
const names = 'names' in edit ? edit.names.filter((name) => name !== input.name).map((name) => quotedValue(name)) : [];
|
|
36270
36687
|
finish({
|
|
36271
36688
|
success: false,
|
|
36272
|
-
error: `wake_add_rule: ${field} must be the name of a rule, and it's ${shownValue(anchor)}, so nothing `
|
|
36273
|
-
+
|
|
36689
|
+
error: `wake_add_rule: ${field} must be the name of a rule, and it's ${shownValue(anchor)}, so nothing was saved. `
|
|
36690
|
+
+ ('refused' in edit
|
|
36691
|
+
? edit.refused
|
|
36692
|
+
: (names.length > 0
|
|
36693
|
+
? `The other rules a change edits are ${names.join(', ')}. Give ${field} one of them to place the rule beside it, or leave it out`
|
|
36694
|
+
: `There is no other rule to place it beside, so leave ${field} out`)
|
|
36695
|
+
+ ' to add the rule at the end, or, where a rule already has its name, to replace that rule where it is.'
|
|
36696
|
+
+ edit.note),
|
|
36274
36697
|
isError: true,
|
|
36275
36698
|
});
|
|
36276
36699
|
return;
|
|
@@ -36300,9 +36723,12 @@ export class AgentFramework {
|
|
|
36300
36723
|
const before = new Map(
|
|
36301
36724
|
this.eventGate!.probeTable().map((r) => [r.probe, r]),
|
|
36302
36725
|
);
|
|
36303
|
-
const
|
|
36726
|
+
const fieldNames = Object.fromEntries(Object.entries(AgentFramework.WAKE_ADD_RULE_FIELDS)
|
|
36727
|
+
.filter(([field]) => !(field.startsWith('behavior.') && typeof input.behavior === 'object')));
|
|
36728
|
+
const policy = this.eventGate!.addPolicy(rawPolicy, {
|
|
36304
36729
|
...(position ? { position } : {}),
|
|
36305
36730
|
onIgnoredFields: (notes) => { ignored.push(...notes); },
|
|
36731
|
+
fieldNames,
|
|
36306
36732
|
});
|
|
36307
36733
|
const after = this.eventGate!.probeTable();
|
|
36308
36734
|
const probes = after.map((r) => {
|
|
@@ -36615,21 +37041,19 @@ export class AgentFramework {
|
|
|
36615
37041
|
return decision;
|
|
36616
37042
|
}
|
|
36617
37043
|
|
|
36618
|
-
|
|
36619
|
-
|
|
36620
|
-
* in the deferred queue: a tune-out release flips durable epoch state
|
|
36621
|
-
* first, so its dump must not be lost to a crash in between (#232 gap 6).
|
|
36622
|
-
*/
|
|
36623
|
-
private commitDeliveries(): void {
|
|
37044
|
+
private commitDeliveries(): boolean {
|
|
37045
|
+
let durable = true;
|
|
36624
37046
|
if (this.deferredMessages.length > 0) {
|
|
36625
37047
|
this.deferredWritesPersisted = true;
|
|
36626
|
-
this.persistDeferredWrites();
|
|
37048
|
+
durable = this.persistDeferredWrites();
|
|
36627
37049
|
}
|
|
36628
37050
|
try {
|
|
36629
37051
|
this.store.sync();
|
|
36630
37052
|
} catch (err) {
|
|
36631
37053
|
console.error('[tune-out] could not sync the store after a release:', err);
|
|
37054
|
+
return false;
|
|
36632
37055
|
}
|
|
37056
|
+
return durable;
|
|
36633
37057
|
}
|
|
36634
37058
|
|
|
36635
37059
|
/** The history-reading tool the primary resident actually has, if any (#232 gap 11). */
|
|
@@ -36640,7 +37064,7 @@ export class AgentFramework {
|
|
|
36640
37064
|
return names.find((n) => /(^|--)(fetch_history|channel_read|read_history|history)$/.test(n)) ?? null;
|
|
36641
37065
|
}
|
|
36642
37066
|
|
|
36643
|
-
private dispatchSleepToolCall(agentName: string, call: ToolCall): void {
|
|
37067
|
+
private dispatchSleepToolCall(agentName: string, call: ToolCall, deliver = this.pushToolResult(agentName, call, 'gate')): void {
|
|
36644
37068
|
// The gate's sleep quiets the whole agent, every conversation of it, so a
|
|
36645
37069
|
// conversation fork or an ephemeral run neither sets one nor ends the
|
|
36646
37070
|
// agent's own. Refused before the tool runs, as a fork's channel_open is.
|
|
@@ -36659,7 +37083,7 @@ export class AgentFramework {
|
|
|
36659
37083
|
? `Nothing was set: a sleep quiets the whole agent, not one ${one}.${comeBack}`
|
|
36660
37084
|
: `Nothing was changed: a sleep is the agent's, so this ${one} can't end one.`;
|
|
36661
37085
|
this.emitTrace({ type: 'tool:failed', module: 'gate', tool: call.name, callId: call.id, error: `${agentName}: ${error}` });
|
|
36662
|
-
|
|
37086
|
+
deliver(this.refusedResult(agentName, call.id, error));
|
|
36663
37087
|
return;
|
|
36664
37088
|
}
|
|
36665
37089
|
this.emitTrace({ type: 'tool:started', module: 'gate', tool: call.name, callId: call.id, input: call.input });
|
|
@@ -36668,7 +37092,7 @@ export class AgentFramework {
|
|
|
36668
37092
|
|
|
36669
37093
|
const finish = (result: ToolResult) => {
|
|
36670
37094
|
this.emitTrace({ type: 'tool:completed', module: 'gate', tool: call.name, callId: call.id, durationMs: 0 });
|
|
36671
|
-
|
|
37095
|
+
deliver(result);
|
|
36672
37096
|
};
|
|
36673
37097
|
|
|
36674
37098
|
if (call.name === 'wake') {
|
|
@@ -36788,30 +37212,34 @@ export class AgentFramework {
|
|
|
36788
37212
|
*/
|
|
36789
37213
|
/** RFC-005 on-demand dereference: [ref id] → workspace file, via the
|
|
36790
37214
|
* origin-bound, ceiling-bounded, digest-verified host fetcher. */
|
|
36791
|
-
private dispatchFetchReferenceToolCall(agentName: string, call: ToolCall): void {
|
|
37215
|
+
private dispatchFetchReferenceToolCall(agentName: string, call: ToolCall, deliver = this.pushToolResult(agentName, call, 'workspace')): void {
|
|
36792
37216
|
this.emitTrace({ type: 'tool:started', module: 'workspace', tool: call.name, callId: call.id, input: call.input });
|
|
36793
37217
|
const startTime = Date.now();
|
|
36794
37218
|
void (async () => {
|
|
36795
|
-
const args = (call.input ?? {}) as Record<string, unknown>;
|
|
36796
|
-
const refId = String(args.ref_id ?? '');
|
|
36797
|
-
const record = referenceRegistry.get(refId);
|
|
36798
37219
|
let result: { success: boolean; data?: string; error?: string; isError?: boolean };
|
|
36799
|
-
|
|
36800
|
-
|
|
36801
|
-
|
|
36802
|
-
|
|
36803
|
-
|
|
36804
|
-
|
|
36805
|
-
|
|
36806
|
-
|
|
36807
|
-
|
|
36808
|
-
|
|
36809
|
-
|
|
36810
|
-
|
|
36811
|
-
|
|
36812
|
-
|
|
36813
|
-
|
|
36814
|
-
|
|
37220
|
+
try {
|
|
37221
|
+
const args = (call.input ?? {}) as Record<string, unknown>;
|
|
37222
|
+
const refId = String(args.ref_id ?? '');
|
|
37223
|
+
const record = referenceRegistry.get(refId);
|
|
37224
|
+
if (!record) {
|
|
37225
|
+
// Vector 20: a stale or unknown id is a defined miss, never a
|
|
37226
|
+
// different record.
|
|
37227
|
+
result = { success: false, isError: true,
|
|
37228
|
+
error: `unknown reference: ${refId || '(missing ref_id)'} — it may have been evicted; ids are never reused` };
|
|
37229
|
+
} else {
|
|
37230
|
+
const maxBytes = typeof args.max_bytes === 'number'
|
|
37231
|
+
? Math.min(args.max_bytes, DEFAULT_FETCH_MAX_BYTES) : undefined;
|
|
37232
|
+
const outcome = await this.getReferenceFetcher().fetch(record, { maxBytes });
|
|
37233
|
+
result = outcome.ok
|
|
37234
|
+
? { success: true, data: JSON.stringify({ path: outcome.path, bytes: outcome.bytes,
|
|
37235
|
+
mime_type: outcome.mimeType, digest_verified: outcome.digestVerified ?? 'no digest claimed' }) }
|
|
37236
|
+
// The fetcher says what happened as a clause about the reference,
|
|
37237
|
+
// and what calling again can do as a sentence of its own.
|
|
37238
|
+
: { success: false, isError: true,
|
|
37239
|
+
error: `fetch_reference couldn't fetch "${refId}": ${outcome.error}. ${outcome.remedy}` };
|
|
37240
|
+
}
|
|
37241
|
+
} catch (error) {
|
|
37242
|
+
result = { success: false, isError: true, error: `fetch_reference couldn't answer: ${thrownText(error)}` };
|
|
36815
37243
|
}
|
|
36816
37244
|
this.emitTrace({
|
|
36817
37245
|
type: result.isError ? 'tool:failed' : 'tool:completed',
|
|
@@ -36819,14 +37247,11 @@ export class AgentFramework {
|
|
|
36819
37247
|
durationMs: Date.now() - startTime,
|
|
36820
37248
|
...(result.isError ? { error: result.error } : {}),
|
|
36821
37249
|
});
|
|
36822
|
-
|
|
36823
|
-
type: 'tool-result', callId: call.id, toolName: call.name, agentName, moduleName: 'workspace',
|
|
36824
|
-
result: { ...result, isError: result.isError ?? false },
|
|
36825
|
-
});
|
|
37250
|
+
deliver({ ...result, isError: result.isError ?? false });
|
|
36826
37251
|
})();
|
|
36827
37252
|
}
|
|
36828
37253
|
|
|
36829
|
-
private dispatchReadImageToolCall(agentName: string, call: ToolCall): void {
|
|
37254
|
+
private dispatchReadImageToolCall(agentName: string, call: ToolCall, deliver = this.pushToolResult(agentName, call, 'workspace')): void {
|
|
36830
37255
|
this.emitTrace({ type: 'tool:started', module: 'workspace', tool: call.name, callId: call.id, input: call.input });
|
|
36831
37256
|
const finish = (result: ToolResult): void => {
|
|
36832
37257
|
this.emitTrace({
|
|
@@ -36837,7 +37262,7 @@ export class AgentFramework {
|
|
|
36837
37262
|
durationMs: 0,
|
|
36838
37263
|
...(result.isError ? { error: result.error } : {}),
|
|
36839
37264
|
});
|
|
36840
|
-
|
|
37265
|
+
deliver(result);
|
|
36841
37266
|
};
|
|
36842
37267
|
void (async (): Promise<void> => {
|
|
36843
37268
|
try {
|
|
@@ -36929,13 +37354,11 @@ export class AgentFramework {
|
|
|
36929
37354
|
* every sibling in the round to settle; if one never does, fail closed
|
|
36930
37355
|
* rather than trust an index the batch may still change. Sibling saves
|
|
36931
37356
|
* produce no images and are not waited on (two saves in one batch would
|
|
36932
|
-
* otherwise deadlock into a double timeout).
|
|
36933
|
-
*
|
|
36934
|
-
*
|
|
36935
|
-
* on itself.
|
|
37357
|
+
* otherwise deadlock into a double timeout). Only the round's own calls
|
|
37358
|
+
* wait: a call that isn't among its pending calls, a script's (`pytc-…`)
|
|
37359
|
+
* or an awaited call's, counts the images the agent has now.
|
|
36936
37360
|
*/
|
|
36937
37361
|
private async awaitSiblingToolCalls(agent: Agent, call: ToolCall): Promise<void> {
|
|
36938
|
-
if (call.id.startsWith('pytc-')) return;
|
|
36939
37362
|
const BARRIER_MS = 120_000;
|
|
36940
37363
|
const POLL_MS = 20;
|
|
36941
37364
|
const startedAt = Date.now();
|
|
@@ -36943,6 +37366,7 @@ export class AgentFramework {
|
|
|
36943
37366
|
const state = agent.state;
|
|
36944
37367
|
// Idle (puppet path) or the round was torn down: nothing to wait on.
|
|
36945
37368
|
if (state.status !== 'waiting_for_tools') return;
|
|
37369
|
+
if (!state.pending.has(call.id)) return;
|
|
36946
37370
|
const open = [...state.pending.values()].filter((p) => p.id !== call.id && p.name !== 'save_recent_image');
|
|
36947
37371
|
if (open.length === 0) return;
|
|
36948
37372
|
if (Date.now() - startedAt > BARRIER_MS) {
|
|
@@ -36971,7 +37395,7 @@ export class AgentFramework {
|
|
|
36971
37395
|
* nothing. It never slides to an older image — a filename must not
|
|
36972
37396
|
* authenticate bytes from another surface.
|
|
36973
37397
|
*/
|
|
36974
|
-
private dispatchSaveImageToolCall(agentName: string, call: ToolCall): void {
|
|
37398
|
+
private dispatchSaveImageToolCall(agentName: string, call: ToolCall, deliver = this.pushToolResult(agentName, call, 'workspace')): void {
|
|
36975
37399
|
this.emitTrace({ type: 'tool:started', module: 'workspace', tool: call.name, callId: call.id, input: call.input });
|
|
36976
37400
|
const finish = (result: ToolResult): void => {
|
|
36977
37401
|
this.emitTrace({
|
|
@@ -36982,7 +37406,7 @@ export class AgentFramework {
|
|
|
36982
37406
|
durationMs: 0,
|
|
36983
37407
|
...(result.isError ? { error: result.error } : {}),
|
|
36984
37408
|
});
|
|
36985
|
-
|
|
37409
|
+
deliver(result);
|
|
36986
37410
|
};
|
|
36987
37411
|
type Candidate =
|
|
36988
37412
|
| { kind: 'attachment'; data: string | null; mediaType: string; messagesBack: number }
|
|
@@ -37133,20 +37557,39 @@ export class AgentFramework {
|
|
|
37133
37557
|
let count = 1;
|
|
37134
37558
|
/** Direct-ref mode: the one ref to find in the scannable context. */
|
|
37135
37559
|
let wantedRef: string | null = null;
|
|
37136
|
-
|
|
37560
|
+
// A selector is in use only when it carries a value (fieldInUse), so a
|
|
37561
|
+
// caller whose provider presents every property as required can still
|
|
37562
|
+
// choose one, whichever of the two it fills the other selector with.
|
|
37563
|
+
// Any other value is checked, never read as absent: a whitespace or
|
|
37564
|
+
// malformed ref, or an index that isn't an integer, is refused rather
|
|
37565
|
+
// than turned into a different image than the one the caller named.
|
|
37566
|
+
// A malformed ref beside an index or count in use says how to leave a
|
|
37567
|
+
// selector unused, since the caller may have meant the positional one, as
|
|
37568
|
+
// the conflict refusal below does for a well-formed ref. No refusal of one
|
|
37569
|
+
// selector on its own says it: a ref alone is the caller's one selector,
|
|
37570
|
+
// and dropping it would save the newest image; an index and a count are
|
|
37571
|
+
// one selector, and dropping either changes which images are saved.
|
|
37572
|
+
if (fieldInUse(input.ref)) {
|
|
37137
37573
|
if (typeof input.ref !== 'string' || !TOOL_IMAGE_REF_RE.test(input.ref)) {
|
|
37138
|
-
|
|
37574
|
+
const positionalInUse = fieldInUse(input.index) || fieldInUse(input.count);
|
|
37575
|
+
throw new Error(
|
|
37576
|
+
'save_recent_image: `ref` must look like "img_k7x3q2_7" (copy it from the image placeholder in your context)' +
|
|
37577
|
+
(positionalInUse ? ' (pass null, or leave it out, for a selector you are not using)' : ''),
|
|
37578
|
+
);
|
|
37139
37579
|
}
|
|
37140
|
-
if (input.index
|
|
37141
|
-
throw new Error(
|
|
37580
|
+
if (fieldInUse(input.index) || fieldInUse(input.count)) {
|
|
37581
|
+
throw new Error(
|
|
37582
|
+
'save_recent_image: `ref` is mutually exclusive with `index`/`count` — pass null (or omit) ' +
|
|
37583
|
+
'for the selector you are not using',
|
|
37584
|
+
);
|
|
37142
37585
|
}
|
|
37143
37586
|
wantedRef = input.ref;
|
|
37144
37587
|
} else {
|
|
37145
|
-
index = input.index
|
|
37588
|
+
index = fieldInUse(input.index) ? integerField(input.index) : 0;
|
|
37146
37589
|
if (!Number.isInteger(index) || index < 0) {
|
|
37147
37590
|
throw new Error('save_recent_image: `index` must be a non-negative integer');
|
|
37148
37591
|
}
|
|
37149
|
-
count = input.count
|
|
37592
|
+
count = fieldInUse(input.count) ? integerField(input.count) : 1;
|
|
37150
37593
|
const MAX_COUNT = 20;
|
|
37151
37594
|
if (!Number.isInteger(count) || count < 1 || count > MAX_COUNT) {
|
|
37152
37595
|
throw new Error(`save_recent_image: \`count\` must be an integer in 1..${MAX_COUNT}`);
|
|
@@ -37408,7 +37851,7 @@ export class AgentFramework {
|
|
|
37408
37851
|
: `Changed before the refusal: ${changed.join(', ')}; no other setting reads differently.`;
|
|
37409
37852
|
}
|
|
37410
37853
|
|
|
37411
|
-
private dispatchAgentSettingsToolCall(agentName: string, call: ToolCall): void {
|
|
37854
|
+
private dispatchAgentSettingsToolCall(agentName: string, call: ToolCall, deliver = this.pushToolResult(agentName, call, 'agent')): void {
|
|
37412
37855
|
this.emitTrace({
|
|
37413
37856
|
type: 'tool:started',
|
|
37414
37857
|
module: 'agent',
|
|
@@ -37596,14 +38039,7 @@ export class AgentFramework {
|
|
|
37596
38039
|
durationMs: 0,
|
|
37597
38040
|
...(result.isError ? { error: result.error } : {}),
|
|
37598
38041
|
});
|
|
37599
|
-
|
|
37600
|
-
type: 'tool-result',
|
|
37601
|
-
callId: call.id,
|
|
37602
|
-
toolName: call.name,
|
|
37603
|
-
agentName,
|
|
37604
|
-
moduleName: 'agent',
|
|
37605
|
-
result,
|
|
37606
|
-
});
|
|
38042
|
+
deliver(result);
|
|
37607
38043
|
}
|
|
37608
38044
|
|
|
37609
38045
|
/** Aggregate the event-tag vocabulary: reserved chat:* core + each connected
|
|
@@ -37628,7 +38064,7 @@ export class AgentFramework {
|
|
|
37628
38064
|
};
|
|
37629
38065
|
}
|
|
37630
38066
|
|
|
37631
|
-
private dispatchEventTagsToolCall(agentName: string, call: ToolCall): void {
|
|
38067
|
+
private dispatchEventTagsToolCall(agentName: string, call: ToolCall, deliver = this.pushToolResult(agentName, call, 'gate')): void {
|
|
37632
38068
|
this.emitTrace({ type: 'tool:started', module: 'gate', tool: call.name, callId: call.id, input: call.input });
|
|
37633
38069
|
let result: import('./types/events.js').ToolResult;
|
|
37634
38070
|
try {
|
|
@@ -37640,10 +38076,10 @@ export class AgentFramework {
|
|
|
37640
38076
|
this.toolLifecycleEmitter?.markDispatchFailure(agentName, call.id);
|
|
37641
38077
|
result = { success: false, error: err.message, isError: true };
|
|
37642
38078
|
}
|
|
37643
|
-
|
|
38079
|
+
deliver(result);
|
|
37644
38080
|
}
|
|
37645
38081
|
|
|
37646
|
-
private dispatchGateToolCall(agentName: string, call: ToolCall): void {
|
|
38082
|
+
private dispatchGateToolCall(agentName: string, call: ToolCall, deliver = this.pushToolResult(agentName, call, 'gate')): void {
|
|
37647
38083
|
this.emitTrace({ type: 'tool:started', module: 'gate', tool: call.name, callId: call.id, input: call.input });
|
|
37648
38084
|
const startTime = Date.now();
|
|
37649
38085
|
|
|
@@ -37651,27 +38087,13 @@ export class AgentFramework {
|
|
|
37651
38087
|
.then((result) => {
|
|
37652
38088
|
const durationMs = Date.now() - startTime;
|
|
37653
38089
|
this.emitTrace({ type: 'tool:completed', module: 'gate', tool: call.name, callId: call.id, durationMs });
|
|
37654
|
-
|
|
37655
|
-
type: 'tool-result',
|
|
37656
|
-
callId: call.id,
|
|
37657
|
-
toolName: call.name,
|
|
37658
|
-
agentName,
|
|
37659
|
-
moduleName: 'gate',
|
|
37660
|
-
result,
|
|
37661
|
-
});
|
|
38090
|
+
deliver(result);
|
|
37662
38091
|
})
|
|
37663
38092
|
.catch((error) => {
|
|
37664
38093
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
37665
38094
|
this.emitTrace({ type: 'tool:failed', module: 'gate', tool: call.name, callId: call.id, error: err.message });
|
|
37666
38095
|
this.toolLifecycleEmitter?.markDispatchFailure(agentName, call.id);
|
|
37667
|
-
|
|
37668
|
-
type: 'tool-result',
|
|
37669
|
-
callId: call.id,
|
|
37670
|
-
toolName: call.name,
|
|
37671
|
-
agentName,
|
|
37672
|
-
moduleName: 'gate',
|
|
37673
|
-
result: { success: false, error: err.message, isError: true },
|
|
37674
|
-
});
|
|
38096
|
+
deliver({ success: false, error: err.message, isError: true });
|
|
37675
38097
|
});
|
|
37676
38098
|
}
|
|
37677
38099
|
|