@animalabs/agent-framework 0.29.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -8
- package/dist/src/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 +50 -9
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +125 -23
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/mcp-server-core.d.ts.map +1 -1
- package/dist/src/api/mcp-server-core.js +10 -3
- package/dist/src/api/mcp-server-core.js.map +1 -1
- package/dist/src/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/code-execution/isolate/render.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/render.js +7 -6
- package/dist/src/code-execution/isolate/render.js.map +1 -1
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +5 -4
- package/dist/src/code-execution/isolate/scripts.js.map +1 -1
- package/dist/src/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 +806 -118
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +4180 -1121
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +36 -21
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +223 -149
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/recipe-reconcile.d.ts.map +1 -1
- package/dist/src/gate/recipe-reconcile.js +6 -5
- package/dist/src/gate/recipe-reconcile.js.map +1 -1
- package/dist/src/host-presentation.d.ts +4 -0
- package/dist/src/host-presentation.d.ts.map +1 -1
- package/dist/src/host-presentation.js +12 -10
- package/dist/src/host-presentation.js.map +1 -1
- package/dist/src/index.d.ts +6 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/inference-bounds.d.ts +37 -0
- package/dist/src/inference-bounds.d.ts.map +1 -0
- package/dist/src/inference-bounds.js +84 -0
- package/dist/src/inference-bounds.js.map +1 -0
- package/dist/src/line-reader.d.ts +7 -1
- package/dist/src/line-reader.d.ts.map +1 -1
- package/dist/src/line-reader.js +17 -6
- package/dist/src/line-reader.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +84 -9
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +136 -58
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/errors.d.ts +1 -0
- package/dist/src/mcpl/errors.d.ts.map +1 -1
- package/dist/src/mcpl/errors.js +3 -0
- package/dist/src/mcpl/errors.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +2 -2
- package/dist/src/mcpl/inbound-source.js +2 -2
- package/dist/src/mcpl/inference-router.d.ts +11 -1
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.js +65 -9
- package/dist/src/mcpl/inference-router.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts +10 -7
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
- package/dist/src/mcpl/modern-connection.js +85 -74
- package/dist/src/mcpl/modern-connection.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts +1 -1
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +45 -3
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +11 -7
- 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/references.d.ts +1 -2
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +3 -3
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +14 -0
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +26 -22
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts +1 -0
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
- package/dist/src/mcpl/tool-signature.js +16 -11
- package/dist/src/mcpl/tool-signature.js.map +1 -1
- package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
- package/dist/src/mcpl/toolsets-module.js +7 -0
- package/dist/src/mcpl/toolsets-module.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +44 -21
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +283 -64
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +17 -1
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +32 -4
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +189 -25
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
- package/dist/src/modules/discord/discord-js-client.js +24 -16
- package/dist/src/modules/discord/discord-js-client.js.map +1 -1
- package/dist/src/modules/sender-name.d.ts +5 -4
- package/dist/src/modules/sender-name.d.ts.map +1 -1
- package/dist/src/modules/sender-name.js +7 -5
- package/dist/src/modules/sender-name.js.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.d.ts +16 -8
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +33 -12
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +6 -3
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +61 -15
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +9 -1
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +50 -10
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts +11 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +19 -8
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +8 -0
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts +7 -0
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +7 -5
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/notice-tags.d.ts +2 -0
- package/dist/src/notice-tags.d.ts.map +1 -1
- package/dist/src/notice-tags.js +2 -0
- package/dist/src/notice-tags.js.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.js +1 -9
- package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
- package/dist/src/silent-heartbeat.d.ts +4 -2
- package/dist/src/silent-heartbeat.d.ts.map +1 -1
- package/dist/src/silent-heartbeat.js +7 -4
- 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 +2 -0
- package/dist/src/supplied-text.d.ts.map +1 -1
- package/dist/src/supplied-text.js +16 -7
- package/dist/src/supplied-text.js.map +1 -1
- package/dist/src/tool-result-guard.js +1 -1
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +368 -16
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +1224 -136
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts +115 -7
- 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 +27 -1
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +153 -15
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +42 -3
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +140 -3
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/admission/admit.ts +113 -0
- package/src/agent.ts +138 -31
- package/src/api/mcp-server-core.ts +10 -3
- package/src/author-presentation.ts +81 -0
- package/src/code-execution/isolate/render.ts +7 -6
- package/src/code-execution/isolate/scripts.ts +5 -4
- 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 +4344 -1052
- package/src/gate/event-gate.ts +251 -150
- package/src/gate/recipe-reconcile.ts +7 -5
- package/src/host-presentation.ts +14 -10
- package/src/index.ts +16 -2
- package/src/inference-bounds.ts +111 -0
- package/src/line-reader.ts +22 -5
- package/src/mcpl/channel-registry.ts +170 -60
- package/src/mcpl/errors.ts +4 -0
- package/src/mcpl/inbound-source.ts +2 -2
- package/src/mcpl/inference-router.ts +67 -10
- package/src/mcpl/modern-connection.ts +83 -68
- package/src/mcpl/protocol-family.ts +37 -4
- package/src/mcpl/push-coalescer.ts +21 -7
- package/src/mcpl/references.ts +3 -4
- package/src/mcpl/server-connection.ts +30 -21
- package/src/mcpl/tool-signature.ts +18 -11
- package/src/mcpl/toolsets-module.ts +7 -0
- package/src/mcpl/transport.ts +261 -60
- package/src/mcpl/types.ts +18 -1
- package/src/module-registry.ts +178 -25
- package/src/modules/discord/discord-js-client.ts +33 -29
- package/src/modules/sender-name.ts +8 -5
- package/src/modules/workspace/disk-agreement.ts +39 -15
- package/src/modules/workspace/index.ts +65 -14
- package/src/modules/workspace/observe.ts +51 -10
- package/src/modules/workspace/reconcile.ts +22 -8
- package/src/modules/workspace/types.ts +5 -0
- package/src/modules/workspace/watcher.ts +8 -6
- package/src/notice-tags.ts +2 -0
- package/src/recovery/discord-awareness-outbox.ts +1 -11
- package/src/silent-heartbeat.ts +7 -4
- package/src/stored-injections.ts +7 -0
- package/src/supplied-text.ts +17 -6
- package/src/tool-result-guard.ts +1 -1
- package/src/tune-out/coordinator.ts +1374 -141
- package/src/tune-out/tools.ts +166 -13
- package/src/types/agent.ts +30 -1
- package/src/types/framework.ts +157 -15
- package/src/types/module.ts +42 -3
- package/src/types/trace.ts +154 -3
|
@@ -566,11 +566,11 @@ const CHANNEL_TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
566
566
|
},
|
|
567
567
|
{
|
|
568
568
|
name: 'channel_publish',
|
|
569
|
-
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.",
|
|
570
570
|
inputSchema: {
|
|
571
571
|
type: 'object' as const,
|
|
572
572
|
properties: {
|
|
573
|
-
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)" },
|
|
574
574
|
content: { type: 'string', description: 'Text content to publish' },
|
|
575
575
|
text: { type: 'string', description: 'Alias for content' },
|
|
576
576
|
},
|
|
@@ -992,8 +992,35 @@ interface ChannelRegistryOptions {
|
|
|
992
992
|
) => Promise<ChannelIncomingMessageResult & { firstView?: true }>;
|
|
993
993
|
/** Chronicle store used for durable desired channel lifecycle state. */
|
|
994
994
|
store?: JsStore;
|
|
995
|
-
/**
|
|
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
|
+
*/
|
|
996
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;
|
|
997
1024
|
/**
|
|
998
1025
|
* Called when a text-only turn's speech was not delivered to its
|
|
999
1026
|
* conversational locus (`failed`), or may not have been (`unknown`), for
|
|
@@ -1043,7 +1070,7 @@ interface ChannelRegistryOptions {
|
|
|
1043
1070
|
/**
|
|
1044
1071
|
* Resolve a conversation fork's HOME channel from its agent name. Conversation
|
|
1045
1072
|
* forks are spawned bound to a single channel (the framework tracks this in
|
|
1046
|
-
* `
|
|
1073
|
+
* the agent registry's `homeChannel` / `ConversationRouter.channelForAgent`); their
|
|
1047
1074
|
* plain-text speech must route THERE. The process-global `defaultPublishChannel`
|
|
1048
1075
|
* tracks only the most recent inbound message across ALL channels (reaction,
|
|
1049
1076
|
* reaction-removal, edit and deletion markers aside, apart from an edit the
|
|
@@ -1120,6 +1147,10 @@ export type ChannelToolOrigin =
|
|
|
1120
1147
|
* wake due before it, when there is one, which its confirmation names
|
|
1121
1148
|
* (skipReplyNote). */
|
|
1122
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;
|
|
1123
1154
|
};
|
|
1124
1155
|
|
|
1125
1156
|
/** The machine decision sources a module-origin channel_close may record.
|
|
@@ -1154,6 +1185,9 @@ export class ChannelRegistry {
|
|
|
1154
1185
|
op?: 'start' | 'stop',
|
|
1155
1186
|
) => void;
|
|
1156
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;
|
|
1157
1191
|
private onRouteFailure?: ChannelRegistryOptions['onRouteFailure'];
|
|
1158
1192
|
private onChannelAutoOpened?: (info: {
|
|
1159
1193
|
conversationId?: string;
|
|
@@ -1582,6 +1616,9 @@ export class ChannelRegistry {
|
|
|
1582
1616
|
this.emitTraceFn = emitTraceFn;
|
|
1583
1617
|
this.sendTypingFn = options?.sendTypingFn;
|
|
1584
1618
|
this.shouldTriggerInference = options?.shouldTriggerInference;
|
|
1619
|
+
this.latestInboundLocus = options?.latestInboundLocus === true;
|
|
1620
|
+
this.onToolClose = options?.onToolClose;
|
|
1621
|
+
this.focusChannelResolver = options?.focusChannelResolver;
|
|
1585
1622
|
this.onRouteFailure = options?.onRouteFailure;
|
|
1586
1623
|
this.onChannelAutoOpened = options?.onChannelAutoOpened;
|
|
1587
1624
|
this.homeChannelResolver = options?.homeChannelResolver;
|
|
@@ -1823,9 +1860,6 @@ export class ChannelRegistry {
|
|
|
1823
1860
|
// The batch came on the transport open now: a close after this point
|
|
1824
1861
|
// (transportClosed) leaves its traffic nothing to confirm.
|
|
1825
1862
|
const closesAtArrival = this.transportCloses.get(serverId) ?? 0;
|
|
1826
|
-
// And came from a server registered now: one removed since (removeServer)
|
|
1827
|
-
// gives no message the agent replies to (markAccepted).
|
|
1828
|
-
const removalsAtArrival = this.serverRemovals.get(serverId) ?? 0;
|
|
1829
1863
|
|
|
1830
1864
|
for (const message of params.messages) {
|
|
1831
1865
|
if (!message || typeof message.channelId !== 'string' || !message.channelId
|
|
@@ -1931,28 +1965,19 @@ export class ChannelRegistry {
|
|
|
1931
1965
|
continue;
|
|
1932
1966
|
}
|
|
1933
1967
|
|
|
1934
|
-
//
|
|
1935
|
-
//
|
|
1936
|
-
//
|
|
1937
|
-
//
|
|
1938
|
-
//
|
|
1939
|
-
//
|
|
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).
|
|
1940
1976
|
const incomingEntry = this.channels.get(incomingKey)!;
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
// must not retarget the default publish locus or become
|
|
1946
|
-
// `context.incoming`'s reply anchor. An edit the coalescer delivered
|
|
1947
|
-
// as the agent's first view counts as the message it replaced.
|
|
1948
|
-
// A message the coalescer admits after its server was removed sets no
|
|
1949
|
-
// default: the removal cleared that server's message, and a server
|
|
1950
|
-
// that's gone mustn't displace a live one's default.
|
|
1951
|
-
if (!isMarkerTagged(message.tags, firstView) && (this.serverRemovals.get(serverId) ?? 0) === removalsAtArrival) {
|
|
1952
|
-
this.defaultPublishChannel = { serverId, channelId: message.channelId };
|
|
1953
|
-
this.defaultPublishMessageId = message.messageId;
|
|
1954
|
-
this.defaultPublishThreadId = message.threadId;
|
|
1955
|
-
}
|
|
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) => {
|
|
1956
1981
|
// Where the host directs this server's lifecycle, traffic shows the
|
|
1957
1982
|
// transport open only for a channel still decided open or tuned out,
|
|
1958
1983
|
// and, but for one exception, only for a confirmed target: after a
|
|
@@ -1987,7 +2012,7 @@ export class ChannelRegistry {
|
|
|
1987
2012
|
};
|
|
1988
2013
|
if (!coalesced) markAccepted();
|
|
1989
2014
|
|
|
1990
|
-
//
|
|
2015
|
+
// The server-level veto only; the wake gate runs at admission.
|
|
1991
2016
|
let triggerInference = true;
|
|
1992
2017
|
if (this.shouldTriggerInference) {
|
|
1993
2018
|
// The text the agent's context gets, as the push lane's filter reads
|
|
@@ -2289,16 +2314,58 @@ export class ChannelRegistry {
|
|
|
2289
2314
|
// Accessors
|
|
2290
2315
|
// ==========================================================================
|
|
2291
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
|
+
|
|
2292
2348
|
/**
|
|
2293
2349
|
* Get the default publish channel ID: the channel of the most recent
|
|
2294
|
-
* incoming message that isn't a reaction, reaction removal, edit
|
|
2295
|
-
* deletion marker, apart from an edit the coalescer delivers on
|
|
2296
|
-
* 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.
|
|
2297
2353
|
*/
|
|
2298
2354
|
getDefaultPublishChannel(): ChannelRef | null {
|
|
2299
2355
|
return this.defaultPublishChannel;
|
|
2300
2356
|
}
|
|
2301
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
|
+
|
|
2302
2369
|
/**
|
|
2303
2370
|
* RFC-006 §3.2: is `channelId` registered BY `serverId` through a server
|
|
2304
2371
|
* declaration (channels/register, channels/changed) or an authorized open?
|
|
@@ -2439,7 +2506,7 @@ export class ChannelRegistry {
|
|
|
2439
2506
|
|
|
2440
2507
|
/**
|
|
2441
2508
|
* The message the agent is replying to, with its channel: the most recent
|
|
2442
|
-
* 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
|
|
2443
2510
|
* marker, apart from an edit the coalescer delivers on acceptance as the
|
|
2444
2511
|
* agent's first view. Left process-global: per-channel inbound tracking
|
|
2445
2512
|
* (the right messageId for a fork's own channel) is a separate concern from
|
|
@@ -2456,9 +2523,13 @@ export class ChannelRegistry {
|
|
|
2456
2523
|
if (agentName !== undefined && this.activeChannelResolver?.(agentName) === null && !this.homeChannelResolver?.(agentName)) {
|
|
2457
2524
|
return undefined;
|
|
2458
2525
|
}
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
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 };
|
|
2462
2533
|
}
|
|
2463
2534
|
|
|
2464
2535
|
/**
|
|
@@ -2484,16 +2555,19 @@ export class ChannelRegistry {
|
|
|
2484
2555
|
// '' is a name an ephemeral run can have, and resolveLocus reads it too.
|
|
2485
2556
|
const home = agentName !== undefined ? this.homeChannelResolver?.(agentName) : undefined;
|
|
2486
2557
|
const active = agentName !== undefined ? this.activeChannelResolver?.(agentName) : undefined;
|
|
2487
|
-
// An ephemeral run takes no global fallback (takesGlobalFallback):
|
|
2488
|
-
// the latest inbound message's channel as
|
|
2489
|
-
// message as the one it is replying to.
|
|
2490
|
-
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();
|
|
2491
2562
|
// A turn that deliberately has no channel (null) takes no fallback.
|
|
2492
2563
|
const outgoing = home ?? (active !== undefined ? active : fallback);
|
|
2493
2564
|
|
|
2494
2565
|
const context: ChannelContext = {};
|
|
2495
2566
|
|
|
2496
|
-
// 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.
|
|
2497
2571
|
const incoming = this.incomingFor(agentName);
|
|
2498
2572
|
if (incoming && incoming.channel.serverId === serverId) {
|
|
2499
2573
|
context.incoming = {
|
|
@@ -3994,8 +4068,8 @@ export class ChannelRegistry {
|
|
|
3994
4068
|
/**
|
|
3995
4069
|
* How a tuned-out channel's tune-out ends, for the texts that tell an agent
|
|
3996
4070
|
* channel_open doesn't change it: a tune_out cancel, by the name that
|
|
3997
|
-
* picks the channel (nameFor), or, where no name picks it alone, its
|
|
3998
|
-
*
|
|
4071
|
+
* picks the channel (nameFor), or, where no name picks it alone, by its id
|
|
4072
|
+
* and server, since tune_out takes a serverId.
|
|
3999
4073
|
* For channel_close, which refuses a tuned-out channel too, it adds that
|
|
4000
4074
|
* the close comes after that.
|
|
4001
4075
|
*/
|
|
@@ -4008,12 +4082,15 @@ export class ChannelRegistry {
|
|
|
4008
4082
|
|
|
4009
4083
|
private tuneOutEndText(entry: ChannelEntry, tool?: string): string {
|
|
4010
4084
|
const close = tool === 'channel_close';
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
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.' : '.'}`;
|
|
4017
4094
|
}
|
|
4018
4095
|
|
|
4019
4096
|
/**
|
|
@@ -5156,12 +5233,13 @@ export class ChannelRegistry {
|
|
|
5156
5233
|
// Closed already, perhaps only by default: the agent's close is still
|
|
5157
5234
|
// its decision, and is recorded as one (setDesiredState), so a later
|
|
5158
5235
|
// registration's lift of a default leaves the channel closed.
|
|
5236
|
+
const ended = this.onToolClose?.(entry.serverId, input.channelId);
|
|
5159
5237
|
try {
|
|
5160
5238
|
this.setDesiredState(entry.serverId, input.channelId, 'closed', closeSource);
|
|
5161
5239
|
} catch (err) {
|
|
5162
5240
|
return {
|
|
5163
5241
|
success: false,
|
|
5164
|
-
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}` : ''}`,
|
|
5165
5243
|
isError: true,
|
|
5166
5244
|
};
|
|
5167
5245
|
}
|
|
@@ -5204,12 +5282,18 @@ export class ChannelRegistry {
|
|
|
5204
5282
|
const refusal = this.tunedOutRefusal('channel_close', entry);
|
|
5205
5283
|
return isModuleOrigin ? { ...refusal, isError: false } : refusal;
|
|
5206
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);
|
|
5207
5289
|
|
|
5208
|
-
// 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.
|
|
5209
5292
|
try {
|
|
5210
5293
|
this.setDesiredState(entry.serverId, input.channelId, 'closed', closeSource);
|
|
5211
5294
|
} catch (err) {
|
|
5212
|
-
|
|
5295
|
+
const text = this.lifecycleFailureText('close', entry, unrecordedDecision(err));
|
|
5296
|
+
return { success: false, error: ended ? `${text} ${ended}` : text, isError: true };
|
|
5213
5297
|
}
|
|
5214
5298
|
// This channel's typing, not every channel with its id (stopTyping's
|
|
5215
5299
|
// fan-out of a bare name).
|
|
@@ -5686,9 +5770,29 @@ export class ChannelRegistry {
|
|
|
5686
5770
|
return active !== undefined ? active : this.globalFallbackFor(conversationId);
|
|
5687
5771
|
}
|
|
5688
5772
|
|
|
5689
|
-
/**
|
|
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
|
+
*/
|
|
5690
5779
|
private globalFallbackFor(conversationId: string): ChannelRef | null {
|
|
5691
|
-
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;
|
|
5692
5796
|
}
|
|
5693
5797
|
|
|
5694
5798
|
/**
|
|
@@ -6140,17 +6244,20 @@ export class ChannelRegistry {
|
|
|
6140
6244
|
if (input.channelId === '' && tool === 'channel_publish') {
|
|
6141
6245
|
return { success: false, error: `Nothing was posted: channelId is empty. ${findIds}`, isError: true };
|
|
6142
6246
|
}
|
|
6143
|
-
// Resolve channelId:
|
|
6144
|
-
//
|
|
6145
|
-
//
|
|
6146
|
-
//
|
|
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
|
|
6147
6253
|
// (takesGlobalFallback), so it names one.
|
|
6148
6254
|
// A channel named by a string means what it means to every channel tool
|
|
6149
6255
|
// (entriesNamed): one channel, by its id or its server-qualified name,
|
|
6150
6256
|
// and a name two servers' channels answer to is refused with them as
|
|
6151
6257
|
// candidates, never sent to the first.
|
|
6152
6258
|
const caller = origin?.kind === 'agent' ? origin.agentName : undefined;
|
|
6153
|
-
const
|
|
6259
|
+
const inTurn = origin?.kind === 'agent' && origin.inTurn === true;
|
|
6260
|
+
const fallback = caller === undefined ? this.sharedFallback() : inTurn ? null : this.globalFallbackFor(caller);
|
|
6154
6261
|
if (!(input.channelId ?? fallback)) {
|
|
6155
6262
|
return {
|
|
6156
6263
|
success: false,
|
|
@@ -6159,7 +6266,10 @@ export class ChannelRegistry {
|
|
|
6159
6266
|
: caller !== undefined && this.takesGlobalFallback?.(caller) === false
|
|
6160
6267
|
? 'Nothing was posted: name the channel with channelId. A subagent has no default channel, since its plain '
|
|
6161
6268
|
+ 'speech is returned to whoever called it, so channel_publish sends only to a channel it names.'
|
|
6162
|
-
:
|
|
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}`,
|
|
6163
6273
|
isError: true,
|
|
6164
6274
|
};
|
|
6165
6275
|
}
|
package/src/mcpl/errors.ts
CHANGED
|
@@ -68,6 +68,10 @@ export function boundedForLog(text: string, max = 200): string {
|
|
|
68
68
|
return points.length > max ? `${points.slice(0, max).join('')}…` : points.join('');
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
export function hostStoppedWhileHandling(): JsonRpcError {
|
|
72
|
+
return { code: -32000, message: 'the host stopped while handling this request' };
|
|
73
|
+
}
|
|
74
|
+
|
|
71
75
|
/**
|
|
72
76
|
* Create a "feature set not enabled" error.
|
|
73
77
|
* Returned when a server sends a message tagged with a feature set
|
|
@@ -167,8 +167,8 @@ export function conversationKey(source: InboundSource): string | undefined {
|
|
|
167
167
|
* well-formed is dropped (the store doesn't keep it). An `undefined` array
|
|
168
168
|
* element is written as `null`, as JSON writes it: the store's append
|
|
169
169
|
* refuses a body holding one, so no stored copy does. So a value
|
|
170
|
-
* hashes alike however its keys were ordered (
|
|
171
|
-
*
|
|
170
|
+
* hashes alike however its keys were ordered (a store on chronicle before
|
|
171
|
+
* 0.5.1 hands content back with its keys sorted), and alike
|
|
172
172
|
* before and after the store. editMessage writes JSON instead, so an edited
|
|
173
173
|
* copy can keep what an append wouldn't; it has changed anyway.
|
|
174
174
|
*
|
|
@@ -25,8 +25,10 @@ import type {
|
|
|
25
25
|
} from './types.js';
|
|
26
26
|
import type { FeatureSetManager } from './feature-set-manager.js';
|
|
27
27
|
import { McplFeatureSetError } from './feature-set-manager.js';
|
|
28
|
+
import { hostStoppedWhileHandling } from './errors.js';
|
|
28
29
|
import type { HookOrchestrator } from './hook-orchestrator.js';
|
|
29
30
|
import { logValue } from './log-value.js';
|
|
31
|
+
import { streamBounds, wholeCallMs, withHostSetting, type InferenceBounds } from '../inference-bounds.js';
|
|
30
32
|
|
|
31
33
|
// ============================================================================
|
|
32
34
|
// Responder interface
|
|
@@ -41,6 +43,7 @@ interface Responder {
|
|
|
41
43
|
/** Sends a streamed chunk on the transport the request came on, as its
|
|
42
44
|
* answer is sent. */
|
|
43
45
|
sendChunk?(params: InferenceChunkParams): void;
|
|
46
|
+
signal?: AbortSignal;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
// ============================================================================
|
|
@@ -186,6 +189,7 @@ export class InferenceRouter {
|
|
|
186
189
|
* server with no policy, and per server and feature set with one, so a
|
|
187
190
|
* server that keeps asking doesn't flood the operator's log. */
|
|
188
191
|
private loggedRefusals = new Set<string>();
|
|
192
|
+
private running = new Set<{ serverId: string; stop: AbortController; answered: Promise<void> }>();
|
|
189
193
|
|
|
190
194
|
constructor(
|
|
191
195
|
membrane: Membrane,
|
|
@@ -193,6 +197,7 @@ export class InferenceRouter {
|
|
|
193
197
|
featureSetManager: FeatureSetManager,
|
|
194
198
|
routingPolicy: InferenceRoutingPolicy | null,
|
|
195
199
|
emitTraceFn: (event: { type: string; [key: string]: unknown }) => void,
|
|
200
|
+
private readonly inferenceBounds: () => InferenceBounds | null = () => null,
|
|
196
201
|
) {
|
|
197
202
|
this.membrane = membrane;
|
|
198
203
|
this.hookOrchestrator = hookOrchestrator;
|
|
@@ -216,6 +221,31 @@ export class InferenceRouter {
|
|
|
216
221
|
serverId: string,
|
|
217
222
|
params: McplInferenceRequestParams,
|
|
218
223
|
responder: Responder,
|
|
224
|
+
): Promise<void> {
|
|
225
|
+
const stop = new AbortController();
|
|
226
|
+
let ended!: () => void;
|
|
227
|
+
const entry = { serverId, stop, answered: new Promise<void>((resolve) => { ended = resolve; }) };
|
|
228
|
+
this.running.add(entry);
|
|
229
|
+
try {
|
|
230
|
+
await this.answer(serverId, params, responder, stop.signal);
|
|
231
|
+
} finally {
|
|
232
|
+
this.running.delete(entry);
|
|
233
|
+
ended();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
stopAll(): Array<{ label: string; promise: Promise<void> }> {
|
|
238
|
+
return [...this.running].map(({ serverId, stop, answered }) => {
|
|
239
|
+
stop.abort();
|
|
240
|
+
return { label: `inference/request:${serverId}`, promise: answered };
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private async answer(
|
|
245
|
+
serverId: string,
|
|
246
|
+
params: McplInferenceRequestParams,
|
|
247
|
+
responder: Responder,
|
|
248
|
+
stopped: AbortSignal,
|
|
219
249
|
): Promise<void> {
|
|
220
250
|
// 1. Validate feature set
|
|
221
251
|
try {
|
|
@@ -295,6 +325,24 @@ export class InferenceRouter {
|
|
|
295
325
|
return;
|
|
296
326
|
}
|
|
297
327
|
|
|
328
|
+
const signal = responder.signal ? AbortSignal.any([stopped, responder.signal]) : stopped;
|
|
329
|
+
let stopAnswered = false;
|
|
330
|
+
const answerStop = () => {
|
|
331
|
+
if (responder.signal?.aborted) return;
|
|
332
|
+
stopAnswered = true;
|
|
333
|
+
const answer = hostStoppedWhileHandling();
|
|
334
|
+
this.emitTraceFn({ type: 'mcpl:inference_fail', serverId, model, error: answer.message, streamed });
|
|
335
|
+
// A throw from an abort listener would be uncaught.
|
|
336
|
+
try {
|
|
337
|
+
responder.respondError(answer.code, answer.message);
|
|
338
|
+
} catch {}
|
|
339
|
+
};
|
|
340
|
+
stopped.addEventListener('abort', answerStop, { once: true });
|
|
341
|
+
const settled = (): boolean => {
|
|
342
|
+
stopped.removeEventListener('abort', answerStop);
|
|
343
|
+
return stopAnswered;
|
|
344
|
+
};
|
|
345
|
+
|
|
298
346
|
// Emit trace: inference start
|
|
299
347
|
this.emitTraceFn({
|
|
300
348
|
type: 'mcpl:inference_start',
|
|
@@ -319,12 +367,14 @@ export class InferenceRouter {
|
|
|
319
367
|
},
|
|
320
368
|
};
|
|
321
369
|
|
|
370
|
+
if (stopAnswered) return;
|
|
322
371
|
try {
|
|
323
372
|
// 5. Run inference. Both modes end in membrane's response, and one
|
|
324
373
|
// builder makes the result from it.
|
|
325
374
|
const response = chunkRequestId != null
|
|
326
|
-
? await this.streamResponse(request, chunkRequestId, sendChunk
|
|
327
|
-
: await this.membrane.complete(request);
|
|
375
|
+
? await this.streamResponse(request, chunkRequestId, sendChunk!, signal)
|
|
376
|
+
: await this.membrane.complete(request, { ...this.wholeCallBound(request), signal });
|
|
377
|
+
if (settled()) return;
|
|
328
378
|
|
|
329
379
|
// 6. Answer
|
|
330
380
|
const result = resultFrom(response, model);
|
|
@@ -344,22 +394,23 @@ export class InferenceRouter {
|
|
|
344
394
|
|
|
345
395
|
responder.respond(result);
|
|
346
396
|
} catch (err) {
|
|
397
|
+
if (settled()) return;
|
|
347
398
|
// Every ending that isn't a completion lands here, in either mode:
|
|
348
399
|
// membrane's error, a stream that stopped early, a tool call, or a
|
|
349
400
|
// stop FinishReason can't name. The answer is the JSON-RPC error, never
|
|
350
401
|
// a result carrying what arrived.
|
|
402
|
+
const transportClosed = responder.signal?.aborted === true;
|
|
351
403
|
this.emitTraceFn({
|
|
352
404
|
type: 'mcpl:inference_fail',
|
|
353
405
|
serverId,
|
|
354
406
|
model,
|
|
355
|
-
error: err instanceof Error ? err.message : String(err),
|
|
407
|
+
error: transportClosed ? 'the transport the request came on closed' : err instanceof Error ? err.message : String(err),
|
|
356
408
|
streamed,
|
|
357
409
|
});
|
|
358
410
|
|
|
359
|
-
|
|
360
|
-
-32603,
|
|
361
|
-
|
|
362
|
-
);
|
|
411
|
+
if (!transportClosed) {
|
|
412
|
+
responder.respondError(-32603, err instanceof Error ? err.message : 'Inference failed');
|
|
413
|
+
}
|
|
363
414
|
}
|
|
364
415
|
}
|
|
365
416
|
|
|
@@ -407,6 +458,11 @@ export class InferenceRouter {
|
|
|
407
458
|
// Private: Streaming
|
|
408
459
|
// ==========================================================================
|
|
409
460
|
|
|
461
|
+
private wholeCallBound(request: NormalizedRequest): { progressTimeoutMs?: number } {
|
|
462
|
+
const bounds = this.inferenceBounds();
|
|
463
|
+
return bounds === null ? {} : { progressTimeoutMs: wholeCallMs(bounds, request.config.maxTokens) };
|
|
464
|
+
}
|
|
465
|
+
|
|
410
466
|
/**
|
|
411
467
|
* Run the inference as a stream, sending each visible text delta to the
|
|
412
468
|
* server as an inference/chunk, and return membrane's response once the
|
|
@@ -424,8 +480,9 @@ export class InferenceRouter {
|
|
|
424
480
|
request: NormalizedRequest,
|
|
425
481
|
requestId: string | number,
|
|
426
482
|
sendChunk: (params: InferenceChunkParams) => void,
|
|
483
|
+
signal: AbortSignal,
|
|
427
484
|
): Promise<NormalizedResponse> {
|
|
428
|
-
const stream = this.membrane.streamYielding(request);
|
|
485
|
+
const stream = this.membrane.streamYielding(request, { ...streamBounds(this.inferenceBounds(), request.config.maxTokens), signal });
|
|
429
486
|
let index = 0;
|
|
430
487
|
|
|
431
488
|
for await (const event of stream) {
|
|
@@ -437,7 +494,7 @@ export class InferenceRouter {
|
|
|
437
494
|
// XML tool mode the chunks can miss text the content holds: prose
|
|
438
495
|
// naming a tag the stream parser opens a block for, or a thinking
|
|
439
496
|
// block cut off before its close tag.
|
|
440
|
-
if (event.meta.visible) {
|
|
497
|
+
if (event.meta.visible && !signal.aborted) {
|
|
441
498
|
sendChunk({ requestId, index: index++, delta: event.content });
|
|
442
499
|
}
|
|
443
500
|
break;
|
|
@@ -448,7 +505,7 @@ export class InferenceRouter {
|
|
|
448
505
|
case 'error':
|
|
449
506
|
throw event.error;
|
|
450
507
|
case 'aborted':
|
|
451
|
-
throw new Error(`The inference stopped before it finished (${event.reason})`);
|
|
508
|
+
throw new Error(`The inference stopped before it finished (${event.reason}${event.message ? `: ${event.reason === 'timeout' ? withHostSetting(event.message) : event.message}` : ''})`);
|
|
452
509
|
}
|
|
453
510
|
}
|
|
454
511
|
throw new Error('The inference stream ended without a result');
|