@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/dist/src/framework.js
CHANGED
|
@@ -32,20 +32,23 @@ import { checkToolPattern, describeUnmatchedPattern } from './mcpl/tool-pattern-
|
|
|
32
32
|
import { BUILTIN_TOOL_CLASSES, normalizeClassTable, parseDeclaredClasses, resolveToolClass } from './mcpl/tool-classes.js';
|
|
33
33
|
import { PushHandler } from './mcpl/push-handler.js';
|
|
34
34
|
import { readSkipReplyWake, skipReplyWakeRefusal } from './mcpl/skip-reply-wake.js';
|
|
35
|
-
import {
|
|
35
|
+
import { convertBlocks } from './mcpl/convert-block.js';
|
|
36
36
|
import { PushCoalescer, CoalesceError, PUSH_COALESCING_SUPPORT, validateCoalesceMember, validateCoalescedContent, coalescingSubjectKey, } from './mcpl/push-coalescer.js';
|
|
37
37
|
import { isSilentHeartbeatMarker, isVisiblyEmptyContent, NON_STRING_TEXT_PLACEHOLDER } from './mcpl/visible-content.js';
|
|
38
38
|
import { isProseTargetToken, parseProsePrefix, parseHybridProsePrefix } from './mcpl/prose-grammar.js';
|
|
39
39
|
import { detectKnownToolWrapperProse } from './tool-wrapper-prose-guard.js';
|
|
40
40
|
import { InferenceRouter } from './mcpl/inference-router.js';
|
|
41
|
-
import { ChannelRegistry, TUNE_OUT_REPLACED, connectorText, desiredStateInLog, destinationKey, journalToolDescription, NON_HOLDER_CHANNEL_DESCRIPTIONS, qualifiedChannelName, sameChannel } from './mcpl/channel-registry.js';
|
|
41
|
+
import { ChannelRegistry, TUNE_OUT_REPLACED, connectorText, desiredStateInLog, destinationKey, journalToolDescription, NON_HOLDER_CHANNEL_DESCRIPTIONS, NO_CHANNEL_EXAMPLE, NO_PERSON_EXAMPLE, qualifiedChannelName, sameChannel } from './mcpl/channel-registry.js';
|
|
42
42
|
import { KEEP_IDLE_MS, KEEP_NEWEST, ProseDraftStore, draftState, uncertainAttempt } from './prose-drafts.js';
|
|
43
43
|
import { ConversationRouter, CONVERSATION_ROUTING_DEPRECATION_NOTICE } from './mcpl/conversation-router.js';
|
|
44
44
|
import { INBOUND_SOURCE_KEY, sourceBodyDigest } from './mcpl/inbound-source.js';
|
|
45
|
+
import { markNoticeOpenings, nameToTypeBack, quotedValue, showsAsItself, suppliedValue } from './supplied-text.js';
|
|
46
|
+
import { NOTICE_TAG } from './notice-tags.js';
|
|
45
47
|
import { safeSlice } from './safe-slice.js';
|
|
46
48
|
import { SEND_TOOLS, bareToolName } from './send-tools.js';
|
|
47
49
|
import { boundedErrorMessage, errorMessage } from './error-message.js';
|
|
48
50
|
import { ProviderWaits, EVERY_MODEL, isReleaseModel, withoutControls } from './provider-waits.js';
|
|
51
|
+
import { Refusal, errnoCode, errnoWayOn, systemWords } from './modules/workspace/answers.js';
|
|
49
52
|
import { goesNative, nonErrorResultText, nonErrorResultTextOrThrow, isContentArray, resultClaimsOrThrow, scriptViewClaims, shownToScript, shownToScriptFromServer, toolResultDataToHistoryString, truncateForHistory, DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS, } from './tool-result-history.js';
|
|
50
53
|
import { TOOL_IMAGE_REF_RE, ToolImageLedger, cutStoredToolResultToFit, formatKeptImageSlots, parseImagePlaceholders, sha256Hex, storedImageSlots, toolResultCutAt, toolResultShare, } from './tool-image-ledger.js';
|
|
51
54
|
import { randomUUID, createHash } from 'node:crypto';
|
|
@@ -278,6 +281,11 @@ const HOST_METADATA_KEYS = [
|
|
|
278
281
|
* marker too. A marker that wakes the agent can still set that turn's speech
|
|
279
282
|
* channel (agent-framework#189); a `chat:system` notice can't (systemNotice).
|
|
280
283
|
*/
|
|
284
|
+
const isOwnScheduleWrite = (metadata) => {
|
|
285
|
+
const m = metadata;
|
|
286
|
+
return m?.serverId === 'heartbeat' || m?.source === 'gate:self-wake'
|
|
287
|
+
|| (Array.isArray(m?.tags) && m.tags.includes('script:wake'));
|
|
288
|
+
};
|
|
281
289
|
const isConversationalInjection = (metadata) => {
|
|
282
290
|
if (!metadata)
|
|
283
291
|
return true;
|
|
@@ -517,18 +525,30 @@ const ambientMark = (tags) => Array.isArray(tags) && tags.includes('chat:ambient
|
|
|
517
525
|
* something anyone said there, so it never picks where the turn's plain
|
|
518
526
|
* speech goes. */
|
|
519
527
|
const systemNoticeMark = (tags) => isSystemNotice(tags) ? { systemNotice: true } : {};
|
|
528
|
+
/**
|
|
529
|
+
* A wake from the heartbeat: a tick or a reminder, from the framework's own
|
|
530
|
+
* heartbeat or from an MCPL server whose id is `heartbeat` (heartbeat-mcpl),
|
|
531
|
+
* which carries no channel. A silent tick isn't one here: its turn keeps
|
|
532
|
+
* every plain word private already (suppressProse). Its turn's plain speech
|
|
533
|
+
* has no destination (TurnRouting.heartbeat).
|
|
534
|
+
*/
|
|
535
|
+
function isHeartbeatWake(trigger) {
|
|
536
|
+
if (trigger === undefined || trigger.channelId || trigger.suppressProse === true)
|
|
537
|
+
return false;
|
|
538
|
+
return trigger.heartbeatTurn ?? trigger.source === 'heartbeat';
|
|
539
|
+
}
|
|
520
540
|
// parseProsePrefix lives in ./mcpl/prose-grammar.ts. The outgoing stream
|
|
521
541
|
// (Spec 14.3) needs no grammar of its own: it carries what delivery
|
|
522
542
|
// published.
|
|
523
543
|
/** One-time primer appended when an agent's proseRouting mode changes. */
|
|
524
544
|
function proseModePrimer(mode, helpAvailable) {
|
|
525
545
|
if (mode === 'locus') {
|
|
526
|
-
return
|
|
546
|
+
return `[${NOTICE_TAG.proseRouting}] Mode change: your plain text auto-routes to the ` +
|
|
527
547
|
'conversational locus again, unless its server declared that channel inbound-only. ' +
|
|
528
548
|
'`>>` destination prefixes are no longer needed.';
|
|
529
549
|
}
|
|
530
550
|
if (mode === 'hybrid') {
|
|
531
|
-
return
|
|
551
|
+
return `[${NOTICE_TAG.proseRouting}] Hybrid routing enabled: unprefixed text still lands in the ` +
|
|
532
552
|
'current conversational locus, unless its server declared that channel inbound-only. ' +
|
|
533
553
|
'A leading `>>>destination` envelope instead routes ' +
|
|
534
554
|
'that prose to one uniquely resolved authorized Discord or Eidoverse channel. The ' +
|
|
@@ -544,7 +564,7 @@ function proseModePrimer(mode, helpAvailable) {
|
|
|
544
564
|
// content — the safest role there is). Nothing is injected into the system
|
|
545
565
|
// prompt: those bytes belong to the recipe, and the deepest KV prefix must
|
|
546
566
|
// not vary with framework code.
|
|
547
|
-
return (
|
|
567
|
+
return (`[${NOTICE_TAG.proseRouting}] Output mode changed: plain text is delivered only with a ` +
|
|
548
568
|
'destination prefix line, e.g. ">>#channel-name your text".' +
|
|
549
569
|
(helpAvailable ? ' The prose_help tool shows the full syntax.' : ''));
|
|
550
570
|
}
|
|
@@ -579,27 +599,28 @@ const PROSE_HELP_TOOL = {
|
|
|
579
599
|
description: 'Show the plain-text output routing syntax (destination prefixes, resend token, skip).',
|
|
580
600
|
inputSchema: { type: 'object', properties: {} },
|
|
581
601
|
};
|
|
582
|
-
/**
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
602
|
+
/** What a call of the removed `utils` tool is answered: where its utilities went. */
|
|
603
|
+
const UTILS_REMOVED = `utils was removed: module utilities are in the host's built-in toolset now. ` +
|
|
604
|
+
`${META_TOOL_NAMES.look} {toolset:"${BUILTIN_TOOLSET}"} lists them, ${META_TOOL_NAMES.open} {toolset:"${BUILTIN_TOOLSET}"} opens it, ` +
|
|
605
|
+
`and ${META_TOOL_NAMES.act} {tool, args} calls one. Nothing was run.`;
|
|
606
|
+
/**
|
|
607
|
+
* The framework's own tools that belong to the host's built-in toolset
|
|
608
|
+
* ("settings", host-presentation.ts) rather than the tool list: the ones an
|
|
609
|
+
* agent reaches for rarely. Modules place theirs with Module.toolPlacement,
|
|
610
|
+
* and every module utility is there too.
|
|
611
|
+
*/
|
|
612
|
+
export const BUILTIN_TOOLSET_TOOLS = new Set([
|
|
613
|
+
'agent_settings',
|
|
614
|
+
'wake_add_rule',
|
|
615
|
+
'wake_remove_rule',
|
|
616
|
+
'gate_status',
|
|
617
|
+
'event_tags',
|
|
618
|
+
'set_tool_visibility',
|
|
619
|
+
'set_tool_description',
|
|
620
|
+
'heartbeat--heartbeat_status',
|
|
621
|
+
'heartbeat--heartbeat_trigger',
|
|
622
|
+
'workspace--status',
|
|
623
|
+
]);
|
|
603
624
|
const readExtensionSettings = (ext, agentName) => {
|
|
604
625
|
const values = ext.get(agentName);
|
|
605
626
|
return Object.fromEntries(Object.entries(values).map(([key, value]) => [key, value === undefined ? 'undefined' : JSON.stringify(value)]));
|
|
@@ -614,6 +635,19 @@ const settingNumber = (name, value) => {
|
|
|
614
635
|
return n;
|
|
615
636
|
throw new Error(`${name} must be a number, not ${shownValue(value)}`);
|
|
616
637
|
};
|
|
638
|
+
const DEFAULT_TURN_REARM_MS = [2 * 60_000, 8 * 60_000, 32 * 60_000];
|
|
639
|
+
const turnRearmDelays = (value) => {
|
|
640
|
+
if (value === undefined)
|
|
641
|
+
return DEFAULT_TURN_REARM_MS;
|
|
642
|
+
if (value === false)
|
|
643
|
+
return [];
|
|
644
|
+
if (!Array.isArray(value) || value.length === 0
|
|
645
|
+
|| ![...value].every((ms) => typeof ms === 'number' && Number.isFinite(ms) && ms > 0)) {
|
|
646
|
+
throw new Error('FrameworkConfig.exhaustedTurnRearmMs must be false, or a list of one or more delays in ms, ' +
|
|
647
|
+
`each a number above 0, not ${shownValue(value)}`);
|
|
648
|
+
}
|
|
649
|
+
return [...value];
|
|
650
|
+
};
|
|
617
651
|
const settingBoolean = (name, value) => {
|
|
618
652
|
if (typeof value === 'boolean')
|
|
619
653
|
return value;
|
|
@@ -621,22 +655,17 @@ const settingBoolean = (name, value) => {
|
|
|
621
655
|
return value === 'true';
|
|
622
656
|
throw new Error(`${name} must be true or false, not ${shownValue(value)}`);
|
|
623
657
|
};
|
|
624
|
-
/** First sentence (or line) of a description — what `utils list` shows. */
|
|
625
|
-
const utilityOneLiner = (description) => {
|
|
626
|
-
const line = description.split('\n')[0];
|
|
627
|
-
const dot = line.indexOf('. ');
|
|
628
|
-
return safeSlice(dot >= 0 ? line.slice(0, dot + 1) : line, 0, 160);
|
|
629
|
-
};
|
|
630
658
|
import { CheckpointManager } from './mcpl/checkpoint-manager.js';
|
|
631
659
|
import { isToolAllowed } from './mcpl/tool-policy.js';
|
|
632
|
-
import { asCalled, cutOffRefusal, markedInputRefusal
|
|
660
|
+
import { asCalled, cutOffRefusal, markedInputRefusal } from './tool-args.js';
|
|
633
661
|
import { cutOffRound } from './cut-off-round.js';
|
|
634
|
-
import { EventGate, checkToolInput, formatShadowWarning, shownValue, wakeLineText } from './gate/event-gate.js';
|
|
662
|
+
import { EventGate, MCPL_GATE_EVENT_TYPES, checkToolInput, formatShadowWarning, shownValue, wakeLineText } from './gate/event-gate.js';
|
|
635
663
|
import { ReplyJudge } from './gate/reply-judge.js';
|
|
636
664
|
import { HEARTBEAT_CHANGING_TOOLS, Heartbeat, isHeartbeatMcplServer } from './heartbeat/heartbeat.js';
|
|
637
665
|
import { EPHEMERAL_USAGE_NAME, UsageTracker, persistedUsageProblem } from './usage/usage-tracker.js';
|
|
638
666
|
import { McplProtocolVersionError, McplRequestError, McplUnreapedLaunchError, stderrTailOf, toolsetDeclarationsOf, withStderr } from './mcpl/server-connection.js';
|
|
639
667
|
import { ToolsetHub, TOOLSETS_MODULE, META_TOOL_NAMES } from './mcpl/toolset-host.js';
|
|
668
|
+
import { HostPresentation, BUILTIN_TOOLSET } from './host-presentation.js';
|
|
640
669
|
import { isEmptyChange } from './mcpl/tool-presence.js';
|
|
641
670
|
import { ToolsetsModule, resolveAct, withLookHint } from './mcpl/toolsets-module.js';
|
|
642
671
|
import { ModernMcpConnection } from './mcpl/modern-connection.js';
|
|
@@ -797,7 +826,29 @@ const TURN_RETRY = Symbol('turn-retry');
|
|
|
797
826
|
* record. A symbol, as TURN_RETRY is, so spreads and rests copy it.
|
|
798
827
|
*/
|
|
799
828
|
const MERGED_BATCH = Symbol('merged-batch');
|
|
829
|
+
const REARMED = Symbol('rearmed');
|
|
800
830
|
const TURN_CARRY = Symbol('turn-carry');
|
|
831
|
+
function isRearmed(request) {
|
|
832
|
+
return request !== undefined && request[REARMED] === true;
|
|
833
|
+
}
|
|
834
|
+
function waitAlertsOnItsOwn(wait) {
|
|
835
|
+
return wait.until === null || wait.until - wait.setAt > PROVIDER_WAIT_ALERT_MS;
|
|
836
|
+
}
|
|
837
|
+
// membrane 0.8.0 marks a deadline it reports as run out with this registered
|
|
838
|
+
// symbol (on a sent request, or the Anthropic SDK's timeout, which covers its
|
|
839
|
+
// connect timeout too), and doesn't export its reader.
|
|
840
|
+
const MEMBRANE_DEADLINE_EXPIRY = Symbol.for('membrane.deadlineExpiry');
|
|
841
|
+
function isTransientFailure(error) {
|
|
842
|
+
if (!isMembraneError(error) || error.retryable !== true)
|
|
843
|
+
return false;
|
|
844
|
+
if (error[MEMBRANE_DEADLINE_EXPIRY] === true)
|
|
845
|
+
return false;
|
|
846
|
+
return error.type === 'server' || error.type === 'network' || error.type === 'timeout' || error.type === 'rate_limit';
|
|
847
|
+
}
|
|
848
|
+
function gotPartway(routing) {
|
|
849
|
+
return routing.roundsRan === true || (routing.unflushedRounds ?? 0) > 0;
|
|
850
|
+
}
|
|
851
|
+
const REARM_REASON = 'exhausted-turn-rearm';
|
|
801
852
|
function mergedBatch(request) {
|
|
802
853
|
return request[MERGED_BATCH];
|
|
803
854
|
}
|
|
@@ -953,7 +1004,7 @@ function ephemeralRunStopped(name, observed) {
|
|
|
953
1004
|
}
|
|
954
1005
|
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.";
|
|
955
1006
|
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.';
|
|
956
|
-
const LIVE_SPEECH_UNRECEIPTED =
|
|
1007
|
+
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.`;
|
|
957
1008
|
/** The reason on the inference:aborted that ends a turn whose retry stop()
|
|
958
1009
|
* cancelled, whichever frame was waiting to retry: beginAgentTurn's, when
|
|
959
1010
|
* the stream couldn't start, or driveStream's, when it failed once started;
|
|
@@ -1475,7 +1526,9 @@ const WAITING_TRACE_EVERY_MS = 60_000;
|
|
|
1475
1526
|
/** A stated wait recorded to hold a model longer than this, or until
|
|
1476
1527
|
* released, raises an ops alert. It is honoured in full either way: a hold
|
|
1477
1528
|
* that long is a quota-scale event or a provider's mistake, and a person
|
|
1478
|
-
* decides which (release-provider-wait). Shorter waits are pacing
|
|
1529
|
+
* decides which (release-provider-wait). Shorter waits are pacing, until
|
|
1530
|
+
* waits in a row hold one turn longer than this (noteStatedWaitRun,
|
|
1531
|
+
* endStatedWaitRun). */
|
|
1479
1532
|
const PROVIDER_WAIT_ALERT_MS = 60 * 60_000;
|
|
1480
1533
|
/**
|
|
1481
1534
|
* A MembraneError, from this framework's copy of membrane or from another: the
|
|
@@ -1816,9 +1869,13 @@ function failureDescription(error) {
|
|
|
1816
1869
|
return `${head}: ${String(error.message)}`;
|
|
1817
1870
|
}
|
|
1818
1871
|
catch {
|
|
1819
|
-
return
|
|
1872
|
+
return UNDESCRIBED_FAILURE;
|
|
1820
1873
|
}
|
|
1821
1874
|
}
|
|
1875
|
+
const UNDESCRIBED_FAILURE = `[${NOTICE_TAG.undescribedFailure}]`;
|
|
1876
|
+
function failureShown(text) {
|
|
1877
|
+
return text === UNDESCRIBED_FAILURE ? text : suppliedValue(text);
|
|
1878
|
+
}
|
|
1822
1879
|
/**
|
|
1823
1880
|
* A bounded string describing a failure, for logs that would otherwise print
|
|
1824
1881
|
* the error object; it is one line unless one of its parts has newlines, as a
|
|
@@ -2090,6 +2147,17 @@ const JUDGE_PARAMETER = {
|
|
|
2090
2147
|
},
|
|
2091
2148
|
required: ['debounceMs'],
|
|
2092
2149
|
};
|
|
2150
|
+
/** Whether a server's configuration resolves to the MCPL (legacy) family,
|
|
2151
|
+
* the one that can negotiate RFC-014 tool hints. A configuration that names
|
|
2152
|
+
* no usable transport is refused elsewhere; here it has no hints. */
|
|
2153
|
+
function isLegacyFamily(config) {
|
|
2154
|
+
try {
|
|
2155
|
+
return resolveServerBinding(config).family === 'legacy';
|
|
2156
|
+
}
|
|
2157
|
+
catch {
|
|
2158
|
+
return false;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2093
2161
|
export class AgentFramework {
|
|
2094
2162
|
toolPresentations = new Map();
|
|
2095
2163
|
presentationPreviews = new WeakMap();
|
|
@@ -2332,8 +2400,6 @@ export class AgentFramework {
|
|
|
2332
2400
|
* by the object, not the run's record (ephemeralRuns), which its cleanup
|
|
2333
2401
|
* drops before a cancelled stream may count its last usage. */
|
|
2334
2402
|
ephemeralAgents = new WeakSet();
|
|
2335
|
-
/** Warn only once per Agent instance after utilities are registered. */
|
|
2336
|
-
emptyUtilityPermissionWarnings = new WeakSet();
|
|
2337
2403
|
/** Physical ephemeral frames deliberately disposed by their run watchdog/caller.
|
|
2338
2404
|
* The name is single-generation, so these frames still own their terminal
|
|
2339
2405
|
* typing/outgoing close even after runEphemeralToCompletion deregisters them. */
|
|
@@ -2342,6 +2408,10 @@ export class AgentFramework {
|
|
|
2342
2408
|
* completes (inference:completed), not when a call answers mid-turn.
|
|
2343
2409
|
* Drives hard-down escalation — see noteInferenceExhausted. */
|
|
2344
2410
|
consecutiveInferenceFailures = new Map();
|
|
2411
|
+
turnRearmMs = DEFAULT_TURN_REARM_MS;
|
|
2412
|
+
turnRearms = new Map();
|
|
2413
|
+
exhaustingRearms = new Set();
|
|
2414
|
+
statedWaitRuns = new Map();
|
|
2345
2415
|
/** N consecutive failed inferences ⇒ the agent is treated as hard-down and
|
|
2346
2416
|
* escalated loudly to stderr. */
|
|
2347
2417
|
inferenceFailureEscalationThreshold = 3;
|
|
@@ -2586,15 +2656,16 @@ export class AgentFramework {
|
|
|
2586
2656
|
releasesAfterResume = new Map();
|
|
2587
2657
|
managerReleases = new WeakMap();
|
|
2588
2658
|
releasedByCleanup = new WeakSet();
|
|
2589
|
-
// context-manager 0.23.
|
|
2590
|
-
// AutobiographicalStrategy does (hostContext, autobiographical.ts:
|
|
2659
|
+
// context-manager 0.23.1: a strategy that keeps its context, as
|
|
2660
|
+
// AutobiographicalStrategy does (hostContext, autobiographical.ts:2897),
|
|
2591
2661
|
// keeps its manager reachable while its work is pending, since the
|
|
2592
2662
|
// context's tools and systemPrompt getters close over the manager
|
|
2593
|
-
// (context-manager.ts:
|
|
2663
|
+
// (context-manager.ts:2343-2359). So such a released manager is collected
|
|
2594
2664
|
// only once nothing of its work can still read the store, and stop()
|
|
2595
2665
|
// closes, before an owned store, each one alive.
|
|
2596
2666
|
openManagers = new Set();
|
|
2597
2667
|
openManagerRefs = new WeakMap();
|
|
2668
|
+
storeSees = new WeakMap();
|
|
2598
2669
|
collectedManagers = new FinalizationRegistry((ref) => this.openManagers.delete(ref));
|
|
2599
2670
|
/** The coalescing commit in flight, if any (see commitCoalescingDurable). */
|
|
2600
2671
|
coalescingCommit = null;
|
|
@@ -2732,10 +2803,15 @@ export class AgentFramework {
|
|
|
2732
2803
|
/** Host capabilities advertised during the MCP handshake — stored so servers
|
|
2733
2804
|
* can be connected at runtime (connectMcplServer) after initialization. */
|
|
2734
2805
|
mcplHostCapabilities = null;
|
|
2735
|
-
/** MCPL RFC-014: every
|
|
2806
|
+
/** MCPL RFC-014: every hinted server's tool presence, for the
|
|
2736
2807
|
* `toolsets` module's meta-tools and reminders. Made when the first
|
|
2737
|
-
* server configured
|
|
2808
|
+
* server with tool hints (any not configured `toolHints: false`) is set
|
|
2809
|
+
* up; null until then. */
|
|
2738
2810
|
toolsetHub = null;
|
|
2811
|
+
/** The host's own toolsets: the built-in "settings" and MCPL server tabs. */
|
|
2812
|
+
hostPresentation = null;
|
|
2813
|
+
/** MCPL servers kept disconnected as closed tabs, with the configuration an open connects them with. */
|
|
2814
|
+
closedTabConfigMap;
|
|
2739
2815
|
/** Inference routing policy from FrameworkConfig — stored so the MCPL
|
|
2740
2816
|
* subsystem can be lazily initialized by connectMcplServer when the
|
|
2741
2817
|
* framework started with zero configured servers. */
|
|
@@ -2996,6 +3072,7 @@ export class AgentFramework {
|
|
|
2996
3072
|
}
|
|
2997
3073
|
const framework = building.framework = new AgentFramework(store, ownsStore, config.membrane, config.inferencePolicy ?? new DefaultInferencePolicy(), config.errorPolicy ?? new DefaultErrorPolicy(), config.syncIntervalMs ?? DEFAULT_SYNC_INTERVAL_MS, config.maintenanceIntervalMs ?? DEFAULT_MAINTENANCE_INTERVAL_MS, processLoggingPersist, resolveTimeZone(config.timeZone), discordAwarenessOutbox, config.discordAwarenessEmoji ?? DEFAULT_DISCORD_AWARENESS_EMOJI, normalizeDiscordAwarenessDeadline(config.discordAwarenessDeadlineMs), new OperatorLog(operatorLogPath));
|
|
2998
3074
|
framework.providerHoldHook = config.providerHold;
|
|
3075
|
+
framework.turnRearmMs = turnRearmDelays(config.exhaustedTurnRearmMs);
|
|
2999
3076
|
// Before any delivery.
|
|
3000
3077
|
await framework.reconcileDiscordAwarenessAtStartup();
|
|
3001
3078
|
// Restore persisted usage data (if any) from prior session. The names it
|
|
@@ -3025,9 +3102,6 @@ export class AgentFramework {
|
|
|
3025
3102
|
for (const module of config.modules) {
|
|
3026
3103
|
await framework.addModule(module);
|
|
3027
3104
|
}
|
|
3028
|
-
for (const agent of framework.agents.values()) {
|
|
3029
|
-
framework.warnEmptyUtilityPermissions(agent);
|
|
3030
|
-
}
|
|
3031
3105
|
for (const [agentName, presentation] of framework.toolPresentations) {
|
|
3032
3106
|
const workspace = framework.getWorkspaceModule();
|
|
3033
3107
|
if (!workspace)
|
|
@@ -3090,6 +3164,8 @@ export class AgentFramework {
|
|
|
3090
3164
|
upcomingWakes: (agentName) => framework.upcomingWakesFor(agentName),
|
|
3091
3165
|
});
|
|
3092
3166
|
framework.judgeWired = judge !== undefined;
|
|
3167
|
+
const configuredServerIds = new Set((config.mcplServers ?? []).map((server) => server.id));
|
|
3168
|
+
const heartbeatOn = config.heartbeat !== undefined && config.heartbeat !== null && config.heartbeat !== false;
|
|
3093
3169
|
framework.eventGate = new EventGate({
|
|
3094
3170
|
configPath,
|
|
3095
3171
|
initialConfig: config.gate.config,
|
|
@@ -3149,11 +3225,22 @@ export class AgentFramework {
|
|
|
3149
3225
|
// debounce can fire).
|
|
3150
3226
|
// A batched wake's summary names each channel as the agent names it
|
|
3151
3227
|
// back, with its server where another server shares its id.
|
|
3152
|
-
channelShown: (channel) => framework.channelRegistry?.nameShown(channel) ?? channel.channelId,
|
|
3228
|
+
channelShown: (channel) => framework.channelRegistry?.nameShown(channel) ?? suppliedValue(channel.channelId),
|
|
3153
3229
|
// A batched wake for one message still newest in the window hands its
|
|
3154
3230
|
// `[Gate: …]` line to the wake, for a fresh start of the wake's turn
|
|
3155
3231
|
// to decide (placeWakeLines).
|
|
3156
3232
|
isNewestEntry: (entry) => framework.isNewestContextEntry(entry),
|
|
3233
|
+
// What a source can send, for the rule-order notes: an MCPL server's
|
|
3234
|
+
// events reach the gate as its pushes and channel messages only (a
|
|
3235
|
+
// module's events carry a source of the module's own, such as "cli"
|
|
3236
|
+
// or "mcpl:<id>"), and the heartbeat's ticks as pushes.
|
|
3237
|
+
eventTypesFrom: (source) => {
|
|
3238
|
+
if (configuredServerIds.has(source) || framework.mcplServerConfigs.has(source))
|
|
3239
|
+
return MCPL_GATE_EVENT_TYPES;
|
|
3240
|
+
if (source === 'heartbeat' && heartbeatOn)
|
|
3241
|
+
return ['mcpl:push-event'];
|
|
3242
|
+
return undefined;
|
|
3243
|
+
},
|
|
3157
3244
|
resolveRouteChannel: (info) => {
|
|
3158
3245
|
if (info.eventType === 'mcpl:channel-incoming')
|
|
3159
3246
|
return info.channelId || undefined;
|
|
@@ -3281,16 +3368,19 @@ export class AgentFramework {
|
|
|
3281
3368
|
});
|
|
3282
3369
|
}
|
|
3283
3370
|
}
|
|
3371
|
+
// The host's own toolsets (host-presentation.ts): the built-in "settings"
|
|
3372
|
+
// and each MCPL server's tab, per agent, kept beside the store.
|
|
3373
|
+
framework.hostPresentation = framework.makeHostPresentation(config.storePath);
|
|
3284
3374
|
// Initialize MCPL subsystems if configured
|
|
3285
3375
|
if (config.mcplServers && config.mcplServers.length > 0) {
|
|
3286
3376
|
// Check every server's config before any is brought up: its boolean
|
|
3287
3377
|
// settings, and its tool prefix, which mustn't collide with a module
|
|
3288
3378
|
// name or another server's prefix.
|
|
3289
3379
|
const moduleNames = new Set(config.modules.map(m => m.name));
|
|
3290
|
-
//
|
|
3291
|
-
// module
|
|
3292
|
-
|
|
3293
|
-
|
|
3380
|
+
// The name `toolsets` is the framework's from creation: its toolsets
|
|
3381
|
+
// module (or a module given here that holds the name) takes it, so no
|
|
3382
|
+
// prefix may.
|
|
3383
|
+
moduleNames.add(TOOLSETS_MODULE);
|
|
3294
3384
|
const prefixesSeen = new Map(); // prefix → serverId
|
|
3295
3385
|
for (const serverConfig of config.mcplServers) {
|
|
3296
3386
|
assertServerConfigBooleans(serverConfig);
|
|
@@ -3309,9 +3399,28 @@ export class AgentFramework {
|
|
|
3309
3399
|
}
|
|
3310
3400
|
prefixesSeen.set(prefix, serverConfig.id);
|
|
3311
3401
|
}
|
|
3312
|
-
|
|
3402
|
+
framework.reserveToolsetsName(config.modules, config.mcplServers);
|
|
3403
|
+
if (framework.toolsetsNameHeldAtStart === null)
|
|
3313
3404
|
await framework.ensureToolsetsModule();
|
|
3314
|
-
|
|
3405
|
+
// A server kept as a closed tab (by its configured presentation, or as
|
|
3406
|
+
// an agent last left it) isn't connected until an agent opens it.
|
|
3407
|
+
let closed = framework.hostPresentation.startClosed(config.mcplServers);
|
|
3408
|
+
if (framework.toolsetsNameHeldAtStart !== null && closed.size > 0) {
|
|
3409
|
+
// No agent could open a closed tab: the meta-tools aren't the framework's.
|
|
3410
|
+
console.error(`[toolsets] ${[...closed].join(', ')} ${closed.size === 1 ? 'is' : 'are'} connected at startup though kept as a closed tab: ` +
|
|
3411
|
+
`the name "${TOOLSETS_MODULE}" is ${framework.toolsetsNameHeldAtStart}, so no agent could open ${closed.size === 1 ? 'it' : 'them'}`);
|
|
3412
|
+
closed = new Set();
|
|
3413
|
+
}
|
|
3414
|
+
for (const serverConfig of config.mcplServers) {
|
|
3415
|
+
if (closed.has(serverConfig.id))
|
|
3416
|
+
framework.closedTabConfigs().set(serverConfig.id, serverConfig);
|
|
3417
|
+
}
|
|
3418
|
+
await framework.initializeMcpl(config.mcplServers.filter((c) => !closed.has(c.id)), config.inferenceRouting);
|
|
3419
|
+
}
|
|
3420
|
+
else if (!config.modules.some((m) => m.name === TOOLSETS_MODULE)) {
|
|
3421
|
+
// The meta-tools are every agent's: the built-in toolset needs them,
|
|
3422
|
+
// MCPL or none.
|
|
3423
|
+
await framework.ensureToolsetsModule();
|
|
3315
3424
|
}
|
|
3316
3425
|
// Tune-out coordinator (issue #77): needs both the subconscious resident
|
|
3317
3426
|
// and the MCPL channel subsystem; created after both exist.
|
|
@@ -4172,7 +4281,7 @@ export class AgentFramework {
|
|
|
4172
4281
|
* only its own model, which auxiliary admission checks per call. */
|
|
4173
4282
|
cooldownBlocksAuxiliary(agentName) {
|
|
4174
4283
|
const cooldown = this.providerAccelerationCooldowns.get(agentName);
|
|
4175
|
-
return cooldown !== undefined && cooldown.waitModel === undefined;
|
|
4284
|
+
return cooldown !== undefined && cooldown.waitModel === undefined && cooldown.rearm === undefined;
|
|
4176
4285
|
}
|
|
4177
4286
|
acquirePrimaryProviderGate(agentName) {
|
|
4178
4287
|
const gate = this.providerGate(agentName);
|
|
@@ -4298,23 +4407,50 @@ export class AgentFramework {
|
|
|
4298
4407
|
}
|
|
4299
4408
|
const cooldown = this.providerAccelerationCooldowns.get(agentName);
|
|
4300
4409
|
if (cooldown && cooldown.heldRequests.length > 0 && Number.isFinite(cooldown.until)) {
|
|
4301
|
-
wakes.push({ at: cooldown.until, certain: true, what:
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
// they decide a heartbeat push: one the rules would hold back is listed
|
|
4305
|
-
// as only a "maybe".
|
|
4306
|
-
if (this.heartbeat && this.heartbeatTargets().includes(agentName)) {
|
|
4307
|
-
const probe = (kind) => !this.eventGate
|
|
4308
|
-
|| this.eventGate.probe(this.heartbeatGateEvent({ kind, eventId: 'probe', text: 'probe' })).trigger;
|
|
4309
|
-
const tickWakes = probe('heartbeat');
|
|
4310
|
-
const reminderWakes = probe('reminder');
|
|
4311
|
-
for (const w of this.heartbeat.upcomingWakes()) {
|
|
4312
|
-
const isReminder = w.what.startsWith('its reminder');
|
|
4313
|
-
wakes.push((isReminder ? reminderWakes : tickWakes) ? w : { ...w, certain: false });
|
|
4314
|
-
}
|
|
4410
|
+
wakes.push({ at: cooldown.until, certain: true, what: cooldown.rearm
|
|
4411
|
+
? `a turn whose retries ran out, sent again (${cooldown.rearm.number} of ${cooldown.rearm.of})`
|
|
4412
|
+
: 'a turn held by a provider cooldown, when it ends' });
|
|
4315
4413
|
}
|
|
4414
|
+
wakes.push(...this.heartbeatWakesFor(agentName));
|
|
4316
4415
|
return wakes;
|
|
4317
4416
|
}
|
|
4417
|
+
/**
|
|
4418
|
+
* The heartbeat's ticks and reminders for `agentName`, soonest first, which
|
|
4419
|
+
* the agent's rules decide as they decide a heartbeat push: one the rules
|
|
4420
|
+
* would hold back is only a "maybe" (`certain` false). Each is probed as
|
|
4421
|
+
* the event its delivery is judged by (deliverHeartbeat): a silent tick as
|
|
4422
|
+
* silent, with the text it will bring, so a rule on the content or the
|
|
4423
|
+
* silent flag decides the probe as it will decide the wake. None for an
|
|
4424
|
+
* agent the heartbeat doesn't wake. `whose` words them for the judge ("its
|
|
4425
|
+
* heartbeat") or for the agent ("your heartbeat").
|
|
4426
|
+
*/
|
|
4427
|
+
heartbeatWakesFor(agentName, whose = 'its') {
|
|
4428
|
+
if (!this.heartbeat || !this.heartbeatTargets().includes(agentName))
|
|
4429
|
+
return [];
|
|
4430
|
+
const wakes = (w) => !this.eventGate
|
|
4431
|
+
|| this.eventGate.probe(this.heartbeatGateEvent({ kind: w.kind, eventId: 'probe', text: w.text, silent: w.silent })).trigger;
|
|
4432
|
+
return this.heartbeat.scheduledWakes(whose).map((w) => (wakes(w) ? w : { ...w, certain: false }));
|
|
4433
|
+
}
|
|
4434
|
+
/**
|
|
4435
|
+
* The heartbeat's wake due before a self-wake due at `selfWakeAt` (ms), for
|
|
4436
|
+
* the skip_reply that armed it: the soonest of its ticks and reminders that
|
|
4437
|
+
* the agent's rules, as they stand, let wake it. The confirmation says when
|
|
4438
|
+
* it's due, not that it will wake the agent (skipReplyNote), so a probe
|
|
4439
|
+
* that can't foresee the gate at the wake's time (a gate.js, a rate limit's
|
|
4440
|
+
* tokens then) leaves it true. A failure here costs only that line: the
|
|
4441
|
+
* self-wake is armed already, and the confirmation goes out without it.
|
|
4442
|
+
*/
|
|
4443
|
+
heartbeatWakeBefore(agentName, selfWakeAt) {
|
|
4444
|
+
try {
|
|
4445
|
+
const now = Date.now();
|
|
4446
|
+
const first = this.heartbeatWakesFor(agentName, 'your').find((w) => w.certain && w.at < selfWakeAt);
|
|
4447
|
+
return first ? { inSeconds: Math.max(1, Math.round((first.at - now) / 1000)), what: first.what } : undefined;
|
|
4448
|
+
}
|
|
4449
|
+
catch (err) {
|
|
4450
|
+
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))}`);
|
|
4451
|
+
return undefined;
|
|
4452
|
+
}
|
|
4453
|
+
}
|
|
4318
4454
|
/** The agents the heartbeat wakes: the residents, not conversation forks,
|
|
4319
4455
|
* ephemeral runs or the subconscious, which keep windows of their own. */
|
|
4320
4456
|
heartbeatTargets() {
|
|
@@ -4723,8 +4859,63 @@ export class AgentFramework {
|
|
|
4723
4859
|
console.error(`[provider-cooldown] agent=${agent.name} ` +
|
|
4724
4860
|
`${hostHold ? `host hold (${hostHold.reason})` : 'organization acceleration 429'} — ` +
|
|
4725
4861
|
`holding primary/auxiliary for ${delayMs}ms; ${held.length} request(s) retained`);
|
|
4862
|
+
if (wait && trigger && (wait.until === null || wait.until > now))
|
|
4863
|
+
this.noteStatedWaitRun(agent.name, wait, now);
|
|
4726
4864
|
return true;
|
|
4727
4865
|
}
|
|
4866
|
+
rearmExhaustedTurn(agent, error, trigger, ownsProviderGate, routing) {
|
|
4867
|
+
if (!ownsProviderGate || trigger === undefined || this.providerAdmissionClosed || this.turnRearmMs.length === 0)
|
|
4868
|
+
return;
|
|
4869
|
+
const done = this.turnRearms.get(agent.name) ?? 0;
|
|
4870
|
+
const notSent = (why) => console.error(`[inference-rearm] agent=${agent.name} turn not re-armed: ${why}`);
|
|
4871
|
+
if (!isTransientFailure(error)) {
|
|
4872
|
+
if (done > 0)
|
|
4873
|
+
notSent(`its failure isn't transient (${isMembraneError(error) ? error.type : boundFailureText(error.message, FAILURE_SUMMARY_MAX_CHARS)})`);
|
|
4874
|
+
return;
|
|
4875
|
+
}
|
|
4876
|
+
const continuation = isTurnContinuation(trigger.reason);
|
|
4877
|
+
const speaking = routing?.locus ?? routing?.triggerChannel;
|
|
4878
|
+
if (!speaking && routing?.noChannel && !trigger.channelId) {
|
|
4879
|
+
notSent('it would have to guess a channel');
|
|
4880
|
+
return;
|
|
4881
|
+
}
|
|
4882
|
+
if (done >= this.turnRearmMs.length) {
|
|
4883
|
+
notSent(`its ${this.turnRearmMs.length} re-arms are spent until a turn completes or a message is said to it`);
|
|
4884
|
+
return;
|
|
4885
|
+
}
|
|
4886
|
+
const delayMs = this.turnRearmMs[done];
|
|
4887
|
+
const rearm = { number: done + 1, of: this.turnRearmMs.length };
|
|
4888
|
+
this.turnRearms.set(agent.name, rearm.number);
|
|
4889
|
+
const request = { ...trigger, [REARMED]: true };
|
|
4890
|
+
delete request[TURN_RETRY];
|
|
4891
|
+
delete request[MERGED_BATCH];
|
|
4892
|
+
if (continuation) {
|
|
4893
|
+
request.reason = REARM_REASON;
|
|
4894
|
+
delete request.counterparty;
|
|
4895
|
+
}
|
|
4896
|
+
if (speaking) {
|
|
4897
|
+
request.channelId = speaking.channelId;
|
|
4898
|
+
request.channelServerId = speaking.serverId;
|
|
4899
|
+
request.addressed = true;
|
|
4900
|
+
}
|
|
4901
|
+
const existing = this.providerAccelerationCooldowns.get(agent.name);
|
|
4902
|
+
if (existing) {
|
|
4903
|
+
this.holdRequest(existing.heldRequests, request);
|
|
4904
|
+
}
|
|
4905
|
+
else {
|
|
4906
|
+
const now = Date.now();
|
|
4907
|
+
const timer = setTimeout(() => this.releaseProviderAccelerationCooldown(agent.name), this.providerHoldTimerMs(delayMs));
|
|
4908
|
+
timer.unref?.();
|
|
4909
|
+
this.providerAccelerationCooldowns.set(agent.name, {
|
|
4910
|
+
startedAt: now, until: now + delayMs, timer, heldRequests: [request], failures: 0, rearm,
|
|
4911
|
+
reason: `its turn's retries ran out (${boundFailureText(error.message, FAILURE_SUMMARY_MAX_CHARS)}), ` +
|
|
4912
|
+
`so it is sent again (${rearm.number} of ${rearm.of})`,
|
|
4913
|
+
});
|
|
4914
|
+
}
|
|
4915
|
+
console.error(`[inference-rearm] agent=${agent.name} turn sent again ` +
|
|
4916
|
+
(existing ? 'when the provider hold already on the agent ends' : `in ${delayMs}ms`) +
|
|
4917
|
+
` (${rearm.number} of ${rearm.of})`);
|
|
4918
|
+
}
|
|
4728
4919
|
/**
|
|
4729
4920
|
* A provider's stated wait on a turn that doesn't own provider admission:
|
|
4730
4921
|
* a conversation agent's or an ephemeral run's. Left to the error policy,
|
|
@@ -4842,8 +5033,10 @@ export class AgentFramework {
|
|
|
4842
5033
|
const requests = cooldown.heldRequests.length > 0 ? cooldown.heldRequests : [{ agentName,
|
|
4843
5034
|
reason: 'provider-acceleration-retry', source: 'framework', timestamp: Date.now() }];
|
|
4844
5035
|
this.pendingRequests.push(...requests);
|
|
4845
|
-
console.error(
|
|
4846
|
-
`
|
|
5036
|
+
console.error(cooldown.rearm
|
|
5037
|
+
? `[inference-rearm] agent=${agentName} turn queued again (${cooldown.rearm.number} of ${cooldown.rearm.of})`
|
|
5038
|
+
: `[provider-cooldown] agent=${agentName} released after ${Date.now() - cooldown.startedAt}ms — ` +
|
|
5039
|
+
`fresh compile queued with ${requests.length} retained request(s)`);
|
|
4847
5040
|
}
|
|
4848
5041
|
/**
|
|
4849
5042
|
* Wait an error policy's retry delay, all of it: the delay can be a
|
|
@@ -4921,13 +5114,47 @@ export class AgentFramework {
|
|
|
4921
5114
|
: `until ${new Date(wait.until).toISOString()}`;
|
|
4922
5115
|
const stated = `${error.type}${error.httpStatus !== undefined ? ` HTTP ${error.httpStatus}` : ''}, retry-after ${String(error.retryAfterMs)}ms`;
|
|
4923
5116
|
console.error(`[provider-wait] agent=${agentName} model=${model} holds ${until}: ${stated}`);
|
|
4924
|
-
if (
|
|
5117
|
+
if (waitAlertsOnItsOwn(wait)) {
|
|
4925
5118
|
this.opsAlert('provider-wait', agentName, `provider wait on ${model} holds ${until} (${stated}: ${reason}); ` +
|
|
4926
5119
|
'if the provider did not mean that, release it with release-provider-wait', { data: { model, until: wait.until === null ? null : new Date(wait.until).toISOString() } });
|
|
4927
5120
|
}
|
|
4928
5121
|
}
|
|
4929
5122
|
return wait;
|
|
4930
5123
|
}
|
|
5124
|
+
noteStatedWaitRun(agentName, wait, now) {
|
|
5125
|
+
let run = this.statedWaitRuns.get(agentName);
|
|
5126
|
+
if (!run) {
|
|
5127
|
+
run = { since: now, holds: 0, alerted: false, model: wait.model, reason: wait.reason, heldUntil: now };
|
|
5128
|
+
this.statedWaitRuns.set(agentName, run);
|
|
5129
|
+
}
|
|
5130
|
+
run.holds++;
|
|
5131
|
+
run.model = wait.model;
|
|
5132
|
+
run.reason = wait.reason;
|
|
5133
|
+
if (wait.until !== null)
|
|
5134
|
+
run.heldUntil = Math.max(run.heldUntil, wait.until);
|
|
5135
|
+
if (waitAlertsOnItsOwn(wait))
|
|
5136
|
+
run.alerted = true;
|
|
5137
|
+
if (now - run.since > PROVIDER_WAIT_ALERT_MS)
|
|
5138
|
+
this.alertStatedWaitRun(agentName, run, now);
|
|
5139
|
+
}
|
|
5140
|
+
alertStatedWaitRun(agentName, run, now) {
|
|
5141
|
+
if (run.alerted)
|
|
5142
|
+
return;
|
|
5143
|
+
run.alerted = true;
|
|
5144
|
+
const since = new Date(run.since).toISOString();
|
|
5145
|
+
this.opsAlert('provider-wait', agentName, `a turn's ${run.holds} stated waits in a row on ${run.model} span ${Math.round((now - run.since) / 60_000)} min ` +
|
|
5146
|
+
`since ${since}: ${run.reason}; a provider stating waits that long in a row points to the account's rate limit ` +
|
|
5147
|
+
"or quota, or this host's load", { data: { model: run.model, since, holds: run.holds } });
|
|
5148
|
+
}
|
|
5149
|
+
endStatedWaitRun(agentName) {
|
|
5150
|
+
const run = this.statedWaitRuns?.get(agentName);
|
|
5151
|
+
if (!run)
|
|
5152
|
+
return;
|
|
5153
|
+
this.statedWaitRuns.delete(agentName);
|
|
5154
|
+
const end = Math.min(Date.now(), run.heldUntil);
|
|
5155
|
+
if (end - run.since > PROVIDER_WAIT_ALERT_MS)
|
|
5156
|
+
this.alertStatedWaitRun(agentName, run, end);
|
|
5157
|
+
}
|
|
4931
5158
|
/**
|
|
4932
5159
|
* Hold this agent's primary turns for a provider wait on its model. The
|
|
4933
5160
|
* requests are kept and released together, into one fresh compile, when
|
|
@@ -4961,8 +5188,11 @@ export class AgentFramework {
|
|
|
4961
5188
|
this.holdRequest(held, request);
|
|
4962
5189
|
// An acceleration or host hold already parks the whole agent; its release
|
|
4963
5190
|
// re-checks this wait at admission.
|
|
4964
|
-
if (existing && existing.waitModel === undefined)
|
|
5191
|
+
if (existing && existing.waitModel === undefined) {
|
|
5192
|
+
if (failed)
|
|
5193
|
+
this.noteStatedWaitRun(agent.name, wait, now);
|
|
4965
5194
|
return true;
|
|
5195
|
+
}
|
|
4966
5196
|
if (existing)
|
|
4967
5197
|
clearTimeout(existing.timer);
|
|
4968
5198
|
const timer = setTimeout(() => this.releaseProviderAccelerationCooldown(agent.name), this.providerHoldTimerMs(until - now));
|
|
@@ -4976,6 +5206,8 @@ export class AgentFramework {
|
|
|
4976
5206
|
console.error(`[provider-wait] agent=${agent.name} model=${wait.model} holding primary turns ` +
|
|
4977
5207
|
`${wait.until === null ? 'until released' : `until ${new Date(wait.until).toISOString()}`}; ${held.length} request(s) retained`);
|
|
4978
5208
|
}
|
|
5209
|
+
if (failed)
|
|
5210
|
+
this.noteStatedWaitRun(agent.name, wait, now);
|
|
4979
5211
|
return true;
|
|
4980
5212
|
}
|
|
4981
5213
|
/**
|
|
@@ -5029,6 +5261,9 @@ export class AgentFramework {
|
|
|
5029
5261
|
if (model === EVERY_MODEL)
|
|
5030
5262
|
model = undefined;
|
|
5031
5263
|
const released = this.providerWaits?.release(agentName, model, by) ?? [];
|
|
5264
|
+
const run = this.statedWaitRuns?.get(agentName);
|
|
5265
|
+
if (run && released.some(({ wait }) => wait.model === run.model))
|
|
5266
|
+
run.heldUntil = Math.min(run.heldUntil, Date.now());
|
|
5032
5267
|
for (const { wait, release } of released) {
|
|
5033
5268
|
console.error(`[provider-wait] agent=${agentName} model=${wait.model} released by ${by} (${release})`);
|
|
5034
5269
|
}
|
|
@@ -5108,6 +5343,8 @@ export class AgentFramework {
|
|
|
5108
5343
|
clearTimeout(cooldown.timer);
|
|
5109
5344
|
this.providerAccelerationCooldowns.clear();
|
|
5110
5345
|
this.providerAccelerationRecoveries.clear();
|
|
5346
|
+
this.turnRearms?.clear();
|
|
5347
|
+
this.exhaustingRearms?.clear();
|
|
5111
5348
|
for (const gate of this.providerGates.values()) {
|
|
5112
5349
|
gate.primaryDepth = 0;
|
|
5113
5350
|
gate.primaryPending = false;
|
|
@@ -5513,6 +5750,12 @@ export class AgentFramework {
|
|
|
5513
5750
|
if (claimedBy) {
|
|
5514
5751
|
throw new Error(`Module "${module.name}" collides with the toolPrefix "${module.name}" the restart of "${claimedBy.restarting}" holds`);
|
|
5515
5752
|
}
|
|
5753
|
+
// A closed tab keeps its prefix for the connect its next open makes.
|
|
5754
|
+
for (const [id, config] of this.closedTabConfigs()) {
|
|
5755
|
+
if ((config.toolPrefix ?? `mcpl--${id}`) === module.name) {
|
|
5756
|
+
throw new Error(`Module "${module.name}" collides with MCPL server "${id}"'s toolPrefix "${module.name}" (a closed tab)`);
|
|
5757
|
+
}
|
|
5758
|
+
}
|
|
5516
5759
|
this.resetCompletedStop('addModule()');
|
|
5517
5760
|
await this.moduleRegistry.addModule(module);
|
|
5518
5761
|
// The workspace judges whether an agent's history holds a message as the
|
|
@@ -5675,12 +5918,12 @@ export class AgentFramework {
|
|
|
5675
5918
|
agent.getContextManager().addMessage('user', [{
|
|
5676
5919
|
type: 'text',
|
|
5677
5920
|
text: (before === 'sending'
|
|
5678
|
-
? `[
|
|
5921
|
+
? `[${NOTICE_TAG.turnInterrupted}] Your previous turn was stopped before it sent anything — ` +
|
|
5679
5922
|
`a deliberate stop, not a failure.`
|
|
5680
5923
|
: before === 'finishing'
|
|
5681
|
-
? `[
|
|
5924
|
+
? `[${NOTICE_TAG.turnInterrupted}] Your previous turn was stopped before it finished — ` +
|
|
5682
5925
|
`a deliberate stop, not a failure.`
|
|
5683
|
-
: `[
|
|
5926
|
+
: `[${NOTICE_TAG.turnInterrupted}] Your previous turn was cancelled mid-stream — ` +
|
|
5684
5927
|
`a deliberate stop, not a failure. Whatever you were still ` +
|
|
5685
5928
|
`producing when it stopped was cut off there.`) + handedOn,
|
|
5686
5929
|
}], { system: true, kind: 'turn-interrupted', reason });
|
|
@@ -5818,8 +6061,10 @@ export class AgentFramework {
|
|
|
5818
6061
|
: [];
|
|
5819
6062
|
return [
|
|
5820
6063
|
...moduleTools,
|
|
5821
|
-
// Utilities
|
|
5822
|
-
|
|
6064
|
+
// Utilities: callable tools, presented in the built-in toolset
|
|
6065
|
+
// (placedInBuiltin), never in the tool list. One named like a tool of
|
|
6066
|
+
// its module is that tool's: the tool takes the name.
|
|
6067
|
+
...this.moduleRegistry.getAllUtilities().filter((u) => !moduleTools.some((t) => t.name === u.name)),
|
|
5823
6068
|
...this.mcplTools,
|
|
5824
6069
|
...channelTools,
|
|
5825
6070
|
...gateTools,
|
|
@@ -5888,6 +6133,8 @@ export class AgentFramework {
|
|
|
5888
6133
|
setMcplServerConfig(config) {
|
|
5889
6134
|
this.mcplServerConfigs.set(config.id, config);
|
|
5890
6135
|
referenceRegistry.setServerAddress(config.id, serverAddressOf(config), this);
|
|
6136
|
+
// The gate's rule-order notes read which sources are servers.
|
|
6137
|
+
this.eventGate?.sourcesChanged();
|
|
5891
6138
|
}
|
|
5892
6139
|
/** Register every RFC-005 reference in an MCPL peer's tool-result content
|
|
5893
6140
|
* against its server, then fetch by `mode`:
|
|
@@ -6175,7 +6422,7 @@ export class AgentFramework {
|
|
|
6175
6422
|
return undefined;
|
|
6176
6423
|
const reach = this.residentHoldsTraffic(who) ? ' Messages there that address you, such as an @-mention, still reach you.' : '';
|
|
6177
6424
|
const open = this.canAgentUseTool(who, 'channel_open') && !registry.lifecycleWithheld(channel.serverId)
|
|
6178
|
-
? ` channel_open with channelId
|
|
6425
|
+
? ` channel_open with channelId ${quotedValue(channel.channelId)} and serverId "${channel.serverId}" opens it.`
|
|
6179
6426
|
: '';
|
|
6180
6427
|
return `${this.channelShown(channel)} stays closed: you ${how} it, and your words there don't open it.${reach}${open}`;
|
|
6181
6428
|
}
|
|
@@ -6265,14 +6512,14 @@ export class AgentFramework {
|
|
|
6265
6512
|
const body = closedNote ?? tunedOutNote;
|
|
6266
6513
|
if (!body)
|
|
6267
6514
|
return answer;
|
|
6268
|
-
const tag = closedNote ?
|
|
6515
|
+
const tag = `[${closedNote ? NOTICE_TAG.closedChannel : NOTICE_TAG.tunedOut}]`;
|
|
6269
6516
|
if (forScript) {
|
|
6270
6517
|
const window = this.noticeTargetFor(agentName, 'closed-channel');
|
|
6271
6518
|
try {
|
|
6272
6519
|
if (window) {
|
|
6273
6520
|
this.addMessage('user', [{
|
|
6274
6521
|
type: 'text',
|
|
6275
|
-
text: `${tag} Your script's ${toolName}: ${body}${this.closedChannelNoteOff(agentName)}`,
|
|
6522
|
+
text: `${tag} Your script's ${suppliedValue(toolName)}: ${body}${this.closedChannelNoteOff(agentName)}`,
|
|
6276
6523
|
}], { system: true, kind: 'channel-notice' }, window);
|
|
6277
6524
|
}
|
|
6278
6525
|
}
|
|
@@ -6430,16 +6677,6 @@ export class AgentFramework {
|
|
|
6430
6677
|
},
|
|
6431
6678
|
};
|
|
6432
6679
|
}
|
|
6433
|
-
warnEmptyUtilityPermissions(agent) {
|
|
6434
|
-
if (agent.allowedTools === 'all' || !agent.allowedTools.includes('utils')
|
|
6435
|
-
|| this.emptyUtilityPermissionWarnings.has(agent))
|
|
6436
|
-
return;
|
|
6437
|
-
const utilities = this.moduleRegistry.getAllUtilities();
|
|
6438
|
-
if (utilities.length === 0 || utilities.some(tool => agent.canUseTool(tool.name)))
|
|
6439
|
-
return;
|
|
6440
|
-
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.`);
|
|
6441
|
-
this.emptyUtilityPermissionWarnings.add(agent);
|
|
6442
|
-
}
|
|
6443
6680
|
getToolsForAgent(agentName, snapshot) {
|
|
6444
6681
|
const agent = this.agents.get(agentName);
|
|
6445
6682
|
// Model-facing advice names skip_reply only where dispatch would let this
|
|
@@ -6457,11 +6694,8 @@ export class AgentFramework {
|
|
|
6457
6694
|
: t);
|
|
6458
6695
|
return [...SUBCONSCIOUS_TOOLS, ...basics];
|
|
6459
6696
|
}
|
|
6460
|
-
if (agent)
|
|
6461
|
-
this.warnEmptyUtilityPermissions(agent);
|
|
6462
|
-
const hasPermittedUtility = !agent || this.moduleRegistry.getAllUtilities().some(tool => agent.canUseTool(tool.name));
|
|
6463
6697
|
return [...this.getAllTools(), ...(this.toolPresentations.has(agentName) ? presentationTools(this.toolPresentations.get(agentName).config.cataloguePath) : [])]
|
|
6464
|
-
.
|
|
6698
|
+
.map((tool) => {
|
|
6465
6699
|
if (tool.name === 'think') {
|
|
6466
6700
|
return this.buildThinkTool(snapshot?.sameRoundThinkTextPolicy
|
|
6467
6701
|
?? this.getAgentRuntimeSettings(agentName).sameRoundThinkTextPolicy, canSkipReply);
|
|
@@ -6491,7 +6725,7 @@ export class AgentFramework {
|
|
|
6491
6725
|
return null;
|
|
6492
6726
|
const tools = this.availableToolsForPresentation(agentName, snapshot);
|
|
6493
6727
|
const sources = new Map();
|
|
6494
|
-
for (const tool of this.moduleRegistry.getAllTools()) {
|
|
6728
|
+
for (const tool of [...this.moduleRegistry.getAllTools(), ...this.moduleRegistry.getAllUtilities()]) {
|
|
6495
6729
|
sources.set(tool.name, `Module: ${tool.name.split('--')[0]}`);
|
|
6496
6730
|
}
|
|
6497
6731
|
// An MCPL tool's source is the server a call of it reaches, by the one
|
|
@@ -6506,8 +6740,8 @@ export class AgentFramework {
|
|
|
6506
6740
|
return presentation.resolve(tools, sources);
|
|
6507
6741
|
}
|
|
6508
6742
|
advertisedToolsForAgent(agentName, snapshot) {
|
|
6509
|
-
return this.inspectToolPresentation(agentName, snapshot)?.advertised
|
|
6510
|
-
?? this.availableToolsForPresentation(agentName, snapshot);
|
|
6743
|
+
return this.headerTools(agentName, this.inspectToolPresentation(agentName, snapshot)?.advertised
|
|
6744
|
+
?? this.availableToolsForPresentation(agentName, snapshot));
|
|
6511
6745
|
}
|
|
6512
6746
|
/** Compression can revisit calls to hidden tools; never apply visibility here. */
|
|
6513
6747
|
compressionToolsForAgent(agentName, snapshot) {
|
|
@@ -7020,8 +7254,10 @@ export class AgentFramework {
|
|
|
7020
7254
|
* CURRENT settings — maintenance may have moved the folded floor and the
|
|
7021
7255
|
* branch generation, so verdicts are computed at resume time, never reused.
|
|
7022
7256
|
* Throws ResumeBlockedError (with all failing verdicts) unless `force`.
|
|
7023
|
-
* Unavailable previews
|
|
7024
|
-
* strategy
|
|
7257
|
+
* Unavailable previews pass, with a warning naming the reason unless it is
|
|
7258
|
+
* `no_preview_support`, a strategy with nothing to preview — refusing to
|
|
7259
|
+
* resume because the strategy cannot preview would hold hosts hostage to a
|
|
7260
|
+
* diagnostic.
|
|
7025
7261
|
*/
|
|
7026
7262
|
async resume(opts) {
|
|
7027
7263
|
this.refuseOperatorAfterStop('resume');
|
|
@@ -7898,7 +8134,8 @@ export class AgentFramework {
|
|
|
7898
8134
|
throw new Error(`Agent not found: ${agentName}`);
|
|
7899
8135
|
}
|
|
7900
8136
|
const presentation = this.inspectToolPresentation(agentName);
|
|
7901
|
-
const
|
|
8137
|
+
const availableTools = presentation?.available ?? this.availableToolsForPresentation(agentName);
|
|
8138
|
+
const tools = this.headerTools(agentName, presentation?.advertised ?? availableTools);
|
|
7902
8139
|
const capture = (request) => {
|
|
7903
8140
|
if (presentation)
|
|
7904
8141
|
this.presentationPreviews.set(request, presentation);
|
|
@@ -7911,7 +8148,7 @@ export class AgentFramework {
|
|
|
7911
8148
|
// Default: no dynamic injection gathering → no inference, no Chronicle
|
|
7912
8149
|
// writes of its own, no external RPC. Opt in explicitly.
|
|
7913
8150
|
if (!opts?.injections) {
|
|
7914
|
-
return capture(await agent.buildActivationRequest(tools, undefined, opts?.budget,
|
|
8151
|
+
return capture(await agent.buildActivationRequest(tools, undefined, opts?.budget, availableTools, { dryRun: true }));
|
|
7915
8152
|
}
|
|
7916
8153
|
// Full-fidelity path: mirrors startAgentStream's injection gathering.
|
|
7917
8154
|
// NOT transparent — see the doc comment above.
|
|
@@ -7951,7 +8188,7 @@ export class AgentFramework {
|
|
|
7951
8188
|
const plan = this.planInjectionStores(agent, injections);
|
|
7952
8189
|
const held = plan !== undefined && this.slotHeldFrom(agent);
|
|
7953
8190
|
const placed = new Set(plan ? (held ? [...plan.leftAt, ...plan.rows.map((row) => row.at)] : plan.leftAt) : []);
|
|
7954
|
-
return capture(await agent.buildActivationRequest(tools, plan ? (injections ?? []).filter((_, at) => placed.has(at)) : injections, opts?.budget,
|
|
8191
|
+
return capture(await agent.buildActivationRequest(tools, plan ? (injections ?? []).filter((_, at) => placed.has(at)) : injections, opts?.budget, availableTools, {
|
|
7955
8192
|
dryRun: true,
|
|
7956
8193
|
...(plan && !held ? { appendedMessages: plan.rows.map(({ participant, content }) => ({ participant, content })) } : {}),
|
|
7957
8194
|
}));
|
|
@@ -7969,7 +8206,13 @@ export class AgentFramework {
|
|
|
7969
8206
|
* see `AutobiographicalStrategy.previewContext` and
|
|
7970
8207
|
* `Agent.compileWithInjections`.
|
|
7971
8208
|
*
|
|
7972
|
-
* Returns null when the agent's strategy has
|
|
8209
|
+
* Returns null when the agent's strategy has nothing to preview: one without
|
|
8210
|
+
* `previewContext`, or a non-adaptive `AutobiographicalStrategy` whose select
|
|
8211
|
+
* fits the budget, built from the framework's own copy of context-manager.
|
|
8212
|
+
* Built from another copy, such a strategy throws its `previewContext
|
|
8213
|
+
* requires adaptiveResolution` error here instead, since the manager
|
|
8214
|
+
* recognises only its own copy's `NoFoldPlanToPreviewError`. Over the
|
|
8215
|
+
* budget, a non-adaptive strategy's preview is an infeasible one.
|
|
7973
8216
|
*/
|
|
7974
8217
|
previewContextSettings(agentName, budgetTokens, overrides, opts) {
|
|
7975
8218
|
const agent = this.agents.get(agentName);
|
|
@@ -8005,9 +8248,9 @@ export class AgentFramework {
|
|
|
8005
8248
|
*
|
|
8006
8249
|
* Never throws for preview-layer reasons: strategy without previewContext,
|
|
8007
8250
|
* preview overlap, stale branch generation, the hierarchical (non-
|
|
8008
|
-
* adaptive) path, and a context manager that refuses its
|
|
8009
|
-
* strategy's initialization pending or failed) all come back
|
|
8010
|
-
* `{available: false, reason}`. Unknown agent still throws, matching the
|
|
8251
|
+
* adaptive) path within its budget, and a context manager that refuses its
|
|
8252
|
+
* queries (its strategy's initialization pending or failed) all come back
|
|
8253
|
+
* as `{available: false, reason}`. Unknown agent still throws, matching the
|
|
8011
8254
|
* sibling accessors, and so does a patch whose values the agent refuses,
|
|
8012
8255
|
* whatever its context manager's state. Whether the strategy takes live
|
|
8013
8256
|
* tail and pace settings is the manager's to say, so while it refuses, a
|
|
@@ -9066,16 +9309,18 @@ export class AgentFramework {
|
|
|
9066
9309
|
description: 'Add or replace a wake rule (a gate.json policy) at runtime — no need to ' +
|
|
9067
9310
|
'hand-edit the file. The rule is validated and hot-applied immediately. ' +
|
|
9068
9311
|
'A rule with the same `name` replaces the existing one in place (add a ' +
|
|
9069
|
-
'position to move it).
|
|
9070
|
-
'
|
|
9071
|
-
'
|
|
9072
|
-
'
|
|
9073
|
-
'
|
|
9074
|
-
'
|
|
9075
|
-
'
|
|
9076
|
-
'
|
|
9077
|
-
'
|
|
9078
|
-
'
|
|
9312
|
+
'position to move it). Rules are checked in order and the first one that ' +
|
|
9313
|
+
'matches an event decides it, so the rules below never see that event: a ' +
|
|
9314
|
+
'broad rule placed early takes events from them, your DM and mention ' +
|
|
9315
|
+
'rules included. Place a rule next to a particular one with ' +
|
|
9316
|
+
'`insertBefore`/`insertAfter`; `position: "prepend"` puts it above all of ' +
|
|
9317
|
+
'them. For a rule that only adds wakes ("also wake me every Nth event", ' +
|
|
9318
|
+
'"at most N an hour"), set `passthrough: true`: the events it doesn\'t ' +
|
|
9319
|
+
'wake for go on to your other rules. The result shows how a few common ' +
|
|
9320
|
+
'events are decided before and after the change. Its `shadowWarnings` ' +
|
|
9321
|
+
'are notes on your whole rule list, not only this change, wherever an ' +
|
|
9322
|
+
'earlier rule takes events a later one could get: each says whether that ' +
|
|
9323
|
+
'matters and what to do about it. Two common shapes:\n' +
|
|
9079
9324
|
'• Watch a FILE/workspace path: match on `mount` + `pathGlob`, e.g. ' +
|
|
9080
9325
|
'`{ name: "watch-notes", match: { scope: ["workspace:modified"], mount: "project", pathGlob: "notes/*.md" }, behavior: "always" }`.\n' +
|
|
9081
9326
|
'• Watch a CHANNEL: match on `source` + `channel` (and/or `tagsAny: ["chat:ambient"]`), ' +
|
|
@@ -9564,14 +9809,14 @@ export class AgentFramework {
|
|
|
9564
9809
|
const ep = this.rewindEpisode.get(agent.name);
|
|
9565
9810
|
const count = (ep?.count ?? 0) + 1;
|
|
9566
9811
|
const why = cause === 'refusal'
|
|
9567
|
-
? `the model refused on them (content filter: ${category}). Their content is ` +
|
|
9812
|
+
? `the model refused on them (content filter: ${failureShown(category)}). Their content is ` +
|
|
9568
9813
|
`not reproduced here (so this note can't re-trigger the filter); the ` +
|
|
9569
9814
|
`originals remain in the raw record`
|
|
9570
|
-
: `the model API kept refusing the requests that carried them (${category}), ` +
|
|
9815
|
+
: `the model API kept refusing the requests that carried them (${failureShown(category)}), ` +
|
|
9571
9816
|
`so that the conversation could go on. Their content is not reproduced ` +
|
|
9572
9817
|
`here (so this note can't trigger the same refusal); the originals remain ` +
|
|
9573
9818
|
`in the raw record`;
|
|
9574
|
-
const text = `[
|
|
9819
|
+
const text = `[${NOTICE_TAG.refusalRewind}] The ${count} most recent turn(s) were set aside because ` +
|
|
9575
9820
|
`${why}. You are clear to continue — if you ` +
|
|
9576
9821
|
`were mid-task, take a different approach; otherwise carry on with whatever ` +
|
|
9577
9822
|
`is now in front of you, or briefly acknowledge the gap and ask what's next.`;
|
|
@@ -11673,6 +11918,8 @@ export class AgentFramework {
|
|
|
11673
11918
|
}
|
|
11674
11919
|
}
|
|
11675
11920
|
async createAgent(config) {
|
|
11921
|
+
const sees = (metadata) => !metadata?.tuneOut
|
|
11922
|
+
&& !isAnotherAgentsInjection(metadata, config.name);
|
|
11676
11923
|
// Create context manager for this agent
|
|
11677
11924
|
const contextManager = await ContextManager.open({
|
|
11678
11925
|
store: this.store,
|
|
@@ -11693,10 +11940,10 @@ export class AgentFramework {
|
|
|
11693
11940
|
// Another agent's stored context injections (stored-injections.ts) are
|
|
11694
11941
|
// that agent's context, not conversation: they stay out of this view,
|
|
11695
11942
|
// and so out of its folds, as the residents share this slot.
|
|
11696
|
-
viewFilter: (message) =>
|
|
11697
|
-
&& !isAnotherAgentsInjection(message.metadata, config.name),
|
|
11943
|
+
viewFilter: (message) => sees(message.metadata),
|
|
11698
11944
|
});
|
|
11699
11945
|
this.trackManager(contextManager);
|
|
11946
|
+
this.storeSees.set(contextManager, sees);
|
|
11700
11947
|
const agent = new Agent(config, contextManager, this.membrane);
|
|
11701
11948
|
this.adoptToolResultGuard(agent);
|
|
11702
11949
|
agent.markContextConsumed(); // RFC-006: nothing stored before now is replaceable
|
|
@@ -11753,6 +12000,7 @@ export class AgentFramework {
|
|
|
11753
12000
|
// oversized tool results / attachments under the same policy.
|
|
11754
12001
|
maxMessageTokens: primaryConfig.strategy?.maxMessageTokens,
|
|
11755
12002
|
});
|
|
12003
|
+
const sees = (metadata) => !isAnotherAgentsInjection(metadata, name);
|
|
11756
12004
|
const contextManager = await ContextManager.open({
|
|
11757
12005
|
store: this.store,
|
|
11758
12006
|
namespace: `subconscious/${primaryName}`,
|
|
@@ -11763,12 +12011,13 @@ export class AgentFramework {
|
|
|
11763
12011
|
// residents' stored context injections (stored-injections.ts): their
|
|
11764
12012
|
// context, which this agent never saw while they were placed unstored.
|
|
11765
12013
|
auxiliaryMessageViews: [{}],
|
|
11766
|
-
viewFilter: (message) =>
|
|
12014
|
+
viewFilter: (message) => sees(message.metadata),
|
|
11767
12015
|
strategy,
|
|
11768
12016
|
membrane: this.membrane,
|
|
11769
12017
|
debugLogContext: !!process.env.DEBUG_CONTEXT,
|
|
11770
12018
|
});
|
|
11771
12019
|
this.trackManager(contextManager);
|
|
12020
|
+
this.storeSees.set(contextManager, sees);
|
|
11772
12021
|
// Inherit the principal's inference settings wholesale (fork-template
|
|
11773
12022
|
// idiom): reasoning effort (thinking), provider params, token ceilings,
|
|
11774
12023
|
// caching, refusal handling, timezone — a same-model side-process must
|
|
@@ -12005,6 +12254,7 @@ export class AgentFramework {
|
|
|
12005
12254
|
flushedTurn.roundsRan = true;
|
|
12006
12255
|
if (flushedTurn.unflushedRounds)
|
|
12007
12256
|
flushedTurn.unflushedRounds--;
|
|
12257
|
+
this.endStatedWaitRun(agent.name);
|
|
12008
12258
|
}
|
|
12009
12259
|
if (flushedTurn && replySent === null && !flushedTurn.privateEnd && !flushedTurn.silent
|
|
12010
12260
|
&& pendingBlocks.some((b) => b.type === 'text' && b.text?.trim())) {
|
|
@@ -12069,11 +12319,12 @@ export class AgentFramework {
|
|
|
12069
12319
|
// with everything queued behind it — the queue keeps its order —
|
|
12070
12320
|
// for the turn-end flush, where no reply follows it in this stream.
|
|
12071
12321
|
//
|
|
12072
|
-
// A message
|
|
12073
|
-
//
|
|
12074
|
-
//
|
|
12075
|
-
//
|
|
12076
|
-
//
|
|
12322
|
+
// A message the agent's own view filter leaves out, such as one a
|
|
12323
|
+
// tune-out holds from the residents or another agent's stored
|
|
12324
|
+
// context injection, is drained and stored like any other, but not
|
|
12325
|
+
// injected: the next compile leaves it out as well, so the window's
|
|
12326
|
+
// compiled prefix and the live request still match without it
|
|
12327
|
+
// (#232 gap 5).
|
|
12077
12328
|
//
|
|
12078
12329
|
// A stream that declares it can't carry injected messages
|
|
12079
12330
|
// (supportsInjectedMessages false: membrane's XML prefill path,
|
|
@@ -12105,14 +12356,11 @@ export class AgentFramework {
|
|
|
12105
12356
|
// counts as stored, as what landed of it (writeDrainedInOrder).
|
|
12106
12357
|
const stored = this.writeDrainedInOrder(deferred, `tool boundary for ${agent.name}`, (msg) => agent.getContextManager().addMessage(msg.participant, msg.content, withDeferredWriteId(msg.metadata, msg.id)));
|
|
12107
12358
|
for (const [msg, { id, content }] of stored) {
|
|
12359
|
+
this.noteSpokenTo(agent.name, msg.metadata);
|
|
12108
12360
|
// Every other resident reads what this boundary stores; for its
|
|
12109
12361
|
// own agent, the boundary weighs what it injects below.
|
|
12110
12362
|
this.noteForOtherResidents(agent, msg.metadata, id);
|
|
12111
|
-
|
|
12112
|
-
// subconscious reads it) and left out of every resident's
|
|
12113
|
-
// compiled view; injecting it here would put in the live turn
|
|
12114
|
-
// what the next compile hides (#232 gap 5).
|
|
12115
|
-
if (msg.metadata?.tuneOut)
|
|
12363
|
+
if (!this.agentSees(agent, msg.metadata))
|
|
12116
12364
|
continue;
|
|
12117
12365
|
heard.push({ msg, id, content });
|
|
12118
12366
|
}
|
|
@@ -12125,10 +12373,12 @@ export class AgentFramework {
|
|
|
12125
12373
|
const rendered = this.renderForInjection(agent, [...(clock ? [clock.id] : []), ...heard.map((h) => h.id)]);
|
|
12126
12374
|
const first = clock ? 1 : 0;
|
|
12127
12375
|
// A null is a message the render places before its window. Under
|
|
12128
|
-
//
|
|
12129
|
-
// what this boundary stored is bigger than it,
|
|
12130
|
-
// while a head window a topic change reset hasn't
|
|
12131
|
-
//
|
|
12376
|
+
// context-manager 0.23.0's memory strategy that's one before its
|
|
12377
|
+
// recent window, when what this boundary stored is bigger than it,
|
|
12378
|
+
// or any message while a head window a topic change reset hasn't
|
|
12379
|
+
// filled yet; from 0.23.1 that strategy gives no null for a
|
|
12380
|
+
// message its view holds. The next compile still sends such a
|
|
12381
|
+
// message, raw and in order, after the tool
|
|
12132
12382
|
// result, so leaving it out would change the prefix as surely as
|
|
12133
12383
|
// any form, and the agent wouldn't hear it (context-manager#275's
|
|
12134
12384
|
// review, R1-1). It goes in as stored, which is the form the next
|
|
@@ -12139,13 +12389,12 @@ export class AgentFramework {
|
|
|
12139
12389
|
// cap reaches it. And a body the strategy stored in parts goes in
|
|
12140
12390
|
// whole, joined as the store reads it back, where the next
|
|
12141
12391
|
// compile sends it in more than one piece.
|
|
12142
|
-
// That rests on two things.
|
|
12143
|
-
//
|
|
12144
|
-
//
|
|
12145
|
-
//
|
|
12146
|
-
//
|
|
12147
|
-
//
|
|
12148
|
-
// windowed passthrough's anchor past it, before the render. A
|
|
12392
|
+
// That rests on two things. A message the agent's own view filter
|
|
12393
|
+
// leaves out is skipped above, by that filter, so no null here
|
|
12394
|
+
// means a message the view hides. And the store and the render
|
|
12395
|
+
// run with nothing between them, so nothing can put a summary in
|
|
12396
|
+
// place of what this flush stored, or move windowed passthrough's
|
|
12397
|
+
// anchor past it, before the render. A
|
|
12149
12398
|
// write found here that landed at an earlier flush
|
|
12150
12399
|
// (writeDrainedInOrder) is the exception: by now a summary may
|
|
12151
12400
|
// stand in for it, or the anchor be past it, and then it goes in
|
|
@@ -12503,6 +12752,10 @@ export class AgentFramework {
|
|
|
12503
12752
|
// Dispatch to all modules, tracking responses with module names
|
|
12504
12753
|
const responses = [];
|
|
12505
12754
|
for (const module of this.moduleRegistry.getAllModules()) {
|
|
12755
|
+
// The framework's own toolsets module answers no event; every agent has
|
|
12756
|
+
// it now, so it isn't listed beside the modules that do.
|
|
12757
|
+
if (module instanceof ToolsetsModule)
|
|
12758
|
+
continue;
|
|
12506
12759
|
try {
|
|
12507
12760
|
const processState = this.moduleRegistry.createProcessState(module.name);
|
|
12508
12761
|
const response = await module.onProcess(event, processState);
|
|
@@ -12589,6 +12842,7 @@ export class AgentFramework {
|
|
|
12589
12842
|
// surface carries where the framework records none.
|
|
12590
12843
|
const eventChannel = event.metadata?.channelId;
|
|
12591
12844
|
const namesChannel = typeof eventChannel === 'string' && eventChannel !== '';
|
|
12845
|
+
const localInput = event.type === 'external-message' || event.type === 'api:message';
|
|
12592
12846
|
let added;
|
|
12593
12847
|
if (response.addMessages) {
|
|
12594
12848
|
for (const [i, msg] of response.addMessages.entries()) {
|
|
@@ -12601,7 +12855,7 @@ export class AgentFramework {
|
|
|
12601
12855
|
if (surface)
|
|
12602
12856
|
metadata = { ...metadata, [INBOUND_SOURCE_KEY]: surface };
|
|
12603
12857
|
const placement = {};
|
|
12604
|
-
const id = this.addMessage(msg.participant, msg.content, metadata, { placement });
|
|
12858
|
+
const id = this.addMessage(msg.participant, localInput ? markNoticeOpenings(msg.content) : msg.content, metadata, { placement });
|
|
12605
12859
|
if (response.addMessages.length === 1 && !surface && !namesChannel && metadata?.channelId === undefined)
|
|
12606
12860
|
added = placement;
|
|
12607
12861
|
this.emitTrace({ type: 'message:added', messageId: id, source: event.type });
|
|
@@ -12816,6 +13070,7 @@ export class AgentFramework {
|
|
|
12816
13070
|
missedCharacters: typeof event.metadata?.missedCharacters === 'number'
|
|
12817
13071
|
? event.metadata.missedCharacters
|
|
12818
13072
|
: undefined,
|
|
13073
|
+
targetAgents: event.targetAgents,
|
|
12819
13074
|
});
|
|
12820
13075
|
if (invitation) {
|
|
12821
13076
|
incomingContent.push(invitation.block);
|
|
@@ -13397,11 +13652,6 @@ export class AgentFramework {
|
|
|
13397
13652
|
* explicit-prose agent's natural plain reply bounced with nowhere to go
|
|
13398
13653
|
* (Rhys's first portal mention: a 2,320-char reply stranded).
|
|
13399
13654
|
*
|
|
13400
|
-
* Wording: option 1 teaches the `>>` destination prefix, which delivers for
|
|
13401
|
-
* BOTH legacy (host-inferred) and explicit prose-routing agents. The
|
|
13402
|
-
* previous "simply write your reply as normal text" was a false promise
|
|
13403
|
-
* under explicit routing (the metadata flags it set had no consumers).
|
|
13404
|
-
*
|
|
13405
13655
|
* Human-readable names wherever the surface provided them — a mind reading
|
|
13406
13656
|
* this should see WHO addressed it WHERE, not a wall of snowflake ids. Raw
|
|
13407
13657
|
* ids remain only inside the tool-argument instructions, where they are the
|
|
@@ -13460,8 +13710,11 @@ export class AgentFramework {
|
|
|
13460
13710
|
// only where the name lacks one, by proseTargetFor's rule.
|
|
13461
13711
|
const bareLabel = label && label !== opts.channelId ? label.replace(/^#/, '') : undefined;
|
|
13462
13712
|
const hashLabel = bareLabel ? `#${bareLabel}` : undefined;
|
|
13463
|
-
const
|
|
13464
|
-
const
|
|
13713
|
+
const channelId = quotedValue(opts.channelId);
|
|
13714
|
+
const messageId = quotedValue(opts.messageId);
|
|
13715
|
+
const authorName = suppliedValue(opts.authorName);
|
|
13716
|
+
const channelLabel = hashLabel ? suppliedValue(hashLabel) : channelId;
|
|
13717
|
+
const place = opts.guildName ? `${channelLabel} in "${suppliedValue(opts.guildName)}"` : channelLabel;
|
|
13465
13718
|
// The label is for reading; the prefix must be ONE token that resolves back
|
|
13466
13719
|
// here, on this server (a DM label "DM: alice" quoted verbatim parses as
|
|
13467
13720
|
// target "#DM:"). A registry that can name targets is authoritative: no safe
|
|
@@ -13470,7 +13723,7 @@ export class AgentFramework {
|
|
|
13470
13723
|
const namer = this.channelRegistry;
|
|
13471
13724
|
const target = typeof namer.proseTargetFor === 'function'
|
|
13472
13725
|
? namer.proseTargetFor(channel)
|
|
13473
|
-
: [hashLabel, opts.channelId].find((t) => !!t && isProseTargetToken(t));
|
|
13726
|
+
: [hashLabel, opts.channelId].find((t) => !!t && isProseTargetToken(t) && showsAsItself(t));
|
|
13474
13727
|
const inboundOnlyServer = this.channelRegistry.inboundOnlyServer(channel);
|
|
13475
13728
|
// What replying does here is the registry's to say (speechOpens): a reply
|
|
13476
13729
|
// opens a channel nobody decided to keep closed, and leaves one the agent
|
|
@@ -13480,23 +13733,39 @@ export class AgentFramework {
|
|
|
13480
13733
|
const replyOpens = this.channelRegistry.speechOpens(channel);
|
|
13481
13734
|
// The decline is the way to reply without opening a channel, except in a
|
|
13482
13735
|
// tuned-out one, which channel_decline refuses: the tune-out ends first.
|
|
13483
|
-
const
|
|
13736
|
+
const tunedOut = this.channelRegistry.getDesiredState(opts.serverId, opts.channelId) === 'tuned-out';
|
|
13737
|
+
const withoutOpening = tunedOut
|
|
13484
13738
|
? ''
|
|
13485
13739
|
: " To reply without opening it, decline it first (option 3): a channel you've declined stays closed when you speak there.";
|
|
13740
|
+
const named = [...new Set(opts.targetAgents ?? [])].filter((name) => this.agents.has(name));
|
|
13741
|
+
const woken = named.length === 1 ? this.agents.get(named[0]) : undefined;
|
|
13742
|
+
const mode = (woken ?? this.deliveryAgent())?.proseRouting ?? 'locus';
|
|
13743
|
+
const prefix = mode === 'hybrid' ? '>>>' : '>>';
|
|
13744
|
+
const delivered = replyOpens
|
|
13745
|
+
? `it will be delivered there, which opens the channel.${withoutOpening}`
|
|
13746
|
+
: 'it will be delivered there, and the channel stays closed.';
|
|
13747
|
+
const sendTool = `check whether ${opts.serverId} exposes a send tool that can address it`;
|
|
13748
|
+
const plainSpeechRule = `your plain speech lands where the latest word on where it lands says: ` +
|
|
13749
|
+
`a [${NOTICE_TAG.routing}] notice, or channel_open's result. If that is ${this.channelShown(channel)}, ` +
|
|
13750
|
+
`write your reply with no prefix;`;
|
|
13486
13751
|
const replyOption = inboundOnlyServer
|
|
13487
13752
|
? `1. Replying there isn't available: ${inboundOnlyServer} declared this channel inbound-only, so nothing ` +
|
|
13488
13753
|
`you write is posted into it. To speak there, use ${inboundOnlyServer}'s own tools.\n`
|
|
13489
|
-
:
|
|
13490
|
-
?
|
|
13491
|
-
|
|
13492
|
-
|
|
13493
|
-
|
|
13494
|
-
|
|
13495
|
-
|
|
13496
|
-
|
|
13497
|
-
|
|
13498
|
-
|
|
13499
|
-
|
|
13754
|
+
: mode === 'disabled'
|
|
13755
|
+
? `1. Replying in plain text isn't available: your plain text is never published. To speak there, ${sendTool}.\n`
|
|
13756
|
+
: mode === 'locus' && tunedOut
|
|
13757
|
+
? `1. Reply — ${plainSpeechRule} it will be delivered there. If that is another channel, or you see none, to reply here, ` +
|
|
13758
|
+
`${sendTool}.\n`
|
|
13759
|
+
: mode === 'locus'
|
|
13760
|
+
? `1. Reply — ${plainSpeechRule} ${delivered} If that is another channel, or you see none, to reply here, ` +
|
|
13761
|
+
`${sendTool}, or open it (option 2), whose result says where your plain speech lands.\n`
|
|
13762
|
+
: target
|
|
13763
|
+
? `1. Reply — write your reply this turn prefixed with "${prefix}${target}"; ${delivered}\n`
|
|
13764
|
+
: `1. Reply — no single-word target reaches this channel unambiguously, ` +
|
|
13765
|
+
`so a "${prefix}" prefix can't name it; ${sendTool}.` +
|
|
13766
|
+
(replyOpens
|
|
13767
|
+
? ` Replying there opens the channel.${withoutOpening}\n`
|
|
13768
|
+
: ` Replying there leaves the channel closed.\n`);
|
|
13500
13769
|
const missedCount = opts.missedMessages ?? 0;
|
|
13501
13770
|
const missedNote = missedCount > 0
|
|
13502
13771
|
? `While closed, ${missedCount} message${missedCount === 1 ? '' : 's'}` +
|
|
@@ -13505,18 +13774,18 @@ export class AgentFramework {
|
|
|
13505
13774
|
: '';
|
|
13506
13775
|
const block = {
|
|
13507
13776
|
type: 'text',
|
|
13508
|
-
text: `\n[
|
|
13777
|
+
text: `\n[${NOTICE_TAG.channelInvitation}] ${authorName} addressed you in ${place} — ` +
|
|
13509
13778
|
`a channel you haven't opened. Messages there that address you, such as an @-mention, ` +
|
|
13510
13779
|
`reach you whether or not you open it. ` +
|
|
13511
13780
|
missedNote +
|
|
13512
13781
|
`Your options:\n` +
|
|
13513
13782
|
replyOption +
|
|
13514
|
-
`2. Open the channel — call channel_open with channelId
|
|
13783
|
+
`2. Open the channel — call channel_open with channelId ${channelId} and serverId "${opts.serverId}"` +
|
|
13515
13784
|
(maxBackscroll > 0
|
|
13516
|
-
? `; to also read recent history, add backscroll (a number up to ${maxBackscroll}) and beforeMessageId
|
|
13785
|
+
? `; to also read recent history, add backscroll (a number up to ${maxBackscroll}) and beforeMessageId ${messageId}.\n`
|
|
13517
13786
|
: `.\n`) +
|
|
13518
|
-
`3. Stay out — call channel_decline with channelId
|
|
13519
|
-
`and messageId
|
|
13787
|
+
`3. Stay out — call channel_decline with channelId ${channelId}, serverId "${opts.serverId}", ` +
|
|
13788
|
+
`and messageId ${messageId} (optionally set acknowledge to an emoji like 👀 so ${authorName} isn't left hanging).\n` +
|
|
13520
13789
|
`Doing nothing is also fine.`,
|
|
13521
13790
|
};
|
|
13522
13791
|
return {
|
|
@@ -14132,7 +14401,7 @@ export class AgentFramework {
|
|
|
14132
14401
|
messageId: identity.messageId ?? str(origin.messageId) ?? occ.eventId,
|
|
14133
14402
|
threadId: identity.threadId ?? str(origin.threadId),
|
|
14134
14403
|
author: identity.author ?? { id: str(origin.authorId) ?? '', name: str(origin.authorName) ?? str(origin.authorId) ?? 'Someone' },
|
|
14135
|
-
content: materialized ? materialized
|
|
14404
|
+
content: materialized ? convertBlocks(materialized, 'attachment on push event', occ.serverId) : push.content,
|
|
14136
14405
|
timestamp: occ.timestamp,
|
|
14137
14406
|
metadata: origin,
|
|
14138
14407
|
tags: push.tags,
|
|
@@ -14153,7 +14422,7 @@ export class AgentFramework {
|
|
|
14153
14422
|
...occ.event.event,
|
|
14154
14423
|
eventId: occ.eventId,
|
|
14155
14424
|
timestamp: occ.timestamp,
|
|
14156
|
-
...(materialized ? { content: materialized
|
|
14425
|
+
...(materialized ? { content: convertBlocks(materialized, 'attachment on push event', occ.serverId) } : {}),
|
|
14157
14426
|
coalescingSubject,
|
|
14158
14427
|
// §5.2: the materialized occurrence is consumed by the request being
|
|
14159
14428
|
// assembled; the wake that started it is not repeated.
|
|
@@ -14290,21 +14559,13 @@ export class AgentFramework {
|
|
|
14290
14559
|
// Every agent that can read this message must not have compiled past it.
|
|
14291
14560
|
// Residents share one message slot, so a message one of them read is
|
|
14292
14561
|
// history for all of them (the never-rewrite rule is about the message,
|
|
14293
|
-
// not the agent that happened to be asked). A
|
|
14294
|
-
//
|
|
14295
|
-
//
|
|
14296
|
-
|
|
14297
|
-
|
|
14298
|
-
|
|
14299
|
-
|
|
14300
|
-
return true;
|
|
14301
|
-
readers = [subconscious];
|
|
14302
|
-
}
|
|
14303
|
-
else {
|
|
14304
|
-
readers = this.sharedSlotAgents.has(agent)
|
|
14305
|
-
? [...this.sharedSlotAgents].filter((a) => this.agents.get(a.name) === a)
|
|
14306
|
-
: [agent];
|
|
14307
|
-
}
|
|
14562
|
+
// not the agent that happened to be asked). A reader is one whose view
|
|
14563
|
+
// filter keeps the message: a tune-out-diverted message never enters a
|
|
14564
|
+
// resident's compiled view, and only the subconscious reads it through
|
|
14565
|
+
// its merged view — no subconscious, nobody reads it.
|
|
14566
|
+
const readers = (this.sharedSlotAgents.has(agent)
|
|
14567
|
+
? [...this.sharedSlotAgents].filter((a) => this.agents.get(a.name) === a)
|
|
14568
|
+
: [agent]).filter((a) => this.agentSees(a, message.metadata));
|
|
14308
14569
|
const branch = cm.currentBranch().name;
|
|
14309
14570
|
const ts = message.timestamp instanceof Date ? message.timestamp.getTime() : Number(message.timestamp);
|
|
14310
14571
|
for (const reader of readers) {
|
|
@@ -14442,6 +14703,7 @@ export class AgentFramework {
|
|
|
14442
14703
|
tags: event.tags,
|
|
14443
14704
|
missedMessages: typeof origin.missedMessages === 'number' ? origin.missedMessages : undefined,
|
|
14444
14705
|
missedCharacters: typeof origin.missedCharacters === 'number' ? origin.missedCharacters : undefined,
|
|
14706
|
+
targetAgents: event.targetAgents,
|
|
14445
14707
|
});
|
|
14446
14708
|
if (invitation) {
|
|
14447
14709
|
content.push(invitation.block);
|
|
@@ -14661,7 +14923,8 @@ export class AgentFramework {
|
|
|
14661
14923
|
// A provider wait binds its own model; with a different primary model
|
|
14662
14924
|
// selected now, the turn proceeds (the wait stays recorded).
|
|
14663
14925
|
const otherModel = providerCooldown.waitModel !== undefined && providerCooldown.waitModel !== agent.model;
|
|
14664
|
-
if (!otherModel &&
|
|
14926
|
+
if (!otherModel && !providerCooldown.rearm
|
|
14927
|
+
&& (now < providerCooldown.until || this.extendHostProviderHold(agentName, providerCooldown))) {
|
|
14665
14928
|
for (const req of requests)
|
|
14666
14929
|
this.holdRequest(providerCooldown.heldRequests, req);
|
|
14667
14930
|
const recovery = this.providerAccelerationRecoveries.get(agentName);
|
|
@@ -14674,6 +14937,10 @@ export class AgentFramework {
|
|
|
14674
14937
|
// the held causes into this one current compile.
|
|
14675
14938
|
clearTimeout(providerCooldown.timer);
|
|
14676
14939
|
this.providerAccelerationCooldowns.delete(agentName);
|
|
14940
|
+
if (providerCooldown.rearm) {
|
|
14941
|
+
console.error(`[inference-rearm] agent=${agentName} turn queued again with a wake ` +
|
|
14942
|
+
`(${providerCooldown.rearm.number} of ${providerCooldown.rearm.of})`);
|
|
14943
|
+
}
|
|
14677
14944
|
const recovery = this.providerAccelerationRecoveries.get(agentName);
|
|
14678
14945
|
if (recovery)
|
|
14679
14946
|
recovery.releasedAt = now;
|
|
@@ -14754,7 +15021,10 @@ export class AgentFramework {
|
|
|
14754
15021
|
// and a wake for a deferred write or a stored message is dropped
|
|
14755
15022
|
// once the agent is done with that write: neither may absorb an
|
|
14756
15023
|
// unrelated wake that would vanish with it.
|
|
14757
|
-
|
|
15024
|
+
// A heartbeat's wake keeps its own key, so a later channel-less wake
|
|
15025
|
+
// can't absorb it: resumed together, they make a heartbeat's batch
|
|
15026
|
+
// (heartbeatTurn), as they would have without the quiesce.
|
|
15027
|
+
const key = `${request.reason}|${request.addressed ? 'a' : 'n'}|${isHeartbeatWake(request) ? 'h' : ''}|${request.coalescingSubject ?? ''}|${request.readsDeferredWrite ?? ''}|${request.readsStoredMessage ?? ''}`;
|
|
14758
15028
|
const prev = newestByKey.get(key);
|
|
14759
15029
|
const kept = !prev || request.timestamp >= prev.timestamp ? request : prev;
|
|
14760
15030
|
// The wake it absorbs hands on its lines (WakeLine): each event's
|
|
@@ -14955,13 +15225,14 @@ export class AgentFramework {
|
|
|
14955
15225
|
// an open channel — legacy last-inbound semantics).
|
|
14956
15226
|
// A chat:system notice (RFC-001 rev 3: weather in a world, someone
|
|
14957
15227
|
// joining) names no channel at all: nobody spoke there, so a turn it
|
|
14958
|
-
// wakes alone takes the global fallback,
|
|
14959
|
-
//
|
|
15228
|
+
// wakes alone takes the global fallback, and the agent sends there
|
|
15229
|
+
// explicitly if it means to.
|
|
14960
15230
|
// Ambient chatter must not outrank an addressed message just by being
|
|
14961
15231
|
// newest (2026-07-21 Cairn lounge misroute, turn-start variant).
|
|
14962
15232
|
// Non-channel wakes (heartbeats, module events, reactions — which never
|
|
14963
15233
|
// carry channelId) leave both undefined → global fallback, which an
|
|
14964
|
-
// ephemeral run never takes (takesGlobalFallback)
|
|
15234
|
+
// ephemeral run never takes (takesGlobalFallback), except a heartbeat's
|
|
15235
|
+
// turn, which takes none (heartbeatTurn below).
|
|
14965
15236
|
// Track the winning REQUEST, not just its channel: channel, addressed
|
|
14966
15237
|
// and counterparty must come from the same message, or a batch of
|
|
14967
15238
|
// "ambient from A, then addressed from B" would report B's channel
|
|
@@ -15042,6 +15313,9 @@ export class AgentFramework {
|
|
|
15042
15313
|
wakeLines: mergeWakeLines(requests),
|
|
15043
15314
|
suppressProse: silentOnly ? trigger?.suppressProse : undefined,
|
|
15044
15315
|
silentHeartbeat: silentOnly ? trigger?.silentHeartbeat : undefined,
|
|
15316
|
+
// A heartbeat's turn when any cause is a heartbeat's wake and none
|
|
15317
|
+
// names a channel, whatever the batch's order (isHeartbeatWake).
|
|
15318
|
+
heartbeatTurn: !channelReq && requests.some((r) => isHeartbeatWake(r)),
|
|
15045
15319
|
// The channel and its server from the one request, so the turn's
|
|
15046
15320
|
// channel is that message's, never one server's id with another's.
|
|
15047
15321
|
channelId: channelReq?.channelId,
|
|
@@ -15070,6 +15344,10 @@ export class AgentFramework {
|
|
|
15070
15344
|
start[TURN_CARRY] = carry;
|
|
15071
15345
|
else
|
|
15072
15346
|
delete start[TURN_CARRY];
|
|
15347
|
+
if (requests.every(isRearmed))
|
|
15348
|
+
start[REARMED] = true;
|
|
15349
|
+
else
|
|
15350
|
+
delete start[REARMED];
|
|
15073
15351
|
// A start that carries a turn on and took in wakes that don't records
|
|
15074
15352
|
// the batch it was merged from (MERGED_BATCH): until it takes its
|
|
15075
15353
|
// token it stands for them all.
|
|
@@ -15291,7 +15569,7 @@ export class AgentFramework {
|
|
|
15291
15569
|
if (hold.kind === 'place') {
|
|
15292
15570
|
const { place } = hold;
|
|
15293
15571
|
const where = this.channelShown(place.to);
|
|
15294
|
-
return `your ${place.tool} ${said(place.round)} ${place.delivered ? 'delivered' : 'may have posted'} to ${where}, ` +
|
|
15572
|
+
return `your ${suppliedValue(place.tool)} ${said(place.round)} ${place.delivered ? 'delivered' : 'may have posted'} to ${where}, ` +
|
|
15295
15573
|
`where this was going${lasts(place.round, true)}`;
|
|
15296
15574
|
}
|
|
15297
15575
|
const why = hold.why === 'error'
|
|
@@ -15304,7 +15582,7 @@ export class AgentFramework {
|
|
|
15304
15582
|
? "it hadn't answered when this speech was routed"
|
|
15305
15583
|
: "it hadn't answered by the end of its round";
|
|
15306
15584
|
const going = target ? `, and this was going to ${this.channelShown(target)}` : '';
|
|
15307
|
-
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)}`;
|
|
15585
|
+
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)}`;
|
|
15308
15586
|
}
|
|
15309
15587
|
/**
|
|
15310
15588
|
* Hold plain speech as drafts (src/prose-drafts.ts) instead of
|
|
@@ -15378,7 +15656,7 @@ export class AgentFramework {
|
|
|
15378
15656
|
const r = registry.resolveProseTarget(name, { forSpeech: true, own });
|
|
15379
15657
|
return 'channelId' in r && sameChannel(r, target) ? name : undefined;
|
|
15380
15658
|
};
|
|
15381
|
-
const destination =
|
|
15659
|
+
const destination = quotedValue((target && (resent(registry?.proseTargetFor(target)) ?? resent(qualifiedChannelName(target)))) ?? NO_CHANNEL_EXAMPLE);
|
|
15382
15660
|
// Words copied from a draft that may already have been posted resend
|
|
15383
15661
|
// only with the resident's confirmation; the rest resend freely.
|
|
15384
15662
|
const free = held.filter((d) => draftState(d) !== 'unconfirmed');
|
|
@@ -15393,7 +15671,7 @@ export class AgentFramework {
|
|
|
15393
15671
|
];
|
|
15394
15672
|
// Named: residents that share one message slot (#197) share this window.
|
|
15395
15673
|
// "Not sent" only when no copied words may already have been posted.
|
|
15396
|
-
return (`[drafts] ${agentName}: ${risky.length > 0 ? '' : 'not sent — '}` +
|
|
15674
|
+
return (`[${NOTICE_TAG.drafts}] ${agentName}: ${risky.length > 0 ? '' : 'not sent — '}` +
|
|
15397
15675
|
`${many ? `${held.length} plain-speech segments` : 'a plain-speech segment'} ` +
|
|
15398
15676
|
`held as draft${many ? 's' : ''} ${ids.join(', ')} — ${because ? `because ${because}` : AgentFramework.DRAFT_REASON_TEXT[reason]}. ` +
|
|
15399
15677
|
`${resend.join('; ')}; drafts(action: "dismiss", draftIds: [${quoted(held)}]) sets ${many ? 'them' : 'it'} aside.`);
|
|
@@ -15410,8 +15688,9 @@ export class AgentFramework {
|
|
|
15410
15688
|
/** A destination as the agent is shown it: its label, with its name as
|
|
15411
15689
|
* ChannelRegistry.nameShown gives it. */
|
|
15412
15690
|
destinationText(d) {
|
|
15413
|
-
const
|
|
15414
|
-
|
|
15691
|
+
const shown = d.label && d.label !== d.channelId ? suppliedValue(d.label) : '';
|
|
15692
|
+
const label = shown ? `${shown.startsWith('#') || shown.startsWith('DM') ? '' : '#'}${shown} ` : '';
|
|
15693
|
+
return `${label}(${this.channelRegistry?.nameShown(d) ?? suppliedValue(d.channelId)})`;
|
|
15415
15694
|
}
|
|
15416
15695
|
/** One line per draft: id, state, when, why, size, preview. */
|
|
15417
15696
|
draftLine(draft) {
|
|
@@ -15441,7 +15720,7 @@ export class AgentFramework {
|
|
|
15441
15720
|
const risky = uncertainAttempt(draft);
|
|
15442
15721
|
if (risky) {
|
|
15443
15722
|
return `${draft.id}'s attempt to ${this.destinationText(risky.destination)} at ` +
|
|
15444
|
-
`${this.draftTime(risky.at)} may already have been posted: ${risky.outcome?.reason
|
|
15723
|
+
`${this.draftTime(risky.at)} may already have been posted: ${risky.outcome?.reason !== undefined ? suppliedValue(risky.outcome.reason) : this.noOutcomeText(draft, risky)}`;
|
|
15445
15724
|
}
|
|
15446
15725
|
const inherited = draft.inheritedRisk;
|
|
15447
15726
|
const owner = inherited.sourceDraftId ?? inherited.draftId;
|
|
@@ -15450,7 +15729,7 @@ export class AgentFramework {
|
|
|
15450
15729
|
? `${owner}'s attempt to ${this.destinationText(inherited.destination)}` +
|
|
15451
15730
|
(inherited.at !== undefined ? ` at ${this.draftTime(inherited.at)}` : '')
|
|
15452
15731
|
: owner;
|
|
15453
|
-
return `${draft.id} includes the words of ${inherited.draftId}${via}, and ${where} may already have been posted: ${inherited.reason}`;
|
|
15732
|
+
return `${draft.id} includes the words of ${inherited.draftId}${via}, and ${where} may already have been posted: ${suppliedValue(inherited.reason)}`;
|
|
15454
15733
|
}
|
|
15455
15734
|
/** Whether an attempt with no outcome is still out: its draft's latest,
|
|
15456
15735
|
* while that draft is being sent. Otherwise it never got one (a crash). */
|
|
@@ -15469,7 +15748,7 @@ export class AgentFramework {
|
|
|
15469
15748
|
return `${draft.id}: not delivered`;
|
|
15470
15749
|
return `${draft.id}: delivered — confirmed at ${this.draftTime(delivered.outcome.at)} to ` +
|
|
15471
15750
|
`${this.destinationText(delivered.destination)}` +
|
|
15472
|
-
(delivered.outcome.messageId ? `, message ${delivered.outcome.messageId}` : '') +
|
|
15751
|
+
(delivered.outcome.messageId ? `, message ${nameToTypeBack(delivered.outcome.messageId)}` : '') +
|
|
15473
15752
|
' (a record of that delivery, not a check that the message is still there)';
|
|
15474
15753
|
}
|
|
15475
15754
|
/**
|
|
@@ -15514,7 +15793,7 @@ export class AgentFramework {
|
|
|
15514
15793
|
const shown = pending.slice(0, 8).map((d) => `${d.id} (${this.draftStateText(d)}; ${AgentFramework.DRAFT_REASON_SHORT[d.reason]})`);
|
|
15515
15794
|
const rest = pending.slice(shown.length).map((d) => d.id);
|
|
15516
15795
|
const more = rest.length > 0 ? ` · and ${rest.length} more: ${rest.join(', ')}` : '';
|
|
15517
|
-
const text = `[drafts] ${agent.name}: ${pending.length === 1 ? 'an open draft' : `${pending.length} open drafts`} of yours ` +
|
|
15796
|
+
const text = `[${NOTICE_TAG.drafts}] ${agent.name}: ${pending.length === 1 ? 'an open draft' : `${pending.length} open drafts`} of yours ` +
|
|
15518
15797
|
`that no recent notice on this branch names: ${shown.join(' · ')}${more}. ` +
|
|
15519
15798
|
'drafts(action: "list") shows your open drafts; resend or dismiss them by id.';
|
|
15520
15799
|
let mid;
|
|
@@ -15751,17 +16030,17 @@ export class AgentFramework {
|
|
|
15751
16030
|
const blocks = drafts.map((d) => {
|
|
15752
16031
|
const lines = [
|
|
15753
16032
|
`${d.id} — ${draftState(d)}; held ${this.draftTime(d.heldAt)} (${AgentFramework.DRAFT_REASON_TEXT[d.reason]})` +
|
|
15754
|
-
(d.note ? `; ${d.note}` : ''),
|
|
16033
|
+
(d.note ? `; ${suppliedValue(d.note)}` : ''),
|
|
15755
16034
|
...(d.inheritedRisk && draftState(d) === 'unconfirmed' && !uncertainAttempt(d) ? [` ${this.riskText(d)}`] : []),
|
|
15756
16035
|
...d.attempts.map((a) => {
|
|
15757
16036
|
const o = a.outcome;
|
|
15758
16037
|
const status = !o
|
|
15759
16038
|
? (this.attemptIsOut(d, a) ? 'being sent right now — no outcome yet' : 'outcome never recorded — it may have been posted')
|
|
15760
16039
|
: o.status === 'delivered'
|
|
15761
|
-
? `delivered${o.messageId ? `, message ${o.messageId}` : ''}`
|
|
16040
|
+
? `delivered${o.messageId ? `, message ${nameToTypeBack(o.messageId)}` : ''}`
|
|
15762
16041
|
: o.status === 'unknown'
|
|
15763
|
-
? `UNKNOWN — may have been posted: ${o.reason
|
|
15764
|
-
: `failed, nothing posted: ${o.reason
|
|
16042
|
+
? `UNKNOWN — may have been posted: ${o.reason !== undefined ? suppliedValue(o.reason) : 'no valid receipt'}`
|
|
16043
|
+
: `failed, nothing posted: ${o.reason !== undefined ? suppliedValue(o.reason) : 'refused'}`;
|
|
15765
16044
|
return ` attempt ${this.draftTime(a.at)} → ${this.destinationText(a.destination)}: ${status}` +
|
|
15766
16045
|
(o?.status === 'unknown' && o.detail !== undefined ? `; connector detail: ${connectorText(o.detail)}` : '') +
|
|
15767
16046
|
(a.confirmedDuplicate ? ' (resent knowing it might duplicate)' : '');
|
|
@@ -15854,8 +16133,10 @@ export class AgentFramework {
|
|
|
15854
16133
|
// its turn a name the turn's own channel answers to means that channel.
|
|
15855
16134
|
const resolved = registry.resolveProseTarget(spec, { forSpeech: true, own });
|
|
15856
16135
|
if ('error' in resolved) {
|
|
15857
|
-
const cand = resolved.candidates?.length
|
|
15858
|
-
|
|
16136
|
+
const cand = resolved.candidates?.length
|
|
16137
|
+
? ` It could mean: ${resolved.candidates.join(', ')}.${registry.publishRouteFor(resolved.candidates) ?? ''}`
|
|
16138
|
+
: '';
|
|
16139
|
+
return refuse(`Destination "${spec}" did not resolve: ${suppliedValue(resolved.error)}.${cand} Nothing was sent.`);
|
|
15859
16140
|
}
|
|
15860
16141
|
named = resolved;
|
|
15861
16142
|
}
|
|
@@ -15863,9 +16144,9 @@ export class AgentFramework {
|
|
|
15863
16144
|
named = home;
|
|
15864
16145
|
}
|
|
15865
16146
|
const target = registry.resolveDestination({ serverId: named.serverId, channelId: named.channelId });
|
|
15866
|
-
// Channels named as the agent names them back (
|
|
16147
|
+
// Channels named as the agent names them back (quotedShown).
|
|
15867
16148
|
if ('error' in target)
|
|
15868
|
-
return refuse(`Destination "${spec
|
|
16149
|
+
return refuse(`Destination ${spec ? `"${spec}"` : registry.quotedShown(home)}: ${suppliedValue(target.error)}. Nothing was sent.`);
|
|
15869
16150
|
const destination = target.destination;
|
|
15870
16151
|
if (home && !sameChannel(destination, home)) {
|
|
15871
16152
|
return refuse(`This conversation is bound to channel ${registry.quotedShown(home)}; resending to ${registry.quotedShown(destination)} ` +
|
|
@@ -15954,7 +16235,7 @@ export class AgentFramework {
|
|
|
15954
16235
|
// it, so say what the draft reads as now rather than guess.
|
|
15955
16236
|
const unrecorded = recorded ? '' : ` (recording this outcome failed; the draft reads as ${this.draftStateNow(agentName, d.id)} now)`;
|
|
15956
16237
|
if (outcome.status === 'delivered') {
|
|
15957
|
-
lines.push(`${d.id}: delivered to ${where}${outcome.messageId ? `, message ${outcome.messageId}` : ''}${unrecorded}.`);
|
|
16238
|
+
lines.push(`${d.id}: delivered to ${where}${outcome.messageId ? `, message ${nameToTypeBack(outcome.messageId)}` : ''}${unrecorded}.`);
|
|
15958
16239
|
sendingTurn?.engaged.add(destinationKey(destination));
|
|
15959
16240
|
resender?.notePublished(destinationKey(destination), readThrough);
|
|
15960
16241
|
continue;
|
|
@@ -15965,9 +16246,9 @@ export class AgentFramework {
|
|
|
15965
16246
|
// The connector's data may say what a multi-part send already posted.
|
|
15966
16247
|
const detail = outcome.status === 'unknown' && outcome.detail !== undefined ? ` Connector detail: ${connectorText(outcome.detail)}` : '';
|
|
15967
16248
|
lines.push((outcome.status === 'unknown'
|
|
15968
|
-
? `${d.id}: delivery to ${where} NOT confirmed — it may or may not have been posted: ${outcome.reason
|
|
16249
|
+
? `${d.id}: delivery to ${where} NOT confirmed — it may or may not have been posted: ${outcome.reason !== undefined ? suppliedValue(outcome.reason) : 'no valid receipt'}. ` +
|
|
15969
16250
|
`Sending it again could post it twice, so a resend needs confirmDuplicate: true${unrecorded}.`
|
|
15970
|
-
: `${d.id}: not sent to ${where} — ${outcome.reason
|
|
16251
|
+
: `${d.id}: not sent to ${where} — ${outcome.reason !== undefined ? suppliedValue(outcome.reason) : 'refused'}. Nothing was posted by this attempt${unrecorded}.` +
|
|
15971
16252
|
(stillAtRisk
|
|
15972
16253
|
? ` The draft stays unconfirmed: ${this.riskText(after)}.`
|
|
15973
16254
|
: ' The draft stays held.')) + detail);
|
|
@@ -15993,6 +16274,37 @@ export class AgentFramework {
|
|
|
15993
16274
|
}
|
|
15994
16275
|
return undefined;
|
|
15995
16276
|
}
|
|
16277
|
+
/** Why a heartbeat's turn holds its plain speech, in a held-draft notice ("because …"). */
|
|
16278
|
+
static HEARTBEAT_HOLD_BECAUSE = 'this turn came from your heartbeat, not from a channel, so its plain speech has no destination ' +
|
|
16279
|
+
'and is kept for you to send where you choose';
|
|
16280
|
+
/**
|
|
16281
|
+
* Locus mode's delivery of plain speech to the turn's frozen locus. A
|
|
16282
|
+
* heartbeat's turn with no locus of its own (TurnRouting.heartbeat) holds
|
|
16283
|
+
* the words as a draft instead of failing the delivery: nobody was waiting
|
|
16284
|
+
* for them, and the agent may send them where it chooses.
|
|
16285
|
+
*/
|
|
16286
|
+
async routeLocusSpeech(agent, round, text, locus) {
|
|
16287
|
+
if (locus === null && round.routing.heartbeat) {
|
|
16288
|
+
this.holdHeartbeatProse(agent, [text], round);
|
|
16289
|
+
return null;
|
|
16290
|
+
}
|
|
16291
|
+
return this.channelRegistry.routeSpeech(agent.name, text, locus);
|
|
16292
|
+
}
|
|
16293
|
+
/** The mark a framework wake takes from a heartbeat's turn with no locus
|
|
16294
|
+
* of its own (InferenceRequest.heartbeatTurn), so the turn it opens is a
|
|
16295
|
+
* heartbeat's too; nothing for any other turn. */
|
|
16296
|
+
heartbeatTurnMark(routing) {
|
|
16297
|
+
return routing.heartbeat && !routing.locus ? { heartbeatTurn: true } : {};
|
|
16298
|
+
}
|
|
16299
|
+
/** Hold a heartbeat turn's plain speech as drafts ('no-destination'), named by `round`'s notice. */
|
|
16300
|
+
holdHeartbeatProse(agent, texts, round) {
|
|
16301
|
+
console.error(`[prose] ${agent.name}: heartbeat turn — ${texts.length} plain-speech segment(s) held as drafts (no destination)`);
|
|
16302
|
+
return this.holdProseDrafts(agent, texts, 'no-destination', {
|
|
16303
|
+
routing: round.routing,
|
|
16304
|
+
notice: round.draftsRound ? 'later' : round.notice,
|
|
16305
|
+
because: AgentFramework.HEARTBEAT_HOLD_BECAUSE,
|
|
16306
|
+
});
|
|
16307
|
+
}
|
|
15996
16308
|
/** Record a successful plain-prose delivery for this turn's receipt. */
|
|
15997
16309
|
recordProseDelivery(routing, outcome) {
|
|
15998
16310
|
if (!outcome?.delivered)
|
|
@@ -16120,10 +16432,10 @@ export class AgentFramework {
|
|
|
16120
16432
|
// reports itself in its tool result. Disabled mode holds no drafts, so
|
|
16121
16433
|
// it is never uncertain.
|
|
16122
16434
|
const text = shown.length > 0
|
|
16123
|
-
? `[delivered] plain speech → ${shown.join(' · ')}${suppressedNote ? ` · ${suppressedNote}` : ''}${closedNote}`
|
|
16435
|
+
? `[${NOTICE_TAG.delivered}] plain speech → ${shown.join(' · ')}${suppressedNote ? ` · ${suppressedNote}` : ''}${closedNote}`
|
|
16124
16436
|
: agent.proseRouting === 'disabled'
|
|
16125
|
-
? `[delivered] plain speech → no channel — ${suppressedNote}`
|
|
16126
|
-
: `[delivered] nothing${uncertain ? ' confirmed' : ''} — ${suppressedNote}`;
|
|
16437
|
+
? `[${NOTICE_TAG.delivered}] plain speech → no channel — ${suppressedNote}`
|
|
16438
|
+
: `[${NOTICE_TAG.delivered}] nothing${uncertain ? ' confirmed' : ''} — ${suppressedNote}`;
|
|
16127
16439
|
try {
|
|
16128
16440
|
const mid = agent.getContextManager().addMessage('user', [{ type: 'text', text }], {
|
|
16129
16441
|
system: true,
|
|
@@ -16142,7 +16454,7 @@ export class AgentFramework {
|
|
|
16142
16454
|
try {
|
|
16143
16455
|
agent.getContextManager().addMessage('user', [{
|
|
16144
16456
|
type: 'text',
|
|
16145
|
-
text: `[
|
|
16457
|
+
text: `[${NOTICE_TAG.speechNotPosted}] Nothing you wrote before your tool calls in the rounds just above was posted. ` +
|
|
16146
16458
|
`If any of it should go out, send it again.`,
|
|
16147
16459
|
}], { system: true, kind: 'speech-not-posted' });
|
|
16148
16460
|
delete routing.unpostedSpeech;
|
|
@@ -16153,7 +16465,7 @@ export class AgentFramework {
|
|
|
16153
16465
|
}
|
|
16154
16466
|
noteFailedTurnEnd(agent, routing) {
|
|
16155
16467
|
const callsCutOff = (routing.unflushedRounds ?? 0) > 0;
|
|
16156
|
-
if (routing
|
|
16468
|
+
if (gotPartway(routing)) {
|
|
16157
16469
|
this.exhaustedTurns?.set(agent.name, {
|
|
16158
16470
|
roundsRan: routing.roundsRan === true, callsCutOff, liveSpeech: routing.liveSpeech === true,
|
|
16159
16471
|
speechRouting: (routing.speechRouting ?? 0) > 0,
|
|
@@ -16187,12 +16499,32 @@ export class AgentFramework {
|
|
|
16187
16499
|
if (!target)
|
|
16188
16500
|
return;
|
|
16189
16501
|
try {
|
|
16190
|
-
this.addMessage('user', [{ type: 'text', text: `[
|
|
16502
|
+
this.addMessage('user', [{ type: 'text', text: `[${NOTICE_TAG.closedChannel}] ${notes.join(' ')}${this.closedChannelNoteOff(agent.name)}` }], { system: true, kind: 'delivery-receipt' }, target);
|
|
16191
16503
|
}
|
|
16192
16504
|
catch (err) {
|
|
16193
16505
|
console.error('closed-channel note append failed:', err);
|
|
16194
16506
|
}
|
|
16195
16507
|
}
|
|
16508
|
+
/**
|
|
16509
|
+
* Tell a heartbeat's turn, before it compiles, that its plain speech has
|
|
16510
|
+
* no destination (TurnRouting.heartbeat): the last `[routing]` notice may
|
|
16511
|
+
* name a channel, and this turn's words won't go there. It leaves the
|
|
16512
|
+
* announced baseline alone (announceLocusIfChanged).
|
|
16513
|
+
*/
|
|
16514
|
+
noticeHeartbeatTurn(agentName) {
|
|
16515
|
+
const agent = this.agents.get(agentName);
|
|
16516
|
+
if (!agent)
|
|
16517
|
+
return;
|
|
16518
|
+
const text = `[${NOTICE_TAG.routing}] This turn came from your heartbeat, not from a channel, so plain speech here isn't posted: ` +
|
|
16519
|
+
'it\'s held as a draft you can send with the drafts tool. An explicit send reaches a channel now.';
|
|
16520
|
+
try {
|
|
16521
|
+
const id = agent.getContextManager().addMessage('user', [{ type: 'text', text }], { system: true, kind: 'routing-notice' });
|
|
16522
|
+
this.emitTrace({ type: 'message:added', messageId: id, source: 'routing-notice' });
|
|
16523
|
+
}
|
|
16524
|
+
catch (err) {
|
|
16525
|
+
console.error('noticeHeartbeatTurn: failed to record the notice:', err);
|
|
16526
|
+
}
|
|
16527
|
+
}
|
|
16196
16528
|
/**
|
|
16197
16529
|
* Announce-on-change locus notice (turn-frozen routing). Appends a durable
|
|
16198
16530
|
* `[routing]` window message when the turn's effective outbound locus
|
|
@@ -16217,16 +16549,16 @@ export class AgentFramework {
|
|
|
16217
16549
|
let text;
|
|
16218
16550
|
if (locus === null) {
|
|
16219
16551
|
text =
|
|
16220
|
-
|
|
16552
|
+
`[${NOTICE_TAG.routing}] Your plain speech currently has no channel — it stays in ` +
|
|
16221
16553
|
'your archive. Use an explicit send tool to reach a channel.';
|
|
16222
16554
|
}
|
|
16223
16555
|
else {
|
|
16224
16556
|
const shown = this.channelShown(locus);
|
|
16225
16557
|
const inboundOnlyServer = this.channelRegistry?.inboundOnlyServer(locus);
|
|
16226
16558
|
text = inboundOnlyServer
|
|
16227
|
-
? `[routing] The conversation is now in ${shown}. ${inboundOnlySpeechNote(inboundOnlyServer)} ` +
|
|
16559
|
+
? `[${NOTICE_TAG.routing}] The conversation is now in ${shown}. ${inboundOnlySpeechNote(inboundOnlyServer)} ` +
|
|
16228
16560
|
'Other channels need an explicit send tool.'
|
|
16229
|
-
: `[routing] Your plain speech now lands in ${shown}. ` +
|
|
16561
|
+
: `[${NOTICE_TAG.routing}] Your plain speech now lands in ${shown}. ` +
|
|
16230
16562
|
'Other channels need an explicit send tool.';
|
|
16231
16563
|
}
|
|
16232
16564
|
try {
|
|
@@ -16499,18 +16831,23 @@ export class AgentFramework {
|
|
|
16499
16831
|
}
|
|
16500
16832
|
return { newestRowAt, lastClockAt: undefined };
|
|
16501
16833
|
}
|
|
16834
|
+
agentSees(agent, metadata) {
|
|
16835
|
+
const sees = this.storeSees.get(agent.getContextManager());
|
|
16836
|
+
return !sees || sees(metadata);
|
|
16837
|
+
}
|
|
16502
16838
|
/**
|
|
16503
16839
|
* Weigh a message that no tool boundary of the agent's open logical turn
|
|
16504
16840
|
* will weigh: one that would re-pin the locus becomes one of the turn's
|
|
16505
|
-
* arrivals (TurnRouting.arrivals). A message
|
|
16506
|
-
*
|
|
16507
|
-
* won't see it, and it moves nothing.
|
|
16841
|
+
* arrivals (TurnRouting.arrivals). A message the agent's own view leaves
|
|
16842
|
+
* out, such as one a tune-out diverted (metadata.tuneOut), never enters
|
|
16843
|
+
* its compiled view, so the agent won't see it, and it moves nothing.
|
|
16508
16844
|
*/
|
|
16509
16845
|
noteArrival(agentName, metadata, messageId) {
|
|
16510
16846
|
const open = this.openLogical.get(agentName);
|
|
16511
16847
|
if (!open)
|
|
16512
16848
|
return;
|
|
16513
|
-
|
|
16849
|
+
const agent = this.agents.get(agentName);
|
|
16850
|
+
if (agent && !this.agentSees(agent, metadata))
|
|
16514
16851
|
return;
|
|
16515
16852
|
if (isConversationalInjection(metadata))
|
|
16516
16853
|
(open.heard ??= new Map()).set(messageId, ++lastHeardAt);
|
|
@@ -16538,6 +16875,7 @@ export class AgentFramework {
|
|
|
16538
16875
|
return;
|
|
16539
16876
|
for (const reader of this.sharedSlotAgents) {
|
|
16540
16877
|
if (reader !== agent && this.agents.get(reader.name) === reader && this.isOrdinaryResident(reader)) {
|
|
16878
|
+
this.noteSpokenTo(reader.name, metadata);
|
|
16541
16879
|
this.noteArrival(reader.name, metadata, messageId);
|
|
16542
16880
|
}
|
|
16543
16881
|
}
|
|
@@ -16604,9 +16942,9 @@ export class AgentFramework {
|
|
|
16604
16942
|
const content = [{
|
|
16605
16943
|
type: 'text',
|
|
16606
16944
|
text: inboundOnlyServer
|
|
16607
|
-
? `[routing] The conversation moved to ${shown}. ${inboundOnlySpeechNote(inboundOnlyServer)} ` +
|
|
16945
|
+
? `[${NOTICE_TAG.routing}] The conversation moved to ${shown}. ${inboundOnlySpeechNote(inboundOnlyServer)} ` +
|
|
16608
16946
|
'Other channels need an explicit send tool.'
|
|
16609
|
-
: `[routing] The conversation moved to ${shown} — your plain ` +
|
|
16947
|
+
: `[${NOTICE_TAG.routing}] The conversation moved to ${shown} — your plain ` +
|
|
16610
16948
|
'speech now lands there for the rest of this turn. Other ' +
|
|
16611
16949
|
'channels need an explicit send tool.',
|
|
16612
16950
|
}];
|
|
@@ -16742,7 +17080,7 @@ export class AgentFramework {
|
|
|
16742
17080
|
}
|
|
16743
17081
|
const sender = this.agentForPrimary(agentName);
|
|
16744
17082
|
return [{
|
|
16745
|
-
text: `[channels] Now open: ${shown}: a message from ${sender.first} into the closed channel opened it; ${sender.again} has ended.` +
|
|
17083
|
+
text: `[${NOTICE_TAG.channels}] Now open: ${shown}: a message from ${sender.first} into the closed channel opened it; ${sender.again} has ended.` +
|
|
16746
17084
|
optOut(this.holdsChannelTraffic(primary, refs), this.canCloseChannels(primary, refs)),
|
|
16747
17085
|
metadata: { system: true, kind: 'channel-notice', speaker: agentName },
|
|
16748
17086
|
told: `the primary, by name (${agentName} has ended)`,
|
|
@@ -16762,13 +17100,13 @@ export class AgentFramework {
|
|
|
16762
17100
|
const tellPrimary = !!agentName && !canClose && primary !== undefined && this.agents.has(primary) &&
|
|
16763
17101
|
this.canCloseChannels(primary, refs);
|
|
16764
17102
|
const readerText = (told) => (cause === 'subscription-policy'
|
|
16765
|
-
? `[channels] Selected for opening by subscription policy: ${shown}. ` +
|
|
17103
|
+
? `[${NOTICE_TAG.channels}] Selected for opening by subscription policy: ${shown}. ` +
|
|
16766
17104
|
(plural
|
|
16767
17105
|
? "The host opens them, and if one doesn't open, tries again when its server next registers it; " +
|
|
16768
17106
|
'channel_list shows whether each is open yet.'
|
|
16769
17107
|
: 'The host opens it, and if that fails, tries again when its server next registers it; ' +
|
|
16770
17108
|
"channel_list shows whether it's open yet.")
|
|
16771
|
-
: `[channels] Now open: ${shown}: your own message into the closed channel opened it.`) +
|
|
17109
|
+
: `[${NOTICE_TAG.channels}] Now open: ${shown}: your own message into the closed channel opened it.`) +
|
|
16772
17110
|
optOut(this.holdsChannelTraffic(who, refs), canClose) + told;
|
|
16773
17111
|
const reader = {
|
|
16774
17112
|
text: readerText(tellPrimary ? ` ${this.primaryForReader(agentName, primary)} has been told and can close ${plural ? 'them' : 'it'}.` : ''),
|
|
@@ -16785,7 +17123,7 @@ export class AgentFramework {
|
|
|
16785
17123
|
if (tellPrimary) {
|
|
16786
17124
|
const sender = this.agentForPrimary(agentName);
|
|
16787
17125
|
notices.push({
|
|
16788
|
-
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'}.` +
|
|
17126
|
+
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'}.` +
|
|
16789
17127
|
optOut(this.holdsChannelTraffic(primary, refs), true),
|
|
16790
17128
|
metadata: { system: true, kind: 'channel-notice', speaker: agentName },
|
|
16791
17129
|
told: `the primary, by name (${agentName} can't close ${plural ? 'them' : 'it'})`,
|
|
@@ -16967,7 +17305,7 @@ export class AgentFramework {
|
|
|
16967
17305
|
primary !== undefined && this.agents.has(primary) &&
|
|
16968
17306
|
this.canCloseChannels(primary, [{ serverId, channelId: channel.channelId }]);
|
|
16969
17307
|
const sender = ended ? this.agentForPrimary(agentName) : undefined;
|
|
16970
|
-
const readerText = (told) => `[channels] Not open: ${shown}. ` +
|
|
17308
|
+
const readerText = (told) => `[${NOTICE_TAG.channels}] Not open: ${shown}. ` +
|
|
16971
17309
|
(ended
|
|
16972
17310
|
? `A reply from ${sender.first} went out there, but opening the channel failed; ${sender.again} has ended.`
|
|
16973
17311
|
: 'Your reply there went out, but opening the channel failed.') +
|
|
@@ -16994,7 +17332,7 @@ export class AgentFramework {
|
|
|
16994
17332
|
const primaryCanOpen = !this.conversationAgentHomes.has(primary) && this.canAgentUseTool(primary, 'channel_open');
|
|
16995
17333
|
const reader = this.agentForPrimary(who);
|
|
16996
17334
|
notices.push({
|
|
16997
|
-
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.` +
|
|
17335
|
+
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.` +
|
|
16998
17336
|
selected + ', and channel_close withdraws that.' +
|
|
16999
17337
|
(primaryCanOpen && !hostRetries ? ' channel_open tries again.' : ''),
|
|
17000
17338
|
metadata: { system: true, kind: 'channel-notice', speaker: who },
|
|
@@ -17034,6 +17372,7 @@ export class AgentFramework {
|
|
|
17034
17372
|
reason: 'prose-continuation',
|
|
17035
17373
|
source: 'framework',
|
|
17036
17374
|
timestamp: Date.now(),
|
|
17375
|
+
...this.heartbeatTurnMark(routing),
|
|
17037
17376
|
});
|
|
17038
17377
|
}
|
|
17039
17378
|
/**
|
|
@@ -17102,7 +17441,7 @@ export class AgentFramework {
|
|
|
17102
17441
|
}
|
|
17103
17442
|
this.autoContinueStreaks.set(name, count);
|
|
17104
17443
|
console.error(`[continue] ${name}: turn ended (${stopReason}) without skip_reply — continuation ${count} of ${config.maxConsecutive}` +
|
|
17105
|
-
(routing.locus ? ` in ${this.channelRegistry?.nameShown(routing.locus) ?? routing.locus.channelId}` : ''));
|
|
17444
|
+
(routing.locus ? ` in ${this.channelRegistry?.nameShown(routing.locus) ?? suppliedValue(routing.locus.channelId)}` : ''));
|
|
17106
17445
|
// The next turn speaks where this one did (locus and hybrid modes): the
|
|
17107
17446
|
// locus's server and id, as the framework's own wakes name a channel. A
|
|
17108
17447
|
// turn that had no channel (noChannel: its wake named one that resolves
|
|
@@ -17120,6 +17459,7 @@ export class AgentFramework {
|
|
|
17120
17459
|
timestamp: Date.now(),
|
|
17121
17460
|
...channel,
|
|
17122
17461
|
autoContinueRun: { stop, ...(opts.heldAfterSend ? { heldAfterSend: true } : {}) },
|
|
17462
|
+
...this.heartbeatTurnMark(routing),
|
|
17123
17463
|
});
|
|
17124
17464
|
}
|
|
17125
17465
|
/** How an autoContinue agent stops a run, as its `[continue]` row says it,
|
|
@@ -17140,11 +17480,12 @@ export class AgentFramework {
|
|
|
17140
17480
|
* starts (startAgentStream), with the run's count as it stands and the
|
|
17141
17481
|
* agent's cap. False when it couldn't be stored.
|
|
17142
17482
|
*/
|
|
17143
|
-
writeAutoContinueNotice(agent, mark) {
|
|
17483
|
+
writeAutoContinueNotice(agent, mark, heartbeat = false) {
|
|
17144
17484
|
const count = this.autoContinueStreaks.get(agent.name) ?? 1;
|
|
17145
17485
|
const max = agent.autoContinue?.maxConsecutive ?? count;
|
|
17146
17486
|
const text = autoContinueNotice({
|
|
17147
17487
|
agentName: agent.name, stop: mark.stop, last: count >= max, heldAfterSend: mark.heldAfterSend,
|
|
17488
|
+
heartbeat,
|
|
17148
17489
|
});
|
|
17149
17490
|
const before = this.storedCountOf(agent);
|
|
17150
17491
|
let id;
|
|
@@ -17341,7 +17682,7 @@ export class AgentFramework {
|
|
|
17341
17682
|
const target = this.noticeTargetFor(agent.name, 'empty-{{unsent}}');
|
|
17342
17683
|
try {
|
|
17343
17684
|
if (target)
|
|
17344
|
-
this.addMessage('user', [{ type: 'text', text: `[
|
|
17685
|
+
this.addMessage('user', [{ type: 'text', text: `[${NOTICE_TAG.proseRouting}] ${reason}, so nothing was sent. drafts(action: "list") shows your open drafts.` }], { system: true, kind: 'prose-bounce' }, target);
|
|
17345
17686
|
}
|
|
17346
17687
|
catch (err) {
|
|
17347
17688
|
console.error('[drafts] empty-unsent notice failed:', err);
|
|
@@ -17363,7 +17704,7 @@ export class AgentFramework {
|
|
|
17363
17704
|
else if (draftState(draft) === 'unconfirmed') {
|
|
17364
17705
|
why = this.riskText(draft);
|
|
17365
17706
|
next = 'sending that draft again could post it twice. To send it anyway, use ' +
|
|
17366
|
-
`drafts(action: "resend", draftIds: ["${draft.id}"], destination:
|
|
17707
|
+
`drafts(action: "resend", draftIds: ["${draft.id}"], destination: ${quotedValue(NO_CHANNEL_EXAMPLE)}, confirmDuplicate: true)`;
|
|
17367
17708
|
}
|
|
17368
17709
|
else {
|
|
17369
17710
|
return { draft };
|
|
@@ -17381,7 +17722,7 @@ export class AgentFramework {
|
|
|
17381
17722
|
? ` Your message is held as draft ${held.id}, with {{unsent}} written out; since it includes ${draft.id}'s words, it ` +
|
|
17382
17723
|
`can be resent only with confirmDuplicate: true${afterResult}.`
|
|
17383
17724
|
: ' Your message could not be held as a draft; its words remain in your history.';
|
|
17384
|
-
const text = `[
|
|
17725
|
+
const text = `[${NOTICE_TAG.proseRouting}] {{unsent}} is draft ${draft.id}, ${why.startsWith('which') ? why : `and ${why}`}. ` +
|
|
17385
17726
|
`Nothing was sent; ${next}.${kept}`;
|
|
17386
17727
|
const target = this.noticeTargetFor(agent.name, 'refused-{{unsent}}');
|
|
17387
17728
|
try {
|
|
@@ -17440,7 +17781,7 @@ export class AgentFramework {
|
|
|
17440
17781
|
}
|
|
17441
17782
|
catch (err) {
|
|
17442
17783
|
console.error(`[drafts] ${agent.name}: outcome of ${unsent.id} not recorded:`, err);
|
|
17443
|
-
const text = `[
|
|
17784
|
+
const text = `[${NOTICE_TAG.proseRouting}] {{unsent}} was draft ${unsent.id}; its send came back ${outcome.status}, but recording that ` +
|
|
17444
17785
|
`failed, so the draft reads as ${this.draftStateNow(agent.name, unsent.id)} now. drafts(action: "read") shows it.`;
|
|
17445
17786
|
const target = this.noticeTargetFor(agent.name, 'unrecorded-outcome');
|
|
17446
17787
|
try {
|
|
@@ -17523,14 +17864,14 @@ export class AgentFramework {
|
|
|
17523
17864
|
// not bounced, so its words are not re-held as a fresh draft that
|
|
17524
17865
|
// invites a duplicate.
|
|
17525
17866
|
if (outcome.status === 'failed') {
|
|
17526
|
-
this.bounceProse(agent, parsed.body, `delivery to ${this.channelRegistry?.nameShown(resolved) ?? resolved.channelId} failed: ${outcome.reason
|
|
17867
|
+
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 });
|
|
17527
17868
|
}
|
|
17528
17869
|
}
|
|
17529
17870
|
}
|
|
17530
17871
|
catch (err) {
|
|
17531
17872
|
delete routing.pin;
|
|
17532
17873
|
routing.hybridHold = 'failed';
|
|
17533
|
-
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' });
|
|
17874
|
+
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' });
|
|
17534
17875
|
}
|
|
17535
17876
|
return;
|
|
17536
17877
|
}
|
|
@@ -17549,6 +17890,10 @@ export class AgentFramework {
|
|
|
17549
17890
|
this.holdProseDrafts(agent, [envelope], 'no-destination', round.draftsRound ? { ...round, notice: 'later' } : round);
|
|
17550
17891
|
return;
|
|
17551
17892
|
}
|
|
17893
|
+
if (!sticky && locus === null && routing.heartbeat) {
|
|
17894
|
+
this.holdHeartbeatProse(agent, [envelope], round);
|
|
17895
|
+
return;
|
|
17896
|
+
}
|
|
17552
17897
|
const target = sticky ?? locus;
|
|
17553
17898
|
try {
|
|
17554
17899
|
const outcome = await this.channelRegistry.routeSpeech(agent.name, envelope, target);
|
|
@@ -17584,7 +17929,7 @@ export class AgentFramework {
|
|
|
17584
17929
|
}
|
|
17585
17930
|
};
|
|
17586
17931
|
if (this.draftsSending.has(`${agent.name}\u0000${draft.id}`)) {
|
|
17587
|
-
tell(`[
|
|
17932
|
+
tell(`[${NOTICE_TAG.proseRouting}] {{unsent}} is draft ${draft.id}, which is being sent right now. It was not set aside, ` +
|
|
17588
17933
|
'since that can\'t call back words already on their way; wait for that result.', 'in-flight-dismiss');
|
|
17589
17934
|
return;
|
|
17590
17935
|
}
|
|
@@ -17601,13 +17946,13 @@ export class AgentFramework {
|
|
|
17601
17946
|
if (now === 'dismissed')
|
|
17602
17947
|
this.emitTrace({ type: 'prose:drafts-dismissed', agentName: agent.name, draftIds: [draft.id] });
|
|
17603
17948
|
const named = this.proseDrafts.latestBounce(agent.name)?.id === draft.id;
|
|
17604
|
-
tell(`[
|
|
17949
|
+
tell(`[${NOTICE_TAG.proseRouting}] Recording setting aside draft ${draft.id}, which {{unsent}} named, failed ` +
|
|
17605
17950
|
`(${err instanceof Error ? err.message : String(err)}): it reads as ${now} now, and {{unsent}} ` +
|
|
17606
17951
|
`${named ? 'still names it' : 'names nothing'}.${now === 'dismissed' && risk ? ` Note that ${risk}.` : ''}`, 'failed-dismiss');
|
|
17607
17952
|
return;
|
|
17608
17953
|
}
|
|
17609
17954
|
if (risk)
|
|
17610
|
-
tell(`[
|
|
17955
|
+
tell(`[${NOTICE_TAG.proseRouting}] Draft ${draft.id} is set aside. Note that ${risk}.`, 'at-risk-dismiss');
|
|
17611
17956
|
}
|
|
17612
17957
|
async deliverProseEnvelope(agent, envelope, round) {
|
|
17613
17958
|
const name = agent.name;
|
|
@@ -17669,12 +18014,12 @@ export class AgentFramework {
|
|
|
17669
18014
|
if (!('draft' in words)) {
|
|
17670
18015
|
const held = this.holdProseDrafts(agent, [{ text: words.text, ...(words.inheritedRisk ? { inheritedRisk: words.inheritedRisk } : {}) }], 'no-destination', { ...round, notice: 'later' });
|
|
17671
18016
|
if (held.length === 0 && words.text.trim().length > 0) {
|
|
17672
|
-
this.bounceProse(agent, rawText, 'no destination prefix, and this turn has no destination now', undefined, '>>', round);
|
|
18017
|
+
this.bounceProse(agent, rawText, 'no destination prefix, and this turn has no destination now', undefined, '>>', round, { unprefixed: true });
|
|
17673
18018
|
}
|
|
17674
18019
|
}
|
|
17675
18020
|
return;
|
|
17676
18021
|
}
|
|
17677
|
-
this.bounceProse(agent, rawText, 'no destination prefix, and this turn has no destination now', undefined, '>>', round);
|
|
18022
|
+
this.bounceProse(agent, rawText, 'no destination prefix, and this turn has no destination now', undefined, '>>', round, { unprefixed: true });
|
|
17678
18023
|
return;
|
|
17679
18024
|
}
|
|
17680
18025
|
targetChannel = sticky;
|
|
@@ -17706,11 +18051,11 @@ export class AgentFramework {
|
|
|
17706
18051
|
else if (outcome.status === 'failed') {
|
|
17707
18052
|
// Unknown deliveries are marker-only: offering a fresh draft would
|
|
17708
18053
|
// invite a duplicate. Keep the authored body for the re-bounce rule.
|
|
17709
|
-
this.bounceProse(agent, envelopeBody, `delivery to ${this.channelRegistry?.nameShown(targetChannel) ?? targetChannel.channelId} failed: ${outcome.reason
|
|
18054
|
+
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 });
|
|
17710
18055
|
}
|
|
17711
18056
|
}
|
|
17712
18057
|
catch (err) {
|
|
17713
|
-
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' });
|
|
18058
|
+
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' });
|
|
17714
18059
|
}
|
|
17715
18060
|
if (!delivered && this.channelRegistry.inboundOnlyServer(targetChannel) !== undefined) {
|
|
17716
18061
|
// The channel is inbound-only by now: its server declared it after the
|
|
@@ -17801,7 +18146,7 @@ export class AgentFramework {
|
|
|
17801
18146
|
// Words with no draft behind them, whose send threw, may have gone
|
|
17802
18147
|
// out. Like an unknown outcome, that is marker-only: holding them as a
|
|
17803
18148
|
// draft would invite a duplicate post.
|
|
17804
|
-
const notice = `[
|
|
18149
|
+
const notice = `[${NOTICE_TAG.proseRouting}] Your text (${text.length} chars) may not have been delivered — ${suppliedValue(reason)}. ` +
|
|
17805
18150
|
"It isn't held as a draft, since a resend could post it twice; the words remain in your history.";
|
|
17806
18151
|
const target = this.noticeTargetFor(name, 'unknown-send');
|
|
17807
18152
|
try {
|
|
@@ -17827,13 +18172,22 @@ export class AgentFramework {
|
|
|
17827
18172
|
// The bounce notice below names the draft, so no separate held notice.
|
|
17828
18173
|
draft = this.holdProseDrafts(agent, [{ text: words.text, ...(inherited ? { inheritedRisk: inherited } : {}) }], 'bounced', { routing: round.routing, note: reason, notice: 'later' })[0];
|
|
17829
18174
|
}
|
|
17830
|
-
|
|
17831
|
-
|
|
17832
|
-
|
|
18175
|
+
// A heartbeat's turn came from no channel: its bounce is noticed, and
|
|
18176
|
+
// waits for the agent's next turn, rather than starting one of its own,
|
|
18177
|
+
// so it neither wakes the agent nor counts toward the bounce-wake cap.
|
|
18178
|
+
// Only words with no destination: a `>>` or `>>>` target that fails was
|
|
18179
|
+
// meant for someone, and its bounce wakes the agent as any does.
|
|
18180
|
+
const quiet = round.routing.heartbeat === true && after.unprefixed === true;
|
|
18181
|
+
const streak = (this.proseBounceStreaks.get(name) ?? 0) + (quiet ? 0 : 1);
|
|
18182
|
+
if (!quiet)
|
|
18183
|
+
this.proseBounceStreaks.set(name, streak);
|
|
18184
|
+
const cand = candidates?.length
|
|
18185
|
+
? ` Known channels it could mean: ${candidates.map(suppliedValue).join(', ')}.${this.channelRegistry?.publishRouteFor(candidates) ?? ''}`
|
|
18186
|
+
: '';
|
|
17833
18187
|
// The examples name the first candidate that is one token, as a target
|
|
17834
|
-
// takes it, so they show the form to write, such as a server-qualified
|
|
17835
|
-
// `a:general`; with none,
|
|
17836
|
-
const example = candidates?.find(isProseTargetToken) ??
|
|
18188
|
+
// takes it, and shows as itself, so they show the form to write, such as a server-qualified
|
|
18189
|
+
// `a:general`; with none, the generic `#<channel>`.
|
|
18190
|
+
const example = candidates?.find((c) => isProseTargetToken(c) && showsAsItself(c)) ?? NO_CHANNEL_EXAMPLE;
|
|
17837
18191
|
// A bare {{unsent}} whose send failed re-bounces the draft it sent, which
|
|
17838
18192
|
// a later bounce may have displaced as the latest; and {{unsent}} refuses
|
|
17839
18193
|
// a draft that may already have been posted. Then only the drafts tool
|
|
@@ -17844,11 +18198,11 @@ export class AgentFramework {
|
|
|
17844
18198
|
? 'It could not be held as a draft; the words remain in your history.'
|
|
17845
18199
|
: byToken
|
|
17846
18200
|
? `It is held as draft ${draft.id}; nothing is lost. To deliver it unchanged, reply with a destination plus the ` +
|
|
17847
|
-
`token {{unsent}}, e.g. "${prefix}${example} {{unsent}}" or "${prefix}
|
|
17848
|
-
`drafts(action: "resend", draftIds: ["${draft.id}"], destination:
|
|
18201
|
+
`token {{unsent}}, e.g. "${prefix}${example} {{unsent}}" or "${prefix}${NO_PERSON_EXAMPLE} {{unsent}}", or use ` +
|
|
18202
|
+
`drafts(action: "resend", draftIds: ["${draft.id}"], destination: ${quotedValue(example)}). ` +
|
|
17849
18203
|
`"${prefix}skip_reply {{unsent}}" sets it aside.`
|
|
17850
18204
|
: `It is held as draft ${draft.id}; nothing is lost. To deliver it unchanged, use ` +
|
|
17851
|
-
`drafts(action: "resend", draftIds: ["${draft.id}"], destination:
|
|
18205
|
+
`drafts(action: "resend", draftIds: ["${draft.id}"], destination: ${quotedValue(example)}` +
|
|
17852
18206
|
`${inherited || ownRisk ? ', confirmDuplicate: true' : ''}); ` +
|
|
17853
18207
|
`drafts(action: "dismiss", draftIds: ["${draft.id}"]) sets it aside.`;
|
|
17854
18208
|
const risky = inherited && draft
|
|
@@ -17863,7 +18217,7 @@ export class AgentFramework {
|
|
|
17863
18217
|
// A send that threw has no known outcome: the notice doesn't say it
|
|
17864
18218
|
// wasn't delivered.
|
|
17865
18219
|
const delivered = after.outcome === 'unknown' ? 'may not have been delivered' : 'was not delivered';
|
|
17866
|
-
const notice = `[
|
|
18220
|
+
const notice = `[${NOTICE_TAG.proseRouting}] Your text (${text.length} chars) ${delivered} — ${suppliedValue(reason)}.${cand} ${kept}${risky}` +
|
|
17867
18221
|
(prefix === '>>' && this.canAgentUseTool(agent.name, 'prose_help') ? ' The prose_help tool shows the full syntax.' : '');
|
|
17868
18222
|
const target = this.noticeTargetFor(name, 'prose-bounce');
|
|
17869
18223
|
if (target) {
|
|
@@ -17887,8 +18241,8 @@ export class AgentFramework {
|
|
|
17887
18241
|
}
|
|
17888
18242
|
const capped = streak > AgentFramework.PROSE_BOUNCE_WAKE_CAP;
|
|
17889
18243
|
console.error(`[prose] ${name}: BOUNCED ${text.length} chars — ${reason}` +
|
|
17890
|
-
(capped ? ' (bounce-wake cap reached; notice appended without wake)' : ''));
|
|
17891
|
-
if (!capped && target) {
|
|
18244
|
+
(quiet ? ' (a heartbeat turn: notice appended without wake)' : capped ? ' (bounce-wake cap reached; notice appended without wake)' : ''));
|
|
18245
|
+
if (!capped && !quiet && target) {
|
|
17892
18246
|
// Wake so the resend can happen immediately — an unsent DM reply must
|
|
17893
18247
|
// not sit invisible until the next unrelated event.
|
|
17894
18248
|
this.pendingRequests.push({
|
|
@@ -17896,6 +18250,7 @@ export class AgentFramework {
|
|
|
17896
18250
|
reason: 'prose-bounce',
|
|
17897
18251
|
source: 'framework',
|
|
17898
18252
|
timestamp: Date.now(),
|
|
18253
|
+
...this.heartbeatTurnMark(round.routing),
|
|
17899
18254
|
});
|
|
17900
18255
|
}
|
|
17901
18256
|
}
|
|
@@ -18105,6 +18460,7 @@ export class AgentFramework {
|
|
|
18105
18460
|
stopReason: 'exhausted', speech: '', error: error instanceof Error ? error.message : String(error),
|
|
18106
18461
|
}, this.openLogical.get(agent.name));
|
|
18107
18462
|
}
|
|
18463
|
+
this.endStatedWaitRun(agent.name);
|
|
18108
18464
|
// The agent's window at the event gate ends here too, while this start
|
|
18109
18465
|
// still holds the turn token, as at the provider-wait hold above. A
|
|
18110
18466
|
// start that resumes a turn (a budget restart, a tool-result-guard
|
|
@@ -18602,13 +18958,16 @@ export class AgentFramework {
|
|
|
18602
18958
|
// without the mark, so a retry doesn't write it again. A row that can't
|
|
18603
18959
|
// be stored gives back the run's count, and withdraws a turn the
|
|
18604
18960
|
// continuation alone caused: it would wake to nothing it can read.
|
|
18961
|
+
// A framework wake that continues a heartbeat's turn (heartbeatTurn) is
|
|
18962
|
+
// one too, and isn't told again what the turn that started the run was.
|
|
18963
|
+
const continuesHeartbeat = trigger?.heartbeatTurn === true && trigger.source === 'framework';
|
|
18605
18964
|
if (attempt === 0 && !resumesTurn && trigger?.autoContinueRun) {
|
|
18606
18965
|
const mark = trigger.autoContinueRun;
|
|
18607
18966
|
const { autoContinueRun: _mark, ...unmarked } = trigger;
|
|
18608
18967
|
trigger = unmarked;
|
|
18609
18968
|
// Another reader's turn may have started since the scheduler checked.
|
|
18610
18969
|
const busy = this.sharedSlotBusy(agent);
|
|
18611
|
-
if (busy || !this.writeAutoContinueNotice(agent, mark)) {
|
|
18970
|
+
if (busy || !this.writeAutoContinueNotice(agent, mark, trigger.heartbeatTurn === true)) {
|
|
18612
18971
|
this.giveBackAutoContinue(agent.name, busy ? "another resident's turn is alive in the shared slot" : 'its row could not be stored');
|
|
18613
18972
|
if (mark.sole) {
|
|
18614
18973
|
console.error(`[continue] ${agent.name}: turn not started (the continuation was its only cause)`);
|
|
@@ -18747,9 +19106,10 @@ export class AgentFramework {
|
|
|
18747
19106
|
this.noticeHeadMoves(agent);
|
|
18748
19107
|
if (opensTurn) {
|
|
18749
19108
|
// The turn's trigger channel (item-3 redux): the channel of a
|
|
18750
|
-
// channel/DM-triggered turn, with its server, and none for a
|
|
18751
|
-
//
|
|
18752
|
-
//
|
|
19109
|
+
// channel/DM-triggered turn, with its server, and none for a
|
|
19110
|
+
// no-trigger turn, which doesn't inherit a previous turn's channel: a
|
|
19111
|
+
// heartbeat's takes no locus at all (TurnRouting.heartbeat), and any
|
|
19112
|
+
// other falls back to the global default. A wake whose channel,
|
|
18753
19113
|
// given without its server, names no one registered channel (a host's
|
|
18754
19114
|
// id that two servers' channels answer to, or that none does) has none
|
|
18755
19115
|
// either, and takes no fallback: it was for a channel, and the latest
|
|
@@ -18766,6 +19126,12 @@ export class AgentFramework {
|
|
|
18766
19126
|
routing.noChannel = true;
|
|
18767
19127
|
else
|
|
18768
19128
|
delete routing.noChannel;
|
|
19129
|
+
// A heartbeat's turn has no channel, and takes no fallback: its plain
|
|
19130
|
+
// speech is held as a draft (TurnRouting.heartbeat).
|
|
19131
|
+
if (isHeartbeatWake(trigger))
|
|
19132
|
+
routing.heartbeat = true;
|
|
19133
|
+
else
|
|
19134
|
+
delete routing.heartbeat;
|
|
18769
19135
|
// Resolve the turn's outbound locus now: the home channel for forks,
|
|
18770
19136
|
// else the trigger channel, else the global default, which an
|
|
18771
19137
|
// ephemeral run never takes (takesGlobalFallback). Ambient mid-turn
|
|
@@ -18773,8 +19139,8 @@ export class AgentFramework {
|
|
|
18773
19139
|
// chatter and reactions hijacked it before, 2026-07-21); only an
|
|
18774
19140
|
// addressed message, a follow-up in a channel the turn engaged
|
|
18775
19141
|
// (repinTurnLocus) or the agent's own channel_open moves it, between
|
|
18776
|
-
// rounds. The eager snapshot protects
|
|
18777
|
-
// defaultPublishChannel. Explicit mode uses a turn-scoped `>>` target
|
|
19142
|
+
// rounds. The eager snapshot protects a no-trigger turn that takes the
|
|
19143
|
+
// fallback (a timer's) from a moving defaultPublishChannel. Explicit mode uses a turn-scoped `>>` target
|
|
18778
19144
|
// and disabled mode has no prose target, so neither resolves or
|
|
18779
19145
|
// announces a locus. If the effective locus differs from the last one
|
|
18780
19146
|
// announced, a durable `[routing]` notice goes into the window BEFORE
|
|
@@ -18788,8 +19154,17 @@ export class AgentFramework {
|
|
|
18788
19154
|
routing.locus = locus;
|
|
18789
19155
|
noteTurnChannel(routing, locus);
|
|
18790
19156
|
}
|
|
18791
|
-
|
|
19157
|
+
// A heartbeat's turn speaks nowhere by design. It's told so up front,
|
|
19158
|
+
// once per heartbeat (a continuation of its run isn't told again),
|
|
19159
|
+
// without moving the announced baseline, so the next channel turn
|
|
19160
|
+
// doesn't announce its channel again.
|
|
19161
|
+
if (attempt === 0 && routing.heartbeat) {
|
|
19162
|
+
if (!continuesHeartbeat)
|
|
19163
|
+
this.noticeHeartbeatTurn(agent.name);
|
|
19164
|
+
}
|
|
19165
|
+
else if (attempt === 0) {
|
|
18792
19166
|
this.announceLocusIfChanged(agent.name, locus);
|
|
19167
|
+
}
|
|
18793
19168
|
}
|
|
18794
19169
|
}
|
|
18795
19170
|
else {
|
|
@@ -18856,7 +19231,9 @@ export class AgentFramework {
|
|
|
18856
19231
|
const presentation = this.inspectToolPresentation(agent.name, requestSnapshot);
|
|
18857
19232
|
const compressionTools = presentation?.available
|
|
18858
19233
|
?? structuredClone(this.availableToolsForPresentation(agent.name, requestSnapshot));
|
|
18859
|
-
|
|
19234
|
+
// The tool list: what the agent's presentation advertises, less the
|
|
19235
|
+
// host's own toolsets (the built-in one, servers it has as tabs).
|
|
19236
|
+
const tools = this.headerTools(agent.name, presentation?.advertised ?? compressionTools);
|
|
18860
19237
|
// Gather context from modules (pull-based) and MCPL hooks (push-based)
|
|
18861
19238
|
// Both produce ContextInjection[] that get merged before inference.
|
|
18862
19239
|
let injections;
|
|
@@ -19120,7 +19497,8 @@ export class AgentFramework {
|
|
|
19120
19497
|
this.endHeldStart(agent, checkWindow, turnToken, turnStartedAt, routing);
|
|
19121
19498
|
return false;
|
|
19122
19499
|
}
|
|
19123
|
-
const action = statedWait === 'fail'
|
|
19500
|
+
const action = statedWait === 'fail' || (isRearmed(trigger) && !gotPartway(routing))
|
|
19501
|
+
? { retry: false } : this.errorPolicy.onInferenceError(err, agent.name, attempt);
|
|
19124
19502
|
if (action.retry) {
|
|
19125
19503
|
// A policy's delay can be a provider's retry-after: wait all of it.
|
|
19126
19504
|
await this.waitForRetry(action.delayMs, checkWindow, agent);
|
|
@@ -19181,6 +19559,8 @@ export class AgentFramework {
|
|
|
19181
19559
|
speech: '',
|
|
19182
19560
|
...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: err.message }),
|
|
19183
19561
|
}, routing);
|
|
19562
|
+
if (isRearmed(trigger) && isTransientFailure(err))
|
|
19563
|
+
this.exhaustingRearms.add(agent.name);
|
|
19184
19564
|
this.emitTrace({
|
|
19185
19565
|
type: 'inference:exhausted',
|
|
19186
19566
|
agentName: agent.name,
|
|
@@ -19200,6 +19580,7 @@ export class AgentFramework {
|
|
|
19200
19580
|
if (action.emit) {
|
|
19201
19581
|
this.pushEvent(action.emit);
|
|
19202
19582
|
}
|
|
19583
|
+
this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, routing);
|
|
19203
19584
|
}
|
|
19204
19585
|
// No handoff on either failure branch: the terminal branch deleted the
|
|
19205
19586
|
// token above; the retry branch's recursive startAgentStream replaced
|
|
@@ -19484,9 +19865,9 @@ export class AgentFramework {
|
|
|
19484
19865
|
}
|
|
19485
19866
|
};
|
|
19486
19867
|
};
|
|
19487
|
-
const enqueueSpeech = (text, locus, onPublished) => {
|
|
19868
|
+
const enqueueSpeech = (text, locus, onPublished, round) => {
|
|
19488
19869
|
chainSpeech(routingSpeech(async () => {
|
|
19489
|
-
const outcome = await this.
|
|
19870
|
+
const outcome = await this.routeLocusSpeech(agent, round ?? { routing: streamRouting, notice: 'later' }, text, locus);
|
|
19490
19871
|
this.recordProseDelivery(streamRouting, outcome);
|
|
19491
19872
|
if (outcome?.delivered) {
|
|
19492
19873
|
streamRouting.liveSpeech = true;
|
|
@@ -20008,9 +20389,11 @@ export class AgentFramework {
|
|
|
20008
20389
|
console.error(`[routing] ${agent.name}: mid-turn round [${toolNames.join(', ')}] -> prose NOT routed (same_round_think_text_policy=private)`);
|
|
20009
20390
|
}
|
|
20010
20391
|
else {
|
|
20011
|
-
console.error(
|
|
20392
|
+
console.error(roundLocus === null && streamRouting.heartbeat
|
|
20393
|
+
? `[routing] ${agent.name}: mid-turn round [${toolNames.join(', ')}] -> ${roundSegments.length} prose segment(s) held as drafts (a heartbeat turn)`
|
|
20394
|
+
: `[routing] ${agent.name}: mid-turn round [${toolNames.join(', ')}] -> routing ${roundSegments.length} prose segment(s) live -> ${roundLocus ?? '(default)'}`);
|
|
20012
20395
|
for (const seg of roundSegments) {
|
|
20013
|
-
enqueueSpeech(seg, roundLocus, roundPublished);
|
|
20396
|
+
enqueueSpeech(seg, roundLocus, roundPublished, roundOpts);
|
|
20014
20397
|
}
|
|
20015
20398
|
}
|
|
20016
20399
|
};
|
|
@@ -20311,7 +20694,7 @@ export class AgentFramework {
|
|
|
20311
20694
|
// self-seed. Exact raw output remains in the provider ledger.
|
|
20312
20695
|
agent.getContextManager().addMessage('user', [{
|
|
20313
20696
|
type: 'text',
|
|
20314
|
-
text:
|
|
20697
|
+
text: `[${NOTICE_TAG.toolBoundary}] No tool was called.`,
|
|
20315
20698
|
}], {
|
|
20316
20699
|
system: true,
|
|
20317
20700
|
kind: 'tool-wrapper-prose-contained',
|
|
@@ -20350,7 +20733,7 @@ export class AgentFramework {
|
|
|
20350
20733
|
else if (cut?.unnamed) {
|
|
20351
20734
|
agent.getContextManager().addMessage('user', [{
|
|
20352
20735
|
type: 'text',
|
|
20353
|
-
text:
|
|
20736
|
+
text: `[${NOTICE_TAG.toolBoundary}] Your response was cut off at the output limit inside a tool-call block, before any call in it ` +
|
|
20354
20737
|
'was named, so nothing ran. To make a call, send less in one response.',
|
|
20355
20738
|
}], { system: true, kind: 'cut-off-tool-block', ...this.silentTurnRowMetadata(agent.name) });
|
|
20356
20739
|
}
|
|
@@ -20617,7 +21000,7 @@ export class AgentFramework {
|
|
|
20617
21000
|
console.error(`[routing] ${agent.name}: text-only turn -> routing speech -> ${locus ? qualifiedChannelName(locus) : '(none)'}`);
|
|
20618
21001
|
for (const segment of speechSegments) {
|
|
20619
21002
|
try {
|
|
20620
|
-
const outcome = await this.
|
|
21003
|
+
const outcome = await this.routeLocusSpeech(agent, textOnlyHold, segment, locus);
|
|
20621
21004
|
this.recordProseDelivery(streamRouting, outcome);
|
|
20622
21005
|
if (outcome?.delivered)
|
|
20623
21006
|
endPublished(outcome, segment, outcome.messageId);
|
|
@@ -20737,7 +21120,7 @@ export class AgentFramework {
|
|
|
20737
21120
|
// Deliver sequentially (await each) so the segments land in order.
|
|
20738
21121
|
for (const seg of segments) {
|
|
20739
21122
|
try {
|
|
20740
|
-
const outcome = await this.
|
|
21123
|
+
const outcome = await this.routeLocusSpeech(agent, trailingHold, seg, locus);
|
|
20741
21124
|
this.recordProseDelivery(streamRouting, outcome);
|
|
20742
21125
|
if (outcome?.delivered)
|
|
20743
21126
|
endPublished(outcome, seg, outcome.messageId);
|
|
@@ -20883,7 +21266,8 @@ export class AgentFramework {
|
|
|
20883
21266
|
this.eventGate?.onInferenceEnded(agent.name);
|
|
20884
21267
|
break;
|
|
20885
21268
|
}
|
|
20886
|
-
const action = statedWait === 'fail'
|
|
21269
|
+
const action = statedWait === 'fail' || (isRearmed(trigger) && !gotPartway(streamRouting))
|
|
21270
|
+
? { retry: false } : this.errorPolicy.onInferenceError(err, agent.name, attempt);
|
|
20887
21271
|
if (action.retry) {
|
|
20888
21272
|
// A stop made during the wait reaches no stream: the check after
|
|
20889
21273
|
// the wait ends the turn, as the guard's check after its wait
|
|
@@ -20945,6 +21329,8 @@ export class AgentFramework {
|
|
|
20945
21329
|
speech: '',
|
|
20946
21330
|
...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: err.message }),
|
|
20947
21331
|
}, this.agents.get(agent.name) === agent && agent.streamId === myStreamId ? streamRouting : undefined, streamRouting);
|
|
21332
|
+
if (isRearmed(trigger) && isTransientFailure(err))
|
|
21333
|
+
this.exhaustingRearms.add(agent.name);
|
|
20948
21334
|
this.emitTrace({
|
|
20949
21335
|
type: 'inference:exhausted',
|
|
20950
21336
|
agentName: agent.name,
|
|
@@ -20963,6 +21349,7 @@ export class AgentFramework {
|
|
|
20963
21349
|
if (action.emit) {
|
|
20964
21350
|
this.pushEvent(action.emit);
|
|
20965
21351
|
}
|
|
21352
|
+
this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, streamRouting);
|
|
20966
21353
|
}
|
|
20967
21354
|
break;
|
|
20968
21355
|
}
|
|
@@ -21037,6 +21424,7 @@ export class AgentFramework {
|
|
|
21037
21424
|
agentName: agent.name,
|
|
21038
21425
|
error: terminal,
|
|
21039
21426
|
});
|
|
21427
|
+
this.rearmExhaustedTurn(agent, new Error(terminal), trigger, ownsProviderGate, streamRouting);
|
|
21040
21428
|
// Postmortem 2026-05-28 P2 #7: persist the abort to the
|
|
21041
21429
|
// inference log so future investigations can attribute the
|
|
21042
21430
|
// terminal cause without relying on live in-memory reducer
|
|
@@ -21313,12 +21701,15 @@ export class AgentFramework {
|
|
|
21313
21701
|
speech: '',
|
|
21314
21702
|
...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: err.message }),
|
|
21315
21703
|
}, streamRouting);
|
|
21704
|
+
if (isRearmed(trigger) && isTransientFailure(err))
|
|
21705
|
+
this.exhaustingRearms.add(agent.name);
|
|
21316
21706
|
this.emitTrace({
|
|
21317
21707
|
type: 'inference:exhausted',
|
|
21318
21708
|
agentName: agent.name,
|
|
21319
21709
|
error: err.message,
|
|
21320
21710
|
...this.classifyInferenceError(err),
|
|
21321
21711
|
});
|
|
21712
|
+
this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, streamRouting);
|
|
21322
21713
|
// Postmortem 2026-05-28 P2 #7: catch-path failures (stream itself
|
|
21323
21714
|
// threw) were previously only visible via in-memory trace listeners.
|
|
21324
21715
|
// Persist to inference log for forensic attribution.
|
|
@@ -21537,8 +21928,7 @@ export class AgentFramework {
|
|
|
21537
21928
|
* exists only through the private closure handed to ModuleRegistry
|
|
21538
21929
|
* (ctx.callTool → executeToolCallFrom with {kind:'module'}), which is exempt
|
|
21539
21930
|
* from agent allowlists. Registered agents' allowedTools is enforced at
|
|
21540
|
-
* dispatch, including namespaced utility names
|
|
21541
|
-
* list/describe/run slot. Genuinely unidentified public callers (including
|
|
21931
|
+
* dispatch, including namespaced utility names. Genuinely unidentified public callers (including
|
|
21542
21932
|
* unknown legacy names) remain unrestricted. Framework-created identities
|
|
21543
21933
|
* are refused while unregistered, even if their configured list allows it.
|
|
21544
21934
|
*/
|
|
@@ -21735,6 +22125,9 @@ export class AgentFramework {
|
|
|
21735
22125
|
this.emitTrace({ type: 'tool:failed', module: 'framework', tool: call.name, callId: call.id, error: traced });
|
|
21736
22126
|
return failure;
|
|
21737
22127
|
};
|
|
22128
|
+
// utils was removed: said whatever the allowlist names (dispatchToolCall).
|
|
22129
|
+
if (call.name === 'utils')
|
|
22130
|
+
return failed({ success: false, isError: true, error: this.utilsRemoved(origin.kind === 'agent' ? origin.agentName : call.callerAgentName) });
|
|
21738
22131
|
if (origin.kind === 'agent') {
|
|
21739
22132
|
const failure = this.agentToolPermissionFailure(origin.agentName, call, false);
|
|
21740
22133
|
if (failure)
|
|
@@ -21754,6 +22147,15 @@ export class AgentFramework {
|
|
|
21754
22147
|
if (call.name === CODE_EXECUTION_TOOL_NAME && this.codeExecution) {
|
|
21755
22148
|
return this.runCodeExecution(call.callerAgentName ?? '__ephemeral__', call);
|
|
21756
22149
|
}
|
|
22150
|
+
// toolsets--act on a host toolset's tool (the built-in one, a server's
|
|
22151
|
+
// tab) goes where a programmatic call of that tool goes, under its name.
|
|
22152
|
+
if (call.name === META_TOOL_NAMES.act && call.callerAgentName !== undefined) {
|
|
22153
|
+
const host = this.hostPresentation?.resolveAct(call.callerAgentName, call.input) ?? null;
|
|
22154
|
+
if (host?.kind === 'refused')
|
|
22155
|
+
return failed({ success: false, error: host.error, isError: true });
|
|
22156
|
+
if (host?.kind === 'call')
|
|
22157
|
+
return this.executeToolCallFrom({ ...call, name: host.name, input: host.args }, origin);
|
|
22158
|
+
}
|
|
21757
22159
|
// RFC-014: toolsets--act, resolved to the dynamic tool it calls, goes
|
|
21758
22160
|
// where a programmatic call of that tool goes, with its instance.
|
|
21759
22161
|
if (call.name === META_TOOL_NAMES.act && this.toolsetHub && this.mcplServerRegistry) {
|
|
@@ -21797,21 +22199,13 @@ export class AgentFramework {
|
|
|
21797
22199
|
if (call.name === 'drafts' && this.channelRegistry && origin.kind === 'agent') {
|
|
21798
22200
|
return this.draftsToolResult(origin.agentName, call.input);
|
|
21799
22201
|
}
|
|
21800
|
-
// The utils meta-tool, for callers that await a tool's result. A call
|
|
21801
|
-
// through executeToolCall (the puppet, a host) reaches what its agent's
|
|
21802
|
-
// allowedTools permits, as the model's does, and an unidentified caller
|
|
21803
|
-
// is unrestricted, as executeToolCall's doc says; a module, through
|
|
21804
|
-
// callTool, is trusted and reaches every utility.
|
|
21805
22202
|
// The heartbeat's tools, for callers that await a tool's result (an
|
|
21806
|
-
// in-process call, a puppet, a module's callTool). A module is trusted
|
|
21807
|
-
//
|
|
22203
|
+
// in-process call, a puppet, a module's callTool). A module is trusted;
|
|
22204
|
+
// an agent is held to what dispatch holds it to.
|
|
21808
22205
|
if (this.heartbeat?.ownsTool(call.name)) {
|
|
21809
22206
|
const refusal = origin.kind === 'module' ? null : this.heartbeatRefusal(call.callerAgentName ?? '__ephemeral__', call);
|
|
21810
22207
|
return refusal ? { success: false, error: refusal, isError: true } : this.runHeartbeatTool(call);
|
|
21811
22208
|
}
|
|
21812
|
-
if (call.name === 'utils') {
|
|
21813
|
-
return this.handleUtilsToolCall(call.callerAgentName ?? '__ephemeral__', call, { inProcess: true, trustedModule: origin.kind === 'module' });
|
|
21814
|
-
}
|
|
21815
22209
|
// Module tools. The caller uses the result in process, so it gets the
|
|
21816
22210
|
// module's own result, as the module returned it, less a refusal it
|
|
21817
22211
|
// claims, which is the dispatcher's to say.
|
|
@@ -21855,6 +22249,22 @@ export class AgentFramework {
|
|
|
21855
22249
|
(this.toolsetActCalls ??= new WeakMap()).set(inner, target.instance !== undefined ? { instance: target.instance } : {});
|
|
21856
22250
|
return { ok: true, call: inner, serverId: target.serverId, prefix, ...(target.instance !== undefined ? { instance: target.instance } : {}) };
|
|
21857
22251
|
}
|
|
22252
|
+
/** A toolsets--act call a host toolset resolved (HostPresentation.resolveAct):
|
|
22253
|
+
* its refusal answered, or the tool's own call dispatched in its place,
|
|
22254
|
+
* with the act's id, so its result is the act's. */
|
|
22255
|
+
dispatchHostAct(agentName, call, host, turnId) {
|
|
22256
|
+
if (host.kind === 'refused') {
|
|
22257
|
+
this.emitTrace({ type: 'tool:failed', module: TOOLSETS_MODULE, tool: call.name, callId: call.id, error: host.error });
|
|
22258
|
+
this.pushEvent({
|
|
22259
|
+
type: 'tool-result', callId: call.id, toolName: call.name, agentName, moduleName: TOOLSETS_MODULE,
|
|
22260
|
+
result: this.refusedResult(agentName, call.id, host.error),
|
|
22261
|
+
});
|
|
22262
|
+
return;
|
|
22263
|
+
}
|
|
22264
|
+
const inner = { ...call, name: host.name, input: host.args };
|
|
22265
|
+
this.toolLifecycleEmitter?.retarget(agentName, call.id, inner);
|
|
22266
|
+
this.dispatchToolCall(agentName, inner, turnId);
|
|
22267
|
+
}
|
|
21858
22268
|
/**
|
|
21859
22269
|
* RFC-014 §8.2: a toolsets--act call from the agent (the model's, or a
|
|
21860
22270
|
* script's) goes down the path a direct call of the server's tool takes
|
|
@@ -22669,17 +23079,17 @@ export class AgentFramework {
|
|
|
22669
23079
|
spillNotice(headLength, content, shown, bound, readCap, written) {
|
|
22670
23080
|
if (written && written.error === undefined) {
|
|
22671
23081
|
const canRaise = bound === undefined || cutStoredToolResultToFit(content, bound, shown, bound).head.length > headLength;
|
|
22672
|
-
return `\n\n[truncated — showing ${headLength} of ${content.length} chars; full content: workspace file ${written.path}`
|
|
23082
|
+
return `\n\n[${NOTICE_TAG.truncated} — showing ${headLength} of ${content.length} chars; full content: workspace file ${written.path}`
|
|
22673
23083
|
+ `. ${this.spillPageAdvice(written.path, readCap, canRaise)}]`;
|
|
22674
23084
|
}
|
|
22675
23085
|
if (written) {
|
|
22676
23086
|
// A workspace was there and the write FAILED — say that, loudly and
|
|
22677
23087
|
// distinctly. Telling the agent "no writable workspace" would teach
|
|
22678
23088
|
// them their residence lacks a capability it actually has.
|
|
22679
|
-
return `\n\n[truncated — showing ${headLength} of ${content.length} chars; spill to workspace file ${written.path} FAILED`
|
|
23089
|
+
return `\n\n[${NOTICE_TAG.truncated} — showing ${headLength} of ${content.length} chars; spill to workspace file ${written.path} FAILED`
|
|
22680
23090
|
+ ` (${written.error}); content over the cap was not retained]`;
|
|
22681
23091
|
}
|
|
22682
|
-
return
|
|
23092
|
+
return `\n\n[${NOTICE_TAG.truncated} — original was ${content.length} chars; ${this.noFileMountText()}, full content not retained]`;
|
|
22683
23093
|
}
|
|
22684
23094
|
/**
|
|
22685
23095
|
* The length of the notice a cut of `content` that keeps no head would
|
|
@@ -23539,6 +23949,17 @@ export class AgentFramework {
|
|
|
23539
23949
|
* script left running, which no call watches, has none.
|
|
23540
23950
|
*/
|
|
23541
23951
|
dispatchToolCall(agentName, call, turnId) {
|
|
23952
|
+
// utils was removed; its utilities are in the built-in toolset. Said
|
|
23953
|
+
// whatever the allowlist names, so an old call learns where they went.
|
|
23954
|
+
if (call.name === 'utils') {
|
|
23955
|
+
const removed = this.utilsRemoved(agentName);
|
|
23956
|
+
this.emitTrace({ type: 'tool:failed', module: 'framework', tool: call.name, callId: call.id, error: removed });
|
|
23957
|
+
this.pushEvent({
|
|
23958
|
+
type: 'tool-result', callId: call.id, toolName: call.name, agentName, moduleName: 'framework',
|
|
23959
|
+
result: this.refusedResult(agentName, call.id, removed),
|
|
23960
|
+
});
|
|
23961
|
+
return;
|
|
23962
|
+
}
|
|
23542
23963
|
const refusal = this.agentToolPermissionFailure(agentName, call) ?? this.markedInputFailure(agentName, call);
|
|
23543
23964
|
if (refusal) {
|
|
23544
23965
|
this.pushEvent({
|
|
@@ -23559,6 +23980,16 @@ export class AgentFramework {
|
|
|
23559
23980
|
this.pushEvent({ type: 'tool-result', callId: call.id, toolName: call.name, agentName, moduleName: 'tool-presentation', result });
|
|
23560
23981
|
return;
|
|
23561
23982
|
}
|
|
23983
|
+
// toolsets--act on a host toolset's tool (the built-in one, a server's
|
|
23984
|
+
// tab): the call of that tool, under its own name, down the path a
|
|
23985
|
+
// direct call of it takes, permissions included.
|
|
23986
|
+
if (enrichedCall.name === META_TOOL_NAMES.act) {
|
|
23987
|
+
const host = this.hostPresentation?.resolveAct(agentName, enrichedCall.input) ?? null;
|
|
23988
|
+
if (host) {
|
|
23989
|
+
this.dispatchHostAct(agentName, call, host, turnId);
|
|
23990
|
+
return;
|
|
23991
|
+
}
|
|
23992
|
+
}
|
|
23562
23993
|
// RFC-014: toolsets--act calls a server's dynamic tool, down the same
|
|
23563
23994
|
// path as a direct call of it (dispatchToolsetAct).
|
|
23564
23995
|
if (enrichedCall.name === META_TOOL_NAMES.act && this.toolsetHub && this.mcplServerRegistry) {
|
|
@@ -23584,11 +24015,6 @@ export class AgentFramework {
|
|
|
23584
24015
|
this.dispatchChannelToolCall(agentName, enrichedCall);
|
|
23585
24016
|
return;
|
|
23586
24017
|
}
|
|
23587
|
-
// Route the agent's typed, allowlisted hot-settings surface.
|
|
23588
|
-
if (enrichedCall.name === 'utils') {
|
|
23589
|
-
this.dispatchUtilsToolCall(agentName, enrichedCall);
|
|
23590
|
-
return;
|
|
23591
|
-
}
|
|
23592
24018
|
if (enrichedCall.name === 'agent_settings') {
|
|
23593
24019
|
this.dispatchAgentSettingsToolCall(agentName, enrichedCall);
|
|
23594
24020
|
return;
|
|
@@ -23621,7 +24047,7 @@ export class AgentFramework {
|
|
|
23621
24047
|
void this.tuneOutCoordinator
|
|
23622
24048
|
.handleSubconsciousTool(enrichedCall.name, enrichedCall.input, this.callOwnChannels(agentName, enrichedCall))
|
|
23623
24049
|
// A rejection is answered with a failed result, as a framework tool's
|
|
23624
|
-
// dispatch failure is (
|
|
24050
|
+
// dispatch failure is (dispatchAgentSettingsToolCall), never left unhandled.
|
|
23625
24051
|
.catch((err) => ({ success: false, error: thrownText(err), isError: true }))
|
|
23626
24052
|
.then((result) => {
|
|
23627
24053
|
// Through pushEvent, like every other tool's result, so one that
|
|
@@ -23688,10 +24114,18 @@ export class AgentFramework {
|
|
|
23688
24114
|
}
|
|
23689
24115
|
const wake = readSkipReplyWake(enrichedCall.input);
|
|
23690
24116
|
if (wake.kind === 'wake' && this.eventGate) {
|
|
24117
|
+
const armedAt = Date.now();
|
|
23691
24118
|
const { inMs, armed } = this.eventGate.armSelfWake(agentName, wake.seconds, 'skip_reply');
|
|
24119
|
+
// The heartbeat's tick or reminder due before the timer: if it wakes
|
|
24120
|
+
// the agent, its turn cancels the timer, so the confirmation names
|
|
24121
|
+
// it and the time the self-wake would have left.
|
|
24122
|
+
const earlierWake = armed ? this.heartbeatWakeBefore(agentName, armedAt + inMs) : undefined;
|
|
23692
24123
|
console.error(armed
|
|
23693
24124
|
? `[self-wake] agent=${agentName} armed in ${Math.round(inMs / 1000)}s (skip_reply)`
|
|
24125
|
+
+ (earlierWake ? `; due sooner, in ~${earlierWake.inSeconds}s: ${logDetail(earlierWake.what)}` : '')
|
|
23694
24126
|
: `[self-wake] agent=${agentName} not armed: the framework has stopped (skip_reply)`);
|
|
24127
|
+
this.dispatchChannelToolCall(agentName, enrichedCall, earlierWake);
|
|
24128
|
+
return;
|
|
23695
24129
|
}
|
|
23696
24130
|
}
|
|
23697
24131
|
this.dispatchChannelToolCall(agentName, enrichedCall);
|
|
@@ -23853,7 +24287,7 @@ export class AgentFramework {
|
|
|
23853
24287
|
// messages flush at the next turn's START (before its compile — riding
|
|
23854
24288
|
// their own wake), at the agent's next tool boundary (where they are
|
|
23855
24289
|
// ALSO injected into the live stream — hear-while-acting — all but a
|
|
23856
|
-
// message
|
|
24290
|
+
// message the agent's own view leaves out), or in driveStream's
|
|
23857
24291
|
// finally when the turn ends.
|
|
23858
24292
|
// Quiesce (issue #122): the window exists to run compression/refold
|
|
23859
24293
|
// against a FROZEN context — module events and api message.send must not
|
|
@@ -24044,18 +24478,20 @@ export class AgentFramework {
|
|
|
24044
24478
|
}
|
|
24045
24479
|
}
|
|
24046
24480
|
/** Whether `entry`'s own stored entry is the newest in `agent`'s window,
|
|
24047
|
-
* clock rows
|
|
24048
|
-
* message by its server, channel and
|
|
24049
|
-
* server and event id, and never one
|
|
24481
|
+
* clock rows, stored context injections and rows the agent's own view
|
|
24482
|
+
* leaves out after it aside: a channel message by its server, channel and
|
|
24483
|
+
* message id, a push event by its server and event id, and never one the
|
|
24484
|
+
* agent's view leaves out, such as one a tune-out holds. Past
|
|
24050
24485
|
* NEWEST_SCAN_ROWS such rows, it answers no. */
|
|
24051
24486
|
isNewestIn(agent, entry) {
|
|
24052
24487
|
// A clock row the framework wrote after it (a turn start's, an event's),
|
|
24053
24488
|
// or a stored context injection (any agent's, a start's), isn't a
|
|
24054
24489
|
// message a reply could be meant for, so it doesn't count.
|
|
24055
24490
|
const newest = agent.getContextManager().queryMessages({ limit: AgentFramework.NEWEST_SCAN_ROWS, reverse: true }).messages
|
|
24056
|
-
.find((m) => m.metadata?.kind !== CLOCK_ROW_KIND && !storedInjectionMarkOf(m.metadata)
|
|
24491
|
+
.find((m) => m.metadata?.kind !== CLOCK_ROW_KIND && !storedInjectionMarkOf(m.metadata)
|
|
24492
|
+
&& this.agentSees(agent, m.metadata));
|
|
24057
24493
|
const md = newest?.metadata;
|
|
24058
|
-
if (!md ||
|
|
24494
|
+
if (!md || entry.serverId === undefined || md.serverId !== entry.serverId)
|
|
24059
24495
|
return false;
|
|
24060
24496
|
if (entry.eventType === 'mcpl:channel-incoming') {
|
|
24061
24497
|
return entry.messageId !== undefined && md.messageId === entry.messageId && md.channelId === entry.channelId;
|
|
@@ -24074,11 +24510,19 @@ export class AgentFramework {
|
|
|
24074
24510
|
? this.agents.get(this.primaryAgentName)
|
|
24075
24511
|
: this.agents.values().next().value;
|
|
24076
24512
|
}
|
|
24513
|
+
noteSpokenTo(agentName, metadata) {
|
|
24514
|
+
const agent = this.agents.get(agentName);
|
|
24515
|
+
if ((agent && !this.agentSees(agent, metadata)) || isOwnScheduleWrite(metadata) || !isConversationalInjection(metadata))
|
|
24516
|
+
return;
|
|
24517
|
+
if (this.turnRearms?.delete(agentName))
|
|
24518
|
+
console.error(`[inference-rearm] agent=${agentName} re-send count started again: a message was said to it`);
|
|
24519
|
+
}
|
|
24077
24520
|
/** The notes addMessage takes for a message it stored: an arrival for the
|
|
24078
24521
|
* agent when none of its turns is alive or the message was assembled for
|
|
24079
24522
|
* the start now beginning (no tool boundary will weigh it), and one for
|
|
24080
24523
|
* every other resident's open turn. */
|
|
24081
24524
|
noteStored(agent, metadata, stored, bypassDeferralFor) {
|
|
24525
|
+
this.noteSpokenTo(agent.name, metadata);
|
|
24082
24526
|
if (!this.activeTurnTokens.has(agent.name) || bypassDeferralFor === agent.name) {
|
|
24083
24527
|
this.noteArrival(agent.name, metadata, stored);
|
|
24084
24528
|
}
|
|
@@ -24525,6 +24969,7 @@ export class AgentFramework {
|
|
|
24525
24969
|
const stored = this.writeDrainedInOrder(this.drainDeferredFor(agent.name), `${occasion} flush for ${agent.name}`, (msg) => agent.getContextManager().addMessage(msg.participant, msg.content, withDeferredWriteId(msg.metadata, msg.id)));
|
|
24526
24970
|
for (const [msg, { id }] of stored) {
|
|
24527
24971
|
this.emitTrace({ type: 'message:added', messageId: id, source: `deferred-flush:${occasion}` });
|
|
24972
|
+
this.noteSpokenTo(agent.name, msg.metadata);
|
|
24528
24973
|
this.noteArrival(agent.name, msg.metadata, id);
|
|
24529
24974
|
this.noteForOtherResidents(agent, msg.metadata, id);
|
|
24530
24975
|
}
|
|
@@ -25017,18 +25462,18 @@ export class AgentFramework {
|
|
|
25017
25462
|
const outcome = failure.outcome === 'failed' || failure.outcome === 'unknown' ? failure.outcome : undefined;
|
|
25018
25463
|
const detail = typeof failure.detail === 'string' ? failure.detail : undefined;
|
|
25019
25464
|
const size = textLen !== undefined ? ` (${textLen} chars)` : '';
|
|
25020
|
-
const why = reason !== undefined ? ` (${reason})` : '';
|
|
25465
|
+
const why = reason !== undefined ? ` (${suppliedValue(reason)})` : '';
|
|
25021
25466
|
// A notice queued by an earlier build names no server: its channel is
|
|
25022
25467
|
// the one registered channel its id means, or shown by its id alone.
|
|
25023
25468
|
const named = this.channelNamed(failure.channelId, typeof failure.serverId === 'string' ? failure.serverId : undefined);
|
|
25024
|
-
const where = named ? this.channelForNotice(named) : failure.channelId;
|
|
25469
|
+
const where = named ? this.channelForNotice(named) : suppliedValue(failure.channelId);
|
|
25025
25470
|
const text = outcome === 'unknown'
|
|
25026
|
-
? `[
|
|
25471
|
+
? `[${NOTICE_TAG.discordSendFailed}] Delivery of a reply by ${speaker}${size} to ${where} was not confirmed${why}. ` +
|
|
25027
25472
|
`It may or may not have been posted, so sending it again could post it twice. ${speaker} has ended.`
|
|
25028
|
-
: `[
|
|
25473
|
+
: `[${NOTICE_TAG.discordSendFailed}] A reply by ${speaker}${size} could not be delivered to ${where}${why}. ` +
|
|
25029
25474
|
`Nothing was posted. ${speaker} has ended.`;
|
|
25030
25475
|
return {
|
|
25031
|
-
content: [{ type: 'text', text: text + (detail !== undefined ? ` Connector detail: ${detail}` : '') }],
|
|
25476
|
+
content: [{ type: 'text', text: text + (detail !== undefined ? ` Connector detail: ${suppliedValue(detail)}` : '') }],
|
|
25032
25477
|
metadata: {
|
|
25033
25478
|
system: true, kind: 'discord-send-failed', channelId: failure.channelId, speaker,
|
|
25034
25479
|
// The channel's server, as the marker it passes on carries it.
|
|
@@ -25055,17 +25500,16 @@ export class AgentFramework {
|
|
|
25055
25500
|
* channel no string picks alone, its id and its server.
|
|
25056
25501
|
*/
|
|
25057
25502
|
channelShown(channel) {
|
|
25058
|
-
const name = this.channelRegistry?.nameShown(channel) ?? channel.channelId;
|
|
25503
|
+
const name = this.channelRegistry?.nameShown(channel) ?? suppliedValue(channel.channelId);
|
|
25059
25504
|
const label = this.channelRegistry?.getDescriptor(channel)?.label;
|
|
25060
|
-
|
|
25061
|
-
|
|
25062
|
-
: name;
|
|
25505
|
+
const shown = label && label !== channel.channelId ? suppliedValue(label) : undefined;
|
|
25506
|
+
return shown ? `${shown.startsWith('#') ? '' : '#'}${shown} (${name})` : name;
|
|
25063
25507
|
}
|
|
25064
25508
|
/** A channel named in a notice with the label it was given there: the
|
|
25065
25509
|
* label, with its name as ChannelRegistry.nameShown gives it. */
|
|
25066
25510
|
labelledChannel(channel) {
|
|
25067
|
-
const name = this.channelRegistry?.nameShown(channel) ?? channel.channelId;
|
|
25068
|
-
return channel.label && channel.label !== channel.channelId ? `${channel.label} (${name})` : name;
|
|
25511
|
+
const name = this.channelRegistry?.nameShown(channel) ?? suppliedValue(channel.channelId);
|
|
25512
|
+
return channel.label && channel.label !== channel.channelId ? `${suppliedValue(channel.label)} (${name})` : name;
|
|
25069
25513
|
}
|
|
25070
25514
|
/**
|
|
25071
25515
|
* A module's write through its context (ModuleContext.addMessage):
|
|
@@ -25481,6 +25925,11 @@ export class AgentFramework {
|
|
|
25481
25925
|
if (name && this.exhaustionRewinds.get(name)) {
|
|
25482
25926
|
this.exhaustionRewinds.set(name, 0);
|
|
25483
25927
|
}
|
|
25928
|
+
if (name)
|
|
25929
|
+
this.turnRearms?.delete(name);
|
|
25930
|
+
}
|
|
25931
|
+
if (event.type === 'inference:completed' || event.type === 'inference:exhausted' || event.type === 'inference:aborted') {
|
|
25932
|
+
this.endStatedWaitRun(event.agentName);
|
|
25484
25933
|
}
|
|
25485
25934
|
const traceEvent = {
|
|
25486
25935
|
...event,
|
|
@@ -25788,7 +26237,7 @@ export class AgentFramework {
|
|
|
25788
26237
|
streak,
|
|
25789
26238
|
// The refused agent's own view: a subagent's refusal names no channel.
|
|
25790
26239
|
// Named as the agent is shown it, so a shared id names its server.
|
|
25791
|
-
locus: ((channel) => (channel ? this.channelRegistry?.nameShown(channel) ?? channel.channelId : null))(this.channelRegistry?.incomingFor(agentName)?.channel),
|
|
26240
|
+
locus: ((channel) => (channel ? this.channelRegistry?.nameShown(channel) ?? suppliedValue(channel.channelId) : null))(this.channelRegistry?.incomingFor(agentName)?.channel),
|
|
25792
26241
|
tokens: tokens ?? null,
|
|
25793
26242
|
});
|
|
25794
26243
|
if (streak >= 2) {
|
|
@@ -25853,6 +26302,7 @@ export class AgentFramework {
|
|
|
25853
26302
|
noteInferenceExhausted(agentName, rawReason, retryable, rawErrorType, detail) {
|
|
25854
26303
|
const ended = this.exhaustedTurns?.get(agentName);
|
|
25855
26304
|
this.exhaustedTurns?.delete(agentName);
|
|
26305
|
+
const rearmed = this.exhaustingRearms?.delete(agentName) === true;
|
|
25856
26306
|
// The reason is the failure's own message, and a provider can echo the
|
|
25857
26307
|
// whole rejected request in it: one household's [inference-failed]
|
|
25858
26308
|
// marker carried ~1.7 MB of its own request back into its context. Every
|
|
@@ -25902,7 +26352,7 @@ export class AgentFramework {
|
|
|
25902
26352
|
this.logFailure({ agent: agentName, consecutive: streak, reason, kind: 'inference-exhausted', ...classification, ...cutAt(reason) });
|
|
25903
26353
|
// (2) Agent-facing chronicle marker (no inference triggered → no loop).
|
|
25904
26354
|
const agent = this.agents.get(agentName);
|
|
25905
|
-
if (agent && process.env.SUPPRESS_INFERENCE_FAILED_MARKER !== '1') {
|
|
26355
|
+
if (agent && !(rearmed && !ended) && process.env.SUPPRESS_INFERENCE_FAILED_MARKER !== '1') {
|
|
25906
26356
|
const cause = [errorType, httpStatus !== undefined ? `HTTP ${httpStatus}` : undefined]
|
|
25907
26357
|
.filter((part) => part !== undefined)
|
|
25908
26358
|
.join(', ');
|
|
@@ -25910,15 +26360,15 @@ export class AgentFramework {
|
|
|
25910
26360
|
agent.getContextManager().addMessage('user', [{
|
|
25911
26361
|
type: 'text',
|
|
25912
26362
|
text: (ended
|
|
25913
|
-
? `[
|
|
25914
|
-
`failed${cause ? ` (${cause})` : ''}.` +
|
|
26363
|
+
? `[${NOTICE_TAG.inferenceFailed}] Your previous turn did not complete: it ` +
|
|
26364
|
+
`failed${cause ? ` (${suppliedValue(cause)})` : ''}.` +
|
|
25915
26365
|
`${ended.roundsRan ? ' Its earlier rounds ran, and their tool calls ran as their results show.' : ''}` +
|
|
25916
26366
|
`${ended.callsCutOff ? ` ${CALLS_CUT_OFF}` : ''}` +
|
|
25917
26367
|
`${ended.liveSpeech ? ` ${LIVE_SPEECH_UNRECEIPTED}` : ''}` +
|
|
25918
26368
|
`${ended.speechRouting ? ` ${SPEECH_STILL_ROUTING}` : ''} Reason: `
|
|
25919
|
-
: `[
|
|
25920
|
-
`call failed${cause ? ` (${cause})` : ''} and produced no response, so nothing was sent. Reason: `) +
|
|
25921
|
-
`${markerReason}. If this recurs with the same cause, change approach ` +
|
|
26369
|
+
: `[${NOTICE_TAG.inferenceFailed}] Your previous turn did not complete: the model ` +
|
|
26370
|
+
`call failed${cause ? ` (${suppliedValue(cause)})` : ''} and produced no response, so nothing was sent. Reason: `) +
|
|
26371
|
+
`${failureShown(markerReason)}. If this recurs with the same cause, change approach ` +
|
|
25922
26372
|
`rather than retrying identically (e.g. drop an oversized attachment ` +
|
|
25923
26373
|
`or an unsupported setting).`,
|
|
25924
26374
|
}], { system: true, kind: 'inference-failed', reason: markerReason, consecutive: streak, ...classification, ...cutAt(markerReason) });
|
|
@@ -26168,13 +26618,14 @@ export class AgentFramework {
|
|
|
26168
26618
|
// channel hijacks it. An opening start resolves the turn's locus from
|
|
26169
26619
|
// it (the trigger channel then), and the channel context names it, so
|
|
26170
26620
|
// a later start of the turn is told where its unprefixed speech lands
|
|
26171
|
-
// unless a hybrid `>>>` destination takes it. Empty for a
|
|
26172
|
-
//
|
|
26173
|
-
//
|
|
26174
|
-
//
|
|
26621
|
+
// unless a hybrid `>>>` destination takes it. Empty for a no-trigger
|
|
26622
|
+
// turn until its locus is resolved, so that resolution takes the
|
|
26623
|
+
// global fallback (none for an ephemeral run), and the turn then keeps
|
|
26624
|
+
// that channel; null for a heartbeat's turn, which takes none
|
|
26625
|
+
// (TurnRouting.heartbeat).
|
|
26175
26626
|
activeChannelResolver: (agentName) => {
|
|
26176
26627
|
const routing = this.turnRouting.get(agentName);
|
|
26177
|
-
return routing?.locus ?? routing?.triggerChannel ?? (routing?.noChannel ? null : undefined);
|
|
26628
|
+
return routing?.locus ?? routing?.triggerChannel ?? (routing?.noChannel || routing?.heartbeat ? null : undefined);
|
|
26178
26629
|
},
|
|
26179
26630
|
// An ephemeral run (a subagent) never takes the process-global
|
|
26180
26631
|
// fallback: its prose is its caller's result, and the latest inbound
|
|
@@ -26230,11 +26681,11 @@ export class AgentFramework {
|
|
|
26230
26681
|
? (outcome === 'unknown'
|
|
26231
26682
|
// A request went out and no valid receipt came back: it may
|
|
26232
26683
|
// have been posted (in part or whole). Never claim it wasn't.
|
|
26233
|
-
? `[
|
|
26234
|
-
: `[
|
|
26235
|
-
: `[
|
|
26684
|
+
? `[${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.`
|
|
26685
|
+
: `[${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.`)
|
|
26686
|
+
: `[${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.`;
|
|
26236
26687
|
// A registered speaker's notice is addressed to it (noticeTargetFor).
|
|
26237
|
-
this.addMessage('user', [{ type: 'text', text: text + (detail !== undefined ? ` Connector detail: ${detail}` : '') }], metadata, { forAgent: conversationId });
|
|
26688
|
+
this.addMessage('user', [{ type: 'text', text: text + (detail !== undefined ? ` Connector detail: ${suppliedValue(detail)}` : '') }], metadata, { forAgent: conversationId });
|
|
26238
26689
|
}
|
|
26239
26690
|
catch (err) {
|
|
26240
26691
|
console.error('onRouteFailure: failed to record send-failure marker:', err);
|
|
@@ -26351,6 +26802,9 @@ export class AgentFramework {
|
|
|
26351
26802
|
restartClaims() {
|
|
26352
26803
|
return (this.mcpRestartClaims ??= { ids: new Map(), prefixes: new Map() });
|
|
26353
26804
|
}
|
|
26805
|
+
closedTabConfigs() {
|
|
26806
|
+
return (this.closedTabConfigMap ??= new Map());
|
|
26807
|
+
}
|
|
26354
26808
|
legacyConnectAborts() {
|
|
26355
26809
|
return (this.legacyConnectAbortMap ??= new Map());
|
|
26356
26810
|
}
|
|
@@ -26898,14 +27352,63 @@ export class AgentFramework {
|
|
|
26898
27352
|
'it stays registered until a disconnect, restart or shutdown closes it:', closeError instanceof Error ? closeError.message : closeError);
|
|
26899
27353
|
});
|
|
26900
27354
|
}
|
|
27355
|
+
/** RFC-014: who held the name `toolsets` at startup (a module given to
|
|
27356
|
+
* create(); a server's toolPrefix of it is refused), so a server that
|
|
27357
|
+
* leaves toolHints out has none for the life of this framework; null when
|
|
27358
|
+
* the name was free. */
|
|
27359
|
+
toolsetsNameHeldAtStart = null;
|
|
27360
|
+
/** At startup: note who holds the name the meta-tools need, and say which
|
|
27361
|
+
* servers that leaves without hints. */
|
|
27362
|
+
reserveToolsetsName(modules, servers) {
|
|
27363
|
+
const module = modules.find((m) => m.name === TOOLSETS_MODULE);
|
|
27364
|
+
this.toolsetsNameHeldAtStart = module ? `module "${TOOLSETS_MODULE}"` : null;
|
|
27365
|
+
if (!this.toolsetsNameHeldAtStart)
|
|
27366
|
+
return;
|
|
27367
|
+
// Those that would have had hints: MCPL servers that leave the setting out.
|
|
27368
|
+
const defaulted = servers
|
|
27369
|
+
.filter((s) => (s.toolHints === undefined || s.toolHints === null) && isLegacyFamily(s))
|
|
27370
|
+
.map((s) => s.id);
|
|
27371
|
+
if (defaulted.length) {
|
|
27372
|
+
console.error(`[mcpl] tool hints (RFC-014) are off for ${defaulted.join(', ')}, which leave toolHints out: the name "${TOOLSETS_MODULE}" ` +
|
|
27373
|
+
`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.`);
|
|
27374
|
+
}
|
|
27375
|
+
}
|
|
27376
|
+
/**
|
|
27377
|
+
* RFC-014: whether a server has tool hints. Only an MCPL (legacy-family)
|
|
27378
|
+
* server can: a modern MCP server never has them. `true` and `false` are
|
|
27379
|
+
* as configured. Left out (or null) they're on, wherever the name
|
|
27380
|
+
* `toolsets` the meta-tools need is free: no other module holds it and no
|
|
27381
|
+
* server's prefix is it, now or at startup. So a configuration that loaded
|
|
27382
|
+
* when hints were opt-in still loads. A name held at startup stays held
|
|
27383
|
+
* for the framework's life.
|
|
27384
|
+
*/
|
|
27385
|
+
hintsFor(config) {
|
|
27386
|
+
if (config.toolHints === false || !isLegacyFamily(config))
|
|
27387
|
+
return false;
|
|
27388
|
+
if (config.toolHints === true)
|
|
27389
|
+
return true;
|
|
27390
|
+
if (this.toolsetsNameHeldAtStart !== null)
|
|
27391
|
+
return false;
|
|
27392
|
+
if ((config.toolPrefix ?? `mcpl--${config.id}`) === TOOLSETS_MODULE)
|
|
27393
|
+
return false;
|
|
27394
|
+
const module = this.moduleRegistry.getAllModules().find((m) => m.name === TOOLSETS_MODULE);
|
|
27395
|
+
if (module && !(module instanceof ToolsetsModule))
|
|
27396
|
+
return false;
|
|
27397
|
+
// A restart moving its server to the prefix holds the name as a claim.
|
|
27398
|
+
if (this.restartClaims().prefixes.has(TOOLSETS_MODULE))
|
|
27399
|
+
return false;
|
|
27400
|
+
const owner = this.mcplPrefixMap.get(TOOLSETS_MODULE);
|
|
27401
|
+
return owner === undefined || owner === config.id;
|
|
27402
|
+
}
|
|
26901
27403
|
/**
|
|
26902
27404
|
* The host capabilities offered to one server: the host's own, plus
|
|
26903
|
-
* RFC-014's `toolHints`
|
|
26904
|
-
* the very object every server is
|
|
27405
|
+
* RFC-014's `toolHints` unless its configuration turns them off
|
|
27406
|
+
* (`toolHints: false`). With them off, the very object every server is
|
|
27407
|
+
* offered.
|
|
26905
27408
|
*/
|
|
26906
|
-
mcplHostCapabilitiesFor(config) {
|
|
27409
|
+
mcplHostCapabilitiesFor(config, hints = this.hintsFor(config)) {
|
|
26907
27410
|
const base = this.mcplHostCapabilities;
|
|
26908
|
-
return
|
|
27411
|
+
return hints ? { ...base, toolHints: true } : base;
|
|
26909
27412
|
}
|
|
26910
27413
|
/** RFC-014: every toolHints server's presence, made on first use. */
|
|
26911
27414
|
toolsets() {
|
|
@@ -26923,20 +27426,181 @@ export class AgentFramework {
|
|
|
26923
27426
|
return undefined;
|
|
26924
27427
|
return this.mcplTools.some((t) => t.name === name) ? 'static' : 'dynamic';
|
|
26925
27428
|
},
|
|
27429
|
+
hostOpen: (agentName) => this.hostPresentation?.openMarkers(agentName) ?? [],
|
|
26926
27430
|
}));
|
|
26927
27431
|
}
|
|
26928
|
-
/**
|
|
26929
|
-
*
|
|
26930
|
-
*
|
|
27432
|
+
/**
|
|
27433
|
+
* The host's own toolsets (host-presentation.ts): the built-in "settings"
|
|
27434
|
+
* toolset and each MCPL server's tab. State is kept beside the store, in
|
|
27435
|
+
* `config/toolsets.json`; with no store path, in memory only.
|
|
27436
|
+
*/
|
|
27437
|
+
makeHostPresentation(storePath) {
|
|
27438
|
+
return new HostPresentation({
|
|
27439
|
+
// As the agent is shown them: its description overrides applied.
|
|
27440
|
+
available: (agentName) => (this.agents.has(agentName) ? this.compressionToolsForAgent(agentName) : this.getAllTools()),
|
|
27441
|
+
placedInBuiltin: (name) => this.placedInBuiltin(name),
|
|
27442
|
+
hidden: (agentName) => {
|
|
27443
|
+
if (!this.agents.has(agentName))
|
|
27444
|
+
return new Set();
|
|
27445
|
+
const snapshot = this.inspectToolPresentation(agentName);
|
|
27446
|
+
return new Set(snapshot?.entries.filter((e) => !e.visible).map((e) => e.name) ?? []);
|
|
27447
|
+
},
|
|
27448
|
+
serverOf: (name) => this.resolveMcplTool(name)?.[0] ?? null,
|
|
27449
|
+
servers: () => {
|
|
27450
|
+
const out = new Map();
|
|
27451
|
+
for (const [id, config] of this.mcplServerConfigs) {
|
|
27452
|
+
out.set(id, { id, configured: config.presentation ?? 'full', connected: !!this.mcplServerRegistry?.getServer(id) || this.modernServers().has(id) });
|
|
27453
|
+
}
|
|
27454
|
+
for (const [id, config] of this.closedTabConfigs()) {
|
|
27455
|
+
if (!out.has(id))
|
|
27456
|
+
out.set(id, { id, configured: config.presentation ?? 'full', connected: false });
|
|
27457
|
+
}
|
|
27458
|
+
return [...out.values()];
|
|
27459
|
+
},
|
|
27460
|
+
canUseMeta: (agentName) => this.hostToolsetsFor(agentName),
|
|
27461
|
+
disconnect: async (serverId) => {
|
|
27462
|
+
const config = this.mcplServerConfigs.get(serverId);
|
|
27463
|
+
if (!config)
|
|
27464
|
+
throw new Error(`no MCPL server "${serverId}" is connected here`);
|
|
27465
|
+
this.refuseOperatorAfterStop('disconnect MCPL server');
|
|
27466
|
+
// As a transient close: its checkpoint trees and RFC-014 state stay
|
|
27467
|
+
// for the connect its next open makes.
|
|
27468
|
+
await this.trackOperatorOperation('mcpl:disconnect', () => this.runDisconnectMcplServer(serverId, { keepToolsets: true, keepCheckpoints: true }));
|
|
27469
|
+
this.closedTabConfigs().set(serverId, config);
|
|
27470
|
+
},
|
|
27471
|
+
reconnect: async (serverId) => {
|
|
27472
|
+
const config = this.closedTabConfigs().get(serverId);
|
|
27473
|
+
if (!config)
|
|
27474
|
+
throw new Error(`no MCPL server "${serverId}" is kept as a closed tab here`);
|
|
27475
|
+
this.refuseOperatorAfterStop('connect MCPL server');
|
|
27476
|
+
await this.trackOperatorOperation('mcpl:connect', () => this.runConnectMcplServer(config, undefined, { reopen: true }));
|
|
27477
|
+
},
|
|
27478
|
+
log: (line) => console.error(line),
|
|
27479
|
+
isUtility: (name) => this.isModuleUtility(name),
|
|
27480
|
+
residents: () => this.heartbeatTargets(),
|
|
27481
|
+
}, storePath ? join(storePath, 'config', 'toolsets.json') : null);
|
|
27482
|
+
}
|
|
27483
|
+
/** Whether a name is a module utility (and not a tool of its module, which takes the name). */
|
|
27484
|
+
isModuleUtility(name) {
|
|
27485
|
+
const sep = name.indexOf('--');
|
|
27486
|
+
if (sep <= 0)
|
|
27487
|
+
return false;
|
|
27488
|
+
const module = this.moduleRegistry.getModule(name.slice(0, sep));
|
|
27489
|
+
if (!module)
|
|
27490
|
+
return false;
|
|
27491
|
+
const bare = name.slice(sep + 2);
|
|
27492
|
+
try {
|
|
27493
|
+
return !module.getTools().some((t) => t.name === bare) && (module.getUtilities?.()?.some((t) => t.name === bare) ?? false);
|
|
27494
|
+
}
|
|
27495
|
+
catch {
|
|
27496
|
+
return false;
|
|
27497
|
+
}
|
|
27498
|
+
}
|
|
27499
|
+
/** Whether an agent has the host's own toolsets: the meta-tools registered, and
|
|
27500
|
+
* usable by it (the subconscious keeps its own small surface). */
|
|
27501
|
+
/**
|
|
27502
|
+
* What a call of the removed `utils` is answered: where utilities went, as
|
|
27503
|
+
* this agent can reach them. One that can't use the built-in toolset is
|
|
27504
|
+
* told to call a utility by its own name and which ones it may call.
|
|
27505
|
+
*/
|
|
27506
|
+
utilsRemoved(agentName) {
|
|
27507
|
+
if (agentName === undefined || this.hostToolsetsFor(agentName))
|
|
27508
|
+
return UTILS_REMOVED;
|
|
27509
|
+
const agent = this.agents.get(agentName);
|
|
27510
|
+
const why = this.moduleRegistry.getAllModules().find((m) => m.name === TOOLSETS_MODULE) instanceof ToolsetsModule
|
|
27511
|
+
? `its allowedTools doesn't permit ${META_TOOL_NAMES.open} and ${META_TOOL_NAMES.act}`
|
|
27512
|
+
: `this host's module named "${TOOLSETS_MODULE}" isn't the framework's`;
|
|
27513
|
+
const mine = this.getAllTools().filter((t) => this.isModuleUtility(t.name) && (agent?.canUseTool(t.name) ?? true)).map((t) => t.name);
|
|
27514
|
+
return `utils was removed: module utilities are in the host's built-in toolset now, which this agent can't use (${why}). ` +
|
|
27515
|
+
`Call a utility by its own name instead${mine.length ? `; this agent may call ${mine.join(', ')}` : '; this agent may call none'}. Nothing was run.`;
|
|
27516
|
+
}
|
|
27517
|
+
hostToolsetsFor(agentName) {
|
|
27518
|
+
if (agentName === this.subconsciousAgentName)
|
|
27519
|
+
return false;
|
|
27520
|
+
const module = this.moduleRegistry.getAllModules().find((m) => m.name === TOOLSETS_MODULE);
|
|
27521
|
+
if (!(module instanceof ToolsetsModule))
|
|
27522
|
+
return false;
|
|
27523
|
+
const agent = this.agents.get(agentName);
|
|
27524
|
+
if (!agent)
|
|
27525
|
+
return true;
|
|
27526
|
+
return agent.canUseTool(META_TOOL_NAMES.open) && agent.canUseTool(META_TOOL_NAMES.act);
|
|
27527
|
+
}
|
|
27528
|
+
/**
|
|
27529
|
+
* Whether a host tool belongs to the built-in toolset: the framework's own
|
|
27530
|
+
* rarely-used ones (BUILTIN_TOOLSET_TOOLS), a module's that it places there
|
|
27531
|
+
* (Module.toolPlacement), and every module utility.
|
|
27532
|
+
*/
|
|
27533
|
+
placedInBuiltin(name) {
|
|
27534
|
+
if (BUILTIN_TOOLSET_TOOLS.has(name))
|
|
27535
|
+
return true;
|
|
27536
|
+
const sep = name.indexOf('--');
|
|
27537
|
+
if (sep <= 0)
|
|
27538
|
+
return false;
|
|
27539
|
+
const module = this.moduleRegistry.getModule(name.slice(0, sep));
|
|
27540
|
+
if (!module)
|
|
27541
|
+
return false;
|
|
27542
|
+
const bare = name.slice(sep + 2);
|
|
27543
|
+
// A module whose listing throws keeps its tools where they are.
|
|
27544
|
+
try {
|
|
27545
|
+
if (module.getTools().some((t) => t.name === bare))
|
|
27546
|
+
return module.toolPlacement?.(bare) === 'builtin';
|
|
27547
|
+
return module.getUtilities?.()?.some((t) => t.name === bare) ?? false;
|
|
27548
|
+
}
|
|
27549
|
+
catch {
|
|
27550
|
+
return false;
|
|
27551
|
+
}
|
|
27552
|
+
}
|
|
27553
|
+
/**
|
|
27554
|
+
* The tool list an agent's requests carry, from the tools it may call: its
|
|
27555
|
+
* host presentation applied (the built-in toolset's tools, the servers it
|
|
27556
|
+
* has as tabs and its hidden tools left out; a utility never in it), as
|
|
27557
|
+
* every request of a registered agent has it (with its own tool
|
|
27558
|
+
* presentation, hidden tools and descriptions, applied first). For a stream that isn't
|
|
27559
|
+
* registered yet, such as a fork whose orientation is written before its
|
|
27560
|
+
* run starts, the host says whether it will be able to call
|
|
27561
|
+
* `toolsets--open` and `toolsets--act` (`canUseMeta`); left out, the agent
|
|
27562
|
+
* registered under that name is asked. Presentation it chose itself (open
|
|
27563
|
+
* toolsets, tabs) is read under that name, so a new name starts from the
|
|
27564
|
+
* configured presentation.
|
|
27565
|
+
*/
|
|
27566
|
+
presentedTools(agentName, tools, canUseMeta) {
|
|
27567
|
+
// A registered agent's own presentation first, as its requests have it
|
|
27568
|
+
// before the host's: the tools it hid left out, its descriptions applied.
|
|
27569
|
+
const snapshot = this.agents.has(agentName) ? this.inspectToolPresentation(agentName) : undefined;
|
|
27570
|
+
if (snapshot) {
|
|
27571
|
+
const hidden = new Set(snapshot.entries.filter((e) => !e.visible).map((e) => e.name));
|
|
27572
|
+
const shown = new Map(snapshot.advertised.map((t) => [t.name, t]));
|
|
27573
|
+
tools = tools.filter((t) => !hidden.has(t.name)).map((t) => shown.get(t.name) ?? t);
|
|
27574
|
+
}
|
|
27575
|
+
if (!this.hostPresentation)
|
|
27576
|
+
return tools;
|
|
27577
|
+
if (canUseMeta === undefined)
|
|
27578
|
+
return this.hostPresentation.header(agentName, tools);
|
|
27579
|
+
// As hostToolsetsFor decides for a registered agent: the subconscious and
|
|
27580
|
+
// a host whose module named toolsets isn't the framework's present nothing.
|
|
27581
|
+
const meta = canUseMeta && agentName !== this.subconsciousAgentName
|
|
27582
|
+
&& this.moduleRegistry.getAllModules().find((m) => m.name === TOOLSETS_MODULE) instanceof ToolsetsModule;
|
|
27583
|
+
return this.hostPresentation.header(agentName, tools, meta);
|
|
27584
|
+
}
|
|
27585
|
+
/** The host presentation of a server's tools, for the tool list: the agent's
|
|
27586
|
+
* view, less the built-in toolset and the servers it has as tabs. */
|
|
27587
|
+
headerTools(agentName, tools) {
|
|
27588
|
+
return this.hostPresentation ? this.hostPresentation.header(agentName, tools) : tools;
|
|
27589
|
+
}
|
|
27590
|
+
/** The `toolsets` module (toolsets--open, --act, --look, --close): the
|
|
27591
|
+
* meta-tools of the host's own toolsets and of RFC-014 servers' toolsets.
|
|
27592
|
+
* Registered once, at create(), unless a module given to create() holds
|
|
27593
|
+
* the name; a server with toolHints: true then can't have hints, and its
|
|
27594
|
+
* connect is refused here. Its tools don't change after that. */
|
|
26931
27595
|
async ensureToolsetsModule() {
|
|
26932
27596
|
const hub = this.toolsets();
|
|
26933
27597
|
const existing = this.moduleRegistry.getAllModules().find((m) => m.name === TOOLSETS_MODULE);
|
|
26934
27598
|
if (existing instanceof ToolsetsModule)
|
|
26935
27599
|
return;
|
|
26936
27600
|
if (existing) {
|
|
26937
|
-
throw new Error(`A module named "${TOOLSETS_MODULE}" is registered, and a server configured with toolHints needs that name for its meta-tools`);
|
|
27601
|
+
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`);
|
|
26938
27602
|
}
|
|
26939
|
-
await this.addModule(new ToolsetsModule(hub, (call) => this.changeTaker(call)));
|
|
27603
|
+
await this.addModule(new ToolsetsModule(hub, (call) => this.changeTaker(call), this.hostPresentation));
|
|
26940
27604
|
}
|
|
26941
27605
|
/** RFC-014 §5.3: after a (re)connect whose policy has settled, read what
|
|
26942
27606
|
* the server holds open and adopt it. In the background: its notices are
|
|
@@ -26969,7 +27633,7 @@ export class AgentFramework {
|
|
|
26969
27633
|
return false;
|
|
26970
27634
|
}
|
|
26971
27635
|
try {
|
|
26972
|
-
this.addMessage('user', [{ type: 'text', text: `MCPL server "${serverId}", tool changes:\n${text}` }], { system: true, kind: 'mcpl-toolsets', serverId, ...(opts.invite ? { invite: true } : {}) });
|
|
27636
|
+
this.addMessage('user', [{ type: 'text', text: `[${NOTICE_TAG.toolsets}] MCPL server "${serverId}", tool changes:\n${text}` }], { system: true, kind: 'mcpl-toolsets', serverId, ...(opts.invite ? { invite: true } : {}) });
|
|
26973
27637
|
return true;
|
|
26974
27638
|
}
|
|
26975
27639
|
catch (error) {
|
|
@@ -27119,20 +27783,24 @@ export class AgentFramework {
|
|
|
27119
27783
|
let registeredEpoch = 0;
|
|
27120
27784
|
const abort = new AbortController();
|
|
27121
27785
|
this.legacyConnectAborts().set(config.id, abort);
|
|
27786
|
+
// RFC-014: decided once for this connect, so the meta-tools, the offer
|
|
27787
|
+
// and the read tap agree though the state hintsFor reads can change
|
|
27788
|
+
// across the awaits below.
|
|
27789
|
+
const hints = this.hintsFor(config);
|
|
27122
27790
|
try {
|
|
27123
|
-
// RFC-014: a
|
|
27124
|
-
if (
|
|
27791
|
+
// RFC-014: a hinted server's meta-tools exist before it connects.
|
|
27792
|
+
if (hints)
|
|
27125
27793
|
await this.ensureToolsetsModule();
|
|
27126
27794
|
// Its policy exchange is in flight from its registration until the
|
|
27127
27795
|
// exchange settles: an opener that runs meanwhile (a resume, startup's
|
|
27128
27796
|
// staged open) passes the connection by, so nothing opens its planes
|
|
27129
27797
|
// before its grant exists.
|
|
27130
|
-
connection = await this.mcplServerRegistry.addServer(connectionConfig, this.mcplHostCapabilitiesFor(config), abort.signal, (registering) => {
|
|
27798
|
+
connection = await this.mcplServerRegistry.addServer(connectionConfig, this.mcplHostCapabilitiesFor(config, hints), abort.signal, (registering) => {
|
|
27131
27799
|
registeredEpoch = registering.transportEpoch;
|
|
27132
27800
|
this.mcplPolicyExchanges.set(registering, registeredEpoch);
|
|
27133
27801
|
// RFC-014 §6.1: hints apply as the connection reads them, from its
|
|
27134
27802
|
// first message, so the tap is on before any I/O can deliver one.
|
|
27135
|
-
if (
|
|
27803
|
+
if (hints)
|
|
27136
27804
|
this.toolsets().controllerFor(config).attach(registering);
|
|
27137
27805
|
});
|
|
27138
27806
|
}
|
|
@@ -27444,7 +28112,7 @@ export class AgentFramework {
|
|
|
27444
28112
|
// instances; toolHints withdrawn makes every tool static. Its impact is
|
|
27445
28113
|
// surface-changed, applied, with no grant consequence (§17.6).
|
|
27446
28114
|
const declarations = toolsetDeclarationsOf(fetched);
|
|
27447
|
-
const toolsetsChanged = config
|
|
28115
|
+
const toolsetsChanged = this.hintsFor(config)
|
|
27448
28116
|
&& JSON.stringify(declarations) !== JSON.stringify(connection.toolsetDeclarations);
|
|
27449
28117
|
connection.toolsetDeclarations = declarations;
|
|
27450
28118
|
if (toolsetsChanged)
|
|
@@ -27727,7 +28395,7 @@ export class AgentFramework {
|
|
|
27727
28395
|
}
|
|
27728
28396
|
/** connectMcplServer's work, for a restart's connect too: the id and tool
|
|
27729
28397
|
* prefix its `claim` holds are free to it, and refused to everyone else. */
|
|
27730
|
-
async runConnectMcplServer(config, claim) {
|
|
28398
|
+
async runConnectMcplServer(config, claim, opts = {}) {
|
|
27731
28399
|
assertServerConfigBooleans(config);
|
|
27732
28400
|
// Lazily bring up the MCPL subsystem — a framework that started with zero
|
|
27733
28401
|
// configured servers can still deploy its first one at runtime.
|
|
@@ -27753,8 +28421,31 @@ export class AgentFramework {
|
|
|
27753
28421
|
// registers. A server already registered was refused just above, so the
|
|
27754
28422
|
// read never reaches a registered server's trees.
|
|
27755
28423
|
this.checkpointManager?.loadHostStates(config.id);
|
|
28424
|
+
// An operator's connect ends a closed tab as it begins, as a restart
|
|
28425
|
+
// does: one that fails once its configuration is taken leaves an
|
|
28426
|
+
// ordinary server that's down, under that configuration; one refused
|
|
28427
|
+
// before (its transport, the awareness ledger) changes nothing, and the
|
|
28428
|
+
// closed tab is put back. Whether it was one is the kept configuration's
|
|
28429
|
+
// to say. An agent's open (reopen) ends it only once connected, so one
|
|
28430
|
+
// that fails leaves the closed tab to open again.
|
|
28431
|
+
const keptClosedTab = opts.reopen ? undefined : this.closedTabConfigs().get(config.id);
|
|
28432
|
+
const closedWas = keptClosedTab ? this.hostPresentation?.closedState(config.id) : undefined;
|
|
28433
|
+
if (keptClosedTab) {
|
|
28434
|
+
this.closedTabConfigs().delete(config.id);
|
|
28435
|
+
this.hostPresentation?.noteConnected(config.id, true);
|
|
28436
|
+
}
|
|
27756
28437
|
const oldToolNames = new Set(this.mcplTools.map(t => t.name));
|
|
27757
|
-
|
|
28438
|
+
let connection;
|
|
28439
|
+
try {
|
|
28440
|
+
connection = await this.connectMcplServerInternal(config, false, claim);
|
|
28441
|
+
}
|
|
28442
|
+
catch (error) {
|
|
28443
|
+
if (keptClosedTab && !this.mcplServerConfigs.has(config.id) && !this.closedTabConfigs().has(config.id)) {
|
|
28444
|
+
this.closedTabConfigs().set(config.id, keptClosedTab);
|
|
28445
|
+
this.hostPresentation?.restoreClosed(config.id, closedWas);
|
|
28446
|
+
}
|
|
28447
|
+
throw error;
|
|
28448
|
+
}
|
|
27758
28449
|
// A stop() that began during the connect's setup or its tool listing
|
|
27759
28450
|
// ends the connect: it doesn't wait on a server for a framework that is
|
|
27760
28451
|
// stopping, and the teardown closes what the connect set up.
|
|
@@ -27777,6 +28468,9 @@ export class AgentFramework {
|
|
|
27777
28468
|
if (this.mcpServerOps?.get(config.id)?.kind === 'disconnect' || !AgentFramework.mcpConnectionServes(connection)) {
|
|
27778
28469
|
throw new Error(`MCP server "${config.id}" was not connected: it was disconnected or lost while its tools were being listed`);
|
|
27779
28470
|
}
|
|
28471
|
+
// Connected, however it was asked for: not a closed tab any more.
|
|
28472
|
+
const wasClosedTab = this.closedTabConfigs().delete(config.id);
|
|
28473
|
+
this.hostPresentation?.noteConnected(config.id, wasClosedTab);
|
|
27780
28474
|
this.emitMcplToolDiff(oldToolNames, config.id);
|
|
27781
28475
|
}
|
|
27782
28476
|
/**
|
|
@@ -27803,7 +28497,17 @@ export class AgentFramework {
|
|
|
27803
28497
|
const claim = this.restartClaims().ids.get(id);
|
|
27804
28498
|
if (claim)
|
|
27805
28499
|
claim.endedBy = `MCP server "${id}" was disconnected, which ends the restart of "${claim.restarting}"`;
|
|
27806
|
-
|
|
28500
|
+
// A closed tab removed by its operator: there's nothing to connect it
|
|
28501
|
+
// again with, and what its disconnect kept for that connect (checkpoint
|
|
28502
|
+
// trees, RFC-014 state) goes, as any disconnect removes it.
|
|
28503
|
+
if (this.closedTabConfigs().delete(id) && !this.mcplServerConfigs.has(id)) {
|
|
28504
|
+
this.checkpointManager?.removeServer(id);
|
|
28505
|
+
this.toolsetHub?.forget(id);
|
|
28506
|
+
this.hostPresentation?.noteRemoved(id);
|
|
28507
|
+
return;
|
|
28508
|
+
}
|
|
28509
|
+
await this.trackOperatorOperation('mcpl:disconnect', () => this.runDisconnectMcplServer(id));
|
|
28510
|
+
this.hostPresentation?.noteRemoved(id);
|
|
27807
28511
|
}
|
|
27808
28512
|
async runDisconnectMcplServer(id, opts = {}) {
|
|
27809
28513
|
if (!this.mcplServerRegistry) {
|
|
@@ -27856,7 +28560,10 @@ export class AgentFramework {
|
|
|
27856
28560
|
// already transiently closed (reconnect pending) emits no second 'close'
|
|
27857
28561
|
// from close(), and the transient path deliberately preserves checkpoints.
|
|
27858
28562
|
this.featureSetManager?.removeServer(id);
|
|
27859
|
-
|
|
28563
|
+
// A closed tab (host-presentation.ts) keeps its checkpoint trees for
|
|
28564
|
+
// the connect its next open makes, as a transient close does.
|
|
28565
|
+
if (!opts.keepCheckpoints)
|
|
28566
|
+
this.checkpointManager?.removeServer(id);
|
|
27860
28567
|
if (!opts.keepToolsets)
|
|
27861
28568
|
this.toolsetHub?.forget(id);
|
|
27862
28569
|
// Only this server's own claims, as the prefix map holds them rather
|
|
@@ -27869,6 +28576,7 @@ export class AgentFramework {
|
|
|
27869
28576
|
}
|
|
27870
28577
|
this.mcplServerConfigs.delete(id);
|
|
27871
28578
|
referenceRegistry.forgetServerAddress(id, this);
|
|
28579
|
+
this.eventGate?.sourcesChanged();
|
|
27872
28580
|
await this.refreshMcplTools();
|
|
27873
28581
|
this.emitMcplToolDiff(oldToolNames, id);
|
|
27874
28582
|
}
|
|
@@ -27920,7 +28628,8 @@ export class AgentFramework {
|
|
|
27920
28628
|
// A stop(), or a disconnect of the id it moves the server to, that
|
|
27921
28629
|
// begins after these checks isn't held off: it ends the restart, as it
|
|
27922
28630
|
// would end a connect.
|
|
27923
|
-
|
|
28631
|
+
// A closed tab restarts from the configuration it's kept with.
|
|
28632
|
+
const config = newConfig ?? this.mcplServerConfigs.get(id) ?? this.closedTabConfigs().get(id);
|
|
27924
28633
|
if (!config) {
|
|
27925
28634
|
throw new Error(`MCPL server "${id}" is not configured`);
|
|
27926
28635
|
}
|
|
@@ -27962,7 +28671,17 @@ export class AgentFramework {
|
|
|
27962
28671
|
this.restartClaims().ids.set(config.id, claim);
|
|
27963
28672
|
this.restartClaims().prefixes.set(prefix, claim);
|
|
27964
28673
|
try {
|
|
27965
|
-
await this.runDisconnectMcplServer(id, { keepToolsets: config.id === id && config
|
|
28674
|
+
await this.runDisconnectMcplServer(id, { keepToolsets: config.id === id && this.hintsFor(config) });
|
|
28675
|
+
// A restart ends a closed tab as it begins: the operator connects the
|
|
28676
|
+
// server now, and one whose connect fails is left an ordinary server
|
|
28677
|
+
// that's down. Moved to another id, the old id's tab is no more.
|
|
28678
|
+
if (this.closedTabConfigs().has(id)) {
|
|
28679
|
+
if (config.id === id)
|
|
28680
|
+
this.hostPresentation?.noteConnected(id, true);
|
|
28681
|
+
else
|
|
28682
|
+
this.hostPresentation?.noteRemoved(id);
|
|
28683
|
+
this.closedTabConfigs().delete(id);
|
|
28684
|
+
}
|
|
27966
28685
|
try {
|
|
27967
28686
|
await this.runConnectMcplServer(config, claim);
|
|
27968
28687
|
}
|
|
@@ -28076,6 +28795,17 @@ export class AgentFramework {
|
|
|
28076
28795
|
return prefixesMeetError(config.id, prefix, otherId, other).message;
|
|
28077
28796
|
}
|
|
28078
28797
|
}
|
|
28798
|
+
// A closed tab keeps its prefix (and the prefixes it would meet) for the
|
|
28799
|
+
// connect its next open makes.
|
|
28800
|
+
for (const [otherId, other] of this.closedTabConfigs()) {
|
|
28801
|
+
if (otherId === config.id || otherId === leaving || this.mcplServerConfigs.has(otherId))
|
|
28802
|
+
continue;
|
|
28803
|
+
const otherPrefix = other.toolPrefix ?? `mcpl--${otherId}`;
|
|
28804
|
+
if (otherPrefix === prefix)
|
|
28805
|
+
return `MCPL server "${config.id}" toolPrefix "${prefix}" collides with server "${otherId}" (a closed tab)`;
|
|
28806
|
+
if (prefixesMeet(prefix, otherPrefix))
|
|
28807
|
+
return prefixesMeetError(config.id, prefix, otherId, otherPrefix).message;
|
|
28808
|
+
}
|
|
28079
28809
|
for (const [other, holder] of this.restartClaims().prefixes) {
|
|
28080
28810
|
if (holder !== claim && other !== prefix && prefixesMeet(prefix, other)) {
|
|
28081
28811
|
return `MCPL server "${config.id}" toolPrefix "${prefix}" meets the toolPrefix "${other}" the restart of ` +
|
|
@@ -28107,7 +28837,12 @@ export class AgentFramework {
|
|
|
28107
28837
|
*/
|
|
28108
28838
|
listMcplServers() {
|
|
28109
28839
|
const result = [];
|
|
28110
|
-
|
|
28840
|
+
// Closed tabs too: configured, kept disconnected until an agent opens one.
|
|
28841
|
+
const configured = [...this.mcplServerConfigs];
|
|
28842
|
+
for (const [id, config] of this.closedTabConfigs())
|
|
28843
|
+
if (!this.mcplServerConfigs.has(id))
|
|
28844
|
+
configured.push([id, config]);
|
|
28845
|
+
for (const [id, config] of configured) {
|
|
28111
28846
|
const prefix = config.toolPrefix ?? `mcpl--${id}`;
|
|
28112
28847
|
const modern = this.modernServers().get(id) ?? null;
|
|
28113
28848
|
const connection = modern ? null : this.mcplServerRegistry?.getServer(id) ?? null;
|
|
@@ -28131,6 +28866,7 @@ export class AgentFramework {
|
|
|
28131
28866
|
result.push({
|
|
28132
28867
|
id,
|
|
28133
28868
|
connected,
|
|
28869
|
+
closedTab: this.closedTabConfigs().has(id),
|
|
28134
28870
|
retrying: !connected && (modern?.willReconnect ?? connection?.willReconnect ?? false),
|
|
28135
28871
|
family: binding?.family ?? null,
|
|
28136
28872
|
protocolVersion: modern?.protocolVersion ?? connection?.protocolVersion ?? null,
|
|
@@ -29125,7 +29861,7 @@ export class AgentFramework {
|
|
|
29125
29861
|
moduleName: `mcpl:${serverId}:tools-refreshed`,
|
|
29126
29862
|
});
|
|
29127
29863
|
if (newTools.length > 0) {
|
|
29128
|
-
const toolList = newTools.map(t => t.name).join(', ');
|
|
29864
|
+
const toolList = newTools.map(t => suppliedValue(t.name)).join(', ');
|
|
29129
29865
|
this.pushEvent({
|
|
29130
29866
|
type: 'external-message',
|
|
29131
29867
|
source: `mcpl:${serverId}`,
|
|
@@ -29703,7 +30439,7 @@ export class AgentFramework {
|
|
|
29703
30439
|
/**
|
|
29704
30440
|
* Dispatch a synthesized channel tool call.
|
|
29705
30441
|
*/
|
|
29706
|
-
dispatchChannelToolCall(agentName, call) {
|
|
30442
|
+
dispatchChannelToolCall(agentName, call, earlierWake) {
|
|
29707
30443
|
// Conversation forks act only on their home channel. channel_publish and
|
|
29708
30444
|
// channel_close default a missing channelId to home and reject foreign
|
|
29709
30445
|
// ones; channel_open is rejected outright — opening channels mutates
|
|
@@ -29729,7 +30465,7 @@ export class AgentFramework {
|
|
|
29729
30465
|
};
|
|
29730
30466
|
// The home quoted for a sentence (quotedShown), as the agent names it
|
|
29731
30467
|
// back, whose id-and-server form carries its own quotes.
|
|
29732
|
-
const homeQuoted = this.channelRegistry?.quotedShown(home) ??
|
|
30468
|
+
const homeQuoted = this.channelRegistry?.quotedShown(home) ?? quotedValue(home.channelId);
|
|
29733
30469
|
// A home its server no longer registers (a channels/changed update
|
|
29734
30470
|
// removed it, or this host removed its server; a dropped connection
|
|
29735
30471
|
// keeps the entry) takes nothing, and the fork has nowhere else to
|
|
@@ -29811,7 +30547,9 @@ export class AgentFramework {
|
|
|
29811
30547
|
const publisher = call.name === 'channel_publish' && callingTurn ? this.agents.get(agentName) : undefined;
|
|
29812
30548
|
const readThrough = publisher ? this.publishReadThrough(publisher, call.id) : null;
|
|
29813
30549
|
const published = {};
|
|
29814
|
-
this.channelRegistry.handleChannelToolCall(call.name, call.input, {
|
|
30550
|
+
this.channelRegistry.handleChannelToolCall(call.name, call.input, {
|
|
30551
|
+
kind: 'agent', agentName, own: callingTurn ? this.ownChannels(callingTurn) : [], ...(earlierWake ? { earlierWake } : {}),
|
|
30552
|
+
}, published)
|
|
29815
30553
|
.then((result) => {
|
|
29816
30554
|
const destination = published.outcome?.status === 'delivered' ? published.outcome.destination : undefined;
|
|
29817
30555
|
if (publisher && destination)
|
|
@@ -29861,13 +30599,15 @@ export class AgentFramework {
|
|
|
29861
30599
|
noteTurnChannel(callingTurn, opened);
|
|
29862
30600
|
this.lastAnnouncedLocus.set(agentName, opened);
|
|
29863
30601
|
const inboundOnlyServer = this.channelRegistry?.inboundOnlyServer(opened);
|
|
30602
|
+
// The routing word comes first, ahead of any backscroll, so a
|
|
30603
|
+
// result cut to its head for being long keeps it.
|
|
29864
30604
|
result = {
|
|
29865
30605
|
...result,
|
|
29866
30606
|
data: {
|
|
29867
|
-
...result.data,
|
|
29868
30607
|
routing: inboundOnlyServer
|
|
29869
30608
|
? `${inboundOnlySpeechNote(inboundOnlyServer)} Other channels need an explicit send tool.`
|
|
29870
30609
|
: 'Your plain speech now lands in this channel. Other channels need an explicit send tool.',
|
|
30610
|
+
...result.data,
|
|
29871
30611
|
},
|
|
29872
30612
|
};
|
|
29873
30613
|
console.error(`[routing] ${agentName}: channel_open -> pin moved to ${qualifiedChannelName(opened)} (announced in tool result)`);
|
|
@@ -30202,7 +30942,7 @@ export class AgentFramework {
|
|
|
30202
30942
|
// answered for it.
|
|
30203
30943
|
const now = this.channelRegistry.getTuneOutState(channel.serverId, channelId);
|
|
30204
30944
|
const error = now?.params.epochId === tuneOut.params.epochId
|
|
30205
|
-
? alreadyTunedOutText(
|
|
30945
|
+
? alreadyTunedOutText(this.channelRegistry.quotedShown(channel), now.params, openError ?? 'Its transport is open now, so its messages reach the subconscious.')
|
|
30206
30946
|
: TUNE_OUT_REPLACED;
|
|
30207
30947
|
this.pushEvent({
|
|
30208
30948
|
type: 'tool-result',
|
|
@@ -30341,6 +31081,9 @@ export class AgentFramework {
|
|
|
30341
31081
|
console.error(`[sleep] ${agentName}: no prose target this turn — sleep announcement not posted (explicit mode never guesses)`);
|
|
30342
31082
|
}
|
|
30343
31083
|
}
|
|
31084
|
+
else if (!callingTurn.locus && callingTurn.heartbeat) {
|
|
31085
|
+
console.error(`[sleep] ${agentName}: a heartbeat turn has no channel — sleep announcement not posted`);
|
|
31086
|
+
}
|
|
30344
31087
|
else {
|
|
30345
31088
|
const locus = callingTurn.locus ?? null;
|
|
30346
31089
|
registry.routeSpeech(agentName, announcement(locus), locus).then(noted).catch((err) => {
|
|
@@ -30523,7 +31266,7 @@ export class AgentFramework {
|
|
|
30523
31266
|
return;
|
|
30524
31267
|
if (Date.now() - startedAt > BARRIER_MS) {
|
|
30525
31268
|
throw new Error(`save_recent_image: ${open.length} sibling tool call(s) in this batch still pending after ` +
|
|
30526
|
-
`${Math.round(BARRIER_MS / 1000)}s (${open.map((p) => `${p.name} (call ${p.id})`).join(', ')}) — their results ` +
|
|
31269
|
+
`${Math.round(BARRIER_MS / 1000)}s (${open.map((p) => `${suppliedValue(p.name)} (call ${p.id})`).join(', ')}) — their results ` +
|
|
30527
31270
|
'may carry newer images than anything in your context, so no index can be trusted yet. Nothing written. ' +
|
|
30528
31271
|
'Save again once they have finished.');
|
|
30529
31272
|
}
|
|
@@ -30570,7 +31313,7 @@ export class AgentFramework {
|
|
|
30570
31313
|
throw new Error('save_recent_image: `path` (mount-prefixed) is required');
|
|
30571
31314
|
}
|
|
30572
31315
|
const ledger = this.toolImageLedgerFor(agentName);
|
|
30573
|
-
const describeTool = (c) => `${c.toolName} (call ${c.toolCallId})`;
|
|
31316
|
+
const describeTool = (c) => `${suppliedValue(c.toolName)} (call ${c.toolCallId})`;
|
|
30574
31317
|
const unavailable = (status) => status === 'evicted'
|
|
30575
31318
|
? 'its bytes were evicted from the bounded retention budget'
|
|
30576
31319
|
: 'this process has no record of that ref (refs are per-process and a restart clears them; ' +
|
|
@@ -30773,9 +31516,10 @@ export class AgentFramework {
|
|
|
30773
31516
|
const content = messages[i]?.content;
|
|
30774
31517
|
if (!Array.isArray(content))
|
|
30775
31518
|
continue;
|
|
30776
|
-
//
|
|
30777
|
-
//
|
|
30778
|
-
|
|
31519
|
+
// A row the agent's own view leaves out, such as another agent's
|
|
31520
|
+
// stored context injection (stored-injections.ts), isn't in its
|
|
31521
|
+
// window, so its images aren't either.
|
|
31522
|
+
if (!this.agentSees(agent, messages[i]?.metadata))
|
|
30779
31523
|
continue;
|
|
30780
31524
|
const messagesBack = total - (start + i);
|
|
30781
31525
|
for (let b = content.length - 1; b >= 0 && collecting(); b--) {
|
|
@@ -30847,7 +31591,8 @@ export class AgentFramework {
|
|
|
30847
31591
|
// error says that whether the entry changed isn't known
|
|
30848
31592
|
// (writeBinary); the images before it are saved, and the
|
|
30849
31593
|
// failure says so, as a returned one does.
|
|
30850
|
-
failed
|
|
31594
|
+
console.error(`[save_recent_image] writing "${destination}" failed:`, error);
|
|
31595
|
+
failed(thrownWords(error));
|
|
30851
31596
|
return;
|
|
30852
31597
|
}
|
|
30853
31598
|
const inChronicle = result.data?.inChronicle === true;
|
|
@@ -30884,117 +31629,6 @@ export class AgentFramework {
|
|
|
30884
31629
|
}
|
|
30885
31630
|
})();
|
|
30886
31631
|
}
|
|
30887
|
-
/** The `utils` meta-tool: list/describe/run over module utilities
|
|
30888
|
-
* (Module.getUtilities). Shared by the model-facing dispatcher, which a
|
|
30889
|
-
* running code_execution script's calls take too, and executeToolCallFrom,
|
|
30890
|
-
* which the puppet and a host reach through executeToolCall and a module
|
|
30891
|
-
* through callTool. */
|
|
30892
|
-
async handleUtilsToolCall(agentName, call, options = {}) {
|
|
30893
|
-
const input = (call.input ?? {});
|
|
30894
|
-
const allUtilities = this.moduleRegistry.getAllUtilities();
|
|
30895
|
-
const utilities = allUtilities.filter(def => options.trustedModule || this.canAgentUseTool(agentName, def.name));
|
|
30896
|
-
const counts = `${registeredUtilities(allUtilities.length)}, ${utilities.length} permitted`;
|
|
30897
|
-
const available = () => utilities.length
|
|
30898
|
-
? `Available: ${utilities.map((u) => u.name).join(', ')} (${counts})`
|
|
30899
|
-
: allUtilities.length
|
|
30900
|
-
? `${counts}; none permitted by this agent's allowedTools. Add each required module--utility name to allowedTools.`
|
|
30901
|
-
: `${counts} (none registered)`;
|
|
30902
|
-
switch (input.action) {
|
|
30903
|
-
case 'list':
|
|
30904
|
-
if (allUtilities.length > 0 && utilities.length === 0) {
|
|
30905
|
-
return { success: false, isError: true, error: available() };
|
|
30906
|
-
}
|
|
30907
|
-
return {
|
|
30908
|
-
success: true,
|
|
30909
|
-
data: utilities.map((u) => ({ name: u.name, description: utilityOneLiner(u.description) })),
|
|
30910
|
-
};
|
|
30911
|
-
case 'describe': {
|
|
30912
|
-
const def = allUtilities.find((u) => u.name === input.name);
|
|
30913
|
-
if (!def) {
|
|
30914
|
-
return { success: false, isError: true, error: `No utility "${String(input.name)}". ${available()}` };
|
|
30915
|
-
}
|
|
30916
|
-
if (!options.trustedModule) {
|
|
30917
|
-
const refusal = this.agentToolPermissionFailure(agentName, { ...call, name: def.name }, false);
|
|
30918
|
-
if (refusal)
|
|
30919
|
-
return refusal;
|
|
30920
|
-
}
|
|
30921
|
-
return { success: true, data: def };
|
|
30922
|
-
}
|
|
30923
|
-
case 'run': {
|
|
30924
|
-
const def = allUtilities.find((u) => u.name === input.name);
|
|
30925
|
-
if (!def) {
|
|
30926
|
-
return { success: false, isError: true, error: `No utility "${String(input.name)}", so nothing was done. ${available()}` };
|
|
30927
|
-
}
|
|
30928
|
-
if (!options.trustedModule) {
|
|
30929
|
-
// The allowed outer utils call owns its lifecycle and terminal trace;
|
|
30930
|
-
// refusing the utility does not create or refuse a separate call.
|
|
30931
|
-
const refusal = this.agentToolPermissionFailure(agentName, { ...call, name: def.name }, false);
|
|
30932
|
-
if (refusal)
|
|
30933
|
-
return refusal;
|
|
30934
|
-
}
|
|
30935
|
-
const args = input.args ?? {};
|
|
30936
|
-
const problems = toolArgsProblems(args, def.inputSchema, { values: true, field: 'args' });
|
|
30937
|
-
if (problems.length > 0) {
|
|
30938
|
-
// Teach by bounce: the error carries the schema, so the retry needs
|
|
30939
|
-
// no describe round-trip.
|
|
30940
|
-
return {
|
|
30941
|
-
success: false,
|
|
30942
|
-
isError: true,
|
|
30943
|
-
error: `Nothing was done: ${def.name} ${problems.join('; ')}. Schema for ${def.name}: ${JSON.stringify(def.inputSchema)}`,
|
|
30944
|
-
};
|
|
30945
|
-
}
|
|
30946
|
-
// Same dispatch as a first-class tool call — the module cannot tell
|
|
30947
|
-
// which surface the call came through. The registry checks the call
|
|
30948
|
-
// against the utility resolved here, even where one of the module's
|
|
30949
|
-
// tools shares its name. An in-process caller gets the module's own
|
|
30950
|
-
// result; the model's path takes the result's stored form.
|
|
30951
|
-
//
|
|
30952
|
-
// The result goes where the utils call's does. On the model's path it
|
|
30953
|
-
// is stored or handed to a script as that call's result, so it may be
|
|
30954
|
-
// as long as that call's may (resultMaxCharsFor). An in-process caller
|
|
30955
|
-
// gets it back, and the utils call carries what that caller may take,
|
|
30956
|
-
// if anything does: the puppet's carries the agent's cap, since the
|
|
30957
|
-
// puppet stores the pair under it.
|
|
30958
|
-
const resultMaxChars = options.inProcess ? call.resultMaxChars : this.resultMaxCharsFor(agentName, call.id);
|
|
30959
|
-
const utilityCall = {
|
|
30960
|
-
id: call.id, name: def.name, input: args, callerAgentName: agentName,
|
|
30961
|
-
...(call.callerTurnId !== undefined ? { callerTurnId: call.callerTurnId } : {}),
|
|
30962
|
-
...(resultMaxChars !== undefined ? { resultMaxChars } : {}),
|
|
30963
|
-
};
|
|
30964
|
-
return options.inProcess
|
|
30965
|
-
? this.moduleRegistry.handleToolCall(utilityCall, { definition: def })
|
|
30966
|
-
: this.moduleRegistry.storableToolCall(utilityCall, { definition: def });
|
|
30967
|
-
}
|
|
30968
|
-
default:
|
|
30969
|
-
return { success: false, isError: true, error: 'action must be "list", "describe", or "run"' };
|
|
30970
|
-
}
|
|
30971
|
-
}
|
|
30972
|
-
dispatchUtilsToolCall(agentName, call) {
|
|
30973
|
-
this.emitTrace({ type: 'tool:started', module: 'framework', tool: 'utils', callId: call.id, input: call.input });
|
|
30974
|
-
const startedAt = Date.now();
|
|
30975
|
-
void this.handleUtilsToolCall(agentName, call)
|
|
30976
|
-
.catch((err) => ({
|
|
30977
|
-
success: false,
|
|
30978
|
-
error: err instanceof Error ? err.message : String(err),
|
|
30979
|
-
isError: true,
|
|
30980
|
-
}))
|
|
30981
|
-
.then((result) => {
|
|
30982
|
-
if (result.success) {
|
|
30983
|
-
this.emitTrace({ type: 'tool:completed', module: 'framework', tool: 'utils', callId: call.id, durationMs: Date.now() - startedAt });
|
|
30984
|
-
}
|
|
30985
|
-
else {
|
|
30986
|
-
this.emitTrace({ type: 'tool:failed', module: 'framework', tool: 'utils', callId: call.id, error: result.error ?? 'unknown error' });
|
|
30987
|
-
}
|
|
30988
|
-
this.pushEvent({
|
|
30989
|
-
type: 'tool-result',
|
|
30990
|
-
callId: call.id,
|
|
30991
|
-
toolName: call.name,
|
|
30992
|
-
agentName,
|
|
30993
|
-
moduleName: 'framework',
|
|
30994
|
-
result,
|
|
30995
|
-
});
|
|
30996
|
-
});
|
|
30997
|
-
}
|
|
30998
31632
|
readAgentSettings(agentName, touched) {
|
|
30999
31633
|
const s = this.getAgentRuntimeSettings(agentName);
|
|
31000
31634
|
const saved = this.agents.get(agentName)?.getRuntimeSettingsOverrides() ?? {};
|
|
@@ -31326,4 +31960,11 @@ export class AgentFramework {
|
|
|
31326
31960
|
};
|
|
31327
31961
|
}
|
|
31328
31962
|
}
|
|
31963
|
+
function thrownWords(error) {
|
|
31964
|
+
if (error instanceof Refusal)
|
|
31965
|
+
return error.message;
|
|
31966
|
+
const code = errnoCode(error);
|
|
31967
|
+
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.");
|
|
31968
|
+
return `${systemWords(code, error).replace(/\.$/, '')}. ${on}`;
|
|
31969
|
+
}
|
|
31329
31970
|
//# sourceMappingURL=framework.js.map
|