@animalabs/agent-framework 0.30.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -8
- package/dist/src/agent.d.ts +29 -9
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +82 -17
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/mcp-server-core.d.ts.map +1 -1
- package/dist/src/api/mcp-server-core.js +10 -3
- package/dist/src/api/mcp-server-core.js.map +1 -1
- package/dist/src/code-execution/isolate/render.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/render.js +7 -6
- package/dist/src/code-execution/isolate/render.js.map +1 -1
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +5 -4
- package/dist/src/code-execution/isolate/scripts.js.map +1 -1
- package/dist/src/framework.d.ts +138 -102
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1133 -752
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +24 -17
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +198 -144
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/recipe-reconcile.d.ts.map +1 -1
- package/dist/src/gate/recipe-reconcile.js +6 -5
- package/dist/src/gate/recipe-reconcile.js.map +1 -1
- package/dist/src/host-presentation.d.ts.map +1 -1
- package/dist/src/host-presentation.js +5 -7
- package/dist/src/host-presentation.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/inference-bounds.d.ts +37 -0
- package/dist/src/inference-bounds.d.ts.map +1 -0
- package/dist/src/inference-bounds.js +84 -0
- package/dist/src/inference-bounds.js.map +1 -0
- package/dist/src/line-reader.d.ts +7 -1
- package/dist/src/line-reader.d.ts.map +1 -1
- package/dist/src/line-reader.js +17 -6
- package/dist/src/line-reader.js.map +1 -1
- package/dist/src/mcpl/errors.d.ts +1 -0
- package/dist/src/mcpl/errors.d.ts.map +1 -1
- package/dist/src/mcpl/errors.js +3 -0
- package/dist/src/mcpl/errors.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +2 -2
- package/dist/src/mcpl/inbound-source.js +2 -2
- package/dist/src/mcpl/inference-router.d.ts +11 -1
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.js +65 -9
- package/dist/src/mcpl/inference-router.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts +10 -7
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
- package/dist/src/mcpl/modern-connection.js +85 -74
- package/dist/src/mcpl/modern-connection.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts +1 -1
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +45 -3
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +2 -7
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +1 -2
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +3 -3
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +14 -0
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +26 -22
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts +1 -0
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
- package/dist/src/mcpl/tool-signature.js +16 -11
- package/dist/src/mcpl/tool-signature.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +44 -21
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +283 -64
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +17 -1
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +32 -4
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +189 -25
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
- package/dist/src/modules/discord/discord-js-client.js +24 -16
- package/dist/src/modules/discord/discord-js-client.js.map +1 -1
- package/dist/src/modules/sender-name.d.ts +5 -4
- package/dist/src/modules/sender-name.d.ts.map +1 -1
- package/dist/src/modules/sender-name.js +7 -5
- package/dist/src/modules/sender-name.js.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.d.ts +16 -8
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +33 -12
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +6 -3
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +61 -15
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +9 -1
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +50 -10
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts +11 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +19 -8
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +8 -0
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts +7 -0
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +7 -5
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.js +1 -9
- package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
- package/dist/src/supplied-text.d.ts +2 -0
- package/dist/src/supplied-text.d.ts.map +1 -1
- package/dist/src/supplied-text.js +16 -7
- package/dist/src/supplied-text.js.map +1 -1
- package/dist/src/tool-result-guard.js +1 -1
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +2 -20
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +175 -70
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts +14 -2
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/tune-out/tools.js.map +1 -1
- package/dist/src/types/agent.d.ts +6 -12
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +65 -24
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +42 -3
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +4 -3
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agent.ts +90 -25
- package/src/api/mcp-server-core.ts +10 -3
- package/src/code-execution/isolate/render.ts +7 -6
- package/src/code-execution/isolate/scripts.ts +5 -4
- package/src/framework.ts +1163 -770
- package/src/gate/event-gate.ts +220 -145
- package/src/gate/recipe-reconcile.ts +7 -5
- package/src/host-presentation.ts +4 -7
- package/src/index.ts +1 -0
- package/src/inference-bounds.ts +111 -0
- package/src/line-reader.ts +22 -5
- package/src/mcpl/errors.ts +4 -0
- package/src/mcpl/inbound-source.ts +2 -2
- package/src/mcpl/inference-router.ts +67 -10
- package/src/mcpl/modern-connection.ts +83 -68
- package/src/mcpl/protocol-family.ts +37 -4
- package/src/mcpl/push-coalescer.ts +2 -7
- package/src/mcpl/references.ts +3 -4
- package/src/mcpl/server-connection.ts +30 -21
- package/src/mcpl/tool-signature.ts +18 -11
- package/src/mcpl/transport.ts +261 -60
- package/src/mcpl/types.ts +18 -1
- package/src/module-registry.ts +178 -25
- package/src/modules/discord/discord-js-client.ts +33 -29
- package/src/modules/sender-name.ts +8 -5
- package/src/modules/workspace/disk-agreement.ts +39 -15
- package/src/modules/workspace/index.ts +65 -14
- package/src/modules/workspace/observe.ts +51 -10
- package/src/modules/workspace/reconcile.ts +22 -8
- package/src/modules/workspace/types.ts +5 -0
- package/src/modules/workspace/watcher.ts +8 -6
- package/src/recovery/discord-awareness-outbox.ts +1 -11
- package/src/supplied-text.ts +17 -6
- package/src/tool-result-guard.ts +1 -1
- package/src/tune-out/coordinator.ts +187 -85
- package/src/tune-out/tools.ts +14 -2
- package/src/types/agent.ts +6 -12
- package/src/types/framework.ts +66 -24
- package/src/types/module.ts +42 -3
- package/src/types/trace.ts +4 -3
- package/dist/src/reasoning-loss-notice.d.ts +0 -23
- package/dist/src/reasoning-loss-notice.d.ts.map +0 -1
- package/dist/src/reasoning-loss-notice.js +0 -52
- package/dist/src/reasoning-loss-notice.js.map +0 -1
- package/dist/src/reasoning-loss.d.ts +0 -24
- package/dist/src/reasoning-loss.d.ts.map +0 -1
- package/dist/src/reasoning-loss.js +0 -102
- package/dist/src/reasoning-loss.js.map +0 -1
- package/src/reasoning-loss-notice.ts +0 -64
- package/src/reasoning-loss.ts +0 -120
package/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';
|
|
@@ -198,7 +199,6 @@ import { cumulativeDelta } from './usage-accounting.js';
|
|
|
198
199
|
import { stampThinkingTokenEstimates } from './thinking-token-stamp.js';
|
|
199
200
|
import { splitThinkingDrops, ThinkingLossIncidents, type PlacedDrop, type StreamThinkingDrops } from './thinking-drops.js';
|
|
200
201
|
import { asNextRequestCarries, roundRows, type RoundRow, type SentRow } from './sent-rows.js';
|
|
201
|
-
import { ReasoningLossTeller } from './reasoning-loss.js';
|
|
202
202
|
import { contentAsStored, nameAsStored } from './text-as-stored.js';
|
|
203
203
|
import { STORED_INJECTION_KEY, STORED_INJECTION_KIND, asStoredRow, hasRenderedContent, isAnotherAgentsInjection, isOwnInjection, isStoredKind, storedInjectionMarkOf, unitHash, type StoredInjectionMark } from './stored-injections.js';
|
|
204
204
|
|
|
@@ -815,6 +815,37 @@ export const BUILTIN_TOOLSET_TOOLS: ReadonlySet<string> = new Set([
|
|
|
815
815
|
'workspace--status',
|
|
816
816
|
]);
|
|
817
817
|
|
|
818
|
+
type ToolRoute =
|
|
819
|
+
| { kind: 'presentation' }
|
|
820
|
+
| { kind: 'host-act'; host: HostActTarget }
|
|
821
|
+
| { kind: 'toolset-act' }
|
|
822
|
+
| { kind: 'mcpl'; serverId: string; prefix: string }
|
|
823
|
+
| { kind: 'channel' }
|
|
824
|
+
| { kind: 'agent-settings' }
|
|
825
|
+
| { kind: 'save-image' }
|
|
826
|
+
| { kind: 'read-image' }
|
|
827
|
+
| { kind: 'fetch-reference' }
|
|
828
|
+
| { kind: 'gate-status' }
|
|
829
|
+
| { kind: 'tune-out' }
|
|
830
|
+
| { kind: 'subconscious' }
|
|
831
|
+
| { kind: 'sleep' }
|
|
832
|
+
| { kind: 'heartbeat' }
|
|
833
|
+
| { kind: 'wake-rule' }
|
|
834
|
+
| { kind: 'event-tags' }
|
|
835
|
+
| { kind: 'agent-verb' }
|
|
836
|
+
| { kind: 'code-execution' }
|
|
837
|
+
| { kind: 'drafts' }
|
|
838
|
+
| { kind: 'prose-help' }
|
|
839
|
+
| { kind: 'module' };
|
|
840
|
+
|
|
841
|
+
function agentsOwnVerbRefusal(toolName: string): ToolResult {
|
|
842
|
+
return {
|
|
843
|
+
success: false,
|
|
844
|
+
error: `${toolName} is the agent's own, which a module's call can't make for it, so nothing was done.`,
|
|
845
|
+
isError: true,
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
|
|
818
849
|
type SettingsReading = {
|
|
819
850
|
core: Record<string, string>;
|
|
820
851
|
exts: Map<AgentSettingsExtension, { touched: readonly string[]; fields: Record<string, string> }>;
|
|
@@ -867,7 +898,7 @@ import { HEARTBEAT_CHANGING_TOOLS, Heartbeat, isHeartbeatMcplServer, type Heartb
|
|
|
867
898
|
import type { GateEntryRef, GateEventInfo, WakeLine } from './gate/types.js';
|
|
868
899
|
import { EPHEMERAL_USAGE_NAME, UsageTracker, persistedUsageProblem, type PersistedUsageState } from './usage/usage-tracker.js';
|
|
869
900
|
import type { SessionUsageSnapshot, UsageUpdatedEvent } from './usage/types.js';
|
|
870
|
-
import {
|
|
901
|
+
import { McplRequestError, McplUnreapedLaunchError, stderrTailOf, toolsetDeclarationsOf, withStderr, type McplConnectionCloseInfo, type McplServerConnection } from './mcpl/server-connection.js';
|
|
871
902
|
import { ToolsetHub, TOOLSETS_MODULE, META_TOOL_NAMES, type ToolsetController } from './mcpl/toolset-host.js';
|
|
872
903
|
import { HostPresentation, BUILTIN_TOOLSET, type HostActTarget } from './host-presentation.js';
|
|
873
904
|
import { isEmptyChange, type Presence } from './mcpl/tool-presence.js';
|
|
@@ -875,7 +906,7 @@ import { ToolsetsModule, resolveAct, withLookHint, type TakenChange } from './mc
|
|
|
875
906
|
import { ModernMcpConnection } from './mcpl/modern-connection.js';
|
|
876
907
|
import { MAX_TIMER_MS, WITHHELD_TEXT, asText, credentialRedactor, redactUrl, redactServerText, resolveServerBinding, serverConfigProblems, serverCredentialPatterns, serverTextRedactor, type CredentialPattern } from './mcpl/protocol-family.js';
|
|
877
908
|
import { MALFORMED_CONTENT_NOTICE, normalizeStandardToolResult } from './mcpl/tool-result-normalize.js';
|
|
878
|
-
import { boundedForLog, describeCause } from './mcpl/errors.js';
|
|
909
|
+
import { boundedForLog, describeCause, hostStoppedWhileHandling } from './mcpl/errors.js';
|
|
879
910
|
import { assertServerConfigBooleans, buildWebSocketUrl, isWebSocketTransport } from './mcpl/transport.js';
|
|
880
911
|
import type {
|
|
881
912
|
McplServerConfig,
|
|
@@ -1198,8 +1229,8 @@ function resumesLogicalTurn(reason: string | undefined): boolean {
|
|
|
1198
1229
|
*/
|
|
1199
1230
|
const TURN_RETRY: unique symbol = Symbol('turn-retry');
|
|
1200
1231
|
/**
|
|
1201
|
-
* The batch a start
|
|
1202
|
-
*
|
|
1232
|
+
* The batch a start was merged from, when the scheduler merged more than
|
|
1233
|
+
* one request into it and any of them isn't the turn's: every
|
|
1203
1234
|
* request it took in, as each was. Until the start takes its turn token it
|
|
1204
1235
|
* stands for them all. A scheduler pass that takes it back puts the batch in
|
|
1205
1236
|
* its place, each request with its own identity and flags; a quiesce at the
|
|
@@ -1230,14 +1261,9 @@ function waitAlertsOnItsOwn(wait: ProviderWait): boolean {
|
|
|
1230
1261
|
return wait.until === null || wait.until - wait.setAt > PROVIDER_WAIT_ALERT_MS;
|
|
1231
1262
|
}
|
|
1232
1263
|
|
|
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
1264
|
function isTransientFailure(error: Error): boolean {
|
|
1239
1265
|
if (!isMembraneError(error) || error.retryable !== true) return false;
|
|
1240
|
-
if ((error
|
|
1266
|
+
if (isDeadlineExpiry(error)) return false;
|
|
1241
1267
|
return error.type === 'server' || error.type === 'network' || error.type === 'timeout' || error.type === 'rate_limit';
|
|
1242
1268
|
}
|
|
1243
1269
|
|
|
@@ -1492,6 +1518,7 @@ interface TurnRouting {
|
|
|
1492
1518
|
*/
|
|
1493
1519
|
addressedElsewhere: Set<string>;
|
|
1494
1520
|
heard?: Map<MessageId, number>;
|
|
1521
|
+
inputsPin?: { agent: Agent; id: string };
|
|
1495
1522
|
}
|
|
1496
1523
|
|
|
1497
1524
|
/** A message that would re-pin its turn's locus (TurnRouting.arrivals). */
|
|
@@ -1522,6 +1549,29 @@ function freshTurnRouting(): TurnRouting {
|
|
|
1522
1549
|
};
|
|
1523
1550
|
}
|
|
1524
1551
|
|
|
1552
|
+
const TURN_INPUTS_SCAN = 64;
|
|
1553
|
+
|
|
1554
|
+
function turnInputs(
|
|
1555
|
+
cm: ContextManager, floor: number, agentName: string, sees: (metadata: unknown) => boolean,
|
|
1556
|
+
): { first: MessageId; last: MessageId } | undefined {
|
|
1557
|
+
let first: MessageId | undefined;
|
|
1558
|
+
let last: MessageId | undefined;
|
|
1559
|
+
scan: for (let end = cm.getMessageCount(); end > 0; end -= TURN_INPUTS_SCAN) {
|
|
1560
|
+
const { messages } = cm.getMessageWindow(Math.max(0, end - TURN_INPUTS_SCAN), Math.min(TURN_INPUTS_SCAN, end), { resolveBlobs: false });
|
|
1561
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1562
|
+
const message = messages[i];
|
|
1563
|
+
if (message.sequence <= floor) break scan;
|
|
1564
|
+
if (!sees(message.metadata)) continue;
|
|
1565
|
+
if (message.participant === agentName) break scan;
|
|
1566
|
+
last ??= message.id;
|
|
1567
|
+
const answers = Array.isArray(message.content) && message.content.length > 0
|
|
1568
|
+
&& message.content.every((block) => block.type === 'tool_result');
|
|
1569
|
+
if (!answers) first = message.id;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
return first !== undefined && last !== undefined ? { first, last } : undefined;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1525
1575
|
/**
|
|
1526
1576
|
* What a prose delivery tells its stream once a publish succeeds: the
|
|
1527
1577
|
* destination, its server and channel, and the text it published. The
|
|
@@ -1727,11 +1777,6 @@ const LIVE_SPEECH_UNRECEIPTED = `Plain speech its rounds routed live was handed
|
|
|
1727
1777
|
* and this says no retry follows it. */
|
|
1728
1778
|
const RETRY_CANCELLED_BY_STOP = 'a retry waiting while the framework was stopping is dropped before it sends anything';
|
|
1729
1779
|
|
|
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
1780
|
/** The reason on the inference:aborted that ends a turn whose continuation
|
|
1736
1781
|
* was dropped before it started, other than by an abandon or a stop. */
|
|
1737
1782
|
const CONTINUATION_DROPPED = "a turn's continuation was dropped before it started";
|
|
@@ -1822,6 +1867,26 @@ interface DiscordAwarenessDrainPass {
|
|
|
1822
1867
|
drain?: Promise<DiscordAwarenessDrainOutcome>;
|
|
1823
1868
|
}
|
|
1824
1869
|
|
|
1870
|
+
/** Whether a synthesized tool's nullable field is in use: omission, null and
|
|
1871
|
+
* the empty string all mean it isn't, so a caller whose provider presents
|
|
1872
|
+
* every property as required can still leave one unset. Any other value is
|
|
1873
|
+
* in use, and its field checks it. */
|
|
1874
|
+
function fieldInUse(value: unknown): boolean {
|
|
1875
|
+
return value !== undefined && value !== null && value !== '';
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
/** A synthesized tool's integer field: an integer number, or a string of ASCII
|
|
1879
|
+
* digits once trimmed (a model may write an integer as a string), within the
|
|
1880
|
+
* integers a number holds exactly; NaN for anything else. Number() would read
|
|
1881
|
+
* " ", false and [] as 0, and "2.0" or "+2" as 2, and a longer run of digits
|
|
1882
|
+
* as a different, rounded number. */
|
|
1883
|
+
function integerField(value: unknown): number {
|
|
1884
|
+
const n = typeof value === 'number' ? value
|
|
1885
|
+
: typeof value === 'string' && /^[0-9]+$/.test(value.trim()) ? Number(value.trim())
|
|
1886
|
+
: Number.NaN;
|
|
1887
|
+
return Number.isSafeInteger(n) ? n : Number.NaN;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1825
1890
|
function normalizeDiscordAwarenessDeadline(value: number | undefined): number {
|
|
1826
1891
|
if (value === undefined || !Number.isFinite(value) || value <= 0) {
|
|
1827
1892
|
return DEFAULT_DISCORD_AWARENESS_DEADLINE_MS;
|
|
@@ -1835,9 +1900,10 @@ function normalizeDiscordAwarenessDeadline(value: number | undefined): number {
|
|
|
1835
1900
|
/**
|
|
1836
1901
|
* Refuse a config whose `subconscious`, `watchdog` or `processLogging` is
|
|
1837
1902
|
* anything but an object, null or false (holdsSettings), or whose switch in
|
|
1838
|
-
* one holds anything but true or false (assertBooleanSetting)
|
|
1839
|
-
*
|
|
1840
|
-
*
|
|
1903
|
+
* one holds anything but true or false (assertBooleanSetting), or whose
|
|
1904
|
+
* subconscious's `thinkingBinding` assertThinkingBinding refuses, whichever
|
|
1905
|
+
* its reader. A subconscious that isn't on has its `allowChannelSpeech` and
|
|
1906
|
+
* `thinkingBinding` left unchecked, since nothing reads them then.
|
|
1841
1907
|
*/
|
|
1842
1908
|
function assertFrameworkSwitches(config: FrameworkConfig): void {
|
|
1843
1909
|
if (holdsSettings('subconscious', config.subconscious, '{ enabled: true }')) {
|
|
@@ -1853,6 +1919,7 @@ function assertFrameworkSwitches(config: FrameworkConfig): void {
|
|
|
1853
1919
|
config.subconscious.allowChannelSpeech,
|
|
1854
1920
|
'true lets the subconscious speak in channels with speak_in_channel, and false or leaving it out refuses it',
|
|
1855
1921
|
);
|
|
1922
|
+
assertThinkingBinding('subconscious.thinkingBinding', config.subconscious.thinkingBinding);
|
|
1856
1923
|
}
|
|
1857
1924
|
}
|
|
1858
1925
|
if (holdsSettings('watchdog', config.watchdog, '{ enabled: true }')) {
|
|
@@ -2180,15 +2247,16 @@ export interface HostModeStatus {
|
|
|
2180
2247
|
activeTurnAgents: string[];
|
|
2181
2248
|
/** Wakes that own provider admission while waiting for an in-flight
|
|
2182
2249
|
* 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
|
|
2250
|
+
* admission continuation rechecks quiesce and provider holds, and requeues
|
|
2251
|
+
* a wake either keeps from starting; `drained` is false until it has. A
|
|
2252
|
+
* parked continuation of a turn is counted here and, as a turn alive, in
|
|
2253
|
+
* `activeTurns` and `activeTurnAgents`. */
|
|
2186
2254
|
parkedAdmissions: number;
|
|
2187
2255
|
/** Inference requests waiting to start: queued (parked by the quiesce wake
|
|
2188
2256
|
* gate while quiesced, or, while serving, until the scheduler's next pass
|
|
2189
2257
|
* takes them), in a scheduler pass's hands, or held by a provider hold (an
|
|
2190
2258
|
* acceleration or host hold, or a provider's stated wait). There, a start
|
|
2191
|
-
*
|
|
2259
|
+
* merged from several requests counts as those it took in. A start parked
|
|
2192
2260
|
* on provider admission behind an auxiliary call counts once in
|
|
2193
2261
|
* `parkedAdmissions` instead, however many wakes are merged into it.
|
|
2194
2262
|
* Continuations of a turn (budget restart, tool results) and a held turn's
|
|
@@ -2356,9 +2424,15 @@ function hostCommandRequester(serverId: string, params: HostCommandParams): Oper
|
|
|
2356
2424
|
* schedule, jittered, 10 s doubling to 5 min (by default 13 tries, where
|
|
2357
2425
|
* membrane's settings have 7: about 30 min).
|
|
2358
2426
|
* - rate limited (429): the base schedule with at least 5 tries.
|
|
2359
|
-
* -
|
|
2360
|
-
*
|
|
2361
|
-
*
|
|
2427
|
+
* - a deadline expiry, which membrane leaves to its caller: a progress
|
|
2428
|
+
* bound's cut (FrameworkConfig.inferenceBounds), the adapter's own
|
|
2429
|
+
* watchdogs, the SDK's request timeout or a runtime's fetch timeout. The
|
|
2430
|
+
* deadline schedule: by default 2 tries, 1 s apart, so one fresh call. For
|
|
2431
|
+
* one, the framework's `attempt` counts that call's consecutive expiries,
|
|
2432
|
+
* not the turn's other failures, so each call a deadline cuts gets its
|
|
2433
|
+
* fresh call. Settings with no deadline schedule give it their base one.
|
|
2434
|
+
* - any other retryable failure: the base schedule (by default 3 tries, 1 s
|
|
2435
|
+
* then 2 s).
|
|
2362
2436
|
* A provider's stated wait (`retryAfterMs`) is waited when it is longer than
|
|
2363
2437
|
* the backoff, up to the schedule's ceiling, which by default is at least the
|
|
2364
2438
|
* framework's own cap (PROVIDER_ACCELERATION_MAX_COOLDOWN_MS, ten minutes);
|
|
@@ -2380,6 +2454,10 @@ export class DefaultErrorPolicy implements ErrorPolicy {
|
|
|
2380
2454
|
}
|
|
2381
2455
|
|
|
2382
2456
|
onInferenceError(error: Error, _agentName: string, attempt: number): ErrorAction {
|
|
2457
|
+
if (isDeadlineExpiry(error)) {
|
|
2458
|
+
const decision = retryDecision(this.settings.deadline ? 'deadline' : 'retryable', attempt + 1, undefined, this.settings);
|
|
2459
|
+
return decision.retry ? { retry: true, delayMs: decision.delayMs } : { retry: false };
|
|
2460
|
+
}
|
|
2383
2461
|
if (!isMembraneError(error)) {
|
|
2384
2462
|
// Not a provider failure membrane classified: retried as any other
|
|
2385
2463
|
// retryable failure, on the base schedule.
|
|
@@ -2388,9 +2466,7 @@ export class DefaultErrorPolicy implements ErrorPolicy {
|
|
|
2388
2466
|
}
|
|
2389
2467
|
if (error.retryable === false) return { retry: false };
|
|
2390
2468
|
// 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.
|
|
2469
|
+
// copy of membrane, as isMembraneError takes one).
|
|
2394
2470
|
const facts = error as { retryable?: boolean; type?: string; httpStatus?: number; message?: string };
|
|
2395
2471
|
// A failure that doesn't say is retried as an unknown one is; only false refuses (handled above).
|
|
2396
2472
|
const cls = classifyFailure({ retryable: facts.retryable !== false, type: facts.type, httpStatus: facts.httpStatus, message: facts.message });
|
|
@@ -2420,6 +2496,19 @@ const MAINTENANCE_TICKS_PER_PASS = 8;
|
|
|
2420
2496
|
* together, and then on awareness drains still running, such as its
|
|
2421
2497
|
* startup's delivery. */
|
|
2422
2498
|
const FAILED_CREATE_TEARDOWN_TIMEOUT_MS = 30_000;
|
|
2499
|
+
const ABANDON_GRACE_MS = 10_000;
|
|
2500
|
+
|
|
2501
|
+
const testAbandonGraces = new WeakMap<AgentFramework, number>();
|
|
2502
|
+
|
|
2503
|
+
/**
|
|
2504
|
+
* For tests only: `framework`'s abandon takes `ms` as its grace in place of
|
|
2505
|
+
* {@link ABANDON_GRACE_MS}. Set it only where the test holds every turn the
|
|
2506
|
+
* abandon reaches past the grace, so no turn can end within it and a short
|
|
2507
|
+
* grace only brings the abandon's report sooner.
|
|
2508
|
+
*/
|
|
2509
|
+
export function setAbandonGraceForTests(framework: AgentFramework, ms: number): void {
|
|
2510
|
+
testAbandonGraces.set(framework, ms);
|
|
2511
|
+
}
|
|
2423
2512
|
|
|
2424
2513
|
/** Local per-residence provider admission (AF #114 bounded first slice). */
|
|
2425
2514
|
interface LocalProviderGate {
|
|
@@ -2443,9 +2532,10 @@ interface ProviderAccelerationCooldown {
|
|
|
2443
2532
|
/** Armed by an auxiliary (compression) failure with no primary involved:
|
|
2444
2533
|
* release reopens auxiliary admission without synthesising an inference. */
|
|
2445
2534
|
auxiliaryOrigin?: boolean;
|
|
2446
|
-
/** Its held requests were taken out by dropContinuations
|
|
2447
|
-
*
|
|
2448
|
-
|
|
2535
|
+
/** Its held requests were all taken out, by dropContinuations ('drop') or
|
|
2536
|
+
* by a withdrawal of their coalescing subject ('withdrawal'): release
|
|
2537
|
+
* reopens admission without synthesising an inference in their place. */
|
|
2538
|
+
emptiedBy?: 'drop' | 'withdrawal';
|
|
2449
2539
|
rearm?: { number: number; of: number };
|
|
2450
2540
|
/** Set when this hold is a provider wait (a stated retry-after) for the
|
|
2451
2541
|
* agent's primary model: it holds primary turns only. Auxiliary calls are
|
|
@@ -2484,13 +2574,21 @@ const PROVIDER_ACCELERATION_DEFAULT_COOLDOWN_MS = 65_000;
|
|
|
2484
2574
|
* longest stated wait an ephemeral run sleeps in its turn: a longer one ends
|
|
2485
2575
|
* the run instead (nonOwningProviderWait). */
|
|
2486
2576
|
const PROVIDER_ACCELERATION_MAX_COOLDOWN_MS = 10 * 60_000;
|
|
2577
|
+
const FAILURE_ADVICE = 'If this recurs with the same cause, change approach rather than retrying identically ' +
|
|
2578
|
+
'(e.g. drop an oversized attachment or an unsupported setting).';
|
|
2579
|
+
const TIMEOUT_ADVICE = 'If a reply is still owed from that turn, you can give it now.';
|
|
2580
|
+
const MADE_AGAIN = 'The call was made again, and it timed out again.';
|
|
2487
2581
|
const OVERLOADED_TURN_TRIES = 13;
|
|
2488
|
-
const
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2582
|
+
const DEADLINE_TURN_TRIES = 2;
|
|
2583
|
+
const TURN_RETRY_SETTINGS: RetrySettings = {
|
|
2584
|
+
...Object.fromEntries(
|
|
2585
|
+
Object.entries(MEMBRANE_RETRY_SETTINGS as RetrySettings).map(([cls, s]) => [cls, s && {
|
|
2586
|
+
...s,
|
|
2587
|
+
...(cls === 'overloaded' ? { maxAttempts: OVERLOADED_TURN_TRIES } : {}),
|
|
2588
|
+
statedWaitCeilingMs: s.statedWaitCeilingMs === null ? null : Math.max(s.statedWaitCeilingMs, PROVIDER_ACCELERATION_MAX_COOLDOWN_MS),
|
|
2589
|
+
}])),
|
|
2590
|
+
deadline: { maxAttempts: DEADLINE_TURN_TRIES, firstDelayMs: 1_000, multiplier: 1, maxDelayMs: 1_000, statedWaitCeilingMs: null },
|
|
2591
|
+
} as RetrySettings;
|
|
2494
2592
|
const PROVIDER_ACCELERATION_JITTER_MS = 5_000;
|
|
2495
2593
|
/** Longest delay one setTimeout can represent (2^31-1 ms, about 24.8 days);
|
|
2496
2594
|
* a longer timer fires almost at once, so longer waits re-arm in steps. */
|
|
@@ -2540,6 +2638,11 @@ async function* turnStreamEvents(stream: YieldingStream, agentName: string): Asy
|
|
|
2540
2638
|
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
2639
|
return;
|
|
2542
2640
|
}
|
|
2641
|
+
if (event.type === 'aborted' && event.reason === 'timeout') {
|
|
2642
|
+
decidedBy = 'error';
|
|
2643
|
+
yield { type: 'error', error: deadlineCut(event.message) } as StreamEvent;
|
|
2644
|
+
continue;
|
|
2645
|
+
}
|
|
2543
2646
|
if (event.type === 'complete' || event.type === 'error' || event.type === 'aborted') decidedBy = event.type;
|
|
2544
2647
|
yield event;
|
|
2545
2648
|
}
|
|
@@ -2684,6 +2787,7 @@ interface EphemeralRun {
|
|
|
2684
2787
|
endReason?: AgentEndReason;
|
|
2685
2788
|
/** Who ended it, when it was stopped from outside. */
|
|
2686
2789
|
endedBy?: string;
|
|
2790
|
+
endedFromOutside?: true;
|
|
2687
2791
|
}
|
|
2688
2792
|
|
|
2689
2793
|
/**
|
|
@@ -3026,6 +3130,31 @@ const isLowSurrogate = (text: string, index: number): boolean => {
|
|
|
3026
3130
|
const splitsPair = (text: string, index: number): boolean =>
|
|
3027
3131
|
index > 0 && index < text.length && isHighSurrogate(text, index - 1) && isLowSurrogate(text, index);
|
|
3028
3132
|
|
|
3133
|
+
/**
|
|
3134
|
+
* Whether an MCPL server's child ended the way a server ends when it's asked
|
|
3135
|
+
* to: exit 0, death by a signal that asks a process to end (SIGTERM, SIGINT,
|
|
3136
|
+
* SIGHUP), or a shell's report of such a death by its convention, 128 plus the
|
|
3137
|
+
* signal's number (143, 130, 129). A service manager's stop signals the host
|
|
3138
|
+
* and its children together, so a child can go before the host's stop()
|
|
3139
|
+
* closes it: its loss is that stop, not a failure, and raises no mcpl-down.
|
|
3140
|
+
* A child the stdio transport ended itself, after a pipe error or a line on
|
|
3141
|
+
* its stdout longer than the bound, was ended by that, whatever its code or
|
|
3142
|
+
* signal. A WebSocket's close codes are 1000 and up, so they never match.
|
|
3143
|
+
*/
|
|
3144
|
+
function endedAsAsked(ended: { code?: number | null; signal?: string | null; pipeFailed?: boolean }): boolean {
|
|
3145
|
+
if (ended.pipeFailed) return false;
|
|
3146
|
+
const { code, signal } = ended;
|
|
3147
|
+
return code === 0 || code === 143 || code === 130 || code === 129 ||
|
|
3148
|
+
signal === 'SIGTERM' || signal === 'SIGINT' || signal === 'SIGHUP';
|
|
3149
|
+
}
|
|
3150
|
+
|
|
3151
|
+
/** A lost connection's words for an alert: the transport's reason, then its
|
|
3152
|
+
* exit or close code and signal, each when there is one. */
|
|
3153
|
+
function closeWords(reason: string | null | undefined, code: number | null | undefined, signal: string | null | undefined): string {
|
|
3154
|
+
const how = [reason ?? null, code != null ? `code ${code}` : null, signal ? `signal ${signal}` : null].filter((part) => part !== null);
|
|
3155
|
+
return how.length > 0 ? ` (${how.join(', ')})` : '';
|
|
3156
|
+
}
|
|
3157
|
+
|
|
3029
3158
|
/**
|
|
3030
3159
|
* `text` when it fits within `max` characters; otherwise its head and tail
|
|
3031
3160
|
* around a marker stating how many of how many characters were omitted, the
|
|
@@ -3261,6 +3390,17 @@ export function shedMayHelp(errorType: string | undefined, httpStatus: number |
|
|
|
3261
3390
|
return errorType === 'invalid_request' && (httpStatus === undefined || httpStatus === 400 || httpStatus === 422);
|
|
3262
3391
|
}
|
|
3263
3392
|
|
|
3393
|
+
function responseParts(response: EventResponse | undefined): string {
|
|
3394
|
+
return [
|
|
3395
|
+
response?.addMessages?.length ? `${response.addMessages.length} message(s) to add` : '',
|
|
3396
|
+
response?.editMessages?.length ? `${response.editMessages.length} edit(s)` : '',
|
|
3397
|
+
response?.removeMessages?.length ? `${response.removeMessages.length} removal(s)` : '',
|
|
3398
|
+
response?.stateUpdate !== undefined ? 'a state update' : '',
|
|
3399
|
+
response?.requestInference ? 'an inference request' : '',
|
|
3400
|
+
response?.toolsChanged ? 'a tools change' : '',
|
|
3401
|
+
].filter(Boolean).join(', ');
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3264
3404
|
/**
|
|
3265
3405
|
* What a thrown value says, safely and briefly, as boundedErrorMessage gives
|
|
3266
3406
|
* it, with `recorded` applied before the cut, so no part of a credential
|
|
@@ -3423,6 +3563,13 @@ interface StreamRequestNotes {
|
|
|
3423
3563
|
|
|
3424
3564
|
const SUBCONSCIOUS_MODEL_FIELDS = ['physicalWindowTokens', 'thinkingBinding'] as const satisfies ReadonlyArray<keyof AgentConfig>;
|
|
3425
3565
|
|
|
3566
|
+
function readerModelFields(cfg: SubconsciousConfig): Pick<AgentConfig, (typeof SUBCONSCIOUS_MODEL_FIELDS)[number]> {
|
|
3567
|
+
return {
|
|
3568
|
+
...(cfg.physicalWindowTokens != null ? { physicalWindowTokens: cfg.physicalWindowTokens } : {}),
|
|
3569
|
+
...(holdsSettings('subconscious.thinkingBinding', cfg.thinkingBinding, '{ binds: true, dropBlock: true }') ? { thinkingBinding: cfg.thinkingBinding } : {}),
|
|
3570
|
+
};
|
|
3571
|
+
}
|
|
3572
|
+
|
|
3426
3573
|
const JUDGE_PARAMETER: ToolParameter = {
|
|
3427
3574
|
type: 'object',
|
|
3428
3575
|
description:
|
|
@@ -3491,9 +3638,10 @@ export class AgentFramework {
|
|
|
3491
3638
|
private store: JsStore;
|
|
3492
3639
|
private ownsStore: boolean;
|
|
3493
3640
|
private stopPromise: Promise<void> | null = null;
|
|
3494
|
-
|
|
3641
|
+
// Nothing sets it back to 'none': a stop ends its instance (#315).
|
|
3642
|
+
private stopState: 'none' | 'stopping' | 'completed' | 'failed' = 'none';
|
|
3643
|
+
private closedStopFailure: { error: unknown } | undefined;
|
|
3495
3644
|
private ownedStoreClosed = false;
|
|
3496
|
-
private ownedStoreManagersClosed = false;
|
|
3497
3645
|
/** Whether the latest stop() got through its teardown with nothing failed:
|
|
3498
3646
|
* every step and every wait up to and including the one on awareness
|
|
3499
3647
|
* drains, whatever failed after that, in the storage work that finishes
|
|
@@ -3509,8 +3657,8 @@ export class AgentFramework {
|
|
|
3509
3657
|
* promise: a stream it cancelled or an event loop still in a call; and a
|
|
3510
3658
|
* stream whose cancel threw, which stop() doesn't wait for. A later
|
|
3511
3659
|
* 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
|
-
*
|
|
3660
|
+
* since to the entry it was found under (a run's end removes its stream's),
|
|
3661
|
+
* so neither a budget nor a cancel that
|
|
3514
3662
|
* threw lets the store close under work still running. Each leaves once it
|
|
3515
3663
|
* settles. */
|
|
3516
3664
|
private readonly leftRunning = new Map<Promise<unknown>, { label: string; kind: 'stream' | 'loop' }>();
|
|
@@ -3576,6 +3724,9 @@ export class AgentFramework {
|
|
|
3576
3724
|
private providerAccelerationLastRecovery: Map<string, ProviderAccelerationReceipt> = new Map();
|
|
3577
3725
|
private providerAccelerationDefaultCooldownMs = PROVIDER_ACCELERATION_DEFAULT_COOLDOWN_MS;
|
|
3578
3726
|
private providerAccelerationJitterMs = PROVIDER_ACCELERATION_JITTER_MS;
|
|
3727
|
+
private inferenceBounds: InferenceBounds | null = resolveInferenceBounds(undefined);
|
|
3728
|
+
private readonly deadlineCuts = new Map<string, number>();
|
|
3729
|
+
private readonly freshCallsCut = new Set<string>();
|
|
3579
3730
|
private providerHoldHook: ProviderHoldHook | undefined;
|
|
3580
3731
|
/** Provider waits (stated retry-after) per (agent, model), kept across restarts. */
|
|
3581
3732
|
private providerWaits: ProviderWaits | undefined;
|
|
@@ -3583,8 +3734,6 @@ export class AgentFramework {
|
|
|
3583
3734
|
private retryWaitWakers = new Set<() => void>();
|
|
3584
3735
|
private waitingTraceEveryMs = WAITING_TRACE_EVERY_MS;
|
|
3585
3736
|
private providerAdmissionClosed = false;
|
|
3586
|
-
/** Invalidates framework turns still assembling when stop begins. */
|
|
3587
|
-
private lifecycleGeneration = 0;
|
|
3588
3737
|
/** Last time we reported stale (busy-requeued) inference requests, per agent. */
|
|
3589
3738
|
private staleWarnAt = new Map<string, number>();
|
|
3590
3739
|
/** Per-agent last inference activity (epoch ms), for /healthz + doctor tooling. */
|
|
@@ -3640,7 +3789,6 @@ export class AgentFramework {
|
|
|
3640
3789
|
* next turn.
|
|
3641
3790
|
*/
|
|
3642
3791
|
private turnRouting: Map<string, TurnRouting> = new Map();
|
|
3643
|
-
private readonly reasoningLoss = new ReasoningLossTeller();
|
|
3644
3792
|
/**
|
|
3645
3793
|
* The latest link of each agent's chain of queued prose deliveries. Every
|
|
3646
3794
|
* stream queues its own behind it, a fresh turn's included, so the agent's
|
|
@@ -3736,6 +3884,7 @@ export class AgentFramework {
|
|
|
3736
3884
|
private turnRearmMs: readonly number[] = DEFAULT_TURN_REARM_MS;
|
|
3737
3885
|
private turnRearms: Map<string, number> = new Map();
|
|
3738
3886
|
private exhaustingRearms: Set<string> = new Set();
|
|
3887
|
+
private rearmDeadlineCuts: Set<string> = new Set();
|
|
3739
3888
|
private statedWaitRuns: Map<string, StatedWaitRun> = new Map();
|
|
3740
3889
|
/** N consecutive failed inferences ⇒ the agent is treated as hard-down and
|
|
3741
3890
|
* escalated loudly to stderr. */
|
|
@@ -4034,7 +4183,7 @@ export class AgentFramework {
|
|
|
4034
4183
|
private collapseDerivationNoticesAfter = 10;
|
|
4035
4184
|
/** The host's say over each derivation, ephemeral spawn and resume (config.dendrite.beforeDerive). */
|
|
4036
4185
|
private beforeDerive?: (spec: AgentSpec) => void | { refuse: string };
|
|
4037
|
-
/** What a source is told of a derivation (config.dendrite.derivationNotice); absent,
|
|
4186
|
+
/** What a source is told of a derivation (config.dendrite.derivationNotice); absent, nothing. */
|
|
4038
4187
|
private derivationNoticeHook?: (record: AgentRecord, from: string, defaultText: string) => string | null;
|
|
4039
4188
|
/** Per source agent, per kind: how many derivations, since when, and at which count the last note went. */
|
|
4040
4189
|
private derivationTallies: Map<string, Map<string, { count: number; since: number }>> = new Map();
|
|
@@ -4314,6 +4463,7 @@ export class AgentFramework {
|
|
|
4314
4463
|
getMessage: (id) => this.getMessage(id),
|
|
4315
4464
|
queryMessages: (filter, opts) => this.queryMessages(filter, opts),
|
|
4316
4465
|
pushEvent: (event, moduleName) => this.pushModuleEvent(event, moduleName, true),
|
|
4466
|
+
dropRetiredPush: (event, moduleName) => this.dropRetiredModuleEvent(event, moduleName),
|
|
4317
4467
|
onTrace: (listener) => this.onTrace(listener),
|
|
4318
4468
|
// The ONLY entry that confers module origin — a private closure the
|
|
4319
4469
|
// framework hands to its own module registry. The public
|
|
@@ -4337,6 +4487,7 @@ export class AgentFramework {
|
|
|
4337
4487
|
static async create(config: FrameworkConfig): Promise<AgentFramework> {
|
|
4338
4488
|
// Before anything opens or starts: a refused config must leave nothing behind.
|
|
4339
4489
|
if (config.codeExecution != null) validateCodeExecutionConfig(config.codeExecution);
|
|
4490
|
+
resolveInferenceBounds(config.inferenceBounds);
|
|
4340
4491
|
assertFrameworkSwitches(config);
|
|
4341
4492
|
for (const agentConfig of config.agents) assertAgentSwitches(agentConfig);
|
|
4342
4493
|
assertAgentNamesUnique(config.agents);
|
|
@@ -4538,6 +4689,7 @@ export class AgentFramework {
|
|
|
4538
4689
|
);
|
|
4539
4690
|
framework.providerHoldHook = config.providerHold;
|
|
4540
4691
|
framework.turnRearmMs = turnRearmDelays(config.exhaustedTurnRearmMs);
|
|
4692
|
+
framework.inferenceBounds = resolveInferenceBounds(config.inferenceBounds);
|
|
4541
4693
|
|
|
4542
4694
|
// Before any delivery.
|
|
4543
4695
|
await framework.reconcileDiscordAwarenessAtStartup();
|
|
@@ -5062,9 +5214,7 @@ export class AgentFramework {
|
|
|
5062
5214
|
* maintenance, event loop and operator operations, so a dump shows what
|
|
5063
5215
|
* those waits are on, and before its modules and MCPL servers stop, so a
|
|
5064
5216
|
* 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.
|
|
5217
|
+
* With none in the set, one line says no framework is running.
|
|
5068
5218
|
*
|
|
5069
5219
|
* One listener serves the process, installed by the first create() to
|
|
5070
5220
|
* reach the join and kept: once a process's last SIGUSR2 listener is
|
|
@@ -5108,36 +5258,39 @@ export class AgentFramework {
|
|
|
5108
5258
|
|
|
5109
5259
|
/**
|
|
5110
5260
|
* Start the event loop. A start() on a running framework does nothing.
|
|
5111
|
-
*
|
|
5112
|
-
*
|
|
5113
|
-
*
|
|
5114
|
-
*
|
|
5261
|
+
* A stop ends its instance (#315): once a stop() is admitted, start() and
|
|
5262
|
+
* addModule() are refused with an error, for good. While that stop() runs,
|
|
5263
|
+
* the error says the framework is stopping. Once it has settled, the error
|
|
5264
|
+
* says what to do: create a new AgentFramework on the store, and after a
|
|
5265
|
+
* failed stop(), call stop() again first, until one resolves. When a
|
|
5266
|
+
* failed stop() left its owned store closed, so that it can't be tried
|
|
5267
|
+
* again, the error carries that failure as its cause.
|
|
5268
|
+
*
|
|
5269
|
+
* A new instance on the store keeps what the store holds. With the
|
|
5270
|
+
* heartbeat on and the same heartbeat state file (heartbeat.statePath, or
|
|
5271
|
+
* its default), it keeps the heartbeat's schedule as it reads it from that
|
|
5272
|
+
* file: a tick or a reminder that fell due meanwhile fires once, shortly
|
|
5273
|
+
* after it starts. What an instance holds only in memory is lost with it,
|
|
5274
|
+
* as on a process restart: an agent's sleep and its wake, a self-wake, the
|
|
5275
|
+
* inference requests still queued (a budget restart, a heartbeat's wake, a
|
|
5276
|
+
* nudge), a re-armed turn's wait and count, and the redo stack. #627 is
|
|
5277
|
+
* about keeping a sleep and a self-wake across a restart. To pause and
|
|
5278
|
+
* resume one framework in place, use quiesce() and resume().
|
|
5115
5279
|
*/
|
|
5116
5280
|
start(): void {
|
|
5117
5281
|
// Before the running check: stop() claims its attempt before it clears
|
|
5118
5282
|
// running, so a start() made in its first steps, such as from a trace
|
|
5119
5283
|
// listener, is refused too.
|
|
5120
|
-
this.
|
|
5284
|
+
this.refuseOnceStopped('start()');
|
|
5121
5285
|
if (this.running) {
|
|
5122
5286
|
return;
|
|
5123
5287
|
}
|
|
5124
5288
|
|
|
5125
5289
|
this.running = true;
|
|
5126
|
-
this.providerAdmissionClosed = false;
|
|
5127
5290
|
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
5291
|
for (const config of this.mcplServerConfigs.values()) {
|
|
5135
5292
|
referenceRegistry.setServerAddress(config.id, serverAddressOf(config), this);
|
|
5136
5293
|
}
|
|
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
5294
|
this.heartbeat?.start();
|
|
5142
5295
|
this.loopPromise = this.runLoop();
|
|
5143
5296
|
|
|
@@ -5164,11 +5317,14 @@ export class AgentFramework {
|
|
|
5164
5317
|
}
|
|
5165
5318
|
|
|
5166
5319
|
/**
|
|
5167
|
-
* Stop the event loop
|
|
5168
|
-
*
|
|
5169
|
-
*
|
|
5170
|
-
*
|
|
5171
|
-
*
|
|
5320
|
+
* Stop the event loop, and with it this framework, for good: once a stop()
|
|
5321
|
+
* is admitted, start() and addModule() are refused, as start() says. A
|
|
5322
|
+
* stop() whose budget is refused, which it rejects with a RangeError, is
|
|
5323
|
+
* not admitted and changes nothing. Each step of the teardown runs
|
|
5324
|
+
* whatever an earlier one did: one that throws, such as a host stream's
|
|
5325
|
+
* cancel(), fails the stop() once the rest has run. When a step or a wait
|
|
5326
|
+
* up to and including the wait on awareness drains fails or times out,
|
|
5327
|
+
* storage stays open and
|
|
5172
5328
|
* stop() can be called again; one that fails after it, in the storage work
|
|
5173
5329
|
* that finishes the stop, fails the stop() without keeping storage open.
|
|
5174
5330
|
* A timed-out wait lets teardown go on while what it waited for may still
|
|
@@ -5176,7 +5332,10 @@ export class AgentFramework {
|
|
|
5176
5332
|
* @param teardownTimeoutMs Optional budget for each of stop()'s waits, in
|
|
5177
5333
|
* turn: on the streams it cancels, on maintenance in flight (a pass or an
|
|
5178
5334
|
* over-budget drain kick), on the event loop, on an operator's operations
|
|
5179
|
-
* already running, on modules and MCPL
|
|
5335
|
+
* already running, on modules and MCPL (its servers and the inference
|
|
5336
|
+
* requests they made) together, where a module whose start() is still
|
|
5337
|
+
* running counts until that start() has settled and its stop() returned,
|
|
5338
|
+
* then on Discord awareness
|
|
5180
5339
|
* drains, which send nothing once stop() has begun, recording the call the
|
|
5181
5340
|
* close ended. With a budget, a stop() can take up to six of them, besides
|
|
5182
5341
|
* the withheld-spill deadline of a guard's write still under way. Omit to
|
|
@@ -5199,9 +5358,7 @@ export class AgentFramework {
|
|
|
5199
5358
|
* dropped, though a write the stream already had under way can still
|
|
5200
5359
|
* land in the run's namespace after them.
|
|
5201
5360
|
* - 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.
|
|
5361
|
+
* that call and the rest of its iteration, and then exits.
|
|
5205
5362
|
* - a later stop() waits again, within its own budget, for any stream or
|
|
5206
5363
|
* loop this one left running, so the store never closes under it.
|
|
5207
5364
|
*
|
|
@@ -5224,11 +5381,13 @@ export class AgentFramework {
|
|
|
5224
5381
|
let settle!: { resolve: () => void; reject: (error: unknown) => void };
|
|
5225
5382
|
const attempt = new Promise<void>((resolve, reject) => { settle = { resolve, reject }; });
|
|
5226
5383
|
this.stopPromise = attempt;
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5384
|
+
this.stopState = 'stopping';
|
|
5385
|
+
void attempt.then(() => { this.stopState = 'completed'; }, (error: unknown) => {
|
|
5386
|
+
this.stopState = 'failed';
|
|
5387
|
+
// Failed cleanup can be retried while its original store is open. A
|
|
5388
|
+
// terminal storage failure after successful close must remain a failure.
|
|
5389
|
+
if (this.ownedStoreClosed) this.closedStopFailure = { error };
|
|
5390
|
+
else if (this.stopPromise === attempt) this.stopPromise = null;
|
|
5232
5391
|
});
|
|
5233
5392
|
// The body still starts in this call, so its synchronous steps (closing
|
|
5234
5393
|
// the queue among them) happen before stop() returns.
|
|
@@ -5236,22 +5395,29 @@ export class AgentFramework {
|
|
|
5236
5395
|
return attempt;
|
|
5237
5396
|
}
|
|
5238
5397
|
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5398
|
+
private refuseOnceStopped(verb: 'start()' | 'addModule()'): void {
|
|
5399
|
+
switch (this.stopState) {
|
|
5400
|
+
case 'none':
|
|
5401
|
+
return;
|
|
5402
|
+
case 'stopping':
|
|
5403
|
+
throw new Error(`${verb} refused: the framework is stopping`);
|
|
5404
|
+
case 'completed':
|
|
5405
|
+
throw new Error(`${verb} refused: this framework has stopped, and a stopped framework doesn't start again. ` +
|
|
5406
|
+
'Create a new AgentFramework on its store to go on, with its storePath if it opened the store itself.');
|
|
5407
|
+
case 'failed': {
|
|
5408
|
+
const closed = this.closedStopFailure;
|
|
5409
|
+
if (closed) {
|
|
5410
|
+
throw new Error(`${verb} refused: this framework's stop() failed and its owned store is closed, so stop() can't be tried again: ` +
|
|
5411
|
+
`it rejects with that same failure, this error's cause: ${shutdownErrorMessage(closed.error)}. ` +
|
|
5412
|
+
"A stopped framework doesn't start again, and a new AgentFramework on its store doesn't finish what that stop() left unfinished.",
|
|
5413
|
+
{ cause: closed.error });
|
|
5414
|
+
}
|
|
5415
|
+
throw new Error(`${verb} refused: this framework's stop() failed, and a stopped framework doesn't start again. ` +
|
|
5416
|
+
'A failed stop() may have left its cleanup or final persistence unfinished. ' +
|
|
5417
|
+
"Call stop() again: once a stop() of this one resolves, they're done, and only then should a new " +
|
|
5418
|
+
'AgentFramework use its store, with its storePath if this one opened the store itself, ' +
|
|
5419
|
+
'or another instance use anything this one shares.');
|
|
5420
|
+
}
|
|
5255
5421
|
}
|
|
5256
5422
|
}
|
|
5257
5423
|
|
|
@@ -5373,6 +5539,13 @@ export class AgentFramework {
|
|
|
5373
5539
|
// holding the wait for operator operations below until its handshake's
|
|
5374
5540
|
// deadline.
|
|
5375
5541
|
step(() => this.endConnectsInFlight());
|
|
5542
|
+
// Nor does a server lost before or during the stop launch or dial again
|
|
5543
|
+
// on its own: a retry already scheduled starts nothing, and none is
|
|
5544
|
+
// scheduled after.
|
|
5545
|
+
step(() => {
|
|
5546
|
+
for (const connection of this.mcplServerRegistry?.getAllServers() ?? []) connection.stopReconnecting();
|
|
5547
|
+
for (const connection of this.modernServers().values()) connection.stopReconnecting();
|
|
5548
|
+
});
|
|
5376
5549
|
step(() => this.pushCoalescer?.suspend());
|
|
5377
5550
|
step(() => this.flushCoalescingSnapshot());
|
|
5378
5551
|
this.running = false;
|
|
@@ -5386,11 +5559,10 @@ export class AgentFramework {
|
|
|
5386
5559
|
step(() => this.ackDeferredWrites());
|
|
5387
5560
|
this.providerAdmissionClosed = true;
|
|
5388
5561
|
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;
|
|
5562
|
+
this.guardSpillsClosed = false;
|
|
5393
5563
|
step(() => this.queue.close());
|
|
5564
|
+
let inferences: Array<{ label: string; promise: Promise<void> }> = [];
|
|
5565
|
+
step(() => { inferences = this.inferenceRouter?.stopAll() ?? []; });
|
|
5394
5566
|
step(() => this.tuneOutCoordinator?.stop());
|
|
5395
5567
|
|
|
5396
5568
|
// Stop running code_execution scripts before cancelling streams: a
|
|
@@ -5405,9 +5577,8 @@ export class AgentFramework {
|
|
|
5405
5577
|
this.toolImageLedgers.clear();
|
|
5406
5578
|
|
|
5407
5579
|
// 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
|
|
5580
|
+
// a turn's start a stop reaches does: the continuation never starts.
|
|
5581
|
+
// Before the holds and the parks go, so the
|
|
5411
5582
|
// continuation leaves them with its turn. Each drop is a step of its
|
|
5412
5583
|
// own, so one that throws leaves the next agent's turn to end all the
|
|
5413
5584
|
// same, as stop()'s other loops do.
|
|
@@ -5418,11 +5589,7 @@ export class AgentFramework {
|
|
|
5418
5589
|
});
|
|
5419
5590
|
// A stopped host must never hang behind its own cooldown.
|
|
5420
5591
|
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.
|
|
5592
|
+
// An open logical turn doesn't survive a stop.
|
|
5426
5593
|
this.openLogical.clear();
|
|
5427
5594
|
|
|
5428
5595
|
// Cancel the stream of every agent whose state holds one: streaming,
|
|
@@ -5541,8 +5708,9 @@ export class AgentFramework {
|
|
|
5541
5708
|
// its idle watchdog. Its end stores its queue, as at any run's end; that
|
|
5542
5709
|
// stream's late events are dropped once the run has left, and a later
|
|
5543
5710
|
// stop() waits for the stream itself (leftRunning). (A start() is refused
|
|
5544
|
-
//
|
|
5711
|
+
// once a stop() is admitted, so no new lifecycle begins beside it.)
|
|
5545
5712
|
for (const [name, run] of this.ephemeralRuns) run.settle.reject(ephemeralRunStopped(name));
|
|
5713
|
+
for (const agent of this.agents.values()) step(() => { agent.unpinTurnInputs(); });
|
|
5546
5714
|
|
|
5547
5715
|
// Admission closed when stop() began. What admitted operations start now,
|
|
5548
5716
|
// such as a surgery's config materialization, joins the set, so wait until
|
|
@@ -5572,11 +5740,9 @@ export class AgentFramework {
|
|
|
5572
5740
|
// generation and nothing reads the old fork's window again. So each live
|
|
5573
5741
|
// fork ends here as a closing fork's end disposes it, and its release
|
|
5574
5742
|
// 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).
|
|
5743
|
+
// first, as the idle sweep's does. A fork whose spawn is still waiting
|
|
5744
|
+
// for its creation to return has no binding yet, and its spawn binds
|
|
5745
|
+
// nothing once it has ended (spawnConversationAgent).
|
|
5580
5746
|
// Each is a step of its own, and each fork ends whatever its stream or
|
|
5581
5747
|
// turn is doing, as each run does above: its end settles a tool-result
|
|
5582
5748
|
// batch its stream still held and removes the stream's entries
|
|
@@ -5664,17 +5830,17 @@ export class AgentFramework {
|
|
|
5664
5830
|
// workspace: its stub names the audit record. A write that was past its
|
|
5665
5831
|
// deadline isn't waited for here, but the workspace's own stop waits for
|
|
5666
5832
|
// the mount turn it holds, bounded only by teardownTimeoutMs.
|
|
5667
|
-
this.
|
|
5833
|
+
this.guardSpillsClosed = true;
|
|
5668
5834
|
|
|
5669
5835
|
// Stop modules and both engines' MCP servers in parallel
|
|
5670
|
-
const shutdownPromises: Promise<void>[] = [this.moduleRegistry.stopAll(teardownTimeoutMs)];
|
|
5836
|
+
const shutdownPromises: Promise<void>[] = [this.moduleRegistry.stopAll(teardownTimeoutMs), waitFor(inferences)];
|
|
5671
5837
|
if (this.mcplServerRegistry) {
|
|
5672
5838
|
shutdownPromises.push(this.mcplServerRegistry.closeAll(teardownTimeoutMs));
|
|
5673
5839
|
}
|
|
5674
5840
|
shutdownPromises.push(this.closeModernMcpConnections(teardownTimeoutMs));
|
|
5675
5841
|
// A stopped framework's servers' addresses leave the reference registry,
|
|
5676
5842
|
// so no other framework's stub reads them and the registry doesn't keep
|
|
5677
|
-
// this one reachable.
|
|
5843
|
+
// this one reachable.
|
|
5678
5844
|
step(() => { for (const id of this.mcplServerConfigs.keys()) referenceRegistry.forgetServerAddress(id, this); });
|
|
5679
5845
|
// Timed-out or failed teardown retains its context and may still write.
|
|
5680
5846
|
// Sync available state best-effort, but leave its store open for a retry.
|
|
@@ -5734,8 +5900,8 @@ export class AgentFramework {
|
|
|
5734
5900
|
// No stream is left, unless its wait above ran out or its cancel threw:
|
|
5735
5901
|
// from the start of stop() none registers and no agent turn starts
|
|
5736
5902
|
// (providerAdmissionClosed, checked for every agent), and every stream
|
|
5737
|
-
// registered before was cancelled and awaited above. (A start()
|
|
5738
|
-
//
|
|
5903
|
+
// registered before was cancelled and awaited above. (A start() is
|
|
5904
|
+
// refused once a stop() is admitted, so admission stays closed.)
|
|
5739
5905
|
// What is still deferred (a message held for a turn's end whose flush
|
|
5740
5906
|
// found the target busy again, or one deferred behind its target's own
|
|
5741
5907
|
// state) would wait for a flush that may never come. Land each at its
|
|
@@ -5820,7 +5986,7 @@ export class AgentFramework {
|
|
|
5820
5986
|
}
|
|
5821
5987
|
|
|
5822
5988
|
if (this.ownsStore && teardownSucceeded) {
|
|
5823
|
-
|
|
5989
|
+
this.closeOpenManagers();
|
|
5824
5990
|
try {
|
|
5825
5991
|
this.store.close();
|
|
5826
5992
|
this.ownedStoreClosed = true;
|
|
@@ -6253,10 +6419,9 @@ export class AgentFramework {
|
|
|
6253
6419
|
}
|
|
6254
6420
|
contextManager.close();
|
|
6255
6421
|
}
|
|
6256
|
-
private closeOpenManagers():
|
|
6422
|
+
private closeOpenManagers(): void {
|
|
6257
6423
|
const alive = [...this.openManagers].flatMap((ref) => ref.deref() ?? []);
|
|
6258
6424
|
for (const contextManager of alive) this.closeManager(contextManager);
|
|
6259
|
-
return alive.length > 0;
|
|
6260
6425
|
}
|
|
6261
6426
|
/** The framework's membrane, with each complete() call passed through
|
|
6262
6427
|
* `admit` along with the model it names. */
|
|
@@ -6266,8 +6431,17 @@ export class AgentFramework {
|
|
|
6266
6431
|
const value = Reflect.get(obj, prop, receiver);
|
|
6267
6432
|
if (prop === 'complete' && typeof value === 'function') {
|
|
6268
6433
|
return (...args: unknown[]) => {
|
|
6269
|
-
const
|
|
6270
|
-
|
|
6434
|
+
const config = (args[0] as { config?: { model?: unknown; maxTokens?: unknown } } | undefined)?.config;
|
|
6435
|
+
const model = config?.model;
|
|
6436
|
+
const options = args[1] as { progressTimeoutMs?: unknown } | undefined;
|
|
6437
|
+
const bounds = this.inferenceBounds;
|
|
6438
|
+
const sent = bounds !== null && options?.progressTimeoutMs === undefined
|
|
6439
|
+
? [args[0], {
|
|
6440
|
+
...options,
|
|
6441
|
+
progressTimeoutMs: wholeCallMs(bounds, typeof config?.maxTokens === 'number' ? config.maxTokens : undefined),
|
|
6442
|
+
}, ...args.slice(2)]
|
|
6443
|
+
: args;
|
|
6444
|
+
return admit(() => Reflect.apply(value, this.membrane, sent) as Promise<unknown>,
|
|
6271
6445
|
typeof model === 'string' ? model : undefined);
|
|
6272
6446
|
};
|
|
6273
6447
|
}
|
|
@@ -6392,7 +6566,7 @@ export class AgentFramework {
|
|
|
6392
6566
|
// length: an operator release of it returns the hold to AF's own pacing.
|
|
6393
6567
|
...(pacing?.stated ? { statedWait: { model: agent.model, ownUntil: now + pacing.ownMs } } : {}),
|
|
6394
6568
|
...(auxiliary && (!existing || existing.auxiliaryOrigin) ? { auxiliaryOrigin: true } : {}),
|
|
6395
|
-
...(existing?.
|
|
6569
|
+
...(existing?.emptiedBy && held.length === 0 ? { emptiedBy: existing.emptiedBy } : {}) });
|
|
6396
6570
|
if (auxiliary && !existing) {
|
|
6397
6571
|
console.error(`[provider-cooldown] agent=${agent.name} host hold (${hostHold!.reason}) from an auxiliary call — ` +
|
|
6398
6572
|
`holding primary/auxiliary for ${delayMs}ms`);
|
|
@@ -6407,11 +6581,22 @@ export class AgentFramework {
|
|
|
6407
6581
|
if (wait && trigger && (wait.until === null || wait.until > now)) this.noteStatedWaitRun(agent.name, wait, now);
|
|
6408
6582
|
return true;
|
|
6409
6583
|
}
|
|
6410
|
-
private
|
|
6584
|
+
private takeAsTransient(agentName: string, error: Error, trigger: InferenceRequest | undefined): boolean {
|
|
6585
|
+
if (isTransientFailure(error)) {
|
|
6586
|
+
this.rearmDeadlineCuts.delete(agentName);
|
|
6587
|
+
return true;
|
|
6588
|
+
}
|
|
6589
|
+
if (!isRearmed(trigger) || !isDeadlineExpiry(error)) return false;
|
|
6590
|
+
const first = !this.rearmDeadlineCuts.has(agentName);
|
|
6591
|
+
this.rearmDeadlineCuts.add(agentName);
|
|
6592
|
+
return first;
|
|
6593
|
+
}
|
|
6594
|
+
|
|
6595
|
+
private rearmExhaustedTurn(agent: Agent, error: Error, trigger: InferenceRequest | undefined, ownsProviderGate: boolean, routing: TurnRouting | undefined, asTransient: boolean): void {
|
|
6411
6596
|
if (!ownsProviderGate || trigger === undefined || this.providerAdmissionClosed || this.turnRearmMs.length === 0) return;
|
|
6412
6597
|
const done = this.turnRearms.get(agent.name) ?? 0;
|
|
6413
6598
|
const notSent = (why: string) => console.error(`[inference-rearm] agent=${agent.name} turn not re-armed: ${why}`);
|
|
6414
|
-
if (!
|
|
6599
|
+
if (!asTransient) {
|
|
6415
6600
|
if (done > 0) notSent(`its failure isn't transient (${isMembraneError(error) ? error.type : boundFailureText(error.message, FAILURE_SUMMARY_MAX_CHARS)})`);
|
|
6416
6601
|
return;
|
|
6417
6602
|
}
|
|
@@ -6551,12 +6736,15 @@ export class AgentFramework {
|
|
|
6551
6736
|
? `no longer holds primary turns: the primary model is now ${model} (the wait stays recorded)`
|
|
6552
6737
|
: `primary hold ended after ${Date.now() - cooldown.startedAt}ms`));
|
|
6553
6738
|
}
|
|
6554
|
-
if ((cooldown.auxiliaryOrigin || cooldown.
|
|
6739
|
+
if ((cooldown.auxiliaryOrigin || cooldown.emptiedBy) && cooldown.heldRequests.length === 0) {
|
|
6555
6740
|
// 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
|
-
|
|
6741
|
+
// continuation since dropped with its turn, or wakes since withdrawn:
|
|
6742
|
+
// just reopen the gate. A primary the hold left pending won't run, so
|
|
6743
|
+
// its recovery ends.
|
|
6744
|
+
const which = cooldown.emptiedBy === 'drop' ? 'hold, its held continuation dropped,'
|
|
6745
|
+
: cooldown.emptiedBy === 'withdrawal' ? 'hold, its held wakes withdrawn,' : 'auxiliary hold';
|
|
6746
|
+
console.error(`[provider-cooldown] agent=${agentName} ${which} released after ${Date.now() - cooldown.startedAt}ms`);
|
|
6747
|
+
if (cooldown.emptiedBy) this.endPendingPrimary(agentName);
|
|
6560
6748
|
this.flushAuxiliaryAdmission(agentName);
|
|
6561
6749
|
return;
|
|
6562
6750
|
}
|
|
@@ -7212,12 +7400,7 @@ export class AgentFramework {
|
|
|
7212
7400
|
*/
|
|
7213
7401
|
pushEvent(event: ProcessEvent): void {
|
|
7214
7402
|
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}`);
|
|
7403
|
+
console.error(droppedToolResultLine(event, 'after stop'));
|
|
7221
7404
|
return;
|
|
7222
7405
|
}
|
|
7223
7406
|
this.queue.push(event);
|
|
@@ -7247,6 +7430,14 @@ export class AgentFramework {
|
|
|
7247
7430
|
else this.queue.push(event);
|
|
7248
7431
|
}
|
|
7249
7432
|
|
|
7433
|
+
private dropRetiredModuleEvent(event: ProcessEvent, moduleName: string): void {
|
|
7434
|
+
if (this.queue.isClosed) {
|
|
7435
|
+
this.pushModuleEvent(event, moduleName, true);
|
|
7436
|
+
return;
|
|
7437
|
+
}
|
|
7438
|
+
console.error(retiredPushLine(event, moduleName));
|
|
7439
|
+
}
|
|
7440
|
+
|
|
7250
7441
|
/**
|
|
7251
7442
|
* Add a trace event listener for observability.
|
|
7252
7443
|
*/
|
|
@@ -7298,12 +7489,17 @@ export class AgentFramework {
|
|
|
7298
7489
|
}
|
|
7299
7490
|
|
|
7300
7491
|
/**
|
|
7301
|
-
* Add a module at runtime. Refused, with a rejection,
|
|
7302
|
-
*
|
|
7303
|
-
*
|
|
7304
|
-
* past it
|
|
7492
|
+
* Add a module at runtime. Refused, with a rejection, once a stop() has
|
|
7493
|
+
* been admitted, before anything else is checked: while that stop() runs,
|
|
7494
|
+
* a module added then would either be stopped by it or, once it has taken
|
|
7495
|
+
* its list of modules to stop, stay started past it; once it has settled,
|
|
7496
|
+
* the framework has stopped for good, as start() says. A stop() admitted
|
|
7497
|
+
* while the module's start() runs stops the module once that start() has
|
|
7498
|
+
* settled; if that start() fulfils, this rejects the same way then, with
|
|
7499
|
+
* no `module:added`, and if it rejects, this rejects with its error.
|
|
7305
7500
|
*/
|
|
7306
7501
|
async addModule(module: Module): Promise<void> {
|
|
7502
|
+
this.refuseOnceStopped('addModule()');
|
|
7307
7503
|
// A server's tool prefix and a module's name share one namespace, as
|
|
7308
7504
|
// create() checks at startup: a module isn't added under a prefix a
|
|
7309
7505
|
// server holds, or one a restart is moving its server to.
|
|
@@ -7321,8 +7517,8 @@ export class AgentFramework {
|
|
|
7321
7517
|
throw new Error(`Module "${module.name}" collides with MCPL server "${id}"'s toolPrefix "${module.name}" (a closed tab)`);
|
|
7322
7518
|
}
|
|
7323
7519
|
}
|
|
7324
|
-
this.resetCompletedStop('addModule()');
|
|
7325
7520
|
await this.moduleRegistry.addModule(module);
|
|
7521
|
+
this.refuseOnceStopped('addModule()');
|
|
7326
7522
|
// The workspace judges whether an agent's history holds a message as the
|
|
7327
7523
|
// framework judges its own notices, for its head-move notice
|
|
7328
7524
|
// (noticeHeadMoves) and the writes it weighs against it. A module taken
|
|
@@ -7337,7 +7533,9 @@ export class AgentFramework {
|
|
|
7337
7533
|
}
|
|
7338
7534
|
|
|
7339
7535
|
/**
|
|
7340
|
-
* Remove a module at runtime.
|
|
7536
|
+
* Remove a module at runtime. A module whose start() is still running is
|
|
7537
|
+
* stopped once that start() has settled; this waits for that and for its
|
|
7538
|
+
* stop(), with no time limit.
|
|
7341
7539
|
*/
|
|
7342
7540
|
async removeModule(name: string): Promise<void> {
|
|
7343
7541
|
await this.moduleRegistry.removeModule(name);
|
|
@@ -7686,7 +7884,7 @@ export class AgentFramework {
|
|
|
7686
7884
|
];
|
|
7687
7885
|
}
|
|
7688
7886
|
|
|
7689
|
-
/** The registered workspace module, if any (used by
|
|
7887
|
+
/** The registered workspace module, if any (used by save_recent_image). */
|
|
7690
7888
|
private getWorkspaceModule(): WorkspaceModule | undefined {
|
|
7691
7889
|
const module = this.moduleRegistry.getAllModules().find((m) => m.name === 'workspace');
|
|
7692
7890
|
return module && typeof (module as WorkspaceModule).writeBinary === 'function'
|
|
@@ -8165,15 +8363,11 @@ export class AgentFramework {
|
|
|
8165
8363
|
private stallSkips = 0;
|
|
8166
8364
|
/** See WITHHELD_SPILL_DEADLINE_MS; an instance field so tests can shorten it. */
|
|
8167
8365
|
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;
|
|
8366
|
+
/** Set once a stop() has let the running annotations finish, and cleared
|
|
8367
|
+
* as each stop() begins: while set, a withheld original is no longer
|
|
8368
|
+
* written to a workspace that is stopping, and its stub names the audit
|
|
8369
|
+
* record. */
|
|
8370
|
+
private guardSpillsClosed = false;
|
|
8177
8371
|
|
|
8178
8372
|
/** A new agent's guard: its persisted setting, the tool-results spill its
|
|
8179
8373
|
* withheld originals are written to, and the set stop() waits on
|
|
@@ -8904,7 +9098,7 @@ export class AgentFramework {
|
|
|
8904
9098
|
// Bounded grace for cancelled streams. Stop cancels streams too; a turn
|
|
8905
9099
|
// with no stream settles on its own. Report the abandon that already ran,
|
|
8906
9100
|
// even if stop cuts its grace short.
|
|
8907
|
-
const grace = Date.now() +
|
|
9101
|
+
const grace = Date.now() + (testAbandonGraces.get(this) ?? ABANDON_GRACE_MS);
|
|
8908
9102
|
while (this.quiesced && !this.queue?.isClosed && this.activeTurnTokens.size > 0 && Date.now() < grace) {
|
|
8909
9103
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
8910
9104
|
}
|
|
@@ -9334,8 +9528,9 @@ export class AgentFramework {
|
|
|
9334
9528
|
* memory-only: they land within the turn, at a tool boundary or at its
|
|
9335
9529
|
* end, and a crash before then loses them. That window runs to the turn's
|
|
9336
9530
|
* end for a message held there and what is queued behind it, and, on a
|
|
9337
|
-
* stream that
|
|
9338
|
-
* prefill path
|
|
9531
|
+
* stream that doesn't declare it carries injected messages (membrane's XML
|
|
9532
|
+
* prefill path declares it can't), for every message deferred during the
|
|
9533
|
+
* turn.
|
|
9339
9534
|
*
|
|
9340
9535
|
* A write that fails, or a queue over the cap, which writes nothing,
|
|
9341
9536
|
* leaves the record as it last held it, except that store only, a write
|
|
@@ -9353,20 +9548,21 @@ export class AgentFramework {
|
|
|
9353
9548
|
* itself before it changes the host's mode, and refuses instead, unless
|
|
9354
9549
|
* the call has `abandon` (writeDeferredWritesForQuiesce).
|
|
9355
9550
|
*/
|
|
9356
|
-
private persistDeferredWrites():
|
|
9551
|
+
private persistDeferredWrites(): boolean {
|
|
9357
9552
|
let outcome: DeferredWritesOutcome;
|
|
9358
9553
|
try {
|
|
9359
9554
|
outcome = this.writeDeferredWrites(this.quiesced);
|
|
9360
9555
|
} catch (err) {
|
|
9361
9556
|
console.error('[host-mode] failed to persist deferred context writes:', err);
|
|
9362
9557
|
this.deferredWritesFellBehind({ error: err });
|
|
9363
|
-
return;
|
|
9558
|
+
return false;
|
|
9364
9559
|
}
|
|
9365
9560
|
if (outcome === 'over-cap') {
|
|
9366
9561
|
this.deferredWritesFellBehind('over-cap');
|
|
9367
|
-
|
|
9368
|
-
this.deferredWritesCaughtUp();
|
|
9562
|
+
return false;
|
|
9369
9563
|
}
|
|
9564
|
+
if (outcome !== 'nothing') this.deferredWritesCaughtUp();
|
|
9565
|
+
return true;
|
|
9370
9566
|
}
|
|
9371
9567
|
|
|
9372
9568
|
/**
|
|
@@ -10757,8 +10953,9 @@ export class AgentFramework {
|
|
|
10757
10953
|
* **Inheritance.** `mode: 'shared'` (the default) gives the child the
|
|
10758
10954
|
* parent's messages and fold state through shared storage: nothing is
|
|
10759
10955
|
* copied, the child folds with the parent's memory tree, and neither can
|
|
10760
|
-
* alter what the other reads. It needs
|
|
10761
|
-
*
|
|
10956
|
+
* alter what the other reads. It needs a store that supports derivation
|
|
10957
|
+
* (`ContextManager.supportsDerivation`), and is refused, not silently
|
|
10958
|
+
* downgraded, without one. `mode: 'copy'`
|
|
10762
10959
|
* is the historical mechanism — the parent's compiled context re-added as
|
|
10763
10960
|
* the child's own messages — and works everywhere.
|
|
10764
10961
|
*
|
|
@@ -11347,6 +11544,7 @@ export class AgentFramework {
|
|
|
11347
11544
|
if (run) {
|
|
11348
11545
|
run.endReason ??= 'stopped';
|
|
11349
11546
|
run.endedBy ??= options.by;
|
|
11547
|
+
run.endedFromOutside = true;
|
|
11350
11548
|
// The awaiting caller is told who stopped the job and why, whether or
|
|
11351
11549
|
// not a stream was in flight: settled first, so the aborted stream's own
|
|
11352
11550
|
// settle ('Stream cancelled') finds the run already settled. The caller's
|
|
@@ -11597,13 +11795,14 @@ export class AgentFramework {
|
|
|
11597
11795
|
/**
|
|
11598
11796
|
* The creation ("consent") event: names what kind of agent this is, what
|
|
11599
11797
|
* ends it and what happens to it when its spawner ends. A derived agent's
|
|
11600
|
-
* creation is
|
|
11601
|
-
*
|
|
11602
|
-
* host-framed line in the source's context,
|
|
11603
|
-
*
|
|
11604
|
-
*
|
|
11605
|
-
* fork's branch or copy exists, so a fork never
|
|
11606
|
-
* itself; a source that has ended is told
|
|
11798
|
+
* creation is told to the agent it was derived from only when the host
|
|
11799
|
+
* asks for it with a derivationNotice and that returns text: one
|
|
11800
|
+
* host-framed line in the source's context, with no wake, given the
|
|
11801
|
+
* framework's line built from the same facts as the trace. Without one the
|
|
11802
|
+
* source is told nothing; the trace is emitted either way. Both derivation
|
|
11803
|
+
* paths announce after the fork's branch or copy exists, so a fork never
|
|
11804
|
+
* inherits the notice about itself; a source that has ended is told
|
|
11805
|
+
* nothing (lifecycleNotice).
|
|
11607
11806
|
*/
|
|
11608
11807
|
private announceAgentCreated(record: AgentRecord): void {
|
|
11609
11808
|
this.emitTrace({
|
|
@@ -11620,7 +11819,8 @@ export class AgentFramework {
|
|
|
11620
11819
|
...(record.inherit ? { inherit: record.inherit } : {}),
|
|
11621
11820
|
});
|
|
11622
11821
|
const from = record.inherit?.from;
|
|
11623
|
-
if (!from || from === record.name) return;
|
|
11822
|
+
if (!from || from === record.name || !this.derivationNoticeHook) return;
|
|
11823
|
+
const hook = this.derivationNoticeHook;
|
|
11624
11824
|
const detail = {
|
|
11625
11825
|
event: 'derived',
|
|
11626
11826
|
agent: record.name,
|
|
@@ -11631,15 +11831,16 @@ export class AgentFramework {
|
|
|
11631
11831
|
...(record.relationships.resultTo ? { resultTo: record.relationships.resultTo } : {}),
|
|
11632
11832
|
lifetime: record.lifetime,
|
|
11633
11833
|
};
|
|
11634
|
-
//
|
|
11635
|
-
// past the threshold, a reader loop that
|
|
11636
|
-
// would fill its source's context with
|
|
11637
|
-
// the
|
|
11638
|
-
// the latest described; every derivation still emits its
|
|
11834
|
+
// With the host's hook: a resumption is rare and always passed to it. A
|
|
11835
|
+
// first incarnation is tallied: past the threshold, a reader loop that
|
|
11836
|
+
// forks a hundred times a session would fill its source's context with
|
|
11837
|
+
// stamps, so the hook is given a count line every `threshold` forks
|
|
11838
|
+
// instead, with the latest described; every derivation still emits its
|
|
11839
|
+
// trace.
|
|
11639
11840
|
const threshold = this.collapseDerivationNoticesAfter;
|
|
11640
11841
|
const tell = (defaultText: string, extra: Record<string, unknown> = {}) => {
|
|
11641
11842
|
try {
|
|
11642
|
-
const text =
|
|
11843
|
+
const text = hook(record, from, defaultText);
|
|
11643
11844
|
if (text !== null) this.lifecycleNotice(from, text, { ...detail, ...extra });
|
|
11644
11845
|
} catch (error) {
|
|
11645
11846
|
console.error(`[dendrite] ${record.name} was created, but ${from} could not be told:`, error);
|
|
@@ -11725,6 +11926,7 @@ export class AgentFramework {
|
|
|
11725
11926
|
if (run) {
|
|
11726
11927
|
run.endReason = reason;
|
|
11727
11928
|
run.endedBy = record.ended?.by;
|
|
11929
|
+
run.endedFromOutside = true;
|
|
11728
11930
|
run.settle.reject(new AgentStoppedError(name, reason, record.ended?.by, detail));
|
|
11729
11931
|
return;
|
|
11730
11932
|
}
|
|
@@ -11733,6 +11935,15 @@ export class AgentFramework {
|
|
|
11733
11935
|
this.releaseAgentRuntime(agent, this.channelOfHome(record.homeChannel), undefined, record.lifetime.kind === 'persistent');
|
|
11734
11936
|
}
|
|
11735
11937
|
|
|
11938
|
+
private forgetFailureAccounting(name: string): void {
|
|
11939
|
+
this.consecutiveInferenceFailures.delete(name);
|
|
11940
|
+
this.exhaustionRewinds.delete(name);
|
|
11941
|
+
this.refusalStreak.delete(name);
|
|
11942
|
+
this.refusalRewinds.delete(name);
|
|
11943
|
+
this.rewindEpisode.delete(name);
|
|
11944
|
+
if (this.forcedRewind.delete(name)) console.error(`[unstick] agent=${name} its /unstick session ended with its agent`);
|
|
11945
|
+
}
|
|
11946
|
+
|
|
11736
11947
|
/**
|
|
11737
11948
|
* Everything the process holds for a live, non-ephemeral agent that has
|
|
11738
11949
|
* ended, released once, on every path an agent ends by — stopAgent, the
|
|
@@ -11776,8 +11987,10 @@ export class AgentFramework {
|
|
|
11776
11987
|
this.eventGate?.agentGone(name);
|
|
11777
11988
|
this.agents.delete(name);
|
|
11778
11989
|
this.agentConfigs.delete(name);
|
|
11990
|
+
this.deadlineCuts.delete(name);
|
|
11991
|
+
this.freshCallsCut.delete(name);
|
|
11992
|
+
this.forgetFailureAccounting(name);
|
|
11779
11993
|
this.thinkingLossIncidents.forget(name);
|
|
11780
|
-
this.reasoningLoss.forget(name);
|
|
11781
11994
|
// main's manager release: the context manager an ephemeral run or a fork
|
|
11782
11995
|
// opened is released with the agent (releaseManager).
|
|
11783
11996
|
this.releaseManager(agent);
|
|
@@ -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,8 +12786,10 @@ 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
|
-
this.reasoningLoss.forget(agent.name);
|
|
12548
12793
|
// Its pending self-wake ends with it, as a fork's does.
|
|
12549
12794
|
this.eventGate?.agentGone(agent.name);
|
|
12550
12795
|
this.releaseWindowNowOrAfterResume(agent);
|
|
@@ -12717,6 +12962,15 @@ export class AgentFramework {
|
|
|
12717
12962
|
},
|
|
12718
12963
|
];
|
|
12719
12964
|
|
|
12965
|
+
private static readonly WAKE_ADD_RULE_FIELDS: Readonly<Record<string, string>> = {
|
|
12966
|
+
'behavior.debounce': 'debounceMs',
|
|
12967
|
+
'behavior.rate_limit': 'rateLimit',
|
|
12968
|
+
'behavior.passive_sample': 'passiveSample',
|
|
12969
|
+
'behavior.judge': 'judge',
|
|
12970
|
+
'position.before': 'insertBefore',
|
|
12971
|
+
'position.after': 'insertAfter',
|
|
12972
|
+
};
|
|
12973
|
+
|
|
12720
12974
|
/** Synthesized wake-rule tools: add/remove a gate.json policy at runtime.
|
|
12721
12975
|
* Present when a gate is wired. They write validated policies into the
|
|
12722
12976
|
* hot-reloaded gate.json (same validation as load), so a rule takes effect
|
|
@@ -12909,10 +13163,10 @@ export class AgentFramework {
|
|
|
12909
13163
|
/** Synthesized drafts tool — present whenever MCPL channels are
|
|
12910
13164
|
* configured. A resident's held drafts (src/prose-drafts.ts): plain speech
|
|
12911
13165
|
* 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 = {
|
|
13166
|
+
* kept as that module's retention says. Its optional fields are nullable,
|
|
13167
|
+
* as save_recent_image's selectors are: a caller whose provider presents
|
|
13168
|
+
* every property as required can still say "not in use" (fieldInUse). */
|
|
13169
|
+
private static readonly DRAFTS_TOOL: import('./types/index.js').ToolDefinition = {
|
|
12916
13170
|
name: 'drafts',
|
|
12917
13171
|
description:
|
|
12918
13172
|
'Your held drafts: plain speech of yours that was NOT sent — held back because an explicit send in its turn ' +
|
|
@@ -12951,12 +13205,12 @@ export class AgentFramework {
|
|
|
12951
13205
|
},
|
|
12952
13206
|
required: ['action'],
|
|
12953
13207
|
},
|
|
12954
|
-
}
|
|
13208
|
+
};
|
|
12955
13209
|
|
|
12956
|
-
/** Synthesized
|
|
13210
|
+
/** Synthesized save_recent_image tool — present when a workspace module is
|
|
12957
13211
|
* registered. Lets the agent persist an image it has already seen in its
|
|
12958
13212
|
* own context (Discord attachments arrive inlined as base64 and are
|
|
12959
|
-
* otherwise unreachable as files). */
|
|
13213
|
+
* otherwise unreachable as files). Its three selectors are nullable. */
|
|
12960
13214
|
private static readonly SAVE_IMAGE_TOOL: import('./types/index.js').ToolDefinition = {
|
|
12961
13215
|
name: 'save_recent_image',
|
|
12962
13216
|
description:
|
|
@@ -12972,7 +13226,8 @@ export class AgentFramework {
|
|
|
12972
13226
|
'restart) the call fails and writes nothing — it never substitutes an older ' +
|
|
12973
13227
|
'image. When other tool calls in the same batch are still running, the save ' +
|
|
12974
13228
|
'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.'
|
|
13229
|
+
'files are visible via workspace tools. Select by `ref` or by ' +
|
|
13230
|
+
'`index`/`count`; a selector you are not using may be omitted or passed as null.',
|
|
12976
13231
|
inputSchema: {
|
|
12977
13232
|
type: 'object',
|
|
12978
13233
|
properties: {
|
|
@@ -12980,19 +13235,27 @@ export class AgentFramework {
|
|
|
12980
13235
|
type: 'string',
|
|
12981
13236
|
description: 'Mount-prefixed destination path, e.g. "project/photos/name.png".',
|
|
12982
13237
|
},
|
|
13238
|
+
// Nullable selectors: a caller whose provider presents every property
|
|
13239
|
+
// as required must still be able to say "not this selector". Omission,
|
|
13240
|
+
// null and "" all mean it isn't used (fieldInUse); any other value is
|
|
13241
|
+
// checked. The descriptions name null, the form to ask for.
|
|
12983
13242
|
index: {
|
|
12984
|
-
type: '
|
|
12985
|
-
description:
|
|
13243
|
+
type: ['integer', 'null'],
|
|
13244
|
+
description:
|
|
13245
|
+
'Which image, counting back from the most recent (0 = most recent). Default 0. ' +
|
|
13246
|
+
'null, like omitting it, means the default.',
|
|
12986
13247
|
},
|
|
12987
13248
|
count: {
|
|
12988
|
-
type: '
|
|
12989
|
-
description:
|
|
13249
|
+
type: ['integer', 'null'],
|
|
13250
|
+
description:
|
|
13251
|
+
'How many images to save, starting at `index` and going further back. Default 1. ' +
|
|
13252
|
+
'null, like omitting it, means the default.',
|
|
12990
13253
|
},
|
|
12991
13254
|
ref: {
|
|
12992
|
-
type: 'string',
|
|
13255
|
+
type: ['string', 'null'],
|
|
12993
13256
|
description:
|
|
12994
13257
|
'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
|
|
13258
|
+
'Mutually exclusive with `index`/`count`: pass null for the selector you are not using. null, like omitting it, means no ref.',
|
|
12996
13259
|
},
|
|
12997
13260
|
},
|
|
12998
13261
|
required: ['path'],
|
|
@@ -15808,8 +16071,9 @@ export class AgentFramework {
|
|
|
15808
16071
|
// idiom): reasoning effort (thinking), provider params, token ceilings,
|
|
15809
16072
|
// caching, refusal handling, timezone — a same-model side-process must
|
|
15810
16073
|
// not silently run under different inference conditions than the
|
|
15811
|
-
// resident it serves.
|
|
15812
|
-
// overridden
|
|
16074
|
+
// resident it serves. Below, identity- and surface-defining fields are
|
|
16075
|
+
// overridden, and the window and thinking binding where the
|
|
16076
|
+
// subconscious's config sets its own.
|
|
15813
16077
|
const model = cfg.model ?? primaryConfig.model;
|
|
15814
16078
|
const inherited: AgentConfig = { ...primaryConfig };
|
|
15815
16079
|
if (model !== primaryConfig.model) {
|
|
@@ -15817,6 +16081,7 @@ export class AgentFramework {
|
|
|
15817
16081
|
}
|
|
15818
16082
|
const agentConfig: AgentConfig = {
|
|
15819
16083
|
...inherited,
|
|
16084
|
+
...readerModelFields(cfg),
|
|
15820
16085
|
name,
|
|
15821
16086
|
model,
|
|
15822
16087
|
systemPrompt: cfg.systemPrompt,
|
|
@@ -15924,6 +16189,7 @@ export class AgentFramework {
|
|
|
15924
16189
|
config: {
|
|
15925
16190
|
model: cfg.model,
|
|
15926
16191
|
proseRouting: 'disabled',
|
|
16192
|
+
...readerModelFields(cfg),
|
|
15927
16193
|
},
|
|
15928
16194
|
inheritRefusals: true,
|
|
15929
16195
|
onParentEnd: 'end',
|
|
@@ -16037,13 +16303,7 @@ export class AgentFramework {
|
|
|
16037
16303
|
}
|
|
16038
16304
|
|
|
16039
16305
|
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) {
|
|
16306
|
+
while (this.running) {
|
|
16047
16307
|
try {
|
|
16048
16308
|
await this.processNextEvent();
|
|
16049
16309
|
} catch (error) {
|
|
@@ -16331,18 +16591,21 @@ export class AgentFramework {
|
|
|
16331
16591
|
// compiled prefix and the live request still match without it
|
|
16332
16592
|
// (#232 gap 5).
|
|
16333
16593
|
//
|
|
16334
|
-
//
|
|
16335
|
-
// (supportsInjectedMessages
|
|
16336
|
-
//
|
|
16337
|
-
//
|
|
16338
|
-
//
|
|
16339
|
-
//
|
|
16594
|
+
// Only a stream that declares it carries injected messages
|
|
16595
|
+
// (supportsInjectedMessages true: membrane's native path) gets
|
|
16596
|
+
// them. Membrane's XML prefill path declares false, and a stream
|
|
16597
|
+
// that doesn't say is taken as one that doesn't, so nothing is
|
|
16598
|
+
// drained for either: everything queued for the agent waits for
|
|
16599
|
+
// the turn-end flush, as a held message does, rather than sitting
|
|
16600
|
+
// in the window between rounds the stream never showed it. A
|
|
16601
|
+
// boundary with no stream, the direct API's, drains: its
|
|
16602
|
+
// re-inference compiles from the store.
|
|
16340
16603
|
const midTurnInjections: Array<{ participant: string; content: ContentBlock[]; metadata?: MessageMetadata; id?: MessageId }> = [];
|
|
16341
16604
|
// What this boundary stored for the agent to hear, in store order,
|
|
16342
16605
|
// for a boundary that leaves it to the turn's next start as
|
|
16343
16606
|
// arrivals: that start's compile sends it.
|
|
16344
16607
|
const heard: Array<{ msg: DeferredWrite; id: MessageId; content: ContentBlock[] }> = [];
|
|
16345
|
-
if (currentState.supportsInjectedMessages
|
|
16608
|
+
if (currentState.supportsInjectedMessages === true || !currentState.stream) {
|
|
16346
16609
|
// A clock row due (storeDueClock) goes right after the round's
|
|
16347
16610
|
// results, ahead of what is drained below, and joins the stream
|
|
16348
16611
|
// the same way. A boundary that passed during the round can't
|
|
@@ -16443,12 +16706,10 @@ export class AgentFramework {
|
|
|
16443
16706
|
|
|
16444
16707
|
// A live stream handed these messages after the results follows
|
|
16445
16708
|
// 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
|
|
16709
|
+
// [Continue]: that round's rows carry no prompt. A boundary that
|
|
16710
|
+
// ends the turn, restarts the stream or falls back to a new start
|
|
16711
|
+
// below hands them to no live stream, and a new start reads its own
|
|
16712
|
+
// prompt from
|
|
16452
16713
|
// what it sends. (The routing notice below rides only an injection
|
|
16453
16714
|
// already here.)
|
|
16454
16715
|
if (midTurnInjections.length > 0) {
|
|
@@ -16683,11 +16944,10 @@ export class AgentFramework {
|
|
|
16683
16944
|
}
|
|
16684
16945
|
// Streaming path: convert results and resume the stream.
|
|
16685
16946
|
// 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
|
|
16947
|
+
// messages. Membrane's native path appends them after the
|
|
16948
|
+
// tool_result envelope, so the next round of THIS turn hears
|
|
16949
|
+
// them. Its XML prefill path declares it can't carry them, so
|
|
16950
|
+
// none are collected for it. Each goes as its participant and content
|
|
16691
16951
|
// alone: a stored row's metadata is the host's record, not input
|
|
16692
16952
|
// for membrane. Each goes with its name and text as the store
|
|
16693
16953
|
// keeps them, whether it's a heard message, rendered or not, or a
|
|
@@ -16718,15 +16978,11 @@ export class AgentFramework {
|
|
|
16718
16978
|
// the watermark of the turn's compile and read as unread, so a
|
|
16719
16979
|
// coalesced replacement could remove a message the agent heard
|
|
16720
16980
|
// 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.
|
|
16981
|
+
// Only a stream that declares it carries them gets them, and so
|
|
16982
|
+
// is marked read (supportsInjectedMessages true: membrane's native
|
|
16983
|
+
// path).
|
|
16984
|
+
// The shown mark moves at every boundary that injects into its
|
|
16985
|
+
// stream.
|
|
16730
16986
|
// The mark is the store's head, a scalar, as a compile's is: it
|
|
16731
16987
|
// also covers what was stored since the compile without being
|
|
16732
16988
|
// carried, such as another slot's messages, and a write that
|
|
@@ -16738,8 +16994,7 @@ export class AgentFramework {
|
|
|
16738
16994
|
// replies after it go without their thinking, whatever the mark
|
|
16739
16995
|
// says; deferring those writers is the fix.
|
|
16740
16996
|
if (midTurnInjections.length > 0) {
|
|
16741
|
-
//
|
|
16742
|
-
// mark (CoalescerHost.isUnshown) counts a doubt as shown.
|
|
16997
|
+
// Shown: the stream carries them (CoalescerHost.isUnshown).
|
|
16743
16998
|
agent.markContextShown();
|
|
16744
16999
|
if (currentState.supportsInjectedMessages === true) agent.markContextConsumed();
|
|
16745
17000
|
}
|
|
@@ -16799,14 +17054,22 @@ export class AgentFramework {
|
|
|
16799
17054
|
private async dispatchProcessEventToModules(event: ProcessEvent, startTime = Date.now()): Promise<void> {
|
|
16800
17055
|
// Dispatch to all modules, tracking responses with module names
|
|
16801
17056
|
const responses: ModuleProcessResponse[] = [];
|
|
16802
|
-
|
|
17057
|
+
const registrations = new Map<ModuleProcessResponse, object | undefined>();
|
|
17058
|
+
const stillRegistered = (entry: ModuleProcessResponse): boolean =>
|
|
17059
|
+
this.moduleRegistry.registrationOf(entry.moduleName) === registrations.get(entry);
|
|
17060
|
+
const dispatched = this.moduleRegistry.getAllModules()
|
|
17061
|
+
.map((module) => ({ module, registration: this.moduleRegistry.registrationOf(module.name) }));
|
|
17062
|
+
for (const { module, registration } of dispatched) {
|
|
16803
17063
|
// The framework's own toolsets module answers no event; every agent has
|
|
16804
17064
|
// it now, so it isn't listed beside the modules that do.
|
|
16805
17065
|
if (module instanceof ToolsetsModule) continue;
|
|
17066
|
+
if (this.moduleRegistry.registrationOf(module.name) !== registration) continue;
|
|
16806
17067
|
try {
|
|
16807
17068
|
const processState = this.moduleRegistry.createProcessState(module.name);
|
|
16808
17069
|
const response = await module.onProcess(event, processState);
|
|
16809
|
-
|
|
17070
|
+
const entry = { moduleName: module.name, response };
|
|
17071
|
+
responses.push(entry);
|
|
17072
|
+
registrations.set(entry, registration);
|
|
16810
17073
|
} catch (error) {
|
|
16811
17074
|
console.error(`Module ${module.name} error handling process event:`, error);
|
|
16812
17075
|
}
|
|
@@ -16832,7 +17095,17 @@ export class AgentFramework {
|
|
|
16832
17095
|
// responses, a tool call's dispatch and the trace go on, since a module
|
|
16833
17096
|
// can't catch a throw on this path, and one module's mustn't cost the
|
|
16834
17097
|
// event.
|
|
16835
|
-
const
|
|
17098
|
+
const dropUnregistered = (entry: ModuleProcessResponse): boolean => {
|
|
17099
|
+
if (stillRegistered(entry)) return false;
|
|
17100
|
+
const parts = responseParts(entry.response);
|
|
17101
|
+
if (parts) {
|
|
17102
|
+
console.error(
|
|
17103
|
+
`[process] module ${entry.moduleName} stopped before its response to ${event.type} was applied, so none of it was: ${parts}`,
|
|
17104
|
+
);
|
|
17105
|
+
}
|
|
17106
|
+
return true;
|
|
17107
|
+
};
|
|
17108
|
+
const applying = responses.filter((entry) => !dropUnregistered(entry)).filter(({ moduleName, response }) => {
|
|
16836
17109
|
const refusal = malformedResponseContent(response);
|
|
16837
17110
|
if (refusal === null) return true;
|
|
16838
17111
|
const { where, places } = refusal;
|
|
@@ -16855,7 +17128,7 @@ export class AgentFramework {
|
|
|
16855
17128
|
const { moduleName, response } = entry;
|
|
16856
17129
|
const step = { at: 'its start' };
|
|
16857
17130
|
try {
|
|
16858
|
-
|
|
17131
|
+
this.applyProcessResponse(response, event, moduleName, surface, step);
|
|
16859
17132
|
} catch (error) {
|
|
16860
17133
|
entry.threw = { at: step.at, error: error instanceof Error ? error.message : String(error) };
|
|
16861
17134
|
console.error(
|
|
@@ -16883,7 +17156,7 @@ export class AgentFramework {
|
|
|
16883
17156
|
}
|
|
16884
17157
|
}
|
|
16885
17158
|
|
|
16886
|
-
private
|
|
17159
|
+
private applyProcessResponse(
|
|
16887
17160
|
response: EventResponse,
|
|
16888
17161
|
event: ProcessEvent,
|
|
16889
17162
|
moduleName: string,
|
|
@@ -16891,7 +17164,7 @@ export class AgentFramework {
|
|
|
16891
17164
|
surface: InboundSource | undefined,
|
|
16892
17165
|
/** Names the operation under way, for the line when one throws. */
|
|
16893
17166
|
step: { at: string },
|
|
16894
|
-
):
|
|
17167
|
+
): void {
|
|
16895
17168
|
// Add messages. Only the framework writes the inbound envelope: one a
|
|
16896
17169
|
// module's metadata carries (echoed from a server's event, say) is
|
|
16897
17170
|
// dropped, and a surface event's messages carry the framework's own.
|
|
@@ -17695,8 +17968,10 @@ export class AgentFramework {
|
|
|
17695
17968
|
} else {
|
|
17696
17969
|
// Nothing live under the name: only what the process keeps by name.
|
|
17697
17970
|
this.closingConversationAgents.delete(agentName);
|
|
17971
|
+
this.deadlineCuts.delete(agentName);
|
|
17972
|
+
this.freshCallsCut.delete(agentName);
|
|
17973
|
+
this.forgetFailureAccounting(agentName);
|
|
17698
17974
|
this.thinkingLossIncidents.forget(agentName);
|
|
17699
|
-
this.reasoningLoss.forget(agentName);
|
|
17700
17975
|
// A continuation of its turn still on its way goes with it.
|
|
17701
17976
|
this.dropContinuations(agentName, { kind: 'dropped', why: 'its agent was disposed' });
|
|
17702
17977
|
this.turnRouting.delete(agentName);
|
|
@@ -18728,25 +19003,45 @@ export class AgentFramework {
|
|
|
18728
19003
|
}
|
|
18729
19004
|
}
|
|
18730
19005
|
|
|
18731
|
-
/** Withdraw unstarted wakes whose sole cause is this subject (§4.2)
|
|
19006
|
+
/** Withdraw unstarted wakes whose sole cause is this subject (§4.2), and
|
|
19007
|
+
* a merged start whose every request is withdrawn. */
|
|
18732
19008
|
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
|
|
19009
|
+
// A wake of the subject's merged into a start
|
|
18738
19010
|
// (MERGED_BATCH) is withdrawn from the start's batch, so no stop puts
|
|
18739
19011
|
// it back and no pass unpacks it.
|
|
19012
|
+
const emptied = new Set<InferenceRequest>();
|
|
18740
19013
|
const unrecord = (r: InferenceRequest): void => {
|
|
18741
19014
|
const batch = mergedBatch(r);
|
|
18742
19015
|
if (batch?.some((w) => w.coalescingSubject === subject)) {
|
|
18743
|
-
|
|
19016
|
+
const left = batch.filter((w) => w.coalescingSubject !== subject);
|
|
19017
|
+
(r as MarkedRequest)[MERGED_BATCH] = left;
|
|
19018
|
+
if (left.length === 0) emptied.add(r);
|
|
18744
19019
|
}
|
|
18745
19020
|
};
|
|
18746
19021
|
for (const r of this.pendingRequests) unrecord(r);
|
|
18747
19022
|
for (const cooldown of this.providerAccelerationCooldowns.values()) cooldown.heldRequests.forEach(unrecord);
|
|
18748
19023
|
for (const r of this.parkedContinuations ?? []) unrecord(r);
|
|
18749
19024
|
for (const pass of this.passRequests ?? []) for (const requests of pass.values()) requests.forEach(unrecord);
|
|
19025
|
+
const withdrawn = (r: InferenceRequest): boolean => r.coalescingSubject === subject || emptied.has(r);
|
|
19026
|
+
this.pendingRequests = this.pendingRequests.filter((r) => !withdrawn(r));
|
|
19027
|
+
for (const cooldown of this.providerAccelerationCooldowns.values()) {
|
|
19028
|
+
const held = cooldown.heldRequests.length;
|
|
19029
|
+
cooldown.heldRequests = cooldown.heldRequests.filter((r) => !withdrawn(r));
|
|
19030
|
+
if (held > 0 && cooldown.heldRequests.length === 0) cooldown.emptiedBy = 'withdrawal';
|
|
19031
|
+
}
|
|
19032
|
+
for (const pass of this.passRequests ?? []) {
|
|
19033
|
+
for (const requests of pass.values()) {
|
|
19034
|
+
if (!requests.some((r) => emptied.has(r))) continue;
|
|
19035
|
+
const left = requests.filter((r) => !emptied.has(r));
|
|
19036
|
+
requests.length = 0;
|
|
19037
|
+
requests.push(...left);
|
|
19038
|
+
}
|
|
19039
|
+
}
|
|
19040
|
+
for (const start of emptied) {
|
|
19041
|
+
if (!start.coalescingBatch || start.coalescingSubject !== undefined) continue;
|
|
19042
|
+
console.error(`[coalescing] ${start.agentName}: its deferred batches were withdrawn while its wake waited; turn not started (no wake cause left)`);
|
|
19043
|
+
this.emitTrace({ type: 'mcpl:coalescing', kind: 'turn-withdrawn', agentName: start.agentName, subjects: start.coalescingBatchSubjects ?? [] });
|
|
19044
|
+
}
|
|
18750
19045
|
}
|
|
18751
19046
|
|
|
18752
19047
|
/**
|
|
@@ -19261,7 +19556,7 @@ export class AgentFramework {
|
|
|
19261
19556
|
if (recovery) recovery.releasedAt = now;
|
|
19262
19557
|
requests = [...providerCooldown.heldRequests, ...requests];
|
|
19263
19558
|
}
|
|
19264
|
-
// A start that
|
|
19559
|
+
// A start that came back before it took its
|
|
19265
19560
|
// token stands for the batch it was merged from (MERGED_BATCH),
|
|
19266
19561
|
// including one a hold released just now. The batch takes its place,
|
|
19267
19562
|
// each request as it was, judged one by one from here: a wait holds
|
|
@@ -19662,10 +19957,10 @@ export class AgentFramework {
|
|
|
19662
19957
|
else delete start[TURN_CARRY];
|
|
19663
19958
|
if (requests.every(isRearmed)) start[REARMED] = true;
|
|
19664
19959
|
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 (
|
|
19960
|
+
// A start merged from more than one request, any of them not the
|
|
19961
|
+
// turn's, records the batch it was merged from (MERGED_BATCH): until
|
|
19962
|
+
// it takes its token it stands for them all.
|
|
19963
|
+
if (requests.length > 1 && requests.some((r) => !continuesTurn(r))) start[MERGED_BATCH] = [...requests];
|
|
19669
19964
|
else delete start[MERGED_BATCH];
|
|
19670
19965
|
await this.startAgentStream(agent, start);
|
|
19671
19966
|
}
|
|
@@ -20345,8 +20640,6 @@ export class AgentFramework {
|
|
|
20345
20640
|
const refuse = (error: string): ToolResult => ({ success: false, error, isError: true });
|
|
20346
20641
|
// Plain text, not a JSON-quoted string: draft text must read exactly as held.
|
|
20347
20642
|
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
20643
|
const action = input.action;
|
|
20351
20644
|
if (action !== 'list' && action !== 'read' && action !== 'resend' && action !== 'dismiss') {
|
|
20352
20645
|
return refuse('action must be one of "list", "read", "resend" or "dismiss".');
|
|
@@ -20355,7 +20648,7 @@ export class AgentFramework {
|
|
|
20355
20648
|
refuse(`${field} is not used by action "${action}" — pass null or omit it.`);
|
|
20356
20649
|
|
|
20357
20650
|
let ids: string[] = [];
|
|
20358
|
-
if (
|
|
20651
|
+
if (fieldInUse(input.draftIds)) {
|
|
20359
20652
|
if (action === 'list') return unused('draftIds');
|
|
20360
20653
|
if (!Array.isArray(input.draftIds) || input.draftIds.some((id) => typeof id !== 'string' || !id.trim())) {
|
|
20361
20654
|
return refuse('draftIds must be a list of draft ids such as "d-k7x3qm2", or null.');
|
|
@@ -20364,15 +20657,19 @@ export class AgentFramework {
|
|
|
20364
20657
|
if (new Set(ids).size !== ids.length) return refuse('draftIds names a draft more than once.');
|
|
20365
20658
|
if (ids.length > 20) return refuse('draftIds names more than 20 drafts; act on them in smaller groups.');
|
|
20366
20659
|
}
|
|
20367
|
-
if (
|
|
20368
|
-
|
|
20369
|
-
|
|
20660
|
+
if (fieldInUse(input.destination) && action !== 'resend') return unused('destination');
|
|
20661
|
+
// false is "don't confirm", which every other action already does, so only
|
|
20662
|
+
// true is a confirmDuplicate those actions refuse.
|
|
20663
|
+
if (fieldInUse(input.confirmDuplicate) && input.confirmDuplicate !== false && action !== 'resend') {
|
|
20664
|
+
return unused('confirmDuplicate');
|
|
20665
|
+
}
|
|
20666
|
+
if (fieldInUse(input.offset) && action !== 'list') return unused('offset');
|
|
20370
20667
|
|
|
20371
20668
|
if (action === 'list') {
|
|
20372
20669
|
let offset = 0;
|
|
20373
|
-
if (
|
|
20670
|
+
if (fieldInUse(input.offset)) {
|
|
20374
20671
|
const raw = input.offset;
|
|
20375
|
-
const n =
|
|
20672
|
+
const n = integerField(raw);
|
|
20376
20673
|
if (!Number.isInteger(n) || n < 0) return refuse('offset must be a whole number of drafts to skip (0 or more), or null.');
|
|
20377
20674
|
offset = n;
|
|
20378
20675
|
}
|
|
@@ -20489,12 +20786,12 @@ export class AgentFramework {
|
|
|
20489
20786
|
if (!registry) return refuse('No channels are configured, so there is nowhere to resend to.');
|
|
20490
20787
|
const home = this.homeOf(agentName);
|
|
20491
20788
|
let spec = typeof input.destination === 'string' ? input.destination.trim() : '';
|
|
20492
|
-
if (
|
|
20789
|
+
if (fieldInUse(input.destination) && typeof input.destination !== 'string') {
|
|
20493
20790
|
return refuse('destination must be "#channel", "@person" or a channel id.');
|
|
20494
20791
|
}
|
|
20495
20792
|
if (!spec && !home) return refuse('resend needs a destination: "#channel", "@person" or a channel id.');
|
|
20496
20793
|
let confirmDuplicate = false;
|
|
20497
|
-
if (
|
|
20794
|
+
if (fieldInUse(input.confirmDuplicate)) {
|
|
20498
20795
|
if (typeof input.confirmDuplicate !== 'boolean') return refuse('confirmDuplicate must be true, false or null.');
|
|
20499
20796
|
confirmDuplicate = input.confirmDuplicate;
|
|
20500
20797
|
}
|
|
@@ -21522,25 +21819,6 @@ export class AgentFramework {
|
|
|
21522
21819
|
this.writeChannelNotices(this.channelOpenedNotices(agentName, serverId, channels, cause), `auto-open notice (${cause})`, 'recordChannelAutoOpenNotice');
|
|
21523
21820
|
}
|
|
21524
21821
|
|
|
21525
|
-
private tellReasoningLoss(agent: Agent, make: () => string | undefined): void {
|
|
21526
|
-
if (this.agents.get(agent.name) !== agent) return;
|
|
21527
|
-
let line: string | undefined;
|
|
21528
|
-
try {
|
|
21529
|
-
line = make();
|
|
21530
|
-
} catch (err) {
|
|
21531
|
-
console.error(`[reasoning] ${agent.name}: could not tell what reasoning was lost:`, err);
|
|
21532
|
-
return;
|
|
21533
|
-
}
|
|
21534
|
-
if (!line) return;
|
|
21535
|
-
try {
|
|
21536
|
-
this.addMessage('user', [{ type: 'text', text: line }], { system: true, kind: 'reasoning-loss' } as MessageMetadata, { forAgent: agent.name });
|
|
21537
|
-
} catch (err) {
|
|
21538
|
-
console.error(`[reasoning] ${agent.name}: the line for lost reasoning could not be stored:`, err);
|
|
21539
|
-
return;
|
|
21540
|
-
}
|
|
21541
|
-
console.error(`[reasoning] ${agent.name}: told of reasoning no longer in its context`);
|
|
21542
|
-
}
|
|
21543
|
-
|
|
21544
21822
|
/**
|
|
21545
21823
|
* Write a builder's channel notices in order, each on its own: one that
|
|
21546
21824
|
* can't be written is logged, and the rest are still written. A reader's
|
|
@@ -22923,6 +23201,7 @@ export class AgentFramework {
|
|
|
22923
23201
|
attempt = 0,
|
|
22924
23202
|
providerGateAlreadyHeld = false,
|
|
22925
23203
|
): Promise<void> {
|
|
23204
|
+
if (attempt === 0) this.deadlineCuts.delete(agent.name);
|
|
22926
23205
|
const ownsProviderGate = this.registry.ownsProviderScheduling(agent.name);
|
|
22927
23206
|
// A stopping framework starts no turn, whoever owns provider admission
|
|
22928
23207
|
// (conversation agents and ephemeral runs do not take the gate).
|
|
@@ -22989,6 +23268,15 @@ export class AgentFramework {
|
|
|
22989
23268
|
console.error(`[provider-admission] ${agent.name}: host quiesced while parked — the merged start goes back as its ${batch.length} requests`);
|
|
22990
23269
|
return;
|
|
22991
23270
|
}
|
|
23271
|
+
if (this.providerAccelerationCooldowns.has(agent.name)) {
|
|
23272
|
+
this.releasePrimaryProviderGate(agent.name);
|
|
23273
|
+
this.eventGate?.onInferenceEnded(agent.name);
|
|
23274
|
+
this.pendingRequests.push(trigger ?? {
|
|
23275
|
+
agentName: agent.name, reason: 'provider-admission:requeue', source: 'scheduler', timestamp: Date.now(),
|
|
23276
|
+
});
|
|
23277
|
+
console.error(`[provider-admission] ${agent.name}: a provider hold stands — parked wake requeued for the scheduler, not started`);
|
|
23278
|
+
return;
|
|
23279
|
+
}
|
|
22992
23280
|
await this.startAgentStream(agent, trigger, attempt, true);
|
|
22993
23281
|
}).catch((error) => {
|
|
22994
23282
|
this.releasePrimaryProviderGate(agent.name);
|
|
@@ -23147,7 +23435,7 @@ export class AgentFramework {
|
|
|
23147
23435
|
if (hold) {
|
|
23148
23436
|
const held = hold.heldRequests.length;
|
|
23149
23437
|
hold.heldRequests = take(hold.heldRequests);
|
|
23150
|
-
if (held > 0 && hold.heldRequests.length === 0) hold.
|
|
23438
|
+
if (held > 0 && hold.heldRequests.length === 0) hold.emptiedBy = 'drop';
|
|
23151
23439
|
}
|
|
23152
23440
|
for (const parked of [...this.parkedContinuations]) {
|
|
23153
23441
|
if (!isContinuation(parked)) continue;
|
|
@@ -23260,9 +23548,8 @@ export class AgentFramework {
|
|
|
23260
23548
|
* gets inference:aborted with `reason`, and lastInference records its end
|
|
23261
23549
|
* as driveStream's would, when the record is still this turn's. Nothing of
|
|
23262
23550
|
* the kind when another turn of this agent's name holds the turn token (one
|
|
23263
|
-
* begun
|
|
23264
|
-
*
|
|
23265
|
-
* newer turn instead.
|
|
23551
|
+
* begun by a replacement agent object): the event and the record carry only
|
|
23552
|
+
* the name, so this frame's would end that newer turn instead.
|
|
23266
23553
|
*/
|
|
23267
23554
|
private abortDroppedTurn(agentName: string, turnToken: number, reason: string, startedAt: number): void {
|
|
23268
23555
|
const owner = this.activeTurnTokens.get(agentName);
|
|
@@ -23451,7 +23738,6 @@ export class AgentFramework {
|
|
|
23451
23738
|
* operator's abandon. */
|
|
23452
23739
|
checkWindow: CheckWindow,
|
|
23453
23740
|
): Promise<boolean> {
|
|
23454
|
-
const lifecycle = this.lifecycleGeneration;
|
|
23455
23741
|
const resumesTurn = resumesLogicalTurn(trigger?.reason);
|
|
23456
23742
|
let carry = turnCarryOf(trigger);
|
|
23457
23743
|
const turnProseRouting = trigger?.suppressProse ? 'disabled' : agent.proseRouting;
|
|
@@ -23639,7 +23925,7 @@ export class AgentFramework {
|
|
|
23639
23925
|
// everything the turn has decided about where its speech goes.
|
|
23640
23926
|
// - A resume (a budget restart, a tool-result-guard retry) carries its
|
|
23641
23927
|
// turn's request on and keeps the turn's object as it is, and the
|
|
23642
|
-
// turn stays, or
|
|
23928
|
+
// turn stays, or becomes again, the agent's open turn.
|
|
23643
23929
|
// - A silent start routes no prose. It gets an object of its own, with
|
|
23644
23930
|
// no locus, so it neither takes nor disturbs an open turn's. Its
|
|
23645
23931
|
// object is the agent's live routing while it runs, and the open
|
|
@@ -23664,8 +23950,6 @@ export class AgentFramework {
|
|
|
23664
23950
|
let opensTurn = false;
|
|
23665
23951
|
if (resumesTurn) {
|
|
23666
23952
|
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
23953
|
if (!trigger?.suppressProse && !this.openLogical.has(agent.name)) this.openLogical.set(agent.name, routing);
|
|
23670
23954
|
} else if (trigger?.suppressProse) {
|
|
23671
23955
|
const continued = continuesTurn(trigger) ? this.turnRouting.get(agent.name) : undefined;
|
|
@@ -23687,6 +23971,8 @@ export class AgentFramework {
|
|
|
23687
23971
|
if (opensTurn) {
|
|
23688
23972
|
const head = agent.branchHead();
|
|
23689
23973
|
if (head) routing.start = head;
|
|
23974
|
+
if (!routing.silent) this.pinTurnInputs(agent, routing);
|
|
23975
|
+
else agent.unpinTurnInputs();
|
|
23690
23976
|
}
|
|
23691
23977
|
this.turnRouting.set(agent.name, routing);
|
|
23692
23978
|
if (opensTurn) carry = undefined;
|
|
@@ -23821,11 +24107,11 @@ export class AgentFramework {
|
|
|
23821
24107
|
});
|
|
23822
24108
|
// A budget restart continues the same logical inference window. The
|
|
23823
24109
|
// 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
|
|
24110
|
+
// and the successor takes it again if it ended in between: at a
|
|
23826
24111
|
// provider hold that kept the successor from starting (a stated wait at
|
|
23827
|
-
// its last admission check,
|
|
23828
|
-
//
|
|
24112
|
+
// its last admission check, a hold standing when its park behind an
|
|
24113
|
+
// auxiliary call ended, or a cooldown after its own call failed), whose
|
|
24114
|
+
// end queues it again.
|
|
23829
24115
|
if (!resumesTurn) {
|
|
23830
24116
|
this.eventGate?.onInferenceStarted(agent.name);
|
|
23831
24117
|
} else {
|
|
@@ -23872,10 +24158,6 @@ export class AgentFramework {
|
|
|
23872
24158
|
const name = agent.name;
|
|
23873
24159
|
agent.onCompiledToSend = (messages) => hub.observeCompile(name, messages);
|
|
23874
24160
|
}
|
|
23875
|
-
if (!agent.onThinkingStrip) {
|
|
23876
|
-
agent.onThinkingStrip = (strip, branch) => this.tellReasoningLoss(agent, () => this.reasoningLoss.forStrip(
|
|
23877
|
-
agent.name, branch, strip, (this.activeTurnTokens.has(agent.name) && this.turnRouting.get(agent.name)) || {}, agent.getContextManager()));
|
|
23878
|
-
}
|
|
23879
24161
|
|
|
23880
24162
|
// Module gatherContext (fail-open, per-module timeout — the module's
|
|
23881
24163
|
// contextTimeoutMs, else the registry default). Injections are
|
|
@@ -23913,13 +24195,11 @@ export class AgentFramework {
|
|
|
23913
24195
|
// An ephemeral watchdog may dispose this Agent while hooks/compile await,
|
|
23914
24196
|
// and stop() may close provider admission. Do not create a provider
|
|
23915
24197
|
// 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) {
|
|
24198
|
+
// as after the compile: these come before a stop kept for the turn,
|
|
24199
|
+
// which goes with it.
|
|
24200
|
+
if (this.agents.get(agent.name) !== agent || this.providerAdmissionClosed) {
|
|
23920
24201
|
const why = this.agents.get(agent.name) !== agent ? 'the agent was replaced or removed'
|
|
23921
|
-
:
|
|
23922
|
-
: 'the framework was stopped and started again';
|
|
24202
|
+
: 'the framework was stopping';
|
|
23923
24203
|
const reason = `a turn assembled while ${why} is dropped before it sends anything`;
|
|
23924
24204
|
console.error(`[turn] ${agent.name}: ${reason}`);
|
|
23925
24205
|
this.endDroppedTurn(agent.name, routing);
|
|
@@ -23973,23 +24253,22 @@ export class AgentFramework {
|
|
|
23973
24253
|
// This turn's retries are the framework's (errorPolicy below), not
|
|
23974
24254
|
// membrane's inside each attempt.
|
|
23975
24255
|
membraneRetries: false,
|
|
24256
|
+
inferenceBounds: this.inferenceBounds,
|
|
23976
24257
|
});
|
|
23977
24258
|
// Each later round this stream sends is noted as it goes (noteRoundSent).
|
|
23978
24259
|
if (noteRequestSent) this.streamRequestNotes.set(stream, { note: noteRequestSent, rounds: [], stopped: false });
|
|
23979
24260
|
if (carriedBodies) this.streamCarried.set(stream, new Set(carriedBodies));
|
|
23980
24261
|
// 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.
|
|
24262
|
+
// deadline), so check ownership and admission again.
|
|
23983
24263
|
// Membrane's yielding stream starts its request when it is first
|
|
23984
24264
|
// iterated, so cancelling it here, before driveStream, sends nothing.
|
|
23985
24265
|
// A turn that was mid-compile when stop() cancelled every stream would
|
|
23986
24266
|
// otherwise register after that pass and outlive stop(). The caller's
|
|
23987
24267
|
// finally releases the turn token.
|
|
23988
24268
|
const stopping = this.providerAdmissionClosed;
|
|
23989
|
-
if (this.agents.get(agent.name) !== agent || stopping
|
|
24269
|
+
if (this.agents.get(agent.name) !== agent || stopping) {
|
|
23990
24270
|
const why = this.agents.get(agent.name) !== agent ? 'the agent was replaced or removed'
|
|
23991
|
-
:
|
|
23992
|
-
: 'the framework was stopped and started again';
|
|
24271
|
+
: 'the framework was stopping';
|
|
23993
24272
|
const reason = `a turn compiled while ${why} is dropped before it sends anything`;
|
|
23994
24273
|
console.error(`[turn] ${agent.name}: ${reason}`);
|
|
23995
24274
|
this.cancelUnsentStream(agent, stream, `compiled while ${why}`);
|
|
@@ -24120,14 +24399,12 @@ export class AgentFramework {
|
|
|
24120
24399
|
// catch never holds a compiled request.) As at the checks after the
|
|
24121
24400
|
// hooks and after the compile, a replaced agent or a stopping framework
|
|
24122
24401
|
// 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.
|
|
24402
|
+
// it. A failed start with no stop goes on below as before.
|
|
24125
24403
|
const stopAtFailure = this.takeStartStop(agent, checkWindow);
|
|
24126
24404
|
if (stopAtFailure) {
|
|
24127
|
-
if (this.agents.get(agent.name) !== agent || this.providerAdmissionClosed
|
|
24405
|
+
if (this.agents.get(agent.name) !== agent || this.providerAdmissionClosed) {
|
|
24128
24406
|
const why = this.agents.get(agent.name) !== agent ? 'the agent was replaced or removed'
|
|
24129
|
-
:
|
|
24130
|
-
: 'the framework was stopped and started again';
|
|
24407
|
+
: 'the framework was stopping';
|
|
24131
24408
|
const reason = `a turn whose request couldn't be built while ${why} is dropped before it sends anything`;
|
|
24132
24409
|
console.error(`[turn] ${agent.name}: ${reason}`);
|
|
24133
24410
|
this.endDroppedTurn(agent.name, routing);
|
|
@@ -24140,6 +24417,7 @@ export class AgentFramework {
|
|
|
24140
24417
|
}
|
|
24141
24418
|
|
|
24142
24419
|
const retry = trigger && withTurnCarry(trigger, carry);
|
|
24420
|
+
const asTransient = this.takeAsTransient(agent.name, err, trigger);
|
|
24143
24421
|
if (ownsProviderGate && this.holdProviderAcceleration(agent, err, retry)) {
|
|
24144
24422
|
// Capacity scheduling is not poisoned history. The cooldown timer owns
|
|
24145
24423
|
// one later fresh compile; no immediate retry or exhaustion marker.
|
|
@@ -24156,24 +24434,25 @@ export class AgentFramework {
|
|
|
24156
24434
|
return false;
|
|
24157
24435
|
}
|
|
24158
24436
|
|
|
24159
|
-
const
|
|
24160
|
-
|
|
24437
|
+
const cuts = isDeadlineExpiry(err) ? this.deadlineCuts.get(agent.name) ?? 0 : undefined;
|
|
24438
|
+
const action: ErrorAction = statedWait === 'fail' || (isRearmed(trigger) && (!gotPartway(routing) || (isDeadlineExpiry(err) && !asTransient)))
|
|
24439
|
+
? { retry: false } : this.errorPolicy.onInferenceError(err, agent.name, cuts ?? attempt);
|
|
24440
|
+
if (action.retry && cuts !== undefined) this.deadlineCuts.set(agent.name, cuts + 1);
|
|
24441
|
+
else this.deadlineCuts.delete(agent.name);
|
|
24442
|
+
if (!action.retry && cuts !== undefined && cuts > 0) this.freshCallsCut.add(agent.name);
|
|
24161
24443
|
if (action.retry) {
|
|
24162
24444
|
// A policy's delay can be a provider's retry-after: wait all of it.
|
|
24163
24445
|
await this.waitForRetry(action.delayMs, checkWindow, agent);
|
|
24164
|
-
if (this.providerAdmissionClosed
|
|
24446
|
+
if (this.providerAdmissionClosed) {
|
|
24165
24447
|
// 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
|
|
24448
|
+
// it, and a stop kept for the turn goes with it. The turn ends as
|
|
24449
|
+
// one dropped before it sends anything does: a terminal event
|
|
24450
|
+
// saying why, after the inference:failed above, and its record's
|
|
24451
|
+
// end. Nothing is written
|
|
24172
24452
|
// but the [speech-not-posted] notice; this frame still owns its
|
|
24173
24453
|
// token, since no retry frame replaced it.
|
|
24174
24454
|
this.nameUnpostedSpeech(agent, routing);
|
|
24175
|
-
this.abortDroppedTurn(agent.name, turnToken,
|
|
24176
|
-
this.providerAdmissionClosed ? RETRY_CANCELLED_BY_STOP : RETRY_ACROSS_RESTART, turnStartedAt);
|
|
24455
|
+
this.abortDroppedTurn(agent.name, turnToken, RETRY_CANCELLED_BY_STOP, turnStartedAt);
|
|
24177
24456
|
if (this.activeTurnTokens.get(agent.name) === turnToken) {
|
|
24178
24457
|
this.activeTurnTokens.delete(agent.name);
|
|
24179
24458
|
this.activeTurnTriggers.delete(agent.name);
|
|
@@ -24217,7 +24496,7 @@ export class AgentFramework {
|
|
|
24217
24496
|
speech: '',
|
|
24218
24497
|
...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: err.message }),
|
|
24219
24498
|
}, routing);
|
|
24220
|
-
if (isRearmed(trigger) &&
|
|
24499
|
+
if (isRearmed(trigger) && asTransient) this.exhaustingRearms.add(agent.name);
|
|
24221
24500
|
this.emitTrace({
|
|
24222
24501
|
type: 'inference:exhausted',
|
|
24223
24502
|
agentName: agent.name,
|
|
@@ -24237,7 +24516,7 @@ export class AgentFramework {
|
|
|
24237
24516
|
if (action.emit) {
|
|
24238
24517
|
this.pushEvent(action.emit);
|
|
24239
24518
|
}
|
|
24240
|
-
this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, routing);
|
|
24519
|
+
this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, routing, asTransient);
|
|
24241
24520
|
}
|
|
24242
24521
|
// No handoff on either failure branch: the terminal branch deleted the
|
|
24243
24522
|
// token above; the retry branch's recursive startAgentStream replaced
|
|
@@ -24869,7 +25148,7 @@ export class AgentFramework {
|
|
|
24869
25148
|
(b) => b.type !== 'tool_result'
|
|
24870
25149
|
);
|
|
24871
25150
|
} else {
|
|
24872
|
-
//
|
|
25151
|
+
// XML tool mode: this round's text and its calls.
|
|
24873
25152
|
// XML mode: persist the verbatim generation on each tool_use
|
|
24874
25153
|
// (membrane#36 round-trip fidelity). context.rawText is the
|
|
24875
25154
|
// parser's fullMatch — the exact <function_calls> block the
|
|
@@ -24881,15 +25160,12 @@ export class AgentFramework {
|
|
|
24881
25160
|
const rawXml = rawText && /^<(antml:)?function_calls>/.test(rawText)
|
|
24882
25161
|
? rawText
|
|
24883
25162
|
: 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;
|
|
25163
|
+
// Persist THIS round's text only: `roundPreamble` is the
|
|
25164
|
+
// per-round delta. The `preamble` beside it is CUMULATIVE in XML
|
|
25165
|
+
// mode: persisting it per round would store each round's text N
|
|
25166
|
+
// times and re-persist injected <function_results> as the
|
|
25167
|
+
// agent's own words.
|
|
25168
|
+
const roundText = (event.context as { roundPreamble?: string }).roundPreamble;
|
|
24893
25169
|
assistantBlocks = [];
|
|
24894
25170
|
if (roundText) {
|
|
24895
25171
|
assistantBlocks.push({ type: 'text', text: roundText });
|
|
@@ -24914,9 +25190,7 @@ export class AgentFramework {
|
|
|
24914
25190
|
|
|
24915
25191
|
// Whether this stream can carry messages injected with the round's
|
|
24916
25192
|
// 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.
|
|
25193
|
+
// prefill path. The tool boundary reads it from the agent's state.
|
|
24920
25194
|
agent.enterWaitingForTools(
|
|
24921
25195
|
calls,
|
|
24922
25196
|
stream,
|
|
@@ -24958,13 +25232,11 @@ export class AgentFramework {
|
|
|
24958
25232
|
if (liveRound) liveProseRouting = true;
|
|
24959
25233
|
// Whether this round's tool-result batch (and anything queued
|
|
24960
25234
|
// 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
|
|
25235
|
+
// again: the stream declares it (true on membrane's native path).
|
|
25236
|
+
// On the XML path, which declares false, a 'now' notice reaches
|
|
24965
25237
|
// the agent at its next compile instead.
|
|
24966
25238
|
const presentsInjections =
|
|
24967
|
-
(event.context as { supportsInjectedMessages?: boolean }).supportsInjectedMessages
|
|
25239
|
+
(event.context as { supportsInjectedMessages?: boolean }).supportsInjectedMessages === true;
|
|
24968
25240
|
// Queued under this turn's routing, whenever it posts, and
|
|
24969
25241
|
// streamed by this stream once published.
|
|
24970
25242
|
// This round's request was answered: what it carried is heard.
|
|
@@ -25742,14 +26014,14 @@ export class AgentFramework {
|
|
|
25742
26014
|
// active (native roundContent), each mid-turn round's prose was
|
|
25743
26015
|
// already delivered when that round yielded — only the TRAILING
|
|
25744
26016
|
// prose (blocks after the last tool round) remains here. On the
|
|
25745
|
-
//
|
|
25746
|
-
//
|
|
26017
|
+
// XML tool path (no roundContent, so nothing was live-routed)
|
|
26018
|
+
// the historical behavior applies:
|
|
25747
26019
|
// route the whole turn's parsed segments, once, now. The global
|
|
25748
26020
|
// speech/thoughts split is left untouched (module/TUI rendering
|
|
25749
26021
|
// unaffected) — this only governs what reaches the channel.
|
|
25750
26022
|
//
|
|
25751
26023
|
// Native round routing uses the live sticky flag, which a fresh
|
|
25752
|
-
// injected message can reset. The
|
|
26024
|
+
// injected message can reset. The XML tool path has no
|
|
25753
26025
|
// reliable round boundaries, so retain its historical turn-wide
|
|
25754
26026
|
// scan to avoid double-posting.
|
|
25755
26027
|
const toolCalls = response.content
|
|
@@ -25985,6 +26257,7 @@ export class AgentFramework {
|
|
|
25985
26257
|
|
|
25986
26258
|
this.abandonAgentScripts(agent.name, 'stream error', streamRouting);
|
|
25987
26259
|
const retry = retryOf(trigger);
|
|
26260
|
+
const asTransient = this.takeAsTransient(agent.name, err, trigger);
|
|
25988
26261
|
|
|
25989
26262
|
// A hold takes the turn's retry, or, once stop() has closed
|
|
25990
26263
|
// admission, the turn ends here (endFailureAtStop).
|
|
@@ -26000,8 +26273,12 @@ export class AgentFramework {
|
|
|
26000
26273
|
break;
|
|
26001
26274
|
}
|
|
26002
26275
|
|
|
26003
|
-
const
|
|
26004
|
-
|
|
26276
|
+
const cuts = isDeadlineExpiry(err) ? this.deadlineCuts.get(agent.name) ?? 0 : undefined;
|
|
26277
|
+
const action: ErrorAction = statedWait === 'fail' || (isRearmed(trigger) && (!gotPartway(streamRouting) || (isDeadlineExpiry(err) && !asTransient)))
|
|
26278
|
+
? { retry: false } : this.errorPolicy.onInferenceError(err, agent.name, cuts ?? attempt);
|
|
26279
|
+
if (action.retry && cuts !== undefined) this.deadlineCuts.set(agent.name, cuts + 1);
|
|
26280
|
+
else this.deadlineCuts.delete(agent.name);
|
|
26281
|
+
if (!action.retry && cuts !== undefined && cuts > 0) this.freshCallsCut.add(agent.name);
|
|
26005
26282
|
if (action.retry) {
|
|
26006
26283
|
// A stop made during the wait reaches no stream: the check after
|
|
26007
26284
|
// the wait ends the turn, as the guard's check after its wait
|
|
@@ -26058,7 +26335,7 @@ export class AgentFramework {
|
|
|
26058
26335
|
speech: '',
|
|
26059
26336
|
...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: err.message }),
|
|
26060
26337
|
}, this.agents.get(agent.name) === agent && agent.streamId === myStreamId ? streamRouting : undefined, streamRouting);
|
|
26061
|
-
if (isRearmed(trigger) &&
|
|
26338
|
+
if (isRearmed(trigger) && asTransient) this.exhaustingRearms.add(agent.name);
|
|
26062
26339
|
this.emitTrace({
|
|
26063
26340
|
type: 'inference:exhausted',
|
|
26064
26341
|
agentName: agent.name,
|
|
@@ -26076,7 +26353,7 @@ export class AgentFramework {
|
|
|
26076
26353
|
if (action.emit) {
|
|
26077
26354
|
this.pushEvent(action.emit);
|
|
26078
26355
|
}
|
|
26079
|
-
this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, streamRouting);
|
|
26356
|
+
this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, streamRouting, asTransient);
|
|
26080
26357
|
}
|
|
26081
26358
|
break;
|
|
26082
26359
|
}
|
|
@@ -26151,7 +26428,7 @@ export class AgentFramework {
|
|
|
26151
26428
|
agentName: agent.name,
|
|
26152
26429
|
error: terminal,
|
|
26153
26430
|
});
|
|
26154
|
-
this.rearmExhaustedTurn(agent, new Error(terminal), trigger, ownsProviderGate, streamRouting);
|
|
26431
|
+
this.rearmExhaustedTurn(agent, new Error(terminal), trigger, ownsProviderGate, streamRouting, false);
|
|
26155
26432
|
// Postmortem 2026-05-28 P2 #7: persist the abort to the
|
|
26156
26433
|
// inference log so future investigations can attribute the
|
|
26157
26434
|
// terminal cause without relying on live in-memory reducer
|
|
@@ -26169,6 +26446,8 @@ export class AgentFramework {
|
|
|
26169
26446
|
}
|
|
26170
26447
|
|
|
26171
26448
|
case 'usage': {
|
|
26449
|
+
this.rearmDeadlineCuts.delete(agent.name);
|
|
26450
|
+
this.deadlineCuts.delete(agent.name);
|
|
26172
26451
|
// Cumulative across the stream's rounds: replace, never sum.
|
|
26173
26452
|
latestUsage = event.usage;
|
|
26174
26453
|
// The stream's accumulated fresh input, which is what the
|
|
@@ -26276,10 +26555,6 @@ export class AgentFramework {
|
|
|
26276
26555
|
if (thinkingDrops?.recordedAfter !== undefined) {
|
|
26277
26556
|
this.noteThinkingLoss(agent.name, placedDrops, thinkingDrops.recordedAfter);
|
|
26278
26557
|
}
|
|
26279
|
-
if (thinkingDrops) {
|
|
26280
|
-
this.tellReasoningLoss(agent, () => this.reasoningLoss.forDrops(agent.name, thinkingDrops.branch ?? '',
|
|
26281
|
-
placedDrops, streamRouting, agent.getContextManager()));
|
|
26282
|
-
}
|
|
26283
26558
|
try {
|
|
26284
26559
|
if (!thinkingDroppedInStream) {
|
|
26285
26560
|
strat?.reportRealInputTokens?.(
|
|
@@ -26411,6 +26686,7 @@ export class AgentFramework {
|
|
|
26411
26686
|
agent.reset();
|
|
26412
26687
|
this.emitTrace({ type: 'inference:failed', agentName: agent.name, error: err.message, stack: err.stack });
|
|
26413
26688
|
const retry = retryOf(trigger);
|
|
26689
|
+
const asTransient = this.takeAsTransient(agent.name, err, trigger);
|
|
26414
26690
|
if (ownsProviderGate && this.holdProviderAcceleration(agent, err, retry)) {
|
|
26415
26691
|
// Once stop() has closed admission, nothing is held, and the turn
|
|
26416
26692
|
// ends here (endFailureAtStop).
|
|
@@ -26442,14 +26718,14 @@ export class AgentFramework {
|
|
|
26442
26718
|
speech: '',
|
|
26443
26719
|
...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: err.message }),
|
|
26444
26720
|
}, streamRouting);
|
|
26445
|
-
if (isRearmed(trigger) &&
|
|
26721
|
+
if (isRearmed(trigger) && asTransient) this.exhaustingRearms.add(agent.name);
|
|
26446
26722
|
this.emitTrace({
|
|
26447
26723
|
type: 'inference:exhausted',
|
|
26448
26724
|
agentName: agent.name,
|
|
26449
26725
|
error: err.message,
|
|
26450
26726
|
...this.classifyInferenceError(err),
|
|
26451
26727
|
});
|
|
26452
|
-
this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, streamRouting);
|
|
26728
|
+
this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, streamRouting, asTransient);
|
|
26453
26729
|
// Postmortem 2026-05-28 P2 #7: catch-path failures (stream itself
|
|
26454
26730
|
// threw) were previously only visible via in-memory trace listeners.
|
|
26455
26731
|
// Persist to inference log for forensic attribution.
|
|
@@ -26660,7 +26936,11 @@ export class AgentFramework {
|
|
|
26660
26936
|
|
|
26661
26937
|
/**
|
|
26662
26938
|
* Execute a tool call and return the result.
|
|
26663
|
-
* Routes
|
|
26939
|
+
* Routes it where the agent's own call of the name goes: to the
|
|
26940
|
+
* framework's own tools the agent has (agent_settings, the workspace and
|
|
26941
|
+
* gate tools, think, journal, prose_help and the rest), the channel tools,
|
|
26942
|
+
* MCPL or the module registry. skip_reply, which ends the turn it's called
|
|
26943
|
+
* in, is refused, since this call isn't made in one.
|
|
26664
26944
|
* Its one caller in the framework is the operator's puppet
|
|
26665
26945
|
* (puppetToolCall), which puts the agent's inline cap on the call as
|
|
26666
26946
|
* resultMaxChars; a host may call it as well.
|
|
@@ -26707,8 +26987,11 @@ export class AgentFramework {
|
|
|
26707
26987
|
* wire ordering. For its own duration the puppet HOLDS the turn-alive
|
|
26708
26988
|
* marker (wakes requeue, cross-turn writers defer), so a wake landing
|
|
26709
26989
|
* during the tool's execution cannot start a turn underneath the pair.
|
|
26710
|
-
* - Refused for tools outside the agent's own surface (
|
|
26711
|
-
*
|
|
26990
|
+
* - Refused for tools outside the agent's own surface (the tools it may
|
|
26991
|
+
* call, availableToolsForPresentation), and for skip_reply, by name or
|
|
26992
|
+
* through a toolsets--act that reaches it, which ends the turn it's
|
|
26993
|
+
* called in: the stored pair must be an act the agent could genuinely
|
|
26994
|
+
* have taken.
|
|
26712
26995
|
* - The call executes FOR REAL through the shared dispatch (MCPL,
|
|
26713
26996
|
* channel tools, utils, modules) with the agent's provenance — a
|
|
26714
26997
|
* puppeted walk_to actually moves the avatar.
|
|
@@ -26760,14 +27043,21 @@ export class AgentFramework {
|
|
|
26760
27043
|
`injecting a turn under an active stream corrupts wire ordering)`,
|
|
26761
27044
|
);
|
|
26762
27045
|
}
|
|
26763
|
-
const onSurface = this.
|
|
26764
|
-
.some((t) => t.name === toolName && agent.canUseTool(t.name));
|
|
27046
|
+
const onSurface = this.availableToolsForPresentation(agentName).some((t) => t.name === toolName);
|
|
26765
27047
|
if (!onSurface) {
|
|
26766
27048
|
throw new Error(
|
|
26767
27049
|
`puppet refused: tool ${toolName} is not on ${agentName}'s surface — ` +
|
|
26768
27050
|
`the stored pair must be an act the agent could genuinely have taken`,
|
|
26769
27051
|
);
|
|
26770
27052
|
}
|
|
27053
|
+
const route = this.toolRoute({ id: '', name: toolName, input }, agentName);
|
|
27054
|
+
const acting = route.kind === 'host-act' && route.host.kind === 'call' ? route.host.name : toolName;
|
|
27055
|
+
if (acting === 'skip_reply') {
|
|
27056
|
+
throw new Error(
|
|
27057
|
+
"puppet refused: skip_reply ends the turn it's called in, and a puppeted call isn't made in one, " +
|
|
27058
|
+
'so nothing was run or stored',
|
|
27059
|
+
);
|
|
27060
|
+
}
|
|
26771
27061
|
|
|
26772
27062
|
// Anthropic-shaped id so the stored pair is indistinguishable from a
|
|
26773
27063
|
// provider-issued call.
|
|
@@ -26906,96 +27196,123 @@ export class AgentFramework {
|
|
|
26906
27196
|
return { success: false, error: marked, isError: true };
|
|
26907
27197
|
}
|
|
26908
27198
|
|
|
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
|
-
|
|
27199
|
+
const subject = origin.kind === 'agent' ? origin.agentName : call.callerAgentName ?? '__ephemeral__';
|
|
27200
|
+
const route = this.toolRoute(call, call.callerAgentName);
|
|
27201
|
+
switch (route.kind) {
|
|
27202
|
+
case 'presentation':
|
|
27203
|
+
return this.editToolPresentation(call.callerAgentName ?? '__ephemeral__', call);
|
|
27204
|
+
|
|
27205
|
+
// toolsets--act on a host toolset's tool (the built-in one, a server's
|
|
27206
|
+
// tab) goes where a programmatic call of that tool goes, under its name.
|
|
27207
|
+
case 'host-act':
|
|
27208
|
+
if (route.host.kind === 'refused') return failed({ success: false, error: route.host.error, isError: true });
|
|
27209
|
+
return this.executeToolCallFrom({ ...call, name: route.host.name, input: route.host.args }, origin);
|
|
27210
|
+
|
|
27211
|
+
// RFC-014: toolsets--act, resolved to the dynamic tool it calls, goes
|
|
27212
|
+
// where a programmatic call of that tool goes, with its instance.
|
|
27213
|
+
case 'toolset-act': {
|
|
27214
|
+
const target = this.resolveToolsetAct(call);
|
|
27215
|
+
if (!target.ok) return failed({ success: false, error: target.error, isError: true }, this.everyServerRedactor()(target.error));
|
|
27216
|
+
if (origin.kind === 'agent') {
|
|
27217
|
+
const failure = this.agentToolPermissionFailure(origin.agentName, target.call, false);
|
|
27218
|
+
if (failure) return failed(failure, this.everyServerRedactor()(failure.error ?? ''));
|
|
27219
|
+
}
|
|
27220
|
+
return this.executeMcplToolCall(target.call, target.serverId, target.prefix);
|
|
27221
|
+
}
|
|
27222
|
+
|
|
27223
|
+
case 'mcpl':
|
|
27224
|
+
return this.executeMcplToolCall(call, route.serverId, route.prefix);
|
|
27225
|
+
|
|
27226
|
+
// Synthesized channel tools. This route was missing: when
|
|
27227
|
+
// subscription-GC moved off the retired MCPL unsubscribe tool onto the
|
|
27228
|
+
// generic channel_close (discord-mcpl b095a9f), module-originated
|
|
27229
|
+
// closes started failing downstream as "Invalid tool name format" —
|
|
27230
|
+
// the janitor had been silently broken since. `origin` is trusted
|
|
27231
|
+
// dispatch context established at the call boundary above, not
|
|
27232
|
+
// inferred from which method was used: only the ModuleRegistry closure
|
|
27233
|
+
// carries module provenance (see handleToolClose).
|
|
27234
|
+
case 'channel': {
|
|
27235
|
+
const turn = call.name === 'channel_publish' && call.callerAgentName ? this.callingTurnOf(call.callerAgentName, call) : undefined;
|
|
27236
|
+
const input = call.name === 'channel_publish' && call.callerAgentName
|
|
27237
|
+
? this.publishInputWithTurnLocus(call.input, call.callerAgentName, turn)
|
|
27238
|
+
: call.input;
|
|
27239
|
+
return this.channelRegistry!.handleChannelToolCall(call.name, input, origin.kind === 'agent' && turn ? { ...origin, inTurn: true } : origin);
|
|
27240
|
+
}
|
|
27241
|
+
|
|
27242
|
+
case 'agent-settings':
|
|
27243
|
+
return this.awaitedToolAnswer((deliver) => this.dispatchAgentSettingsToolCall(subject, call, deliver));
|
|
27244
|
+
case 'save-image':
|
|
27245
|
+
return this.awaitedToolAnswer((deliver) => this.dispatchSaveImageToolCall(subject, call, deliver));
|
|
27246
|
+
case 'read-image':
|
|
27247
|
+
return this.awaitedToolAnswer((deliver) => this.dispatchReadImageToolCall(subject, call, deliver));
|
|
27248
|
+
case 'fetch-reference':
|
|
27249
|
+
return this.awaitedToolAnswer((deliver) => this.dispatchFetchReferenceToolCall(subject, call, deliver));
|
|
27250
|
+
case 'gate-status':
|
|
27251
|
+
return this.awaitedToolAnswer((deliver) => this.dispatchGateToolCall(subject, call, deliver));
|
|
27252
|
+
|
|
27253
|
+
// The tune-out tool, in process: the same body the dispatcher runs, its
|
|
27254
|
+
// answer awaited here (it can wait for the tune-out's transport).
|
|
27255
|
+
case 'tune-out': {
|
|
27256
|
+
const agentName = call.callerAgentName ?? this.primaryAgentName ?? '__ephemeral__';
|
|
27257
|
+
const input = (call.input ?? {}) as Record<string, unknown>;
|
|
27258
|
+
// The calling turn's own channels, as the channel tools read them here (origin.own).
|
|
27259
|
+
const own = origin.kind === 'agent' ? origin.own ?? [] : [];
|
|
27260
|
+
const turn = this.callingTurnOf(agentName, call);
|
|
27261
|
+
return call.name === 'focus' ? this.handleFocusTool(agentName, input, own, turn) : this.handleTuneOutTool(agentName, input, own, turn);
|
|
27262
|
+
}
|
|
27263
|
+
case 'subconscious': {
|
|
27264
|
+
const routed = this.readerToolCall(subject, call);
|
|
27265
|
+
return 'refused' in routed ? failed({ success: false, error: `${routed.refused}, so the call didn't run.`, isError: true }) : routed.result;
|
|
27266
|
+
}
|
|
27267
|
+
case 'sleep':
|
|
27268
|
+
return this.awaitedToolAnswer((deliver) => this.dispatchSleepToolCall(subject, call, deliver));
|
|
27269
|
+
|
|
27270
|
+
// The heartbeat's tools, for callers that await a tool's result (an
|
|
27271
|
+
// in-process call, a puppet, a module's callTool). A module is trusted;
|
|
27272
|
+
// an agent is held to what dispatch holds it to.
|
|
27273
|
+
case 'heartbeat': {
|
|
27274
|
+
const refusal = origin.kind === 'module' ? null : this.heartbeatRefusal(call.callerAgentName ?? '__ephemeral__', call);
|
|
27275
|
+
return refusal ? { success: false, error: refusal, isError: true } : this.runHeartbeatTool(call);
|
|
27276
|
+
}
|
|
27277
|
+
case 'wake-rule':
|
|
27278
|
+
return this.awaitedToolAnswer((deliver) => this.dispatchWakeRuleToolCall(subject, call, deliver));
|
|
27279
|
+
case 'event-tags':
|
|
27280
|
+
return this.awaitedToolAnswer((deliver) => this.dispatchEventTagsToolCall(subject, call, deliver));
|
|
27281
|
+
case 'agent-verb':
|
|
27282
|
+
if (origin.kind === 'module') return failed(agentsOwnVerbRefusal(call.name));
|
|
27283
|
+
if (call.name === 'skip_reply') {
|
|
27284
|
+
return failed({
|
|
27285
|
+
success: false,
|
|
27286
|
+
error: "skip_reply ends the turn it's called in, and this call wasn't made in a turn, so nothing was done.",
|
|
27287
|
+
isError: true,
|
|
27288
|
+
});
|
|
27289
|
+
}
|
|
27290
|
+
return this.channelRegistry!.handleChannelToolCall(call.name, call.input, origin);
|
|
27291
|
+
|
|
27292
|
+
// Client-side programmatic tool calling for callers that await a tool's
|
|
27293
|
+
// result: the puppet and a host through executeToolCall, and a module
|
|
27294
|
+
// through callTool. Keyed by callerAgentName, so each agent's scripts are
|
|
27295
|
+
// its own.
|
|
27296
|
+
case 'code-execution':
|
|
27297
|
+
return this.runCodeExecution(call.callerAgentName ?? '__ephemeral__', call);
|
|
27298
|
+
|
|
27299
|
+
// drafts: the calling resident's own held drafts. A puppeted call has
|
|
27300
|
+
// agent origin and acts on that resident's drafts, as the model's own
|
|
27301
|
+
// call does through dispatchToolCall, but it isn't made inside the
|
|
27302
|
+
// resident's turn, so it takes none of the turn's channels, engages none,
|
|
27303
|
+
// and isn't noted as the resident's speech (callingTurnOf). A module
|
|
27304
|
+
// holds no drafts.
|
|
27305
|
+
case 'drafts':
|
|
27306
|
+
if (origin.kind === 'module') return failed(agentsOwnVerbRefusal(call.name));
|
|
27307
|
+
return this.draftsToolResult(origin.agentName, call.input);
|
|
27308
|
+
case 'prose-help':
|
|
27309
|
+
return { success: true, data: PROSE_ROUTING_HELP };
|
|
27310
|
+
case 'module':
|
|
27311
|
+
// Module tools. The caller uses the result in process, so it gets the
|
|
27312
|
+
// module's own result, as the module returned it, less a refusal it
|
|
27313
|
+
// claims, which is the dispatcher's to say.
|
|
27314
|
+
return this.moduleRegistry.handleToolCall(call);
|
|
26936
27315
|
}
|
|
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
27316
|
}
|
|
27000
27317
|
|
|
27001
27318
|
/**
|
|
@@ -27739,7 +28056,7 @@ export class AgentFramework {
|
|
|
27739
28056
|
// A batch with no finite deadline (a host whose deadline() threw) gets
|
|
27740
28057
|
// this write's own, rather than a timer Node would cut to 1 ms.
|
|
27741
28058
|
if (!Number.isFinite(deadline)) deadline = Date.now() + this.withheldSpillDeadlineMs;
|
|
27742
|
-
if (this.
|
|
28059
|
+
if (this.guardSpillsClosed) return null;
|
|
27743
28060
|
const place = this.toolResultFilePlace(label);
|
|
27744
28061
|
if (!place) return null;
|
|
27745
28062
|
if (this.stalledSpills.size > 0) {
|
|
@@ -28842,6 +29159,53 @@ export class AgentFramework {
|
|
|
28842
29159
|
return { name: call.name, cause: permitted || refused ? silenceCauseOf([call]) : null, refused, permitted };
|
|
28843
29160
|
}
|
|
28844
29161
|
|
|
29162
|
+
private toolRoute(call: ToolCall, actor: string | undefined): ToolRoute {
|
|
29163
|
+
const name = call.name;
|
|
29164
|
+
if (isPresentationTool(name)) return { kind: 'presentation' };
|
|
29165
|
+
if (name === META_TOOL_NAMES.act) {
|
|
29166
|
+
const host = actor === undefined ? null : this.hostPresentation?.resolveAct(actor, call.input) ?? null;
|
|
29167
|
+
if (host) return { kind: 'host-act', host };
|
|
29168
|
+
if (this.toolsetHub && this.mcplServerRegistry) return { kind: 'toolset-act' };
|
|
29169
|
+
}
|
|
29170
|
+
const mcpl = this.mcplServerRegistry ? this.resolveMcplTool(name) : null;
|
|
29171
|
+
if (mcpl) return { kind: 'mcpl', serverId: mcpl[0], prefix: mcpl[1] };
|
|
29172
|
+
if (name.startsWith('channel_') && this.channelRegistry) return { kind: 'channel' };
|
|
29173
|
+
if (name === 'agent_settings') return { kind: 'agent-settings' };
|
|
29174
|
+
if (name === 'save_recent_image') return { kind: 'save-image' };
|
|
29175
|
+
if (name === 'read_image') return { kind: 'read-image' };
|
|
29176
|
+
if (name === 'fetch_reference') return { kind: 'fetch-reference' };
|
|
29177
|
+
if (name === 'gate_status' && this.eventGate) return { kind: 'gate-status' };
|
|
29178
|
+
if ((name === 'tune_out' || name === 'focus') && this.tuneOutCoordinator) return { kind: 'tune-out' };
|
|
29179
|
+
if ((SUBCONSCIOUS_TOOL_NAMES as readonly string[]).includes(name)) return { kind: 'subconscious' };
|
|
29180
|
+
if ((name === 'sleep' || name === 'wake') && this.eventGate) return { kind: 'sleep' };
|
|
29181
|
+
if (this.heartbeat?.ownsTool(name)) return { kind: 'heartbeat' };
|
|
29182
|
+
if ((name === 'wake_add_rule' || name === 'wake_remove_rule') && this.eventGate) return { kind: 'wake-rule' };
|
|
29183
|
+
if (name === 'event_tags' && this.eventGate) return { kind: 'event-tags' };
|
|
29184
|
+
if ((name === 'think' || name === 'journal' || name === 'skip_reply') && this.channelRegistry) return { kind: 'agent-verb' };
|
|
29185
|
+
if (name === CODE_EXECUTION_TOOL_NAME && this.codeExecution) return { kind: 'code-execution' };
|
|
29186
|
+
if (name === 'drafts' && this.channelRegistry) return { kind: 'drafts' };
|
|
29187
|
+
if (name === PROSE_HELP_TOOL.name) return { kind: 'prose-help' };
|
|
29188
|
+
return { kind: 'module' };
|
|
29189
|
+
}
|
|
29190
|
+
|
|
29191
|
+
private pushToolResult(agentName: string, call: ToolCall, moduleName: string): (result: ToolResult) => void {
|
|
29192
|
+
return (result) => this.pushEvent({ type: 'tool-result', callId: call.id, toolName: call.name, agentName, moduleName, result });
|
|
29193
|
+
}
|
|
29194
|
+
|
|
29195
|
+
private awaitedToolAnswer(run: (deliver: (result: ToolResult) => void) => void): Promise<ToolResult> {
|
|
29196
|
+
return new Promise((resolve) => {
|
|
29197
|
+
try {
|
|
29198
|
+
run((result) => {
|
|
29199
|
+
const answer = { ...result };
|
|
29200
|
+
delete answer.refused;
|
|
29201
|
+
resolve(answer);
|
|
29202
|
+
});
|
|
29203
|
+
} catch (error) {
|
|
29204
|
+
resolve({ success: false, error: thrownText(error), isError: true });
|
|
29205
|
+
}
|
|
29206
|
+
});
|
|
29207
|
+
}
|
|
29208
|
+
|
|
28845
29209
|
/**
|
|
28846
29210
|
* Dispatch a call the agent made. `turnId` is the turn it was made in
|
|
28847
29211
|
* (TurnRouting.turnId), as its caller knows it: a model's call, the turn
|
|
@@ -28892,217 +29256,207 @@ export class AgentFramework {
|
|
|
28892
29256
|
const enrichedCall: ToolCall = { ...call, callerAgentName: agentName };
|
|
28893
29257
|
if (turnId !== undefined) enrichedCall.callerTurnId = turnId;
|
|
28894
29258
|
else delete enrichedCall.callerTurnId;
|
|
28895
|
-
|
|
28896
|
-
|
|
28897
|
-
|
|
28898
|
-
|
|
28899
|
-
|
|
29259
|
+
const route = this.toolRoute(enrichedCall, agentName);
|
|
29260
|
+
switch (route.kind) {
|
|
29261
|
+
case 'presentation': {
|
|
29262
|
+
const result = this.editToolPresentation(agentName, enrichedCall);
|
|
29263
|
+
this.pushEvent({type:'tool-result',callId:call.id,toolName:call.name,agentName,moduleName:'tool-presentation',result});
|
|
29264
|
+
return;
|
|
29265
|
+
}
|
|
28900
29266
|
|
|
29267
|
+
// toolsets--act on a host toolset's tool (the built-in one, a server's
|
|
29268
|
+
// tab): the call of that tool, under its own name, down the path a
|
|
29269
|
+
// direct call of it takes, permissions included.
|
|
29270
|
+
case 'host-act':
|
|
29271
|
+
this.dispatchHostAct(agentName, call, route.host, turnId);
|
|
29272
|
+
return;
|
|
28901
29273
|
|
|
28902
|
-
|
|
28903
|
-
|
|
28904
|
-
|
|
28905
|
-
|
|
28906
|
-
|
|
28907
|
-
|
|
28908
|
-
this.dispatchHostAct(agentName, call, host, turnId);
|
|
29274
|
+
// RFC-014: toolsets--act calls a server's dynamic tool, down the same
|
|
29275
|
+
// path as a direct call of it (dispatchToolsetAct).
|
|
29276
|
+
case 'toolset-act': {
|
|
29277
|
+
const scriptWaiter = this.scriptToolWaiters.get(enrichedCall.id);
|
|
29278
|
+
if (scriptWaiter) scriptWaiter.route.toServer = true;
|
|
29279
|
+
this.dispatchToolsetAct(agentName, enrichedCall);
|
|
28909
29280
|
return;
|
|
28910
29281
|
}
|
|
28911
|
-
}
|
|
28912
|
-
|
|
28913
|
-
// RFC-014: toolsets--act calls a server's dynamic tool, down the same
|
|
28914
|
-
// path as a direct call of it (dispatchToolsetAct).
|
|
28915
|
-
if (enrichedCall.name === META_TOOL_NAMES.act && this.toolsetHub && this.mcplServerRegistry) {
|
|
28916
|
-
const scriptWaiter = this.scriptToolWaiters.get(enrichedCall.id);
|
|
28917
|
-
if (scriptWaiter) scriptWaiter.route.toServer = true;
|
|
28918
|
-
this.dispatchToolsetAct(agentName, enrichedCall);
|
|
28919
|
-
return;
|
|
28920
|
-
}
|
|
28921
29282
|
|
|
28922
|
-
|
|
28923
|
-
|
|
28924
|
-
|
|
28925
|
-
|
|
28926
|
-
|
|
28927
|
-
|
|
28928
|
-
|
|
28929
|
-
|
|
28930
|
-
return;
|
|
28931
|
-
}
|
|
29283
|
+
case 'mcpl': {
|
|
29284
|
+
// A script's call: its waiter keeps that it went to a server, for the
|
|
29285
|
+
// script lane's exit (scriptToolWaiters).
|
|
29286
|
+
const scriptWaiter = this.scriptToolWaiters.get(enrichedCall.id);
|
|
29287
|
+
if (scriptWaiter) scriptWaiter.route.toServer = true;
|
|
29288
|
+
this.dispatchMcplToolCall(agentName, enrichedCall, route.serverId, route.prefix);
|
|
29289
|
+
return;
|
|
29290
|
+
}
|
|
28932
29291
|
|
|
28933
|
-
|
|
28934
|
-
|
|
28935
|
-
|
|
28936
|
-
|
|
28937
|
-
}
|
|
29292
|
+
case 'channel': {
|
|
29293
|
+
this.dispatchChannelToolCall(agentName, enrichedCall);
|
|
29294
|
+
return;
|
|
29295
|
+
}
|
|
28938
29296
|
|
|
28939
|
-
|
|
28940
|
-
|
|
28941
|
-
|
|
28942
|
-
|
|
29297
|
+
case 'agent-settings': {
|
|
29298
|
+
this.dispatchAgentSettingsToolCall(agentName, enrichedCall);
|
|
29299
|
+
return;
|
|
29300
|
+
}
|
|
28943
29301
|
|
|
28944
|
-
|
|
28945
|
-
|
|
28946
|
-
|
|
28947
|
-
|
|
29302
|
+
case 'save-image': {
|
|
29303
|
+
this.dispatchSaveImageToolCall(agentName, enrichedCall);
|
|
29304
|
+
return;
|
|
29305
|
+
}
|
|
28948
29306
|
|
|
28949
|
-
|
|
28950
|
-
|
|
28951
|
-
|
|
28952
|
-
|
|
29307
|
+
case 'read-image': {
|
|
29308
|
+
this.dispatchReadImageToolCall(agentName, enrichedCall);
|
|
29309
|
+
return;
|
|
29310
|
+
}
|
|
28953
29311
|
|
|
28954
|
-
|
|
28955
|
-
|
|
28956
|
-
|
|
28957
|
-
|
|
29312
|
+
case 'fetch-reference': {
|
|
29313
|
+
this.dispatchFetchReferenceToolCall(agentName, enrichedCall);
|
|
29314
|
+
return;
|
|
29315
|
+
}
|
|
28958
29316
|
|
|
28959
|
-
|
|
28960
|
-
|
|
28961
|
-
|
|
28962
|
-
|
|
28963
|
-
}
|
|
29317
|
+
case 'gate-status': {
|
|
29318
|
+
this.dispatchGateToolCall(agentName, enrichedCall);
|
|
29319
|
+
return;
|
|
29320
|
+
}
|
|
28964
29321
|
|
|
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
|
-
});
|
|
29322
|
+
case 'tune-out': {
|
|
29323
|
+
this.dispatchTuneOutToolCall(agentName, enrichedCall);
|
|
28979
29324
|
return;
|
|
28980
29325
|
}
|
|
28981
|
-
|
|
28982
|
-
.
|
|
28983
|
-
|
|
28984
|
-
|
|
29326
|
+
case 'subconscious': {
|
|
29327
|
+
const routed = this.readerToolCall(agentName, enrichedCall);
|
|
29328
|
+
if ('refused' in routed) {
|
|
29329
|
+
const error = routed.refused;
|
|
29330
|
+
const result = this.refusedResult(agentName, call.id, error);
|
|
29331
|
+
this.emitTrace({ type: 'tool:failed', module: 'tune-out', tool: call.name, callId: call.id, error });
|
|
28985
29332
|
this.pushEvent({
|
|
28986
|
-
type: 'tool-result',
|
|
28987
|
-
callId: enrichedCall.id,
|
|
28988
|
-
toolName: enrichedCall.name,
|
|
28989
|
-
agentName,
|
|
28990
|
-
moduleName: 'tune-out',
|
|
28991
|
-
result,
|
|
29333
|
+
type: 'tool-result', callId: call.id, toolName: call.name, agentName, moduleName: 'tune-out', result,
|
|
28992
29334
|
});
|
|
28993
|
-
|
|
28994
|
-
|
|
28995
|
-
|
|
29335
|
+
return;
|
|
29336
|
+
}
|
|
29337
|
+
void routed.result
|
|
29338
|
+
.then((result) => {
|
|
29339
|
+
// Through pushEvent, like every other tool's result, so one that
|
|
29340
|
+
// arrives after stop() is dropped rather than rejected unhandled.
|
|
29341
|
+
this.pushEvent({
|
|
29342
|
+
type: 'tool-result',
|
|
29343
|
+
callId: enrichedCall.id,
|
|
29344
|
+
toolName: enrichedCall.name,
|
|
29345
|
+
agentName,
|
|
29346
|
+
moduleName: 'tune-out',
|
|
29347
|
+
result,
|
|
29348
|
+
});
|
|
29349
|
+
});
|
|
29350
|
+
return;
|
|
29351
|
+
}
|
|
28996
29352
|
|
|
28997
|
-
|
|
28998
|
-
|
|
28999
|
-
|
|
29000
|
-
|
|
29001
|
-
}
|
|
29353
|
+
case 'sleep': {
|
|
29354
|
+
this.dispatchSleepToolCall(agentName, enrichedCall);
|
|
29355
|
+
return;
|
|
29356
|
+
}
|
|
29002
29357
|
|
|
29003
|
-
|
|
29004
|
-
|
|
29005
|
-
|
|
29006
|
-
|
|
29007
|
-
}
|
|
29358
|
+
case 'heartbeat': {
|
|
29359
|
+
this.dispatchHeartbeatToolCall(agentName, enrichedCall);
|
|
29360
|
+
return;
|
|
29361
|
+
}
|
|
29008
29362
|
|
|
29009
|
-
|
|
29010
|
-
|
|
29011
|
-
|
|
29012
|
-
|
|
29013
|
-
}
|
|
29363
|
+
case 'wake-rule': {
|
|
29364
|
+
this.dispatchWakeRuleToolCall(agentName, enrichedCall);
|
|
29365
|
+
return;
|
|
29366
|
+
}
|
|
29014
29367
|
|
|
29015
|
-
|
|
29016
|
-
|
|
29017
|
-
|
|
29018
|
-
|
|
29019
|
-
}
|
|
29368
|
+
case 'event-tags': {
|
|
29369
|
+
this.dispatchEventTagsToolCall(agentName, enrichedCall);
|
|
29370
|
+
return;
|
|
29371
|
+
}
|
|
29020
29372
|
|
|
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;
|
|
29373
|
+
case 'agent-verb': {
|
|
29374
|
+
// skip_reply(wake_in_seconds): arm a gate self-wake so "not replying
|
|
29375
|
+
// NOW" can also mean "back in a moment" — ends the turn, then wakes
|
|
29376
|
+
// the agent after N seconds unless something else wakes it first
|
|
29377
|
+
// (any turn start cancels the pending self-wake). Armed here because
|
|
29378
|
+
// the framework owns the EventGate; the registry only words the
|
|
29379
|
+
// confirmation. A wake the host can't set is refused, so the model
|
|
29380
|
+
// learns that its turn remains open and no wake was armed: any
|
|
29381
|
+
// wake_in_seconds without a gate, and one that isn't a number of
|
|
29382
|
+
// seconds with one.
|
|
29383
|
+
if (enrichedCall.name === 'skip_reply') {
|
|
29384
|
+
const error = this.skipReplyRefusal(enrichedCall);
|
|
29385
|
+
if (error !== undefined) {
|
|
29386
|
+
const wakeValue = (enrichedCall.input as { wake_in_seconds?: unknown } | null | undefined)?.wake_in_seconds;
|
|
29387
|
+
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)')}`);
|
|
29388
|
+
const result = this.refusedResult(agentName, enrichedCall.id, error);
|
|
29389
|
+
this.emitTrace({ type: 'tool:failed', module: 'channels', tool: enrichedCall.name, callId: enrichedCall.id, error });
|
|
29390
|
+
this.pushEvent({ type: 'tool-result', callId: enrichedCall.id, toolName: enrichedCall.name, agentName, moduleName: 'channels', result });
|
|
29391
|
+
return;
|
|
29392
|
+
}
|
|
29393
|
+
const skipping = this.callingTurnOf(agentName, enrichedCall);
|
|
29394
|
+
if (skipping) {
|
|
29395
|
+
skipping.privateEnd = true;
|
|
29396
|
+
delete skipping.unpostedSpeech;
|
|
29397
|
+
}
|
|
29398
|
+
const wake = readSkipReplyWake(enrichedCall.input);
|
|
29399
|
+
if (wake.kind === 'wake' && this.eventGate) {
|
|
29400
|
+
const armedAt = Date.now();
|
|
29401
|
+
const { inMs, armed } = this.eventGate.armSelfWake(agentName, wake.seconds, 'skip_reply');
|
|
29402
|
+
// The heartbeat's tick or reminder due before the timer: if it wakes
|
|
29403
|
+
// the agent, its turn cancels the timer, so the confirmation names
|
|
29404
|
+
// it and the time the self-wake would have left.
|
|
29405
|
+
const earlierWake = armed ? this.heartbeatWakeBefore(agentName, armedAt + inMs) : undefined;
|
|
29406
|
+
console.error(armed
|
|
29407
|
+
? `[self-wake] agent=${agentName} armed in ${Math.round(inMs / 1000)}s (skip_reply)`
|
|
29408
|
+
+ (earlierWake ? `; due sooner, in ~${earlierWake.inSeconds}s: ${logDetail(earlierWake.what)}` : '')
|
|
29409
|
+
: `[self-wake] agent=${agentName} not armed: the framework has stopped (skip_reply)`);
|
|
29410
|
+
this.dispatchChannelToolCall(agentName, enrichedCall, earlierWake);
|
|
29411
|
+
return;
|
|
29412
|
+
}
|
|
29064
29413
|
}
|
|
29414
|
+
this.dispatchChannelToolCall(agentName, enrichedCall);
|
|
29415
|
+
return;
|
|
29065
29416
|
}
|
|
29066
|
-
this.dispatchChannelToolCall(agentName, enrichedCall);
|
|
29067
|
-
return;
|
|
29068
|
-
}
|
|
29069
29417
|
|
|
29070
|
-
|
|
29071
|
-
|
|
29072
|
-
|
|
29073
|
-
|
|
29074
|
-
|
|
29075
|
-
|
|
29076
|
-
|
|
29418
|
+
// Client-side programmatic tool calling: run a script that calls the
|
|
29419
|
+
// agent's other tools in-process (script-inner calls come back through
|
|
29420
|
+
// this very dispatcher with waiter-tracked call IDs).
|
|
29421
|
+
case 'code-execution': {
|
|
29422
|
+
this.dispatchCodeExecutionToolCall(agentName, enrichedCall);
|
|
29423
|
+
return;
|
|
29424
|
+
}
|
|
29077
29425
|
|
|
29078
|
-
|
|
29079
|
-
|
|
29080
|
-
|
|
29081
|
-
|
|
29082
|
-
|
|
29083
|
-
|
|
29084
|
-
|
|
29085
|
-
|
|
29086
|
-
|
|
29087
|
-
|
|
29426
|
+
// drafts: the resident's own held drafts (list/read/resend/dismiss).
|
|
29427
|
+
// Answered by the framework; resend publishes through the registry's
|
|
29428
|
+
// publish executor, so the result arrives asynchronously.
|
|
29429
|
+
case 'drafts': {
|
|
29430
|
+
const startedAt = Date.now();
|
|
29431
|
+
this.emitTrace({ type: 'tool:started', module: 'framework', tool: 'drafts', callId: enrichedCall.id, input: enrichedCall.input });
|
|
29432
|
+
void this.draftsToolResult(agentName, enrichedCall.input, this.callingTurnOf(agentName, enrichedCall), enrichedCall.id)
|
|
29433
|
+
.then((result) => {
|
|
29434
|
+
this.emitTrace({ type: 'tool:completed', module: 'framework', tool: 'drafts', callId: enrichedCall.id, durationMs: Date.now() - startedAt });
|
|
29435
|
+
this.pushEvent({ type: 'tool-result', callId: enrichedCall.id, toolName: enrichedCall.name, agentName, moduleName: 'framework', result });
|
|
29436
|
+
});
|
|
29437
|
+
return;
|
|
29438
|
+
}
|
|
29439
|
+
|
|
29440
|
+
// prose_help: on-demand routing reference for explicit-prose agents.
|
|
29441
|
+
// Answered inline — the grammar is a tool RESULT (model-requested), never
|
|
29442
|
+
// ambient context (see PROSE_ROUTING_HELP / teach-by-bounce).
|
|
29443
|
+
case 'prose-help': {
|
|
29444
|
+
this.emitTrace({ type: 'tool:completed', module: 'framework', tool: 'prose_help', callId: enrichedCall.id, durationMs: 0 });
|
|
29445
|
+
this.pushEvent({
|
|
29446
|
+
type: 'tool-result',
|
|
29447
|
+
callId: enrichedCall.id,
|
|
29448
|
+
toolName: enrichedCall.name,
|
|
29449
|
+
agentName,
|
|
29450
|
+
moduleName: 'framework',
|
|
29451
|
+
result: { success: true, data: PROSE_ROUTING_HELP },
|
|
29088
29452
|
});
|
|
29089
|
-
|
|
29090
|
-
|
|
29453
|
+
return;
|
|
29454
|
+
}
|
|
29091
29455
|
|
|
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;
|
|
29456
|
+
case 'module':
|
|
29457
|
+
break;
|
|
29458
|
+
default:
|
|
29459
|
+
route satisfies never;
|
|
29106
29460
|
}
|
|
29107
29461
|
|
|
29108
29462
|
const sepIndex = enrichedCall.name.indexOf('--');
|
|
@@ -29502,6 +29856,7 @@ export class AgentFramework {
|
|
|
29502
29856
|
private noteSpokenTo(agentName: string, metadata: MessageMetadata | undefined): void {
|
|
29503
29857
|
const agent = this.agents.get(agentName);
|
|
29504
29858
|
if ((agent && !this.agentSees(agent, metadata)) || isOwnScheduleWrite(metadata) || !isConversationalInjection(metadata)) return;
|
|
29859
|
+
this.rearmDeadlineCuts?.delete(agentName);
|
|
29505
29860
|
if (this.turnRearms?.delete(agentName)) console.error(`[inference-rearm] agent=${agentName} re-send count started again: a message was said to it`);
|
|
29506
29861
|
}
|
|
29507
29862
|
|
|
@@ -30233,11 +30588,9 @@ export class AgentFramework {
|
|
|
30233
30588
|
* at a tool boundary whose stream declares it carries the messages
|
|
30234
30589
|
* injected there (markContextConsumed). So a notice such a stream carried
|
|
30235
30590
|
* 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.
|
|
30591
|
+
* a budget restart, the direct API's boundary, which has no stream) is
|
|
30592
|
+
* above it until a compile sends it, and counts as unread. null when the
|
|
30593
|
+
* window can't be read, which is logged.
|
|
30241
30594
|
*/
|
|
30242
30595
|
private releasedWindow(agent: Agent, held: ReadonlySet<string>): ReleasedWindow | null {
|
|
30243
30596
|
try {
|
|
@@ -31395,6 +31748,7 @@ export class AgentFramework {
|
|
|
31395
31748
|
const ended = this.exhaustedTurns?.get(agentName);
|
|
31396
31749
|
this.exhaustedTurns?.delete(agentName);
|
|
31397
31750
|
const rearmed = this.exhaustingRearms?.delete(agentName) === true;
|
|
31751
|
+
const madeAgain = this.freshCallsCut?.delete(agentName) === true;
|
|
31398
31752
|
// The reason is the failure's own message, and a provider can echo the
|
|
31399
31753
|
// whole rejected request in it: one household's [inference-failed]
|
|
31400
31754
|
// marker carried ~1.7 MB of its own request back into its context. Every
|
|
@@ -31451,9 +31805,10 @@ export class AgentFramework {
|
|
|
31451
31805
|
// (2) Agent-facing chronicle marker (no inference triggered → no loop).
|
|
31452
31806
|
const agent = this.agents.get(agentName);
|
|
31453
31807
|
if (agent && !(rearmed && !ended) && process.env.SUPPRESS_INFERENCE_FAILED_MARKER !== '1') {
|
|
31454
|
-
const
|
|
31455
|
-
|
|
31456
|
-
.
|
|
31808
|
+
const timedOut = errorType === 'timeout';
|
|
31809
|
+
const causeParts = [errorType, httpStatus !== undefined ? `HTTP ${httpStatus}` : undefined]
|
|
31810
|
+
.filter((part): part is string => part !== undefined);
|
|
31811
|
+
const cause = timedOut && causeParts.length === 1 ? '' : causeParts.join(', ');
|
|
31457
31812
|
try {
|
|
31458
31813
|
agent.getContextManager().addMessage(
|
|
31459
31814
|
'user',
|
|
@@ -31461,17 +31816,17 @@ export class AgentFramework {
|
|
|
31461
31816
|
type: 'text',
|
|
31462
31817
|
text:
|
|
31463
31818
|
(ended
|
|
31464
|
-
? `[${NOTICE_TAG.inferenceFailed}] Your previous turn did not complete:
|
|
31465
|
-
|
|
31819
|
+
? `[${NOTICE_TAG.inferenceFailed}] Your previous turn did not complete: ` +
|
|
31820
|
+
`${timedOut ? 'its last call timed out' : 'it failed'}${cause ? ` (${suppliedValue(cause)})` : ''}.` +
|
|
31821
|
+
`${madeAgain ? ` ${MADE_AGAIN}` : ''}` +
|
|
31466
31822
|
`${ended.roundsRan ? ' Its earlier rounds ran, and their tool calls ran as their results show.' : ''}` +
|
|
31467
31823
|
`${ended.callsCutOff ? ` ${CALLS_CUT_OFF}` : ''}` +
|
|
31468
31824
|
`${ended.liveSpeech ? ` ${LIVE_SPEECH_UNRECEIPTED}` : ''}` +
|
|
31469
31825
|
`${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).`,
|
|
31826
|
+
: `[${NOTICE_TAG.inferenceFailed}] Your previous turn did not complete: ` +
|
|
31827
|
+
`the model call ${timedOut ? 'timed out' : 'failed'}${cause ? ` (${suppliedValue(cause)})` : ''} ` +
|
|
31828
|
+
`and produced no response, so nothing was sent.${madeAgain ? ` ${MADE_AGAIN}` : ''} Reason: `) +
|
|
31829
|
+
`${failureShown(markerReason)}. ${timedOut ? TIMEOUT_ADVICE : FAILURE_ADVICE}`,
|
|
31475
31830
|
}],
|
|
31476
31831
|
{ system: true, kind: 'inference-failed', reason: markerReason, consecutive: streak, ...classification, ...cutAt(markerReason) },
|
|
31477
31832
|
);
|
|
@@ -31732,6 +32087,7 @@ export class AgentFramework {
|
|
|
31732
32087
|
this.featureSetManager,
|
|
31733
32088
|
inferenceRouting ?? null,
|
|
31734
32089
|
(event) => this.emitTrace(event as { type: TraceEvent['type']; [key: string]: unknown }),
|
|
32090
|
+
() => this.inferenceBounds,
|
|
31735
32091
|
);
|
|
31736
32092
|
|
|
31737
32093
|
// Checkpoint manager (Step 8), built and loaded at the top
|
|
@@ -31771,7 +32127,13 @@ export class AgentFramework {
|
|
|
31771
32127
|
if (!coordinator) return undefined;
|
|
31772
32128
|
const scope = coordinator.getAllExcept();
|
|
31773
32129
|
if (!scope || scope.params.except.serverId !== serverId || scope.params.except.channelId !== channelId) return undefined;
|
|
31774
|
-
coordinator.cancelAllExcept
|
|
32130
|
+
let released: ReturnType<typeof coordinator.cancelAllExcept>;
|
|
32131
|
+
try {
|
|
32132
|
+
released = coordinator.cancelAllExcept('channel-close', 'the excepted channel was closed');
|
|
32133
|
+
} catch (err) {
|
|
32134
|
+
throw new Error(`The close wasn't recorded: ${err instanceof Error ? err.message : String(err)}`);
|
|
32135
|
+
}
|
|
32136
|
+
if (!released.ok) throw new Error(`The close wasn't recorded: ${released.error}`);
|
|
31775
32137
|
return 'The focus on it ended all the same, and what the other channels held was delivered.';
|
|
31776
32138
|
},
|
|
31777
32139
|
focusChannelResolver: () => this.tuneOutCoordinator?.focusChannel() ?? null,
|
|
@@ -31948,13 +32310,11 @@ export class AgentFramework {
|
|
|
31948
32310
|
attempt: 0,
|
|
31949
32311
|
willRetry: !!left?.willReconnect,
|
|
31950
32312
|
});
|
|
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}`);
|
|
32313
|
+
// So the server doesn't vanish quietly: nothing connects it again on
|
|
32314
|
+
// its own, so alert at once. One that connected before its setup
|
|
32315
|
+
// threw is still connected, and gets no alert.
|
|
32316
|
+
if (!left?.willReconnect && !left?.isConnected) {
|
|
32317
|
+
this.opsAlert('mcpl-down', config.id, `MCPL server not connected at startup, and this host won't retry it on its own: ${message}`);
|
|
31958
32318
|
}
|
|
31959
32319
|
}
|
|
31960
32320
|
}
|
|
@@ -33892,9 +34252,10 @@ export class AgentFramework {
|
|
|
33892
34252
|
|
|
33893
34253
|
/**
|
|
33894
34254
|
* Restart an MCPL server at runtime: disconnect, then reconnect with the
|
|
33895
|
-
* same (or an updated) config. This
|
|
33896
|
-
*
|
|
33897
|
-
*
|
|
34255
|
+
* same (or an updated) config. This respawns a stdio child at once,
|
|
34256
|
+
* whatever its state; `reconnect: true` spawns a fresh child only after
|
|
34257
|
+
* the connection fails or its child exits. Throws if the server has no
|
|
34258
|
+
* stored config and none is given.
|
|
33898
34259
|
* What its connect would refuse is refused before the running server is
|
|
33899
34260
|
* torn down. Until the connect, a connect or a restart started meanwhile
|
|
33900
34261
|
* is refused the id and the tool prefix the restart moves the server to,
|
|
@@ -34376,12 +34737,9 @@ export class AgentFramework {
|
|
|
34376
34737
|
// request turned away at stop gets, so it can send it again to
|
|
34377
34738
|
// the host's next run. Optional chaining: prototype-built
|
|
34378
34739
|
// harnesses leave the queue unset.
|
|
34379
|
-
const
|
|
34740
|
+
const answer = this.queue?.isClosed ? hostStoppedWhileHandling() : { code: -32603, message: 'Internal error' };
|
|
34380
34741
|
try {
|
|
34381
|
-
tracked.respondError?.(
|
|
34382
|
-
stopping ? -32000 : -32603,
|
|
34383
|
-
stopping ? 'the host stopped while handling this request' : 'Internal error',
|
|
34384
|
-
);
|
|
34742
|
+
tracked.respondError?.(answer.code, answer.message);
|
|
34385
34743
|
} catch (sendError) {
|
|
34386
34744
|
responseError = recorded(describeCause(sendError));
|
|
34387
34745
|
}
|
|
@@ -34538,6 +34896,7 @@ export class AgentFramework {
|
|
|
34538
34896
|
respond: (result: unknown) => void;
|
|
34539
34897
|
respondError: (code: number, message: string) => void;
|
|
34540
34898
|
sendChunk?: (chunk: InferenceChunkParams) => void;
|
|
34899
|
+
signal?: AbortSignal;
|
|
34541
34900
|
},
|
|
34542
34901
|
) => {
|
|
34543
34902
|
// RFC-006 §5.3 / SPEC §10.7: no inference from inside a render.
|
|
@@ -34563,6 +34922,7 @@ export class AgentFramework {
|
|
|
34563
34922
|
// Chunks go out on the transport the request came on, as its
|
|
34564
34923
|
// answer does.
|
|
34565
34924
|
...(responder.sendChunk ? { sendChunk: responder.sendChunk } : {}),
|
|
34925
|
+
...(responder.signal ? { signal: responder.signal } : {}),
|
|
34566
34926
|
});
|
|
34567
34927
|
}
|
|
34568
34928
|
});
|
|
@@ -34878,14 +35238,15 @@ export class AgentFramework {
|
|
|
34878
35238
|
// that next one (see refreshMcplTools).
|
|
34879
35239
|
this.noteMcplServerClosed(connection.id);
|
|
34880
35240
|
this.dropMcplToolClasses(connection.id);
|
|
35241
|
+
// The transport's own words only: a WebSocket server's close text
|
|
35242
|
+
// stays off the trace.
|
|
35243
|
+
const reason = info?.reason !== undefined && !info.reasonFromPeer ? boundFailureText(info.reason) : null;
|
|
34881
35244
|
this.emitTrace({
|
|
34882
35245
|
type: 'mcpl:server-closed',
|
|
34883
35246
|
serverId: connection.id,
|
|
34884
35247
|
code: code ?? null,
|
|
34885
35248
|
signal: signal ?? null,
|
|
34886
|
-
|
|
34887
|
-
// stays off the trace.
|
|
34888
|
-
reason: info?.reason !== undefined && !info.reasonFromPeer ? boundFailureText(info.reason) : null,
|
|
35249
|
+
reason,
|
|
34889
35250
|
willReconnect: connection.willReconnect,
|
|
34890
35251
|
});
|
|
34891
35252
|
this.emitTrace({ type: 'module:removed', moduleName: `mcpl:${connection.id}` });
|
|
@@ -34895,6 +35256,14 @@ export class AgentFramework {
|
|
|
34895
35256
|
// RFC-014: its toolsets go with it; a next connection reads them again.
|
|
34896
35257
|
this.toolsetHub?.get(connection.id)?.detach(connection);
|
|
34897
35258
|
this.handleToolsListChanged(connection.id);
|
|
35259
|
+
// Lost on its own with no reconnect coming: nothing in this host
|
|
35260
|
+
// connects it again on its own. A close the host made itself (stop(),
|
|
35261
|
+
// a disconnect, a restart) is byHost, a loss once stop() has begun is
|
|
35262
|
+
// part of the close the host is making, and a child that ended as
|
|
35263
|
+
// asked was stopped: none of them gets an alert.
|
|
35264
|
+
if (!info?.byHost && !this.stopHasBegun() && !endedAsAsked({ code, signal, pipeFailed: info?.pipeFailed })) {
|
|
35265
|
+
this.opsAlert('mcpl-down', connection.id, `MCPL server closed${closeWords(reason, code, signal)}, and this host won't reconnect it on its own`);
|
|
35266
|
+
}
|
|
34898
35267
|
}
|
|
34899
35268
|
});
|
|
34900
35269
|
}
|
|
@@ -34920,11 +35289,12 @@ export class AgentFramework {
|
|
|
34920
35289
|
this.emitTrace({ type: 'module:added', moduleName: `mcpl:${connection.id}` });
|
|
34921
35290
|
this.handleToolsListChanged(connection.id);
|
|
34922
35291
|
});
|
|
34923
|
-
const failed = (params: { error: string; attempt: number; permanent?: boolean }) => {
|
|
35292
|
+
const failed = (params: { error: string; stderrTail?: string; attempt: number; permanent?: boolean }) => {
|
|
34924
35293
|
this.emitTrace({
|
|
34925
35294
|
type: 'mcpl:server-connect-failed',
|
|
34926
35295
|
serverId: connection.id,
|
|
34927
35296
|
error: recorded(params.error),
|
|
35297
|
+
...(params.stderrTail === undefined ? {} : { stderrTail: recorded(params.stderrTail) }),
|
|
34928
35298
|
attempt: params.attempt,
|
|
34929
35299
|
willRetry: connection.willReconnect,
|
|
34930
35300
|
});
|
|
@@ -34942,7 +35312,7 @@ export class AgentFramework {
|
|
|
34942
35312
|
connection.on('error', (err: Error) => {
|
|
34943
35313
|
this.emitTrace({ type: 'mcpl:server-error', serverId: connection.id, error: recorded(err.message) });
|
|
34944
35314
|
});
|
|
34945
|
-
connection.on('close', () => {
|
|
35315
|
+
connection.on('close', (info: { reason: string; code?: number | null; signal?: NodeJS.Signals | null; pipeFailed?: boolean }) => {
|
|
34946
35316
|
// A closed provider's class hints are stale (RFC-008), as for MCPL,
|
|
34947
35317
|
// and so is a listing already under way.
|
|
34948
35318
|
this.noteMcplServerClosed(connection.id);
|
|
@@ -34950,8 +35320,9 @@ export class AgentFramework {
|
|
|
34950
35320
|
this.emitTrace({
|
|
34951
35321
|
type: 'mcpl:server-closed',
|
|
34952
35322
|
serverId: connection.id,
|
|
34953
|
-
code: null,
|
|
34954
|
-
signal: null,
|
|
35323
|
+
code: info.code ?? null,
|
|
35324
|
+
signal: info.signal ?? null,
|
|
35325
|
+
reason: info.reason,
|
|
34955
35326
|
willReconnect: connection.willReconnect,
|
|
34956
35327
|
});
|
|
34957
35328
|
this.emitTrace({ type: 'module:removed', moduleName: `mcpl:${connection.id}` });
|
|
@@ -34971,6 +35342,13 @@ export class AgentFramework {
|
|
|
34971
35342
|
// Gone for good: its tools leave the agents' list, as a
|
|
34972
35343
|
// disconnect's do.
|
|
34973
35344
|
this.handleToolsListChanged(connection.id);
|
|
35345
|
+
// Lost on its own: nothing in this host connects it again on its
|
|
35346
|
+
// own. A loss once stop() has begun is part of the close the host
|
|
35347
|
+
// is making, and a child that ended as asked was stopped: neither
|
|
35348
|
+
// gets an alert.
|
|
35349
|
+
if (!this.stopHasBegun() && !endedAsAsked(info)) {
|
|
35350
|
+
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`);
|
|
35351
|
+
}
|
|
34974
35352
|
});
|
|
34975
35353
|
}
|
|
34976
35354
|
});
|
|
@@ -36135,7 +36513,7 @@ export class AgentFramework {
|
|
|
36135
36513
|
* (same path as gate.json load), so an invalid rule is surfaced as a tool
|
|
36136
36514
|
* error and nothing is written.
|
|
36137
36515
|
*/
|
|
36138
|
-
private dispatchWakeRuleToolCall(agentName: string, call: ToolCall): void {
|
|
36516
|
+
private dispatchWakeRuleToolCall(agentName: string, call: ToolCall, deliver = this.pushToolResult(agentName, call, 'gate')): void {
|
|
36139
36517
|
this.emitTrace({ type: 'tool:started', module: 'gate', tool: call.name, callId: call.id, input: call.input });
|
|
36140
36518
|
// What wake_add_rule's input held that does nothing, checkToolInput's notes and
|
|
36141
36519
|
// then the gate's on a rule it saved: a refusal names it too, so the call that
|
|
@@ -36150,14 +36528,23 @@ export class AgentFramework {
|
|
|
36150
36528
|
module: 'gate', tool: call.name, callId: call.id, durationMs: 0,
|
|
36151
36529
|
...(result.isError ? { error: result.error } : {}),
|
|
36152
36530
|
});
|
|
36153
|
-
|
|
36531
|
+
deliver(result);
|
|
36154
36532
|
};
|
|
36155
36533
|
|
|
36156
36534
|
try {
|
|
36157
36535
|
if (call.name === 'wake_remove_rule') {
|
|
36158
36536
|
const input = (call.input ?? {}) as { name?: unknown };
|
|
36159
36537
|
if (typeof input.name !== 'string' || !input.name) {
|
|
36160
|
-
|
|
36538
|
+
const edit = this.eventGate!.ruleNamesForChange();
|
|
36539
|
+
const names = 'names' in edit ? edit.names.map((name) => quotedValue(name)) : [];
|
|
36540
|
+
finish({
|
|
36541
|
+
success: false,
|
|
36542
|
+
error: `wake_remove_rule: name is ${shownValue(input.name)}, where the name of a rule is needed, so nothing was removed. `
|
|
36543
|
+
+ ('refused' in edit
|
|
36544
|
+
? edit.refused
|
|
36545
|
+
: (names.length > 0 ? `The rules a change edits are ${names.join(', ')}.` : 'There is no rule to remove.') + edit.note),
|
|
36546
|
+
isError: true,
|
|
36547
|
+
});
|
|
36161
36548
|
return;
|
|
36162
36549
|
}
|
|
36163
36550
|
const removed = this.removeGatePolicy(input.name);
|
|
@@ -36194,9 +36581,8 @@ export class AgentFramework {
|
|
|
36194
36581
|
if (behaviorSources.length === 0) {
|
|
36195
36582
|
finish({
|
|
36196
36583
|
success: false,
|
|
36197
|
-
error:
|
|
36198
|
-
? '
|
|
36199
|
-
: 'wake_add_rule: specify exactly one behavior — `behavior` ("always"/"defer"/"skip"), `debounceMs`, `rateLimit`, or `passiveSample`.',
|
|
36584
|
+
error: 'wake_add_rule got no behavior, so nothing was saved. Give one of behavior ("always", "defer" or "skip"), '
|
|
36585
|
+
+ (this.judgeWired ? 'debounceMs, rateLimit, passiveSample or judge.' : 'debounceMs, rateLimit or passiveSample.'),
|
|
36200
36586
|
isError: true,
|
|
36201
36587
|
});
|
|
36202
36588
|
return;
|
|
@@ -36204,7 +36590,7 @@ export class AgentFramework {
|
|
|
36204
36590
|
if (behaviorSources.length > 1) {
|
|
36205
36591
|
finish({
|
|
36206
36592
|
success: false,
|
|
36207
|
-
error: `wake_add_rule
|
|
36593
|
+
error: `wake_add_rule got more than one behavior (${behaviorSources.join(', ')}), so nothing was saved. Give only one of them.`,
|
|
36208
36594
|
isError: true,
|
|
36209
36595
|
});
|
|
36210
36596
|
return;
|
|
@@ -36247,7 +36633,7 @@ export class AgentFramework {
|
|
|
36247
36633
|
if (placementSources.length > 1) {
|
|
36248
36634
|
finish({
|
|
36249
36635
|
success: false,
|
|
36250
|
-
error: `wake_add_rule
|
|
36636
|
+
error: `wake_add_rule got more than one placement (${placementSources.join(', ')}), so nothing was saved. Give only one of them.`,
|
|
36251
36637
|
isError: true,
|
|
36252
36638
|
});
|
|
36253
36639
|
return;
|
|
@@ -36267,10 +36653,18 @@ export class AgentFramework {
|
|
|
36267
36653
|
for (const field of ['insertBefore', 'insertAfter'] as const) {
|
|
36268
36654
|
const anchor = input[field];
|
|
36269
36655
|
if (anchor !== undefined && (typeof anchor !== 'string' || anchor.length === 0)) {
|
|
36656
|
+
const edit = this.eventGate!.ruleNamesForChange();
|
|
36657
|
+
const names = 'names' in edit ? edit.names.filter((name) => name !== input.name).map((name) => quotedValue(name)) : [];
|
|
36270
36658
|
finish({
|
|
36271
36659
|
success: false,
|
|
36272
|
-
error: `wake_add_rule: ${field} must be the name of a rule, and it's ${shownValue(anchor)}, so nothing `
|
|
36273
|
-
+
|
|
36660
|
+
error: `wake_add_rule: ${field} must be the name of a rule, and it's ${shownValue(anchor)}, so nothing was saved. `
|
|
36661
|
+
+ ('refused' in edit
|
|
36662
|
+
? edit.refused
|
|
36663
|
+
: (names.length > 0
|
|
36664
|
+
? `The other rules a change edits are ${names.join(', ')}. Give ${field} one of them to place the rule beside it, or leave it out`
|
|
36665
|
+
: `There is no other rule to place it beside, so leave ${field} out`)
|
|
36666
|
+
+ ' to add the rule at the end, or, where a rule already has its name, to replace that rule where it is.'
|
|
36667
|
+
+ edit.note),
|
|
36274
36668
|
isError: true,
|
|
36275
36669
|
});
|
|
36276
36670
|
return;
|
|
@@ -36300,9 +36694,12 @@ export class AgentFramework {
|
|
|
36300
36694
|
const before = new Map(
|
|
36301
36695
|
this.eventGate!.probeTable().map((r) => [r.probe, r]),
|
|
36302
36696
|
);
|
|
36303
|
-
const
|
|
36697
|
+
const fieldNames = Object.fromEntries(Object.entries(AgentFramework.WAKE_ADD_RULE_FIELDS)
|
|
36698
|
+
.filter(([field]) => !(field.startsWith('behavior.') && typeof input.behavior === 'object')));
|
|
36699
|
+
const policy = this.eventGate!.addPolicy(rawPolicy, {
|
|
36304
36700
|
...(position ? { position } : {}),
|
|
36305
36701
|
onIgnoredFields: (notes) => { ignored.push(...notes); },
|
|
36702
|
+
fieldNames,
|
|
36306
36703
|
});
|
|
36307
36704
|
const after = this.eventGate!.probeTable();
|
|
36308
36705
|
const probes = after.map((r) => {
|
|
@@ -36615,21 +37012,19 @@ export class AgentFramework {
|
|
|
36615
37012
|
return decision;
|
|
36616
37013
|
}
|
|
36617
37014
|
|
|
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 {
|
|
37015
|
+
private commitDeliveries(): boolean {
|
|
37016
|
+
let durable = true;
|
|
36624
37017
|
if (this.deferredMessages.length > 0) {
|
|
36625
37018
|
this.deferredWritesPersisted = true;
|
|
36626
|
-
this.persistDeferredWrites();
|
|
37019
|
+
durable = this.persistDeferredWrites();
|
|
36627
37020
|
}
|
|
36628
37021
|
try {
|
|
36629
37022
|
this.store.sync();
|
|
36630
37023
|
} catch (err) {
|
|
36631
37024
|
console.error('[tune-out] could not sync the store after a release:', err);
|
|
37025
|
+
return false;
|
|
36632
37026
|
}
|
|
37027
|
+
return durable;
|
|
36633
37028
|
}
|
|
36634
37029
|
|
|
36635
37030
|
/** The history-reading tool the primary resident actually has, if any (#232 gap 11). */
|
|
@@ -36640,7 +37035,7 @@ export class AgentFramework {
|
|
|
36640
37035
|
return names.find((n) => /(^|--)(fetch_history|channel_read|read_history|history)$/.test(n)) ?? null;
|
|
36641
37036
|
}
|
|
36642
37037
|
|
|
36643
|
-
private dispatchSleepToolCall(agentName: string, call: ToolCall): void {
|
|
37038
|
+
private dispatchSleepToolCall(agentName: string, call: ToolCall, deliver = this.pushToolResult(agentName, call, 'gate')): void {
|
|
36644
37039
|
// The gate's sleep quiets the whole agent, every conversation of it, so a
|
|
36645
37040
|
// conversation fork or an ephemeral run neither sets one nor ends the
|
|
36646
37041
|
// agent's own. Refused before the tool runs, as a fork's channel_open is.
|
|
@@ -36659,7 +37054,7 @@ export class AgentFramework {
|
|
|
36659
37054
|
? `Nothing was set: a sleep quiets the whole agent, not one ${one}.${comeBack}`
|
|
36660
37055
|
: `Nothing was changed: a sleep is the agent's, so this ${one} can't end one.`;
|
|
36661
37056
|
this.emitTrace({ type: 'tool:failed', module: 'gate', tool: call.name, callId: call.id, error: `${agentName}: ${error}` });
|
|
36662
|
-
|
|
37057
|
+
deliver(this.refusedResult(agentName, call.id, error));
|
|
36663
37058
|
return;
|
|
36664
37059
|
}
|
|
36665
37060
|
this.emitTrace({ type: 'tool:started', module: 'gate', tool: call.name, callId: call.id, input: call.input });
|
|
@@ -36668,7 +37063,7 @@ export class AgentFramework {
|
|
|
36668
37063
|
|
|
36669
37064
|
const finish = (result: ToolResult) => {
|
|
36670
37065
|
this.emitTrace({ type: 'tool:completed', module: 'gate', tool: call.name, callId: call.id, durationMs: 0 });
|
|
36671
|
-
|
|
37066
|
+
deliver(result);
|
|
36672
37067
|
};
|
|
36673
37068
|
|
|
36674
37069
|
if (call.name === 'wake') {
|
|
@@ -36788,30 +37183,34 @@ export class AgentFramework {
|
|
|
36788
37183
|
*/
|
|
36789
37184
|
/** RFC-005 on-demand dereference: [ref id] → workspace file, via the
|
|
36790
37185
|
* origin-bound, ceiling-bounded, digest-verified host fetcher. */
|
|
36791
|
-
private dispatchFetchReferenceToolCall(agentName: string, call: ToolCall): void {
|
|
37186
|
+
private dispatchFetchReferenceToolCall(agentName: string, call: ToolCall, deliver = this.pushToolResult(agentName, call, 'workspace')): void {
|
|
36792
37187
|
this.emitTrace({ type: 'tool:started', module: 'workspace', tool: call.name, callId: call.id, input: call.input });
|
|
36793
37188
|
const startTime = Date.now();
|
|
36794
37189
|
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
37190
|
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
|
-
|
|
37191
|
+
try {
|
|
37192
|
+
const args = (call.input ?? {}) as Record<string, unknown>;
|
|
37193
|
+
const refId = String(args.ref_id ?? '');
|
|
37194
|
+
const record = referenceRegistry.get(refId);
|
|
37195
|
+
if (!record) {
|
|
37196
|
+
// Vector 20: a stale or unknown id is a defined miss, never a
|
|
37197
|
+
// different record.
|
|
37198
|
+
result = { success: false, isError: true,
|
|
37199
|
+
error: `unknown reference: ${refId || '(missing ref_id)'} — it may have been evicted; ids are never reused` };
|
|
37200
|
+
} else {
|
|
37201
|
+
const maxBytes = typeof args.max_bytes === 'number'
|
|
37202
|
+
? Math.min(args.max_bytes, DEFAULT_FETCH_MAX_BYTES) : undefined;
|
|
37203
|
+
const outcome = await this.getReferenceFetcher().fetch(record, { maxBytes });
|
|
37204
|
+
result = outcome.ok
|
|
37205
|
+
? { success: true, data: JSON.stringify({ path: outcome.path, bytes: outcome.bytes,
|
|
37206
|
+
mime_type: outcome.mimeType, digest_verified: outcome.digestVerified ?? 'no digest claimed' }) }
|
|
37207
|
+
// The fetcher says what happened as a clause about the reference,
|
|
37208
|
+
// and what calling again can do as a sentence of its own.
|
|
37209
|
+
: { success: false, isError: true,
|
|
37210
|
+
error: `fetch_reference couldn't fetch "${refId}": ${outcome.error}. ${outcome.remedy}` };
|
|
37211
|
+
}
|
|
37212
|
+
} catch (error) {
|
|
37213
|
+
result = { success: false, isError: true, error: `fetch_reference couldn't answer: ${thrownText(error)}` };
|
|
36815
37214
|
}
|
|
36816
37215
|
this.emitTrace({
|
|
36817
37216
|
type: result.isError ? 'tool:failed' : 'tool:completed',
|
|
@@ -36819,14 +37218,11 @@ export class AgentFramework {
|
|
|
36819
37218
|
durationMs: Date.now() - startTime,
|
|
36820
37219
|
...(result.isError ? { error: result.error } : {}),
|
|
36821
37220
|
});
|
|
36822
|
-
|
|
36823
|
-
type: 'tool-result', callId: call.id, toolName: call.name, agentName, moduleName: 'workspace',
|
|
36824
|
-
result: { ...result, isError: result.isError ?? false },
|
|
36825
|
-
});
|
|
37221
|
+
deliver({ ...result, isError: result.isError ?? false });
|
|
36826
37222
|
})();
|
|
36827
37223
|
}
|
|
36828
37224
|
|
|
36829
|
-
private dispatchReadImageToolCall(agentName: string, call: ToolCall): void {
|
|
37225
|
+
private dispatchReadImageToolCall(agentName: string, call: ToolCall, deliver = this.pushToolResult(agentName, call, 'workspace')): void {
|
|
36830
37226
|
this.emitTrace({ type: 'tool:started', module: 'workspace', tool: call.name, callId: call.id, input: call.input });
|
|
36831
37227
|
const finish = (result: ToolResult): void => {
|
|
36832
37228
|
this.emitTrace({
|
|
@@ -36837,7 +37233,7 @@ export class AgentFramework {
|
|
|
36837
37233
|
durationMs: 0,
|
|
36838
37234
|
...(result.isError ? { error: result.error } : {}),
|
|
36839
37235
|
});
|
|
36840
|
-
|
|
37236
|
+
deliver(result);
|
|
36841
37237
|
};
|
|
36842
37238
|
void (async (): Promise<void> => {
|
|
36843
37239
|
try {
|
|
@@ -36929,13 +37325,11 @@ export class AgentFramework {
|
|
|
36929
37325
|
* every sibling in the round to settle; if one never does, fail closed
|
|
36930
37326
|
* rather than trust an index the batch may still change. Sibling saves
|
|
36931
37327
|
* 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.
|
|
37328
|
+
* otherwise deadlock into a double timeout). Only the round's own calls
|
|
37329
|
+
* wait: a call that isn't among its pending calls, a script's (`pytc-…`)
|
|
37330
|
+
* or an awaited call's, counts the images the agent has now.
|
|
36936
37331
|
*/
|
|
36937
37332
|
private async awaitSiblingToolCalls(agent: Agent, call: ToolCall): Promise<void> {
|
|
36938
|
-
if (call.id.startsWith('pytc-')) return;
|
|
36939
37333
|
const BARRIER_MS = 120_000;
|
|
36940
37334
|
const POLL_MS = 20;
|
|
36941
37335
|
const startedAt = Date.now();
|
|
@@ -36943,6 +37337,7 @@ export class AgentFramework {
|
|
|
36943
37337
|
const state = agent.state;
|
|
36944
37338
|
// Idle (puppet path) or the round was torn down: nothing to wait on.
|
|
36945
37339
|
if (state.status !== 'waiting_for_tools') return;
|
|
37340
|
+
if (!state.pending.has(call.id)) return;
|
|
36946
37341
|
const open = [...state.pending.values()].filter((p) => p.id !== call.id && p.name !== 'save_recent_image');
|
|
36947
37342
|
if (open.length === 0) return;
|
|
36948
37343
|
if (Date.now() - startedAt > BARRIER_MS) {
|
|
@@ -36971,7 +37366,7 @@ export class AgentFramework {
|
|
|
36971
37366
|
* nothing. It never slides to an older image — a filename must not
|
|
36972
37367
|
* authenticate bytes from another surface.
|
|
36973
37368
|
*/
|
|
36974
|
-
private dispatchSaveImageToolCall(agentName: string, call: ToolCall): void {
|
|
37369
|
+
private dispatchSaveImageToolCall(agentName: string, call: ToolCall, deliver = this.pushToolResult(agentName, call, 'workspace')): void {
|
|
36975
37370
|
this.emitTrace({ type: 'tool:started', module: 'workspace', tool: call.name, callId: call.id, input: call.input });
|
|
36976
37371
|
const finish = (result: ToolResult): void => {
|
|
36977
37372
|
this.emitTrace({
|
|
@@ -36982,7 +37377,7 @@ export class AgentFramework {
|
|
|
36982
37377
|
durationMs: 0,
|
|
36983
37378
|
...(result.isError ? { error: result.error } : {}),
|
|
36984
37379
|
});
|
|
36985
|
-
|
|
37380
|
+
deliver(result);
|
|
36986
37381
|
};
|
|
36987
37382
|
type Candidate =
|
|
36988
37383
|
| { kind: 'attachment'; data: string | null; mediaType: string; messagesBack: number }
|
|
@@ -37133,20 +37528,39 @@ export class AgentFramework {
|
|
|
37133
37528
|
let count = 1;
|
|
37134
37529
|
/** Direct-ref mode: the one ref to find in the scannable context. */
|
|
37135
37530
|
let wantedRef: string | null = null;
|
|
37136
|
-
|
|
37531
|
+
// A selector is in use only when it carries a value (fieldInUse), so a
|
|
37532
|
+
// caller whose provider presents every property as required can still
|
|
37533
|
+
// choose one, whichever of the two it fills the other selector with.
|
|
37534
|
+
// Any other value is checked, never read as absent: a whitespace or
|
|
37535
|
+
// malformed ref, or an index that isn't an integer, is refused rather
|
|
37536
|
+
// than turned into a different image than the one the caller named.
|
|
37537
|
+
// A malformed ref beside an index or count in use says how to leave a
|
|
37538
|
+
// selector unused, since the caller may have meant the positional one, as
|
|
37539
|
+
// the conflict refusal below does for a well-formed ref. No refusal of one
|
|
37540
|
+
// selector on its own says it: a ref alone is the caller's one selector,
|
|
37541
|
+
// and dropping it would save the newest image; an index and a count are
|
|
37542
|
+
// one selector, and dropping either changes which images are saved.
|
|
37543
|
+
if (fieldInUse(input.ref)) {
|
|
37137
37544
|
if (typeof input.ref !== 'string' || !TOOL_IMAGE_REF_RE.test(input.ref)) {
|
|
37138
|
-
|
|
37545
|
+
const positionalInUse = fieldInUse(input.index) || fieldInUse(input.count);
|
|
37546
|
+
throw new Error(
|
|
37547
|
+
'save_recent_image: `ref` must look like "img_k7x3q2_7" (copy it from the image placeholder in your context)' +
|
|
37548
|
+
(positionalInUse ? ' (pass null, or leave it out, for a selector you are not using)' : ''),
|
|
37549
|
+
);
|
|
37139
37550
|
}
|
|
37140
|
-
if (input.index
|
|
37141
|
-
throw new Error(
|
|
37551
|
+
if (fieldInUse(input.index) || fieldInUse(input.count)) {
|
|
37552
|
+
throw new Error(
|
|
37553
|
+
'save_recent_image: `ref` is mutually exclusive with `index`/`count` — pass null (or omit) ' +
|
|
37554
|
+
'for the selector you are not using',
|
|
37555
|
+
);
|
|
37142
37556
|
}
|
|
37143
37557
|
wantedRef = input.ref;
|
|
37144
37558
|
} else {
|
|
37145
|
-
index = input.index
|
|
37559
|
+
index = fieldInUse(input.index) ? integerField(input.index) : 0;
|
|
37146
37560
|
if (!Number.isInteger(index) || index < 0) {
|
|
37147
37561
|
throw new Error('save_recent_image: `index` must be a non-negative integer');
|
|
37148
37562
|
}
|
|
37149
|
-
count = input.count
|
|
37563
|
+
count = fieldInUse(input.count) ? integerField(input.count) : 1;
|
|
37150
37564
|
const MAX_COUNT = 20;
|
|
37151
37565
|
if (!Number.isInteger(count) || count < 1 || count > MAX_COUNT) {
|
|
37152
37566
|
throw new Error(`save_recent_image: \`count\` must be an integer in 1..${MAX_COUNT}`);
|
|
@@ -37408,7 +37822,7 @@ export class AgentFramework {
|
|
|
37408
37822
|
: `Changed before the refusal: ${changed.join(', ')}; no other setting reads differently.`;
|
|
37409
37823
|
}
|
|
37410
37824
|
|
|
37411
|
-
private dispatchAgentSettingsToolCall(agentName: string, call: ToolCall): void {
|
|
37825
|
+
private dispatchAgentSettingsToolCall(agentName: string, call: ToolCall, deliver = this.pushToolResult(agentName, call, 'agent')): void {
|
|
37412
37826
|
this.emitTrace({
|
|
37413
37827
|
type: 'tool:started',
|
|
37414
37828
|
module: 'agent',
|
|
@@ -37596,14 +38010,7 @@ export class AgentFramework {
|
|
|
37596
38010
|
durationMs: 0,
|
|
37597
38011
|
...(result.isError ? { error: result.error } : {}),
|
|
37598
38012
|
});
|
|
37599
|
-
|
|
37600
|
-
type: 'tool-result',
|
|
37601
|
-
callId: call.id,
|
|
37602
|
-
toolName: call.name,
|
|
37603
|
-
agentName,
|
|
37604
|
-
moduleName: 'agent',
|
|
37605
|
-
result,
|
|
37606
|
-
});
|
|
38013
|
+
deliver(result);
|
|
37607
38014
|
}
|
|
37608
38015
|
|
|
37609
38016
|
/** Aggregate the event-tag vocabulary: reserved chat:* core + each connected
|
|
@@ -37628,7 +38035,7 @@ export class AgentFramework {
|
|
|
37628
38035
|
};
|
|
37629
38036
|
}
|
|
37630
38037
|
|
|
37631
|
-
private dispatchEventTagsToolCall(agentName: string, call: ToolCall): void {
|
|
38038
|
+
private dispatchEventTagsToolCall(agentName: string, call: ToolCall, deliver = this.pushToolResult(agentName, call, 'gate')): void {
|
|
37632
38039
|
this.emitTrace({ type: 'tool:started', module: 'gate', tool: call.name, callId: call.id, input: call.input });
|
|
37633
38040
|
let result: import('./types/events.js').ToolResult;
|
|
37634
38041
|
try {
|
|
@@ -37640,10 +38047,10 @@ export class AgentFramework {
|
|
|
37640
38047
|
this.toolLifecycleEmitter?.markDispatchFailure(agentName, call.id);
|
|
37641
38048
|
result = { success: false, error: err.message, isError: true };
|
|
37642
38049
|
}
|
|
37643
|
-
|
|
38050
|
+
deliver(result);
|
|
37644
38051
|
}
|
|
37645
38052
|
|
|
37646
|
-
private dispatchGateToolCall(agentName: string, call: ToolCall): void {
|
|
38053
|
+
private dispatchGateToolCall(agentName: string, call: ToolCall, deliver = this.pushToolResult(agentName, call, 'gate')): void {
|
|
37647
38054
|
this.emitTrace({ type: 'tool:started', module: 'gate', tool: call.name, callId: call.id, input: call.input });
|
|
37648
38055
|
const startTime = Date.now();
|
|
37649
38056
|
|
|
@@ -37651,27 +38058,13 @@ export class AgentFramework {
|
|
|
37651
38058
|
.then((result) => {
|
|
37652
38059
|
const durationMs = Date.now() - startTime;
|
|
37653
38060
|
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
|
-
});
|
|
38061
|
+
deliver(result);
|
|
37662
38062
|
})
|
|
37663
38063
|
.catch((error) => {
|
|
37664
38064
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
37665
38065
|
this.emitTrace({ type: 'tool:failed', module: 'gate', tool: call.name, callId: call.id, error: err.message });
|
|
37666
38066
|
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
|
-
});
|
|
38067
|
+
deliver({ success: false, error: err.message, isError: true });
|
|
37675
38068
|
});
|
|
37676
38069
|
}
|
|
37677
38070
|
|