@animalabs/agent-framework 0.28.0 → 0.30.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/admission/admit.d.ts +90 -0
- package/dist/src/admission/admit.d.ts.map +1 -0
- package/dist/src/admission/admit.js +43 -0
- package/dist/src/admission/admit.js.map +1 -0
- package/dist/src/agent.d.ts +21 -0
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +47 -7
- 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/author-presentation.d.ts +50 -0
- package/dist/src/author-presentation.d.ts.map +1 -0
- package/dist/src/author-presentation.js +58 -0
- package/dist/src/author-presentation.js.map +1 -0
- 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/dendrite/index.d.ts +6 -0
- package/dist/src/dendrite/index.d.ts.map +1 -0
- package/dist/src/dendrite/index.js +3 -0
- package/dist/src/dendrite/index.js.map +1 -0
- package/dist/src/dendrite/presets.d.ts +60 -0
- package/dist/src/dendrite/presets.d.ts.map +1 -0
- package/dist/src/dendrite/presets.js +117 -0
- package/dist/src/dendrite/presets.js.map +1 -0
- package/dist/src/dendrite/registry.d.ts +260 -0
- package/dist/src/dendrite/registry.d.ts.map +1 -0
- package/dist/src/dendrite/registry.js +742 -0
- package/dist/src/dendrite/registry.js.map +1 -0
- package/dist/src/dendrite/types.d.ts +392 -0
- package/dist/src/dendrite/types.d.ts.map +1 -0
- package/dist/src/dendrite/types.js +24 -0
- package/dist/src/dendrite/types.js.map +1 -0
- package/dist/src/framework.d.ts +846 -67
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +4213 -924
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +46 -14
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +143 -53
- 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 +174 -0
- package/dist/src/host-presentation.d.ts.map +1 -0
- package/dist/src/host-presentation.js +565 -0
- package/dist/src/host-presentation.js.map +1 -0
- package/dist/src/index.d.ts +7 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -2
- 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 +118 -15
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +266 -110
- 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-coalescer.d.ts +9 -0
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +21 -0
- package/dist/src/mcpl/push-coalescer.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 +187 -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 +81 -0
- package/dist/src/notice-tags.d.ts.map +1 -0
- package/dist/src/notice-tags.js +85 -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 +5 -55
- package/dist/src/silent-heartbeat.d.ts.map +1 -1
- package/dist/src/silent-heartbeat.js +63 -7
- package/dist/src/silent-heartbeat.js.map +1 -1
- package/dist/src/stored-injections.d.ts +4 -0
- package/dist/src/stored-injections.d.ts.map +1 -1
- package/dist/src/stored-injections.js +6 -0
- package/dist/src/stored-injections.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 +382 -11
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +1100 -119
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts +101 -5
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/tune-out/tools.js +55 -18
- package/dist/src/tune-out/tools.js.map +1 -1
- package/dist/src/types/agent.d.ts +48 -9
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +142 -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/dist/src/types/trace.d.ts +136 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/admission/admit.ts +113 -0
- package/src/agent.ts +52 -7
- package/src/api/mcp-server-core.ts +3 -14
- package/src/api/server.ts +7 -3
- package/src/author-presentation.ts +81 -0
- 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/dendrite/index.ts +32 -0
- package/src/dendrite/presets.ts +149 -0
- package/src/dendrite/registry.ts +808 -0
- package/src/dendrite/types.ts +397 -0
- package/src/framework.ts +4362 -852
- package/src/gate/event-gate.ts +159 -52
- 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 +612 -0
- package/src/index.ts +17 -4
- package/src/line-reader.ts +58 -0
- package/src/mcpl/channel-registry.ts +306 -111
- 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-coalescer.ts +19 -0
- 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 +149 -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 +86 -0
- package/src/reasoning-loss-notice.ts +5 -3
- package/src/silent-heartbeat.ts +11 -7
- package/src/stored-injections.ts +7 -0
- 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 +1252 -123
- package/src/tune-out/tools.ts +152 -11
- package/src/types/agent.ts +51 -9
- package/src/types/framework.ts +146 -0
- package/src/types/module.ts +23 -11
- package/src/types/trace.ts +150 -0
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
* - Build channel context for beforeInference params
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { NOTICE_TAG } from '../notice-tags.js';
|
|
16
17
|
import type { ContentBlock } from '@animalabs/membrane';
|
|
17
|
-
import {
|
|
18
|
+
import { convertBlocks } from './convert-block.js';
|
|
19
|
+
import { cutToFit, markedText, markNoticeOpenings, nameToTypeBack, quotedValue, showsAsItself, suppliedValue } from '../supplied-text.js';
|
|
18
20
|
import type { JsStore } from '@animalabs/chronicle';
|
|
19
21
|
import { isDeepStrictEqual } from 'node:util';
|
|
20
22
|
import { ensureStateRegistered } from '../module-registry.js';
|
|
@@ -45,7 +47,7 @@ import { RefusalLines } from './refusal-lines.js';
|
|
|
45
47
|
import { CapabilityGrant } from './capability-grant.js';
|
|
46
48
|
import type { InboundSource } from './inbound-source.js';
|
|
47
49
|
import { McplRequestError, type McplServerConnection } from './server-connection.js';
|
|
48
|
-
import { readSkipReplyWake } from './skip-reply-wake.js';
|
|
50
|
+
import { readSkipReplyWake, skipReplyNote, type SkipReplyEarlierWake } from './skip-reply-wake.js';
|
|
49
51
|
import { isProseTargetToken } from './prose-grammar.js';
|
|
50
52
|
|
|
51
53
|
// ============================================================================
|
|
@@ -564,11 +566,11 @@ const CHANNEL_TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
564
566
|
},
|
|
565
567
|
{
|
|
566
568
|
name: 'channel_publish',
|
|
567
|
-
description: "Publish a message to a channel. If channelId is omitted, publishes
|
|
569
|
+
description: "Publish a message to a channel. If channelId is omitted, publishes where your plain speech goes this turn: your conversation's channel if you are bound to one; otherwise the channel of the message that woke this turn, or one you opened during it; otherwise, while a focus holds, the channel it keeps. Where none of those names a channel, nothing is posted, unless this host is set to use the channel of the latest incoming message instead. A turn with no channel of its own, such as a heartbeat's, has no default, so it names one. A subagent has no default, so it must name a channel.",
|
|
568
570
|
inputSchema: {
|
|
569
571
|
type: 'object' as const,
|
|
570
572
|
properties: {
|
|
571
|
-
channelId: { type: 'string', description: "ID of the channel to publish to (defaults to your
|
|
573
|
+
channelId: { type: 'string', description: "ID of the channel to publish to (defaults to where your plain speech goes this turn, as the tool's description says; required of a subagent)" },
|
|
572
574
|
content: { type: 'string', description: 'Text content to publish' },
|
|
573
575
|
text: { type: 'string', description: 'Alias for content' },
|
|
574
576
|
},
|
|
@@ -643,19 +645,21 @@ const CHANNEL_TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
643
645
|
// Constructor Options
|
|
644
646
|
// ============================================================================
|
|
645
647
|
|
|
646
|
-
/** Bound connector values for notices, logs and an outcome's reason
|
|
647
|
-
*
|
|
648
|
-
*
|
|
648
|
+
/** Bound connector values for notices, logs and an outcome's reason, on one
|
|
649
|
+
* line with their notice openings marked (suppliedValue). A PublishOutcome
|
|
650
|
+
* keeps the connector's structured data (`detail`) as the connection gives
|
|
651
|
+
* it, its strings redacted (redactedData). */
|
|
649
652
|
export function connectorText(value: unknown): string {
|
|
650
653
|
let text: string;
|
|
651
654
|
try {
|
|
652
655
|
text = typeof value === 'string' ? value : JSON.stringify(value) ?? String(value);
|
|
653
656
|
} catch {
|
|
654
|
-
return
|
|
657
|
+
return `[${NOTICE_TAG.unserializableConnectorValue}]`;
|
|
655
658
|
}
|
|
656
659
|
const limit = 2000;
|
|
657
|
-
const suffix =
|
|
658
|
-
|
|
660
|
+
const suffix = `… [${NOTICE_TAG.truncated}]`;
|
|
661
|
+
const shown = suppliedValue(text);
|
|
662
|
+
return shown.length > limit ? cutToFit(shown, limit - suffix.length) + suffix : shown;
|
|
659
663
|
}
|
|
660
664
|
|
|
661
665
|
/**
|
|
@@ -688,6 +692,9 @@ export function sameChannel(a: ChannelRef | null | undefined, b: ChannelRef | nu
|
|
|
688
692
|
return a.serverId === b.serverId && a.channelId === b.channelId;
|
|
689
693
|
}
|
|
690
694
|
|
|
695
|
+
export const NO_CHANNEL_EXAMPLE = '#<channel>';
|
|
696
|
+
export const NO_PERSON_EXAMPLE = '@<person>';
|
|
697
|
+
|
|
691
698
|
/**
|
|
692
699
|
* A channel's server-qualified name, `serverId:channelId`. A name given as a
|
|
693
700
|
* string (a tool's channelId, a `>>` target, a host's call) may take this
|
|
@@ -699,12 +706,32 @@ export function qualifiedChannelName(ref: ChannelRef): string {
|
|
|
699
706
|
return `${ref.serverId}:${ref.channelId}`;
|
|
700
707
|
}
|
|
701
708
|
|
|
709
|
+
function markedHistory(history: readonly unknown[]): unknown[] {
|
|
710
|
+
return history.map((item) => {
|
|
711
|
+
if (item === null || typeof item !== 'object' || Array.isArray(item) || !('content' in item)) return item;
|
|
712
|
+
const { content } = item as { content: unknown };
|
|
713
|
+
if (Array.isArray(content)) return { ...item, content: markNoticeOpenings(content.map(withEmbeddedTextMarked) as object[]) };
|
|
714
|
+
return typeof content === 'string' ? { ...item, content: markedText(content) } : item;
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function withEmbeddedTextMarked(block: unknown): unknown {
|
|
719
|
+
if (block === null || typeof block !== 'object' || (block as { type?: unknown }).type !== 'resource') return block;
|
|
720
|
+
const { resource } = block as { resource?: unknown };
|
|
721
|
+
if (resource === null || typeof resource !== 'object' || typeof (resource as { text?: unknown }).text !== 'string') return block;
|
|
722
|
+
return { ...block, resource: { ...resource, text: markedText((resource as { text: string }).text) } };
|
|
723
|
+
}
|
|
724
|
+
|
|
702
725
|
/** A channel told apart by its id and its server, for one no string picks
|
|
703
|
-
* alone: `"b:room" on server "a"
|
|
726
|
+
* alone or whose name wouldn't show as itself: `"b:room" on server "a"`,
|
|
727
|
+
* each a JSON literal a channel tool's channelId and serverId take back
|
|
728
|
+
* (quotedValue). No input takes the whole form for a name. */
|
|
704
729
|
function channelOnServer(ref: ChannelRef): string {
|
|
705
|
-
return `${
|
|
730
|
+
return `${quotedValue(ref.channelId)} on server ${quotedValue(ref.serverId)}`;
|
|
706
731
|
}
|
|
707
732
|
|
|
733
|
+
const CHANNEL_ON_SERVER_AT_END = /("(?:[^"\\]|\\.)*") on server ("(?:[^"\\]|\\.)*")$/;
|
|
734
|
+
|
|
708
735
|
/**
|
|
709
736
|
* What one channels/publish attempt established (ChannelRegistry.publish).
|
|
710
737
|
* - `delivered`: the connector returned `delivered: true`. A historical
|
|
@@ -965,8 +992,35 @@ interface ChannelRegistryOptions {
|
|
|
965
992
|
) => Promise<ChannelIncomingMessageResult & { firstView?: true }>;
|
|
966
993
|
/** Chronicle store used for durable desired channel lifecycle state. */
|
|
967
994
|
store?: JsStore;
|
|
968
|
-
/**
|
|
995
|
+
/**
|
|
996
|
+
* The server-level veto on waking for an incoming message (a per-server
|
|
997
|
+
* `McplServerConfig.shouldTriggerInference`). The wake gate is NOT wired
|
|
998
|
+
* here: it runs in the framework's admission stage, after the hold
|
|
999
|
+
* decision, so a held message never forms a gate batch (issue #232 gap 3).
|
|
1000
|
+
*/
|
|
969
1001
|
shouldTriggerInference?: (content: string, metadata: Record<string, unknown>) => boolean;
|
|
1002
|
+
/**
|
|
1003
|
+
* Whether the most recent delivered inbound message is a fallback speech
|
|
1004
|
+
* locus for turns that have none of their own (`speech.locus:
|
|
1005
|
+
* 'latest-inbound'`). Off by default: incoming traffic never moves the
|
|
1006
|
+
* locus (architecture channel, 2026-10-06).
|
|
1007
|
+
*/
|
|
1008
|
+
latestInboundLocus?: boolean;
|
|
1009
|
+
/**
|
|
1010
|
+
* A channel is being closed by tool: an all-except hold whose exception
|
|
1011
|
+
* this is ends through the cancel flow (release the backlog, record the
|
|
1012
|
+
* end) before the lifecycle state flips to closed. A
|
|
1013
|
+
* tuned-out channel's close is refused before this (its cancel ends it).
|
|
1014
|
+
* Returns what it ended, in a sentence the close's answer adds when the
|
|
1015
|
+
* close itself isn't recorded.
|
|
1016
|
+
*/
|
|
1017
|
+
onToolClose?: (serverId: string, channelId: string) => string | undefined;
|
|
1018
|
+
/**
|
|
1019
|
+
* The channel unprefixed speech lands in while an all-except hold is
|
|
1020
|
+
* active (its exception), else null. Consulted after a fork's home and
|
|
1021
|
+
* the turn's own delivery, before the legacy fallback.
|
|
1022
|
+
*/
|
|
1023
|
+
focusChannelResolver?: () => ChannelRef | null;
|
|
970
1024
|
/**
|
|
971
1025
|
* Called when a text-only turn's speech was not delivered to its
|
|
972
1026
|
* conversational locus (`failed`), or may not have been (`unknown`), for
|
|
@@ -1016,7 +1070,7 @@ interface ChannelRegistryOptions {
|
|
|
1016
1070
|
/**
|
|
1017
1071
|
* Resolve a conversation fork's HOME channel from its agent name. Conversation
|
|
1018
1072
|
* forks are spawned bound to a single channel (the framework tracks this in
|
|
1019
|
-
* `
|
|
1073
|
+
* the agent registry's `homeChannel` / `ConversationRouter.channelForAgent`); their
|
|
1020
1074
|
* plain-text speech must route THERE. The process-global `defaultPublishChannel`
|
|
1021
1075
|
* tracks only the most recent inbound message across ALL channels (reaction,
|
|
1022
1076
|
* reaction-removal, edit and deletion markers aside, apart from an edit the
|
|
@@ -1089,6 +1143,14 @@ export type ChannelToolOrigin =
|
|
|
1089
1143
|
* name one of them answers to by its id means that channel
|
|
1090
1144
|
* (ChannelRegistry.ownChannelNamed). */
|
|
1091
1145
|
own?: readonly ChannelRef[];
|
|
1146
|
+
/** For a skip_reply whose self-wake the framework armed: the heartbeat's
|
|
1147
|
+
* wake due before it, when there is one, which its confirmation names
|
|
1148
|
+
* (skipReplyNote). */
|
|
1149
|
+
earlierWake?: SkipReplyEarlierWake;
|
|
1150
|
+
/** Made inside one of the agent's turns (the framework's callingTurnOf):
|
|
1151
|
+
* a channel_publish naming no channel arrives with that turn's default
|
|
1152
|
+
* already applied, so it takes no other. */
|
|
1153
|
+
inTurn?: boolean;
|
|
1092
1154
|
};
|
|
1093
1155
|
|
|
1094
1156
|
/** The machine decision sources a module-origin channel_close may record.
|
|
@@ -1123,6 +1185,9 @@ export class ChannelRegistry {
|
|
|
1123
1185
|
op?: 'start' | 'stop',
|
|
1124
1186
|
) => void;
|
|
1125
1187
|
private shouldTriggerInference?: (content: string, metadata: Record<string, unknown>) => boolean;
|
|
1188
|
+
private readonly latestInboundLocus: boolean;
|
|
1189
|
+
private onToolClose?: (serverId: string, channelId: string) => string | undefined;
|
|
1190
|
+
private focusChannelResolver?: () => ChannelRef | null;
|
|
1126
1191
|
private onRouteFailure?: ChannelRegistryOptions['onRouteFailure'];
|
|
1127
1192
|
private onChannelAutoOpened?: (info: {
|
|
1128
1193
|
conversationId?: string;
|
|
@@ -1551,6 +1616,9 @@ export class ChannelRegistry {
|
|
|
1551
1616
|
this.emitTraceFn = emitTraceFn;
|
|
1552
1617
|
this.sendTypingFn = options?.sendTypingFn;
|
|
1553
1618
|
this.shouldTriggerInference = options?.shouldTriggerInference;
|
|
1619
|
+
this.latestInboundLocus = options?.latestInboundLocus === true;
|
|
1620
|
+
this.onToolClose = options?.onToolClose;
|
|
1621
|
+
this.focusChannelResolver = options?.focusChannelResolver;
|
|
1554
1622
|
this.onRouteFailure = options?.onRouteFailure;
|
|
1555
1623
|
this.onChannelAutoOpened = options?.onChannelAutoOpened;
|
|
1556
1624
|
this.homeChannelResolver = options?.homeChannelResolver;
|
|
@@ -1792,9 +1860,6 @@ export class ChannelRegistry {
|
|
|
1792
1860
|
// The batch came on the transport open now: a close after this point
|
|
1793
1861
|
// (transportClosed) leaves its traffic nothing to confirm.
|
|
1794
1862
|
const closesAtArrival = this.transportCloses.get(serverId) ?? 0;
|
|
1795
|
-
// And came from a server registered now: one removed since (removeServer)
|
|
1796
|
-
// gives no message the agent replies to (markAccepted).
|
|
1797
|
-
const removalsAtArrival = this.serverRemovals.get(serverId) ?? 0;
|
|
1798
1863
|
|
|
1799
1864
|
for (const message of params.messages) {
|
|
1800
1865
|
if (!message || typeof message.channelId !== 'string' || !message.channelId
|
|
@@ -1894,34 +1959,25 @@ export class ChannelRegistry {
|
|
|
1894
1959
|
}
|
|
1895
1960
|
}
|
|
1896
1961
|
if (message.tags) message.tags = expandCoreTags(message.tags);
|
|
1897
|
-
const convertedContent: ContentBlock[] = message.content
|
|
1962
|
+
const convertedContent: ContentBlock[] = convertBlocks(message.content, 'attachment on channel message', serverId);
|
|
1898
1963
|
if (!emptyAllowed && isVisiblyEmptyConversion(message.content, convertedContent)) {
|
|
1899
1964
|
rejectEmpty();
|
|
1900
1965
|
continue;
|
|
1901
1966
|
}
|
|
1902
1967
|
|
|
1903
|
-
//
|
|
1904
|
-
//
|
|
1905
|
-
//
|
|
1906
|
-
//
|
|
1907
|
-
//
|
|
1908
|
-
//
|
|
1968
|
+
// A server sending channels/incoming is evidence that the transport
|
|
1969
|
+
// is open. This repairs transient status only; durable desired state
|
|
1970
|
+
// still changes exclusively through lifecycle operations. Nothing else
|
|
1971
|
+
// is decided here: whether this message is delivered, wakes anyone or
|
|
1972
|
+
// counts as the latest inbound is the framework's admission decision,
|
|
1973
|
+
// taken once the hold is known (see `noteDelivered`; issue #232 root
|
|
1974
|
+
// cause A). A coalesced item is "accepted" only once the coalescer
|
|
1975
|
+
// admits it, so for those this runs after the hook (below).
|
|
1909
1976
|
const incomingEntry = this.channels.get(incomingKey)!;
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
// must not retarget the default publish locus or become
|
|
1915
|
-
// `context.incoming`'s reply anchor. An edit the coalescer delivered
|
|
1916
|
-
// as the agent's first view counts as the message it replaced.
|
|
1917
|
-
// A message the coalescer admits after its server was removed sets no
|
|
1918
|
-
// default: the removal cleared that server's message, and a server
|
|
1919
|
-
// that's gone mustn't displace a live one's default.
|
|
1920
|
-
if (!isMarkerTagged(message.tags, firstView) && (this.serverRemovals.get(serverId) ?? 0) === removalsAtArrival) {
|
|
1921
|
-
this.defaultPublishChannel = { serverId, channelId: message.channelId };
|
|
1922
|
-
this.defaultPublishMessageId = message.messageId;
|
|
1923
|
-
this.defaultPublishThreadId = message.threadId;
|
|
1924
|
-
}
|
|
1977
|
+
// `firstView` is the coalescer's mark that an edit was the agent's first
|
|
1978
|
+
// view of its message; the registry no longer records the publish locus
|
|
1979
|
+
// here (admission does, through noteDelivered, with the same marker rule).
|
|
1980
|
+
const markAccepted = (_firstView = false) => {
|
|
1925
1981
|
// Where the host directs this server's lifecycle, traffic shows the
|
|
1926
1982
|
// transport open only for a channel still decided open or tuned out,
|
|
1927
1983
|
// and, but for one exception, only for a confirmed target: after a
|
|
@@ -1956,7 +2012,7 @@ export class ChannelRegistry {
|
|
|
1956
2012
|
};
|
|
1957
2013
|
if (!coalesced) markAccepted();
|
|
1958
2014
|
|
|
1959
|
-
//
|
|
2015
|
+
// The server-level veto only; the wake gate runs at admission.
|
|
1960
2016
|
let triggerInference = true;
|
|
1961
2017
|
if (this.shouldTriggerInference) {
|
|
1962
2018
|
// The text the agent's context gets, as the push lane's filter reads
|
|
@@ -2258,16 +2314,58 @@ export class ChannelRegistry {
|
|
|
2258
2314
|
// Accessors
|
|
2259
2315
|
// ==========================================================================
|
|
2260
2316
|
|
|
2317
|
+
/**
|
|
2318
|
+
* The most recent DELIVERED inbound message (channel, message, thread).
|
|
2319
|
+
* Recorded by the framework after admission, so a held message never
|
|
2320
|
+
* becomes "the latest inbound" (issue #232 gap 2), and never for a marker
|
|
2321
|
+
* (a reaction, reaction removal, edit or deletion, apart from an edit the
|
|
2322
|
+
* coalescer delivered as the agent's first view: isMarkerTagged). Read as
|
|
2323
|
+
* the `incoming` reference in the channel context and, under
|
|
2324
|
+
* `speech.locus: 'latest-inbound'`, as the fallback speech locus. The
|
|
2325
|
+
* channel is on the server the message came from. A message whose server
|
|
2326
|
+
* no longer registers its channel sets nothing, as one the coalescer admits
|
|
2327
|
+
* after its server was removed: the removal cleared that server's message,
|
|
2328
|
+
* and a server that's gone mustn't displace a live one's anchor. That's
|
|
2329
|
+
* read at admission, where main's guard compared the server's removal
|
|
2330
|
+
* count at arrival and at acceptance (markAccepted): a message that
|
|
2331
|
+
* arrived before its server's removal and is admitted once the server
|
|
2332
|
+
* registers the channel again sets the anchor here, as a message delivered
|
|
2333
|
+
* on a live channel, where main's would set nothing.
|
|
2334
|
+
*/
|
|
2335
|
+
noteDelivered(channel: ChannelRef, messageId: string, threadId?: string, tags?: unknown, firstView = false): void {
|
|
2336
|
+
// A marker is ABOUT a message, not one the agent replies to: it leaves
|
|
2337
|
+
// the anchor where it was (#157), and so does a chat:system notice (a
|
|
2338
|
+
// join, the weather), which is about the place (isMarkerTagged). An edit
|
|
2339
|
+
// the coalescer delivered as the agent's first view of its message counts
|
|
2340
|
+
// as that message.
|
|
2341
|
+
if (isMarkerTagged(tags, firstView)) return;
|
|
2342
|
+
if (!this.entryAt(channel)) return;
|
|
2343
|
+
this.defaultPublishChannel = { serverId: channel.serverId, channelId: channel.channelId };
|
|
2344
|
+
this.defaultPublishMessageId = messageId;
|
|
2345
|
+
this.defaultPublishThreadId = threadId;
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2261
2348
|
/**
|
|
2262
2349
|
* Get the default publish channel ID: the channel of the most recent
|
|
2263
|
-
* incoming message that isn't a reaction, reaction removal, edit
|
|
2264
|
-
* deletion marker, apart from an edit the coalescer delivers on
|
|
2265
|
-
* as the agent's first view.
|
|
2350
|
+
* delivered incoming message that isn't a reaction, reaction removal, edit
|
|
2351
|
+
* or deletion marker, apart from an edit the coalescer delivers on
|
|
2352
|
+
* acceptance as the agent's first view.
|
|
2266
2353
|
*/
|
|
2267
2354
|
getDefaultPublishChannel(): ChannelRef | null {
|
|
2268
2355
|
return this.defaultPublishChannel;
|
|
2269
2356
|
}
|
|
2270
2357
|
|
|
2358
|
+
/**
|
|
2359
|
+
* Public form of the tool-input channel resolution (resolveToolChannelEntry):
|
|
2360
|
+
* optional server, the calling turn's own channels (`own`), ambiguity is an
|
|
2361
|
+
* error.
|
|
2362
|
+
*/
|
|
2363
|
+
resolveChannel(channelId: string, serverId?: string, own?: readonly ChannelRef[]): ChannelRef | { error: string } {
|
|
2364
|
+
const resolved = this.resolveToolChannelEntry(channelId, serverId, own);
|
|
2365
|
+
if (!resolved.entry) return { error: resolved.error ?? `Channel not found: ${channelId}` };
|
|
2366
|
+
return { serverId: resolved.entry.serverId, channelId: resolved.entry.descriptor.id };
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2271
2369
|
/**
|
|
2272
2370
|
* RFC-006 §3.2: is `channelId` registered BY `serverId` through a server
|
|
2273
2371
|
* declaration (channels/register, channels/changed) or an authorized open?
|
|
@@ -2386,7 +2484,10 @@ export class ChannelRegistry {
|
|
|
2386
2484
|
return this.handleToolJournal(input as { content?: string });
|
|
2387
2485
|
|
|
2388
2486
|
case 'skip_reply':
|
|
2389
|
-
return this.handleToolSkipReply(
|
|
2487
|
+
return this.handleToolSkipReply(
|
|
2488
|
+
input as { reason?: string; wake_in_seconds?: unknown },
|
|
2489
|
+
origin?.kind === 'agent' ? origin.earlierWake : undefined,
|
|
2490
|
+
);
|
|
2390
2491
|
|
|
2391
2492
|
default:
|
|
2392
2493
|
// Not every agent is given every channel tool, so the refusal points
|
|
@@ -2405,7 +2506,7 @@ export class ChannelRegistry {
|
|
|
2405
2506
|
|
|
2406
2507
|
/**
|
|
2407
2508
|
* The message the agent is replying to, with its channel: the most recent
|
|
2408
|
-
* inbound message that isn't a reaction, reaction removal, edit or deletion
|
|
2509
|
+
* delivered inbound message (noteDelivered) that isn't a reaction, reaction removal, edit or deletion
|
|
2409
2510
|
* marker, apart from an edit the coalescer delivers on acceptance as the
|
|
2410
2511
|
* agent's first view. Left process-global: per-channel inbound tracking
|
|
2411
2512
|
* (the right messageId for a fork's own channel) is a separate concern from
|
|
@@ -2422,9 +2523,13 @@ export class ChannelRegistry {
|
|
|
2422
2523
|
if (agentName !== undefined && this.activeChannelResolver?.(agentName) === null && !this.homeChannelResolver?.(agentName)) {
|
|
2423
2524
|
return undefined;
|
|
2424
2525
|
}
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2526
|
+
// The latest delivered inbound itself, not the speech fallback
|
|
2527
|
+
// (globalFallbackFor), which under `speech.locus: 'delivery'` is the
|
|
2528
|
+
// all-except hold's excepted channel while one is open, and none
|
|
2529
|
+
// otherwise.
|
|
2530
|
+
const anchor = agentName !== undefined && this.takesGlobalFallback?.(agentName) === false ? null : this.defaultPublishChannel;
|
|
2531
|
+
if (!anchor || !this.defaultPublishMessageId) return undefined;
|
|
2532
|
+
return { channel: anchor, messageId: this.defaultPublishMessageId, threadId: this.defaultPublishThreadId };
|
|
2428
2533
|
}
|
|
2429
2534
|
|
|
2430
2535
|
/**
|
|
@@ -2442,23 +2547,27 @@ export class ChannelRegistry {
|
|
|
2442
2547
|
// declared it inbound-only, since speech isn't posted there): a conversation
|
|
2443
2548
|
// fork's home channel, else this turn's channel (single trunk agent — item-3
|
|
2444
2549
|
// redux: its locus, else its triggering channel), else the global default
|
|
2445
|
-
// (
|
|
2550
|
+
// (a no-trigger turn's, but not a heartbeat's, whose turn takes none).
|
|
2551
|
+
// Without this, a fork
|
|
2446
2552
|
// was advertised the global locus but published somewhere else; and a trunk
|
|
2447
2553
|
// agent was told the wrong channel under concurrency.
|
|
2448
2554
|
// Each lookup is keyed on whether a name was given, not its truthiness:
|
|
2449
2555
|
// '' is a name an ephemeral run can have, and resolveLocus reads it too.
|
|
2450
2556
|
const home = agentName !== undefined ? this.homeChannelResolver?.(agentName) : undefined;
|
|
2451
2557
|
const active = agentName !== undefined ? this.activeChannelResolver?.(agentName) : undefined;
|
|
2452
|
-
// An ephemeral run takes no global fallback (takesGlobalFallback):
|
|
2453
|
-
// the latest inbound message's channel as
|
|
2454
|
-
// message as the one it is replying to.
|
|
2455
|
-
const fallback = agentName !== undefined ? this.globalFallbackFor(agentName) : this.
|
|
2558
|
+
// An ephemeral run takes no global fallback (takesGlobalFallback): not the
|
|
2559
|
+
// all-except hold's channel, not the latest inbound message's channel as
|
|
2560
|
+
// its outgoing one, nor that message as the one it is replying to.
|
|
2561
|
+
const fallback = agentName !== undefined ? this.globalFallbackFor(agentName) : this.sharedFallback();
|
|
2456
2562
|
// A turn that deliberately has no channel (null) takes no fallback.
|
|
2457
2563
|
const outgoing = home ?? (active !== undefined ? active : fallback);
|
|
2458
2564
|
|
|
2459
2565
|
const context: ChannelContext = {};
|
|
2460
2566
|
|
|
2461
|
-
// Incoming: the message the agent is replying to (incomingFor).
|
|
2567
|
+
// Incoming: the message the agent is replying to (incomingFor). The
|
|
2568
|
+
// anchor is independent of the speech rule: under `speech.locus:
|
|
2569
|
+
// 'delivery'` the latest inbound is no fallback for where prose lands,
|
|
2570
|
+
// but it is still the message the agent is replying to.
|
|
2462
2571
|
const incoming = this.incomingFor(agentName);
|
|
2463
2572
|
if (incoming && incoming.channel.serverId === serverId) {
|
|
2464
2573
|
context.incoming = {
|
|
@@ -3921,12 +4030,20 @@ export class ChannelRegistry {
|
|
|
3921
4030
|
* name, only where no registered channel takes that name, as a channel
|
|
3922
4031
|
* tool reads it or a `>>` target does (by label or raw id too), so the
|
|
3923
4032
|
* name can't send anything to another channel; else by its id and server.
|
|
4033
|
+
* A name that wouldn't show as itself (showsAsItself: one holding a line
|
|
4034
|
+
* break, a notice opening, a quote, a backslash or a lone surrogate, or
|
|
4035
|
+
* whitespace at its edges) is shown by its id and its server too, which
|
|
4036
|
+
* the channel tools take exactly and the prose resolver doesn't, so no name
|
|
4037
|
+
* shown can be another channel's.
|
|
3924
4038
|
*/
|
|
3925
4039
|
nameShown(ref: ChannelRef): string {
|
|
3926
4040
|
const entry = this.entryAt(ref);
|
|
3927
|
-
if (entry)
|
|
4041
|
+
if (entry) {
|
|
4042
|
+
const name = this.nameFor(entry);
|
|
4043
|
+
return name !== undefined && showsAsItself(name) ? name : channelOnServer(ref);
|
|
4044
|
+
}
|
|
3928
4045
|
for (const name of [ref.channelId, qualifiedChannelName(ref)]) {
|
|
3929
|
-
if (this.entriesNamed(name).length === 0 && !('channelId' in this.resolveProseTarget(name))) return name;
|
|
4046
|
+
if (showsAsItself(name) && this.entriesNamed(name).length === 0 && !('channelId' in this.resolveProseTarget(name))) return name;
|
|
3930
4047
|
}
|
|
3931
4048
|
return channelOnServer(ref);
|
|
3932
4049
|
}
|
|
@@ -3951,8 +4068,8 @@ export class ChannelRegistry {
|
|
|
3951
4068
|
/**
|
|
3952
4069
|
* How a tuned-out channel's tune-out ends, for the texts that tell an agent
|
|
3953
4070
|
* channel_open doesn't change it: a tune_out cancel, by the name that
|
|
3954
|
-
* picks the channel (nameFor), or, where no name picks it alone, its
|
|
3955
|
-
*
|
|
4071
|
+
* picks the channel (nameFor), or, where no name picks it alone, by its id
|
|
4072
|
+
* and server, since tune_out takes a serverId.
|
|
3956
4073
|
* For channel_close, which refuses a tuned-out channel too, it adds that
|
|
3957
4074
|
* the close comes after that.
|
|
3958
4075
|
*/
|
|
@@ -3965,12 +4082,15 @@ export class ChannelRegistry {
|
|
|
3965
4082
|
|
|
3966
4083
|
private tuneOutEndText(entry: ChannelEntry, tool?: string): string {
|
|
3967
4084
|
const close = tool === 'channel_close';
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
4085
|
+
// A name is written as a literal tune_out's channelId takes back
|
|
4086
|
+
// (quotedValue, main's #852); one no name picks alone, by its id and
|
|
4087
|
+
// server, since this branch's tune_out takes a serverId.
|
|
4088
|
+
const name = this.nameFor(entry);
|
|
4089
|
+
const names = name !== undefined
|
|
4090
|
+
? `channelId ${quotedValue(name)}`
|
|
4091
|
+
: `channelId ${quotedValue(entry.descriptor.id)}, serverId ${JSON.stringify(entry.serverId)}`;
|
|
4092
|
+
return `Call tune_out with ${names} and mode "cancel" for it to resume normal ` +
|
|
4093
|
+
`attention; cancelling delivers the diverted backlog${close ? ', and channel_close closes it after that.' : '.'}`;
|
|
3974
4094
|
}
|
|
3975
4095
|
|
|
3976
4096
|
/**
|
|
@@ -3979,7 +4099,7 @@ export class ChannelRegistry {
|
|
|
3979
4099
|
*/
|
|
3980
4100
|
quotedShown(ref: ChannelRef): string {
|
|
3981
4101
|
const shown = this.nameShown(ref);
|
|
3982
|
-
return shown === channelOnServer(ref) ? shown :
|
|
4102
|
+
return shown === channelOnServer(ref) ? shown : quotedValue(shown);
|
|
3983
4103
|
}
|
|
3984
4104
|
|
|
3985
4105
|
/**
|
|
@@ -4017,7 +4137,7 @@ export class ChannelRegistry {
|
|
|
4017
4137
|
const nothing = opts.nothing ?? 'nothing ran';
|
|
4018
4138
|
const names = entries.map((e) => this.nameFor(e));
|
|
4019
4139
|
if (names.every((n) => n !== undefined)) {
|
|
4020
|
-
const listed = names.map((n) =>
|
|
4140
|
+
const listed = names.map((n) => quotedValue(n!)).join(', ');
|
|
4021
4141
|
const servers = [...new Set(entries.map((e) => JSON.stringify(e.serverId)))];
|
|
4022
4142
|
// One server's two channels can answer to one name too, as `s`'s
|
|
4023
4143
|
// `s:room` and `room` both do to `s:room`; its server picks neither.
|
|
@@ -4038,8 +4158,8 @@ export class ChannelRegistry {
|
|
|
4038
4158
|
const own = names[i];
|
|
4039
4159
|
if (own === undefined && !opts.takesServerId) return `- ${where}: no channelId reaches it alone.`;
|
|
4040
4160
|
return own !== undefined
|
|
4041
|
-
? `- ${where}: call again with channelId ${
|
|
4042
|
-
: `- ${where}: call again with channelId ${
|
|
4161
|
+
? `- ${where}: call again with channelId ${quotedValue(own)}.`
|
|
4162
|
+
: `- ${where}: call again with channelId ${quotedValue(e.descriptor.id)} and serverId ${JSON.stringify(e.serverId)}.`;
|
|
4043
4163
|
});
|
|
4044
4164
|
const count = entries.length === 2 ? 'either of two channels' : `any of ${entries.length} channels`;
|
|
4045
4165
|
return `Channel ${JSON.stringify(name)} could mean ${count}, so ${nothing}:\n${lines.join('\n')}`;
|
|
@@ -4078,7 +4198,7 @@ export class ChannelRegistry {
|
|
|
4078
4198
|
// so the text says whose.
|
|
4079
4199
|
if (!alone) return 'channel_list shows the channels and their servers.';
|
|
4080
4200
|
const one = named[0]!;
|
|
4081
|
-
const names = `${JSON.stringify(channelId)} alone names channel ${
|
|
4201
|
+
const names = `${JSON.stringify(channelId)} alone names channel ${quotedValue(one.descriptor.id)} on server `
|
|
4082
4202
|
+ JSON.stringify(one.serverId);
|
|
4083
4203
|
return takesServerId ? `${names}: for that one, call again with serverId left out.` : `${names}.`;
|
|
4084
4204
|
}
|
|
@@ -4090,7 +4210,7 @@ export class ChannelRegistry {
|
|
|
4090
4210
|
const places = elsewhere.map((e) => {
|
|
4091
4211
|
const at = `on server ${JSON.stringify(e.serverId)}`;
|
|
4092
4212
|
const name = this.nameFor(e);
|
|
4093
|
-
return name === undefined ? `${at} (no channelId reaches it alone)` : `${at} as ${
|
|
4213
|
+
return name === undefined ? `${at} (no channelId reaches it alone)` : `${at} as ${quotedValue(name)}`;
|
|
4094
4214
|
});
|
|
4095
4215
|
return `It's ${places.length === 1 ? places[0] : `${places.slice(0, -1).join(', ')} and ${places.at(-1)}`}.`;
|
|
4096
4216
|
}
|
|
@@ -4394,17 +4514,19 @@ export class ChannelRegistry {
|
|
|
4394
4514
|
spec: string,
|
|
4395
4515
|
opts?: { forSpeech?: boolean; own?: readonly ChannelRef[] },
|
|
4396
4516
|
): PublishDestination | { error: string; candidates?: string[]; refusedChannelId?: string } {
|
|
4397
|
-
const
|
|
4517
|
+
const name = spec.trim();
|
|
4518
|
+
if (name === NO_CHANNEL_EXAMPLE || name === NO_PERSON_EXAMPLE) return { error: `${name} is the example's placeholder: write the name it stands for in its place` };
|
|
4519
|
+
const own = this.ownChannelNamed(name, opts?.own);
|
|
4398
4520
|
let resolved: PublishDestination | { error: string; candidates?: string[] };
|
|
4399
4521
|
if (own === undefined) {
|
|
4400
|
-
resolved = this.resolveTargetByName(
|
|
4522
|
+
resolved = this.resolveTargetByName(name);
|
|
4401
4523
|
} else if ('neither' in own) {
|
|
4402
|
-
resolved = { error: `"${
|
|
4524
|
+
resolved = { error: `"${name}" is the id of this turn's channels on more than one server`, candidates: this.ownCandidates(own.neither).map((c) => c.prose) };
|
|
4403
4525
|
} else {
|
|
4404
4526
|
const entry = this.entryAt(own.channel);
|
|
4405
4527
|
resolved = entry
|
|
4406
4528
|
? { channelId: entry.descriptor.id, serverId: entry.serverId, label: entry.descriptor.label }
|
|
4407
|
-
: { error: `${this.ownChannelGoneText(
|
|
4529
|
+
: { error: `${this.ownChannelGoneText(name, own.channel)}, and nothing can be sent there until that server registers it again` };
|
|
4408
4530
|
}
|
|
4409
4531
|
if (!opts?.forSpeech || 'error' in resolved) return resolved;
|
|
4410
4532
|
const entry = this.entryAt(resolved);
|
|
@@ -4463,7 +4585,7 @@ export class ChannelRegistry {
|
|
|
4463
4585
|
if (!mine) return undefined;
|
|
4464
4586
|
if ('neither' in mine) {
|
|
4465
4587
|
const done = tool === 'publish' ? 'was posted' : tool === 'deliver' ? 'was delivered' : 'ran';
|
|
4466
|
-
return { error: this.ownNeitherRefusal(name, mine.neither, done) };
|
|
4588
|
+
return { error: this.ownNeitherRefusal(name, mine.neither, done, false) };
|
|
4467
4589
|
}
|
|
4468
4590
|
return this.entryAt(mine.channel) ? { channel: mine.channel } : { error: this.ownChannelGoneRefusal(name, mine.channel, tool) };
|
|
4469
4591
|
}
|
|
@@ -4482,32 +4604,73 @@ export class ChannelRegistry {
|
|
|
4482
4604
|
/**
|
|
4483
4605
|
* The turn's channels a name means neither of (ownChannelNamed), each by a
|
|
4484
4606
|
* string that reaches it outside the turn's rule: its server-qualified
|
|
4485
|
-
* name, where that picks it alone, since its id is the name refused.
|
|
4486
|
-
*
|
|
4487
|
-
*
|
|
4607
|
+
* name, where that picks it alone, since its id is the name refused. `text`
|
|
4608
|
+
* is that name, which a refusal writes as JSON; `prose`, which a `>>`
|
|
4609
|
+
* target reads, is that name where it shows as itself, else its id and
|
|
4610
|
+
* server. One no string picks alone, and one its server no longer
|
|
4611
|
+
* registers, are named by id and server in both, saying so. `reaches` says
|
|
4612
|
+
* which a name reaches.
|
|
4488
4613
|
*/
|
|
4489
|
-
private ownCandidates(channels: readonly ChannelRef[]): Array<{ text: string; reaches: boolean }> {
|
|
4614
|
+
private ownCandidates(channels: readonly ChannelRef[]): Array<{ text: string; prose: string; reaches: boolean; byServer: boolean }> {
|
|
4490
4615
|
return channels.map((c) => {
|
|
4491
4616
|
const entry = this.entryAt(c);
|
|
4492
|
-
if (!entry)
|
|
4617
|
+
if (!entry) {
|
|
4618
|
+
const text = `${channelOnServer(c)} (its server no longer registers it)`;
|
|
4619
|
+
return { text, prose: text, reaches: false, byServer: false };
|
|
4620
|
+
}
|
|
4493
4621
|
const qualified = qualifiedChannelName(c);
|
|
4494
4622
|
const picked = this.entriesNamed(qualified);
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4623
|
+
if (picked.length !== 1 || picked[0] !== entry) {
|
|
4624
|
+
const text = `${channelOnServer(c)} (no channelId reaches it alone)`;
|
|
4625
|
+
return { text, prose: text, reaches: false, byServer: true };
|
|
4626
|
+
}
|
|
4627
|
+
return { text: qualified, prose: showsAsItself(qualified) ? qualified : channelOnServer(c), reaches: true, byServer: false };
|
|
4498
4628
|
});
|
|
4499
4629
|
}
|
|
4500
4630
|
|
|
4631
|
+
/**
|
|
4632
|
+
* What a refusal listing these candidates says of each one that is a
|
|
4633
|
+
* channel shown by its id and server: the channelId channel_publish reaches
|
|
4634
|
+
* it by, or that no channelId reaches it alone. That is its server-qualified
|
|
4635
|
+
* name where that picks it alone, since a bare id is what a turn's own
|
|
4636
|
+
* channels answer to first, whether or not their server still registers
|
|
4637
|
+
* them (ownChannelNamed), and else its id where that picks it alone and
|
|
4638
|
+
* isn't empty (channel_publish refuses an empty one). It leaves out a channel no
|
|
4639
|
+
* longer registered and a candidate that already says no channelId reaches
|
|
4640
|
+
* it alone, and is empty when nothing is left.
|
|
4641
|
+
*/
|
|
4642
|
+
publishRouteFor(candidates: readonly string[]): string {
|
|
4643
|
+
const parts = new Set<string>();
|
|
4644
|
+
for (const candidate of candidates) {
|
|
4645
|
+
const shown = CHANNEL_ON_SERVER_AT_END.exec(candidate);
|
|
4646
|
+
if (!shown) continue;
|
|
4647
|
+
const ref: ChannelRef = { channelId: JSON.parse(shown[1]!) as string, serverId: JSON.parse(shown[2]!) as string };
|
|
4648
|
+
const entry = this.entryAt(ref);
|
|
4649
|
+
if (!entry) continue;
|
|
4650
|
+
const name = [qualifiedChannelName(ref), ref.channelId].find((n) => {
|
|
4651
|
+
const named = n === '' ? [] : this.entriesNamed(n);
|
|
4652
|
+
return named.length === 1 && named[0] === entry;
|
|
4653
|
+
});
|
|
4654
|
+
parts.add(name !== undefined
|
|
4655
|
+
? `channel_publish reaches ${channelOnServer(ref)} with channelId ${quotedValue(name)}`
|
|
4656
|
+
: `no channelId reaches ${channelOnServer(ref)} alone`);
|
|
4657
|
+
}
|
|
4658
|
+
return parts.size ? ` For a channel shown by its id and server: ${[...parts].join('; ')}.` : '';
|
|
4659
|
+
}
|
|
4660
|
+
|
|
4501
4661
|
/** A tool's refusal of a name the turn's channels on more than one server
|
|
4502
4662
|
* answer to by their id (ownChannelNamed's neither). */
|
|
4503
|
-
private ownNeitherRefusal(name: string, channels: readonly ChannelRef[], done: string): string {
|
|
4663
|
+
private ownNeitherRefusal(name: string, channels: readonly ChannelRef[], done: string, takesServer = true): string {
|
|
4504
4664
|
const candidates = this.ownCandidates(channels);
|
|
4505
|
-
const named = candidates.filter((c) => c.reaches).map((c) =>
|
|
4665
|
+
const named = candidates.filter((c) => c.reaches).map((c) => quotedValue(c.text));
|
|
4506
4666
|
const pick = named.length === candidates.length ? ' Call again with channelId set to one of them.'
|
|
4507
4667
|
: named.length === 1 ? ` Call again with channelId ${named[0]}.`
|
|
4508
4668
|
: named.length > 1 ? ` Call again with channelId set to one of ${named.join(', ')}.` : '';
|
|
4509
|
-
const listed = candidates.map((c) => (c.reaches ?
|
|
4510
|
-
|
|
4669
|
+
const listed = candidates.map((c) => (c.reaches ? quotedValue(c.text) : c.text)).join(', ');
|
|
4670
|
+
const byServer = takesServer && candidates.some((c) => c.byServer)
|
|
4671
|
+
? ' For a channel shown by its id and server, call again with that channelId and that serverId.'
|
|
4672
|
+
: '';
|
|
4673
|
+
return `"${name}" is the id of this turn's channels on more than one server (${listed}), so nothing ${done}.${pick}${byServer}`;
|
|
4511
4674
|
}
|
|
4512
4675
|
|
|
4513
4676
|
private resolveTargetByName(
|
|
@@ -4521,7 +4684,7 @@ export class ChannelRegistry {
|
|
|
4521
4684
|
// A candidate is named by the token that picks it alone (nameFor), so
|
|
4522
4685
|
// two servers' channels with one id or one label can be told apart, or,
|
|
4523
4686
|
// where no string picks it alone, by its id and its server.
|
|
4524
|
-
const token = (e: ChannelEntry): string => this.
|
|
4687
|
+
const token = (e: ChannelEntry): string => this.nameShown(refOf(e));
|
|
4525
4688
|
// A DM is shown by its label, the person, where that label, given as a
|
|
4526
4689
|
// target, picks that DM. It doesn't where another channel answers to that
|
|
4527
4690
|
// label too, as two servers' DMs with one person do (a label is matched
|
|
@@ -4537,7 +4700,7 @@ export class ChannelRegistry {
|
|
|
4537
4700
|
};
|
|
4538
4701
|
const dmShown = (list: ChannelEntry[]): string[] => list.map((e) => {
|
|
4539
4702
|
const label = e.descriptor.label ?? e.descriptor.id;
|
|
4540
|
-
return labelPicks(e, label) ? label : token(e);
|
|
4703
|
+
return showsAsItself(label) && labelPicks(e, label) ? label : token(e);
|
|
4541
4704
|
});
|
|
4542
4705
|
|
|
4543
4706
|
// A channel id, or its server-qualified name (entriesNamed).
|
|
@@ -4655,7 +4818,7 @@ export class ChannelRegistry {
|
|
|
4655
4818
|
if (byName.length > 1) {
|
|
4656
4819
|
return {
|
|
4657
4820
|
error: `"${trimmed}" matches several channels`,
|
|
4658
|
-
candidates: byName.map((e) => `${e.descriptor.label} = ${token(e)}`),
|
|
4821
|
+
candidates: byName.map((e) => `${nameToTypeBack(e.descriptor.label ?? '')} = ${token(e)}`),
|
|
4659
4822
|
};
|
|
4660
4823
|
}
|
|
4661
4824
|
|
|
@@ -4664,7 +4827,7 @@ export class ChannelRegistry {
|
|
|
4664
4827
|
const near = entries
|
|
4665
4828
|
.filter((e) => norm(e.descriptor.label ?? '').includes(norm(trimmed)))
|
|
4666
4829
|
.slice(0, 5)
|
|
4667
|
-
.map((e) => `${e.descriptor.label} = ${token(e)}`);
|
|
4830
|
+
.map((e) => `${nameToTypeBack(e.descriptor.label ?? '')} = ${token(e)}`);
|
|
4668
4831
|
return { error: `no channel matches "${trimmed}"`, ...(near.length ? { candidates: near } : {}) };
|
|
4669
4832
|
}
|
|
4670
4833
|
|
|
@@ -4701,7 +4864,7 @@ export class ChannelRegistry {
|
|
|
4701
4864
|
qualifiedChannelName(ref),
|
|
4702
4865
|
];
|
|
4703
4866
|
for (const c of candidates) {
|
|
4704
|
-
if (!isProseTargetToken(c) || c === '#') continue;
|
|
4867
|
+
if (!isProseTargetToken(c) || c === '#' || !showsAsItself(c)) continue;
|
|
4705
4868
|
const r = this.resolveProseTarget(c, { forSpeech: true });
|
|
4706
4869
|
if ('channelId' in r && sameChannel(r, ref)) return c;
|
|
4707
4870
|
}
|
|
@@ -4990,7 +5153,7 @@ export class ChannelRegistry {
|
|
|
4990
5153
|
channelId: input.channelId,
|
|
4991
5154
|
serverId: entry.serverId,
|
|
4992
5155
|
status: alreadyDesiredOpen ? 'reconciled' : 'opened',
|
|
4993
|
-
...(result.history ? { history: result.history } : {}),
|
|
5156
|
+
...(result.history ? { history: markedHistory(result.history) } : {}),
|
|
4994
5157
|
...(result.historyTruncated ? { historyTruncated: true } : {}),
|
|
4995
5158
|
},
|
|
4996
5159
|
};
|
|
@@ -5070,12 +5233,13 @@ export class ChannelRegistry {
|
|
|
5070
5233
|
// Closed already, perhaps only by default: the agent's close is still
|
|
5071
5234
|
// its decision, and is recorded as one (setDesiredState), so a later
|
|
5072
5235
|
// registration's lift of a default leaves the channel closed.
|
|
5236
|
+
const ended = this.onToolClose?.(entry.serverId, input.channelId);
|
|
5073
5237
|
try {
|
|
5074
5238
|
this.setDesiredState(entry.serverId, input.channelId, 'closed', closeSource);
|
|
5075
5239
|
} catch (err) {
|
|
5076
5240
|
return {
|
|
5077
5241
|
success: false,
|
|
5078
|
-
error: `Channel ${shown} is already closed, but recording the decision to close it failed: ${(err as Error).message}`,
|
|
5242
|
+
error: `Channel ${shown} is already closed, but recording the decision to close it failed: ${(err as Error).message}${ended ? ` ${ended}` : ''}`,
|
|
5079
5243
|
isError: true,
|
|
5080
5244
|
};
|
|
5081
5245
|
}
|
|
@@ -5118,12 +5282,18 @@ export class ChannelRegistry {
|
|
|
5118
5282
|
const refusal = this.tunedOutRefusal('channel_close', entry);
|
|
5119
5283
|
return isModuleOrigin ? { ...refusal, isError: false } : refusal;
|
|
5120
5284
|
}
|
|
5285
|
+
// A hold around this channel — the all-except scope it is the excepted
|
|
5286
|
+
// channel of — ends through the cancel flow: backlog released, end
|
|
5287
|
+
// recorded, never by overwriting state (#232 gap 9).
|
|
5288
|
+
const ended = this.onToolClose?.(entry.serverId, input.channelId);
|
|
5121
5289
|
|
|
5122
|
-
// A decision the log refused changed nothing
|
|
5290
|
+
// A decision the log refused changed nothing for the channel, and the
|
|
5291
|
+
// answer says so, and what the hook ended all the same.
|
|
5123
5292
|
try {
|
|
5124
5293
|
this.setDesiredState(entry.serverId, input.channelId, 'closed', closeSource);
|
|
5125
5294
|
} catch (err) {
|
|
5126
|
-
|
|
5295
|
+
const text = this.lifecycleFailureText('close', entry, unrecordedDecision(err));
|
|
5296
|
+
return { success: false, error: ended ? `${text} ${ended}` : text, isError: true };
|
|
5127
5297
|
}
|
|
5128
5298
|
// This channel's typing, not every channel with its id (stopTyping's
|
|
5129
5299
|
// fan-out of a bare name).
|
|
@@ -5479,13 +5649,14 @@ export class ChannelRegistry {
|
|
|
5479
5649
|
* tool, so any trailing prose this turn is NOT posted. Replaces the old
|
|
5480
5650
|
* overloaded use of `think` for staying silent.
|
|
5481
5651
|
*/
|
|
5482
|
-
private handleToolSkipReply(input: { reason?: string; wake_in_seconds?: unknown }): ToolResult {
|
|
5652
|
+
private handleToolSkipReply(input: { reason?: string; wake_in_seconds?: unknown }, earlierWake?: SkipReplyEarlierWake): ToolResult {
|
|
5483
5653
|
// The self-wake itself is armed by the framework (which owns the
|
|
5484
5654
|
// EventGate) BEFORE its dispatch reaches this handler, from the same
|
|
5485
5655
|
// reading, and the framework refuses a wake the host can't set before
|
|
5486
5656
|
// dispatching. A caller of this registry's handleChannelToolCall
|
|
5487
5657
|
// (AgentFramework.channels) goes past both, so its note names a wake
|
|
5488
|
-
// nothing armed.
|
|
5658
|
+
// nothing armed. The framework, which also owns the heartbeat, says
|
|
5659
|
+
// which of its wakes is due before the self-wake (`earlierWake`).
|
|
5489
5660
|
const wake = readSkipReplyWake(input);
|
|
5490
5661
|
const selfWake = wake.kind === 'wake' ? wake.seconds : undefined;
|
|
5491
5662
|
return {
|
|
@@ -5501,9 +5672,7 @@ export class ChannelRegistry {
|
|
|
5501
5672
|
// on Fable: ~60% of skip_reply result bytes were verbatim input echo).
|
|
5502
5673
|
data: {
|
|
5503
5674
|
skipped: true,
|
|
5504
|
-
note: selfWake !== undefined
|
|
5505
|
-
? `Turn ended; nothing sent. Self-wake in ~${selfWake}s unless something wakes you first.`
|
|
5506
|
-
: 'Turn ended; nothing sent.',
|
|
5675
|
+
note: skipReplyNote(selfWake, selfWake !== undefined ? earlierWake : undefined),
|
|
5507
5676
|
},
|
|
5508
5677
|
};
|
|
5509
5678
|
}
|
|
@@ -5601,9 +5770,29 @@ export class ChannelRegistry {
|
|
|
5601
5770
|
return active !== undefined ? active : this.globalFallbackFor(conversationId);
|
|
5602
5771
|
}
|
|
5603
5772
|
|
|
5604
|
-
/**
|
|
5773
|
+
/**
|
|
5774
|
+
* The process-global fallback for this conversation, or null when it takes
|
|
5775
|
+
* none (takesGlobalFallback): an ephemeral run's prose is its caller's
|
|
5776
|
+
* result, so neither the all-except hold's channel nor the latest inbound
|
|
5777
|
+
* message's is ever its destination.
|
|
5778
|
+
*/
|
|
5605
5779
|
private globalFallbackFor(conversationId: string): ChannelRef | null {
|
|
5606
|
-
return this.takesGlobalFallback?.(conversationId) === false ? null : this.
|
|
5780
|
+
return this.takesGlobalFallback?.(conversationId) === false ? null : this.sharedFallback();
|
|
5781
|
+
}
|
|
5782
|
+
|
|
5783
|
+
/**
|
|
5784
|
+
* What every conversation that takes the global fallback falls back to when
|
|
5785
|
+
* neither a home channel nor its own delivery names one: the all-except
|
|
5786
|
+
* hold's channel while one is open, else the latest delivered inbound
|
|
5787
|
+
* channel under the legacy locus rule, else none — the prose stays private
|
|
5788
|
+
* (architecture channel §3 carve-out, 2026-10-06).
|
|
5789
|
+
*/
|
|
5790
|
+
private sharedFallback(): ChannelRef | null {
|
|
5791
|
+
return this.focusChannelResolver?.() ?? this.fallbackLocus();
|
|
5792
|
+
}
|
|
5793
|
+
|
|
5794
|
+
private fallbackLocus(): ChannelRef | null {
|
|
5795
|
+
return this.latestInboundLocus ? this.defaultPublishChannel : null;
|
|
5607
5796
|
}
|
|
5608
5797
|
|
|
5609
5798
|
/**
|
|
@@ -6055,17 +6244,20 @@ export class ChannelRegistry {
|
|
|
6055
6244
|
if (input.channelId === '' && tool === 'channel_publish') {
|
|
6056
6245
|
return { success: false, error: `Nothing was posted: channelId is empty. ${findIds}`, isError: true };
|
|
6057
6246
|
}
|
|
6058
|
-
// Resolve channelId:
|
|
6059
|
-
//
|
|
6060
|
-
//
|
|
6061
|
-
//
|
|
6247
|
+
// Resolve channelId: a call made inside the agent's turn (inTurn) has
|
|
6248
|
+
// the turn's default applied already, where its plain speech goes, and
|
|
6249
|
+
// takes none here. Any other defaults to what the caller falls back to:
|
|
6250
|
+
// the all-except hold's channel, else the latest delivered inbound
|
|
6251
|
+
// channel under the legacy locus rule (a marker — reaction, edit,
|
|
6252
|
+
// deletion — is never that channel), which an ephemeral run never takes
|
|
6062
6253
|
// (takesGlobalFallback), so it names one.
|
|
6063
6254
|
// A channel named by a string means what it means to every channel tool
|
|
6064
6255
|
// (entriesNamed): one channel, by its id or its server-qualified name,
|
|
6065
6256
|
// and a name two servers' channels answer to is refused with them as
|
|
6066
6257
|
// candidates, never sent to the first.
|
|
6067
6258
|
const caller = origin?.kind === 'agent' ? origin.agentName : undefined;
|
|
6068
|
-
const
|
|
6259
|
+
const inTurn = origin?.kind === 'agent' && origin.inTurn === true;
|
|
6260
|
+
const fallback = caller === undefined ? this.sharedFallback() : inTurn ? null : this.globalFallbackFor(caller);
|
|
6069
6261
|
if (!(input.channelId ?? fallback)) {
|
|
6070
6262
|
return {
|
|
6071
6263
|
success: false,
|
|
@@ -6074,7 +6266,10 @@ export class ChannelRegistry {
|
|
|
6074
6266
|
: caller !== undefined && this.takesGlobalFallback?.(caller) === false
|
|
6075
6267
|
? 'Nothing was posted: name the channel with channelId. A subagent has no default channel, since its plain '
|
|
6076
6268
|
+ 'speech is returned to whoever called it, so channel_publish sends only to a channel it names.'
|
|
6077
|
-
:
|
|
6269
|
+
: caller !== undefined && inTurn && this.activeChannelResolver?.(caller) === null
|
|
6270
|
+
? `Nothing was posted: name the channel with channelId. This turn has no channel of its own, so its plain speech `
|
|
6271
|
+
+ `goes to none and channel_publish sends only to a channel it names. ${findIds}`
|
|
6272
|
+
: `Nothing was posted: channel_publish needs a \`channelId\`, and there's no channel to default to yet. ${findIds}`,
|
|
6078
6273
|
isError: true,
|
|
6079
6274
|
};
|
|
6080
6275
|
}
|
|
@@ -6096,7 +6291,7 @@ export class ChannelRegistry {
|
|
|
6096
6291
|
entry = this.entryAt(serverGiven ? { serverId: input.serverId!, channelId: fallback!.channelId } : fallback!);
|
|
6097
6292
|
} else if (own && 'neither' in own) {
|
|
6098
6293
|
if (seen) seen.refusal = 'shared-id';
|
|
6099
|
-
return { success: false, error: this.ownNeitherRefusal(input.channelId, own.neither, 'was posted'), isError: true };
|
|
6294
|
+
return { success: false, error: this.ownNeitherRefusal(input.channelId, own.neither, 'was posted', false), isError: true };
|
|
6100
6295
|
} else if (own) {
|
|
6101
6296
|
entry = this.entryAt(own.channel);
|
|
6102
6297
|
if (!entry) {
|
|
@@ -6127,7 +6322,7 @@ export class ChannelRegistry {
|
|
|
6127
6322
|
// channel with its id isn't where its message came from.
|
|
6128
6323
|
error: input.channelId === undefined
|
|
6129
6324
|
? serverGiven
|
|
6130
|
-
? `Server "${input.serverId}" has no channel
|
|
6325
|
+
? `Server "${input.serverId}" has no channel ${quotedValue(fallback!.channelId)}, the id of the channel this call would default to, so nothing was posted.`
|
|
6131
6326
|
: `The channel this would default to, ${this.quotedShown(fallback!)}, isn't registered now, so nothing was posted.`
|
|
6132
6327
|
// This tool's schema has no serverId, so one given here is a host's
|
|
6133
6328
|
// ref (publishForAgent), or one an agent added unasked, and the
|