@animalabs/agent-framework 0.28.0 → 0.29.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 +88 -25
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +4 -1
- 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 +3 -12
- package/dist/src/api/mcp-server-core.js.map +1 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +7 -3
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/auto-continue.d.ts +2 -0
- package/dist/src/auto-continue.d.ts.map +1 -1
- package/dist/src/auto-continue.js +5 -2
- package/dist/src/auto-continue.js.map +1 -1
- package/dist/src/code-execution/isolate/runner.d.ts +7 -5
- package/dist/src/code-execution/isolate/runner.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/runner.js +7 -5
- package/dist/src/code-execution/isolate/runner.js.map +1 -1
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +3 -2
- package/dist/src/code-execution/isolate/scripts.js.map +1 -1
- package/dist/src/framework.d.ts +164 -36
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1120 -479
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +34 -10
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +118 -48
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/index.d.ts +1 -1
- package/dist/src/gate/index.d.ts.map +1 -1
- package/dist/src/gate/index.js +1 -1
- package/dist/src/gate/index.js.map +1 -1
- package/dist/src/gate/types.d.ts +12 -2
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/heartbeat/heartbeat.d.ts +18 -0
- package/dist/src/heartbeat/heartbeat.d.ts.map +1 -1
- package/dist/src/heartbeat/heartbeat.js +34 -8
- package/dist/src/heartbeat/heartbeat.js.map +1 -1
- package/dist/src/held-writes.d.ts +3 -3
- package/dist/src/held-writes.js +3 -3
- package/dist/src/host-presentation.d.ts +170 -0
- package/dist/src/host-presentation.d.ts.map +1 -0
- package/dist/src/host-presentation.js +561 -0
- package/dist/src/host-presentation.js.map +1 -0
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/line-reader.d.ts +4 -0
- package/dist/src/line-reader.d.ts.map +1 -0
- package/dist/src/line-reader.js +59 -0
- package/dist/src/line-reader.js.map +1 -0
- package/dist/src/mcpl/channel-registry.d.ts +34 -6
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +131 -53
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +2 -1
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/convert-block.d.ts +1 -0
- package/dist/src/mcpl/convert-block.d.ts.map +1 -1
- package/dist/src/mcpl/convert-block.js +16 -4
- package/dist/src/mcpl/convert-block.js.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.js +4 -3
- package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +3 -1
- package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
- package/dist/src/mcpl/inbound-source.js +3 -1
- package/dist/src/mcpl/inbound-source.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
- package/dist/src/mcpl/modern-connection.js +11 -7
- package/dist/src/mcpl/modern-connection.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts +3 -2
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +25 -3
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-handler.js +2 -2
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +12 -40
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +29 -23
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +20 -20
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/skip-reply-wake.d.ts +25 -0
- package/dist/src/mcpl/skip-reply-wake.d.ts.map +1 -1
- package/dist/src/mcpl/skip-reply-wake.js +25 -0
- package/dist/src/mcpl/skip-reply-wake.js.map +1 -1
- package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
- package/dist/src/mcpl/tool-presence.js +18 -17
- package/dist/src/mcpl/tool-presence.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
- package/dist/src/mcpl/tool-signature.js +17 -16
- package/dist/src/mcpl/tool-signature.js.map +1 -1
- package/dist/src/mcpl/toolset-host.d.ts +8 -26
- package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
- package/dist/src/mcpl/toolset-host.js +16 -6
- package/dist/src/mcpl/toolset-host.js.map +1 -1
- package/dist/src/mcpl/toolsets-module.d.ts +14 -25
- package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
- package/dist/src/mcpl/toolsets-module.js +180 -50
- package/dist/src/mcpl/toolsets-module.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +1 -1
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +16 -16
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +28 -12
- 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 +6 -7
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +6 -7
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/api/index.d.ts.map +1 -1
- package/dist/src/modules/api/index.js +4 -2
- package/dist/src/modules/api/index.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +33 -25
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/history/index.js +8 -6
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/sender-name.d.ts +7 -0
- package/dist/src/modules/sender-name.d.ts.map +1 -0
- package/dist/src/modules/sender-name.js +10 -0
- package/dist/src/modules/sender-name.js.map +1 -0
- package/dist/src/modules/workspace/answers.d.ts +5 -1
- package/dist/src/modules/workspace/answers.d.ts.map +1 -1
- package/dist/src/modules/workspace/answers.js +16 -1
- package/dist/src/modules/workspace/answers.js.map +1 -1
- package/dist/src/modules/workspace/direct-write.d.ts.map +1 -1
- package/dist/src/modules/workspace/direct-write.js +6 -4
- package/dist/src/modules/workspace/direct-write.js.map +1 -1
- package/dist/src/modules/workspace/effects.d.ts +4 -2
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +50 -40
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +1 -0
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +66 -36
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +51 -20
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +120 -82
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts +7 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +133 -37
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/notice-tags.d.ts +79 -0
- package/dist/src/notice-tags.d.ts.map +1 -0
- package/dist/src/notice-tags.js +83 -0
- package/dist/src/notice-tags.js.map +1 -0
- package/dist/src/reasoning-loss-notice.d.ts.map +1 -1
- package/dist/src/reasoning-loss-notice.js +5 -3
- package/dist/src/reasoning-loss-notice.js.map +1 -1
- package/dist/src/silent-heartbeat.d.ts +1 -53
- package/dist/src/silent-heartbeat.d.ts.map +1 -1
- package/dist/src/silent-heartbeat.js +56 -3
- package/dist/src/silent-heartbeat.js.map +1 -1
- package/dist/src/supplied-text.d.ts +35 -0
- package/dist/src/supplied-text.d.ts.map +1 -0
- package/dist/src/supplied-text.js +141 -0
- package/dist/src/supplied-text.js.map +1 -0
- package/dist/src/tool-args.d.ts.map +1 -1
- package/dist/src/tool-args.js +9 -8
- package/dist/src/tool-args.js.map +1 -1
- package/dist/src/tool-image-ledger.d.ts.map +1 -1
- package/dist/src/tool-image-ledger.js +2 -1
- package/dist/src/tool-image-ledger.js.map +1 -1
- package/dist/src/tool-presentation.d.ts.map +1 -1
- package/dist/src/tool-presentation.js +5 -4
- package/dist/src/tool-presentation.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +3 -2
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +24 -26
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/types/agent.d.ts +23 -9
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +43 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +22 -11
- package/dist/src/types/module.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/agent.ts +4 -1
- package/src/api/mcp-server-core.ts +3 -14
- package/src/api/server.ts +7 -3
- package/src/auto-continue.ts +8 -2
- package/src/code-execution/isolate/runner.ts +7 -5
- package/src/code-execution/isolate/scripts.ts +3 -2
- package/src/framework.ts +1132 -492
- package/src/gate/event-gate.ts +128 -47
- package/src/gate/index.ts +1 -1
- package/src/gate/types.ts +12 -2
- package/src/heartbeat/heartbeat.ts +48 -9
- package/src/held-writes.ts +3 -3
- package/src/host-presentation.ts +605 -0
- package/src/index.ts +2 -2
- package/src/line-reader.ts +58 -0
- package/src/mcpl/channel-registry.ts +137 -52
- package/src/mcpl/conversation-router.ts +2 -1
- package/src/mcpl/convert-block.ts +17 -4
- package/src/mcpl/hook-orchestrator.ts +4 -3
- package/src/mcpl/inbound-source.ts +3 -1
- package/src/mcpl/modern-connection.ts +11 -7
- package/src/mcpl/protocol-family.ts +24 -3
- package/src/mcpl/push-handler.ts +2 -2
- package/src/mcpl/references.ts +31 -23
- package/src/mcpl/server-connection.ts +20 -18
- package/src/mcpl/skip-reply-wake.ts +35 -0
- package/src/mcpl/tool-presence.ts +18 -17
- package/src/mcpl/tool-signature.ts +17 -16
- package/src/mcpl/toolset-host.ts +18 -6
- package/src/mcpl/toolsets-module.ts +142 -50
- package/src/mcpl/transport.ts +18 -14
- package/src/mcpl/types.ts +29 -12
- package/src/module-registry.ts +6 -7
- package/src/modules/api/index.ts +4 -2
- package/src/modules/discord/index.ts +37 -25
- package/src/modules/history/index.ts +8 -6
- package/src/modules/sender-name.ts +9 -0
- package/src/modules/workspace/answers.ts +20 -2
- package/src/modules/workspace/direct-write.ts +4 -3
- package/src/modules/workspace/effects.ts +50 -40
- package/src/modules/workspace/index.ts +58 -34
- package/src/modules/workspace/observe.ts +139 -88
- package/src/modules/workspace/reconcile.ts +159 -36
- package/src/notice-tags.ts +84 -0
- package/src/reasoning-loss-notice.ts +5 -3
- package/src/silent-heartbeat.ts +4 -3
- package/src/supplied-text.ts +154 -0
- package/src/tool-args.ts +9 -8
- package/src/tool-image-ledger.ts +2 -1
- package/src/tool-presentation.ts +5 -4
- package/src/tune-out/coordinator.ts +25 -27
- package/src/types/agent.ts +23 -9
- package/src/types/framework.ts +44 -0
- package/src/types/module.ts +23 -11
package/src/framework.ts
CHANGED
|
@@ -91,8 +91,8 @@ import { ToolLifecycleEmitter, parseToolObserveParams, type ScriptCallOrigin } f
|
|
|
91
91
|
import { checkToolPattern, describeUnmatchedPattern, type PatternServer } from './mcpl/tool-pattern-check.js';
|
|
92
92
|
import { BUILTIN_TOOL_CLASSES, normalizeClassTable, parseDeclaredClasses, resolveToolClass, type EffectiveToolClass, type ToolClass, type ToolClassSource } from './mcpl/tool-classes.js';
|
|
93
93
|
import { PushHandler, type McplPushEvent } from './mcpl/push-handler.js';
|
|
94
|
-
import { readSkipReplyWake, skipReplyWakeRefusal } from './mcpl/skip-reply-wake.js';
|
|
95
|
-
import {
|
|
94
|
+
import { readSkipReplyWake, skipReplyWakeRefusal, type SkipReplyEarlierWake } from './mcpl/skip-reply-wake.js';
|
|
95
|
+
import { convertBlocks } from './mcpl/convert-block.js';
|
|
96
96
|
import {
|
|
97
97
|
PushCoalescer, CoalesceError, PUSH_COALESCING_SUPPORT, validateCoalesceMember, validateCoalescedContent,
|
|
98
98
|
coalescingSubjectKey, type AssemblyResult, type CoalescedOccurrence, type CoalescingPlacement, type CoalescingSnapshot,
|
|
@@ -103,16 +103,19 @@ import { isSilentHeartbeatMarker, isVisiblyEmptyContent, NON_STRING_TEXT_PLACEHO
|
|
|
103
103
|
import { isProseTargetToken, parseProsePrefix, parseHybridProsePrefix } from './mcpl/prose-grammar.js';
|
|
104
104
|
import { detectKnownToolWrapperProse } from './tool-wrapper-prose-guard.js';
|
|
105
105
|
import { InferenceRouter } from './mcpl/inference-router.js';
|
|
106
|
-
import { ChannelRegistry, TUNE_OUT_REPLACED, connectorText, desiredStateInLog, destinationKey, journalToolDescription, NON_HOLDER_CHANNEL_DESCRIPTIONS, qualifiedChannelName, sameChannel, type ChannelRef, type ChannelToolOrigin, type PublishDestination, type PublishOutcome, type SelectedForOpening } from './mcpl/channel-registry.js';
|
|
106
|
+
import { ChannelRegistry, TUNE_OUT_REPLACED, connectorText, desiredStateInLog, destinationKey, journalToolDescription, NON_HOLDER_CHANNEL_DESCRIPTIONS, NO_CHANNEL_EXAMPLE, NO_PERSON_EXAMPLE, qualifiedChannelName, sameChannel, type ChannelRef, type ChannelToolOrigin, type PublishDestination, type PublishOutcome, type SelectedForOpening } from './mcpl/channel-registry.js';
|
|
107
107
|
import { KEEP_IDLE_MS, KEEP_NEWEST, ProseDraftStore, draftState, uncertainAttempt, type Draft, type DraftAttempt, type DraftReason, type DraftState, type InheritedRisk } from './prose-drafts.js';
|
|
108
108
|
import { ConversationRouter, CONVERSATION_ROUTING_DEPRECATION_NOTICE } from './mcpl/conversation-router.js';
|
|
109
109
|
import { INBOUND_SOURCE_KEY, sourceBodyDigest, type InboundAcceptanceObserver, type InboundChannelSource, type InboundLane, type InboundSource, type InboundUnscopedSource } from './mcpl/inbound-source.js';
|
|
110
|
+
import { markNoticeOpenings, nameToTypeBack, quotedValue, showsAsItself, suppliedValue } from './supplied-text.js';
|
|
111
|
+
import { NOTICE_TAG } from './notice-tags.js';
|
|
110
112
|
import type { IncomingMessageFacts, RoutingDecision } from './mcpl/conversation-router.js';
|
|
111
113
|
import { safeSlice } from './safe-slice.js';
|
|
112
114
|
import { SEND_TOOLS, bareToolName } from './send-tools.js';
|
|
113
115
|
import { boundedErrorMessage, errorMessage } from './error-message.js';
|
|
114
116
|
import { ProviderWaits, EVERY_MODEL, isReleaseModel, withoutControls, type ProviderWait } from './provider-waits.js';
|
|
115
117
|
import type { WorkspaceModule } from './modules/workspace/index.js';
|
|
118
|
+
import { Refusal, errnoCode, errnoWayOn, systemWords } from './modules/workspace/answers.js';
|
|
116
119
|
import type { ToolResultGuard, WithheldSpill } from './tool-result-guard.js';
|
|
117
120
|
import {
|
|
118
121
|
goesNative,
|
|
@@ -408,6 +411,12 @@ const HOST_METADATA_KEYS = [
|
|
|
408
411
|
* marker too. A marker that wakes the agent can still set that turn's speech
|
|
409
412
|
* channel (agent-framework#189); a `chat:system` notice can't (systemNotice).
|
|
410
413
|
*/
|
|
414
|
+
const isOwnScheduleWrite = (metadata?: MessageMetadata): boolean => {
|
|
415
|
+
const m = metadata as { serverId?: unknown; source?: unknown; tags?: unknown } | undefined;
|
|
416
|
+
return m?.serverId === 'heartbeat' || m?.source === 'gate:self-wake'
|
|
417
|
+
|| (Array.isArray(m?.tags) && m.tags.includes('script:wake'));
|
|
418
|
+
};
|
|
419
|
+
|
|
411
420
|
const isConversationalInjection = (metadata?: MessageMetadata): boolean => {
|
|
412
421
|
if (!metadata) return true;
|
|
413
422
|
const m = metadata as Record<string, unknown>;
|
|
@@ -669,6 +678,20 @@ const ambientMark = (tags?: string[]): { ambient?: true } =>
|
|
|
669
678
|
const systemNoticeMark = (tags?: string[]): { systemNotice?: true } =>
|
|
670
679
|
isSystemNotice(tags) ? { systemNotice: true } : {};
|
|
671
680
|
|
|
681
|
+
/**
|
|
682
|
+
* A wake from the heartbeat: a tick or a reminder, from the framework's own
|
|
683
|
+
* heartbeat or from an MCPL server whose id is `heartbeat` (heartbeat-mcpl),
|
|
684
|
+
* which carries no channel. A silent tick isn't one here: its turn keeps
|
|
685
|
+
* every plain word private already (suppressProse). Its turn's plain speech
|
|
686
|
+
* has no destination (TurnRouting.heartbeat).
|
|
687
|
+
*/
|
|
688
|
+
function isHeartbeatWake(
|
|
689
|
+
trigger: Pick<InferenceRequest, 'source' | 'channelId' | 'suppressProse' | 'heartbeatTurn'> | undefined,
|
|
690
|
+
): boolean {
|
|
691
|
+
if (trigger === undefined || trigger.channelId || trigger.suppressProse === true) return false;
|
|
692
|
+
return trigger.heartbeatTurn ?? trigger.source === 'heartbeat';
|
|
693
|
+
}
|
|
694
|
+
|
|
672
695
|
// parseProsePrefix lives in ./mcpl/prose-grammar.ts. The outgoing stream
|
|
673
696
|
// (Spec 14.3) needs no grammar of its own: it carries what delivery
|
|
674
697
|
// published.
|
|
@@ -676,12 +699,12 @@ const systemNoticeMark = (tags?: string[]): { systemNotice?: true } =>
|
|
|
676
699
|
/** One-time primer appended when an agent's proseRouting mode changes. */
|
|
677
700
|
function proseModePrimer(mode: 'explicit' | 'hybrid' | 'locus' | 'disabled', helpAvailable: boolean): string {
|
|
678
701
|
if (mode === 'locus') {
|
|
679
|
-
return
|
|
702
|
+
return `[${NOTICE_TAG.proseRouting}] Mode change: your plain text auto-routes to the ` +
|
|
680
703
|
'conversational locus again, unless its server declared that channel inbound-only. ' +
|
|
681
704
|
'`>>` destination prefixes are no longer needed.';
|
|
682
705
|
}
|
|
683
706
|
if (mode === 'hybrid') {
|
|
684
|
-
return
|
|
707
|
+
return `[${NOTICE_TAG.proseRouting}] Hybrid routing enabled: unprefixed text still lands in the ` +
|
|
685
708
|
'current conversational locus, unless its server declared that channel inbound-only. ' +
|
|
686
709
|
'A leading `>>>destination` envelope instead routes ' +
|
|
687
710
|
'that prose to one uniquely resolved authorized Discord or Eidoverse channel. The ' +
|
|
@@ -698,7 +721,7 @@ function proseModePrimer(mode: 'explicit' | 'hybrid' | 'locus' | 'disabled', hel
|
|
|
698
721
|
// prompt: those bytes belong to the recipe, and the deepest KV prefix must
|
|
699
722
|
// not vary with framework code.
|
|
700
723
|
return (
|
|
701
|
-
|
|
724
|
+
`[${NOTICE_TAG.proseRouting}] Output mode changed: plain text is delivered only with a ` +
|
|
702
725
|
'destination prefix line, e.g. ">>#channel-name your text".' +
|
|
703
726
|
(helpAvailable ? ' The prose_help tool shows the full syntax.' : '')
|
|
704
727
|
);
|
|
@@ -738,29 +761,29 @@ const PROSE_HELP_TOOL: import('./types/index.js').ToolDefinition = {
|
|
|
738
761
|
inputSchema: { type: 'object' as const, properties: {} },
|
|
739
762
|
};
|
|
740
763
|
|
|
741
|
-
/**
|
|
742
|
-
const
|
|
764
|
+
/** What a call of the removed `utils` tool is answered: where its utilities went. */
|
|
765
|
+
const UTILS_REMOVED = `utils was removed: module utilities are in the host's built-in toolset now. ` +
|
|
766
|
+
`${META_TOOL_NAMES.look} {toolset:"${BUILTIN_TOOLSET}"} lists them, ${META_TOOL_NAMES.open} {toolset:"${BUILTIN_TOOLSET}"} opens it, ` +
|
|
767
|
+
`and ${META_TOOL_NAMES.act} {tool, args} calls one. Nothing was run.`;
|
|
743
768
|
|
|
744
|
-
/**
|
|
745
|
-
*
|
|
746
|
-
*
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
},
|
|
763
|
-
};
|
|
769
|
+
/**
|
|
770
|
+
* The framework's own tools that belong to the host's built-in toolset
|
|
771
|
+
* ("settings", host-presentation.ts) rather than the tool list: the ones an
|
|
772
|
+
* agent reaches for rarely. Modules place theirs with Module.toolPlacement,
|
|
773
|
+
* and every module utility is there too.
|
|
774
|
+
*/
|
|
775
|
+
export const BUILTIN_TOOLSET_TOOLS: ReadonlySet<string> = new Set([
|
|
776
|
+
'agent_settings',
|
|
777
|
+
'wake_add_rule',
|
|
778
|
+
'wake_remove_rule',
|
|
779
|
+
'gate_status',
|
|
780
|
+
'event_tags',
|
|
781
|
+
'set_tool_visibility',
|
|
782
|
+
'set_tool_description',
|
|
783
|
+
'heartbeat--heartbeat_status',
|
|
784
|
+
'heartbeat--heartbeat_trigger',
|
|
785
|
+
'workspace--status',
|
|
786
|
+
]);
|
|
764
787
|
|
|
765
788
|
type SettingsReading = {
|
|
766
789
|
core: Record<string, string>;
|
|
@@ -784,31 +807,39 @@ const settingNumber = (name: string, value: unknown): number => {
|
|
|
784
807
|
throw new Error(`${name} must be a number, not ${shownValue(value)}`);
|
|
785
808
|
};
|
|
786
809
|
|
|
810
|
+
const DEFAULT_TURN_REARM_MS: readonly number[] = [2 * 60_000, 8 * 60_000, 32 * 60_000];
|
|
811
|
+
|
|
812
|
+
const turnRearmDelays = (value: unknown): readonly number[] => {
|
|
813
|
+
if (value === undefined) return DEFAULT_TURN_REARM_MS;
|
|
814
|
+
if (value === false) return [];
|
|
815
|
+
if (!Array.isArray(value) || value.length === 0
|
|
816
|
+
|| ![...value].every((ms) => typeof ms === 'number' && Number.isFinite(ms) && ms > 0)) {
|
|
817
|
+
throw new Error('FrameworkConfig.exhaustedTurnRearmMs must be false, or a list of one or more delays in ms, ' +
|
|
818
|
+
`each a number above 0, not ${shownValue(value)}`);
|
|
819
|
+
}
|
|
820
|
+
return [...value];
|
|
821
|
+
};
|
|
822
|
+
|
|
787
823
|
const settingBoolean = (name: string, value: unknown): boolean => {
|
|
788
824
|
if (typeof value === 'boolean') return value;
|
|
789
825
|
if (value === 'true' || value === 'false') return value === 'true';
|
|
790
826
|
throw new Error(`${name} must be true or false, not ${shownValue(value)}`);
|
|
791
827
|
};
|
|
792
828
|
|
|
793
|
-
/** First sentence (or line) of a description — what `utils list` shows. */
|
|
794
|
-
const utilityOneLiner = (description: string): string => {
|
|
795
|
-
const line = description.split('\n')[0]!;
|
|
796
|
-
const dot = line.indexOf('. ');
|
|
797
|
-
return safeSlice(dot >= 0 ? line.slice(0, dot + 1) : line, 0, 160);
|
|
798
|
-
};
|
|
799
829
|
|
|
800
830
|
import { CheckpointManager } from './mcpl/checkpoint-manager.js';
|
|
801
831
|
import { isToolAllowed } from './mcpl/tool-policy.js';
|
|
802
832
|
import { asCalled, cutOffRefusal, markedInputRefusal, toolArgsProblems } from './tool-args.js';
|
|
803
833
|
import { cutOffRound, type CutOffCall } from './cut-off-round.js';
|
|
804
|
-
import { EventGate, checkToolInput, formatShadowWarning, shownValue, wakeLineText } from './gate/event-gate.js';
|
|
834
|
+
import { EventGate, MCPL_GATE_EVENT_TYPES, checkToolInput, formatShadowWarning, shownValue, wakeLineText } from './gate/event-gate.js';
|
|
805
835
|
import { ReplyJudge } from './gate/reply-judge.js';
|
|
806
|
-
import { HEARTBEAT_CHANGING_TOOLS, Heartbeat, isHeartbeatMcplServer, type HeartbeatWake } from './heartbeat/heartbeat.js';
|
|
836
|
+
import { HEARTBEAT_CHANGING_TOOLS, Heartbeat, isHeartbeatMcplServer, type HeartbeatScheduledWake, type HeartbeatWake } from './heartbeat/heartbeat.js';
|
|
807
837
|
import type { GateEntryRef, GateEventInfo, WakeLine } from './gate/types.js';
|
|
808
838
|
import { EPHEMERAL_USAGE_NAME, UsageTracker, persistedUsageProblem, type PersistedUsageState } from './usage/usage-tracker.js';
|
|
809
839
|
import type { SessionUsageSnapshot, UsageUpdatedEvent } from './usage/types.js';
|
|
810
840
|
import { McplProtocolVersionError, McplRequestError, McplUnreapedLaunchError, stderrTailOf, toolsetDeclarationsOf, withStderr, type McplConnectionCloseInfo, type McplServerConnection } from './mcpl/server-connection.js';
|
|
811
841
|
import { ToolsetHub, TOOLSETS_MODULE, META_TOOL_NAMES, type ToolsetController } from './mcpl/toolset-host.js';
|
|
842
|
+
import { HostPresentation, BUILTIN_TOOLSET, type HostActTarget } from './host-presentation.js';
|
|
812
843
|
import { isEmptyChange, type Presence } from './mcpl/tool-presence.js';
|
|
813
844
|
import { ToolsetsModule, resolveAct, withLookHint, type TakenChange } from './mcpl/toolsets-module.js';
|
|
814
845
|
import { ModernMcpConnection } from './mcpl/modern-connection.js';
|
|
@@ -1143,13 +1174,41 @@ const TURN_RETRY: unique symbol = Symbol('turn-retry');
|
|
|
1143
1174
|
* record. A symbol, as TURN_RETRY is, so spreads and rests copy it.
|
|
1144
1175
|
*/
|
|
1145
1176
|
const MERGED_BATCH: unique symbol = Symbol('merged-batch');
|
|
1177
|
+
const REARMED: unique symbol = Symbol('rearmed');
|
|
1146
1178
|
const TURN_CARRY: unique symbol = Symbol('turn-carry');
|
|
1147
1179
|
interface TurnCarry {
|
|
1148
1180
|
silence: TurnSilence | null;
|
|
1149
1181
|
sends: SendsReached | null;
|
|
1150
1182
|
read: number;
|
|
1151
1183
|
}
|
|
1152
|
-
type MarkedRequest = InferenceRequest & { [TURN_RETRY]?: true; [MERGED_BATCH]?: InferenceRequest[]; [TURN_CARRY]?: TurnCarry };
|
|
1184
|
+
type MarkedRequest = InferenceRequest & { [TURN_RETRY]?: true; [MERGED_BATCH]?: InferenceRequest[]; [TURN_CARRY]?: TurnCarry; [REARMED]?: true };
|
|
1185
|
+
|
|
1186
|
+
function isRearmed(request: InferenceRequest | undefined): boolean {
|
|
1187
|
+
return request !== undefined && (request as MarkedRequest)[REARMED] === true;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
interface StatedWaitRun { since: number; holds: number; alerted: boolean; model: string; reason: string; heldUntil: number }
|
|
1191
|
+
|
|
1192
|
+
function waitAlertsOnItsOwn(wait: ProviderWait): boolean {
|
|
1193
|
+
return wait.until === null || wait.until - wait.setAt > PROVIDER_WAIT_ALERT_MS;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// membrane 0.8.0 marks a deadline it reports as run out with this registered
|
|
1197
|
+
// symbol (on a sent request, or the Anthropic SDK's timeout, which covers its
|
|
1198
|
+
// connect timeout too), and doesn't export its reader.
|
|
1199
|
+
const MEMBRANE_DEADLINE_EXPIRY: unique symbol = Symbol.for('membrane.deadlineExpiry');
|
|
1200
|
+
|
|
1201
|
+
function isTransientFailure(error: Error): boolean {
|
|
1202
|
+
if (!isMembraneError(error) || error.retryable !== true) return false;
|
|
1203
|
+
if ((error as { [MEMBRANE_DEADLINE_EXPIRY]?: unknown })[MEMBRANE_DEADLINE_EXPIRY] === true) return false;
|
|
1204
|
+
return error.type === 'server' || error.type === 'network' || error.type === 'timeout' || error.type === 'rate_limit';
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function gotPartway(routing: TurnRouting): boolean {
|
|
1208
|
+
return routing.roundsRan === true || (routing.unflushedRounds ?? 0) > 0;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
const REARM_REASON = 'exhausted-turn-rearm';
|
|
1153
1212
|
|
|
1154
1213
|
function mergedBatch(request: InferenceRequest): InferenceRequest[] | undefined {
|
|
1155
1214
|
return (request as MarkedRequest)[MERGED_BATCH];
|
|
@@ -1274,6 +1333,17 @@ interface TurnRouting {
|
|
|
1274
1333
|
* A fork's home still applies.
|
|
1275
1334
|
*/
|
|
1276
1335
|
noChannel?: true;
|
|
1336
|
+
/**
|
|
1337
|
+
* The turn was opened by a heartbeat tick or reminder (isHeartbeatWake): a
|
|
1338
|
+
* wake that came from no channel, so nobody is waiting for its words. It
|
|
1339
|
+
* takes no locus and no global fallback, in its locus or in what servers
|
|
1340
|
+
* are told. Its plain speech with no destination of its own (a `>>` or
|
|
1341
|
+
* `>>>` target, a re-pin, the agent's channel_open) is held as a draft the
|
|
1342
|
+
* agent can resend, in every routing mode, rather than posted where the
|
|
1343
|
+
* latest inbound message came from; and a bounce of its speech requests no
|
|
1344
|
+
* wake, so a heartbeat can't start a loop of turns.
|
|
1345
|
+
*/
|
|
1346
|
+
heartbeat?: true;
|
|
1277
1347
|
/**
|
|
1278
1348
|
* Locus and hybrid mode: where the turn's unpinned plain prose lands.
|
|
1279
1349
|
* Frozen when the turn opens (home channel for a fork, else the trigger
|
|
@@ -1281,8 +1351,9 @@ interface TurnRouting {
|
|
|
1281
1351
|
* system markers can't move it (2026-07-21 Cairn lounge misroute). Since
|
|
1282
1352
|
* 2026-07-31 a re-pin moves it (repinTurnLocus): an addressed message, or a
|
|
1283
1353
|
* follow-up in a channel the turn engaged. The agent's own channel_open
|
|
1284
|
-
* moves it too. Unset in explicit and disabled mode,
|
|
1285
|
-
* nowhere to speak
|
|
1354
|
+
* moves it too. Unset in explicit and disabled mode, for a turn with
|
|
1355
|
+
* nowhere to speak, and for a heartbeat's turn until a re-pin or a
|
|
1356
|
+
* channel_open gives it one (TurnRouting.heartbeat).
|
|
1286
1357
|
*/
|
|
1287
1358
|
locus?: ChannelRef;
|
|
1288
1359
|
/**
|
|
@@ -1363,10 +1434,11 @@ interface TurnRouting {
|
|
|
1363
1434
|
* for a re-inference, flushed at the end of a stream that didn't settle
|
|
1364
1435
|
* the turn, or stored by the start that continues it. So is one a boundary
|
|
1365
1436
|
* drained but couldn't re-pin for, being over budget or without a live
|
|
1366
|
-
* stream. A message stored for another ordinary resident is one
|
|
1367
|
-
*
|
|
1368
|
-
*
|
|
1369
|
-
*
|
|
1437
|
+
* stream. A message stored for another ordinary resident is one when it
|
|
1438
|
+
* lands, since residents read one message slot and a boundary weighs only
|
|
1439
|
+
* the messages drained for its own agent (noteForOtherResidents). A message
|
|
1440
|
+
* the agent's own view leaves out, such as one tune-out diverted or another
|
|
1441
|
+
* agent's stored context injection, isn't one, since its compiled view
|
|
1370
1442
|
* leaves it out. A boundary that re-pins clears them, since what it injects
|
|
1371
1443
|
* is newer. The turn's next start clears them all, and in locus or hybrid
|
|
1372
1444
|
* mode first re-pins to the newest one still in the agent's store, as a
|
|
@@ -1598,7 +1670,7 @@ function ephemeralRunStopped(name: string, observed?: string): Error {
|
|
|
1598
1670
|
|
|
1599
1671
|
const CALLS_CUT_OFF = "A round's tool calls were started, but its stream ended before their results came back, so that round isn't in your context; those calls may have taken effect.";
|
|
1600
1672
|
const SPEECH_STILL_ROUTING = 'Some of its plain speech was still being routed when it ended, so it may yet be posted; check the channel before sending it again.';
|
|
1601
|
-
const LIVE_SPEECH_UNRECEIPTED =
|
|
1673
|
+
const LIVE_SPEECH_UNRECEIPTED = `Plain speech its rounds routed live was handed on for delivery as written, and no [${NOTICE_TAG.delivered}] line was written for it.`;
|
|
1602
1674
|
|
|
1603
1675
|
/** The reason on the inference:aborted that ends a turn whose retry stop()
|
|
1604
1676
|
* cancelled, whichever frame was waiting to retry: beginAgentTurn's, when
|
|
@@ -2327,6 +2399,7 @@ interface ProviderAccelerationCooldown {
|
|
|
2327
2399
|
/** Its held requests were taken out by dropContinuations: release reopens
|
|
2328
2400
|
* admission without synthesising an inference in their place. */
|
|
2329
2401
|
emptiedByDrop?: boolean;
|
|
2402
|
+
rearm?: { number: number; of: number };
|
|
2330
2403
|
/** Set when this hold is a provider wait (a stated retry-after) for the
|
|
2331
2404
|
* agent's primary model: it holds primary turns only. Auxiliary calls are
|
|
2332
2405
|
* admitted per model by the provider waits themselves, so a compression
|
|
@@ -2383,7 +2456,9 @@ const WAITING_TRACE_EVERY_MS = 60_000;
|
|
|
2383
2456
|
/** A stated wait recorded to hold a model longer than this, or until
|
|
2384
2457
|
* released, raises an ops alert. It is honoured in full either way: a hold
|
|
2385
2458
|
* that long is a quota-scale event or a provider's mistake, and a person
|
|
2386
|
-
* decides which (release-provider-wait). Shorter waits are pacing
|
|
2459
|
+
* decides which (release-provider-wait). Shorter waits are pacing, until
|
|
2460
|
+
* waits in a row hold one turn longer than this (noteStatedWaitRun,
|
|
2461
|
+
* endStatedWaitRun). */
|
|
2387
2462
|
const PROVIDER_WAIT_ALERT_MS = 60 * 60_000;
|
|
2388
2463
|
/**
|
|
2389
2464
|
* A MembraneError, from this framework's copy of membrane or from another: the
|
|
@@ -2856,10 +2931,16 @@ function failureDescription(error: unknown): string {
|
|
|
2856
2931
|
const head = classification.length > 0 ? `${name} (${classification.join(', ')})` : name;
|
|
2857
2932
|
return `${head}: ${String(error.message)}`;
|
|
2858
2933
|
} catch {
|
|
2859
|
-
return
|
|
2934
|
+
return UNDESCRIBED_FAILURE;
|
|
2860
2935
|
}
|
|
2861
2936
|
}
|
|
2862
2937
|
|
|
2938
|
+
const UNDESCRIBED_FAILURE = `[${NOTICE_TAG.undescribedFailure}]`;
|
|
2939
|
+
|
|
2940
|
+
function failureShown(text: string): string {
|
|
2941
|
+
return text === UNDESCRIBED_FAILURE ? text : suppliedValue(text);
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2863
2944
|
/**
|
|
2864
2945
|
* A bounded string describing a failure, for logs that would otherwise print
|
|
2865
2946
|
* the error object; it is one line unless one of its parts has newlines, as a
|
|
@@ -3187,6 +3268,17 @@ const JUDGE_PARAMETER: ToolParameter = {
|
|
|
3187
3268
|
required: ['debounceMs'],
|
|
3188
3269
|
};
|
|
3189
3270
|
|
|
3271
|
+
/** Whether a server's configuration resolves to the MCPL (legacy) family,
|
|
3272
|
+
* the one that can negotiate RFC-014 tool hints. A configuration that names
|
|
3273
|
+
* no usable transport is refused elsewhere; here it has no hints. */
|
|
3274
|
+
function isLegacyFamily(config: McplServerConfig): boolean {
|
|
3275
|
+
try {
|
|
3276
|
+
return resolveServerBinding(config).family === 'legacy';
|
|
3277
|
+
} catch {
|
|
3278
|
+
return false;
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3190
3282
|
export class AgentFramework {
|
|
3191
3283
|
private toolPresentations = new Map<string, ToolPresentation>();
|
|
3192
3284
|
private presentationPreviews = new WeakMap<object, PresentationSnapshot>();
|
|
@@ -3433,8 +3525,6 @@ export class AgentFramework {
|
|
|
3433
3525
|
* by the object, not the run's record (ephemeralRuns), which its cleanup
|
|
3434
3526
|
* drops before a cancelled stream may count its last usage. */
|
|
3435
3527
|
private ephemeralAgents: WeakSet<Agent> = new WeakSet();
|
|
3436
|
-
/** Warn only once per Agent instance after utilities are registered. */
|
|
3437
|
-
private emptyUtilityPermissionWarnings: WeakSet<Agent> = new WeakSet();
|
|
3438
3528
|
/** Physical ephemeral frames deliberately disposed by their run watchdog/caller.
|
|
3439
3529
|
* The name is single-generation, so these frames still own their terminal
|
|
3440
3530
|
* typing/outgoing close even after runEphemeralToCompletion deregisters them. */
|
|
@@ -3443,6 +3533,10 @@ export class AgentFramework {
|
|
|
3443
3533
|
* completes (inference:completed), not when a call answers mid-turn.
|
|
3444
3534
|
* Drives hard-down escalation — see noteInferenceExhausted. */
|
|
3445
3535
|
private consecutiveInferenceFailures: Map<string, number> = new Map();
|
|
3536
|
+
private turnRearmMs: readonly number[] = DEFAULT_TURN_REARM_MS;
|
|
3537
|
+
private turnRearms: Map<string, number> = new Map();
|
|
3538
|
+
private exhaustingRearms: Set<string> = new Set();
|
|
3539
|
+
private statedWaitRuns: Map<string, StatedWaitRun> = new Map();
|
|
3446
3540
|
/** N consecutive failed inferences ⇒ the agent is treated as hard-down and
|
|
3447
3541
|
* escalated loudly to stderr. */
|
|
3448
3542
|
private readonly inferenceFailureEscalationThreshold = 3;
|
|
@@ -3705,15 +3799,16 @@ export class AgentFramework {
|
|
|
3705
3799
|
private readonly releasesAfterResume = new Map<string, Agent>();
|
|
3706
3800
|
private readonly managerReleases = new WeakMap<Agent, ManagerRelease>();
|
|
3707
3801
|
private readonly releasedByCleanup = new WeakSet<Agent>();
|
|
3708
|
-
// context-manager 0.23.
|
|
3709
|
-
// AutobiographicalStrategy does (hostContext, autobiographical.ts:
|
|
3802
|
+
// context-manager 0.23.1: a strategy that keeps its context, as
|
|
3803
|
+
// AutobiographicalStrategy does (hostContext, autobiographical.ts:2897),
|
|
3710
3804
|
// keeps its manager reachable while its work is pending, since the
|
|
3711
3805
|
// context's tools and systemPrompt getters close over the manager
|
|
3712
|
-
// (context-manager.ts:
|
|
3806
|
+
// (context-manager.ts:2343-2359). So such a released manager is collected
|
|
3713
3807
|
// only once nothing of its work can still read the store, and stop()
|
|
3714
3808
|
// closes, before an owned store, each one alive.
|
|
3715
3809
|
private readonly openManagers = new Set<WeakRef<ContextManager>>();
|
|
3716
3810
|
private readonly openManagerRefs = new WeakMap<ContextManager, WeakRef<ContextManager>>();
|
|
3811
|
+
private readonly storeSees = new WeakMap<ContextManager, (metadata: unknown) => boolean>();
|
|
3717
3812
|
private readonly collectedManagers = new FinalizationRegistry<WeakRef<ContextManager>>((ref) => this.openManagers.delete(ref));
|
|
3718
3813
|
/** The coalescing commit in flight, if any (see commitCoalescingDurable). */
|
|
3719
3814
|
private coalescingCommit: Promise<void> | null = null;
|
|
@@ -3853,10 +3948,15 @@ export class AgentFramework {
|
|
|
3853
3948
|
/** Host capabilities advertised during the MCP handshake — stored so servers
|
|
3854
3949
|
* can be connected at runtime (connectMcplServer) after initialization. */
|
|
3855
3950
|
private mcplHostCapabilities: McplHostCapabilities | null = null;
|
|
3856
|
-
/** MCPL RFC-014: every
|
|
3951
|
+
/** MCPL RFC-014: every hinted server's tool presence, for the
|
|
3857
3952
|
* `toolsets` module's meta-tools and reminders. Made when the first
|
|
3858
|
-
* server configured
|
|
3953
|
+
* server with tool hints (any not configured `toolHints: false`) is set
|
|
3954
|
+
* up; null until then. */
|
|
3859
3955
|
private toolsetHub: ToolsetHub | null = null;
|
|
3956
|
+
/** The host's own toolsets: the built-in "settings" and MCPL server tabs. */
|
|
3957
|
+
private hostPresentation: HostPresentation | null = null;
|
|
3958
|
+
/** MCPL servers kept disconnected as closed tabs, with the configuration an open connects them with. */
|
|
3959
|
+
private closedTabConfigMap?: Map<string, McplServerConfig>;
|
|
3860
3960
|
/** Inference routing policy from FrameworkConfig — stored so the MCPL
|
|
3861
3961
|
* subsystem can be lazily initialized by connectMcplServer when the
|
|
3862
3962
|
* framework started with zero configured servers. */
|
|
@@ -4176,6 +4276,7 @@ export class AgentFramework {
|
|
|
4176
4276
|
new OperatorLog(operatorLogPath),
|
|
4177
4277
|
);
|
|
4178
4278
|
framework.providerHoldHook = config.providerHold;
|
|
4279
|
+
framework.turnRearmMs = turnRearmDelays(config.exhaustedTurnRearmMs);
|
|
4179
4280
|
|
|
4180
4281
|
// Before any delivery.
|
|
4181
4282
|
await framework.reconcileDiscordAwarenessAtStartup();
|
|
@@ -4210,9 +4311,6 @@ export class AgentFramework {
|
|
|
4210
4311
|
for (const module of config.modules) {
|
|
4211
4312
|
await framework.addModule(module);
|
|
4212
4313
|
}
|
|
4213
|
-
for (const agent of framework.agents.values()) {
|
|
4214
|
-
framework.warnEmptyUtilityPermissions(agent);
|
|
4215
|
-
}
|
|
4216
4314
|
for (const [agentName, presentation] of framework.toolPresentations) {
|
|
4217
4315
|
const workspace = framework.getWorkspaceModule();
|
|
4218
4316
|
if (!workspace) throw new Error('Tool presentation requires workspace');
|
|
@@ -4280,6 +4378,8 @@ export class AgentFramework {
|
|
|
4280
4378
|
upcomingWakes: (agentName) => framework.upcomingWakesFor(agentName),
|
|
4281
4379
|
});
|
|
4282
4380
|
framework.judgeWired = judge !== undefined;
|
|
4381
|
+
const configuredServerIds = new Set((config.mcplServers ?? []).map((server) => server.id));
|
|
4382
|
+
const heartbeatOn = config.heartbeat !== undefined && config.heartbeat !== null && config.heartbeat !== false;
|
|
4283
4383
|
framework.eventGate = new EventGate({
|
|
4284
4384
|
configPath,
|
|
4285
4385
|
initialConfig: config.gate.config,
|
|
@@ -4341,11 +4441,20 @@ export class AgentFramework {
|
|
|
4341
4441
|
// debounce can fire).
|
|
4342
4442
|
// A batched wake's summary names each channel as the agent names it
|
|
4343
4443
|
// back, with its server where another server shares its id.
|
|
4344
|
-
channelShown: (channel) => framework.channelRegistry?.nameShown(channel) ?? channel.channelId,
|
|
4444
|
+
channelShown: (channel) => framework.channelRegistry?.nameShown(channel) ?? suppliedValue(channel.channelId),
|
|
4345
4445
|
// A batched wake for one message still newest in the window hands its
|
|
4346
4446
|
// `[Gate: …]` line to the wake, for a fresh start of the wake's turn
|
|
4347
4447
|
// to decide (placeWakeLines).
|
|
4348
4448
|
isNewestEntry: (entry) => framework.isNewestContextEntry(entry),
|
|
4449
|
+
// What a source can send, for the rule-order notes: an MCPL server's
|
|
4450
|
+
// events reach the gate as its pushes and channel messages only (a
|
|
4451
|
+
// module's events carry a source of the module's own, such as "cli"
|
|
4452
|
+
// or "mcpl:<id>"), and the heartbeat's ticks as pushes.
|
|
4453
|
+
eventTypesFrom: (source) => {
|
|
4454
|
+
if (configuredServerIds.has(source) || framework.mcplServerConfigs.has(source)) return MCPL_GATE_EVENT_TYPES;
|
|
4455
|
+
if (source === 'heartbeat' && heartbeatOn) return ['mcpl:push-event'];
|
|
4456
|
+
return undefined;
|
|
4457
|
+
},
|
|
4349
4458
|
resolveRouteChannel: (info) => {
|
|
4350
4459
|
if (info.eventType === 'mcpl:channel-incoming') return info.channelId || undefined;
|
|
4351
4460
|
if (info.eventType === 'mcpl:push-event') {
|
|
@@ -4487,15 +4596,20 @@ export class AgentFramework {
|
|
|
4487
4596
|
}
|
|
4488
4597
|
}
|
|
4489
4598
|
|
|
4599
|
+
// The host's own toolsets (host-presentation.ts): the built-in "settings"
|
|
4600
|
+
// and each MCPL server's tab, per agent, kept beside the store.
|
|
4601
|
+
framework.hostPresentation = framework.makeHostPresentation(config.storePath);
|
|
4602
|
+
|
|
4490
4603
|
// Initialize MCPL subsystems if configured
|
|
4491
4604
|
if (config.mcplServers && config.mcplServers.length > 0) {
|
|
4492
4605
|
// Check every server's config before any is brought up: its boolean
|
|
4493
4606
|
// settings, and its tool prefix, which mustn't collide with a module
|
|
4494
4607
|
// name or another server's prefix.
|
|
4495
4608
|
const moduleNames = new Set(config.modules.map(m => m.name));
|
|
4496
|
-
//
|
|
4497
|
-
// module
|
|
4498
|
-
|
|
4609
|
+
// The name `toolsets` is the framework's from creation: its toolsets
|
|
4610
|
+
// module (or a module given here that holds the name) takes it, so no
|
|
4611
|
+
// prefix may.
|
|
4612
|
+
moduleNames.add(TOOLSETS_MODULE);
|
|
4499
4613
|
const prefixesSeen = new Map<string, string>(); // prefix → serverId
|
|
4500
4614
|
for (const serverConfig of config.mcplServers) {
|
|
4501
4615
|
assertServerConfigBooleans(serverConfig);
|
|
@@ -4518,8 +4632,25 @@ export class AgentFramework {
|
|
|
4518
4632
|
prefixesSeen.set(prefix, serverConfig.id);
|
|
4519
4633
|
}
|
|
4520
4634
|
|
|
4521
|
-
|
|
4522
|
-
await framework.
|
|
4635
|
+
framework.reserveToolsetsName(config.modules, config.mcplServers);
|
|
4636
|
+
if (framework.toolsetsNameHeldAtStart === null) await framework.ensureToolsetsModule();
|
|
4637
|
+
// A server kept as a closed tab (by its configured presentation, or as
|
|
4638
|
+
// an agent last left it) isn't connected until an agent opens it.
|
|
4639
|
+
let closed = framework.hostPresentation.startClosed(config.mcplServers);
|
|
4640
|
+
if (framework.toolsetsNameHeldAtStart !== null && closed.size > 0) {
|
|
4641
|
+
// No agent could open a closed tab: the meta-tools aren't the framework's.
|
|
4642
|
+
console.error(`[toolsets] ${[...closed].join(', ')} ${closed.size === 1 ? 'is' : 'are'} connected at startup though kept as a closed tab: ` +
|
|
4643
|
+
`the name "${TOOLSETS_MODULE}" is ${framework.toolsetsNameHeldAtStart}, so no agent could open ${closed.size === 1 ? 'it' : 'them'}`);
|
|
4644
|
+
closed = new Set();
|
|
4645
|
+
}
|
|
4646
|
+
for (const serverConfig of config.mcplServers) {
|
|
4647
|
+
if (closed.has(serverConfig.id)) framework.closedTabConfigs().set(serverConfig.id, serverConfig);
|
|
4648
|
+
}
|
|
4649
|
+
await framework.initializeMcpl(config.mcplServers.filter((c) => !closed.has(c.id)), config.inferenceRouting);
|
|
4650
|
+
} else if (!config.modules.some((m) => m.name === TOOLSETS_MODULE)) {
|
|
4651
|
+
// The meta-tools are every agent's: the built-in toolset needs them,
|
|
4652
|
+
// MCPL or none.
|
|
4653
|
+
await framework.ensureToolsetsModule();
|
|
4523
4654
|
}
|
|
4524
4655
|
|
|
4525
4656
|
// Tune-out coordinator (issue #77): needs both the subconscious resident
|
|
@@ -5392,7 +5523,7 @@ export class AgentFramework {
|
|
|
5392
5523
|
* only its own model, which auxiliary admission checks per call. */
|
|
5393
5524
|
private cooldownBlocksAuxiliary(agentName: string): boolean {
|
|
5394
5525
|
const cooldown = this.providerAccelerationCooldowns.get(agentName);
|
|
5395
|
-
return cooldown !== undefined && cooldown.waitModel === undefined;
|
|
5526
|
+
return cooldown !== undefined && cooldown.waitModel === undefined && cooldown.rearm === undefined;
|
|
5396
5527
|
}
|
|
5397
5528
|
private acquirePrimaryProviderGate(agentName: string): void {
|
|
5398
5529
|
const gate = this.providerGate(agentName); gate.primaryPending = false; gate.primaryDepth++;
|
|
@@ -5505,24 +5636,51 @@ export class AgentFramework {
|
|
|
5505
5636
|
}
|
|
5506
5637
|
const cooldown = this.providerAccelerationCooldowns.get(agentName);
|
|
5507
5638
|
if (cooldown && cooldown.heldRequests.length > 0 && Number.isFinite(cooldown.until)) {
|
|
5508
|
-
wakes.push({ at: cooldown.until, certain: true, what:
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
// they decide a heartbeat push: one the rules would hold back is listed
|
|
5512
|
-
// as only a "maybe".
|
|
5513
|
-
if (this.heartbeat && this.heartbeatTargets().includes(agentName)) {
|
|
5514
|
-
const probe = (kind: 'heartbeat' | 'reminder'): boolean => !this.eventGate
|
|
5515
|
-
|| this.eventGate.probe(this.heartbeatGateEvent({ kind, eventId: 'probe', text: 'probe' })).trigger;
|
|
5516
|
-
const tickWakes = probe('heartbeat');
|
|
5517
|
-
const reminderWakes = probe('reminder');
|
|
5518
|
-
for (const w of this.heartbeat.upcomingWakes()) {
|
|
5519
|
-
const isReminder = w.what.startsWith('its reminder');
|
|
5520
|
-
wakes.push((isReminder ? reminderWakes : tickWakes) ? w : { ...w, certain: false });
|
|
5521
|
-
}
|
|
5639
|
+
wakes.push({ at: cooldown.until, certain: true, what: cooldown.rearm
|
|
5640
|
+
? `a turn whose retries ran out, sent again (${cooldown.rearm.number} of ${cooldown.rearm.of})`
|
|
5641
|
+
: 'a turn held by a provider cooldown, when it ends' });
|
|
5522
5642
|
}
|
|
5643
|
+
wakes.push(...this.heartbeatWakesFor(agentName));
|
|
5523
5644
|
return wakes;
|
|
5524
5645
|
}
|
|
5525
5646
|
|
|
5647
|
+
/**
|
|
5648
|
+
* The heartbeat's ticks and reminders for `agentName`, soonest first, which
|
|
5649
|
+
* the agent's rules decide as they decide a heartbeat push: one the rules
|
|
5650
|
+
* would hold back is only a "maybe" (`certain` false). Each is probed as
|
|
5651
|
+
* the event its delivery is judged by (deliverHeartbeat): a silent tick as
|
|
5652
|
+
* silent, with the text it will bring, so a rule on the content or the
|
|
5653
|
+
* silent flag decides the probe as it will decide the wake. None for an
|
|
5654
|
+
* agent the heartbeat doesn't wake. `whose` words them for the judge ("its
|
|
5655
|
+
* heartbeat") or for the agent ("your heartbeat").
|
|
5656
|
+
*/
|
|
5657
|
+
private heartbeatWakesFor(agentName: string, whose: 'its' | 'your' = 'its'): HeartbeatScheduledWake[] {
|
|
5658
|
+
if (!this.heartbeat || !this.heartbeatTargets().includes(agentName)) return [];
|
|
5659
|
+
const wakes = (w: HeartbeatScheduledWake): boolean => !this.eventGate
|
|
5660
|
+
|| this.eventGate.probe(this.heartbeatGateEvent({ kind: w.kind, eventId: 'probe', text: w.text, silent: w.silent })).trigger;
|
|
5661
|
+
return this.heartbeat.scheduledWakes(whose).map((w) => (wakes(w) ? w : { ...w, certain: false }));
|
|
5662
|
+
}
|
|
5663
|
+
|
|
5664
|
+
/**
|
|
5665
|
+
* The heartbeat's wake due before a self-wake due at `selfWakeAt` (ms), for
|
|
5666
|
+
* the skip_reply that armed it: the soonest of its ticks and reminders that
|
|
5667
|
+
* the agent's rules, as they stand, let wake it. The confirmation says when
|
|
5668
|
+
* it's due, not that it will wake the agent (skipReplyNote), so a probe
|
|
5669
|
+
* that can't foresee the gate at the wake's time (a gate.js, a rate limit's
|
|
5670
|
+
* tokens then) leaves it true. A failure here costs only that line: the
|
|
5671
|
+
* self-wake is armed already, and the confirmation goes out without it.
|
|
5672
|
+
*/
|
|
5673
|
+
private heartbeatWakeBefore(agentName: string, selfWakeAt: number): SkipReplyEarlierWake | undefined {
|
|
5674
|
+
try {
|
|
5675
|
+
const now = Date.now();
|
|
5676
|
+
const first = this.heartbeatWakesFor(agentName, 'your').find((w) => w.certain && w.at < selfWakeAt);
|
|
5677
|
+
return first ? { inSeconds: Math.max(1, Math.round((first.at - now) / 1000)), what: first.what } : undefined;
|
|
5678
|
+
} catch (err) {
|
|
5679
|
+
console.error(`[self-wake] agent=${agentName}: the heartbeat's wakes couldn't be read for skip_reply's note: ${logDetail(err instanceof Error ? err.message : String(err))}`);
|
|
5680
|
+
return undefined;
|
|
5681
|
+
}
|
|
5682
|
+
}
|
|
5683
|
+
|
|
5526
5684
|
/** The agents the heartbeat wakes: the residents, not conversation forks,
|
|
5527
5685
|
* ephemeral runs or the subconscious, which keep windows of their own. */
|
|
5528
5686
|
private heartbeatTargets(): string[] {
|
|
@@ -5905,8 +6063,59 @@ export class AgentFramework {
|
|
|
5905
6063
|
console.error(`[provider-cooldown] agent=${agent.name} ` +
|
|
5906
6064
|
`${hostHold ? `host hold (${hostHold.reason})` : 'organization acceleration 429'} — ` +
|
|
5907
6065
|
`holding primary/auxiliary for ${delayMs}ms; ${held.length} request(s) retained`);
|
|
6066
|
+
if (wait && trigger && (wait.until === null || wait.until > now)) this.noteStatedWaitRun(agent.name, wait, now);
|
|
5908
6067
|
return true;
|
|
5909
6068
|
}
|
|
6069
|
+
private rearmExhaustedTurn(agent: Agent, error: Error, trigger: InferenceRequest | undefined, ownsProviderGate: boolean, routing: TurnRouting | undefined): void {
|
|
6070
|
+
if (!ownsProviderGate || trigger === undefined || this.providerAdmissionClosed || this.turnRearmMs.length === 0) return;
|
|
6071
|
+
const done = this.turnRearms.get(agent.name) ?? 0;
|
|
6072
|
+
const notSent = (why: string) => console.error(`[inference-rearm] agent=${agent.name} turn not re-armed: ${why}`);
|
|
6073
|
+
if (!isTransientFailure(error)) {
|
|
6074
|
+
if (done > 0) notSent(`its failure isn't transient (${isMembraneError(error) ? error.type : boundFailureText(error.message, FAILURE_SUMMARY_MAX_CHARS)})`);
|
|
6075
|
+
return;
|
|
6076
|
+
}
|
|
6077
|
+
const continuation = isTurnContinuation(trigger.reason);
|
|
6078
|
+
const speaking = routing?.locus ?? routing?.triggerChannel;
|
|
6079
|
+
if (!speaking && routing?.noChannel && !trigger.channelId) {
|
|
6080
|
+
notSent('it would have to guess a channel');
|
|
6081
|
+
return;
|
|
6082
|
+
}
|
|
6083
|
+
if (done >= this.turnRearmMs.length) {
|
|
6084
|
+
notSent(`its ${this.turnRearmMs.length} re-arms are spent until a turn completes or a message is said to it`);
|
|
6085
|
+
return;
|
|
6086
|
+
}
|
|
6087
|
+
const delayMs = this.turnRearmMs[done]!;
|
|
6088
|
+
const rearm = { number: done + 1, of: this.turnRearmMs.length };
|
|
6089
|
+
this.turnRearms.set(agent.name, rearm.number);
|
|
6090
|
+
const request: MarkedRequest = { ...trigger, [REARMED]: true };
|
|
6091
|
+
delete request[TURN_RETRY];
|
|
6092
|
+
delete request[MERGED_BATCH];
|
|
6093
|
+
if (continuation) {
|
|
6094
|
+
request.reason = REARM_REASON;
|
|
6095
|
+
delete request.counterparty;
|
|
6096
|
+
}
|
|
6097
|
+
if (speaking) {
|
|
6098
|
+
request.channelId = speaking.channelId;
|
|
6099
|
+
request.channelServerId = speaking.serverId;
|
|
6100
|
+
request.addressed = true;
|
|
6101
|
+
}
|
|
6102
|
+
const existing = this.providerAccelerationCooldowns.get(agent.name);
|
|
6103
|
+
if (existing) {
|
|
6104
|
+
this.holdRequest(existing.heldRequests, request);
|
|
6105
|
+
} else {
|
|
6106
|
+
const now = Date.now();
|
|
6107
|
+
const timer = setTimeout(() => this.releaseProviderAccelerationCooldown(agent.name), this.providerHoldTimerMs(delayMs));
|
|
6108
|
+
timer.unref?.();
|
|
6109
|
+
this.providerAccelerationCooldowns.set(agent.name, {
|
|
6110
|
+
startedAt: now, until: now + delayMs, timer, heldRequests: [request], failures: 0, rearm,
|
|
6111
|
+
reason: `its turn's retries ran out (${boundFailureText(error.message, FAILURE_SUMMARY_MAX_CHARS)}), ` +
|
|
6112
|
+
`so it is sent again (${rearm.number} of ${rearm.of})`,
|
|
6113
|
+
});
|
|
6114
|
+
}
|
|
6115
|
+
console.error(`[inference-rearm] agent=${agent.name} turn sent again ` +
|
|
6116
|
+
(existing ? 'when the provider hold already on the agent ends' : `in ${delayMs}ms`) +
|
|
6117
|
+
` (${rearm.number} of ${rearm.of})`);
|
|
6118
|
+
}
|
|
5910
6119
|
/**
|
|
5911
6120
|
* A provider's stated wait on a turn that doesn't own provider admission:
|
|
5912
6121
|
* a conversation agent's or an ephemeral run's. Left to the error policy,
|
|
@@ -6015,8 +6224,10 @@ export class AgentFramework {
|
|
|
6015
6224
|
const requests = cooldown.heldRequests.length > 0 ? cooldown.heldRequests : [{ agentName,
|
|
6016
6225
|
reason: 'provider-acceleration-retry', source: 'framework', timestamp: Date.now() } satisfies InferenceRequest];
|
|
6017
6226
|
this.pendingRequests.push(...requests);
|
|
6018
|
-
console.error(
|
|
6019
|
-
`
|
|
6227
|
+
console.error(cooldown.rearm
|
|
6228
|
+
? `[inference-rearm] agent=${agentName} turn queued again (${cooldown.rearm.number} of ${cooldown.rearm.of})`
|
|
6229
|
+
: `[provider-cooldown] agent=${agentName} released after ${Date.now() - cooldown.startedAt}ms — ` +
|
|
6230
|
+
`fresh compile queued with ${requests.length} retained request(s)`);
|
|
6020
6231
|
}
|
|
6021
6232
|
/**
|
|
6022
6233
|
* Wait an error policy's retry delay, all of it: the delay can be a
|
|
@@ -6090,7 +6301,7 @@ export class AgentFramework {
|
|
|
6090
6301
|
: `until ${new Date(wait.until).toISOString()}`;
|
|
6091
6302
|
const stated = `${error.type}${error.httpStatus !== undefined ? ` HTTP ${error.httpStatus}` : ''}, retry-after ${String(error.retryAfterMs)}ms`;
|
|
6092
6303
|
console.error(`[provider-wait] agent=${agentName} model=${model} holds ${until}: ${stated}`);
|
|
6093
|
-
if (
|
|
6304
|
+
if (waitAlertsOnItsOwn(wait)) {
|
|
6094
6305
|
this.opsAlert('provider-wait', agentName,
|
|
6095
6306
|
`provider wait on ${model} holds ${until} (${stated}: ${reason}); ` +
|
|
6096
6307
|
'if the provider did not mean that, release it with release-provider-wait',
|
|
@@ -6100,6 +6311,41 @@ export class AgentFramework {
|
|
|
6100
6311
|
return wait;
|
|
6101
6312
|
}
|
|
6102
6313
|
|
|
6314
|
+
private noteStatedWaitRun(agentName: string, wait: ProviderWait, now: number): void {
|
|
6315
|
+
let run = this.statedWaitRuns.get(agentName);
|
|
6316
|
+
if (!run) {
|
|
6317
|
+
run = { since: now, holds: 0, alerted: false, model: wait.model, reason: wait.reason, heldUntil: now };
|
|
6318
|
+
this.statedWaitRuns.set(agentName, run);
|
|
6319
|
+
}
|
|
6320
|
+
run.holds++;
|
|
6321
|
+
run.model = wait.model;
|
|
6322
|
+
run.reason = wait.reason;
|
|
6323
|
+
if (wait.until !== null) run.heldUntil = Math.max(run.heldUntil, wait.until);
|
|
6324
|
+
if (waitAlertsOnItsOwn(wait)) run.alerted = true;
|
|
6325
|
+
if (now - run.since > PROVIDER_WAIT_ALERT_MS) this.alertStatedWaitRun(agentName, run, now);
|
|
6326
|
+
}
|
|
6327
|
+
|
|
6328
|
+
private alertStatedWaitRun(agentName: string, run: StatedWaitRun, now: number): void {
|
|
6329
|
+
if (run.alerted) return;
|
|
6330
|
+
run.alerted = true;
|
|
6331
|
+
const since = new Date(run.since).toISOString();
|
|
6332
|
+
this.opsAlert('provider-wait', agentName,
|
|
6333
|
+
`a turn's ${run.holds} stated waits in a row on ${run.model} span ${Math.round((now - run.since) / 60_000)} min ` +
|
|
6334
|
+
`since ${since}: ${run.reason}; a provider stating waits that long in a row points to the account's rate limit ` +
|
|
6335
|
+
"or quota, or this host's load",
|
|
6336
|
+
{ data: { model: run.model, since, holds: run.holds } });
|
|
6337
|
+
}
|
|
6338
|
+
|
|
6339
|
+
private endStatedWaitRun(agentName: string): void {
|
|
6340
|
+
const run = this.statedWaitRuns?.get(agentName);
|
|
6341
|
+
if (!run) return;
|
|
6342
|
+
this.statedWaitRuns.delete(agentName);
|
|
6343
|
+
const end = Math.min(Date.now(), run.heldUntil);
|
|
6344
|
+
if (end - run.since > PROVIDER_WAIT_ALERT_MS) this.alertStatedWaitRun(agentName, run, end);
|
|
6345
|
+
}
|
|
6346
|
+
|
|
6347
|
+
|
|
6348
|
+
|
|
6103
6349
|
/**
|
|
6104
6350
|
* Hold this agent's primary turns for a provider wait on its model. The
|
|
6105
6351
|
* requests are kept and released together, into one fresh compile, when
|
|
@@ -6130,7 +6376,10 @@ export class AgentFramework {
|
|
|
6130
6376
|
for (const request of requests) this.holdRequest(held, request);
|
|
6131
6377
|
// An acceleration or host hold already parks the whole agent; its release
|
|
6132
6378
|
// re-checks this wait at admission.
|
|
6133
|
-
if (existing && existing.waitModel === undefined)
|
|
6379
|
+
if (existing && existing.waitModel === undefined) {
|
|
6380
|
+
if (failed) this.noteStatedWaitRun(agent.name, wait, now);
|
|
6381
|
+
return true;
|
|
6382
|
+
}
|
|
6134
6383
|
if (existing) clearTimeout(existing.timer);
|
|
6135
6384
|
const timer = setTimeout(() => this.releaseProviderAccelerationCooldown(agent.name), this.providerHoldTimerMs(until - now));
|
|
6136
6385
|
timer.unref?.();
|
|
@@ -6143,6 +6392,7 @@ export class AgentFramework {
|
|
|
6143
6392
|
console.error(`[provider-wait] agent=${agent.name} model=${wait.model} holding primary turns ` +
|
|
6144
6393
|
`${wait.until === null ? 'until released' : `until ${new Date(wait.until).toISOString()}`}; ${held.length} request(s) retained`);
|
|
6145
6394
|
}
|
|
6395
|
+
if (failed) this.noteStatedWaitRun(agent.name, wait, now);
|
|
6146
6396
|
return true;
|
|
6147
6397
|
}
|
|
6148
6398
|
|
|
@@ -6198,6 +6448,8 @@ export class AgentFramework {
|
|
|
6198
6448
|
// names every model, the same as omitting it, everywhere below.
|
|
6199
6449
|
if (model === EVERY_MODEL) model = undefined;
|
|
6200
6450
|
const released = this.providerWaits?.release(agentName, model, by) ?? [];
|
|
6451
|
+
const run = this.statedWaitRuns?.get(agentName);
|
|
6452
|
+
if (run && released.some(({ wait }) => wait.model === run.model)) run.heldUntil = Math.min(run.heldUntil, Date.now());
|
|
6201
6453
|
for (const { wait, release } of released) {
|
|
6202
6454
|
console.error(`[provider-wait] agent=${agentName} model=${wait.model} released by ${by} (${release})`);
|
|
6203
6455
|
}
|
|
@@ -6274,6 +6526,7 @@ export class AgentFramework {
|
|
|
6274
6526
|
private cancelProviderAdmission(): void {
|
|
6275
6527
|
for (const cooldown of this.providerAccelerationCooldowns.values()) clearTimeout(cooldown.timer);
|
|
6276
6528
|
this.providerAccelerationCooldowns.clear(); this.providerAccelerationRecoveries.clear();
|
|
6529
|
+
this.turnRearms?.clear(); this.exhaustingRearms?.clear();
|
|
6277
6530
|
for (const gate of this.providerGates.values()) {
|
|
6278
6531
|
gate.primaryDepth = 0; gate.primaryPending = false;
|
|
6279
6532
|
for (const resolve of gate.auxiliaryWaiters.splice(0)) resolve();
|
|
@@ -6701,6 +6954,12 @@ export class AgentFramework {
|
|
|
6701
6954
|
if (claimedBy) {
|
|
6702
6955
|
throw new Error(`Module "${module.name}" collides with the toolPrefix "${module.name}" the restart of "${claimedBy.restarting}" holds`);
|
|
6703
6956
|
}
|
|
6957
|
+
// A closed tab keeps its prefix for the connect its next open makes.
|
|
6958
|
+
for (const [id, config] of this.closedTabConfigs()) {
|
|
6959
|
+
if ((config.toolPrefix ?? `mcpl--${id}`) === module.name) {
|
|
6960
|
+
throw new Error(`Module "${module.name}" collides with MCPL server "${id}"'s toolPrefix "${module.name}" (a closed tab)`);
|
|
6961
|
+
}
|
|
6962
|
+
}
|
|
6704
6963
|
this.resetCompletedStop('addModule()');
|
|
6705
6964
|
await this.moduleRegistry.addModule(module);
|
|
6706
6965
|
// The workspace judges whether an agent's history holds a message as the
|
|
@@ -6889,12 +7148,12 @@ export class AgentFramework {
|
|
|
6889
7148
|
[{
|
|
6890
7149
|
type: 'text',
|
|
6891
7150
|
text: (before === 'sending'
|
|
6892
|
-
? `[
|
|
7151
|
+
? `[${NOTICE_TAG.turnInterrupted}] Your previous turn was stopped before it sent anything — ` +
|
|
6893
7152
|
`a deliberate stop, not a failure.`
|
|
6894
7153
|
: before === 'finishing'
|
|
6895
|
-
? `[
|
|
7154
|
+
? `[${NOTICE_TAG.turnInterrupted}] Your previous turn was stopped before it finished — ` +
|
|
6896
7155
|
`a deliberate stop, not a failure.`
|
|
6897
|
-
: `[
|
|
7156
|
+
: `[${NOTICE_TAG.turnInterrupted}] Your previous turn was cancelled mid-stream — ` +
|
|
6898
7157
|
`a deliberate stop, not a failure. Whatever you were still ` +
|
|
6899
7158
|
`producing when it stopped was cut off there.`) + handedOn,
|
|
6900
7159
|
}],
|
|
@@ -7047,8 +7306,10 @@ export class AgentFramework {
|
|
|
7047
7306
|
: [];
|
|
7048
7307
|
return [
|
|
7049
7308
|
...moduleTools,
|
|
7050
|
-
// Utilities
|
|
7051
|
-
|
|
7309
|
+
// Utilities: callable tools, presented in the built-in toolset
|
|
7310
|
+
// (placedInBuiltin), never in the tool list. One named like a tool of
|
|
7311
|
+
// its module is that tool's: the tool takes the name.
|
|
7312
|
+
...this.moduleRegistry.getAllUtilities().filter((u) => !moduleTools.some((t) => t.name === u.name)),
|
|
7052
7313
|
...this.mcplTools,
|
|
7053
7314
|
...channelTools,
|
|
7054
7315
|
...gateTools,
|
|
@@ -7120,6 +7381,8 @@ export class AgentFramework {
|
|
|
7120
7381
|
private setMcplServerConfig(config: McplServerConfig): void {
|
|
7121
7382
|
this.mcplServerConfigs.set(config.id, config);
|
|
7122
7383
|
referenceRegistry.setServerAddress(config.id, serverAddressOf(config), this);
|
|
7384
|
+
// The gate's rule-order notes read which sources are servers.
|
|
7385
|
+
this.eventGate?.sourcesChanged();
|
|
7123
7386
|
}
|
|
7124
7387
|
|
|
7125
7388
|
/** Register every RFC-005 reference in an MCPL peer's tool-result content
|
|
@@ -7409,7 +7672,7 @@ export class AgentFramework {
|
|
|
7409
7672
|
if (!how) return undefined;
|
|
7410
7673
|
const reach = this.residentHoldsTraffic(who) ? ' Messages there that address you, such as an @-mention, still reach you.' : '';
|
|
7411
7674
|
const open = this.canAgentUseTool(who, 'channel_open') && !registry.lifecycleWithheld(channel.serverId)
|
|
7412
|
-
? ` channel_open with channelId
|
|
7675
|
+
? ` channel_open with channelId ${quotedValue(channel.channelId)} and serverId "${channel.serverId}" opens it.`
|
|
7413
7676
|
: '';
|
|
7414
7677
|
return `${this.channelShown(channel)} stays closed: you ${how} it, and your words there don't open it.${reach}${open}`;
|
|
7415
7678
|
}
|
|
@@ -7501,14 +7764,14 @@ export class AgentFramework {
|
|
|
7501
7764
|
const tunedOutNote = closedNote ? undefined : this.tunedOutSendNote(agentName, channel);
|
|
7502
7765
|
const body = closedNote ?? tunedOutNote;
|
|
7503
7766
|
if (!body) return answer;
|
|
7504
|
-
const tag = closedNote ?
|
|
7767
|
+
const tag = `[${closedNote ? NOTICE_TAG.closedChannel : NOTICE_TAG.tunedOut}]`;
|
|
7505
7768
|
if (forScript) {
|
|
7506
7769
|
const window = this.noticeTargetFor(agentName, 'closed-channel');
|
|
7507
7770
|
try {
|
|
7508
7771
|
if (window) {
|
|
7509
7772
|
this.addMessage('user', [{
|
|
7510
7773
|
type: 'text',
|
|
7511
|
-
text: `${tag} Your script's ${toolName}: ${body}${this.closedChannelNoteOff(agentName)}`,
|
|
7774
|
+
text: `${tag} Your script's ${suppliedValue(toolName)}: ${body}${this.closedChannelNoteOff(agentName)}`,
|
|
7512
7775
|
}], { system: true, kind: 'channel-notice' } as MessageMetadata, window);
|
|
7513
7776
|
}
|
|
7514
7777
|
} catch (err) {
|
|
@@ -7677,15 +7940,6 @@ export class AgentFramework {
|
|
|
7677
7940
|
};
|
|
7678
7941
|
}
|
|
7679
7942
|
|
|
7680
|
-
private warnEmptyUtilityPermissions(agent: Agent): void {
|
|
7681
|
-
if (agent.allowedTools === 'all' || !agent.allowedTools.includes('utils')
|
|
7682
|
-
|| this.emptyUtilityPermissionWarnings.has(agent)) return;
|
|
7683
|
-
const utilities = this.moduleRegistry.getAllUtilities();
|
|
7684
|
-
if (utilities.length === 0 || utilities.some(tool => agent.canUseTool(tool.name))) return;
|
|
7685
|
-
console.warn(`[utils] Agent '${agent.name}': ${registeredUtilities(utilities.length)}, 0 permitted; none permitted by this agent's allowedTools. Add each required module--utility name to allowedTools.`);
|
|
7686
|
-
this.emptyUtilityPermissionWarnings.add(agent);
|
|
7687
|
-
}
|
|
7688
|
-
|
|
7689
7943
|
private getToolsForAgent(
|
|
7690
7944
|
agentName: string,
|
|
7691
7945
|
snapshot?: InferenceToolSnapshot,
|
|
@@ -7709,10 +7963,8 @@ export class AgentFramework {
|
|
|
7709
7963
|
: t);
|
|
7710
7964
|
return [...SUBCONSCIOUS_TOOLS, ...basics];
|
|
7711
7965
|
}
|
|
7712
|
-
if (agent) this.warnEmptyUtilityPermissions(agent);
|
|
7713
|
-
const hasPermittedUtility = !agent || this.moduleRegistry.getAllUtilities().some(tool => agent.canUseTool(tool.name));
|
|
7714
7966
|
return [...this.getAllTools(), ...(this.toolPresentations.has(agentName) ? presentationTools(this.toolPresentations.get(agentName)!.config.cataloguePath) : [])]
|
|
7715
|
-
.
|
|
7967
|
+
.map((tool) => {
|
|
7716
7968
|
if (tool.name === 'think') {
|
|
7717
7969
|
return this.buildThinkTool(
|
|
7718
7970
|
snapshot?.sameRoundThinkTextPolicy
|
|
@@ -7743,7 +7995,7 @@ export class AgentFramework {
|
|
|
7743
7995
|
if (!presentation) return null;
|
|
7744
7996
|
const tools = this.availableToolsForPresentation(agentName, snapshot);
|
|
7745
7997
|
const sources = new Map<string, string>();
|
|
7746
|
-
for (const tool of this.moduleRegistry.getAllTools()) {
|
|
7998
|
+
for (const tool of [...this.moduleRegistry.getAllTools(), ...this.moduleRegistry.getAllUtilities()]) {
|
|
7747
7999
|
sources.set(tool.name, `Module: ${tool.name.split('--')[0]}`);
|
|
7748
8000
|
}
|
|
7749
8001
|
// An MCPL tool's source is the server a call of it reaches, by the one
|
|
@@ -7757,8 +8009,8 @@ export class AgentFramework {
|
|
|
7757
8009
|
}
|
|
7758
8010
|
|
|
7759
8011
|
private advertisedToolsForAgent(agentName: string, snapshot?: InferenceToolSnapshot) {
|
|
7760
|
-
return this.inspectToolPresentation(agentName, snapshot)?.advertised
|
|
7761
|
-
?? this.availableToolsForPresentation(agentName, snapshot);
|
|
8012
|
+
return this.headerTools(agentName, this.inspectToolPresentation(agentName, snapshot)?.advertised
|
|
8013
|
+
?? this.availableToolsForPresentation(agentName, snapshot));
|
|
7762
8014
|
}
|
|
7763
8015
|
|
|
7764
8016
|
/** Compression can revisit calls to hidden tools; never apply visibility here. */
|
|
@@ -8310,8 +8562,10 @@ export class AgentFramework {
|
|
|
8310
8562
|
* CURRENT settings — maintenance may have moved the folded floor and the
|
|
8311
8563
|
* branch generation, so verdicts are computed at resume time, never reused.
|
|
8312
8564
|
* Throws ResumeBlockedError (with all failing verdicts) unless `force`.
|
|
8313
|
-
* Unavailable previews
|
|
8314
|
-
* strategy
|
|
8565
|
+
* Unavailable previews pass, with a warning naming the reason unless it is
|
|
8566
|
+
* `no_preview_support`, a strategy with nothing to preview — refusing to
|
|
8567
|
+
* resume because the strategy cannot preview would hold hosts hostage to a
|
|
8568
|
+
* diagnostic.
|
|
8315
8569
|
*/
|
|
8316
8570
|
async resume(opts?: { force?: boolean }): Promise<HostModeStatus> {
|
|
8317
8571
|
this.refuseOperatorAfterStop('resume');
|
|
@@ -9220,7 +9474,8 @@ export class AgentFramework {
|
|
|
9220
9474
|
}
|
|
9221
9475
|
|
|
9222
9476
|
const presentation = this.inspectToolPresentation(agentName);
|
|
9223
|
-
const
|
|
9477
|
+
const availableTools = presentation?.available ?? this.availableToolsForPresentation(agentName);
|
|
9478
|
+
const tools = this.headerTools(agentName, presentation?.advertised ?? availableTools);
|
|
9224
9479
|
const capture = (request: NormalizedRequest) => {
|
|
9225
9480
|
if (presentation) this.presentationPreviews.set(request, presentation);
|
|
9226
9481
|
return request;
|
|
@@ -9234,7 +9489,7 @@ export class AgentFramework {
|
|
|
9234
9489
|
// writes of its own, no external RPC. Opt in explicitly.
|
|
9235
9490
|
if (!opts?.injections) {
|
|
9236
9491
|
return capture(await agent.buildActivationRequest(
|
|
9237
|
-
tools, undefined, opts?.budget,
|
|
9492
|
+
tools, undefined, opts?.budget, availableTools, { dryRun: true },
|
|
9238
9493
|
));
|
|
9239
9494
|
}
|
|
9240
9495
|
|
|
@@ -9281,7 +9536,7 @@ export class AgentFramework {
|
|
|
9281
9536
|
const held = plan !== undefined && this.slotHeldFrom(agent);
|
|
9282
9537
|
const placed = new Set(plan ? (held ? [...plan.leftAt, ...plan.rows.map((row) => row.at)] : plan.leftAt) : []);
|
|
9283
9538
|
return capture(await agent.buildActivationRequest(
|
|
9284
|
-
tools, plan ? (injections ?? []).filter((_, at) => placed.has(at)) : injections, opts?.budget,
|
|
9539
|
+
tools, plan ? (injections ?? []).filter((_, at) => placed.has(at)) : injections, opts?.budget, availableTools, {
|
|
9285
9540
|
dryRun: true,
|
|
9286
9541
|
...(plan && !held ? { appendedMessages: plan.rows.map(({ participant, content }) => ({ participant, content })) } : {}),
|
|
9287
9542
|
},
|
|
@@ -9301,7 +9556,13 @@ export class AgentFramework {
|
|
|
9301
9556
|
* see `AutobiographicalStrategy.previewContext` and
|
|
9302
9557
|
* `Agent.compileWithInjections`.
|
|
9303
9558
|
*
|
|
9304
|
-
* Returns null when the agent's strategy has
|
|
9559
|
+
* Returns null when the agent's strategy has nothing to preview: one without
|
|
9560
|
+
* `previewContext`, or a non-adaptive `AutobiographicalStrategy` whose select
|
|
9561
|
+
* fits the budget, built from the framework's own copy of context-manager.
|
|
9562
|
+
* Built from another copy, such a strategy throws its `previewContext
|
|
9563
|
+
* requires adaptiveResolution` error here instead, since the manager
|
|
9564
|
+
* recognises only its own copy's `NoFoldPlanToPreviewError`. Over the
|
|
9565
|
+
* budget, a non-adaptive strategy's preview is an infeasible one.
|
|
9305
9566
|
*/
|
|
9306
9567
|
previewContextSettings(
|
|
9307
9568
|
agentName: string,
|
|
@@ -9357,9 +9618,9 @@ export class AgentFramework {
|
|
|
9357
9618
|
*
|
|
9358
9619
|
* Never throws for preview-layer reasons: strategy without previewContext,
|
|
9359
9620
|
* preview overlap, stale branch generation, the hierarchical (non-
|
|
9360
|
-
* adaptive) path, and a context manager that refuses its
|
|
9361
|
-
* strategy's initialization pending or failed) all come back
|
|
9362
|
-
* `{available: false, reason}`. Unknown agent still throws, matching the
|
|
9621
|
+
* adaptive) path within its budget, and a context manager that refuses its
|
|
9622
|
+
* queries (its strategy's initialization pending or failed) all come back
|
|
9623
|
+
* as `{available: false, reason}`. Unknown agent still throws, matching the
|
|
9363
9624
|
* sibling accessors, and so does a patch whose values the agent refuses,
|
|
9364
9625
|
* whatever its context manager's state. Whether the strategy takes live
|
|
9365
9626
|
* tail and pace settings is the manager's to say, so while it refuses, a
|
|
@@ -10488,16 +10749,18 @@ export class AgentFramework {
|
|
|
10488
10749
|
'Add or replace a wake rule (a gate.json policy) at runtime — no need to ' +
|
|
10489
10750
|
'hand-edit the file. The rule is validated and hot-applied immediately. ' +
|
|
10490
10751
|
'A rule with the same `name` replaces the existing one in place (add a ' +
|
|
10491
|
-
'position to move it).
|
|
10492
|
-
'
|
|
10493
|
-
'
|
|
10494
|
-
'
|
|
10495
|
-
'
|
|
10496
|
-
'
|
|
10497
|
-
'
|
|
10498
|
-
'
|
|
10499
|
-
'
|
|
10500
|
-
'
|
|
10752
|
+
'position to move it). Rules are checked in order and the first one that ' +
|
|
10753
|
+
'matches an event decides it, so the rules below never see that event: a ' +
|
|
10754
|
+
'broad rule placed early takes events from them, your DM and mention ' +
|
|
10755
|
+
'rules included. Place a rule next to a particular one with ' +
|
|
10756
|
+
'`insertBefore`/`insertAfter`; `position: "prepend"` puts it above all of ' +
|
|
10757
|
+
'them. For a rule that only adds wakes ("also wake me every Nth event", ' +
|
|
10758
|
+
'"at most N an hour"), set `passthrough: true`: the events it doesn\'t ' +
|
|
10759
|
+
'wake for go on to your other rules. The result shows how a few common ' +
|
|
10760
|
+
'events are decided before and after the change. Its `shadowWarnings` ' +
|
|
10761
|
+
'are notes on your whole rule list, not only this change, wherever an ' +
|
|
10762
|
+
'earlier rule takes events a later one could get: each says whether that ' +
|
|
10763
|
+
'matters and what to do about it. Two common shapes:\n' +
|
|
10501
10764
|
'• Watch a FILE/workspace path: match on `mount` + `pathGlob`, e.g. ' +
|
|
10502
10765
|
'`{ name: "watch-notes", match: { scope: ["workspace:modified"], mount: "project", pathGlob: "notes/*.md" }, behavior: "always" }`.\n' +
|
|
10503
10766
|
'• Watch a CHANNEL: match on `source` + `channel` (and/or `tagsAny: ["chat:ambient"]`), ' +
|
|
@@ -11020,15 +11283,15 @@ export class AgentFramework {
|
|
|
11020
11283
|
const ep = this.rewindEpisode.get(agent.name);
|
|
11021
11284
|
const count = (ep?.count ?? 0) + 1;
|
|
11022
11285
|
const why = cause === 'refusal'
|
|
11023
|
-
? `the model refused on them (content filter: ${category}). Their content is ` +
|
|
11286
|
+
? `the model refused on them (content filter: ${failureShown(category)}). Their content is ` +
|
|
11024
11287
|
`not reproduced here (so this note can't re-trigger the filter); the ` +
|
|
11025
11288
|
`originals remain in the raw record`
|
|
11026
|
-
: `the model API kept refusing the requests that carried them (${category}), ` +
|
|
11289
|
+
: `the model API kept refusing the requests that carried them (${failureShown(category)}), ` +
|
|
11027
11290
|
`so that the conversation could go on. Their content is not reproduced ` +
|
|
11028
11291
|
`here (so this note can't trigger the same refusal); the originals remain ` +
|
|
11029
11292
|
`in the raw record`;
|
|
11030
11293
|
const text =
|
|
11031
|
-
`[
|
|
11294
|
+
`[${NOTICE_TAG.refusalRewind}] The ${count} most recent turn(s) were set aside because ` +
|
|
11032
11295
|
`${why}. You are clear to continue — if you ` +
|
|
11033
11296
|
`were mid-task, take a different approach; otherwise carry on with whatever ` +
|
|
11034
11297
|
`is now in front of you, or briefly acknowledge the gap and ask what's next.`;
|
|
@@ -13436,6 +13699,9 @@ export class AgentFramework {
|
|
|
13436
13699
|
}
|
|
13437
13700
|
|
|
13438
13701
|
private async createAgent(config: AgentConfig): Promise<Agent> {
|
|
13702
|
+
const sees = (metadata: unknown): boolean =>
|
|
13703
|
+
!(metadata as { tuneOut?: unknown } | undefined)?.tuneOut
|
|
13704
|
+
&& !isAnotherAgentsInjection(metadata, config.name);
|
|
13439
13705
|
// Create context manager for this agent
|
|
13440
13706
|
const contextManager = await ContextManager.open({
|
|
13441
13707
|
store: this.store,
|
|
@@ -13456,10 +13722,10 @@ export class AgentFramework {
|
|
|
13456
13722
|
// Another agent's stored context injections (stored-injections.ts) are
|
|
13457
13723
|
// that agent's context, not conversation: they stay out of this view,
|
|
13458
13724
|
// and so out of its folds, as the residents share this slot.
|
|
13459
|
-
viewFilter: (message) =>
|
|
13460
|
-
&& !isAnotherAgentsInjection(message.metadata, config.name),
|
|
13725
|
+
viewFilter: (message) => sees(message.metadata),
|
|
13461
13726
|
});
|
|
13462
13727
|
this.trackManager(contextManager);
|
|
13728
|
+
this.storeSees.set(contextManager, sees);
|
|
13463
13729
|
|
|
13464
13730
|
const agent = new Agent(config, contextManager, this.membrane);
|
|
13465
13731
|
this.adoptToolResultGuard(agent);
|
|
@@ -13527,6 +13793,7 @@ export class AgentFramework {
|
|
|
13527
13793
|
// oversized tool results / attachments under the same policy.
|
|
13528
13794
|
maxMessageTokens: primaryConfig.strategy?.maxMessageTokens,
|
|
13529
13795
|
});
|
|
13796
|
+
const sees = (metadata: unknown): boolean => !isAnotherAgentsInjection(metadata, name);
|
|
13530
13797
|
const contextManager = await ContextManager.open({
|
|
13531
13798
|
store: this.store,
|
|
13532
13799
|
namespace: `subconscious/${primaryName}`,
|
|
@@ -13537,12 +13804,13 @@ export class AgentFramework {
|
|
|
13537
13804
|
// residents' stored context injections (stored-injections.ts): their
|
|
13538
13805
|
// context, which this agent never saw while they were placed unstored.
|
|
13539
13806
|
auxiliaryMessageViews: [{}],
|
|
13540
|
-
viewFilter: (message) =>
|
|
13807
|
+
viewFilter: (message) => sees(message.metadata),
|
|
13541
13808
|
strategy,
|
|
13542
13809
|
membrane: this.membrane,
|
|
13543
13810
|
debugLogContext: !!process.env.DEBUG_CONTEXT,
|
|
13544
13811
|
});
|
|
13545
13812
|
this.trackManager(contextManager);
|
|
13813
|
+
this.storeSees.set(contextManager, sees);
|
|
13546
13814
|
|
|
13547
13815
|
// Inherit the principal's inference settings wholesale (fork-template
|
|
13548
13816
|
// idiom): reasoning effort (thinking), provider params, token ceilings,
|
|
@@ -13803,6 +14071,7 @@ export class AgentFramework {
|
|
|
13803
14071
|
if (flushedTurn) {
|
|
13804
14072
|
flushedTurn.roundsRan = true;
|
|
13805
14073
|
if (flushedTurn.unflushedRounds) flushedTurn.unflushedRounds--;
|
|
14074
|
+
this.endStatedWaitRun(agent.name);
|
|
13806
14075
|
}
|
|
13807
14076
|
if (flushedTurn && replySent === null && !flushedTurn.privateEnd && !flushedTurn.silent
|
|
13808
14077
|
&& pendingBlocks!.some((b) => b.type === 'text' && (b as { text?: string }).text?.trim())) {
|
|
@@ -13870,11 +14139,12 @@ export class AgentFramework {
|
|
|
13870
14139
|
// with everything queued behind it — the queue keeps its order —
|
|
13871
14140
|
// for the turn-end flush, where no reply follows it in this stream.
|
|
13872
14141
|
//
|
|
13873
|
-
// A message
|
|
13874
|
-
//
|
|
13875
|
-
//
|
|
13876
|
-
//
|
|
13877
|
-
//
|
|
14142
|
+
// A message the agent's own view filter leaves out, such as one a
|
|
14143
|
+
// tune-out holds from the residents or another agent's stored
|
|
14144
|
+
// context injection, is drained and stored like any other, but not
|
|
14145
|
+
// injected: the next compile leaves it out as well, so the window's
|
|
14146
|
+
// compiled prefix and the live request still match without it
|
|
14147
|
+
// (#232 gap 5).
|
|
13878
14148
|
//
|
|
13879
14149
|
// A stream that declares it can't carry injected messages
|
|
13880
14150
|
// (supportsInjectedMessages false: membrane's XML prefill path,
|
|
@@ -13907,14 +14177,11 @@ export class AgentFramework {
|
|
|
13907
14177
|
const stored = this.writeDrainedInOrder(deferred, `tool boundary for ${agent.name}`, (msg) =>
|
|
13908
14178
|
agent.getContextManager().addMessage(msg.participant, msg.content, withDeferredWriteId(msg.metadata, msg.id)));
|
|
13909
14179
|
for (const [msg, { id, content }] of stored) {
|
|
14180
|
+
this.noteSpokenTo(agent.name, msg.metadata);
|
|
13910
14181
|
// Every other resident reads what this boundary stores; for its
|
|
13911
14182
|
// own agent, the boundary weighs what it injects below.
|
|
13912
14183
|
this.noteForOtherResidents(agent, msg.metadata, id);
|
|
13913
|
-
|
|
13914
|
-
// subconscious reads it) and left out of every resident's
|
|
13915
|
-
// compiled view; injecting it here would put in the live turn
|
|
13916
|
-
// what the next compile hides (#232 gap 5).
|
|
13917
|
-
if ((msg.metadata as { tuneOut?: unknown } | undefined)?.tuneOut) continue;
|
|
14184
|
+
if (!this.agentSees(agent, msg.metadata)) continue;
|
|
13918
14185
|
heard.push({ msg, id, content });
|
|
13919
14186
|
}
|
|
13920
14187
|
// Rendered in one call: a render prepares as a compile does, so
|
|
@@ -13926,10 +14193,12 @@ export class AgentFramework {
|
|
|
13926
14193
|
const rendered = this.renderForInjection(agent, [...(clock ? [clock.id] : []), ...heard.map((h) => h.id)]);
|
|
13927
14194
|
const first = clock ? 1 : 0;
|
|
13928
14195
|
// A null is a message the render places before its window. Under
|
|
13929
|
-
//
|
|
13930
|
-
// what this boundary stored is bigger than it,
|
|
13931
|
-
// while a head window a topic change reset hasn't
|
|
13932
|
-
//
|
|
14196
|
+
// context-manager 0.23.0's memory strategy that's one before its
|
|
14197
|
+
// recent window, when what this boundary stored is bigger than it,
|
|
14198
|
+
// or any message while a head window a topic change reset hasn't
|
|
14199
|
+
// filled yet; from 0.23.1 that strategy gives no null for a
|
|
14200
|
+
// message its view holds. The next compile still sends such a
|
|
14201
|
+
// message, raw and in order, after the tool
|
|
13933
14202
|
// result, so leaving it out would change the prefix as surely as
|
|
13934
14203
|
// any form, and the agent wouldn't hear it (context-manager#275's
|
|
13935
14204
|
// review, R1-1). It goes in as stored, which is the form the next
|
|
@@ -13940,13 +14209,12 @@ export class AgentFramework {
|
|
|
13940
14209
|
// cap reaches it. And a body the strategy stored in parts goes in
|
|
13941
14210
|
// whole, joined as the store reads it back, where the next
|
|
13942
14211
|
// compile sends it in more than one piece.
|
|
13943
|
-
// That rests on two things.
|
|
13944
|
-
//
|
|
13945
|
-
//
|
|
13946
|
-
//
|
|
13947
|
-
//
|
|
13948
|
-
//
|
|
13949
|
-
// windowed passthrough's anchor past it, before the render. A
|
|
14212
|
+
// That rests on two things. A message the agent's own view filter
|
|
14213
|
+
// leaves out is skipped above, by that filter, so no null here
|
|
14214
|
+
// means a message the view hides. And the store and the render
|
|
14215
|
+
// run with nothing between them, so nothing can put a summary in
|
|
14216
|
+
// place of what this flush stored, or move windowed passthrough's
|
|
14217
|
+
// anchor past it, before the render. A
|
|
13950
14218
|
// write found here that landed at an earlier flush
|
|
13951
14219
|
// (writeDrainedInOrder) is the exception: by now a summary may
|
|
13952
14220
|
// stand in for it, or the anchor be past it, and then it goes in
|
|
@@ -14314,6 +14582,9 @@ export class AgentFramework {
|
|
|
14314
14582
|
// Dispatch to all modules, tracking responses with module names
|
|
14315
14583
|
const responses: ModuleProcessResponse[] = [];
|
|
14316
14584
|
for (const module of this.moduleRegistry.getAllModules()) {
|
|
14585
|
+
// The framework's own toolsets module answers no event; every agent has
|
|
14586
|
+
// it now, so it isn't listed beside the modules that do.
|
|
14587
|
+
if (module instanceof ToolsetsModule) continue;
|
|
14317
14588
|
try {
|
|
14318
14589
|
const processState = this.moduleRegistry.createProcessState(module.name);
|
|
14319
14590
|
const response = await module.onProcess(event, processState);
|
|
@@ -14413,6 +14684,7 @@ export class AgentFramework {
|
|
|
14413
14684
|
// surface carries where the framework records none.
|
|
14414
14685
|
const eventChannel = (event as { metadata?: { channelId?: unknown } }).metadata?.channelId;
|
|
14415
14686
|
const namesChannel = typeof eventChannel === 'string' && eventChannel !== '';
|
|
14687
|
+
const localInput = event.type === 'external-message' || event.type === 'api:message';
|
|
14416
14688
|
let added: { agent?: string; deferredId?: string; messageId?: string } | undefined;
|
|
14417
14689
|
if (response.addMessages) {
|
|
14418
14690
|
for (const [i, msg] of response.addMessages.entries()) {
|
|
@@ -14424,7 +14696,7 @@ export class AgentFramework {
|
|
|
14424
14696
|
}
|
|
14425
14697
|
if (surface) metadata = { ...metadata, [INBOUND_SOURCE_KEY]: surface };
|
|
14426
14698
|
const placement: { agent?: string; deferredId?: string; messageId?: string } = {};
|
|
14427
|
-
const id = this.addMessage(msg.participant, msg.content, metadata, { placement });
|
|
14699
|
+
const id = this.addMessage(msg.participant, localInput ? markNoticeOpenings(msg.content) : msg.content, metadata, { placement });
|
|
14428
14700
|
if (response.addMessages.length === 1 && !surface && !namesChannel && metadata?.channelId === undefined) added = placement;
|
|
14429
14701
|
this.emitTrace({ type: 'message:added', messageId: id, source: event.type });
|
|
14430
14702
|
}
|
|
@@ -14666,6 +14938,7 @@ export class AgentFramework {
|
|
|
14666
14938
|
missedCharacters: typeof event.metadata?.missedCharacters === 'number'
|
|
14667
14939
|
? event.metadata.missedCharacters
|
|
14668
14940
|
: undefined,
|
|
14941
|
+
targetAgents: event.targetAgents,
|
|
14669
14942
|
});
|
|
14670
14943
|
if (invitation) {
|
|
14671
14944
|
incomingContent.push(invitation.block);
|
|
@@ -15292,11 +15565,6 @@ export class AgentFramework {
|
|
|
15292
15565
|
* explicit-prose agent's natural plain reply bounced with nowhere to go
|
|
15293
15566
|
* (Rhys's first portal mention: a 2,320-char reply stranded).
|
|
15294
15567
|
*
|
|
15295
|
-
* Wording: option 1 teaches the `>>` destination prefix, which delivers for
|
|
15296
|
-
* BOTH legacy (host-inferred) and explicit prose-routing agents. The
|
|
15297
|
-
* previous "simply write your reply as normal text" was a false promise
|
|
15298
|
-
* under explicit routing (the metadata flags it set had no consumers).
|
|
15299
|
-
*
|
|
15300
15568
|
* Human-readable names wherever the surface provided them — a mind reading
|
|
15301
15569
|
* this should see WHO addressed it WHERE, not a wall of snowflake ids. Raw
|
|
15302
15570
|
* ids remain only inside the tool-argument instructions, where they are the
|
|
@@ -15348,6 +15616,7 @@ export class AgentFramework {
|
|
|
15348
15616
|
missedMessages?: number;
|
|
15349
15617
|
/** Characters across those dropped messages. */
|
|
15350
15618
|
missedCharacters?: number;
|
|
15619
|
+
targetAgents?: readonly string[];
|
|
15351
15620
|
}): { block: ContentBlock; metadataPatch: Record<string, unknown> } | null {
|
|
15352
15621
|
if (!this.channelRegistry) return null;
|
|
15353
15622
|
if (!opts.tags?.includes('chat:addressed')) return null;
|
|
@@ -15363,8 +15632,11 @@ export class AgentFramework {
|
|
|
15363
15632
|
// only where the name lacks one, by proseTargetFor's rule.
|
|
15364
15633
|
const bareLabel = label && label !== opts.channelId ? label.replace(/^#/, '') : undefined;
|
|
15365
15634
|
const hashLabel = bareLabel ? `#${bareLabel}` : undefined;
|
|
15366
|
-
const
|
|
15367
|
-
const
|
|
15635
|
+
const channelId = quotedValue(opts.channelId);
|
|
15636
|
+
const messageId = quotedValue(opts.messageId);
|
|
15637
|
+
const authorName = suppliedValue(opts.authorName);
|
|
15638
|
+
const channelLabel = hashLabel ? suppliedValue(hashLabel) : channelId;
|
|
15639
|
+
const place = opts.guildName ? `${channelLabel} in "${suppliedValue(opts.guildName)}"` : channelLabel;
|
|
15368
15640
|
// The label is for reading; the prefix must be ONE token that resolves back
|
|
15369
15641
|
// here, on this server (a DM label "DM: alice" quoted verbatim parses as
|
|
15370
15642
|
// target "#DM:"). A registry that can name targets is authoritative: no safe
|
|
@@ -15373,7 +15645,7 @@ export class AgentFramework {
|
|
|
15373
15645
|
const namer = this.channelRegistry as unknown as { proseTargetFor?: (ref: ChannelRef) => string | undefined };
|
|
15374
15646
|
const target = typeof namer.proseTargetFor === 'function'
|
|
15375
15647
|
? namer.proseTargetFor(channel)
|
|
15376
|
-
: [hashLabel, opts.channelId].find((t): t is string => !!t && isProseTargetToken(t));
|
|
15648
|
+
: [hashLabel, opts.channelId].find((t): t is string => !!t && isProseTargetToken(t) && showsAsItself(t));
|
|
15377
15649
|
const inboundOnlyServer = this.channelRegistry.inboundOnlyServer(channel);
|
|
15378
15650
|
// What replying does here is the registry's to say (speechOpens): a reply
|
|
15379
15651
|
// opens a channel nobody decided to keep closed, and leaves one the agent
|
|
@@ -15383,24 +15655,39 @@ export class AgentFramework {
|
|
|
15383
15655
|
const replyOpens = this.channelRegistry.speechOpens(channel);
|
|
15384
15656
|
// The decline is the way to reply without opening a channel, except in a
|
|
15385
15657
|
// tuned-out one, which channel_decline refuses: the tune-out ends first.
|
|
15386
|
-
const
|
|
15387
|
-
|
|
15388
|
-
|
|
15389
|
-
|
|
15658
|
+
const tunedOut = this.channelRegistry.getDesiredState(opts.serverId, opts.channelId) === 'tuned-out';
|
|
15659
|
+
const withoutOpening = tunedOut
|
|
15660
|
+
? ''
|
|
15661
|
+
: " To reply without opening it, decline it first (option 3): a channel you've declined stays closed when you speak there.";
|
|
15662
|
+
const named = [...new Set(opts.targetAgents ?? [])].filter((name) => this.agents.has(name));
|
|
15663
|
+
const woken = named.length === 1 ? this.agents.get(named[0]!) : undefined;
|
|
15664
|
+
const mode = (woken ?? this.deliveryAgent())?.proseRouting ?? 'locus';
|
|
15665
|
+
const prefix = mode === 'hybrid' ? '>>>' : '>>';
|
|
15666
|
+
const delivered = replyOpens
|
|
15667
|
+
? `it will be delivered there, which opens the channel.${withoutOpening}`
|
|
15668
|
+
: 'it will be delivered there, and the channel stays closed.';
|
|
15669
|
+
const sendTool = `check whether ${opts.serverId} exposes a send tool that can address it`;
|
|
15670
|
+
const plainSpeechRule = `your plain speech lands where the latest word on where it lands says: ` +
|
|
15671
|
+
`a [${NOTICE_TAG.routing}] notice, or channel_open's result. If that is ${this.channelShown(channel)}, ` +
|
|
15672
|
+
`write your reply with no prefix;`;
|
|
15390
15673
|
const replyOption = inboundOnlyServer
|
|
15391
15674
|
? `1. Replying there isn't available: ${inboundOnlyServer} declared this channel inbound-only, so nothing ` +
|
|
15392
15675
|
`you write is posted into it. To speak there, use ${inboundOnlyServer}'s own tools.\n`
|
|
15393
|
-
:
|
|
15394
|
-
?
|
|
15395
|
-
|
|
15396
|
-
|
|
15397
|
-
|
|
15398
|
-
|
|
15399
|
-
|
|
15400
|
-
|
|
15401
|
-
|
|
15402
|
-
? `
|
|
15403
|
-
: `
|
|
15676
|
+
: mode === 'disabled'
|
|
15677
|
+
? `1. Replying in plain text isn't available: your plain text is never published. To speak there, ${sendTool}.\n`
|
|
15678
|
+
: mode === 'locus' && tunedOut
|
|
15679
|
+
? `1. Reply — ${plainSpeechRule} it will be delivered there. If that is another channel, or you see none, to reply here, ` +
|
|
15680
|
+
`${sendTool}.\n`
|
|
15681
|
+
: mode === 'locus'
|
|
15682
|
+
? `1. Reply — ${plainSpeechRule} ${delivered} If that is another channel, or you see none, to reply here, ` +
|
|
15683
|
+
`${sendTool}, or open it (option 2), whose result says where your plain speech lands.\n`
|
|
15684
|
+
: target
|
|
15685
|
+
? `1. Reply — write your reply this turn prefixed with "${prefix}${target}"; ${delivered}\n`
|
|
15686
|
+
: `1. Reply — no single-word target reaches this channel unambiguously, ` +
|
|
15687
|
+
`so a "${prefix}" prefix can't name it; ${sendTool}.` +
|
|
15688
|
+
(replyOpens
|
|
15689
|
+
? ` Replying there opens the channel.${withoutOpening}\n`
|
|
15690
|
+
: ` Replying there leaves the channel closed.\n`);
|
|
15404
15691
|
const missedCount = opts.missedMessages ?? 0;
|
|
15405
15692
|
const missedNote =
|
|
15406
15693
|
missedCount > 0
|
|
@@ -15411,18 +15698,18 @@ export class AgentFramework {
|
|
|
15411
15698
|
const block: ContentBlock = {
|
|
15412
15699
|
type: 'text',
|
|
15413
15700
|
text:
|
|
15414
|
-
`\n[
|
|
15701
|
+
`\n[${NOTICE_TAG.channelInvitation}] ${authorName} addressed you in ${place} — ` +
|
|
15415
15702
|
`a channel you haven't opened. Messages there that address you, such as an @-mention, ` +
|
|
15416
15703
|
`reach you whether or not you open it. ` +
|
|
15417
15704
|
missedNote +
|
|
15418
15705
|
`Your options:\n` +
|
|
15419
15706
|
replyOption +
|
|
15420
|
-
`2. Open the channel — call channel_open with channelId
|
|
15707
|
+
`2. Open the channel — call channel_open with channelId ${channelId} and serverId "${opts.serverId}"` +
|
|
15421
15708
|
(maxBackscroll > 0
|
|
15422
|
-
? `; to also read recent history, add backscroll (a number up to ${maxBackscroll}) and beforeMessageId
|
|
15709
|
+
? `; to also read recent history, add backscroll (a number up to ${maxBackscroll}) and beforeMessageId ${messageId}.\n`
|
|
15423
15710
|
: `.\n`) +
|
|
15424
|
-
`3. Stay out — call channel_decline with channelId
|
|
15425
|
-
`and messageId
|
|
15711
|
+
`3. Stay out — call channel_decline with channelId ${channelId}, serverId "${opts.serverId}", ` +
|
|
15712
|
+
`and messageId ${messageId} (optionally set acknowledge to an emoji like 👀 so ${authorName} isn't left hanging).\n` +
|
|
15426
15713
|
`Doing nothing is also fine.`,
|
|
15427
15714
|
};
|
|
15428
15715
|
return {
|
|
@@ -16056,7 +16343,7 @@ export class AgentFramework {
|
|
|
16056
16343
|
messageId: identity.messageId ?? str(origin.messageId) ?? occ.eventId,
|
|
16057
16344
|
threadId: identity.threadId ?? str(origin.threadId),
|
|
16058
16345
|
author: identity.author ?? { id: str(origin.authorId) ?? '', name: str(origin.authorName) ?? str(origin.authorId) ?? 'Someone' },
|
|
16059
|
-
content: materialized ? materialized
|
|
16346
|
+
content: materialized ? convertBlocks(materialized, 'attachment on push event', occ.serverId) : push.content,
|
|
16060
16347
|
timestamp: occ.timestamp,
|
|
16061
16348
|
metadata: origin,
|
|
16062
16349
|
tags: push.tags,
|
|
@@ -16077,7 +16364,7 @@ export class AgentFramework {
|
|
|
16077
16364
|
...occ.event.event,
|
|
16078
16365
|
eventId: occ.eventId,
|
|
16079
16366
|
timestamp: occ.timestamp,
|
|
16080
|
-
...(materialized ? { content: materialized
|
|
16367
|
+
...(materialized ? { content: convertBlocks(materialized, 'attachment on push event', occ.serverId) } : {}),
|
|
16081
16368
|
coalescingSubject,
|
|
16082
16369
|
// §5.2: the materialized occurrence is consumed by the request being
|
|
16083
16370
|
// assembled; the wake that started it is not repeated.
|
|
@@ -16203,19 +16490,13 @@ export class AgentFramework {
|
|
|
16203
16490
|
// Every agent that can read this message must not have compiled past it.
|
|
16204
16491
|
// Residents share one message slot, so a message one of them read is
|
|
16205
16492
|
// history for all of them (the never-rewrite rule is about the message,
|
|
16206
|
-
// not the agent that happened to be asked). A
|
|
16207
|
-
//
|
|
16208
|
-
//
|
|
16209
|
-
|
|
16210
|
-
|
|
16211
|
-
|
|
16212
|
-
|
|
16213
|
-
readers = [subconscious];
|
|
16214
|
-
} else {
|
|
16215
|
-
readers = this.sharedSlotAgents.has(agent)
|
|
16216
|
-
? [...this.sharedSlotAgents].filter((a) => this.agents.get(a.name) === a)
|
|
16217
|
-
: [agent];
|
|
16218
|
-
}
|
|
16493
|
+
// not the agent that happened to be asked). A reader is one whose view
|
|
16494
|
+
// filter keeps the message: a tune-out-diverted message never enters a
|
|
16495
|
+
// resident's compiled view, and only the subconscious reads it through
|
|
16496
|
+
// its merged view — no subconscious, nobody reads it.
|
|
16497
|
+
const readers = (this.sharedSlotAgents.has(agent)
|
|
16498
|
+
? [...this.sharedSlotAgents].filter((a) => this.agents.get(a.name) === a)
|
|
16499
|
+
: [agent]).filter((a) => this.agentSees(a, message.metadata));
|
|
16219
16500
|
const branch = cm.currentBranch().name;
|
|
16220
16501
|
const ts = message.timestamp instanceof Date ? message.timestamp.getTime() : Number(message.timestamp);
|
|
16221
16502
|
for (const reader of readers) {
|
|
@@ -16351,6 +16632,7 @@ export class AgentFramework {
|
|
|
16351
16632
|
missedMessages: typeof origin.missedMessages === 'number' ? origin.missedMessages : undefined,
|
|
16352
16633
|
missedCharacters:
|
|
16353
16634
|
typeof origin.missedCharacters === 'number' ? origin.missedCharacters : undefined,
|
|
16635
|
+
targetAgents: event.targetAgents,
|
|
16354
16636
|
});
|
|
16355
16637
|
if (invitation) {
|
|
16356
16638
|
content.push(invitation.block);
|
|
@@ -16585,7 +16867,8 @@ export class AgentFramework {
|
|
|
16585
16867
|
// A provider wait binds its own model; with a different primary model
|
|
16586
16868
|
// selected now, the turn proceeds (the wait stays recorded).
|
|
16587
16869
|
const otherModel = providerCooldown.waitModel !== undefined && providerCooldown.waitModel !== agent.model;
|
|
16588
|
-
if (!otherModel &&
|
|
16870
|
+
if (!otherModel && !providerCooldown.rearm
|
|
16871
|
+
&& (now < providerCooldown.until || this.extendHostProviderHold(agentName, providerCooldown))) {
|
|
16589
16872
|
for (const req of requests) this.holdRequest(providerCooldown.heldRequests, req);
|
|
16590
16873
|
const recovery = this.providerAccelerationRecoveries.get(agentName);
|
|
16591
16874
|
if (recovery) recovery.heldRequests = providerCooldown.heldRequests.length;
|
|
@@ -16596,6 +16879,10 @@ export class AgentFramework {
|
|
|
16596
16879
|
// the held causes into this one current compile.
|
|
16597
16880
|
clearTimeout(providerCooldown.timer);
|
|
16598
16881
|
this.providerAccelerationCooldowns.delete(agentName);
|
|
16882
|
+
if (providerCooldown.rearm) {
|
|
16883
|
+
console.error(`[inference-rearm] agent=${agentName} turn queued again with a wake ` +
|
|
16884
|
+
`(${providerCooldown.rearm.number} of ${providerCooldown.rearm.of})`);
|
|
16885
|
+
}
|
|
16599
16886
|
const recovery = this.providerAccelerationRecoveries.get(agentName);
|
|
16600
16887
|
if (recovery) recovery.releasedAt = now;
|
|
16601
16888
|
requests = [...providerCooldown.heldRequests, ...requests];
|
|
@@ -16675,7 +16962,10 @@ export class AgentFramework {
|
|
|
16675
16962
|
// and a wake for a deferred write or a stored message is dropped
|
|
16676
16963
|
// once the agent is done with that write: neither may absorb an
|
|
16677
16964
|
// unrelated wake that would vanish with it.
|
|
16678
|
-
|
|
16965
|
+
// A heartbeat's wake keeps its own key, so a later channel-less wake
|
|
16966
|
+
// can't absorb it: resumed together, they make a heartbeat's batch
|
|
16967
|
+
// (heartbeatTurn), as they would have without the quiesce.
|
|
16968
|
+
const key = `${request.reason}|${request.addressed ? 'a' : 'n'}|${isHeartbeatWake(request) ? 'h' : ''}|${request.coalescingSubject ?? ''}|${request.readsDeferredWrite ?? ''}|${request.readsStoredMessage ?? ''}`;
|
|
16679
16969
|
const prev = newestByKey.get(key);
|
|
16680
16970
|
const kept = !prev || request.timestamp >= prev.timestamp ? request : prev;
|
|
16681
16971
|
// The wake it absorbs hands on its lines (WakeLine): each event's
|
|
@@ -16884,13 +17174,14 @@ export class AgentFramework {
|
|
|
16884
17174
|
// an open channel — legacy last-inbound semantics).
|
|
16885
17175
|
// A chat:system notice (RFC-001 rev 3: weather in a world, someone
|
|
16886
17176
|
// joining) names no channel at all: nobody spoke there, so a turn it
|
|
16887
|
-
// wakes alone takes the global fallback,
|
|
16888
|
-
//
|
|
17177
|
+
// wakes alone takes the global fallback, and the agent sends there
|
|
17178
|
+
// explicitly if it means to.
|
|
16889
17179
|
// Ambient chatter must not outrank an addressed message just by being
|
|
16890
17180
|
// newest (2026-07-21 Cairn lounge misroute, turn-start variant).
|
|
16891
17181
|
// Non-channel wakes (heartbeats, module events, reactions — which never
|
|
16892
17182
|
// carry channelId) leave both undefined → global fallback, which an
|
|
16893
|
-
// ephemeral run never takes (takesGlobalFallback)
|
|
17183
|
+
// ephemeral run never takes (takesGlobalFallback), except a heartbeat's
|
|
17184
|
+
// turn, which takes none (heartbeatTurn below).
|
|
16894
17185
|
// Track the winning REQUEST, not just its channel: channel, addressed
|
|
16895
17186
|
// and counterparty must come from the same message, or a batch of
|
|
16896
17187
|
// "ambient from A, then addressed from B" would report B's channel
|
|
@@ -16966,6 +17257,9 @@ export class AgentFramework {
|
|
|
16966
17257
|
wakeLines: mergeWakeLines(requests),
|
|
16967
17258
|
suppressProse: silentOnly ? trigger?.suppressProse : undefined,
|
|
16968
17259
|
silentHeartbeat: silentOnly ? trigger?.silentHeartbeat : undefined,
|
|
17260
|
+
// A heartbeat's turn when any cause is a heartbeat's wake and none
|
|
17261
|
+
// names a channel, whatever the batch's order (isHeartbeatWake).
|
|
17262
|
+
heartbeatTurn: !channelReq && requests.some((r) => isHeartbeatWake(r)),
|
|
16969
17263
|
// The channel and its server from the one request, so the turn's
|
|
16970
17264
|
// channel is that message's, never one server's id with another's.
|
|
16971
17265
|
channelId: channelReq?.channelId,
|
|
@@ -16992,6 +17286,8 @@ export class AgentFramework {
|
|
|
16992
17286
|
const carry = requests.map(turnCarryOf).find((c) => c !== undefined);
|
|
16993
17287
|
if (carry) start[TURN_CARRY] = carry;
|
|
16994
17288
|
else delete start[TURN_CARRY];
|
|
17289
|
+
if (requests.every(isRearmed)) start[REARMED] = true;
|
|
17290
|
+
else delete start[REARMED];
|
|
16995
17291
|
// A start that carries a turn on and took in wakes that don't records
|
|
16996
17292
|
// the batch it was merged from (MERGED_BATCH): until it takes its
|
|
16997
17293
|
// token it stands for them all.
|
|
@@ -17212,7 +17508,7 @@ export class AgentFramework {
|
|
|
17212
17508
|
if (hold.kind === 'place') {
|
|
17213
17509
|
const { place } = hold;
|
|
17214
17510
|
const where = this.channelShown(place.to);
|
|
17215
|
-
return `your ${place.tool} ${said(place.round)} ${place.delivered ? 'delivered' : 'may have posted'} to ${where}, ` +
|
|
17511
|
+
return `your ${suppliedValue(place.tool)} ${said(place.round)} ${place.delivered ? 'delivered' : 'may have posted'} to ${where}, ` +
|
|
17216
17512
|
`where this was going${lasts(place.round, true)}`;
|
|
17217
17513
|
}
|
|
17218
17514
|
const why = hold.why === 'error'
|
|
@@ -17225,7 +17521,7 @@ export class AgentFramework {
|
|
|
17225
17521
|
? "it hadn't answered when this speech was routed"
|
|
17226
17522
|
: "it hadn't answered by the end of its round";
|
|
17227
17523
|
const going = target ? `, and this was going to ${this.channelShown(target)}` : '';
|
|
17228
|
-
return `the framework can't tell ${hold.why === 'error' || hold.why === 'no-answer' ? 'whether or where' : 'where'} your ${hold.tool} ${said(hold.round)} posted (${why})${going}${lasts(hold.round, false)}`;
|
|
17524
|
+
return `the framework can't tell ${hold.why === 'error' || hold.why === 'no-answer' ? 'whether or where' : 'where'} your ${suppliedValue(hold.tool)} ${said(hold.round)} posted (${why})${going}${lasts(hold.round, false)}`;
|
|
17229
17525
|
}
|
|
17230
17526
|
|
|
17231
17527
|
/**
|
|
@@ -17314,8 +17610,8 @@ export class AgentFramework {
|
|
|
17314
17610
|
const r = registry.resolveProseTarget(name, { forSpeech: true, own });
|
|
17315
17611
|
return 'channelId' in r && sameChannel(r, target) ? name : undefined;
|
|
17316
17612
|
};
|
|
17317
|
-
const destination =
|
|
17318
|
-
(target && (resent(registry?.proseTargetFor(target)) ?? resent(qualifiedChannelName(target)))) ??
|
|
17613
|
+
const destination = quotedValue(
|
|
17614
|
+
(target && (resent(registry?.proseTargetFor(target)) ?? resent(qualifiedChannelName(target)))) ?? NO_CHANNEL_EXAMPLE,
|
|
17319
17615
|
);
|
|
17320
17616
|
// Words copied from a draft that may already have been posted resend
|
|
17321
17617
|
// only with the resident's confirmation; the rest resend freely.
|
|
@@ -17332,7 +17628,7 @@ export class AgentFramework {
|
|
|
17332
17628
|
// Named: residents that share one message slot (#197) share this window.
|
|
17333
17629
|
// "Not sent" only when no copied words may already have been posted.
|
|
17334
17630
|
return (
|
|
17335
|
-
`[drafts] ${agentName}: ${risky.length > 0 ? '' : 'not sent — '}` +
|
|
17631
|
+
`[${NOTICE_TAG.drafts}] ${agentName}: ${risky.length > 0 ? '' : 'not sent — '}` +
|
|
17336
17632
|
`${many ? `${held.length} plain-speech segments` : 'a plain-speech segment'} ` +
|
|
17337
17633
|
`held as draft${many ? 's' : ''} ${ids.join(', ')} — ${because ? `because ${because}` : AgentFramework.DRAFT_REASON_TEXT[reason]}. ` +
|
|
17338
17634
|
`${resend.join('; ')}; drafts(action: "dismiss", draftIds: [${quoted(held)}]) sets ${many ? 'them' : 'it'} aside.`
|
|
@@ -17353,8 +17649,9 @@ export class AgentFramework {
|
|
|
17353
17649
|
/** A destination as the agent is shown it: its label, with its name as
|
|
17354
17650
|
* ChannelRegistry.nameShown gives it. */
|
|
17355
17651
|
private destinationText(d: PublishDestination): string {
|
|
17356
|
-
const
|
|
17357
|
-
|
|
17652
|
+
const shown = d.label && d.label !== d.channelId ? suppliedValue(d.label) : '';
|
|
17653
|
+
const label = shown ? `${shown.startsWith('#') || shown.startsWith('DM') ? '' : '#'}${shown} ` : '';
|
|
17654
|
+
return `${label}(${this.channelRegistry?.nameShown(d) ?? suppliedValue(d.channelId)})`;
|
|
17358
17655
|
}
|
|
17359
17656
|
|
|
17360
17657
|
/** One line per draft: id, state, when, why, size, preview. */
|
|
@@ -17387,7 +17684,7 @@ export class AgentFramework {
|
|
|
17387
17684
|
const risky = uncertainAttempt(draft);
|
|
17388
17685
|
if (risky) {
|
|
17389
17686
|
return `${draft.id}'s attempt to ${this.destinationText(risky.destination)} at ` +
|
|
17390
|
-
`${this.draftTime(risky.at)} may already have been posted: ${risky.outcome?.reason
|
|
17687
|
+
`${this.draftTime(risky.at)} may already have been posted: ${risky.outcome?.reason !== undefined ? suppliedValue(risky.outcome.reason) : this.noOutcomeText(draft, risky)}`;
|
|
17391
17688
|
}
|
|
17392
17689
|
const inherited = draft.inheritedRisk!;
|
|
17393
17690
|
const owner = inherited.sourceDraftId ?? inherited.draftId;
|
|
@@ -17396,7 +17693,7 @@ export class AgentFramework {
|
|
|
17396
17693
|
? `${owner}'s attempt to ${this.destinationText(inherited.destination)}` +
|
|
17397
17694
|
(inherited.at !== undefined ? ` at ${this.draftTime(inherited.at)}` : '')
|
|
17398
17695
|
: owner;
|
|
17399
|
-
return `${draft.id} includes the words of ${inherited.draftId}${via}, and ${where} may already have been posted: ${inherited.reason}`;
|
|
17696
|
+
return `${draft.id} includes the words of ${inherited.draftId}${via}, and ${where} may already have been posted: ${suppliedValue(inherited.reason)}`;
|
|
17400
17697
|
}
|
|
17401
17698
|
|
|
17402
17699
|
/** Whether an attempt with no outcome is still out: its draft's latest,
|
|
@@ -17417,7 +17714,7 @@ export class AgentFramework {
|
|
|
17417
17714
|
if (!delivered) return `${draft.id}: not delivered`;
|
|
17418
17715
|
return `${draft.id}: delivered — confirmed at ${this.draftTime(delivered.outcome!.at)} to ` +
|
|
17419
17716
|
`${this.destinationText(delivered.destination)}` +
|
|
17420
|
-
(delivered.outcome!.messageId ? `, message ${delivered.outcome!.messageId}` : '') +
|
|
17717
|
+
(delivered.outcome!.messageId ? `, message ${nameToTypeBack(delivered.outcome!.messageId)}` : '') +
|
|
17421
17718
|
' (a record of that delivery, not a check that the message is still there)';
|
|
17422
17719
|
}
|
|
17423
17720
|
|
|
@@ -17460,7 +17757,7 @@ export class AgentFramework {
|
|
|
17460
17757
|
const rest = pending.slice(shown.length).map((d) => d.id);
|
|
17461
17758
|
const more = rest.length > 0 ? ` · and ${rest.length} more: ${rest.join(', ')}` : '';
|
|
17462
17759
|
const text =
|
|
17463
|
-
`[drafts] ${agent.name}: ${pending.length === 1 ? 'an open draft' : `${pending.length} open drafts`} of yours ` +
|
|
17760
|
+
`[${NOTICE_TAG.drafts}] ${agent.name}: ${pending.length === 1 ? 'an open draft' : `${pending.length} open drafts`} of yours ` +
|
|
17464
17761
|
`that no recent notice on this branch names: ${shown.join(' · ')}${more}. ` +
|
|
17465
17762
|
'drafts(action: "list") shows your open drafts; resend or dismiss them by id.';
|
|
17466
17763
|
let mid: string;
|
|
@@ -17702,17 +17999,17 @@ export class AgentFramework {
|
|
|
17702
17999
|
const blocks = drafts.map((d) => {
|
|
17703
18000
|
const lines = [
|
|
17704
18001
|
`${d.id} — ${draftState(d)}; held ${this.draftTime(d.heldAt)} (${AgentFramework.DRAFT_REASON_TEXT[d.reason]})` +
|
|
17705
|
-
(d.note ? `; ${d.note}` : ''),
|
|
18002
|
+
(d.note ? `; ${suppliedValue(d.note)}` : ''),
|
|
17706
18003
|
...(d.inheritedRisk && draftState(d) === 'unconfirmed' && !uncertainAttempt(d) ? [` ${this.riskText(d)}`] : []),
|
|
17707
18004
|
...d.attempts.map((a) => {
|
|
17708
18005
|
const o = a.outcome;
|
|
17709
18006
|
const status = !o
|
|
17710
18007
|
? (this.attemptIsOut(d, a) ? 'being sent right now — no outcome yet' : 'outcome never recorded — it may have been posted')
|
|
17711
18008
|
: o.status === 'delivered'
|
|
17712
|
-
? `delivered${o.messageId ? `, message ${o.messageId}` : ''}`
|
|
18009
|
+
? `delivered${o.messageId ? `, message ${nameToTypeBack(o.messageId)}` : ''}`
|
|
17713
18010
|
: o.status === 'unknown'
|
|
17714
|
-
? `UNKNOWN — may have been posted: ${o.reason
|
|
17715
|
-
: `failed, nothing posted: ${o.reason
|
|
18011
|
+
? `UNKNOWN — may have been posted: ${o.reason !== undefined ? suppliedValue(o.reason) : 'no valid receipt'}`
|
|
18012
|
+
: `failed, nothing posted: ${o.reason !== undefined ? suppliedValue(o.reason) : 'refused'}`;
|
|
17716
18013
|
return ` attempt ${this.draftTime(a.at)} → ${this.destinationText(a.destination)}: ${status}` +
|
|
17717
18014
|
(o?.status === 'unknown' && o.detail !== undefined ? `; connector detail: ${connectorText(o.detail)}` : '') +
|
|
17718
18015
|
(a.confirmedDuplicate ? ' (resent knowing it might duplicate)' : '');
|
|
@@ -17803,16 +18100,18 @@ export class AgentFramework {
|
|
|
17803
18100
|
// its turn a name the turn's own channel answers to means that channel.
|
|
17804
18101
|
const resolved = registry.resolveProseTarget(spec, { forSpeech: true, own });
|
|
17805
18102
|
if ('error' in resolved) {
|
|
17806
|
-
const cand = resolved.candidates?.length
|
|
17807
|
-
|
|
18103
|
+
const cand = resolved.candidates?.length
|
|
18104
|
+
? ` It could mean: ${resolved.candidates.join(', ')}.${registry.publishRouteFor(resolved.candidates) ?? ''}`
|
|
18105
|
+
: '';
|
|
18106
|
+
return refuse(`Destination "${spec}" did not resolve: ${suppliedValue(resolved.error)}.${cand} Nothing was sent.`);
|
|
17808
18107
|
}
|
|
17809
18108
|
named = resolved;
|
|
17810
18109
|
} else {
|
|
17811
18110
|
named = home!;
|
|
17812
18111
|
}
|
|
17813
18112
|
const target = registry.resolveDestination({ serverId: named.serverId, channelId: named.channelId });
|
|
17814
|
-
// Channels named as the agent names them back (
|
|
17815
|
-
if ('error' in target) return refuse(`Destination "${spec
|
|
18113
|
+
// Channels named as the agent names them back (quotedShown).
|
|
18114
|
+
if ('error' in target) return refuse(`Destination ${spec ? `"${spec}"` : registry.quotedShown(home!)}: ${suppliedValue(target.error)}. Nothing was sent.`);
|
|
17816
18115
|
const destination = target.destination;
|
|
17817
18116
|
if (home && !sameChannel(destination, home)) {
|
|
17818
18117
|
return refuse(
|
|
@@ -17901,7 +18200,7 @@ export class AgentFramework {
|
|
|
17901
18200
|
// it, so say what the draft reads as now rather than guess.
|
|
17902
18201
|
const unrecorded = recorded ? '' : ` (recording this outcome failed; the draft reads as ${this.draftStateNow(agentName, d.id)} now)`;
|
|
17903
18202
|
if (outcome.status === 'delivered') {
|
|
17904
|
-
lines.push(`${d.id}: delivered to ${where}${outcome.messageId ? `, message ${outcome.messageId}` : ''}${unrecorded}.`);
|
|
18203
|
+
lines.push(`${d.id}: delivered to ${where}${outcome.messageId ? `, message ${nameToTypeBack(outcome.messageId)}` : ''}${unrecorded}.`);
|
|
17905
18204
|
sendingTurn?.engaged.add(destinationKey(destination));
|
|
17906
18205
|
resender?.notePublished(destinationKey(destination), readThrough);
|
|
17907
18206
|
continue;
|
|
@@ -17912,9 +18211,9 @@ export class AgentFramework {
|
|
|
17912
18211
|
// The connector's data may say what a multi-part send already posted.
|
|
17913
18212
|
const detail = outcome.status === 'unknown' && outcome.detail !== undefined ? ` Connector detail: ${connectorText(outcome.detail)}` : '';
|
|
17914
18213
|
lines.push((outcome.status === 'unknown'
|
|
17915
|
-
? `${d.id}: delivery to ${where} NOT confirmed — it may or may not have been posted: ${outcome.reason
|
|
18214
|
+
? `${d.id}: delivery to ${where} NOT confirmed — it may or may not have been posted: ${outcome.reason !== undefined ? suppliedValue(outcome.reason) : 'no valid receipt'}. ` +
|
|
17916
18215
|
`Sending it again could post it twice, so a resend needs confirmDuplicate: true${unrecorded}.`
|
|
17917
|
-
: `${d.id}: not sent to ${where} — ${outcome.reason
|
|
18216
|
+
: `${d.id}: not sent to ${where} — ${outcome.reason !== undefined ? suppliedValue(outcome.reason) : 'refused'}. Nothing was posted by this attempt${unrecorded}.` +
|
|
17918
18217
|
(stillAtRisk
|
|
17919
18218
|
? ` The draft stays unconfirmed: ${this.riskText(after!)}.`
|
|
17920
18219
|
: ' The draft stays held.')) + detail);
|
|
@@ -17940,6 +18239,47 @@ export class AgentFramework {
|
|
|
17940
18239
|
return undefined;
|
|
17941
18240
|
}
|
|
17942
18241
|
|
|
18242
|
+
/** Why a heartbeat's turn holds its plain speech, in a held-draft notice ("because …"). */
|
|
18243
|
+
private static readonly HEARTBEAT_HOLD_BECAUSE =
|
|
18244
|
+
'this turn came from your heartbeat, not from a channel, so its plain speech has no destination ' +
|
|
18245
|
+
'and is kept for you to send where you choose';
|
|
18246
|
+
|
|
18247
|
+
/**
|
|
18248
|
+
* Locus mode's delivery of plain speech to the turn's frozen locus. A
|
|
18249
|
+
* heartbeat's turn with no locus of its own (TurnRouting.heartbeat) holds
|
|
18250
|
+
* the words as a draft instead of failing the delivery: nobody was waiting
|
|
18251
|
+
* for them, and the agent may send them where it chooses.
|
|
18252
|
+
*/
|
|
18253
|
+
private async routeLocusSpeech(
|
|
18254
|
+
agent: Agent,
|
|
18255
|
+
round: ProseRound,
|
|
18256
|
+
text: string,
|
|
18257
|
+
locus: ChannelRef | null,
|
|
18258
|
+
): Promise<{ delivered: boolean; serverId: string; channelId: string; messageId?: string } | null> {
|
|
18259
|
+
if (locus === null && round.routing.heartbeat) {
|
|
18260
|
+
this.holdHeartbeatProse(agent, [text], round);
|
|
18261
|
+
return null;
|
|
18262
|
+
}
|
|
18263
|
+
return this.channelRegistry!.routeSpeech(agent.name, text, locus);
|
|
18264
|
+
}
|
|
18265
|
+
|
|
18266
|
+
/** The mark a framework wake takes from a heartbeat's turn with no locus
|
|
18267
|
+
* of its own (InferenceRequest.heartbeatTurn), so the turn it opens is a
|
|
18268
|
+
* heartbeat's too; nothing for any other turn. */
|
|
18269
|
+
private heartbeatTurnMark(routing: TurnRouting): { heartbeatTurn?: true } {
|
|
18270
|
+
return routing.heartbeat && !routing.locus ? { heartbeatTurn: true } : {};
|
|
18271
|
+
}
|
|
18272
|
+
|
|
18273
|
+
/** Hold a heartbeat turn's plain speech as drafts ('no-destination'), named by `round`'s notice. */
|
|
18274
|
+
private holdHeartbeatProse(agent: Agent, texts: string[], round: ProseRound): Draft[] {
|
|
18275
|
+
console.error(`[prose] ${agent.name}: heartbeat turn — ${texts.length} plain-speech segment(s) held as drafts (no destination)`);
|
|
18276
|
+
return this.holdProseDrafts(agent, texts, 'no-destination', {
|
|
18277
|
+
routing: round.routing,
|
|
18278
|
+
notice: round.draftsRound ? 'later' : round.notice,
|
|
18279
|
+
because: AgentFramework.HEARTBEAT_HOLD_BECAUSE,
|
|
18280
|
+
});
|
|
18281
|
+
}
|
|
18282
|
+
|
|
17943
18283
|
/** Record a successful plain-prose delivery for this turn's receipt. */
|
|
17944
18284
|
private recordProseDelivery(
|
|
17945
18285
|
routing: TurnRouting,
|
|
@@ -18070,10 +18410,10 @@ export class AgentFramework {
|
|
|
18070
18410
|
// it is never uncertain.
|
|
18071
18411
|
const text =
|
|
18072
18412
|
shown.length > 0
|
|
18073
|
-
? `[delivered] plain speech → ${shown.join(' · ')}${suppressedNote ? ` · ${suppressedNote}` : ''}${closedNote}`
|
|
18413
|
+
? `[${NOTICE_TAG.delivered}] plain speech → ${shown.join(' · ')}${suppressedNote ? ` · ${suppressedNote}` : ''}${closedNote}`
|
|
18074
18414
|
: agent.proseRouting === 'disabled'
|
|
18075
|
-
? `[delivered] plain speech → no channel — ${suppressedNote}`
|
|
18076
|
-
: `[delivered] nothing${uncertain ? ' confirmed' : ''} — ${suppressedNote}`;
|
|
18415
|
+
? `[${NOTICE_TAG.delivered}] plain speech → no channel — ${suppressedNote}`
|
|
18416
|
+
: `[${NOTICE_TAG.delivered}] nothing${uncertain ? ' confirmed' : ''} — ${suppressedNote}`;
|
|
18077
18417
|
try {
|
|
18078
18418
|
const mid = agent.getContextManager().addMessage(
|
|
18079
18419
|
'user',
|
|
@@ -18097,7 +18437,7 @@ export class AgentFramework {
|
|
|
18097
18437
|
'user',
|
|
18098
18438
|
[{
|
|
18099
18439
|
type: 'text',
|
|
18100
|
-
text: `[
|
|
18440
|
+
text: `[${NOTICE_TAG.speechNotPosted}] Nothing you wrote before your tool calls in the rounds just above was posted. ` +
|
|
18101
18441
|
`If any of it should go out, send it again.`,
|
|
18102
18442
|
}],
|
|
18103
18443
|
{ system: true, kind: 'speech-not-posted' },
|
|
@@ -18110,7 +18450,7 @@ export class AgentFramework {
|
|
|
18110
18450
|
|
|
18111
18451
|
private noteFailedTurnEnd(agent: Agent, routing: TurnRouting): void {
|
|
18112
18452
|
const callsCutOff = (routing.unflushedRounds ?? 0) > 0;
|
|
18113
|
-
if (routing
|
|
18453
|
+
if (gotPartway(routing)) {
|
|
18114
18454
|
this.exhaustedTurns?.set(agent.name, {
|
|
18115
18455
|
roundsRan: routing.roundsRan === true, callsCutOff, liveSpeech: routing.liveSpeech === true,
|
|
18116
18456
|
speechRouting: (routing.speechRouting ?? 0) > 0,
|
|
@@ -18144,7 +18484,7 @@ export class AgentFramework {
|
|
|
18144
18484
|
try {
|
|
18145
18485
|
this.addMessage(
|
|
18146
18486
|
'user',
|
|
18147
|
-
[{ type: 'text', text: `[
|
|
18487
|
+
[{ type: 'text', text: `[${NOTICE_TAG.closedChannel}] ${notes.join(' ')}${this.closedChannelNoteOff(agent.name)}` }],
|
|
18148
18488
|
{ system: true, kind: 'delivery-receipt' } as MessageMetadata,
|
|
18149
18489
|
target,
|
|
18150
18490
|
);
|
|
@@ -18153,6 +18493,26 @@ export class AgentFramework {
|
|
|
18153
18493
|
}
|
|
18154
18494
|
}
|
|
18155
18495
|
|
|
18496
|
+
/**
|
|
18497
|
+
* Tell a heartbeat's turn, before it compiles, that its plain speech has
|
|
18498
|
+
* no destination (TurnRouting.heartbeat): the last `[routing]` notice may
|
|
18499
|
+
* name a channel, and this turn's words won't go there. It leaves the
|
|
18500
|
+
* announced baseline alone (announceLocusIfChanged).
|
|
18501
|
+
*/
|
|
18502
|
+
private noticeHeartbeatTurn(agentName: string): void {
|
|
18503
|
+
const agent = this.agents.get(agentName);
|
|
18504
|
+
if (!agent) return;
|
|
18505
|
+
const text =
|
|
18506
|
+
`[${NOTICE_TAG.routing}] This turn came from your heartbeat, not from a channel, so plain speech here isn't posted: ` +
|
|
18507
|
+
'it\'s held as a draft you can send with the drafts tool. An explicit send reaches a channel now.';
|
|
18508
|
+
try {
|
|
18509
|
+
const id = agent.getContextManager().addMessage('user', [{ type: 'text', text }], { system: true, kind: 'routing-notice' });
|
|
18510
|
+
this.emitTrace({ type: 'message:added', messageId: id, source: 'routing-notice' });
|
|
18511
|
+
} catch (err) {
|
|
18512
|
+
console.error('noticeHeartbeatTurn: failed to record the notice:', err);
|
|
18513
|
+
}
|
|
18514
|
+
}
|
|
18515
|
+
|
|
18156
18516
|
/**
|
|
18157
18517
|
* Announce-on-change locus notice (turn-frozen routing). Appends a durable
|
|
18158
18518
|
* `[routing]` window message when the turn's effective outbound locus
|
|
@@ -18177,15 +18537,15 @@ export class AgentFramework {
|
|
|
18177
18537
|
let text: string;
|
|
18178
18538
|
if (locus === null) {
|
|
18179
18539
|
text =
|
|
18180
|
-
|
|
18540
|
+
`[${NOTICE_TAG.routing}] Your plain speech currently has no channel — it stays in ` +
|
|
18181
18541
|
'your archive. Use an explicit send tool to reach a channel.';
|
|
18182
18542
|
} else {
|
|
18183
18543
|
const shown = this.channelShown(locus);
|
|
18184
18544
|
const inboundOnlyServer = this.channelRegistry?.inboundOnlyServer(locus);
|
|
18185
18545
|
text = inboundOnlyServer
|
|
18186
|
-
? `[routing] The conversation is now in ${shown}. ${inboundOnlySpeechNote(inboundOnlyServer)} ` +
|
|
18546
|
+
? `[${NOTICE_TAG.routing}] The conversation is now in ${shown}. ${inboundOnlySpeechNote(inboundOnlyServer)} ` +
|
|
18187
18547
|
'Other channels need an explicit send tool.'
|
|
18188
|
-
: `[routing] Your plain speech now lands in ${shown}. ` +
|
|
18548
|
+
: `[${NOTICE_TAG.routing}] Your plain speech now lands in ${shown}. ` +
|
|
18189
18549
|
'Other channels need an explicit send tool.';
|
|
18190
18550
|
}
|
|
18191
18551
|
|
|
@@ -18465,17 +18825,23 @@ export class AgentFramework {
|
|
|
18465
18825
|
return { newestRowAt, lastClockAt: undefined };
|
|
18466
18826
|
}
|
|
18467
18827
|
|
|
18828
|
+
private agentSees(agent: Agent, metadata: unknown): boolean {
|
|
18829
|
+
const sees = this.storeSees.get(agent.getContextManager());
|
|
18830
|
+
return !sees || sees(metadata);
|
|
18831
|
+
}
|
|
18832
|
+
|
|
18468
18833
|
/**
|
|
18469
18834
|
* Weigh a message that no tool boundary of the agent's open logical turn
|
|
18470
18835
|
* will weigh: one that would re-pin the locus becomes one of the turn's
|
|
18471
|
-
* arrivals (TurnRouting.arrivals). A message
|
|
18472
|
-
*
|
|
18473
|
-
* won't see it, and it moves nothing.
|
|
18836
|
+
* arrivals (TurnRouting.arrivals). A message the agent's own view leaves
|
|
18837
|
+
* out, such as one a tune-out diverted (metadata.tuneOut), never enters
|
|
18838
|
+
* its compiled view, so the agent won't see it, and it moves nothing.
|
|
18474
18839
|
*/
|
|
18475
18840
|
private noteArrival(agentName: string, metadata: MessageMetadata | undefined, messageId: MessageId): void {
|
|
18476
18841
|
const open = this.openLogical.get(agentName);
|
|
18477
18842
|
if (!open) return;
|
|
18478
|
-
|
|
18843
|
+
const agent = this.agents.get(agentName);
|
|
18844
|
+
if (agent && !this.agentSees(agent, metadata)) return;
|
|
18479
18845
|
if (isConversationalInjection(metadata)) (open.heard ??= new Map()).set(messageId, ++lastHeardAt);
|
|
18480
18846
|
const channel = this.repinChannel(metadata, open);
|
|
18481
18847
|
if (channel) open.arrivals.push({ messageId, channel });
|
|
@@ -18501,6 +18867,7 @@ export class AgentFramework {
|
|
|
18501
18867
|
if (!this.isOrdinaryResident(agent)) return;
|
|
18502
18868
|
for (const reader of this.sharedSlotAgents) {
|
|
18503
18869
|
if (reader !== agent && this.agents.get(reader.name) === reader && this.isOrdinaryResident(reader)) {
|
|
18870
|
+
this.noteSpokenTo(reader.name, metadata);
|
|
18504
18871
|
this.noteArrival(reader.name, metadata, messageId);
|
|
18505
18872
|
}
|
|
18506
18873
|
}
|
|
@@ -18572,9 +18939,9 @@ export class AgentFramework {
|
|
|
18572
18939
|
const content: ContentBlock[] = [{
|
|
18573
18940
|
type: 'text',
|
|
18574
18941
|
text: inboundOnlyServer
|
|
18575
|
-
? `[routing] The conversation moved to ${shown}. ${inboundOnlySpeechNote(inboundOnlyServer)} ` +
|
|
18942
|
+
? `[${NOTICE_TAG.routing}] The conversation moved to ${shown}. ${inboundOnlySpeechNote(inboundOnlyServer)} ` +
|
|
18576
18943
|
'Other channels need an explicit send tool.'
|
|
18577
|
-
: `[routing] The conversation moved to ${shown} — your plain ` +
|
|
18944
|
+
: `[${NOTICE_TAG.routing}] The conversation moved to ${shown} — your plain ` +
|
|
18578
18945
|
'speech now lands there for the rest of this turn. Other ' +
|
|
18579
18946
|
'channels need an explicit send tool.',
|
|
18580
18947
|
}];
|
|
@@ -18718,7 +19085,7 @@ export class AgentFramework {
|
|
|
18718
19085
|
}
|
|
18719
19086
|
const sender = this.agentForPrimary(agentName);
|
|
18720
19087
|
return [{
|
|
18721
|
-
text: `[channels] Now open: ${shown}: a message from ${sender.first} into the closed channel opened it; ${sender.again} has ended.` +
|
|
19088
|
+
text: `[${NOTICE_TAG.channels}] Now open: ${shown}: a message from ${sender.first} into the closed channel opened it; ${sender.again} has ended.` +
|
|
18722
19089
|
optOut(this.holdsChannelTraffic(primary, refs), this.canCloseChannels(primary, refs)),
|
|
18723
19090
|
metadata: { system: true, kind: 'channel-notice', speaker: agentName } as MessageMetadata,
|
|
18724
19091
|
told: `the primary, by name (${agentName} has ended)`,
|
|
@@ -18738,13 +19105,13 @@ export class AgentFramework {
|
|
|
18738
19105
|
const tellPrimary = !!agentName && !canClose && primary !== undefined && this.agents.has(primary) &&
|
|
18739
19106
|
this.canCloseChannels(primary, refs);
|
|
18740
19107
|
const readerText = (told: string) => (cause === 'subscription-policy'
|
|
18741
|
-
? `[channels] Selected for opening by subscription policy: ${shown}. ` +
|
|
19108
|
+
? `[${NOTICE_TAG.channels}] Selected for opening by subscription policy: ${shown}. ` +
|
|
18742
19109
|
(plural
|
|
18743
19110
|
? "The host opens them, and if one doesn't open, tries again when its server next registers it; " +
|
|
18744
19111
|
'channel_list shows whether each is open yet.'
|
|
18745
19112
|
: 'The host opens it, and if that fails, tries again when its server next registers it; ' +
|
|
18746
19113
|
"channel_list shows whether it's open yet.")
|
|
18747
|
-
: `[channels] Now open: ${shown}: your own message into the closed channel opened it.`) +
|
|
19114
|
+
: `[${NOTICE_TAG.channels}] Now open: ${shown}: your own message into the closed channel opened it.`) +
|
|
18748
19115
|
optOut(this.holdsChannelTraffic(who, refs), canClose) + told;
|
|
18749
19116
|
const reader: ChannelNotice = {
|
|
18750
19117
|
text: readerText(tellPrimary ? ` ${this.primaryForReader(agentName, primary)} has been told and can close ${plural ? 'them' : 'it'}.` : ''),
|
|
@@ -18761,7 +19128,7 @@ export class AgentFramework {
|
|
|
18761
19128
|
if (tellPrimary) {
|
|
18762
19129
|
const sender = this.agentForPrimary(agentName);
|
|
18763
19130
|
notices.push({
|
|
18764
|
-
text: `[channels] Now open: ${shown}: a message from ${sender.first} into the closed channel opened it, and ${sender.again} can't close ${plural ? 'them' : 'it'}.` +
|
|
19131
|
+
text: `[${NOTICE_TAG.channels}] Now open: ${shown}: a message from ${sender.first} into the closed channel opened it, and ${sender.again} can't close ${plural ? 'them' : 'it'}.` +
|
|
18765
19132
|
optOut(this.holdsChannelTraffic(primary, refs), true),
|
|
18766
19133
|
metadata: { system: true, kind: 'channel-notice', speaker: agentName } as MessageMetadata,
|
|
18767
19134
|
told: `the primary, by name (${agentName} can't close ${plural ? 'them' : 'it'})`,
|
|
@@ -18955,7 +19322,7 @@ export class AgentFramework {
|
|
|
18955
19322
|
primary !== undefined && this.agents.has(primary) &&
|
|
18956
19323
|
this.canCloseChannels(primary, [{ serverId, channelId: channel.channelId }]);
|
|
18957
19324
|
const sender = ended ? this.agentForPrimary(agentName!) : undefined;
|
|
18958
|
-
const readerText = (told: string) => `[channels] Not open: ${shown}. ` +
|
|
19325
|
+
const readerText = (told: string) => `[${NOTICE_TAG.channels}] Not open: ${shown}. ` +
|
|
18959
19326
|
(ended
|
|
18960
19327
|
? `A reply from ${sender!.first} went out there, but opening the channel failed; ${sender!.again} has ended.`
|
|
18961
19328
|
: 'Your reply there went out, but opening the channel failed.') +
|
|
@@ -18982,7 +19349,7 @@ export class AgentFramework {
|
|
|
18982
19349
|
const primaryCanOpen = !this.conversationAgentHomes.has(primary!) && this.canAgentUseTool(primary!, 'channel_open');
|
|
18983
19350
|
const reader = this.agentForPrimary(who!);
|
|
18984
19351
|
notices.push({
|
|
18985
|
-
text: `[channels] Not open: ${shown}. A reply from ${reader.first} went out there, but opening the channel failed, and ${reader.again} can't close it.` +
|
|
19352
|
+
text: `[${NOTICE_TAG.channels}] Not open: ${shown}. A reply from ${reader.first} went out there, but opening the channel failed, and ${reader.again} can't close it.` +
|
|
18986
19353
|
selected + ', and channel_close withdraws that.' +
|
|
18987
19354
|
(primaryCanOpen && !hostRetries ? ' channel_open tries again.' : ''),
|
|
18988
19355
|
metadata: { system: true, kind: 'channel-notice', speaker: who } as MessageMetadata,
|
|
@@ -19022,6 +19389,7 @@ export class AgentFramework {
|
|
|
19022
19389
|
reason: 'prose-continuation',
|
|
19023
19390
|
source: 'framework',
|
|
19024
19391
|
timestamp: Date.now(),
|
|
19392
|
+
...this.heartbeatTurnMark(routing),
|
|
19025
19393
|
});
|
|
19026
19394
|
}
|
|
19027
19395
|
|
|
@@ -19094,7 +19462,7 @@ export class AgentFramework {
|
|
|
19094
19462
|
this.autoContinueStreaks.set(name, count);
|
|
19095
19463
|
console.error(
|
|
19096
19464
|
`[continue] ${name}: turn ended (${stopReason}) without skip_reply — continuation ${count} of ${config.maxConsecutive}` +
|
|
19097
|
-
(routing.locus ? ` in ${this.channelRegistry?.nameShown(routing.locus) ?? routing.locus.channelId}` : ''),
|
|
19465
|
+
(routing.locus ? ` in ${this.channelRegistry?.nameShown(routing.locus) ?? suppliedValue(routing.locus.channelId)}` : ''),
|
|
19098
19466
|
);
|
|
19099
19467
|
// The next turn speaks where this one did (locus and hybrid modes): the
|
|
19100
19468
|
// locus's server and id, as the framework's own wakes name a channel. A
|
|
@@ -19113,6 +19481,7 @@ export class AgentFramework {
|
|
|
19113
19481
|
timestamp: Date.now(),
|
|
19114
19482
|
...channel,
|
|
19115
19483
|
autoContinueRun: { stop, ...(opts.heldAfterSend ? { heldAfterSend: true } : {}) },
|
|
19484
|
+
...this.heartbeatTurnMark(routing),
|
|
19116
19485
|
});
|
|
19117
19486
|
}
|
|
19118
19487
|
|
|
@@ -19132,11 +19501,12 @@ export class AgentFramework {
|
|
|
19132
19501
|
* starts (startAgentStream), with the run's count as it stands and the
|
|
19133
19502
|
* agent's cap. False when it couldn't be stored.
|
|
19134
19503
|
*/
|
|
19135
|
-
private writeAutoContinueNotice(agent: Agent, mark: AutoContinueRun): boolean {
|
|
19504
|
+
private writeAutoContinueNotice(agent: Agent, mark: AutoContinueRun, heartbeat = false): boolean {
|
|
19136
19505
|
const count = this.autoContinueStreaks.get(agent.name) ?? 1;
|
|
19137
19506
|
const max = agent.autoContinue?.maxConsecutive ?? count;
|
|
19138
19507
|
const text = autoContinueNotice({
|
|
19139
19508
|
agentName: agent.name, stop: mark.stop, last: count >= max, heldAfterSend: mark.heldAfterSend,
|
|
19509
|
+
heartbeat,
|
|
19140
19510
|
});
|
|
19141
19511
|
const before = this.storedCountOf(agent);
|
|
19142
19512
|
let id: MessageId;
|
|
@@ -19350,7 +19720,7 @@ export class AgentFramework {
|
|
|
19350
19720
|
try {
|
|
19351
19721
|
if (target) this.addMessage(
|
|
19352
19722
|
'user',
|
|
19353
|
-
[{ type: 'text', text: `[
|
|
19723
|
+
[{ type: 'text', text: `[${NOTICE_TAG.proseRouting}] ${reason}, so nothing was sent. drafts(action: "list") shows your open drafts.` }],
|
|
19354
19724
|
{ system: true, kind: 'prose-bounce' },
|
|
19355
19725
|
target,
|
|
19356
19726
|
);
|
|
@@ -19373,7 +19743,7 @@ export class AgentFramework {
|
|
|
19373
19743
|
} else if (draftState(draft) === 'unconfirmed') {
|
|
19374
19744
|
why = this.riskText(draft);
|
|
19375
19745
|
next = 'sending that draft again could post it twice. To send it anyway, use ' +
|
|
19376
|
-
`drafts(action: "resend", draftIds: ["${draft.id}"], destination:
|
|
19746
|
+
`drafts(action: "resend", draftIds: ["${draft.id}"], destination: ${quotedValue(NO_CHANNEL_EXAMPLE)}, confirmDuplicate: true)`;
|
|
19377
19747
|
} else {
|
|
19378
19748
|
return { draft };
|
|
19379
19749
|
}
|
|
@@ -19395,7 +19765,7 @@ export class AgentFramework {
|
|
|
19395
19765
|
? ` Your message is held as draft ${held.id}, with {{unsent}} written out; since it includes ${draft.id}'s words, it ` +
|
|
19396
19766
|
`can be resent only with confirmDuplicate: true${afterResult}.`
|
|
19397
19767
|
: ' Your message could not be held as a draft; its words remain in your history.';
|
|
19398
|
-
const text = `[
|
|
19768
|
+
const text = `[${NOTICE_TAG.proseRouting}] {{unsent}} is draft ${draft.id}, ${why.startsWith('which') ? why : `and ${why}`}. ` +
|
|
19399
19769
|
`Nothing was sent; ${next}.${kept}`;
|
|
19400
19770
|
const target = this.noticeTargetFor(agent.name, 'refused-{{unsent}}');
|
|
19401
19771
|
try {
|
|
@@ -19460,7 +19830,7 @@ export class AgentFramework {
|
|
|
19460
19830
|
} catch (err) {
|
|
19461
19831
|
console.error(`[drafts] ${agent.name}: outcome of ${unsent.id} not recorded:`, err);
|
|
19462
19832
|
const text =
|
|
19463
|
-
`[
|
|
19833
|
+
`[${NOTICE_TAG.proseRouting}] {{unsent}} was draft ${unsent.id}; its send came back ${outcome.status}, but recording that ` +
|
|
19464
19834
|
`failed, so the draft reads as ${this.draftStateNow(agent.name, unsent.id)} now. drafts(action: "read") shows it.`;
|
|
19465
19835
|
const target = this.noticeTargetFor(agent.name, 'unrecorded-outcome');
|
|
19466
19836
|
try {
|
|
@@ -19556,13 +19926,13 @@ export class AgentFramework {
|
|
|
19556
19926
|
// not bounced, so its words are not re-held as a fresh draft that
|
|
19557
19927
|
// invites a duplicate.
|
|
19558
19928
|
if (outcome.status === 'failed') {
|
|
19559
|
-
this.bounceProse(agent, parsed.body, `delivery to ${this.channelRegistry?.nameShown(resolved) ?? resolved.channelId} failed: ${outcome.reason
|
|
19929
|
+
this.bounceProse(agent, parsed.body, `delivery to ${this.channelRegistry?.nameShown(resolved) ?? suppliedValue(resolved.channelId)} failed: ${outcome.reason !== undefined ? suppliedValue(outcome.reason) : 'refused'}`, undefined, '>>>', round, { substituted: unsent.draft });
|
|
19560
19930
|
}
|
|
19561
19931
|
}
|
|
19562
19932
|
} catch (err) {
|
|
19563
19933
|
delete routing.pin;
|
|
19564
19934
|
routing.hybridHold = 'failed';
|
|
19565
|
-
this.bounceProse(agent, parsed.body, `sending to ${this.channelRegistry?.nameShown(resolved) ?? resolved.channelId} threw: ${err instanceof Error ? err.message : String(err)}`, undefined, '>>>', round, { substituted: unsent.draft, outcome: 'unknown' });
|
|
19935
|
+
this.bounceProse(agent, parsed.body, `sending to ${this.channelRegistry?.nameShown(resolved) ?? suppliedValue(resolved.channelId)} threw: ${suppliedValue(err instanceof Error ? err.message : String(err))}`, undefined, '>>>', round, { substituted: unsent.draft, outcome: 'unknown' });
|
|
19566
19936
|
}
|
|
19567
19937
|
return;
|
|
19568
19938
|
}
|
|
@@ -19581,6 +19951,10 @@ export class AgentFramework {
|
|
|
19581
19951
|
this.holdProseDrafts(agent, [envelope], 'no-destination', round.draftsRound ? { ...round, notice: 'later' } : round);
|
|
19582
19952
|
return;
|
|
19583
19953
|
}
|
|
19954
|
+
if (!sticky && locus === null && routing.heartbeat) {
|
|
19955
|
+
this.holdHeartbeatProse(agent, [envelope], round);
|
|
19956
|
+
return;
|
|
19957
|
+
}
|
|
19584
19958
|
const target = sticky ?? locus;
|
|
19585
19959
|
try {
|
|
19586
19960
|
const outcome = await this.channelRegistry!.routeSpeech(agent.name, envelope, target);
|
|
@@ -19613,7 +19987,7 @@ export class AgentFramework {
|
|
|
19613
19987
|
}
|
|
19614
19988
|
};
|
|
19615
19989
|
if (this.draftsSending.has(`${agent.name}\u0000${draft.id}`)) {
|
|
19616
|
-
tell(`[
|
|
19990
|
+
tell(`[${NOTICE_TAG.proseRouting}] {{unsent}} is draft ${draft.id}, which is being sent right now. It was not set aside, ` +
|
|
19617
19991
|
'since that can\'t call back words already on their way; wait for that result.', 'in-flight-dismiss');
|
|
19618
19992
|
return;
|
|
19619
19993
|
}
|
|
@@ -19628,12 +20002,12 @@ export class AgentFramework {
|
|
|
19628
20002
|
const now = this.draftStateNow(agent.name, draft.id);
|
|
19629
20003
|
if (now === 'dismissed') this.emitTrace({ type: 'prose:drafts-dismissed', agentName: agent.name, draftIds: [draft.id] });
|
|
19630
20004
|
const named = this.proseDrafts.latestBounce(agent.name)?.id === draft.id;
|
|
19631
|
-
tell(`[
|
|
20005
|
+
tell(`[${NOTICE_TAG.proseRouting}] Recording setting aside draft ${draft.id}, which {{unsent}} named, failed ` +
|
|
19632
20006
|
`(${err instanceof Error ? err.message : String(err)}): it reads as ${now} now, and {{unsent}} ` +
|
|
19633
20007
|
`${named ? 'still names it' : 'names nothing'}.${now === 'dismissed' && risk ? ` Note that ${risk}.` : ''}`, 'failed-dismiss');
|
|
19634
20008
|
return;
|
|
19635
20009
|
}
|
|
19636
|
-
if (risk) tell(`[
|
|
20010
|
+
if (risk) tell(`[${NOTICE_TAG.proseRouting}] Draft ${draft.id} is set aside. Note that ${risk}.`, 'at-risk-dismiss');
|
|
19637
20011
|
}
|
|
19638
20012
|
|
|
19639
20013
|
private async deliverProseEnvelope(
|
|
@@ -19705,12 +20079,12 @@ export class AgentFramework {
|
|
|
19705
20079
|
{ ...round, notice: 'later' },
|
|
19706
20080
|
);
|
|
19707
20081
|
if (held.length === 0 && words.text.trim().length > 0) {
|
|
19708
|
-
this.bounceProse(agent, rawText, 'no destination prefix, and this turn has no destination now', undefined, '>>', round);
|
|
20082
|
+
this.bounceProse(agent, rawText, 'no destination prefix, and this turn has no destination now', undefined, '>>', round, { unprefixed: true });
|
|
19709
20083
|
}
|
|
19710
20084
|
}
|
|
19711
20085
|
return;
|
|
19712
20086
|
}
|
|
19713
|
-
this.bounceProse(agent, rawText, 'no destination prefix, and this turn has no destination now', undefined, '>>', round);
|
|
20087
|
+
this.bounceProse(agent, rawText, 'no destination prefix, and this turn has no destination now', undefined, '>>', round, { unprefixed: true });
|
|
19714
20088
|
return;
|
|
19715
20089
|
}
|
|
19716
20090
|
targetChannel = sticky;
|
|
@@ -19742,10 +20116,10 @@ export class AgentFramework {
|
|
|
19742
20116
|
} else if (outcome.status === 'failed') {
|
|
19743
20117
|
// Unknown deliveries are marker-only: offering a fresh draft would
|
|
19744
20118
|
// invite a duplicate. Keep the authored body for the re-bounce rule.
|
|
19745
|
-
this.bounceProse(agent, envelopeBody, `delivery to ${this.channelRegistry?.nameShown(targetChannel) ?? targetChannel.channelId} failed: ${outcome.reason
|
|
20119
|
+
this.bounceProse(agent, envelopeBody, `delivery to ${this.channelRegistry?.nameShown(targetChannel) ?? suppliedValue(targetChannel.channelId)} failed: ${outcome.reason !== undefined ? suppliedValue(outcome.reason) : 'refused'}`, undefined, '>>', round, { substituted: unsent.draft });
|
|
19746
20120
|
}
|
|
19747
20121
|
} catch (err) {
|
|
19748
|
-
this.bounceProse(agent, envelopeBody, `sending to ${this.channelRegistry?.nameShown(targetChannel) ?? targetChannel.channelId} threw: ${err instanceof Error ? err.message : String(err)}`, undefined, '>>', round, { substituted: unsent.draft, outcome: 'unknown' });
|
|
20122
|
+
this.bounceProse(agent, envelopeBody, `sending to ${this.channelRegistry?.nameShown(targetChannel) ?? suppliedValue(targetChannel.channelId)} threw: ${suppliedValue(err instanceof Error ? err.message : String(err))}`, undefined, '>>', round, { substituted: unsent.draft, outcome: 'unknown' });
|
|
19749
20123
|
}
|
|
19750
20124
|
if (!delivered && this.channelRegistry!.inboundOnlyServer(targetChannel) !== undefined) {
|
|
19751
20125
|
// The channel is inbound-only by now: its server declared it after the
|
|
@@ -19837,7 +20211,7 @@ export class AgentFramework {
|
|
|
19837
20211
|
round: ProseRound,
|
|
19838
20212
|
/** After a send: the draft it substituted for `{{unsent}}`
|
|
19839
20213
|
* (unsentWords), and whether its outcome is unknown (it threw). */
|
|
19840
|
-
after: { substituted?: Draft; outcome?: 'unknown' } = {},
|
|
20214
|
+
after: { substituted?: Draft; outcome?: 'unknown'; unprefixed?: boolean } = {},
|
|
19841
20215
|
): void {
|
|
19842
20216
|
const name = agent.name;
|
|
19843
20217
|
round.routing.lastEnvelopeBounced = true;
|
|
@@ -19846,7 +20220,7 @@ export class AgentFramework {
|
|
|
19846
20220
|
// out. Like an unknown outcome, that is marker-only: holding them as a
|
|
19847
20221
|
// draft would invite a duplicate post.
|
|
19848
20222
|
const notice =
|
|
19849
|
-
`[
|
|
20223
|
+
`[${NOTICE_TAG.proseRouting}] Your text (${text.length} chars) may not have been delivered — ${suppliedValue(reason)}. ` +
|
|
19850
20224
|
"It isn't held as a draft, since a resend could post it twice; the words remain in your history.";
|
|
19851
20225
|
const target = this.noticeTargetFor(name, 'unknown-send');
|
|
19852
20226
|
try {
|
|
@@ -19875,13 +20249,21 @@ export class AgentFramework {
|
|
|
19875
20249
|
{ routing: round.routing, note: reason, notice: 'later' },
|
|
19876
20250
|
)[0];
|
|
19877
20251
|
}
|
|
19878
|
-
|
|
19879
|
-
|
|
19880
|
-
|
|
20252
|
+
// A heartbeat's turn came from no channel: its bounce is noticed, and
|
|
20253
|
+
// waits for the agent's next turn, rather than starting one of its own,
|
|
20254
|
+
// so it neither wakes the agent nor counts toward the bounce-wake cap.
|
|
20255
|
+
// Only words with no destination: a `>>` or `>>>` target that fails was
|
|
20256
|
+
// meant for someone, and its bounce wakes the agent as any does.
|
|
20257
|
+
const quiet = round.routing.heartbeat === true && after.unprefixed === true;
|
|
20258
|
+
const streak = (this.proseBounceStreaks.get(name) ?? 0) + (quiet ? 0 : 1);
|
|
20259
|
+
if (!quiet) this.proseBounceStreaks.set(name, streak);
|
|
20260
|
+
const cand = candidates?.length
|
|
20261
|
+
? ` Known channels it could mean: ${candidates.map(suppliedValue).join(', ')}.${this.channelRegistry?.publishRouteFor(candidates) ?? ''}`
|
|
20262
|
+
: '';
|
|
19881
20263
|
// The examples name the first candidate that is one token, as a target
|
|
19882
|
-
// takes it, so they show the form to write, such as a server-qualified
|
|
19883
|
-
// `a:general`; with none,
|
|
19884
|
-
const example = candidates?.find(isProseTargetToken) ??
|
|
20264
|
+
// takes it, and shows as itself, so they show the form to write, such as a server-qualified
|
|
20265
|
+
// `a:general`; with none, the generic `#<channel>`.
|
|
20266
|
+
const example = candidates?.find((c) => isProseTargetToken(c) && showsAsItself(c)) ?? NO_CHANNEL_EXAMPLE;
|
|
19885
20267
|
// A bare {{unsent}} whose send failed re-bounces the draft it sent, which
|
|
19886
20268
|
// a later bounce may have displaced as the latest; and {{unsent}} refuses
|
|
19887
20269
|
// a draft that may already have been posted. Then only the drafts tool
|
|
@@ -19892,11 +20274,11 @@ export class AgentFramework {
|
|
|
19892
20274
|
? 'It could not be held as a draft; the words remain in your history.'
|
|
19893
20275
|
: byToken
|
|
19894
20276
|
? `It is held as draft ${draft.id}; nothing is lost. To deliver it unchanged, reply with a destination plus the ` +
|
|
19895
|
-
`token {{unsent}}, e.g. "${prefix}${example} {{unsent}}" or "${prefix}
|
|
19896
|
-
`drafts(action: "resend", draftIds: ["${draft.id}"], destination:
|
|
20277
|
+
`token {{unsent}}, e.g. "${prefix}${example} {{unsent}}" or "${prefix}${NO_PERSON_EXAMPLE} {{unsent}}", or use ` +
|
|
20278
|
+
`drafts(action: "resend", draftIds: ["${draft.id}"], destination: ${quotedValue(example)}). ` +
|
|
19897
20279
|
`"${prefix}skip_reply {{unsent}}" sets it aside.`
|
|
19898
20280
|
: `It is held as draft ${draft.id}; nothing is lost. To deliver it unchanged, use ` +
|
|
19899
|
-
`drafts(action: "resend", draftIds: ["${draft.id}"], destination:
|
|
20281
|
+
`drafts(action: "resend", draftIds: ["${draft.id}"], destination: ${quotedValue(example)}` +
|
|
19900
20282
|
`${inherited || ownRisk ? ', confirmDuplicate: true' : ''}); ` +
|
|
19901
20283
|
`drafts(action: "dismiss", draftIds: ["${draft.id}"]) sets it aside.`;
|
|
19902
20284
|
const risky = inherited && draft
|
|
@@ -19912,7 +20294,7 @@ export class AgentFramework {
|
|
|
19912
20294
|
// wasn't delivered.
|
|
19913
20295
|
const delivered = after.outcome === 'unknown' ? 'may not have been delivered' : 'was not delivered';
|
|
19914
20296
|
const notice =
|
|
19915
|
-
`[
|
|
20297
|
+
`[${NOTICE_TAG.proseRouting}] Your text (${text.length} chars) ${delivered} — ${suppliedValue(reason)}.${cand} ${kept}${risky}` +
|
|
19916
20298
|
(prefix === '>>' && this.canAgentUseTool(agent.name, 'prose_help') ? ' The prose_help tool shows the full syntax.' : '');
|
|
19917
20299
|
const target = this.noticeTargetFor(name, 'prose-bounce');
|
|
19918
20300
|
if (target) {
|
|
@@ -19939,9 +20321,9 @@ export class AgentFramework {
|
|
|
19939
20321
|
const capped = streak > AgentFramework.PROSE_BOUNCE_WAKE_CAP;
|
|
19940
20322
|
console.error(
|
|
19941
20323
|
`[prose] ${name}: BOUNCED ${text.length} chars — ${reason}` +
|
|
19942
|
-
(capped ? ' (bounce-wake cap reached; notice appended without wake)' : ''),
|
|
20324
|
+
(quiet ? ' (a heartbeat turn: notice appended without wake)' : capped ? ' (bounce-wake cap reached; notice appended without wake)' : ''),
|
|
19943
20325
|
);
|
|
19944
|
-
if (!capped && target) {
|
|
20326
|
+
if (!capped && !quiet && target) {
|
|
19945
20327
|
// Wake so the resend can happen immediately — an unsent DM reply must
|
|
19946
20328
|
// not sit invisible until the next unrelated event.
|
|
19947
20329
|
this.pendingRequests.push({
|
|
@@ -19949,6 +20331,7 @@ export class AgentFramework {
|
|
|
19949
20331
|
reason: 'prose-bounce',
|
|
19950
20332
|
source: 'framework',
|
|
19951
20333
|
timestamp: Date.now(),
|
|
20334
|
+
...this.heartbeatTurnMark(round.routing),
|
|
19952
20335
|
});
|
|
19953
20336
|
}
|
|
19954
20337
|
}
|
|
@@ -20159,6 +20542,7 @@ export class AgentFramework {
|
|
|
20159
20542
|
stopReason: 'exhausted', speech: '', error: error instanceof Error ? error.message : String(error),
|
|
20160
20543
|
}, this.openLogical.get(agent.name));
|
|
20161
20544
|
}
|
|
20545
|
+
this.endStatedWaitRun(agent.name);
|
|
20162
20546
|
// The agent's window at the event gate ends here too, while this start
|
|
20163
20547
|
// still holds the turn token, as at the provider-wait hold above. A
|
|
20164
20548
|
// start that resumes a turn (a budget restart, a tool-result-guard
|
|
@@ -20661,13 +21045,16 @@ export class AgentFramework {
|
|
|
20661
21045
|
// without the mark, so a retry doesn't write it again. A row that can't
|
|
20662
21046
|
// be stored gives back the run's count, and withdraws a turn the
|
|
20663
21047
|
// continuation alone caused: it would wake to nothing it can read.
|
|
21048
|
+
// A framework wake that continues a heartbeat's turn (heartbeatTurn) is
|
|
21049
|
+
// one too, and isn't told again what the turn that started the run was.
|
|
21050
|
+
const continuesHeartbeat = trigger?.heartbeatTurn === true && trigger.source === 'framework';
|
|
20664
21051
|
if (attempt === 0 && !resumesTurn && trigger?.autoContinueRun) {
|
|
20665
21052
|
const mark = trigger.autoContinueRun;
|
|
20666
21053
|
const { autoContinueRun: _mark, ...unmarked } = trigger;
|
|
20667
21054
|
trigger = unmarked;
|
|
20668
21055
|
// Another reader's turn may have started since the scheduler checked.
|
|
20669
21056
|
const busy = this.sharedSlotBusy(agent);
|
|
20670
|
-
if (busy || !this.writeAutoContinueNotice(agent, mark)) {
|
|
21057
|
+
if (busy || !this.writeAutoContinueNotice(agent, mark, trigger.heartbeatTurn === true)) {
|
|
20671
21058
|
this.giveBackAutoContinue(agent.name, busy ? "another resident's turn is alive in the shared slot" : 'its row could not be stored');
|
|
20672
21059
|
if (mark.sole) {
|
|
20673
21060
|
console.error(`[continue] ${agent.name}: turn not started (the continuation was its only cause)`);
|
|
@@ -20799,9 +21186,10 @@ export class AgentFramework {
|
|
|
20799
21186
|
|
|
20800
21187
|
if (opensTurn) {
|
|
20801
21188
|
// The turn's trigger channel (item-3 redux): the channel of a
|
|
20802
|
-
// channel/DM-triggered turn, with its server, and none for a
|
|
20803
|
-
//
|
|
20804
|
-
//
|
|
21189
|
+
// channel/DM-triggered turn, with its server, and none for a
|
|
21190
|
+
// no-trigger turn, which doesn't inherit a previous turn's channel: a
|
|
21191
|
+
// heartbeat's takes no locus at all (TurnRouting.heartbeat), and any
|
|
21192
|
+
// other falls back to the global default. A wake whose channel,
|
|
20805
21193
|
// given without its server, names no one registered channel (a host's
|
|
20806
21194
|
// id that two servers' channels answer to, or that none does) has none
|
|
20807
21195
|
// either, and takes no fallback: it was for a channel, and the latest
|
|
@@ -20816,6 +21204,10 @@ export class AgentFramework {
|
|
|
20816
21204
|
const unresolvedTrigger = !!trigger?.channelId && routing.triggerChannel === undefined;
|
|
20817
21205
|
if (unresolvedTrigger) routing.noChannel = true;
|
|
20818
21206
|
else delete routing.noChannel;
|
|
21207
|
+
// A heartbeat's turn has no channel, and takes no fallback: its plain
|
|
21208
|
+
// speech is held as a draft (TurnRouting.heartbeat).
|
|
21209
|
+
if (isHeartbeatWake(trigger)) routing.heartbeat = true;
|
|
21210
|
+
else delete routing.heartbeat;
|
|
20819
21211
|
// Resolve the turn's outbound locus now: the home channel for forks,
|
|
20820
21212
|
// else the trigger channel, else the global default, which an
|
|
20821
21213
|
// ephemeral run never takes (takesGlobalFallback). Ambient mid-turn
|
|
@@ -20823,8 +21215,8 @@ export class AgentFramework {
|
|
|
20823
21215
|
// chatter and reactions hijacked it before, 2026-07-21); only an
|
|
20824
21216
|
// addressed message, a follow-up in a channel the turn engaged
|
|
20825
21217
|
// (repinTurnLocus) or the agent's own channel_open moves it, between
|
|
20826
|
-
// rounds. The eager snapshot protects
|
|
20827
|
-
// defaultPublishChannel. Explicit mode uses a turn-scoped `>>` target
|
|
21218
|
+
// rounds. The eager snapshot protects a no-trigger turn that takes the
|
|
21219
|
+
// fallback (a timer's) from a moving defaultPublishChannel. Explicit mode uses a turn-scoped `>>` target
|
|
20828
21220
|
// and disabled mode has no prose target, so neither resolves or
|
|
20829
21221
|
// announces a locus. If the effective locus differs from the last one
|
|
20830
21222
|
// announced, a durable `[routing]` notice goes into the window BEFORE
|
|
@@ -20838,7 +21230,15 @@ export class AgentFramework {
|
|
|
20838
21230
|
routing.locus = locus;
|
|
20839
21231
|
noteTurnChannel(routing, locus);
|
|
20840
21232
|
}
|
|
20841
|
-
|
|
21233
|
+
// A heartbeat's turn speaks nowhere by design. It's told so up front,
|
|
21234
|
+
// once per heartbeat (a continuation of its run isn't told again),
|
|
21235
|
+
// without moving the announced baseline, so the next channel turn
|
|
21236
|
+
// doesn't announce its channel again.
|
|
21237
|
+
if (attempt === 0 && routing.heartbeat) {
|
|
21238
|
+
if (!continuesHeartbeat) this.noticeHeartbeatTurn(agent.name);
|
|
21239
|
+
} else if (attempt === 0) {
|
|
21240
|
+
this.announceLocusIfChanged(agent.name, locus);
|
|
21241
|
+
}
|
|
20842
21242
|
}
|
|
20843
21243
|
} else {
|
|
20844
21244
|
// Every other start of the turn keeps its locus: a resume, or a start
|
|
@@ -20905,7 +21305,9 @@ export class AgentFramework {
|
|
|
20905
21305
|
const presentation = this.inspectToolPresentation(agent.name, requestSnapshot);
|
|
20906
21306
|
const compressionTools = presentation?.available
|
|
20907
21307
|
?? structuredClone(this.availableToolsForPresentation(agent.name, requestSnapshot));
|
|
20908
|
-
|
|
21308
|
+
// The tool list: what the agent's presentation advertises, less the
|
|
21309
|
+
// host's own toolsets (the built-in one, servers it has as tabs).
|
|
21310
|
+
const tools = this.headerTools(agent.name, presentation?.advertised ?? compressionTools);
|
|
20909
21311
|
|
|
20910
21312
|
// Gather context from modules (pull-based) and MCPL hooks (push-based)
|
|
20911
21313
|
// Both produce ContextInjection[] that get merged before inference.
|
|
@@ -21202,7 +21604,8 @@ export class AgentFramework {
|
|
|
21202
21604
|
return false;
|
|
21203
21605
|
}
|
|
21204
21606
|
|
|
21205
|
-
const action: ErrorAction = statedWait === 'fail'
|
|
21607
|
+
const action: ErrorAction = statedWait === 'fail' || (isRearmed(trigger) && !gotPartway(routing))
|
|
21608
|
+
? { retry: false } : this.errorPolicy.onInferenceError(err, agent.name, attempt);
|
|
21206
21609
|
if (action.retry) {
|
|
21207
21610
|
// A policy's delay can be a provider's retry-after: wait all of it.
|
|
21208
21611
|
await this.waitForRetry(action.delayMs, checkWindow, agent);
|
|
@@ -21262,6 +21665,7 @@ export class AgentFramework {
|
|
|
21262
21665
|
speech: '',
|
|
21263
21666
|
...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: err.message }),
|
|
21264
21667
|
}, routing);
|
|
21668
|
+
if (isRearmed(trigger) && isTransientFailure(err)) this.exhaustingRearms.add(agent.name);
|
|
21265
21669
|
this.emitTrace({
|
|
21266
21670
|
type: 'inference:exhausted',
|
|
21267
21671
|
agentName: agent.name,
|
|
@@ -21281,6 +21685,7 @@ export class AgentFramework {
|
|
|
21281
21685
|
if (action.emit) {
|
|
21282
21686
|
this.pushEvent(action.emit);
|
|
21283
21687
|
}
|
|
21688
|
+
this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, routing);
|
|
21284
21689
|
}
|
|
21285
21690
|
// No handoff on either failure branch: the terminal branch deleted the
|
|
21286
21691
|
// token above; the retry branch's recursive startAgentStream replaced
|
|
@@ -21572,9 +21977,9 @@ export class AgentFramework {
|
|
|
21572
21977
|
}
|
|
21573
21978
|
};
|
|
21574
21979
|
};
|
|
21575
|
-
const enqueueSpeech = (text: string, locus: ChannelRef | null, onPublished: SpeechPublished): void => {
|
|
21980
|
+
const enqueueSpeech = (text: string, locus: ChannelRef | null, onPublished: SpeechPublished, round?: ProseRound): void => {
|
|
21576
21981
|
chainSpeech(routingSpeech(async () => {
|
|
21577
|
-
const outcome = await this.
|
|
21982
|
+
const outcome = await this.routeLocusSpeech(agent, round ?? { routing: streamRouting, notice: 'later' }, text, locus);
|
|
21578
21983
|
this.recordProseDelivery(streamRouting, outcome);
|
|
21579
21984
|
if (outcome?.delivered) {
|
|
21580
21985
|
streamRouting.liveSpeech = true;
|
|
@@ -22117,11 +22522,11 @@ export class AgentFramework {
|
|
|
22117
22522
|
`[routing] ${agent.name}: mid-turn round [${toolNames.join(', ')}] -> prose NOT routed (same_round_think_text_policy=private)`,
|
|
22118
22523
|
);
|
|
22119
22524
|
} else {
|
|
22120
|
-
console.error(
|
|
22121
|
-
`[routing] ${agent.name}: mid-turn round [${toolNames.join(', ')}] ->
|
|
22122
|
-
|
|
22525
|
+
console.error(roundLocus === null && streamRouting.heartbeat
|
|
22526
|
+
? `[routing] ${agent.name}: mid-turn round [${toolNames.join(', ')}] -> ${roundSegments.length} prose segment(s) held as drafts (a heartbeat turn)`
|
|
22527
|
+
: `[routing] ${agent.name}: mid-turn round [${toolNames.join(', ')}] -> routing ${roundSegments.length} prose segment(s) live -> ${roundLocus ?? '(default)'}`);
|
|
22123
22528
|
for (const seg of roundSegments) {
|
|
22124
|
-
enqueueSpeech(seg, roundLocus, roundPublished);
|
|
22529
|
+
enqueueSpeech(seg, roundLocus, roundPublished, roundOpts);
|
|
22125
22530
|
}
|
|
22126
22531
|
}
|
|
22127
22532
|
};
|
|
@@ -22441,7 +22846,7 @@ export class AgentFramework {
|
|
|
22441
22846
|
// self-seed. Exact raw output remains in the provider ledger.
|
|
22442
22847
|
agent.getContextManager().addMessage('user', [{
|
|
22443
22848
|
type: 'text',
|
|
22444
|
-
text:
|
|
22849
|
+
text: `[${NOTICE_TAG.toolBoundary}] No tool was called.`,
|
|
22445
22850
|
}], {
|
|
22446
22851
|
system: true,
|
|
22447
22852
|
kind: 'tool-wrapper-prose-contained',
|
|
@@ -22476,7 +22881,7 @@ export class AgentFramework {
|
|
|
22476
22881
|
} else if (cut?.unnamed) {
|
|
22477
22882
|
agent.getContextManager().addMessage('user', [{
|
|
22478
22883
|
type: 'text',
|
|
22479
|
-
text:
|
|
22884
|
+
text: `[${NOTICE_TAG.toolBoundary}] Your response was cut off at the output limit inside a tool-call block, before any call in it ` +
|
|
22480
22885
|
'was named, so nothing ran. To make a call, send less in one response.',
|
|
22481
22886
|
}], { system: true, kind: 'cut-off-tool-block', ...this.silentTurnRowMetadata(agent.name) });
|
|
22482
22887
|
}
|
|
@@ -22771,7 +23176,7 @@ export class AgentFramework {
|
|
|
22771
23176
|
);
|
|
22772
23177
|
for (const segment of speechSegments) {
|
|
22773
23178
|
try {
|
|
22774
|
-
const outcome = await this.
|
|
23179
|
+
const outcome = await this.routeLocusSpeech(agent, textOnlyHold, segment, locus);
|
|
22775
23180
|
this.recordProseDelivery(streamRouting, outcome);
|
|
22776
23181
|
if (outcome?.delivered) endPublished(outcome, segment, outcome.messageId);
|
|
22777
23182
|
} catch (err) {
|
|
@@ -22890,7 +23295,7 @@ export class AgentFramework {
|
|
|
22890
23295
|
// Deliver sequentially (await each) so the segments land in order.
|
|
22891
23296
|
for (const seg of segments) {
|
|
22892
23297
|
try {
|
|
22893
|
-
const outcome = await this.
|
|
23298
|
+
const outcome = await this.routeLocusSpeech(agent, trailingHold, seg, locus);
|
|
22894
23299
|
this.recordProseDelivery(streamRouting, outcome);
|
|
22895
23300
|
if (outcome?.delivered) endPublished(outcome, seg, outcome.messageId);
|
|
22896
23301
|
} catch (err) {
|
|
@@ -23043,7 +23448,8 @@ export class AgentFramework {
|
|
|
23043
23448
|
break;
|
|
23044
23449
|
}
|
|
23045
23450
|
|
|
23046
|
-
const action: ErrorAction = statedWait === 'fail'
|
|
23451
|
+
const action: ErrorAction = statedWait === 'fail' || (isRearmed(trigger) && !gotPartway(streamRouting))
|
|
23452
|
+
? { retry: false } : this.errorPolicy.onInferenceError(err, agent.name, attempt);
|
|
23047
23453
|
if (action.retry) {
|
|
23048
23454
|
// A stop made during the wait reaches no stream: the check after
|
|
23049
23455
|
// the wait ends the turn, as the guard's check after its wait
|
|
@@ -23100,6 +23506,7 @@ export class AgentFramework {
|
|
|
23100
23506
|
speech: '',
|
|
23101
23507
|
...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: err.message }),
|
|
23102
23508
|
}, this.agents.get(agent.name) === agent && agent.streamId === myStreamId ? streamRouting : undefined, streamRouting);
|
|
23509
|
+
if (isRearmed(trigger) && isTransientFailure(err)) this.exhaustingRearms.add(agent.name);
|
|
23103
23510
|
this.emitTrace({
|
|
23104
23511
|
type: 'inference:exhausted',
|
|
23105
23512
|
agentName: agent.name,
|
|
@@ -23117,6 +23524,7 @@ export class AgentFramework {
|
|
|
23117
23524
|
if (action.emit) {
|
|
23118
23525
|
this.pushEvent(action.emit);
|
|
23119
23526
|
}
|
|
23527
|
+
this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, streamRouting);
|
|
23120
23528
|
}
|
|
23121
23529
|
break;
|
|
23122
23530
|
}
|
|
@@ -23191,6 +23599,7 @@ export class AgentFramework {
|
|
|
23191
23599
|
agentName: agent.name,
|
|
23192
23600
|
error: terminal,
|
|
23193
23601
|
});
|
|
23602
|
+
this.rearmExhaustedTurn(agent, new Error(terminal), trigger, ownsProviderGate, streamRouting);
|
|
23194
23603
|
// Postmortem 2026-05-28 P2 #7: persist the abort to the
|
|
23195
23604
|
// inference log so future investigations can attribute the
|
|
23196
23605
|
// terminal cause without relying on live in-memory reducer
|
|
@@ -23481,12 +23890,14 @@ export class AgentFramework {
|
|
|
23481
23890
|
speech: '',
|
|
23482
23891
|
...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: err.message }),
|
|
23483
23892
|
}, streamRouting);
|
|
23893
|
+
if (isRearmed(trigger) && isTransientFailure(err)) this.exhaustingRearms.add(agent.name);
|
|
23484
23894
|
this.emitTrace({
|
|
23485
23895
|
type: 'inference:exhausted',
|
|
23486
23896
|
agentName: agent.name,
|
|
23487
23897
|
error: err.message,
|
|
23488
23898
|
...this.classifyInferenceError(err),
|
|
23489
23899
|
});
|
|
23900
|
+
this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, streamRouting);
|
|
23490
23901
|
// Postmortem 2026-05-28 P2 #7: catch-path failures (stream itself
|
|
23491
23902
|
// threw) were previously only visible via in-memory trace listeners.
|
|
23492
23903
|
// Persist to inference log for forensic attribution.
|
|
@@ -23710,8 +24121,7 @@ export class AgentFramework {
|
|
|
23710
24121
|
* exists only through the private closure handed to ModuleRegistry
|
|
23711
24122
|
* (ctx.callTool → executeToolCallFrom with {kind:'module'}), which is exempt
|
|
23712
24123
|
* from agent allowlists. Registered agents' allowedTools is enforced at
|
|
23713
|
-
* dispatch, including namespaced utility names
|
|
23714
|
-
* list/describe/run slot. Genuinely unidentified public callers (including
|
|
24124
|
+
* dispatch, including namespaced utility names. Genuinely unidentified public callers (including
|
|
23715
24125
|
* unknown legacy names) remain unrestricted. Framework-created identities
|
|
23716
24126
|
* are refused while unregistered, even if their configured list allows it.
|
|
23717
24127
|
*/
|
|
@@ -23930,6 +24340,8 @@ export class AgentFramework {
|
|
|
23930
24340
|
this.emitTrace({ type: 'tool:failed', module: 'framework', tool: call.name, callId: call.id, error: traced });
|
|
23931
24341
|
return failure;
|
|
23932
24342
|
};
|
|
24343
|
+
// utils was removed: said whatever the allowlist names (dispatchToolCall).
|
|
24344
|
+
if (call.name === 'utils') return failed({ success: false, isError: true, error: this.utilsRemoved(origin.kind === 'agent' ? origin.agentName : call.callerAgentName) });
|
|
23933
24345
|
if (origin.kind === 'agent') {
|
|
23934
24346
|
const failure = this.agentToolPermissionFailure(origin.agentName, call, false);
|
|
23935
24347
|
if (failure) return failed(failure);
|
|
@@ -23949,6 +24361,14 @@ export class AgentFramework {
|
|
|
23949
24361
|
return this.runCodeExecution(call.callerAgentName ?? '__ephemeral__', call);
|
|
23950
24362
|
}
|
|
23951
24363
|
|
|
24364
|
+
// toolsets--act on a host toolset's tool (the built-in one, a server's
|
|
24365
|
+
// tab) goes where a programmatic call of that tool goes, under its name.
|
|
24366
|
+
if (call.name === META_TOOL_NAMES.act && call.callerAgentName !== undefined) {
|
|
24367
|
+
const host = this.hostPresentation?.resolveAct(call.callerAgentName, call.input) ?? null;
|
|
24368
|
+
if (host?.kind === 'refused') return failed({ success: false, error: host.error, isError: true });
|
|
24369
|
+
if (host?.kind === 'call') return this.executeToolCallFrom({ ...call, name: host.name, input: host.args }, origin);
|
|
24370
|
+
}
|
|
24371
|
+
|
|
23952
24372
|
// RFC-014: toolsets--act, resolved to the dynamic tool it calls, goes
|
|
23953
24373
|
// where a programmatic call of that tool goes, with its instance.
|
|
23954
24374
|
if (call.name === META_TOOL_NAMES.act && this.toolsetHub && this.mcplServerRegistry) {
|
|
@@ -23993,22 +24413,13 @@ export class AgentFramework {
|
|
|
23993
24413
|
return this.draftsToolResult(origin.agentName, call.input);
|
|
23994
24414
|
}
|
|
23995
24415
|
|
|
23996
|
-
// The utils meta-tool, for callers that await a tool's result. A call
|
|
23997
|
-
// through executeToolCall (the puppet, a host) reaches what its agent's
|
|
23998
|
-
// allowedTools permits, as the model's does, and an unidentified caller
|
|
23999
|
-
// is unrestricted, as executeToolCall's doc says; a module, through
|
|
24000
|
-
// callTool, is trusted and reaches every utility.
|
|
24001
24416
|
// The heartbeat's tools, for callers that await a tool's result (an
|
|
24002
|
-
// in-process call, a puppet, a module's callTool). A module is trusted
|
|
24003
|
-
//
|
|
24417
|
+
// in-process call, a puppet, a module's callTool). A module is trusted;
|
|
24418
|
+
// an agent is held to what dispatch holds it to.
|
|
24004
24419
|
if (this.heartbeat?.ownsTool(call.name)) {
|
|
24005
24420
|
const refusal = origin.kind === 'module' ? null : this.heartbeatRefusal(call.callerAgentName ?? '__ephemeral__', call);
|
|
24006
24421
|
return refusal ? { success: false, error: refusal, isError: true } : this.runHeartbeatTool(call);
|
|
24007
24422
|
}
|
|
24008
|
-
if (call.name === 'utils') {
|
|
24009
|
-
return this.handleUtilsToolCall(call.callerAgentName ?? '__ephemeral__', call, { inProcess: true, trustedModule: origin.kind === 'module' });
|
|
24010
|
-
}
|
|
24011
|
-
|
|
24012
24423
|
// Module tools. The caller uses the result in process, so it gets the
|
|
24013
24424
|
// module's own result, as the module returned it, less a refusal it
|
|
24014
24425
|
// claims, which is the dispatcher's to say.
|
|
@@ -24052,6 +24463,23 @@ export class AgentFramework {
|
|
|
24052
24463
|
return { ok: true, call: inner, serverId: target.serverId, prefix, ...(target.instance !== undefined ? { instance: target.instance } : {}) };
|
|
24053
24464
|
}
|
|
24054
24465
|
|
|
24466
|
+
/** A toolsets--act call a host toolset resolved (HostPresentation.resolveAct):
|
|
24467
|
+
* its refusal answered, or the tool's own call dispatched in its place,
|
|
24468
|
+
* with the act's id, so its result is the act's. */
|
|
24469
|
+
private dispatchHostAct(agentName: string, call: ToolCall, host: HostActTarget, turnId: number | undefined): void {
|
|
24470
|
+
if (host.kind === 'refused') {
|
|
24471
|
+
this.emitTrace({ type: 'tool:failed', module: TOOLSETS_MODULE, tool: call.name, callId: call.id, error: host.error });
|
|
24472
|
+
this.pushEvent({
|
|
24473
|
+
type: 'tool-result', callId: call.id, toolName: call.name, agentName, moduleName: TOOLSETS_MODULE,
|
|
24474
|
+
result: this.refusedResult(agentName, call.id, host.error),
|
|
24475
|
+
});
|
|
24476
|
+
return;
|
|
24477
|
+
}
|
|
24478
|
+
const inner: ToolCall = { ...call, name: host.name, input: host.args };
|
|
24479
|
+
this.toolLifecycleEmitter?.retarget(agentName, call.id, inner);
|
|
24480
|
+
this.dispatchToolCall(agentName, inner, turnId);
|
|
24481
|
+
}
|
|
24482
|
+
|
|
24055
24483
|
/**
|
|
24056
24484
|
* RFC-014 §8.2: a toolsets--act call from the agent (the model's, or a
|
|
24057
24485
|
* script's) goes down the path a direct call of the server's tool takes
|
|
@@ -24887,17 +25315,17 @@ export class AgentFramework {
|
|
|
24887
25315
|
readCap: number, written: SpillWrite): string {
|
|
24888
25316
|
if (written && written.error === undefined) {
|
|
24889
25317
|
const canRaise = bound === undefined || cutStoredToolResultToFit(content, bound, shown, bound).head.length > headLength;
|
|
24890
|
-
return `\n\n[truncated — showing ${headLength} of ${content.length} chars; full content: workspace file ${written.path}`
|
|
25318
|
+
return `\n\n[${NOTICE_TAG.truncated} — showing ${headLength} of ${content.length} chars; full content: workspace file ${written.path}`
|
|
24891
25319
|
+ `. ${this.spillPageAdvice(written.path, readCap, canRaise)}]`;
|
|
24892
25320
|
}
|
|
24893
25321
|
if (written) {
|
|
24894
25322
|
// A workspace was there and the write FAILED — say that, loudly and
|
|
24895
25323
|
// distinctly. Telling the agent "no writable workspace" would teach
|
|
24896
25324
|
// them their residence lacks a capability it actually has.
|
|
24897
|
-
return `\n\n[truncated — showing ${headLength} of ${content.length} chars; spill to workspace file ${written.path} FAILED`
|
|
25325
|
+
return `\n\n[${NOTICE_TAG.truncated} — showing ${headLength} of ${content.length} chars; spill to workspace file ${written.path} FAILED`
|
|
24898
25326
|
+ ` (${written.error}); content over the cap was not retained]`;
|
|
24899
25327
|
}
|
|
24900
|
-
return
|
|
25328
|
+
return `\n\n[${NOTICE_TAG.truncated} — original was ${content.length} chars; ${this.noFileMountText()}, full content not retained]`;
|
|
24901
25329
|
}
|
|
24902
25330
|
|
|
24903
25331
|
/**
|
|
@@ -25800,6 +26228,17 @@ export class AgentFramework {
|
|
|
25800
26228
|
* script left running, which no call watches, has none.
|
|
25801
26229
|
*/
|
|
25802
26230
|
private dispatchToolCall(agentName: string, call: ToolCall, turnId?: number): void {
|
|
26231
|
+
// utils was removed; its utilities are in the built-in toolset. Said
|
|
26232
|
+
// whatever the allowlist names, so an old call learns where they went.
|
|
26233
|
+
if (call.name === 'utils') {
|
|
26234
|
+
const removed = this.utilsRemoved(agentName);
|
|
26235
|
+
this.emitTrace({ type: 'tool:failed', module: 'framework', tool: call.name, callId: call.id, error: removed });
|
|
26236
|
+
this.pushEvent({
|
|
26237
|
+
type: 'tool-result', callId: call.id, toolName: call.name, agentName, moduleName: 'framework',
|
|
26238
|
+
result: this.refusedResult(agentName, call.id, removed),
|
|
26239
|
+
});
|
|
26240
|
+
return;
|
|
26241
|
+
}
|
|
25803
26242
|
const refusal = this.agentToolPermissionFailure(agentName, call) ?? this.markedInputFailure(agentName, call);
|
|
25804
26243
|
if (refusal) {
|
|
25805
26244
|
this.pushEvent({
|
|
@@ -25821,6 +26260,17 @@ export class AgentFramework {
|
|
|
25821
26260
|
}
|
|
25822
26261
|
|
|
25823
26262
|
|
|
26263
|
+
// toolsets--act on a host toolset's tool (the built-in one, a server's
|
|
26264
|
+
// tab): the call of that tool, under its own name, down the path a
|
|
26265
|
+
// direct call of it takes, permissions included.
|
|
26266
|
+
if (enrichedCall.name === META_TOOL_NAMES.act) {
|
|
26267
|
+
const host = this.hostPresentation?.resolveAct(agentName, enrichedCall.input) ?? null;
|
|
26268
|
+
if (host) {
|
|
26269
|
+
this.dispatchHostAct(agentName, call, host, turnId);
|
|
26270
|
+
return;
|
|
26271
|
+
}
|
|
26272
|
+
}
|
|
26273
|
+
|
|
25824
26274
|
// RFC-014: toolsets--act calls a server's dynamic tool, down the same
|
|
25825
26275
|
// path as a direct call of it (dispatchToolsetAct).
|
|
25826
26276
|
if (enrichedCall.name === META_TOOL_NAMES.act && this.toolsetHub && this.mcplServerRegistry) {
|
|
@@ -25847,12 +26297,6 @@ export class AgentFramework {
|
|
|
25847
26297
|
return;
|
|
25848
26298
|
}
|
|
25849
26299
|
|
|
25850
|
-
// Route the agent's typed, allowlisted hot-settings surface.
|
|
25851
|
-
if (enrichedCall.name === 'utils') {
|
|
25852
|
-
this.dispatchUtilsToolCall(agentName, enrichedCall);
|
|
25853
|
-
return;
|
|
25854
|
-
}
|
|
25855
|
-
|
|
25856
26300
|
if (enrichedCall.name === 'agent_settings') {
|
|
25857
26301
|
this.dispatchAgentSettingsToolCall(agentName, enrichedCall);
|
|
25858
26302
|
return;
|
|
@@ -25892,7 +26336,7 @@ export class AgentFramework {
|
|
|
25892
26336
|
void this.tuneOutCoordinator
|
|
25893
26337
|
.handleSubconsciousTool(enrichedCall.name, enrichedCall.input as Record<string, unknown>, this.callOwnChannels(agentName, enrichedCall))
|
|
25894
26338
|
// A rejection is answered with a failed result, as a framework tool's
|
|
25895
|
-
// dispatch failure is (
|
|
26339
|
+
// dispatch failure is (dispatchAgentSettingsToolCall), never left unhandled.
|
|
25896
26340
|
.catch((err): ToolResult => ({ success: false, error: thrownText(err), isError: true }))
|
|
25897
26341
|
.then((result) => {
|
|
25898
26342
|
// Through pushEvent, like every other tool's result, so one that
|
|
@@ -25964,10 +26408,18 @@ export class AgentFramework {
|
|
|
25964
26408
|
}
|
|
25965
26409
|
const wake = readSkipReplyWake(enrichedCall.input);
|
|
25966
26410
|
if (wake.kind === 'wake' && this.eventGate) {
|
|
26411
|
+
const armedAt = Date.now();
|
|
25967
26412
|
const { inMs, armed } = this.eventGate.armSelfWake(agentName, wake.seconds, 'skip_reply');
|
|
26413
|
+
// The heartbeat's tick or reminder due before the timer: if it wakes
|
|
26414
|
+
// the agent, its turn cancels the timer, so the confirmation names
|
|
26415
|
+
// it and the time the self-wake would have left.
|
|
26416
|
+
const earlierWake = armed ? this.heartbeatWakeBefore(agentName, armedAt + inMs) : undefined;
|
|
25968
26417
|
console.error(armed
|
|
25969
26418
|
? `[self-wake] agent=${agentName} armed in ${Math.round(inMs / 1000)}s (skip_reply)`
|
|
26419
|
+
+ (earlierWake ? `; due sooner, in ~${earlierWake.inSeconds}s: ${logDetail(earlierWake.what)}` : '')
|
|
25970
26420
|
: `[self-wake] agent=${agentName} not armed: the framework has stopped (skip_reply)`);
|
|
26421
|
+
this.dispatchChannelToolCall(agentName, enrichedCall, earlierWake);
|
|
26422
|
+
return;
|
|
25971
26423
|
}
|
|
25972
26424
|
}
|
|
25973
26425
|
this.dispatchChannelToolCall(agentName, enrichedCall);
|
|
@@ -26184,7 +26636,7 @@ export class AgentFramework {
|
|
|
26184
26636
|
// messages flush at the next turn's START (before its compile — riding
|
|
26185
26637
|
// their own wake), at the agent's next tool boundary (where they are
|
|
26186
26638
|
// ALSO injected into the live stream — hear-while-acting — all but a
|
|
26187
|
-
// message
|
|
26639
|
+
// message the agent's own view leaves out), or in driveStream's
|
|
26188
26640
|
// finally when the turn ends.
|
|
26189
26641
|
// Quiesce (issue #122): the window exists to run compression/refold
|
|
26190
26642
|
// against a FROZEN context — module events and api message.send must not
|
|
@@ -26372,18 +26824,20 @@ export class AgentFramework {
|
|
|
26372
26824
|
}
|
|
26373
26825
|
|
|
26374
26826
|
/** Whether `entry`'s own stored entry is the newest in `agent`'s window,
|
|
26375
|
-
* clock rows
|
|
26376
|
-
* message by its server, channel and
|
|
26377
|
-
* server and event id, and never one
|
|
26827
|
+
* clock rows, stored context injections and rows the agent's own view
|
|
26828
|
+
* leaves out after it aside: a channel message by its server, channel and
|
|
26829
|
+
* message id, a push event by its server and event id, and never one the
|
|
26830
|
+
* agent's view leaves out, such as one a tune-out holds. Past
|
|
26378
26831
|
* NEWEST_SCAN_ROWS such rows, it answers no. */
|
|
26379
26832
|
private isNewestIn(agent: Agent, entry: GateEntryRef): boolean {
|
|
26380
26833
|
// A clock row the framework wrote after it (a turn start's, an event's),
|
|
26381
26834
|
// or a stored context injection (any agent's, a start's), isn't a
|
|
26382
26835
|
// message a reply could be meant for, so it doesn't count.
|
|
26383
26836
|
const newest = agent.getContextManager().queryMessages({ limit: AgentFramework.NEWEST_SCAN_ROWS, reverse: true }).messages
|
|
26384
|
-
.find((m) => (m.metadata as { kind?: unknown } | undefined)?.kind !== CLOCK_ROW_KIND && !storedInjectionMarkOf(m.metadata)
|
|
26837
|
+
.find((m) => (m.metadata as { kind?: unknown } | undefined)?.kind !== CLOCK_ROW_KIND && !storedInjectionMarkOf(m.metadata)
|
|
26838
|
+
&& this.agentSees(agent, m.metadata));
|
|
26385
26839
|
const md = newest?.metadata as Record<string, unknown> | undefined;
|
|
26386
|
-
if (!md ||
|
|
26840
|
+
if (!md || entry.serverId === undefined || md.serverId !== entry.serverId) return false;
|
|
26387
26841
|
if (entry.eventType === 'mcpl:channel-incoming') {
|
|
26388
26842
|
return entry.messageId !== undefined && md.messageId === entry.messageId && md.channelId === entry.channelId;
|
|
26389
26843
|
}
|
|
@@ -26403,11 +26857,18 @@ export class AgentFramework {
|
|
|
26403
26857
|
: this.agents.values().next().value;
|
|
26404
26858
|
}
|
|
26405
26859
|
|
|
26860
|
+
private noteSpokenTo(agentName: string, metadata: MessageMetadata | undefined): void {
|
|
26861
|
+
const agent = this.agents.get(agentName);
|
|
26862
|
+
if ((agent && !this.agentSees(agent, metadata)) || isOwnScheduleWrite(metadata) || !isConversationalInjection(metadata)) return;
|
|
26863
|
+
if (this.turnRearms?.delete(agentName)) console.error(`[inference-rearm] agent=${agentName} re-send count started again: a message was said to it`);
|
|
26864
|
+
}
|
|
26865
|
+
|
|
26406
26866
|
/** The notes addMessage takes for a message it stored: an arrival for the
|
|
26407
26867
|
* agent when none of its turns is alive or the message was assembled for
|
|
26408
26868
|
* the start now beginning (no tool boundary will weigh it), and one for
|
|
26409
26869
|
* every other resident's open turn. */
|
|
26410
26870
|
private noteStored(agent: Agent, metadata: MessageMetadata | undefined, stored: MessageId, bypassDeferralFor?: string): void {
|
|
26871
|
+
this.noteSpokenTo(agent.name, metadata);
|
|
26411
26872
|
if (!this.activeTurnTokens.has(agent.name) || bypassDeferralFor === agent.name) {
|
|
26412
26873
|
this.noteArrival(agent.name, metadata, stored);
|
|
26413
26874
|
}
|
|
@@ -26864,6 +27325,7 @@ export class AgentFramework {
|
|
|
26864
27325
|
agent.getContextManager().addMessage(msg.participant, msg.content, withDeferredWriteId(msg.metadata, msg.id)));
|
|
26865
27326
|
for (const [msg, { id }] of stored) {
|
|
26866
27327
|
this.emitTrace({ type: 'message:added', messageId: id, source: `deferred-flush:${occasion}` });
|
|
27328
|
+
this.noteSpokenTo(agent.name, msg.metadata);
|
|
26867
27329
|
this.noteArrival(agent.name, msg.metadata, id);
|
|
26868
27330
|
this.noteForOtherResidents(agent, msg.metadata, id);
|
|
26869
27331
|
}
|
|
@@ -27367,18 +27829,18 @@ export class AgentFramework {
|
|
|
27367
27829
|
const outcome = failure.outcome === 'failed' || failure.outcome === 'unknown' ? failure.outcome : undefined;
|
|
27368
27830
|
const detail = typeof failure.detail === 'string' ? failure.detail : undefined;
|
|
27369
27831
|
const size = textLen !== undefined ? ` (${textLen} chars)` : '';
|
|
27370
|
-
const why = reason !== undefined ? ` (${reason})` : '';
|
|
27832
|
+
const why = reason !== undefined ? ` (${suppliedValue(reason)})` : '';
|
|
27371
27833
|
// A notice queued by an earlier build names no server: its channel is
|
|
27372
27834
|
// the one registered channel its id means, or shown by its id alone.
|
|
27373
27835
|
const named = this.channelNamed(failure.channelId, typeof failure.serverId === 'string' ? failure.serverId : undefined);
|
|
27374
|
-
const where = named ? this.channelForNotice(named) : failure.channelId;
|
|
27836
|
+
const where = named ? this.channelForNotice(named) : suppliedValue(failure.channelId);
|
|
27375
27837
|
const text = outcome === 'unknown'
|
|
27376
|
-
? `[
|
|
27838
|
+
? `[${NOTICE_TAG.discordSendFailed}] Delivery of a reply by ${speaker}${size} to ${where} was not confirmed${why}. ` +
|
|
27377
27839
|
`It may or may not have been posted, so sending it again could post it twice. ${speaker} has ended.`
|
|
27378
|
-
: `[
|
|
27840
|
+
: `[${NOTICE_TAG.discordSendFailed}] A reply by ${speaker}${size} could not be delivered to ${where}${why}. ` +
|
|
27379
27841
|
`Nothing was posted. ${speaker} has ended.`;
|
|
27380
27842
|
return {
|
|
27381
|
-
content: [{ type: 'text', text: text + (detail !== undefined ? ` Connector detail: ${detail}` : '') }],
|
|
27843
|
+
content: [{ type: 'text', text: text + (detail !== undefined ? ` Connector detail: ${suppliedValue(detail)}` : '') }],
|
|
27382
27844
|
metadata: {
|
|
27383
27845
|
system: true, kind: 'discord-send-failed', channelId: failure.channelId, speaker,
|
|
27384
27846
|
// The channel's server, as the marker it passes on carries it.
|
|
@@ -27407,17 +27869,16 @@ export class AgentFramework {
|
|
|
27407
27869
|
* channel no string picks alone, its id and its server.
|
|
27408
27870
|
*/
|
|
27409
27871
|
private channelShown(channel: ChannelRef): string {
|
|
27410
|
-
const name = this.channelRegistry?.nameShown(channel) ?? channel.channelId;
|
|
27872
|
+
const name = this.channelRegistry?.nameShown(channel) ?? suppliedValue(channel.channelId);
|
|
27411
27873
|
const label = this.channelRegistry?.getDescriptor(channel)?.label;
|
|
27412
|
-
|
|
27413
|
-
|
|
27414
|
-
: name;
|
|
27874
|
+
const shown = label && label !== channel.channelId ? suppliedValue(label) : undefined;
|
|
27875
|
+
return shown ? `${shown.startsWith('#') ? '' : '#'}${shown} (${name})` : name;
|
|
27415
27876
|
}
|
|
27416
27877
|
/** A channel named in a notice with the label it was given there: the
|
|
27417
27878
|
* label, with its name as ChannelRegistry.nameShown gives it. */
|
|
27418
27879
|
private labelledChannel(channel: ChannelRef & { label?: string }): string {
|
|
27419
|
-
const name = this.channelRegistry?.nameShown(channel) ?? channel.channelId;
|
|
27420
|
-
return channel.label && channel.label !== channel.channelId ? `${channel.label} (${name})` : name;
|
|
27880
|
+
const name = this.channelRegistry?.nameShown(channel) ?? suppliedValue(channel.channelId);
|
|
27881
|
+
return channel.label && channel.label !== channel.channelId ? `${suppliedValue(channel.label)} (${name})` : name;
|
|
27421
27882
|
}
|
|
27422
27883
|
|
|
27423
27884
|
|
|
@@ -27836,6 +28297,10 @@ export class AgentFramework {
|
|
|
27836
28297
|
if (name && this.exhaustionRewinds.get(name)) {
|
|
27837
28298
|
this.exhaustionRewinds.set(name, 0);
|
|
27838
28299
|
}
|
|
28300
|
+
if (name) this.turnRearms?.delete(name);
|
|
28301
|
+
}
|
|
28302
|
+
if (event.type === 'inference:completed' || event.type === 'inference:exhausted' || event.type === 'inference:aborted') {
|
|
28303
|
+
this.endStatedWaitRun(event.agentName as string);
|
|
27839
28304
|
}
|
|
27840
28305
|
|
|
27841
28306
|
const traceEvent = {
|
|
@@ -28166,7 +28631,7 @@ export class AgentFramework {
|
|
|
28166
28631
|
streak,
|
|
28167
28632
|
// The refused agent's own view: a subagent's refusal names no channel.
|
|
28168
28633
|
// Named as the agent is shown it, so a shared id names its server.
|
|
28169
|
-
locus: ((channel) => (channel ? this.channelRegistry?.nameShown(channel) ?? channel.channelId : null))(
|
|
28634
|
+
locus: ((channel) => (channel ? this.channelRegistry?.nameShown(channel) ?? suppliedValue(channel.channelId) : null))(
|
|
28170
28635
|
this.channelRegistry?.incomingFor(agentName)?.channel),
|
|
28171
28636
|
tokens: tokens ?? null,
|
|
28172
28637
|
});
|
|
@@ -28250,6 +28715,7 @@ export class AgentFramework {
|
|
|
28250
28715
|
): void {
|
|
28251
28716
|
const ended = this.exhaustedTurns?.get(agentName);
|
|
28252
28717
|
this.exhaustedTurns?.delete(agentName);
|
|
28718
|
+
const rearmed = this.exhaustingRearms?.delete(agentName) === true;
|
|
28253
28719
|
// The reason is the failure's own message, and a provider can echo the
|
|
28254
28720
|
// whole rejected request in it: one household's [inference-failed]
|
|
28255
28721
|
// marker carried ~1.7 MB of its own request back into its context. Every
|
|
@@ -28305,7 +28771,7 @@ export class AgentFramework {
|
|
|
28305
28771
|
|
|
28306
28772
|
// (2) Agent-facing chronicle marker (no inference triggered → no loop).
|
|
28307
28773
|
const agent = this.agents.get(agentName);
|
|
28308
|
-
if (agent && process.env.SUPPRESS_INFERENCE_FAILED_MARKER !== '1') {
|
|
28774
|
+
if (agent && !(rearmed && !ended) && process.env.SUPPRESS_INFERENCE_FAILED_MARKER !== '1') {
|
|
28309
28775
|
const cause = [errorType, httpStatus !== undefined ? `HTTP ${httpStatus}` : undefined]
|
|
28310
28776
|
.filter((part): part is string => part !== undefined)
|
|
28311
28777
|
.join(', ');
|
|
@@ -28316,15 +28782,15 @@ export class AgentFramework {
|
|
|
28316
28782
|
type: 'text',
|
|
28317
28783
|
text:
|
|
28318
28784
|
(ended
|
|
28319
|
-
? `[
|
|
28320
|
-
`failed${cause ? ` (${cause})` : ''}.` +
|
|
28785
|
+
? `[${NOTICE_TAG.inferenceFailed}] Your previous turn did not complete: it ` +
|
|
28786
|
+
`failed${cause ? ` (${suppliedValue(cause)})` : ''}.` +
|
|
28321
28787
|
`${ended.roundsRan ? ' Its earlier rounds ran, and their tool calls ran as their results show.' : ''}` +
|
|
28322
28788
|
`${ended.callsCutOff ? ` ${CALLS_CUT_OFF}` : ''}` +
|
|
28323
28789
|
`${ended.liveSpeech ? ` ${LIVE_SPEECH_UNRECEIPTED}` : ''}` +
|
|
28324
28790
|
`${ended.speechRouting ? ` ${SPEECH_STILL_ROUTING}` : ''} Reason: `
|
|
28325
|
-
: `[
|
|
28326
|
-
`call failed${cause ? ` (${cause})` : ''} and produced no response, so nothing was sent. Reason: `) +
|
|
28327
|
-
`${markerReason}. If this recurs with the same cause, change approach ` +
|
|
28791
|
+
: `[${NOTICE_TAG.inferenceFailed}] Your previous turn did not complete: the model ` +
|
|
28792
|
+
`call failed${cause ? ` (${suppliedValue(cause)})` : ''} and produced no response, so nothing was sent. Reason: `) +
|
|
28793
|
+
`${failureShown(markerReason)}. If this recurs with the same cause, change approach ` +
|
|
28328
28794
|
`rather than retrying identically (e.g. drop an oversized attachment ` +
|
|
28329
28795
|
`or an unsupported setting).`,
|
|
28330
28796
|
}],
|
|
@@ -28633,13 +29099,14 @@ export class AgentFramework {
|
|
|
28633
29099
|
// channel hijacks it. An opening start resolves the turn's locus from
|
|
28634
29100
|
// it (the trigger channel then), and the channel context names it, so
|
|
28635
29101
|
// a later start of the turn is told where its unprefixed speech lands
|
|
28636
|
-
// unless a hybrid `>>>` destination takes it. Empty for a
|
|
28637
|
-
//
|
|
28638
|
-
//
|
|
28639
|
-
//
|
|
29102
|
+
// unless a hybrid `>>>` destination takes it. Empty for a no-trigger
|
|
29103
|
+
// turn until its locus is resolved, so that resolution takes the
|
|
29104
|
+
// global fallback (none for an ephemeral run), and the turn then keeps
|
|
29105
|
+
// that channel; null for a heartbeat's turn, which takes none
|
|
29106
|
+
// (TurnRouting.heartbeat).
|
|
28640
29107
|
activeChannelResolver: (agentName) => {
|
|
28641
29108
|
const routing = this.turnRouting.get(agentName);
|
|
28642
|
-
return routing?.locus ?? routing?.triggerChannel ?? (routing?.noChannel ? null : undefined);
|
|
29109
|
+
return routing?.locus ?? routing?.triggerChannel ?? (routing?.noChannel || routing?.heartbeat ? null : undefined);
|
|
28643
29110
|
},
|
|
28644
29111
|
// An ephemeral run (a subagent) never takes the process-global
|
|
28645
29112
|
// fallback: its prose is its caller's result, and the latest inbound
|
|
@@ -28694,13 +29161,13 @@ export class AgentFramework {
|
|
|
28694
29161
|
? (outcome === 'unknown'
|
|
28695
29162
|
// A request went out and no valid receipt came back: it may
|
|
28696
29163
|
// have been posted (in part or whole). Never claim it wasn't.
|
|
28697
|
-
? `[
|
|
28698
|
-
: `[
|
|
28699
|
-
: `[
|
|
29164
|
+
? `[${NOTICE_TAG.discordSendFailed}] Delivery of your previous reply (${textLen} chars) to ${where} was not confirmed (${suppliedValue(reason)}). It may or may not have been posted, so sending it again could post it twice. It is saved in your archive.`
|
|
29165
|
+
: `[${NOTICE_TAG.discordSendFailed}] Your previous reply (${textLen} chars) could not be delivered to ${where} (${suppliedValue(reason)}). Nothing was posted; it is saved in your archive.`)
|
|
29166
|
+
: `[${NOTICE_TAG.sendUndeliverable}] Your previous reply (${textLen} chars) had no channel to go to — ${suppliedValue(reason)}. This is a routing/configuration situation, not a channel failure. This route delivered it nowhere; if another surface showed it (a reply thread, the console), that copy stands. It is saved in your archive.`;
|
|
28700
29167
|
// A registered speaker's notice is addressed to it (noticeTargetFor).
|
|
28701
29168
|
this.addMessage(
|
|
28702
29169
|
'user',
|
|
28703
|
-
[{ type: 'text', text: text + (detail !== undefined ? ` Connector detail: ${detail}` : '') }],
|
|
29170
|
+
[{ type: 'text', text: text + (detail !== undefined ? ` Connector detail: ${suppliedValue(detail)}` : '') }],
|
|
28704
29171
|
metadata,
|
|
28705
29172
|
{ forAgent: conversationId },
|
|
28706
29173
|
);
|
|
@@ -28824,6 +29291,10 @@ export class AgentFramework {
|
|
|
28824
29291
|
return (this.mcpRestartClaims ??= { ids: new Map(), prefixes: new Map() });
|
|
28825
29292
|
}
|
|
28826
29293
|
|
|
29294
|
+
private closedTabConfigs(): Map<string, McplServerConfig> {
|
|
29295
|
+
return (this.closedTabConfigMap ??= new Map());
|
|
29296
|
+
}
|
|
29297
|
+
|
|
28827
29298
|
private legacyConnectAborts(): Map<string, AbortController> {
|
|
28828
29299
|
return (this.legacyConnectAbortMap ??= new Map());
|
|
28829
29300
|
}
|
|
@@ -29436,14 +29907,61 @@ export class AgentFramework {
|
|
|
29436
29907
|
});
|
|
29437
29908
|
}
|
|
29438
29909
|
|
|
29910
|
+
/** RFC-014: who held the name `toolsets` at startup (a module given to
|
|
29911
|
+
* create(); a server's toolPrefix of it is refused), so a server that
|
|
29912
|
+
* leaves toolHints out has none for the life of this framework; null when
|
|
29913
|
+
* the name was free. */
|
|
29914
|
+
private toolsetsNameHeldAtStart: string | null = null;
|
|
29915
|
+
|
|
29916
|
+
/** At startup: note who holds the name the meta-tools need, and say which
|
|
29917
|
+
* servers that leaves without hints. */
|
|
29918
|
+
private reserveToolsetsName(modules: readonly Module[], servers: readonly McplServerConfig[]): void {
|
|
29919
|
+
const module = modules.find((m) => m.name === TOOLSETS_MODULE);
|
|
29920
|
+
this.toolsetsNameHeldAtStart = module ? `module "${TOOLSETS_MODULE}"` : null;
|
|
29921
|
+
if (!this.toolsetsNameHeldAtStart) return;
|
|
29922
|
+
// Those that would have had hints: MCPL servers that leave the setting out.
|
|
29923
|
+
const defaulted = servers
|
|
29924
|
+
.filter((s) => (s.toolHints === undefined || s.toolHints === null) && isLegacyFamily(s))
|
|
29925
|
+
.map((s) => s.id);
|
|
29926
|
+
if (defaulted.length) {
|
|
29927
|
+
console.error(
|
|
29928
|
+
`[mcpl] tool hints (RFC-014) are off for ${defaulted.join(', ')}, which leave toolHints out: the name "${TOOLSETS_MODULE}" ` +
|
|
29929
|
+
`the meta-tools need is ${this.toolsetsNameHeldAtStart}. Free the name to give them hints, or set toolHints: false on them to keep them off without this line.`,
|
|
29930
|
+
);
|
|
29931
|
+
}
|
|
29932
|
+
}
|
|
29933
|
+
|
|
29934
|
+
/**
|
|
29935
|
+
* RFC-014: whether a server has tool hints. Only an MCPL (legacy-family)
|
|
29936
|
+
* server can: a modern MCP server never has them. `true` and `false` are
|
|
29937
|
+
* as configured. Left out (or null) they're on, wherever the name
|
|
29938
|
+
* `toolsets` the meta-tools need is free: no other module holds it and no
|
|
29939
|
+
* server's prefix is it, now or at startup. So a configuration that loaded
|
|
29940
|
+
* when hints were opt-in still loads. A name held at startup stays held
|
|
29941
|
+
* for the framework's life.
|
|
29942
|
+
*/
|
|
29943
|
+
private hintsFor(config: McplServerConfig): boolean {
|
|
29944
|
+
if (config.toolHints === false || !isLegacyFamily(config)) return false;
|
|
29945
|
+
if (config.toolHints === true) return true;
|
|
29946
|
+
if (this.toolsetsNameHeldAtStart !== null) return false;
|
|
29947
|
+
if ((config.toolPrefix ?? `mcpl--${config.id}`) === TOOLSETS_MODULE) return false;
|
|
29948
|
+
const module = this.moduleRegistry.getAllModules().find((m) => m.name === TOOLSETS_MODULE);
|
|
29949
|
+
if (module && !(module instanceof ToolsetsModule)) return false;
|
|
29950
|
+
// A restart moving its server to the prefix holds the name as a claim.
|
|
29951
|
+
if (this.restartClaims().prefixes.has(TOOLSETS_MODULE)) return false;
|
|
29952
|
+
const owner = this.mcplPrefixMap.get(TOOLSETS_MODULE);
|
|
29953
|
+
return owner === undefined || owner === config.id;
|
|
29954
|
+
}
|
|
29955
|
+
|
|
29439
29956
|
/**
|
|
29440
29957
|
* The host capabilities offered to one server: the host's own, plus
|
|
29441
|
-
* RFC-014's `toolHints`
|
|
29442
|
-
* the very object every server is
|
|
29958
|
+
* RFC-014's `toolHints` unless its configuration turns them off
|
|
29959
|
+
* (`toolHints: false`). With them off, the very object every server is
|
|
29960
|
+
* offered.
|
|
29443
29961
|
*/
|
|
29444
|
-
private mcplHostCapabilitiesFor(config: McplServerConfig): McplHostCapabilities {
|
|
29962
|
+
private mcplHostCapabilitiesFor(config: McplServerConfig, hints = this.hintsFor(config)): McplHostCapabilities {
|
|
29445
29963
|
const base = this.mcplHostCapabilities!;
|
|
29446
|
-
return
|
|
29964
|
+
return hints ? { ...base, toolHints: true } : base;
|
|
29447
29965
|
}
|
|
29448
29966
|
|
|
29449
29967
|
/** RFC-014: every toolHints server's presence, made on first use. */
|
|
@@ -29460,20 +29978,170 @@ export class AgentFramework {
|
|
|
29460
29978
|
if (this.mcplToolServers?.get(name) !== serverId) return undefined;
|
|
29461
29979
|
return this.mcplTools.some((t) => t.name === name) ? 'static' : 'dynamic';
|
|
29462
29980
|
},
|
|
29981
|
+
hostOpen: (agentName) => this.hostPresentation?.openMarkers(agentName) ?? [],
|
|
29463
29982
|
}));
|
|
29464
29983
|
}
|
|
29465
29984
|
|
|
29466
|
-
/**
|
|
29467
|
-
*
|
|
29468
|
-
*
|
|
29985
|
+
/**
|
|
29986
|
+
* The host's own toolsets (host-presentation.ts): the built-in "settings"
|
|
29987
|
+
* toolset and each MCPL server's tab. State is kept beside the store, in
|
|
29988
|
+
* `config/toolsets.json`; with no store path, in memory only.
|
|
29989
|
+
*/
|
|
29990
|
+
private makeHostPresentation(storePath: string | undefined): HostPresentation {
|
|
29991
|
+
return new HostPresentation({
|
|
29992
|
+
// As the agent is shown them: its description overrides applied.
|
|
29993
|
+
available: (agentName) => (this.agents.has(agentName) ? this.compressionToolsForAgent(agentName) : this.getAllTools()),
|
|
29994
|
+
placedInBuiltin: (name) => this.placedInBuiltin(name),
|
|
29995
|
+
hidden: (agentName) => {
|
|
29996
|
+
if (!this.agents.has(agentName)) return new Set<string>();
|
|
29997
|
+
const snapshot = this.inspectToolPresentation(agentName);
|
|
29998
|
+
return new Set(snapshot?.entries.filter((e) => !e.visible).map((e) => e.name) ?? []);
|
|
29999
|
+
},
|
|
30000
|
+
serverOf: (name) => this.resolveMcplTool(name)?.[0] ?? null,
|
|
30001
|
+
servers: () => {
|
|
30002
|
+
const out = new Map<string, { id: string; configured: 'full' | 'tab' | 'closed-tab'; connected: boolean }>();
|
|
30003
|
+
for (const [id, config] of this.mcplServerConfigs) {
|
|
30004
|
+
out.set(id, { id, configured: config.presentation ?? 'full', connected: !!this.mcplServerRegistry?.getServer(id) || this.modernServers().has(id) });
|
|
30005
|
+
}
|
|
30006
|
+
for (const [id, config] of this.closedTabConfigs()) {
|
|
30007
|
+
if (!out.has(id)) out.set(id, { id, configured: config.presentation ?? 'full', connected: false });
|
|
30008
|
+
}
|
|
30009
|
+
return [...out.values()];
|
|
30010
|
+
},
|
|
30011
|
+
canUseMeta: (agentName) => this.hostToolsetsFor(agentName),
|
|
30012
|
+
disconnect: async (serverId) => {
|
|
30013
|
+
const config = this.mcplServerConfigs.get(serverId);
|
|
30014
|
+
if (!config) throw new Error(`no MCPL server "${serverId}" is connected here`);
|
|
30015
|
+
this.refuseOperatorAfterStop('disconnect MCPL server');
|
|
30016
|
+
// As a transient close: its checkpoint trees and RFC-014 state stay
|
|
30017
|
+
// for the connect its next open makes.
|
|
30018
|
+
await this.trackOperatorOperation('mcpl:disconnect', () => this.runDisconnectMcplServer(serverId, { keepToolsets: true, keepCheckpoints: true }));
|
|
30019
|
+
this.closedTabConfigs().set(serverId, config);
|
|
30020
|
+
},
|
|
30021
|
+
reconnect: async (serverId) => {
|
|
30022
|
+
const config = this.closedTabConfigs().get(serverId);
|
|
30023
|
+
if (!config) throw new Error(`no MCPL server "${serverId}" is kept as a closed tab here`);
|
|
30024
|
+
this.refuseOperatorAfterStop('connect MCPL server');
|
|
30025
|
+
await this.trackOperatorOperation('mcpl:connect', () => this.runConnectMcplServer(config, undefined, { reopen: true }));
|
|
30026
|
+
},
|
|
30027
|
+
log: (line) => console.error(line),
|
|
30028
|
+
isUtility: (name) => this.isModuleUtility(name),
|
|
30029
|
+
residents: () => this.heartbeatTargets(),
|
|
30030
|
+
}, storePath ? join(storePath, 'config', 'toolsets.json') : null);
|
|
30031
|
+
}
|
|
30032
|
+
|
|
30033
|
+
/** Whether a name is a module utility (and not a tool of its module, which takes the name). */
|
|
30034
|
+
private isModuleUtility(name: string): boolean {
|
|
30035
|
+
const sep = name.indexOf('--');
|
|
30036
|
+
if (sep <= 0) return false;
|
|
30037
|
+
const module = this.moduleRegistry.getModule(name.slice(0, sep));
|
|
30038
|
+
if (!module) return false;
|
|
30039
|
+
const bare = name.slice(sep + 2);
|
|
30040
|
+
try {
|
|
30041
|
+
return !module.getTools().some((t) => t.name === bare) && (module.getUtilities?.()?.some((t) => t.name === bare) ?? false);
|
|
30042
|
+
} catch {
|
|
30043
|
+
return false;
|
|
30044
|
+
}
|
|
30045
|
+
}
|
|
30046
|
+
|
|
30047
|
+
/** Whether an agent has the host's own toolsets: the meta-tools registered, and
|
|
30048
|
+
* usable by it (the subconscious keeps its own small surface). */
|
|
30049
|
+
/**
|
|
30050
|
+
* What a call of the removed `utils` is answered: where utilities went, as
|
|
30051
|
+
* this agent can reach them. One that can't use the built-in toolset is
|
|
30052
|
+
* told to call a utility by its own name and which ones it may call.
|
|
30053
|
+
*/
|
|
30054
|
+
private utilsRemoved(agentName: string | undefined): string {
|
|
30055
|
+
if (agentName === undefined || this.hostToolsetsFor(agentName)) return UTILS_REMOVED;
|
|
30056
|
+
const agent = this.agents.get(agentName);
|
|
30057
|
+
const why = this.moduleRegistry.getAllModules().find((m) => m.name === TOOLSETS_MODULE) instanceof ToolsetsModule
|
|
30058
|
+
? `its allowedTools doesn't permit ${META_TOOL_NAMES.open} and ${META_TOOL_NAMES.act}`
|
|
30059
|
+
: `this host's module named "${TOOLSETS_MODULE}" isn't the framework's`;
|
|
30060
|
+
const mine = this.getAllTools().filter((t) => this.isModuleUtility(t.name) && (agent?.canUseTool(t.name) ?? true)).map((t) => t.name);
|
|
30061
|
+
return `utils was removed: module utilities are in the host's built-in toolset now, which this agent can't use (${why}). ` +
|
|
30062
|
+
`Call a utility by its own name instead${mine.length ? `; this agent may call ${mine.join(', ')}` : '; this agent may call none'}. Nothing was run.`;
|
|
30063
|
+
}
|
|
30064
|
+
|
|
30065
|
+
private hostToolsetsFor(agentName: string): boolean {
|
|
30066
|
+
if (agentName === this.subconsciousAgentName) return false;
|
|
30067
|
+
const module = this.moduleRegistry.getAllModules().find((m) => m.name === TOOLSETS_MODULE);
|
|
30068
|
+
if (!(module instanceof ToolsetsModule)) return false;
|
|
30069
|
+
const agent = this.agents.get(agentName);
|
|
30070
|
+
if (!agent) return true;
|
|
30071
|
+
return agent.canUseTool(META_TOOL_NAMES.open) && agent.canUseTool(META_TOOL_NAMES.act);
|
|
30072
|
+
}
|
|
30073
|
+
|
|
30074
|
+
/**
|
|
30075
|
+
* Whether a host tool belongs to the built-in toolset: the framework's own
|
|
30076
|
+
* rarely-used ones (BUILTIN_TOOLSET_TOOLS), a module's that it places there
|
|
30077
|
+
* (Module.toolPlacement), and every module utility.
|
|
30078
|
+
*/
|
|
30079
|
+
private placedInBuiltin(name: string): boolean {
|
|
30080
|
+
if (BUILTIN_TOOLSET_TOOLS.has(name)) return true;
|
|
30081
|
+
const sep = name.indexOf('--');
|
|
30082
|
+
if (sep <= 0) return false;
|
|
30083
|
+
const module = this.moduleRegistry.getModule(name.slice(0, sep));
|
|
30084
|
+
if (!module) return false;
|
|
30085
|
+
const bare = name.slice(sep + 2);
|
|
30086
|
+
// A module whose listing throws keeps its tools where they are.
|
|
30087
|
+
try {
|
|
30088
|
+
if (module.getTools().some((t) => t.name === bare)) return module.toolPlacement?.(bare) === 'builtin';
|
|
30089
|
+
return module.getUtilities?.()?.some((t) => t.name === bare) ?? false;
|
|
30090
|
+
} catch {
|
|
30091
|
+
return false;
|
|
30092
|
+
}
|
|
30093
|
+
}
|
|
30094
|
+
|
|
30095
|
+
/**
|
|
30096
|
+
* The tool list an agent's requests carry, from the tools it may call: its
|
|
30097
|
+
* host presentation applied (the built-in toolset's tools, the servers it
|
|
30098
|
+
* has as tabs and its hidden tools left out; a utility never in it), as
|
|
30099
|
+
* every request of a registered agent has it (with its own tool
|
|
30100
|
+
* presentation, hidden tools and descriptions, applied first). For a stream that isn't
|
|
30101
|
+
* registered yet, such as a fork whose orientation is written before its
|
|
30102
|
+
* run starts, the host says whether it will be able to call
|
|
30103
|
+
* `toolsets--open` and `toolsets--act` (`canUseMeta`); left out, the agent
|
|
30104
|
+
* registered under that name is asked. Presentation it chose itself (open
|
|
30105
|
+
* toolsets, tabs) is read under that name, so a new name starts from the
|
|
30106
|
+
* configured presentation.
|
|
30107
|
+
*/
|
|
30108
|
+
presentedTools(agentName: string, tools: import('./types/index.js').ToolDefinition[], canUseMeta?: boolean): import('./types/index.js').ToolDefinition[] {
|
|
30109
|
+
// A registered agent's own presentation first, as its requests have it
|
|
30110
|
+
// before the host's: the tools it hid left out, its descriptions applied.
|
|
30111
|
+
const snapshot = this.agents.has(agentName) ? this.inspectToolPresentation(agentName) : undefined;
|
|
30112
|
+
if (snapshot) {
|
|
30113
|
+
const hidden = new Set(snapshot.entries.filter((e) => !e.visible).map((e) => e.name));
|
|
30114
|
+
const shown = new Map(snapshot.advertised.map((t) => [t.name, t]));
|
|
30115
|
+
tools = tools.filter((t) => !hidden.has(t.name)).map((t) => shown.get(t.name) ?? t);
|
|
30116
|
+
}
|
|
30117
|
+
if (!this.hostPresentation) return tools;
|
|
30118
|
+
if (canUseMeta === undefined) return this.hostPresentation.header(agentName, tools);
|
|
30119
|
+
// As hostToolsetsFor decides for a registered agent: the subconscious and
|
|
30120
|
+
// a host whose module named toolsets isn't the framework's present nothing.
|
|
30121
|
+
const meta = canUseMeta && agentName !== this.subconsciousAgentName
|
|
30122
|
+
&& this.moduleRegistry.getAllModules().find((m) => m.name === TOOLSETS_MODULE) instanceof ToolsetsModule;
|
|
30123
|
+
return this.hostPresentation.header(agentName, tools, meta);
|
|
30124
|
+
}
|
|
30125
|
+
|
|
30126
|
+
/** The host presentation of a server's tools, for the tool list: the agent's
|
|
30127
|
+
* view, less the built-in toolset and the servers it has as tabs. */
|
|
30128
|
+
private headerTools(agentName: string, tools: import('./types/index.js').ToolDefinition[]): import('./types/index.js').ToolDefinition[] {
|
|
30129
|
+
return this.hostPresentation ? this.hostPresentation.header(agentName, tools) : tools;
|
|
30130
|
+
}
|
|
30131
|
+
|
|
30132
|
+
/** The `toolsets` module (toolsets--open, --act, --look, --close): the
|
|
30133
|
+
* meta-tools of the host's own toolsets and of RFC-014 servers' toolsets.
|
|
30134
|
+
* Registered once, at create(), unless a module given to create() holds
|
|
30135
|
+
* the name; a server with toolHints: true then can't have hints, and its
|
|
30136
|
+
* connect is refused here. Its tools don't change after that. */
|
|
29469
30137
|
private async ensureToolsetsModule(): Promise<void> {
|
|
29470
30138
|
const hub = this.toolsets();
|
|
29471
30139
|
const existing = this.moduleRegistry.getAllModules().find((m) => m.name === TOOLSETS_MODULE);
|
|
29472
30140
|
if (existing instanceof ToolsetsModule) return;
|
|
29473
30141
|
if (existing) {
|
|
29474
|
-
throw new Error(`A module named "${TOOLSETS_MODULE}" is registered, and a server configured with toolHints needs that name for its meta-tools`);
|
|
30142
|
+
throw new Error(`A module named "${TOOLSETS_MODULE}" is registered, and a server configured with toolHints: true needs that name for its meta-tools: set toolHints: false on it (or leave the setting out: it then has no hints here), or free the name`);
|
|
29475
30143
|
}
|
|
29476
|
-
await this.addModule(new ToolsetsModule(hub, (call) => this.changeTaker(call)));
|
|
30144
|
+
await this.addModule(new ToolsetsModule(hub, (call) => this.changeTaker(call), this.hostPresentation));
|
|
29477
30145
|
}
|
|
29478
30146
|
|
|
29479
30147
|
/** RFC-014 §5.3: after a (re)connect whose policy has settled, read what
|
|
@@ -29509,7 +30177,7 @@ export class AgentFramework {
|
|
|
29509
30177
|
try {
|
|
29510
30178
|
this.addMessage(
|
|
29511
30179
|
'user',
|
|
29512
|
-
[{ type: 'text', text: `MCPL server "${serverId}", tool changes:\n${text}` }],
|
|
30180
|
+
[{ type: 'text', text: `[${NOTICE_TAG.toolsets}] MCPL server "${serverId}", tool changes:\n${text}` }],
|
|
29513
30181
|
{ system: true, kind: 'mcpl-toolsets', serverId, ...(opts.invite ? { invite: true } : {}) } as MessageMetadata,
|
|
29514
30182
|
);
|
|
29515
30183
|
return true;
|
|
@@ -29677,23 +30345,27 @@ export class AgentFramework {
|
|
|
29677
30345
|
let registeredEpoch = 0;
|
|
29678
30346
|
const abort = new AbortController();
|
|
29679
30347
|
this.legacyConnectAborts().set(config.id, abort);
|
|
30348
|
+
// RFC-014: decided once for this connect, so the meta-tools, the offer
|
|
30349
|
+
// and the read tap agree though the state hintsFor reads can change
|
|
30350
|
+
// across the awaits below.
|
|
30351
|
+
const hints = this.hintsFor(config);
|
|
29680
30352
|
try {
|
|
29681
|
-
// RFC-014: a
|
|
29682
|
-
if (
|
|
30353
|
+
// RFC-014: a hinted server's meta-tools exist before it connects.
|
|
30354
|
+
if (hints) await this.ensureToolsetsModule();
|
|
29683
30355
|
// Its policy exchange is in flight from its registration until the
|
|
29684
30356
|
// exchange settles: an opener that runs meanwhile (a resume, startup's
|
|
29685
30357
|
// staged open) passes the connection by, so nothing opens its planes
|
|
29686
30358
|
// before its grant exists.
|
|
29687
30359
|
connection = await this.mcplServerRegistry.addServer(
|
|
29688
30360
|
connectionConfig,
|
|
29689
|
-
this.mcplHostCapabilitiesFor(config),
|
|
30361
|
+
this.mcplHostCapabilitiesFor(config, hints),
|
|
29690
30362
|
abort.signal,
|
|
29691
30363
|
(registering) => {
|
|
29692
30364
|
registeredEpoch = registering.transportEpoch;
|
|
29693
30365
|
this.mcplPolicyExchanges.set(registering, registeredEpoch);
|
|
29694
30366
|
// RFC-014 §6.1: hints apply as the connection reads them, from its
|
|
29695
30367
|
// first message, so the tap is on before any I/O can deliver one.
|
|
29696
|
-
if (
|
|
30368
|
+
if (hints) this.toolsets().controllerFor(config).attach(registering);
|
|
29697
30369
|
},
|
|
29698
30370
|
);
|
|
29699
30371
|
} catch (error) {
|
|
@@ -30018,7 +30690,7 @@ export class AgentFramework {
|
|
|
30018
30690
|
// instances; toolHints withdrawn makes every tool static. Its impact is
|
|
30019
30691
|
// surface-changed, applied, with no grant consequence (§17.6).
|
|
30020
30692
|
const declarations = toolsetDeclarationsOf(fetched);
|
|
30021
|
-
const toolsetsChanged = config
|
|
30693
|
+
const toolsetsChanged = this.hintsFor(config)
|
|
30022
30694
|
&& JSON.stringify(declarations) !== JSON.stringify(connection.toolsetDeclarations);
|
|
30023
30695
|
connection.toolsetDeclarations = declarations;
|
|
30024
30696
|
if (toolsetsChanged) this.toolsetHub?.get(connection.id)?.manifestChanged(declarations);
|
|
@@ -30333,6 +31005,7 @@ export class AgentFramework {
|
|
|
30333
31005
|
private async runConnectMcplServer(
|
|
30334
31006
|
config: import('./mcpl/types.js').McplServerConfig,
|
|
30335
31007
|
claim?: McpRestartClaim,
|
|
31008
|
+
opts: { reopen?: boolean } = {},
|
|
30336
31009
|
): Promise<void> {
|
|
30337
31010
|
assertServerConfigBooleans(config);
|
|
30338
31011
|
// Lazily bring up the MCPL subsystem — a framework that started with zero
|
|
@@ -30359,8 +31032,30 @@ export class AgentFramework {
|
|
|
30359
31032
|
// registers. A server already registered was refused just above, so the
|
|
30360
31033
|
// read never reaches a registered server's trees.
|
|
30361
31034
|
this.checkpointManager?.loadHostStates(config.id);
|
|
31035
|
+
// An operator's connect ends a closed tab as it begins, as a restart
|
|
31036
|
+
// does: one that fails once its configuration is taken leaves an
|
|
31037
|
+
// ordinary server that's down, under that configuration; one refused
|
|
31038
|
+
// before (its transport, the awareness ledger) changes nothing, and the
|
|
31039
|
+
// closed tab is put back. Whether it was one is the kept configuration's
|
|
31040
|
+
// to say. An agent's open (reopen) ends it only once connected, so one
|
|
31041
|
+
// that fails leaves the closed tab to open again.
|
|
31042
|
+
const keptClosedTab = opts.reopen ? undefined : this.closedTabConfigs().get(config.id);
|
|
31043
|
+
const closedWas = keptClosedTab ? this.hostPresentation?.closedState(config.id) : undefined;
|
|
31044
|
+
if (keptClosedTab) {
|
|
31045
|
+
this.closedTabConfigs().delete(config.id);
|
|
31046
|
+
this.hostPresentation?.noteConnected(config.id, true);
|
|
31047
|
+
}
|
|
30362
31048
|
const oldToolNames = new Set(this.mcplTools.map(t => t.name));
|
|
30363
|
-
|
|
31049
|
+
let connection: Awaited<ReturnType<AgentFramework['connectMcplServerInternal']>>;
|
|
31050
|
+
try {
|
|
31051
|
+
connection = await this.connectMcplServerInternal(config, false, claim);
|
|
31052
|
+
} catch (error) {
|
|
31053
|
+
if (keptClosedTab && !this.mcplServerConfigs.has(config.id) && !this.closedTabConfigs().has(config.id)) {
|
|
31054
|
+
this.closedTabConfigs().set(config.id, keptClosedTab);
|
|
31055
|
+
this.hostPresentation?.restoreClosed(config.id, closedWas);
|
|
31056
|
+
}
|
|
31057
|
+
throw error;
|
|
31058
|
+
}
|
|
30364
31059
|
// A stop() that began during the connect's setup or its tool listing
|
|
30365
31060
|
// ends the connect: it doesn't wait on a server for a framework that is
|
|
30366
31061
|
// stopping, and the teardown closes what the connect set up.
|
|
@@ -30381,6 +31076,9 @@ export class AgentFramework {
|
|
|
30381
31076
|
if (this.mcpServerOps?.get(config.id)?.kind === 'disconnect' || !AgentFramework.mcpConnectionServes(connection)) {
|
|
30382
31077
|
throw new Error(`MCP server "${config.id}" was not connected: it was disconnected or lost while its tools were being listed`);
|
|
30383
31078
|
}
|
|
31079
|
+
// Connected, however it was asked for: not a closed tab any more.
|
|
31080
|
+
const wasClosedTab = this.closedTabConfigs().delete(config.id);
|
|
31081
|
+
this.hostPresentation?.noteConnected(config.id, wasClosedTab);
|
|
30384
31082
|
this.emitMcplToolDiff(oldToolNames, config.id);
|
|
30385
31083
|
}
|
|
30386
31084
|
|
|
@@ -30407,10 +31105,20 @@ export class AgentFramework {
|
|
|
30407
31105
|
// even one that finishes before the restart's connect begins.
|
|
30408
31106
|
const claim = this.restartClaims().ids.get(id);
|
|
30409
31107
|
if (claim) claim.endedBy = `MCP server "${id}" was disconnected, which ends the restart of "${claim.restarting}"`;
|
|
30410
|
-
|
|
31108
|
+
// A closed tab removed by its operator: there's nothing to connect it
|
|
31109
|
+
// again with, and what its disconnect kept for that connect (checkpoint
|
|
31110
|
+
// trees, RFC-014 state) goes, as any disconnect removes it.
|
|
31111
|
+
if (this.closedTabConfigs().delete(id) && !this.mcplServerConfigs.has(id)) {
|
|
31112
|
+
this.checkpointManager?.removeServer(id);
|
|
31113
|
+
this.toolsetHub?.forget(id);
|
|
31114
|
+
this.hostPresentation?.noteRemoved(id);
|
|
31115
|
+
return;
|
|
31116
|
+
}
|
|
31117
|
+
await this.trackOperatorOperation('mcpl:disconnect', () => this.runDisconnectMcplServer(id));
|
|
31118
|
+
this.hostPresentation?.noteRemoved(id);
|
|
30411
31119
|
}
|
|
30412
31120
|
|
|
30413
|
-
private async runDisconnectMcplServer(id: string, opts: { keepToolsets?: boolean } = {}): Promise<void> {
|
|
31121
|
+
private async runDisconnectMcplServer(id: string, opts: { keepToolsets?: boolean; keepCheckpoints?: boolean } = {}): Promise<void> {
|
|
30414
31122
|
if (!this.mcplServerRegistry) {
|
|
30415
31123
|
throw new Error('MCPL subsystem is not initialized');
|
|
30416
31124
|
}
|
|
@@ -30462,7 +31170,9 @@ export class AgentFramework {
|
|
|
30462
31170
|
// already transiently closed (reconnect pending) emits no second 'close'
|
|
30463
31171
|
// from close(), and the transient path deliberately preserves checkpoints.
|
|
30464
31172
|
this.featureSetManager?.removeServer(id);
|
|
30465
|
-
|
|
31173
|
+
// A closed tab (host-presentation.ts) keeps its checkpoint trees for
|
|
31174
|
+
// the connect its next open makes, as a transient close does.
|
|
31175
|
+
if (!opts.keepCheckpoints) this.checkpointManager?.removeServer(id);
|
|
30466
31176
|
if (!opts.keepToolsets) this.toolsetHub?.forget(id);
|
|
30467
31177
|
|
|
30468
31178
|
// Only this server's own claims, as the prefix map holds them rather
|
|
@@ -30474,6 +31184,7 @@ export class AgentFramework {
|
|
|
30474
31184
|
}
|
|
30475
31185
|
this.mcplServerConfigs.delete(id);
|
|
30476
31186
|
referenceRegistry.forgetServerAddress(id, this);
|
|
31187
|
+
this.eventGate?.sourcesChanged();
|
|
30477
31188
|
|
|
30478
31189
|
await this.refreshMcplTools();
|
|
30479
31190
|
this.emitMcplToolDiff(oldToolNames, id);
|
|
@@ -30533,7 +31244,8 @@ export class AgentFramework {
|
|
|
30533
31244
|
// A stop(), or a disconnect of the id it moves the server to, that
|
|
30534
31245
|
// begins after these checks isn't held off: it ends the restart, as it
|
|
30535
31246
|
// would end a connect.
|
|
30536
|
-
|
|
31247
|
+
// A closed tab restarts from the configuration it's kept with.
|
|
31248
|
+
const config = newConfig ?? this.mcplServerConfigs.get(id) ?? this.closedTabConfigs().get(id);
|
|
30537
31249
|
if (!config) {
|
|
30538
31250
|
throw new Error(`MCPL server "${id}" is not configured`);
|
|
30539
31251
|
}
|
|
@@ -30573,7 +31285,15 @@ export class AgentFramework {
|
|
|
30573
31285
|
this.restartClaims().ids.set(config.id, claim);
|
|
30574
31286
|
this.restartClaims().prefixes.set(prefix, claim);
|
|
30575
31287
|
try {
|
|
30576
|
-
await this.runDisconnectMcplServer(id, { keepToolsets: config.id === id && config
|
|
31288
|
+
await this.runDisconnectMcplServer(id, { keepToolsets: config.id === id && this.hintsFor(config) });
|
|
31289
|
+
// A restart ends a closed tab as it begins: the operator connects the
|
|
31290
|
+
// server now, and one whose connect fails is left an ordinary server
|
|
31291
|
+
// that's down. Moved to another id, the old id's tab is no more.
|
|
31292
|
+
if (this.closedTabConfigs().has(id)) {
|
|
31293
|
+
if (config.id === id) this.hostPresentation?.noteConnected(id, true);
|
|
31294
|
+
else this.hostPresentation?.noteRemoved(id);
|
|
31295
|
+
this.closedTabConfigs().delete(id);
|
|
31296
|
+
}
|
|
30577
31297
|
try {
|
|
30578
31298
|
await this.runConnectMcplServer(config, claim);
|
|
30579
31299
|
} catch (error) {
|
|
@@ -30686,6 +31406,14 @@ export class AgentFramework {
|
|
|
30686
31406
|
return prefixesMeetError(config.id, prefix, otherId, other).message;
|
|
30687
31407
|
}
|
|
30688
31408
|
}
|
|
31409
|
+
// A closed tab keeps its prefix (and the prefixes it would meet) for the
|
|
31410
|
+
// connect its next open makes.
|
|
31411
|
+
for (const [otherId, other] of this.closedTabConfigs()) {
|
|
31412
|
+
if (otherId === config.id || otherId === leaving || this.mcplServerConfigs.has(otherId)) continue;
|
|
31413
|
+
const otherPrefix = other.toolPrefix ?? `mcpl--${otherId}`;
|
|
31414
|
+
if (otherPrefix === prefix) return `MCPL server "${config.id}" toolPrefix "${prefix}" collides with server "${otherId}" (a closed tab)`;
|
|
31415
|
+
if (prefixesMeet(prefix, otherPrefix)) return prefixesMeetError(config.id, prefix, otherId, otherPrefix).message;
|
|
31416
|
+
}
|
|
30689
31417
|
for (const [other, holder] of this.restartClaims().prefixes) {
|
|
30690
31418
|
if (holder !== claim && other !== prefix && prefixesMeet(prefix, other)) {
|
|
30691
31419
|
return `MCPL server "${config.id}" toolPrefix "${prefix}" meets the toolPrefix "${other}" the restart of ` +
|
|
@@ -30719,6 +31447,9 @@ export class AgentFramework {
|
|
|
30719
31447
|
listMcplServers(): Array<{
|
|
30720
31448
|
id: string;
|
|
30721
31449
|
connected: boolean;
|
|
31450
|
+
/** Kept disconnected as a closed tab (host-presentation.ts) until an
|
|
31451
|
+
* agent's toolsets--open, an operator's connect or a restart connects it. */
|
|
31452
|
+
closedTab: boolean;
|
|
30722
31453
|
retrying: boolean;
|
|
30723
31454
|
/** Which engine serves it: `legacy` (MCP 2024-11-05 + MCPL) or `modern`
|
|
30724
31455
|
* (MCP 2026-07-28), as its configuration selects; null when the
|
|
@@ -30776,7 +31507,7 @@ export class AgentFramework {
|
|
|
30776
31507
|
url?: string;
|
|
30777
31508
|
}> {
|
|
30778
31509
|
const result: Array<{
|
|
30779
|
-
id: string; connected: boolean; retrying: boolean;
|
|
31510
|
+
id: string; connected: boolean; closedTab: boolean; retrying: boolean;
|
|
30780
31511
|
family: 'legacy' | 'modern' | null; protocolVersion: string | null;
|
|
30781
31512
|
transport: 'stdio' | 'websocket' | 'http' | null; refused: string | null;
|
|
30782
31513
|
toolPrefix: string; toolCount: number; policyEstablished: boolean | null;
|
|
@@ -30791,7 +31522,10 @@ export class AgentFramework {
|
|
|
30791
31522
|
};
|
|
30792
31523
|
command?: string; url?: string;
|
|
30793
31524
|
}> = [];
|
|
30794
|
-
|
|
31525
|
+
// Closed tabs too: configured, kept disconnected until an agent opens one.
|
|
31526
|
+
const configured = [...this.mcplServerConfigs];
|
|
31527
|
+
for (const [id, config] of this.closedTabConfigs()) if (!this.mcplServerConfigs.has(id)) configured.push([id, config]);
|
|
31528
|
+
for (const [id, config] of configured) {
|
|
30795
31529
|
const prefix = config.toolPrefix ?? `mcpl--${id}`;
|
|
30796
31530
|
const modern = this.modernServers().get(id) ?? null;
|
|
30797
31531
|
const connection = modern ? null : this.mcplServerRegistry?.getServer(id) ?? null;
|
|
@@ -30814,6 +31548,7 @@ export class AgentFramework {
|
|
|
30814
31548
|
result.push({
|
|
30815
31549
|
id,
|
|
30816
31550
|
connected,
|
|
31551
|
+
closedTab: this.closedTabConfigs().has(id),
|
|
30817
31552
|
retrying: !connected && (modern?.willReconnect ?? connection?.willReconnect ?? false),
|
|
30818
31553
|
family: binding?.family ?? null,
|
|
30819
31554
|
protocolVersion: modern?.protocolVersion ?? connection?.protocolVersion ?? null,
|
|
@@ -31884,7 +32619,7 @@ export class AgentFramework {
|
|
|
31884
32619
|
});
|
|
31885
32620
|
|
|
31886
32621
|
if (newTools.length > 0) {
|
|
31887
|
-
const toolList = newTools.map(t => t.name).join(', ');
|
|
32622
|
+
const toolList = newTools.map(t => suppliedValue(t.name)).join(', ');
|
|
31888
32623
|
this.pushEvent({
|
|
31889
32624
|
type: 'external-message',
|
|
31890
32625
|
source: `mcpl:${serverId}`,
|
|
@@ -32488,7 +33223,7 @@ export class AgentFramework {
|
|
|
32488
33223
|
/**
|
|
32489
33224
|
* Dispatch a synthesized channel tool call.
|
|
32490
33225
|
*/
|
|
32491
|
-
private dispatchChannelToolCall(agentName: string, call: ToolCall): void {
|
|
33226
|
+
private dispatchChannelToolCall(agentName: string, call: ToolCall, earlierWake?: SkipReplyEarlierWake): void {
|
|
32492
33227
|
// Conversation forks act only on their home channel. channel_publish and
|
|
32493
33228
|
// channel_close default a missing channelId to home and reject foreign
|
|
32494
33229
|
// ones; channel_open is rejected outright — opening channels mutates
|
|
@@ -32515,7 +33250,7 @@ export class AgentFramework {
|
|
|
32515
33250
|
|
|
32516
33251
|
// The home quoted for a sentence (quotedShown), as the agent names it
|
|
32517
33252
|
// back, whose id-and-server form carries its own quotes.
|
|
32518
|
-
const homeQuoted = this.channelRegistry?.quotedShown(home) ??
|
|
33253
|
+
const homeQuoted = this.channelRegistry?.quotedShown(home) ?? quotedValue(home.channelId);
|
|
32519
33254
|
// A home its server no longer registers (a channels/changed update
|
|
32520
33255
|
// removed it, or this host removed its server; a dropped connection
|
|
32521
33256
|
// keeps the entry) takes nothing, and the fork has nowhere else to
|
|
@@ -32598,7 +33333,9 @@ export class AgentFramework {
|
|
|
32598
33333
|
const readThrough = publisher ? this.publishReadThrough(publisher, call.id) : null;
|
|
32599
33334
|
const published: { outcome?: PublishOutcome } = {};
|
|
32600
33335
|
|
|
32601
|
-
this.channelRegistry!.handleChannelToolCall(call.name, call.input, {
|
|
33336
|
+
this.channelRegistry!.handleChannelToolCall(call.name, call.input, {
|
|
33337
|
+
kind: 'agent', agentName, own: callingTurn ? this.ownChannels(callingTurn) : [], ...(earlierWake ? { earlierWake } : {}),
|
|
33338
|
+
}, published)
|
|
32602
33339
|
.then((result) => {
|
|
32603
33340
|
const destination = published.outcome?.status === 'delivered' ? published.outcome.destination : undefined;
|
|
32604
33341
|
if (publisher && destination) publisher.notePublished(destinationKey(destination), readThrough);
|
|
@@ -32647,13 +33384,15 @@ export class AgentFramework {
|
|
|
32647
33384
|
noteTurnChannel(callingTurn, opened);
|
|
32648
33385
|
this.lastAnnouncedLocus.set(agentName, opened);
|
|
32649
33386
|
const inboundOnlyServer = this.channelRegistry?.inboundOnlyServer(opened);
|
|
33387
|
+
// The routing word comes first, ahead of any backscroll, so a
|
|
33388
|
+
// result cut to its head for being long keeps it.
|
|
32650
33389
|
result = {
|
|
32651
33390
|
...result,
|
|
32652
33391
|
data: {
|
|
32653
|
-
...(result.data as Record<string, unknown> | undefined),
|
|
32654
33392
|
routing: inboundOnlyServer
|
|
32655
33393
|
? `${inboundOnlySpeechNote(inboundOnlyServer)} Other channels need an explicit send tool.`
|
|
32656
33394
|
: 'Your plain speech now lands in this channel. Other channels need an explicit send tool.',
|
|
33395
|
+
...(result.data as Record<string, unknown> | undefined),
|
|
32657
33396
|
},
|
|
32658
33397
|
};
|
|
32659
33398
|
console.error(
|
|
@@ -33005,7 +33744,7 @@ export class AgentFramework {
|
|
|
33005
33744
|
// answered for it.
|
|
33006
33745
|
const now = this.channelRegistry!.getTuneOutState(channel.serverId, channelId);
|
|
33007
33746
|
const error = now?.params.epochId === tuneOut.params.epochId
|
|
33008
|
-
? alreadyTunedOutText(
|
|
33747
|
+
? alreadyTunedOutText(this.channelRegistry!.quotedShown(channel), now.params,
|
|
33009
33748
|
openError ?? 'Its transport is open now, so its messages reach the subconscious.')
|
|
33010
33749
|
: TUNE_OUT_REPLACED;
|
|
33011
33750
|
this.pushEvent({
|
|
@@ -33145,6 +33884,8 @@ export class AgentFramework {
|
|
|
33145
33884
|
} else {
|
|
33146
33885
|
console.error(`[sleep] ${agentName}: no prose target this turn — sleep announcement not posted (explicit mode never guesses)`);
|
|
33147
33886
|
}
|
|
33887
|
+
} else if (!callingTurn.locus && callingTurn.heartbeat) {
|
|
33888
|
+
console.error(`[sleep] ${agentName}: a heartbeat turn has no channel — sleep announcement not posted`);
|
|
33148
33889
|
} else {
|
|
33149
33890
|
const locus = callingTurn.locus ?? null;
|
|
33150
33891
|
registry.routeSpeech(agentName, announcement(locus), locus).then(noted).catch((err) => {
|
|
@@ -33337,7 +34078,7 @@ export class AgentFramework {
|
|
|
33337
34078
|
if (Date.now() - startedAt > BARRIER_MS) {
|
|
33338
34079
|
throw new Error(
|
|
33339
34080
|
`save_recent_image: ${open.length} sibling tool call(s) in this batch still pending after ` +
|
|
33340
|
-
`${Math.round(BARRIER_MS / 1000)}s (${open.map((p) => `${p.name} (call ${p.id})`).join(', ')}) — their results ` +
|
|
34081
|
+
`${Math.round(BARRIER_MS / 1000)}s (${open.map((p) => `${suppliedValue(p.name)} (call ${p.id})`).join(', ')}) — their results ` +
|
|
33341
34082
|
'may carry newer images than anything in your context, so no index can be trusted yet. Nothing written. ' +
|
|
33342
34083
|
'Save again once they have finished.',
|
|
33343
34084
|
);
|
|
@@ -33413,7 +34154,7 @@ export class AgentFramework {
|
|
|
33413
34154
|
const ledger = this.toolImageLedgerFor(agentName);
|
|
33414
34155
|
|
|
33415
34156
|
const describeTool = (c: { toolName: string; toolCallId: string }): string =>
|
|
33416
|
-
`${c.toolName} (call ${c.toolCallId})`;
|
|
34157
|
+
`${suppliedValue(c.toolName)} (call ${c.toolCallId})`;
|
|
33417
34158
|
const unavailable = (status: 'evicted' | 'unknown'): string =>
|
|
33418
34159
|
status === 'evicted'
|
|
33419
34160
|
? 'its bytes were evicted from the bounded retention budget'
|
|
@@ -33634,9 +34375,10 @@ export class AgentFramework {
|
|
|
33634
34375
|
for (let i = messages.length - 1; i >= 0 && collecting(); i--) {
|
|
33635
34376
|
const content = messages[i]?.content;
|
|
33636
34377
|
if (!Array.isArray(content)) continue;
|
|
33637
|
-
//
|
|
33638
|
-
//
|
|
33639
|
-
|
|
34378
|
+
// A row the agent's own view leaves out, such as another agent's
|
|
34379
|
+
// stored context injection (stored-injections.ts), isn't in its
|
|
34380
|
+
// window, so its images aren't either.
|
|
34381
|
+
if (!this.agentSees(agent, messages[i]?.metadata)) continue;
|
|
33640
34382
|
const messagesBack = total - (start + i);
|
|
33641
34383
|
for (let b = content.length - 1; b >= 0 && collecting(); b--) {
|
|
33642
34384
|
const block = content[b] as {
|
|
@@ -33721,7 +34463,8 @@ export class AgentFramework {
|
|
|
33721
34463
|
// error says that whether the entry changed isn't known
|
|
33722
34464
|
// (writeBinary); the images before it are saved, and the
|
|
33723
34465
|
// failure says so, as a returned one does.
|
|
33724
|
-
failed
|
|
34466
|
+
console.error(`[save_recent_image] writing "${destination}" failed:`, error);
|
|
34467
|
+
failed(thrownWords(error));
|
|
33725
34468
|
return;
|
|
33726
34469
|
}
|
|
33727
34470
|
const inChronicle = (result.data as { inChronicle?: boolean } | undefined)?.inChronicle === true;
|
|
@@ -33758,116 +34501,6 @@ export class AgentFramework {
|
|
|
33758
34501
|
})();
|
|
33759
34502
|
}
|
|
33760
34503
|
|
|
33761
|
-
/** The `utils` meta-tool: list/describe/run over module utilities
|
|
33762
|
-
* (Module.getUtilities). Shared by the model-facing dispatcher, which a
|
|
33763
|
-
* running code_execution script's calls take too, and executeToolCallFrom,
|
|
33764
|
-
* which the puppet and a host reach through executeToolCall and a module
|
|
33765
|
-
* through callTool. */
|
|
33766
|
-
private async handleUtilsToolCall(agentName: string, call: ToolCall, options: { inProcess?: boolean; trustedModule?: boolean } = {}): Promise<ToolResult> {
|
|
33767
|
-
const input = (call.input ?? {}) as { action?: unknown; name?: unknown; args?: unknown };
|
|
33768
|
-
const allUtilities = this.moduleRegistry.getAllUtilities();
|
|
33769
|
-
const utilities = allUtilities.filter(def => options.trustedModule || this.canAgentUseTool(agentName, def.name));
|
|
33770
|
-
const counts = `${registeredUtilities(allUtilities.length)}, ${utilities.length} permitted`;
|
|
33771
|
-
const available = (): string => utilities.length
|
|
33772
|
-
? `Available: ${utilities.map((u) => u.name).join(', ')} (${counts})`
|
|
33773
|
-
: allUtilities.length
|
|
33774
|
-
? `${counts}; none permitted by this agent's allowedTools. Add each required module--utility name to allowedTools.`
|
|
33775
|
-
: `${counts} (none registered)`;
|
|
33776
|
-
switch (input.action) {
|
|
33777
|
-
case 'list':
|
|
33778
|
-
if (allUtilities.length > 0 && utilities.length === 0) {
|
|
33779
|
-
return { success: false, isError: true, error: available() };
|
|
33780
|
-
}
|
|
33781
|
-
return {
|
|
33782
|
-
success: true,
|
|
33783
|
-
data: utilities.map((u) => ({ name: u.name, description: utilityOneLiner(u.description) })),
|
|
33784
|
-
};
|
|
33785
|
-
case 'describe': {
|
|
33786
|
-
const def = allUtilities.find((u) => u.name === input.name);
|
|
33787
|
-
if (!def) {
|
|
33788
|
-
return { success: false, isError: true, error: `No utility "${String(input.name)}". ${available()}` };
|
|
33789
|
-
}
|
|
33790
|
-
if (!options.trustedModule) {
|
|
33791
|
-
const refusal = this.agentToolPermissionFailure(agentName, { ...call, name: def.name }, false);
|
|
33792
|
-
if (refusal) return refusal;
|
|
33793
|
-
}
|
|
33794
|
-
return { success: true, data: def };
|
|
33795
|
-
}
|
|
33796
|
-
case 'run': {
|
|
33797
|
-
const def = allUtilities.find((u) => u.name === input.name);
|
|
33798
|
-
if (!def) {
|
|
33799
|
-
return { success: false, isError: true, error: `No utility "${String(input.name)}", so nothing was done. ${available()}` };
|
|
33800
|
-
}
|
|
33801
|
-
if (!options.trustedModule) {
|
|
33802
|
-
// The allowed outer utils call owns its lifecycle and terminal trace;
|
|
33803
|
-
// refusing the utility does not create or refuse a separate call.
|
|
33804
|
-
const refusal = this.agentToolPermissionFailure(agentName, { ...call, name: def.name }, false);
|
|
33805
|
-
if (refusal) return refusal;
|
|
33806
|
-
}
|
|
33807
|
-
const args = input.args ?? {};
|
|
33808
|
-
const problems = toolArgsProblems(args, def.inputSchema, { values: true, field: 'args' });
|
|
33809
|
-
if (problems.length > 0) {
|
|
33810
|
-
// Teach by bounce: the error carries the schema, so the retry needs
|
|
33811
|
-
// no describe round-trip.
|
|
33812
|
-
return {
|
|
33813
|
-
success: false,
|
|
33814
|
-
isError: true,
|
|
33815
|
-
error: `Nothing was done: ${def.name} ${problems.join('; ')}. Schema for ${def.name}: ${JSON.stringify(def.inputSchema)}`,
|
|
33816
|
-
};
|
|
33817
|
-
}
|
|
33818
|
-
// Same dispatch as a first-class tool call — the module cannot tell
|
|
33819
|
-
// which surface the call came through. The registry checks the call
|
|
33820
|
-
// against the utility resolved here, even where one of the module's
|
|
33821
|
-
// tools shares its name. An in-process caller gets the module's own
|
|
33822
|
-
// result; the model's path takes the result's stored form.
|
|
33823
|
-
//
|
|
33824
|
-
// The result goes where the utils call's does. On the model's path it
|
|
33825
|
-
// is stored or handed to a script as that call's result, so it may be
|
|
33826
|
-
// as long as that call's may (resultMaxCharsFor). An in-process caller
|
|
33827
|
-
// gets it back, and the utils call carries what that caller may take,
|
|
33828
|
-
// if anything does: the puppet's carries the agent's cap, since the
|
|
33829
|
-
// puppet stores the pair under it.
|
|
33830
|
-
const resultMaxChars = options.inProcess ? call.resultMaxChars : this.resultMaxCharsFor(agentName, call.id);
|
|
33831
|
-
const utilityCall: ToolCall = {
|
|
33832
|
-
id: call.id, name: def.name, input: args, callerAgentName: agentName,
|
|
33833
|
-
...(call.callerTurnId !== undefined ? { callerTurnId: call.callerTurnId } : {}),
|
|
33834
|
-
...(resultMaxChars !== undefined ? { resultMaxChars } : {}),
|
|
33835
|
-
};
|
|
33836
|
-
return options.inProcess
|
|
33837
|
-
? this.moduleRegistry.handleToolCall(utilityCall, { definition: def })
|
|
33838
|
-
: this.moduleRegistry.storableToolCall(utilityCall, { definition: def });
|
|
33839
|
-
}
|
|
33840
|
-
default:
|
|
33841
|
-
return { success: false, isError: true, error: 'action must be "list", "describe", or "run"' };
|
|
33842
|
-
}
|
|
33843
|
-
}
|
|
33844
|
-
|
|
33845
|
-
private dispatchUtilsToolCall(agentName: string, call: ToolCall): void {
|
|
33846
|
-
this.emitTrace({ type: 'tool:started', module: 'framework', tool: 'utils', callId: call.id, input: call.input });
|
|
33847
|
-
const startedAt = Date.now();
|
|
33848
|
-
void this.handleUtilsToolCall(agentName, call)
|
|
33849
|
-
.catch((err): ToolResult => ({
|
|
33850
|
-
success: false,
|
|
33851
|
-
error: err instanceof Error ? err.message : String(err),
|
|
33852
|
-
isError: true,
|
|
33853
|
-
}))
|
|
33854
|
-
.then((result) => {
|
|
33855
|
-
if (result.success) {
|
|
33856
|
-
this.emitTrace({ type: 'tool:completed', module: 'framework', tool: 'utils', callId: call.id, durationMs: Date.now() - startedAt });
|
|
33857
|
-
} else {
|
|
33858
|
-
this.emitTrace({ type: 'tool:failed', module: 'framework', tool: 'utils', callId: call.id, error: result.error ?? 'unknown error' });
|
|
33859
|
-
}
|
|
33860
|
-
this.pushEvent({
|
|
33861
|
-
type: 'tool-result',
|
|
33862
|
-
callId: call.id,
|
|
33863
|
-
toolName: call.name,
|
|
33864
|
-
agentName,
|
|
33865
|
-
moduleName: 'framework',
|
|
33866
|
-
result,
|
|
33867
|
-
});
|
|
33868
|
-
});
|
|
33869
|
-
}
|
|
33870
|
-
|
|
33871
34504
|
private readAgentSettings(agentName: string, touched: ReadonlyMap<AgentSettingsExtension, readonly string[]>): SettingsReading {
|
|
33872
34505
|
const s = this.getAgentRuntimeSettings(agentName);
|
|
33873
34506
|
const saved = this.agents.get(agentName)?.getRuntimeSettingsOverrides() ?? {};
|
|
@@ -34203,3 +34836,10 @@ export class AgentFramework {
|
|
|
34203
34836
|
};
|
|
34204
34837
|
}
|
|
34205
34838
|
}
|
|
34839
|
+
|
|
34840
|
+
function thrownWords(error: unknown): string {
|
|
34841
|
+
if (error instanceof Refusal) return error.message;
|
|
34842
|
+
const code = errnoCode(error);
|
|
34843
|
+
const on = errnoWayOn(code, "Calling again may work; if it fails the same way, whoever runs this host can find the whole error in this host's log.");
|
|
34844
|
+
return `${systemWords(code, error).replace(/\.$/, '')}. ${on}`;
|
|
34845
|
+
}
|