@animalabs/agent-framework 0.33.0 → 0.34.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 +136 -8
- package/dist/src/agent.d.ts +25 -4
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +68 -13
- package/dist/src/agent.js.map +1 -1
- package/dist/src/append-all.d.ts +5 -0
- package/dist/src/append-all.d.ts.map +1 -0
- package/dist/src/append-all.js +8 -0
- package/dist/src/append-all.js.map +1 -0
- package/dist/src/context-options.d.ts +9 -6
- package/dist/src/context-options.d.ts.map +1 -1
- package/dist/src/context-options.js +13 -8
- package/dist/src/context-options.js.map +1 -1
- package/dist/src/cut-off-round.js +4 -4
- package/dist/src/dendrite/registry.d.ts.map +1 -1
- package/dist/src/dendrite/registry.js +4 -2
- package/dist/src/dendrite/registry.js.map +1 -1
- package/dist/src/failure-text.d.ts +36 -0
- package/dist/src/failure-text.d.ts.map +1 -0
- package/dist/src/failure-text.js +89 -0
- package/dist/src/failure-text.js.map +1 -0
- package/dist/src/framework.d.ts +94 -30
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +987 -284
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +40 -6
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +66 -25
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/reply-judge.d.ts +4 -0
- package/dist/src/gate/reply-judge.d.ts.map +1 -1
- package/dist/src/gate/reply-judge.js +12 -3
- package/dist/src/gate/reply-judge.js.map +1 -1
- package/dist/src/gate/suppression-review.js +2 -2
- package/dist/src/gate/suppression-review.js.map +1 -1
- package/dist/src/gate/types.d.ts +7 -0
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/heartbeat/heartbeat.js +1 -1
- package/dist/src/heartbeat/heartbeat.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/inference-bounds.d.ts +2 -1
- package/dist/src/inference-bounds.d.ts.map +1 -1
- package/dist/src/inference-bounds.js +9 -6
- package/dist/src/inference-bounds.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +57 -13
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +136 -24
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts +109 -15
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
- package/dist/src/mcpl/modern-connection.js +593 -86
- package/dist/src/mcpl/modern-connection.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts +3 -1
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +15 -9
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +37 -18
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +31 -16
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
- package/dist/src/mcpl/tool-presence.js +10 -6
- package/dist/src/mcpl/tool-presence.js.map +1 -1
- package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
- package/dist/src/mcpl/toolset-host.js +7 -6
- package/dist/src/mcpl/toolset-host.js.map +1 -1
- package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
- package/dist/src/mcpl/toolsets-module.js +2 -1
- package/dist/src/mcpl/toolsets-module.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +25 -4
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/modules/discord/index.js +1 -1
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/health/index.js +2 -2
- package/dist/src/modules/health/index.js.map +1 -1
- package/dist/src/modules/history/semantic.js +3 -3
- package/dist/src/modules/history/semantic.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +4 -3
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +2 -1
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +3 -2
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/notice-tags.d.ts +10 -8
- package/dist/src/notice-tags.d.ts.map +1 -1
- package/dist/src/notice-tags.js +10 -8
- package/dist/src/notice-tags.js.map +1 -1
- package/dist/src/presentation.d.ts +2 -1
- package/dist/src/presentation.d.ts.map +1 -1
- package/dist/src/presentation.js +4 -2
- package/dist/src/presentation.js.map +1 -1
- package/dist/src/reasoning-loss.d.ts +5 -0
- package/dist/src/reasoning-loss.d.ts.map +1 -0
- package/dist/src/reasoning-loss.js +8 -0
- package/dist/src/reasoning-loss.js.map +1 -0
- package/dist/src/recovery/offline-branch.d.ts.map +1 -1
- package/dist/src/recovery/offline-branch.js +5 -4
- package/dist/src/recovery/offline-branch.js.map +1 -1
- package/dist/src/recovery/recover-cli.js +2 -1
- package/dist/src/recovery/recover-cli.js.map +1 -1
- package/dist/src/sent-rows.js +1 -1
- package/dist/src/supplied-text.d.ts +1 -1
- package/dist/src/supplied-text.js +3 -3
- package/dist/src/supplied-text.js.map +1 -1
- package/dist/src/thinking-drops.d.ts +2 -0
- package/dist/src/thinking-drops.d.ts.map +1 -1
- package/dist/src/thinking-drops.js.map +1 -1
- package/dist/src/tool-presentation.d.ts.map +1 -1
- package/dist/src/tool-presentation.js +38 -9
- package/dist/src/tool-presentation.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +13 -4
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +33 -16
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/tune-out/tools.js +2 -1
- package/dist/src/tune-out/tools.js.map +1 -1
- package/dist/src/types/agent.d.ts +39 -5
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +43 -8
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/inference-log.d.ts +1 -0
- package/dist/src/types/inference-log.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +64 -4
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agent.ts +71 -14
- package/src/append-all.ts +6 -0
- package/src/context-options.ts +18 -8
- package/src/cut-off-round.ts +4 -4
- package/src/dendrite/registry.ts +4 -2
- package/src/failure-text.ts +88 -0
- package/src/framework.ts +1018 -293
- package/src/gate/event-gate.ts +90 -26
- package/src/gate/reply-judge.ts +12 -3
- package/src/gate/suppression-review.ts +2 -2
- package/src/gate/types.ts +7 -0
- package/src/heartbeat/heartbeat.ts +1 -1
- package/src/index.ts +1 -0
- package/src/inference-bounds.ts +11 -6
- package/src/mcpl/channel-registry.ts +160 -31
- package/src/mcpl/modern-connection.ts +629 -91
- package/src/mcpl/protocol-family.ts +15 -9
- package/src/mcpl/server-connection.ts +28 -15
- package/src/mcpl/tool-presence.ts +10 -5
- package/src/mcpl/toolset-host.ts +7 -6
- package/src/mcpl/toolsets-module.ts +2 -1
- package/src/mcpl/types.ts +25 -4
- package/src/modules/discord/index.ts +1 -1
- package/src/modules/health/index.ts +2 -2
- package/src/modules/history/semantic.ts +3 -3
- package/src/modules/workspace/index.ts +4 -3
- package/src/modules/workspace/observe.ts +2 -1
- package/src/modules/workspace/reconcile.ts +3 -2
- package/src/notice-tags.ts +11 -8
- package/src/presentation.ts +4 -2
- package/src/reasoning-loss.ts +7 -0
- package/src/recovery/offline-branch.ts +5 -4
- package/src/recovery/recover-cli.ts +2 -1
- package/src/sent-rows.ts +1 -1
- package/src/supplied-text.ts +3 -3
- package/src/thinking-drops.ts +2 -0
- package/src/tool-presentation.ts +34 -9
- package/src/tune-out/coordinator.ts +32 -15
- package/src/tune-out/tools.ts +2 -1
- package/src/types/agent.ts +36 -5
- package/src/types/framework.ts +44 -8
- package/src/types/inference-log.ts +1 -0
- package/src/types/trace.ts +66 -4
|
@@ -43,6 +43,7 @@ import { expandCoreTags, isMarkerTagged } from './tags.js';
|
|
|
43
43
|
import { coalesceRejectionReason, EmptyContentError, validateCoalescedContent } from './push-coalescer.js';
|
|
44
44
|
import { isVisiblyEmptyConversion } from './visible-content.js';
|
|
45
45
|
import { logDetail, logValue } from './log-value.js';
|
|
46
|
+
import { jsonKind } from '../jsonrpc-envelope.js';
|
|
46
47
|
import { RefusalLines } from './refusal-lines.js';
|
|
47
48
|
import { CapabilityGrant } from './capability-grant.js';
|
|
48
49
|
import type { InboundSource } from './inbound-source.js';
|
|
@@ -1093,6 +1094,9 @@ interface ChannelRegistryOptions {
|
|
|
1093
1094
|
outcome?: 'failed' | 'unknown';
|
|
1094
1095
|
/** Bounded display of the connector's structured error data, when present. */
|
|
1095
1096
|
detail?: string;
|
|
1097
|
+
/** Set when the turn had no channel and the reason names the channels
|
|
1098
|
+
* its batch came from. */
|
|
1099
|
+
byDesign?: true;
|
|
1096
1100
|
}) => void;
|
|
1097
1101
|
/**
|
|
1098
1102
|
* Announces either a durable subscription-policy admission (before its
|
|
@@ -1135,15 +1139,29 @@ interface ChannelRegistryOptions {
|
|
|
1135
1139
|
* message across ALL channels (markers aside) and misroutes a reply to
|
|
1136
1140
|
* whichever channel last spoke under concurrency (item-3 redux). The
|
|
1137
1141
|
* framework keeps both on the live turn's routing state and exposes them
|
|
1138
|
-
* here. For a
|
|
1139
|
-
*
|
|
1140
|
-
*
|
|
1141
|
-
*
|
|
1142
|
-
*
|
|
1143
|
-
*
|
|
1144
|
-
*
|
|
1142
|
+
* here. For a no-trigger turn it resolves to undefined until the turn's
|
|
1143
|
+
* locus is resolved, so that resolution takes the global fallback, which the
|
|
1144
|
+
* turn then keeps. Consulted AFTER `homeChannelResolver` (a fork's home
|
|
1145
|
+
* always wins), BEFORE the global fallback. Null where the turn deliberately
|
|
1146
|
+
* has no channel, as a heartbeat's turn, or one woken for a channel id two
|
|
1147
|
+
* servers register, given without its server: that takes no fallback
|
|
1148
|
+
* either.
|
|
1145
1149
|
*/
|
|
1146
1150
|
activeChannelResolver?: (agentName: string) => ChannelRef | null | undefined;
|
|
1151
|
+
/**
|
|
1152
|
+
* FrameworkConfig.unpromptedChannel: where a turn that takes it
|
|
1153
|
+
* (takesUnpromptedChannel) and has no home and no channel of its own is
|
|
1154
|
+
* placed, within the process-global fallback, after an all-except hold's
|
|
1155
|
+
* channel (sharedFallback). A name, as entriesNamed reads one.
|
|
1156
|
+
*/
|
|
1157
|
+
unpromptedChannel?: string;
|
|
1158
|
+
/**
|
|
1159
|
+
* Whether the conversation's current turn takes `unpromptedChannel` where it
|
|
1160
|
+
* has no channel of its own: the framework answers true only for a turn
|
|
1161
|
+
* the agent's own timer started, as FrameworkConfig.unpromptedChannel
|
|
1162
|
+
* says. Absent, every turn does.
|
|
1163
|
+
*/
|
|
1164
|
+
takesUnpromptedChannel?: (conversationId: string) => boolean;
|
|
1147
1165
|
/**
|
|
1148
1166
|
* Whether the agent's current turn has no channel of its own — a
|
|
1149
1167
|
* heartbeat's, or one woken for a channel that names none — so it replies
|
|
@@ -1282,6 +1300,9 @@ export class ChannelRegistry {
|
|
|
1282
1300
|
private channellessTurn?: (agentName: string) => boolean;
|
|
1283
1301
|
private takesGlobalFallback?: (conversationId: string) => boolean;
|
|
1284
1302
|
private serverConnecting?: (serverId: string) => boolean;
|
|
1303
|
+
private unpromptedChannel?: string;
|
|
1304
|
+
private unpromptedChannelWarned = false;
|
|
1305
|
+
private takesUnpromptedChannel?: (conversationId: string) => boolean;
|
|
1285
1306
|
private store?: JsStore;
|
|
1286
1307
|
|
|
1287
1308
|
/**
|
|
@@ -1483,6 +1504,11 @@ export class ChannelRegistry {
|
|
|
1483
1504
|
return new ChannelLifecycleError(`this host isn't permitted to open or close the "${serverId}" server's channels`, 'not-permitted');
|
|
1484
1505
|
}
|
|
1485
1506
|
|
|
1507
|
+
private publishingServer(serverId: string): McplServerConnection | undefined {
|
|
1508
|
+
const server = this.serverRegistry.getServer(serverId);
|
|
1509
|
+
return server && CapabilityGrant.of(server).has('channels.publish') ? server : undefined;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1486
1512
|
/**
|
|
1487
1513
|
* Why a send to this server's channels is refused before anything goes
|
|
1488
1514
|
* out, as one clause with what works from there, for a caller that found
|
|
@@ -1677,6 +1703,18 @@ export class ChannelRegistry {
|
|
|
1677
1703
|
this.handleCoalescedIncoming = options?.handleCoalescedIncoming;
|
|
1678
1704
|
this.acceptInbound = options?.acceptInbound;
|
|
1679
1705
|
this.channelTools = CHANNEL_TOOL_DEFINITIONS.map((tool) => {
|
|
1706
|
+
if (tool.name === 'channel_publish') {
|
|
1707
|
+
if (options?.unpromptedChannel === undefined || options.eventGatePresent !== true) return tool;
|
|
1708
|
+
return {
|
|
1709
|
+
...tool,
|
|
1710
|
+
description: tool.description
|
|
1711
|
+
.replace(
|
|
1712
|
+
'otherwise, while a focus holds, the channel it keeps.',
|
|
1713
|
+
'otherwise, while a focus holds, the channel it keeps; otherwise, on a turn your own skip_reply wake or the end of your sleep started (or a turn continuing one, when your plain speech isn\'t routed), the channel this host names for such turns.',
|
|
1714
|
+
)
|
|
1715
|
+
.replace('A turn with no channel of its own, such as', 'A turn with no channel by design, such as'),
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1680
1718
|
if (tool.name !== 'skip_reply' || options?.eventGatePresent === true) return tool;
|
|
1681
1719
|
const schema = tool.inputSchema as {
|
|
1682
1720
|
type: 'object';
|
|
@@ -1709,6 +1747,8 @@ export class ChannelRegistry {
|
|
|
1709
1747
|
this.activeChannelResolver = options?.activeChannelResolver;
|
|
1710
1748
|
this.channellessTurn = options?.channellessTurn;
|
|
1711
1749
|
this.takesGlobalFallback = options?.takesGlobalFallback;
|
|
1750
|
+
this.unpromptedChannel = options?.unpromptedChannel;
|
|
1751
|
+
this.takesUnpromptedChannel = options?.takesUnpromptedChannel;
|
|
1712
1752
|
this.serverConnecting = options?.serverConnecting;
|
|
1713
1753
|
this.store = options?.store;
|
|
1714
1754
|
this.initializeLifecycleStore();
|
|
@@ -2509,6 +2549,28 @@ export class ChannelRegistry {
|
|
|
2509
2549
|
return this.channelTools;
|
|
2510
2550
|
}
|
|
2511
2551
|
|
|
2552
|
+
/**
|
|
2553
|
+
* The refusal of a channel tool's call that gives a parameter its schema
|
|
2554
|
+
* declares a string as something else, before anything changes; absent and
|
|
2555
|
+
* `null` pass, as each tool reads them. A number written past 2^53 has
|
|
2556
|
+
* usually lost its last digits by the time the call is parsed.
|
|
2557
|
+
*/
|
|
2558
|
+
nonStringRefusal(toolName: string, input: unknown): ToolResult | undefined {
|
|
2559
|
+
if (input === null || typeof input !== 'object') return undefined;
|
|
2560
|
+
const properties = this.channelTools.find((tool) => tool.name === toolName)?.inputSchema.properties ?? {};
|
|
2561
|
+
for (const [field, value] of Object.entries(input)) {
|
|
2562
|
+
if ((properties[field] as { type?: unknown } | undefined)?.type !== 'string') continue;
|
|
2563
|
+
if (value === undefined || value === null || typeof value === 'string') continue;
|
|
2564
|
+
return {
|
|
2565
|
+
success: false,
|
|
2566
|
+
error: `Nothing changed: ${toolName}'s ${field} must be a string, and this call gave ${jsonKind(value)}. Give it in quotes`
|
|
2567
|
+
+ (typeof value === 'number' ? ': a number can lose its last digits as the call is read.' : '.'),
|
|
2568
|
+
isError: true,
|
|
2569
|
+
};
|
|
2570
|
+
}
|
|
2571
|
+
return undefined;
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2512
2574
|
/**
|
|
2513
2575
|
* Handle a call to one of the synthesized channel tools.
|
|
2514
2576
|
*
|
|
@@ -2529,6 +2591,8 @@ export class ChannelRegistry {
|
|
|
2529
2591
|
* attempted, so the caller learns where it was delivered. */
|
|
2530
2592
|
seen?: { outcome?: PublishOutcome },
|
|
2531
2593
|
): Promise<ToolResult> {
|
|
2594
|
+
const refused = this.nonStringRefusal(toolName, input);
|
|
2595
|
+
if (refused) return refused;
|
|
2532
2596
|
switch (toolName) {
|
|
2533
2597
|
case 'channel_list':
|
|
2534
2598
|
return this.handleToolList();
|
|
@@ -2627,7 +2691,9 @@ export class ChannelRegistry {
|
|
|
2627
2691
|
// declared it inbound-only, since speech isn't posted there): a conversation
|
|
2628
2692
|
// fork's home channel, else this turn's channel (single trunk agent — item-3
|
|
2629
2693
|
// redux: its locus, else its triggering channel), else the global default
|
|
2630
|
-
// (a no-trigger turn's, but not a heartbeat's, whose turn takes none)
|
|
2694
|
+
// (a no-trigger turn's, but not a heartbeat's, whose turn takes none), with
|
|
2695
|
+
// the configured unprompted channel in it where the turn takes it: the
|
|
2696
|
+
// channel channel_publish naming none publishes to, in every prose mode.
|
|
2631
2697
|
// Without this, a fork
|
|
2632
2698
|
// was advertised the global locus but published somewhere else; and a trunk
|
|
2633
2699
|
// agent was told the wrong channel under concurrency.
|
|
@@ -2638,9 +2704,11 @@ export class ChannelRegistry {
|
|
|
2638
2704
|
// An ephemeral run takes no global fallback (takesGlobalFallback): not the
|
|
2639
2705
|
// all-except hold's channel, not the latest inbound message's channel as
|
|
2640
2706
|
// its outgoing one, nor that message as the one it is replying to.
|
|
2641
|
-
const fallback =
|
|
2707
|
+
const fallback = (): ChannelRef | null => agentName !== undefined
|
|
2708
|
+
? this.globalFallbackFor(agentName, () => this.configuredUnprompted(agentName))
|
|
2709
|
+
: this.sharedFallback();
|
|
2642
2710
|
// A turn that deliberately has no channel (null) takes no fallback.
|
|
2643
|
-
const outgoing = home ?? (active !== undefined ? active : fallback);
|
|
2711
|
+
const outgoing = home ?? (active !== undefined ? active : fallback());
|
|
2644
2712
|
|
|
2645
2713
|
const context: ChannelContext = {};
|
|
2646
2714
|
|
|
@@ -6022,7 +6090,8 @@ export class ChannelRegistry {
|
|
|
6022
6090
|
}
|
|
6023
6091
|
|
|
6024
6092
|
/** Resolve the outbound locus (fork HOME → this-turn's TRIGGERING channel →
|
|
6025
|
-
* process-global default
|
|
6093
|
+
* process-global default, with the configured unprompted channel in it
|
|
6094
|
+
* where the turn takes it), with its server. Public so a multi-segment
|
|
6026
6095
|
* caller can snapshot it ONCE and pin every segment to it via
|
|
6027
6096
|
* routeSpeech's locus. */
|
|
6028
6097
|
resolveLocus(conversationId: string): ChannelRef | null {
|
|
@@ -6030,7 +6099,33 @@ export class ChannelRegistry {
|
|
|
6030
6099
|
if (home) return home;
|
|
6031
6100
|
// A turn that deliberately has no channel (null) takes no fallback.
|
|
6032
6101
|
const active = this.activeChannelResolver?.(conversationId);
|
|
6033
|
-
return active !== undefined
|
|
6102
|
+
return active !== undefined
|
|
6103
|
+
? active
|
|
6104
|
+
: this.globalFallbackFor(conversationId, () => this.configuredUnprompted(conversationId));
|
|
6105
|
+
}
|
|
6106
|
+
|
|
6107
|
+
private configuredUnprompted(conversationId: string | undefined): ChannelRef | null {
|
|
6108
|
+
if (!this.unpromptedChannel) return null;
|
|
6109
|
+
if (conversationId !== undefined && this.takesUnpromptedChannel?.(conversationId) === false) return null;
|
|
6110
|
+
const named = this.entriesNamed(this.unpromptedChannel);
|
|
6111
|
+
if (named.length === 1 && !isInboundOnly(named[0]!)) {
|
|
6112
|
+
this.unpromptedChannelWarned = false;
|
|
6113
|
+
return refOf(named[0]!);
|
|
6114
|
+
}
|
|
6115
|
+
if (!this.unpromptedChannelWarned) {
|
|
6116
|
+
this.unpromptedChannelWarned = true;
|
|
6117
|
+
const why = named.length === 0
|
|
6118
|
+
? 'names no registered channel'
|
|
6119
|
+
: named.length === 1
|
|
6120
|
+
? 'names a channel its server declared inbound-only'
|
|
6121
|
+
: `names ${named.length} registered channels (name one with its server, as "serverId:channelId")`;
|
|
6122
|
+
console.error(
|
|
6123
|
+
`[routing] unpromptedChannel ${logValue(this.unpromptedChannel)} ${why}, so a turn the agent's own timer starts `
|
|
6124
|
+
+ 'takes the fallback it takes without the setting until the setting names a channel it can speak in. '
|
|
6125
|
+
+ 'Not logged again until a turn has used it.',
|
|
6126
|
+
);
|
|
6127
|
+
}
|
|
6128
|
+
return null;
|
|
6034
6129
|
}
|
|
6035
6130
|
|
|
6036
6131
|
/**
|
|
@@ -6039,19 +6134,21 @@ export class ChannelRegistry {
|
|
|
6039
6134
|
* result, so neither the all-except hold's channel nor the latest inbound
|
|
6040
6135
|
* message's is ever its destination.
|
|
6041
6136
|
*/
|
|
6042
|
-
private globalFallbackFor(conversationId: string): ChannelRef | null {
|
|
6043
|
-
return this.takesGlobalFallback?.(conversationId) === false ? null : this.sharedFallback();
|
|
6137
|
+
private globalFallbackFor(conversationId: string, unprompted?: () => ChannelRef | null): ChannelRef | null {
|
|
6138
|
+
return this.takesGlobalFallback?.(conversationId) === false ? null : this.sharedFallback(unprompted);
|
|
6044
6139
|
}
|
|
6045
6140
|
|
|
6046
6141
|
/**
|
|
6047
6142
|
* What every conversation that takes the global fallback falls back to when
|
|
6048
6143
|
* neither a home channel nor its own delivery names one: the all-except
|
|
6049
|
-
* hold's channel while one is open, else
|
|
6050
|
-
* channel
|
|
6051
|
-
*
|
|
6144
|
+
* hold's channel while one is open, else, for a turn's locus, the
|
|
6145
|
+
* configured unprompted channel where the turn takes it (resolveLocus),
|
|
6146
|
+
* else the latest delivered inbound channel under the legacy locus rule,
|
|
6147
|
+
* else none — the prose stays private (architecture channel §3 carve-out,
|
|
6148
|
+
* 2026-10-06).
|
|
6052
6149
|
*/
|
|
6053
|
-
private sharedFallback(): ChannelRef | null {
|
|
6054
|
-
return this.focusChannelResolver?.() ?? this.fallbackLocus();
|
|
6150
|
+
private sharedFallback(unprompted?: () => ChannelRef | null): ChannelRef | null {
|
|
6151
|
+
return this.focusChannelResolver?.() ?? unprompted?.() ?? this.fallbackLocus();
|
|
6055
6152
|
}
|
|
6056
6153
|
|
|
6057
6154
|
private fallbackLocus(): ChannelRef | null {
|
|
@@ -6204,12 +6301,12 @@ export class ChannelRegistry {
|
|
|
6204
6301
|
if (isInboundOnly(entry)) {
|
|
6205
6302
|
return { status: 'failed', destination, reason: inboundOnlyReason(entry), at: at() };
|
|
6206
6303
|
}
|
|
6207
|
-
const server = this.
|
|
6304
|
+
const server = this.publishingServer(entry.serverId);
|
|
6208
6305
|
// §14.1: channels/publish requires channels.publish in the grant. The
|
|
6209
6306
|
// host not sending is the enforcement, here and at the last check, and
|
|
6210
6307
|
// the server's state words the refusal (sendRefusal).
|
|
6211
6308
|
const ungranted = (): PublishOutcome => ({ status: 'failed', destination, reason: this.sendRefusal(entry.serverId), at: at() });
|
|
6212
|
-
if (!server
|
|
6309
|
+
if (!server) return ungranted();
|
|
6213
6310
|
|
|
6214
6311
|
const prevented = (err: unknown): PublishOutcome => ({
|
|
6215
6312
|
status: 'failed', destination, reason: lifecycleClause(err) + refusalRemedy(err), at: at(),
|
|
@@ -6372,12 +6469,15 @@ export class ChannelRegistry {
|
|
|
6372
6469
|
locus: ChannelRef | null,
|
|
6373
6470
|
/** `heldOnFailure`: the caller holds a `failed` delivery's words as a
|
|
6374
6471
|
* draft with its own notice, so no failure marker is written for it.
|
|
6375
|
-
* An `unknown` outcome still gets its marker.
|
|
6376
|
-
|
|
6472
|
+
* An `unknown` outcome still gets its marker. `cameFrom`: with no
|
|
6473
|
+
* locus, the channels the turn's batch came from, each with how the
|
|
6474
|
+
* agent is shown it; the failure's reason names them, and how to reply
|
|
6475
|
+
* in those registered that take speech. */
|
|
6476
|
+
opts?: { heldOnFailure?: boolean; cameFrom?: ReadonlyArray<{ channel: ChannelRef; shown: string }> },
|
|
6377
6477
|
): Promise<PublishOutcome> {
|
|
6378
6478
|
// `logged` is what the log line and the trace say, where it isn't the
|
|
6379
|
-
// agent's reason (the
|
|
6380
|
-
const fail = (channel: ChannelRef | null, reason: string, outcome: PublishOutcome, logged = reason): PublishOutcome => {
|
|
6479
|
+
// agent's reason (the three no-locus reasons below).
|
|
6480
|
+
const fail = (channel: ChannelRef | null, reason: string, outcome: PublishOutcome, logged = reason, byDesign = false): PublishOutcome => {
|
|
6381
6481
|
const status = outcome.status === 'unknown' ? 'unknown' : 'failed';
|
|
6382
6482
|
const channelId = channel?.channelId ?? null;
|
|
6383
6483
|
const detail = outcome.detail !== undefined ? connectorText(outcome.detail) : undefined;
|
|
@@ -6397,17 +6497,19 @@ export class ChannelRegistry {
|
|
|
6397
6497
|
this.onRouteFailure?.({
|
|
6398
6498
|
conversationId, channelId, ...(channel ? { serverId: channel.serverId } : {}),
|
|
6399
6499
|
reason, textLen: text.length, outcome: status, ...(detail !== undefined ? { detail } : {}),
|
|
6500
|
+
...(byDesign ? { byDesign: true as const } : {}),
|
|
6400
6501
|
});
|
|
6401
6502
|
}
|
|
6402
6503
|
return outcome;
|
|
6403
6504
|
};
|
|
6404
6505
|
|
|
6405
|
-
// These
|
|
6506
|
+
// These three reasons are the only ones whose log line and trace keep the
|
|
6406
6507
|
// operator's words rather than the agent's: connectome-host's
|
|
6407
6508
|
// connectome-doctor reads the last [routeSpeech] line for "no locus". Keep
|
|
6408
|
-
// "no locus" in
|
|
6509
|
+
// "no locus" in each logged text. Each names no tool: whether one the
|
|
6409
6510
|
// agent may call could post the words isn't known here, and the notice
|
|
6410
|
-
// calls it a routing or configuration situation
|
|
6511
|
+
// calls it a routing or configuration situation, or for a turn whose
|
|
6512
|
+
// batch named the channels it came from a routing one (byDesign).
|
|
6411
6513
|
// Runtime backstop for JS callers the compiler can't see: an omitted
|
|
6412
6514
|
// locus is a routing bug at the call site, never something to paper over
|
|
6413
6515
|
// with a live re-resolution.
|
|
@@ -6427,6 +6529,32 @@ export class ChannelRegistry {
|
|
|
6427
6529
|
// global inbound ever seen) — the agent was told its prose stays in
|
|
6428
6530
|
// the archive; honor that.
|
|
6429
6531
|
// The locus froze at the turn's start, so this says what the turn had.
|
|
6532
|
+
const from = opts?.cameFrom ?? [];
|
|
6533
|
+
if (from.length > 0) {
|
|
6534
|
+
const list = (names: readonly string[]) =>
|
|
6535
|
+
names.length === 1 ? names[0] : `${names.slice(0, -1).join(', ')} and ${names.at(-1)}`;
|
|
6536
|
+
const closed: string[] = [];
|
|
6537
|
+
const open: Array<{ channel: ChannelRef; shown: string }> = [];
|
|
6538
|
+
for (const came of from) {
|
|
6539
|
+
const entry = this.entryAt(came.channel);
|
|
6540
|
+
if (!entry) closed.push(`${came.shown} isn't registered with this host now, so this host has no channel there to send to`);
|
|
6541
|
+
else if (isInboundOnly(entry)) closed.push(`${came.shown} is ${inboundOnlyReason(entry)}`);
|
|
6542
|
+
else if (!this.publishingServer(entry.serverId)) {
|
|
6543
|
+
const reach = this.serverReach(entry.serverId);
|
|
6544
|
+
closed.push(reach === 'connecting' || reach === 'down'
|
|
6545
|
+
? `${came.shown} can't take a send: ${unreachableClause(entry.serverId, reach)}, so to reply there, send your reply to that channel explicitly once it's back`
|
|
6546
|
+
: `${came.shown} can't take a send: ${this.sendRefusal(entry.serverId)}`);
|
|
6547
|
+
} else open.push(came);
|
|
6548
|
+
}
|
|
6549
|
+
const reply = open.length === 0 ? ''
|
|
6550
|
+
: open.length === from.length
|
|
6551
|
+
? `; to reply ${from.length === 1 ? 'there' : 'in one of them'}, send your reply to that channel explicitly`
|
|
6552
|
+
: `; to reply in ${open.length === 1 ? open[0].shown : `one of ${list(open.map((c) => c.shown))}`}, send your reply to that channel explicitly`;
|
|
6553
|
+
const reason = `this turn came from ${list(from.map((c) => c.shown))}, and no channel was chosen for its reply` +
|
|
6554
|
+
`${reply}${closed.map((c) => `; ${c}`).join('')}`;
|
|
6555
|
+
return fail(null, reason, { status: 'failed', reason, at: Date.now() },
|
|
6556
|
+
"turn has no locus (no channel was chosen for its batch's reply)", true);
|
|
6557
|
+
}
|
|
6430
6558
|
const reason = 'this turn came from no channel, and it had no home or recent channel to reply in; if it should have ' +
|
|
6431
6559
|
'had one, ask whoever runs this host';
|
|
6432
6560
|
return fail(null, reason, { status: 'failed', reason, at: Date.now() },
|
|
@@ -6461,8 +6589,9 @@ export class ChannelRegistry {
|
|
|
6461
6589
|
conversationId: string,
|
|
6462
6590
|
text: string,
|
|
6463
6591
|
locus: ChannelRef | null,
|
|
6592
|
+
opts?: { cameFrom?: ReadonlyArray<{ channel: ChannelRef; shown: string }> },
|
|
6464
6593
|
): Promise<{ delivered: boolean; serverId: string; channelId: string; messageId?: string } | null> {
|
|
6465
|
-
const outcome = await this.deliverSpeech(conversationId, text, locus);
|
|
6594
|
+
const outcome = await this.deliverSpeech(conversationId, text, locus, opts);
|
|
6466
6595
|
if (outcome.status !== 'delivered') return null;
|
|
6467
6596
|
return {
|
|
6468
6597
|
delivered: true,
|
|
@@ -6621,8 +6750,8 @@ export class ChannelRegistry {
|
|
|
6621
6750
|
|
|
6622
6751
|
// The same refusal, in the same words, as plain speech's (sendRefusal),
|
|
6623
6752
|
// whose server state is the reason's: nothing awaits between the two.
|
|
6624
|
-
const server = this.
|
|
6625
|
-
if (!server
|
|
6753
|
+
const server = this.publishingServer(entry.serverId);
|
|
6754
|
+
if (!server) {
|
|
6626
6755
|
if (seen) {
|
|
6627
6756
|
const reach = this.serverReach(entry.serverId);
|
|
6628
6757
|
seen.refusal = reach === 'up' ? 'not-permitted' : `server-${reach}`;
|