@animalabs/agent-framework 0.31.0 → 0.33.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 +204 -14
- package/dist/src/admission/admit.d.ts +18 -0
- package/dist/src/admission/admit.d.ts.map +1 -1
- package/dist/src/admission/admit.js +16 -3
- package/dist/src/admission/admit.js.map +1 -1
- package/dist/src/agent.d.ts +185 -78
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +279 -102
- 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 +50 -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 +4 -20
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/code-execution/isolate/scripts.d.ts +12 -8
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +24 -0
- package/dist/src/code-execution/isolate/scripts.js.map +1 -1
- package/dist/src/config-checks.d.ts +7 -0
- package/dist/src/config-checks.d.ts.map +1 -1
- package/dist/src/config-checks.js +14 -0
- package/dist/src/config-checks.js.map +1 -1
- package/dist/src/context-manager-errors.d.ts +25 -0
- package/dist/src/context-manager-errors.d.ts.map +1 -0
- package/dist/src/context-manager-errors.js +31 -0
- package/dist/src/context-manager-errors.js.map +1 -0
- package/dist/src/context-options.d.ts +67 -0
- package/dist/src/context-options.d.ts.map +1 -0
- package/dist/src/context-options.js +85 -0
- package/dist/src/context-options.js.map +1 -0
- package/dist/src/deferred-import.d.ts +93 -0
- package/dist/src/deferred-import.d.ts.map +1 -0
- package/dist/src/deferred-import.js +177 -0
- package/dist/src/deferred-import.js.map +1 -0
- package/dist/src/dendrite/audience.d.ts +223 -0
- package/dist/src/dendrite/audience.d.ts.map +1 -0
- package/dist/src/dendrite/audience.js +202 -0
- package/dist/src/dendrite/audience.js.map +1 -0
- package/dist/src/dendrite/index.d.ts +5 -3
- package/dist/src/dendrite/index.d.ts.map +1 -1
- package/dist/src/dendrite/index.js +2 -1
- package/dist/src/dendrite/index.js.map +1 -1
- package/dist/src/dendrite/presets.d.ts +10 -5
- package/dist/src/dendrite/presets.d.ts.map +1 -1
- package/dist/src/dendrite/presets.js +17 -5
- package/dist/src/dendrite/presets.js.map +1 -1
- package/dist/src/dendrite/registry.d.ts +62 -7
- package/dist/src/dendrite/registry.d.ts.map +1 -1
- package/dist/src/dendrite/registry.js +179 -10
- package/dist/src/dendrite/registry.js.map +1 -1
- package/dist/src/dendrite/types.d.ts +91 -8
- package/dist/src/dendrite/types.d.ts.map +1 -1
- package/dist/src/foreign-rounds.d.ts +69 -0
- package/dist/src/foreign-rounds.d.ts.map +1 -0
- package/dist/src/foreign-rounds.js +152 -0
- package/dist/src/foreign-rounds.js.map +1 -0
- package/dist/src/framework.d.ts +1977 -820
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +7571 -3457
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +85 -15
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +421 -47
- 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.map +1 -1
- package/dist/src/gate/rule-scope.d.ts +30 -0
- package/dist/src/gate/rule-scope.d.ts.map +1 -0
- package/dist/src/gate/rule-scope.js +41 -0
- package/dist/src/gate/rule-scope.js.map +1 -0
- package/dist/src/gate/suppress-script-worker.d.ts +2 -0
- package/dist/src/gate/suppress-script-worker.d.ts.map +1 -0
- package/dist/src/gate/suppress-script-worker.js +103 -0
- package/dist/src/gate/suppress-script-worker.js.map +1 -0
- package/dist/src/gate/suppress-scripts.d.ts +105 -0
- package/dist/src/gate/suppress-scripts.d.ts.map +1 -0
- package/dist/src/gate/suppress-scripts.js +403 -0
- package/dist/src/gate/suppress-scripts.js.map +1 -0
- package/dist/src/gate/suppression-review.d.ts +143 -0
- package/dist/src/gate/suppression-review.d.ts.map +1 -0
- package/dist/src/gate/suppression-review.js +463 -0
- package/dist/src/gate/suppression-review.js.map +1 -0
- package/dist/src/gate/suppression-stats.d.ts +125 -0
- package/dist/src/gate/suppression-stats.d.ts.map +1 -0
- package/dist/src/gate/suppression-stats.js +214 -0
- package/dist/src/gate/suppression-stats.js.map +1 -0
- package/dist/src/gate/types.d.ts +135 -7
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/gate/wake-onboarding.d.ts +119 -0
- package/dist/src/gate/wake-onboarding.d.ts.map +1 -0
- package/dist/src/gate/wake-onboarding.js +277 -0
- package/dist/src/gate/wake-onboarding.js.map +1 -0
- package/dist/src/held-writes.d.ts +31 -23
- package/dist/src/held-writes.d.ts.map +1 -1
- package/dist/src/held-writes.js +44 -59
- package/dist/src/held-writes.js.map +1 -1
- package/dist/src/index.d.ts +5 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/jsonrpc-envelope.d.ts +69 -0
- package/dist/src/jsonrpc-envelope.d.ts.map +1 -0
- package/dist/src/jsonrpc-envelope.js +66 -0
- package/dist/src/jsonrpc-envelope.js.map +1 -0
- package/dist/src/mcpl/channel-registry.d.ts +131 -15
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +290 -46
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.d.ts +2 -4
- package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
- package/dist/src/mcpl/feature-set-manager.d.ts +2 -4
- package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
- package/dist/src/mcpl/feature-set-manager.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +2 -1
- package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
- package/dist/src/mcpl/inbound-source.js +2 -1
- package/dist/src/mcpl/inbound-source.js.map +1 -1
- package/dist/src/mcpl/inference-router.d.ts +6 -6
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +132 -22
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +105 -15
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts +9 -7
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +13 -10
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +134 -56
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tool-classes.d.ts.map +1 -1
- package/dist/src/mcpl/tool-classes.js +3 -0
- package/dist/src/mcpl/tool-classes.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +8 -2
- 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 +24 -1
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +68 -18
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts +45 -8
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +237 -86
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/history/index.d.ts +59 -0
- package/dist/src/modules/history/index.d.ts.map +1 -1
- package/dist/src/modules/history/index.js +432 -67
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/history/semantic.d.ts +6 -5
- package/dist/src/modules/history/semantic.d.ts.map +1 -1
- package/dist/src/modules/history/semantic.js +15 -22
- package/dist/src/modules/history/semantic.js.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.d.ts +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +1 -1
- package/dist/src/modules/workspace/index.d.ts +28 -7
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +76 -22
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/notice-tags.d.ts +10 -1
- package/dist/src/notice-tags.d.ts.map +1 -1
- package/dist/src/notice-tags.js +10 -1
- package/dist/src/notice-tags.js.map +1 -1
- package/dist/src/operator-log.d.ts +36 -3
- package/dist/src/operator-log.d.ts.map +1 -1
- package/dist/src/operator-log.js +2 -2
- package/dist/src/operator-log.js.map +1 -1
- package/dist/src/presentation.d.ts +746 -0
- package/dist/src/presentation.d.ts.map +1 -0
- package/dist/src/presentation.js +1435 -0
- package/dist/src/presentation.js.map +1 -0
- package/dist/src/prose-drafts.d.ts +15 -11
- package/dist/src/prose-drafts.d.ts.map +1 -1
- package/dist/src/prose-drafts.js +16 -12
- package/dist/src/prose-drafts.js.map +1 -1
- package/dist/src/read-marks.d.ts +192 -0
- package/dist/src/read-marks.d.ts.map +1 -0
- package/dist/src/read-marks.js +242 -0
- package/dist/src/read-marks.js.map +1 -0
- package/dist/src/recovery/offline-branch.d.ts +91 -6
- package/dist/src/recovery/offline-branch.d.ts.map +1 -1
- package/dist/src/recovery/offline-branch.js +273 -29
- package/dist/src/recovery/offline-branch.js.map +1 -1
- package/dist/src/recovery/recover-cli.js +22 -3
- package/dist/src/recovery/recover-cli.js.map +1 -1
- package/dist/src/send-tools.d.ts +5 -9
- package/dist/src/send-tools.d.ts.map +1 -1
- package/dist/src/send-tools.js +40 -7
- package/dist/src/send-tools.js.map +1 -1
- package/dist/src/stored-injections.d.ts +2 -2
- package/dist/src/stored-injections.js +2 -2
- package/dist/src/stored-removal.d.ts +17 -4
- package/dist/src/stored-removal.d.ts.map +1 -1
- package/dist/src/stored-removal.js +29 -5
- package/dist/src/stored-removal.js.map +1 -1
- package/dist/src/tool-result-guard.d.ts +12 -1
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +42 -8
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +1159 -79
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +3709 -363
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts +114 -17
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/tune-out/tools.js +87 -27
- package/dist/src/tune-out/tools.js.map +1 -1
- package/dist/src/tune-out/window-anchor.d.ts +143 -0
- package/dist/src/tune-out/window-anchor.d.ts.map +1 -0
- package/dist/src/tune-out/window-anchor.js +202 -0
- package/dist/src/tune-out/window-anchor.js.map +1 -0
- package/dist/src/types/agent.d.ts +86 -31
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +2 -0
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +43 -21
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +180 -69
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +510 -36
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/admission/admit.ts +32 -3
- package/src/agent.ts +323 -120
- package/src/api/mcp-server-core.ts +54 -17
- package/src/api/server.ts +4 -23
- package/src/code-execution/isolate/scripts.ts +28 -3
- package/src/config-checks.ts +14 -0
- package/src/context-manager-errors.ts +37 -0
- package/src/context-options.ts +96 -0
- package/src/deferred-import.ts +239 -0
- package/src/dendrite/audience.ts +315 -0
- package/src/dendrite/index.ts +14 -2
- package/src/dendrite/presets.ts +18 -6
- package/src/dendrite/registry.ts +209 -12
- package/src/dendrite/types.ts +92 -8
- package/src/foreign-rounds.ts +199 -0
- package/src/framework.ts +7814 -3585
- package/src/gate/event-gate.ts +428 -65
- package/src/gate/index.ts +3 -0
- package/src/gate/rule-scope.ts +45 -0
- package/src/gate/suppress-script-worker.ts +89 -0
- package/src/gate/suppress-scripts.ts +414 -0
- package/src/gate/suppression-review.ts +517 -0
- package/src/gate/suppression-stats.ts +314 -0
- package/src/gate/types.ts +141 -7
- package/src/gate/wake-onboarding.ts +309 -0
- package/src/held-writes.ts +49 -60
- package/src/index.ts +9 -2
- package/src/jsonrpc-envelope.ts +81 -0
- package/src/mcpl/channel-registry.ts +353 -85
- package/src/mcpl/checkpoint-manager.ts +3 -2
- package/src/mcpl/feature-set-manager.ts +2 -1
- package/src/mcpl/inbound-source.ts +2 -1
- package/src/mcpl/inference-router.ts +7 -4
- package/src/mcpl/push-coalescer.ts +193 -34
- package/src/mcpl/push-handler.ts +10 -5
- package/src/mcpl/server-connection.ts +141 -60
- package/src/mcpl/tool-classes.ts +3 -0
- package/src/mcpl/types.ts +8 -2
- package/src/module-registry.ts +73 -17
- package/src/modules/discord/index.ts +229 -81
- package/src/modules/history/index.ts +439 -70
- package/src/modules/history/semantic.ts +15 -21
- package/src/modules/workspace/disk-agreement.ts +1 -1
- package/src/modules/workspace/index.ts +85 -23
- package/src/notice-tags.ts +10 -1
- package/src/operator-log.ts +36 -3
- package/src/presentation.ts +1676 -0
- package/src/prose-drafts.ts +16 -12
- package/src/read-marks.ts +328 -0
- package/src/recovery/offline-branch.ts +346 -32
- package/src/recovery/recover-cli.ts +21 -3
- package/src/send-tools.ts +40 -7
- package/src/stored-injections.ts +2 -2
- package/src/stored-removal.ts +31 -7
- package/src/tool-result-guard.ts +41 -7
- package/src/tune-out/coordinator.ts +4089 -457
- package/src/tune-out/tools.ts +160 -43
- package/src/tune-out/window-anchor.ts +277 -0
- package/src/types/agent.ts +90 -31
- package/src/types/events.ts +2 -0
- package/src/types/framework.ts +43 -21
- package/src/types/module.ts +181 -68
- package/src/types/trace.ts +542 -40
- package/dist/src/reasoning-loss-notice.d.ts +0 -23
- package/dist/src/reasoning-loss-notice.d.ts.map +0 -1
- package/dist/src/reasoning-loss-notice.js +0 -52
- package/dist/src/reasoning-loss-notice.js.map +0 -1
- package/dist/src/reasoning-loss.d.ts +0 -24
- package/dist/src/reasoning-loss.d.ts.map +0 -1
- package/dist/src/reasoning-loss.js +0 -102
- package/dist/src/reasoning-loss.js.map +0 -1
- package/src/reasoning-loss-notice.ts +0 -64
- package/src/reasoning-loss.ts +0 -120
|
@@ -49,6 +49,8 @@ import type { InboundSource } from './inbound-source.js';
|
|
|
49
49
|
import { McplRequestError, type McplServerConnection } from './server-connection.js';
|
|
50
50
|
import { readSkipReplyWake, skipReplyNote, type SkipReplyEarlierWake } from './skip-reply-wake.js';
|
|
51
51
|
import { isProseTargetToken } from './prose-grammar.js';
|
|
52
|
+
import { persistentReaderName, tuneOutHeldWords, tuneOutNoReaderAnswer, tuneOutReachWords, type TuneOutReader } from '../tune-out/tools.js';
|
|
53
|
+
import type { TraceEventInput } from '../types/trace.js';
|
|
52
54
|
|
|
53
55
|
// ============================================================================
|
|
54
56
|
// Typing indicator interval (Discord typing lasts ~10s, so 7s keeps it alive)
|
|
@@ -200,7 +202,7 @@ function substitutedChannel(
|
|
|
200
202
|
*/
|
|
201
203
|
const CHANNEL_LABEL_HISTORY_LOG_ID = 'mcpl/channel-label-history';
|
|
202
204
|
|
|
203
|
-
type DesiredChannelState = 'open' | 'closed' | 'tuned-out';
|
|
205
|
+
export type DesiredChannelState = 'open' | 'closed' | 'tuned-out';
|
|
204
206
|
|
|
205
207
|
/**
|
|
206
208
|
* Parameters of an active tune-out (issue #77), carried on the
|
|
@@ -275,8 +277,8 @@ function desiredStateRecord(item: unknown): {
|
|
|
275
277
|
|
|
276
278
|
/**
|
|
277
279
|
* The desired state `store`'s lifecycle log records for `channelId` on
|
|
278
|
-
* `serverId`, for a reader with no registry yet, as
|
|
279
|
-
*
|
|
280
|
+
* `serverId`, for a reader with no registry yet, as the boot's import of an
|
|
281
|
+
* earlier release's queued writes is, before initializeMcpl: the last decision wins, as
|
|
280
282
|
* the registry's replay reads it (getDesiredState answers once there is a
|
|
281
283
|
* registry). undefined when no decision names it.
|
|
282
284
|
*/
|
|
@@ -730,6 +732,34 @@ function channelOnServer(ref: ChannelRef): string {
|
|
|
730
732
|
return `${quotedValue(ref.channelId)} on server ${quotedValue(ref.serverId)}`;
|
|
731
733
|
}
|
|
732
734
|
|
|
735
|
+
/** A channel_close's lead where its channel's server no longer registers it. */
|
|
736
|
+
function noLongerRegistered(ref: ChannelRef): string {
|
|
737
|
+
return `Server ${quotedValue(ref.serverId)} no longer registers channel ${quotedValue(ref.channelId)}, so no close was recorded or sent.`;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/** What a channel_close's bare name reached besides the channel it closed
|
|
741
|
+
* (ChannelRegistry.focusNamedBare): each by its id and server, which the
|
|
742
|
+
* bare name leaves out, and the channelId and serverId a call closes it
|
|
743
|
+
* with: its own id, which needn't be the name the call gave (a focus on
|
|
744
|
+
* `b:room` beside server b's `room`), so naming the server alone, the
|
|
745
|
+
* name kept, is no channel there. */
|
|
746
|
+
function notClosedText(refs: readonly ChannelRef[]): string {
|
|
747
|
+
const names = refs.map(channelOnServer);
|
|
748
|
+
const call = (ref: ChannelRef) => `channelId ${quotedValue(ref.channelId)} and serverId ${quotedValue(ref.serverId)}`;
|
|
749
|
+
return refs.length === 1
|
|
750
|
+
? `Channel ${names[0]} was not closed: to close it, call again with ${call(refs[0]!)}.`
|
|
751
|
+
: `Channels ${names.slice(0, -1).join(', ')} and ${names.at(-1)} were not closed: to close one, call again with ` +
|
|
752
|
+
`${refs.slice(0, -1).map(call).join(', with ')}, or with ${call(refs.at(-1)!)}.`;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/** A tool result with a sentence added: a success's after its note (or as
|
|
756
|
+
* its note), a failure's after its error. */
|
|
757
|
+
function withSentence(result: ToolResult, sentence: string): ToolResult {
|
|
758
|
+
if (!result.success) return { ...result, error: result.error ? `${result.error} ${sentence}` : sentence };
|
|
759
|
+
const data = (result.data ?? {}) as Record<string, unknown>;
|
|
760
|
+
return { ...result, data: { ...data, note: typeof data.note === 'string' ? `${data.note} ${sentence}` : sentence } };
|
|
761
|
+
}
|
|
762
|
+
|
|
733
763
|
const CHANNEL_ON_SERVER_AT_END = /("(?:[^"\\]|\\.)*") on server ("(?:[^"\\]|\\.)*")$/;
|
|
734
764
|
|
|
735
765
|
/**
|
|
@@ -1011,16 +1041,31 @@ interface ChannelRegistryOptions {
|
|
|
1011
1041
|
* this is ends through the cancel flow (release the backlog, record the
|
|
1012
1042
|
* end) before the lifecycle state flips to closed. A
|
|
1013
1043
|
* tuned-out channel's close is refused before this (its cancel ends it).
|
|
1014
|
-
*
|
|
1015
|
-
* close
|
|
1044
|
+
* `unregistered`: the close names the channel after its server stopped
|
|
1045
|
+
* registering it, so no close is recorded or sent (handleToolClose).
|
|
1046
|
+
* Returns, where a hold excepts the channel, what became of that hold:
|
|
1047
|
+
* a function that says it in a sentence, read when the close's answer is
|
|
1048
|
+
* built, after its transport's round-trip, so the answer says what holds
|
|
1049
|
+
* when it arrives (a release that waited may have run by then). The
|
|
1050
|
+
* answer carries it whatever became of the close: as `note` on a success,
|
|
1051
|
+
* after the error on a failure. Throws, with the answer's text, when the
|
|
1052
|
+
* hold's release fails; one that ended the hold before it failed says so.
|
|
1016
1053
|
*/
|
|
1017
|
-
onToolClose?:
|
|
1054
|
+
onToolClose?: ToolCloseHook;
|
|
1018
1055
|
/**
|
|
1019
1056
|
* The channel unprefixed speech lands in while an all-except hold is
|
|
1020
1057
|
* active (its exception), else null. Consulted after a fork's home and
|
|
1021
1058
|
* the turn's own delivery, before the legacy fallback.
|
|
1022
1059
|
*/
|
|
1023
1060
|
focusChannelResolver?: () => ChannelRef | null;
|
|
1061
|
+
/**
|
|
1062
|
+
* Who reads a tuned-out channel's traffic (the framework's reader: the
|
|
1063
|
+
* persistent subconscious by its name, or reader forks), for the texts
|
|
1064
|
+
* that tell an agent where a tuned-out channel's messages go. Left out,
|
|
1065
|
+
* the texts name the persistent reader by its default name
|
|
1066
|
+
* (persistentReaderName): there is no hold without a reader (0.33.0).
|
|
1067
|
+
*/
|
|
1068
|
+
tuneOutReader?: () => TuneOutReader;
|
|
1024
1069
|
/**
|
|
1025
1070
|
* Called when a text-only turn's speech was not delivered to its
|
|
1026
1071
|
* conversational locus (`failed`), or may not have been (`unknown`), for
|
|
@@ -1099,6 +1144,15 @@ interface ChannelRegistryOptions {
|
|
|
1099
1144
|
* server: that takes no fallback either.
|
|
1100
1145
|
*/
|
|
1101
1146
|
activeChannelResolver?: (agentName: string) => ChannelRef | null | undefined;
|
|
1147
|
+
/**
|
|
1148
|
+
* Whether the agent's current turn has no channel of its own — a
|
|
1149
|
+
* heartbeat's, or one woken for a channel that names none — so it replies
|
|
1150
|
+
* to no incoming message (incomingFor). Left out, a null from
|
|
1151
|
+
* activeChannelResolver means that, as it always did; given, it separates
|
|
1152
|
+
* a turn with nowhere to speak (a locked turn pointed at drafts) from one
|
|
1153
|
+
* with nothing to answer.
|
|
1154
|
+
*/
|
|
1155
|
+
channellessTurn?: (agentName: string) => boolean;
|
|
1102
1156
|
/**
|
|
1103
1157
|
* Whether a conversation takes the process-global fallback, the channel of
|
|
1104
1158
|
* the most recent delivered inbound message: as where its plain speech lands
|
|
@@ -1127,6 +1181,33 @@ interface ChannelRegistryOptions {
|
|
|
1127
1181
|
// ChannelRegistry
|
|
1128
1182
|
// ============================================================================
|
|
1129
1183
|
|
|
1184
|
+
/** A channel_close call's input (ChannelRegistry.handleToolClose). */
|
|
1185
|
+
type ToolCloseInput = {
|
|
1186
|
+
channelId: string;
|
|
1187
|
+
serverId?: string;
|
|
1188
|
+
/** Machine callers name their decision source ('subscription-gc',
|
|
1189
|
+
* 'housekeeping') so the durable record carries honest provenance.
|
|
1190
|
+
* Honored ONLY for module-origin dispatch — tool input is untrusted,
|
|
1191
|
+
* so a model-origin call carrying this field is still recorded as
|
|
1192
|
+
* 'agent-tool' (the resident may close their own channel; they may
|
|
1193
|
+
* not attribute the act to housekeeping). A module-origin call
|
|
1194
|
+
* without it is the module's own close, MODULE_CLOSE_SOURCE. */
|
|
1195
|
+
source?: string;
|
|
1196
|
+
/** A machine close aimed at an explicitly-opened channel is refused
|
|
1197
|
+
* unless the caller certifies an explicit idle lease (a configured
|
|
1198
|
+
* per-channel budget is consent to close at that budget). A tuned-out
|
|
1199
|
+
* channel is refused even so, since its tune-out's cancel is what
|
|
1200
|
+
* ends it. Module origin only, like `source`. */
|
|
1201
|
+
overrideExplicitOpen?: boolean;
|
|
1202
|
+
};
|
|
1203
|
+
|
|
1204
|
+
/** ChannelRegistryOptions.onToolClose: a tool close's end of the all-except hold that excepts its channel. */
|
|
1205
|
+
export type ToolCloseHook = (
|
|
1206
|
+
serverId: string,
|
|
1207
|
+
channelId: string,
|
|
1208
|
+
opts?: { unregistered?: true },
|
|
1209
|
+
) => (() => string) | undefined;
|
|
1210
|
+
|
|
1130
1211
|
/**
|
|
1131
1212
|
* Who is actually making a channel tool call, as established by the
|
|
1132
1213
|
* framework's own dispatch — model-origin calls come through
|
|
@@ -1177,7 +1258,7 @@ export class ChannelRegistry {
|
|
|
1177
1258
|
private serverRegistry: McplServerRegistry;
|
|
1178
1259
|
private featureSetManager: FeatureSetManager;
|
|
1179
1260
|
private pushEventFn: (event: ProcessEvent) => void;
|
|
1180
|
-
private emitTraceFn: (event:
|
|
1261
|
+
private emitTraceFn: (event: TraceEventInput) => void;
|
|
1181
1262
|
private sendTypingFn?: (
|
|
1182
1263
|
serverId: string,
|
|
1183
1264
|
channelId: string,
|
|
@@ -1186,8 +1267,9 @@ export class ChannelRegistry {
|
|
|
1186
1267
|
) => void;
|
|
1187
1268
|
private shouldTriggerInference?: (content: string, metadata: Record<string, unknown>) => boolean;
|
|
1188
1269
|
private readonly latestInboundLocus: boolean;
|
|
1189
|
-
private onToolClose?:
|
|
1270
|
+
private onToolClose?: ToolCloseHook;
|
|
1190
1271
|
private focusChannelResolver?: () => ChannelRef | null;
|
|
1272
|
+
private readonly tuneOutReaderFn?: () => TuneOutReader;
|
|
1191
1273
|
private onRouteFailure?: ChannelRegistryOptions['onRouteFailure'];
|
|
1192
1274
|
private onChannelAutoOpened?: (info: {
|
|
1193
1275
|
conversationId?: string;
|
|
@@ -1197,6 +1279,7 @@ export class ChannelRegistry {
|
|
|
1197
1279
|
}) => void;
|
|
1198
1280
|
private homeChannelResolver?: (agentName: string) => ChannelRef | undefined;
|
|
1199
1281
|
private activeChannelResolver?: (agentName: string) => ChannelRef | null | undefined;
|
|
1282
|
+
private channellessTurn?: (agentName: string) => boolean;
|
|
1200
1283
|
private takesGlobalFallback?: (conversationId: string) => boolean;
|
|
1201
1284
|
private serverConnecting?: (serverId: string) => boolean;
|
|
1202
1285
|
private store?: JsStore;
|
|
@@ -1478,17 +1561,17 @@ export class ChannelRegistry {
|
|
|
1478
1561
|
(reach === 'gone' ? ' If you need the server back, ask whoever runs this host.' : '') +
|
|
1479
1562
|
(failure === undefined ? ` ${withStop(lifecycleClause(err))}` : '');
|
|
1480
1563
|
}
|
|
1481
|
-
// A tune-out
|
|
1564
|
+
// A tune-out holds the channel's messages for its reader, if any, and
|
|
1482
1565
|
// channel_open refuses a tuned-out channel.
|
|
1483
1566
|
if (action === 'open' ? now !== 'open' : now !== 'closed') {
|
|
1484
1567
|
if (action === 'open' && now === 'tuned-out') {
|
|
1485
|
-
return `Channel ${channel} was tuned out meanwhile: its messages
|
|
1568
|
+
return `Channel ${channel} was tuned out meanwhile: its messages ${this.tuneOutHeld()}, and channel_open doesn't ` +
|
|
1486
1569
|
`change that. ${this.tuneOutEndText(current)}`;
|
|
1487
1570
|
}
|
|
1488
1571
|
// channel_close refuses a tuned-out channel too, so calling it again
|
|
1489
1572
|
// isn't the way: the tune-out's cancel is, and it delivers the backlog.
|
|
1490
1573
|
if (action === 'close' && now === 'tuned-out') {
|
|
1491
|
-
return `Channel ${channel} was tuned out meanwhile: its messages
|
|
1574
|
+
return `Channel ${channel} was tuned out meanwhile: its messages ${this.tuneOutHeld()}, and channel_close doesn't ` +
|
|
1492
1575
|
`change that. ${this.tuneOutEndText(current, 'channel_close')}`;
|
|
1493
1576
|
}
|
|
1494
1577
|
// The later decision's own work settles the transport, and its call
|
|
@@ -1581,7 +1664,7 @@ export class ChannelRegistry {
|
|
|
1581
1664
|
serverRegistry: McplServerRegistry,
|
|
1582
1665
|
featureSetManager: FeatureSetManager,
|
|
1583
1666
|
pushEventFn: (event: ProcessEvent) => void,
|
|
1584
|
-
emitTraceFn: (event:
|
|
1667
|
+
emitTraceFn: (event: TraceEventInput) => void,
|
|
1585
1668
|
options?: ChannelRegistryOptions & {
|
|
1586
1669
|
sendTypingFn?: (
|
|
1587
1670
|
serverId: string,
|
|
@@ -1619,10 +1702,12 @@ export class ChannelRegistry {
|
|
|
1619
1702
|
this.latestInboundLocus = options?.latestInboundLocus === true;
|
|
1620
1703
|
this.onToolClose = options?.onToolClose;
|
|
1621
1704
|
this.focusChannelResolver = options?.focusChannelResolver;
|
|
1705
|
+
this.tuneOutReaderFn = options?.tuneOutReader;
|
|
1622
1706
|
this.onRouteFailure = options?.onRouteFailure;
|
|
1623
1707
|
this.onChannelAutoOpened = options?.onChannelAutoOpened;
|
|
1624
1708
|
this.homeChannelResolver = options?.homeChannelResolver;
|
|
1625
1709
|
this.activeChannelResolver = options?.activeChannelResolver;
|
|
1710
|
+
this.channellessTurn = options?.channellessTurn;
|
|
1626
1711
|
this.takesGlobalFallback = options?.takesGlobalFallback;
|
|
1627
1712
|
this.serverConnecting = options?.serverConnecting;
|
|
1628
1713
|
this.store = options?.store;
|
|
@@ -2303,8 +2388,9 @@ export class ChannelRegistry {
|
|
|
2303
2388
|
}
|
|
2304
2389
|
|
|
2305
2390
|
/** The explicit 'stop' for a channel whose typing ended, while its
|
|
2306
|
-
* server still registers it. */
|
|
2391
|
+
* server still registers it and holds the grant its start needed. */
|
|
2307
2392
|
private sendTypingStop(ref: ChannelRef): void {
|
|
2393
|
+
if (!this.typingGranted(ref)) return;
|
|
2308
2394
|
if (this.sendTypingFn && this.entryAt(ref)) {
|
|
2309
2395
|
this.sendTypingFn(ref.serverId, ref.channelId, this.typingMetadata.get(destinationKey(ref)), 'stop');
|
|
2310
2396
|
}
|
|
@@ -2456,15 +2542,7 @@ export class ChannelRegistry {
|
|
|
2456
2542
|
}, origin?.kind === 'agent' ? origin.own : undefined);
|
|
2457
2543
|
|
|
2458
2544
|
case 'channel_close':
|
|
2459
|
-
return this.handleToolClose(
|
|
2460
|
-
input as {
|
|
2461
|
-
channelId: string;
|
|
2462
|
-
serverId?: string;
|
|
2463
|
-
source?: string;
|
|
2464
|
-
overrideExplicitOpen?: boolean;
|
|
2465
|
-
},
|
|
2466
|
-
origin,
|
|
2467
|
-
);
|
|
2545
|
+
return this.handleToolClose(input as ToolCloseInput, origin);
|
|
2468
2546
|
|
|
2469
2547
|
case 'channel_decline':
|
|
2470
2548
|
return this.handleToolDecline(input as {
|
|
@@ -2520,7 +2598,9 @@ export class ChannelRegistry {
|
|
|
2520
2598
|
* its home.
|
|
2521
2599
|
*/
|
|
2522
2600
|
incomingFor(agentName?: string): { channel: ChannelRef; messageId: string; threadId?: string } | undefined {
|
|
2523
|
-
|
|
2601
|
+
const channelless = agentName === undefined ? false
|
|
2602
|
+
: this.channellessTurn ? this.channellessTurn(agentName) : this.activeChannelResolver?.(agentName) === null;
|
|
2603
|
+
if (channelless && !this.homeChannelResolver?.(agentName!)) {
|
|
2524
2604
|
return undefined;
|
|
2525
2605
|
}
|
|
2526
2606
|
// The latest delivered inbound itself, not the speech fallback
|
|
@@ -3159,7 +3239,7 @@ export class ChannelRegistry {
|
|
|
3159
3239
|
|
|
3160
3240
|
/**
|
|
3161
3241
|
* Make a tuned-out channel's transport match its tune-out, which keeps it
|
|
3162
|
-
* open so its traffic reaches
|
|
3242
|
+
* open so its traffic reaches its reader, or is held (TuneOutParams). The
|
|
3163
3243
|
* tune-out is recorded first (enterTuneOut), and this writes no decision,
|
|
3164
3244
|
* so the tune-out stands whatever becomes of the open. Lifecycle work
|
|
3165
3245
|
* queued for the channel goes first, until none is left, since each
|
|
@@ -3171,8 +3251,8 @@ export class ChannelRegistry {
|
|
|
3171
3251
|
* tune-out as open), and the answer waits for the work queued after it
|
|
3172
3252
|
* too, such as a registration that came back, and comes from the state
|
|
3173
3253
|
* that work leaves. Returns nothing where the transport is open, or, for
|
|
3174
|
-
* tune_out's answer, why the channel's messages don't or may not reach
|
|
3175
|
-
*
|
|
3254
|
+
* tune_out's answer, why the channel's messages don't or may not reach its
|
|
3255
|
+
* reader yet, or be received, and when they will, or that the tune-out isn't in
|
|
3176
3256
|
* effect any more. A call while this tune-out's open is in flight, as a
|
|
3177
3257
|
* repeated tune_out makes, gets that open's answer rather than send
|
|
3178
3258
|
* another.
|
|
@@ -3245,9 +3325,10 @@ export class ChannelRegistry {
|
|
|
3245
3325
|
// that took effect would have its traffic diverted. `again` where calling
|
|
3246
3326
|
// tune_out again tries the open now: the channel is registered here and
|
|
3247
3327
|
// its server up.
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
`
|
|
3328
|
+
// Where no reader runs, each answer is that alone (tuneOutNoReaderAnswer).
|
|
3329
|
+
const notYet = (later: string, why: string, again = false): string => tuneOutNoReaderAnswer(this.tuneOutReaderNow()) ?? withStop(
|
|
3330
|
+
`The tune-out is recorded, but its messages ${this.tuneOutReach('may not reach')} until this host opens the ` +
|
|
3331
|
+
`channel, which it does ${later}${again ? ', or now if you call tune_out again' : ''}; ${why}`,
|
|
3251
3332
|
);
|
|
3252
3333
|
const nextRegistration = `when the "${serverId}" server next registers this channel, if this host may open and ` +
|
|
3253
3334
|
'close its channels then';
|
|
@@ -3279,18 +3360,18 @@ export class ChannelRegistry {
|
|
|
3279
3360
|
// established.
|
|
3280
3361
|
const reach = this.serverReach(serverId);
|
|
3281
3362
|
if (reach !== 'up') {
|
|
3282
|
-
return withStop(
|
|
3283
|
-
`The tune-out is recorded, but its messages don't reach
|
|
3284
|
-
`connected and the channel is open; it can't be opened now: ${unreachableClause(serverId, reach)}` +
|
|
3363
|
+
return tuneOutNoReaderAnswer(this.tuneOutReaderNow()) ?? withStop(
|
|
3364
|
+
`The tune-out is recorded, but its messages ${this.tuneOutReach("don't reach")} until the "${serverId}" server ` +
|
|
3365
|
+
`is connected and the channel is open; it can't be opened now: ${unreachableClause(serverId, reach)}` +
|
|
3285
3366
|
(reach === 'gone' ? unreachableRemedy(reach) : ''),
|
|
3286
3367
|
);
|
|
3287
3368
|
}
|
|
3288
3369
|
const transportOpen = this.tuneOutReadsOpen(entry);
|
|
3289
3370
|
if (!this.directsLifecycle(serverId)) {
|
|
3290
3371
|
return transportOpen ? undefined
|
|
3291
|
-
: `The tune-out is recorded, but ${this.lifecycleRefusal(serverId).message}, so its messages
|
|
3292
|
-
'
|
|
3293
|
-
'this host to open it, ask whoever runs this host.';
|
|
3372
|
+
: tuneOutNoReaderAnswer(this.tuneOutReaderNow()) ?? `The tune-out is recorded, but ${this.lifecycleRefusal(serverId).message}, so its messages ` +
|
|
3373
|
+
`${this.tuneOutReach('reach')} only as that server sends them, and calling tune_out again won't change ` +
|
|
3374
|
+
'that. If you need this host to open it, ask whoever runs this host.';
|
|
3294
3375
|
}
|
|
3295
3376
|
if (!entry) return missing;
|
|
3296
3377
|
// A flag a close that may have taken effect left gets an open.
|
|
@@ -3957,10 +4038,9 @@ export class ChannelRegistry {
|
|
|
3957
4038
|
* (typing timer lifecycle is still managed for when the callback is wired).
|
|
3958
4039
|
*/
|
|
3959
4040
|
private sendTypingNotification(ref: ChannelRef): void {
|
|
3960
|
-
//
|
|
3961
|
-
//
|
|
3962
|
-
|
|
3963
|
-
if (!CapabilityGrant.of(this.serverRegistry.getServer(ref.serverId)).has('channels.typing')) return;
|
|
4041
|
+
// Silent skip — a typing indicator is cosmetic and per-7s, so a
|
|
4042
|
+
// diagnostic per tick would be noise.
|
|
4043
|
+
if (!this.typingGranted(ref)) return;
|
|
3964
4044
|
if (this.sendTypingFn) {
|
|
3965
4045
|
this.sendTypingFn(ref.serverId, ref.channelId, this.typingMetadata.get(destinationKey(ref)));
|
|
3966
4046
|
}
|
|
@@ -3968,6 +4048,11 @@ export class ChannelRegistry {
|
|
|
3968
4048
|
// sendTyping method, wire it here directly instead of using a callback.
|
|
3969
4049
|
}
|
|
3970
4050
|
|
|
4051
|
+
/** §14.1: channels/typing, start and stop alike, requires channels.typing in the grant. */
|
|
4052
|
+
private typingGranted(ref: ChannelRef): boolean {
|
|
4053
|
+
return CapabilityGrant.of(this.serverRegistry.getServer(ref.serverId)).has('channels.typing');
|
|
4054
|
+
}
|
|
4055
|
+
|
|
3971
4056
|
// ==========================================================================
|
|
3972
4057
|
// Private: Channel Lookup
|
|
3973
4058
|
// ==========================================================================
|
|
@@ -4048,6 +4133,21 @@ export class ChannelRegistry {
|
|
|
4048
4133
|
return channelOnServer(ref);
|
|
4049
4134
|
}
|
|
4050
4135
|
|
|
4136
|
+
/** The reader of a tuned-out channel's traffic, as the host says (tuneOutReader), or the default persistent reader. */
|
|
4137
|
+
private tuneOutReaderNow(): TuneOutReader {
|
|
4138
|
+
return this.tuneOutReaderFn?.() ?? { kind: 'persistent', name: persistentReaderName({}) };
|
|
4139
|
+
}
|
|
4140
|
+
|
|
4141
|
+
/** What a tuned-out channel's messages are, after "its messages" (tuneOutHeldWords). */
|
|
4142
|
+
private tuneOutHeld(): string {
|
|
4143
|
+
return tuneOutHeldWords(this.tuneOutReaderNow());
|
|
4144
|
+
}
|
|
4145
|
+
|
|
4146
|
+
/** Whether a tuned-out channel's messages get to its reader, after "its messages" (tuneOutReachWords). */
|
|
4147
|
+
private tuneOutReach(form: Parameters<typeof tuneOutReachWords>[1]): string {
|
|
4148
|
+
return tuneOutReachWords(this.tuneOutReaderNow(), form);
|
|
4149
|
+
}
|
|
4150
|
+
|
|
4051
4151
|
/**
|
|
4052
4152
|
* A tool's refusal of a tuned-out channel, whose decision is the tune-out:
|
|
4053
4153
|
* its cancel is what ends it (tuneOutEndText), and a decision recorded over
|
|
@@ -4059,7 +4159,7 @@ export class ChannelRegistry {
|
|
|
4059
4159
|
success: false,
|
|
4060
4160
|
isError: true,
|
|
4061
4161
|
error:
|
|
4062
|
-
`Channel ${this.quotedShown(refOf(entry))} is tuned out: its messages
|
|
4162
|
+
`Channel ${this.quotedShown(refOf(entry))} is tuned out: its messages ${this.tuneOutHeld()}, and ${tool} ` +
|
|
4063
4163
|
`doesn't change that. ${this.tuneOutEndText(entry, tool)}`,
|
|
4064
4164
|
data: { refusal: 'tuned-out', channelId: entry.descriptor.id, serverId: entry.serverId },
|
|
4065
4165
|
};
|
|
@@ -4080,7 +4180,7 @@ export class ChannelRegistry {
|
|
|
4080
4180
|
return entry ? this.tuneOutEndText(entry) : undefined;
|
|
4081
4181
|
}
|
|
4082
4182
|
|
|
4083
|
-
private tuneOutEndText(entry: ChannelEntry, tool?: string): string {
|
|
4183
|
+
private tuneOutEndText(entry: ChannelEntry, tool?: string, closeWith?: string): string {
|
|
4084
4184
|
const close = tool === 'channel_close';
|
|
4085
4185
|
// A name is written as a literal tune_out's channelId takes back
|
|
4086
4186
|
// (quotedValue, main's #852); one no name picks alone, by its id and
|
|
@@ -4090,7 +4190,7 @@ export class ChannelRegistry {
|
|
|
4090
4190
|
? `channelId ${quotedValue(name)}`
|
|
4091
4191
|
: `channelId ${quotedValue(entry.descriptor.id)}, serverId ${JSON.stringify(entry.serverId)}`;
|
|
4092
4192
|
return `Call tune_out with ${names} and mode "cancel" for it to resume normal ` +
|
|
4093
|
-
`attention; cancelling delivers the diverted backlog${close ?
|
|
4193
|
+
`attention; cancelling delivers the diverted backlog${close ? `, and channel_close${closeWith ? ` with ${closeWith}` : ''} closes it after that.` : '.'}`;
|
|
4094
4194
|
}
|
|
4095
4195
|
|
|
4096
4196
|
/**
|
|
@@ -5170,36 +5270,38 @@ export class ChannelRegistry {
|
|
|
5170
5270
|
}
|
|
5171
5271
|
}
|
|
5172
5272
|
|
|
5173
|
-
private async handleToolClose(
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
* ends it. Module origin only, like `source`. */
|
|
5190
|
-
overrideExplicitOpen?: boolean;
|
|
5191
|
-
},
|
|
5192
|
-
origin?: ChannelToolOrigin,
|
|
5193
|
-
): Promise<ToolResult> {
|
|
5194
|
-
const resolved = this.resolveToolChannelEntry(input.channelId, input.serverId, origin?.kind === 'agent' ? origin.own : undefined);
|
|
5195
|
-
const entry = resolved.entry;
|
|
5196
|
-
if (!entry) {
|
|
5197
|
-
return {
|
|
5198
|
-
success: false,
|
|
5199
|
-
error: resolved.error,
|
|
5200
|
-
isError: true,
|
|
5201
|
-
};
|
|
5273
|
+
private async handleToolClose(input: ToolCloseInput, origin?: ChannelToolOrigin): Promise<ToolResult> {
|
|
5274
|
+
const own = origin?.kind === 'agent' ? origin.own : undefined;
|
|
5275
|
+
// An agent's bare name for the channel the all-except hold excepts means
|
|
5276
|
+
// that channel (focusNamedBare), whether its server still registers it
|
|
5277
|
+
// or not: another server's channel with the id, which the name would
|
|
5278
|
+
// reach otherwise, isn't closed, and the answer says so. A module's
|
|
5279
|
+
// close resolves its name as any other: housekeeping doesn't end the
|
|
5280
|
+
// resident's focus.
|
|
5281
|
+
const focused = origin?.kind === 'module' ? undefined : this.focusNamedBare(input.channelId, input.serverId, own);
|
|
5282
|
+
if (focused && 'refusal' in focused) return { success: false, error: focused.refusal, isError: true };
|
|
5283
|
+
if (focused) {
|
|
5284
|
+
const entry = this.entryAt(focused.focus);
|
|
5285
|
+
const result = entry
|
|
5286
|
+
? await this.closeToolEntry(entry, input, origin)
|
|
5287
|
+
: this.unregisteredFocusClose(focused.focus) ?? { success: false, error: noLongerRegistered(focused.focus), isError: true };
|
|
5288
|
+
return focused.notClosed ? withSentence(result, focused.notClosed) : result;
|
|
5202
5289
|
}
|
|
5290
|
+
const resolved = this.resolveToolChannelEntry(input.channelId, input.serverId, own);
|
|
5291
|
+
if (resolved.entry) return this.closeToolEntry(resolved.entry, input, origin);
|
|
5292
|
+
// The name, with its serverId or as the turn's own channel, means the
|
|
5293
|
+
// channel the all-except hold excepts, which its server no longer
|
|
5294
|
+
// registers (unregisteredFocusNamed). A module's close of such a name is
|
|
5295
|
+
// refused as any name no channel answers to.
|
|
5296
|
+
const gone = origin?.kind === 'module' ? undefined : this.unregisteredFocusNamed(input.channelId, input.serverId, own);
|
|
5297
|
+
return (gone && this.unregisteredFocusClose(gone)) || { success: false, error: resolved.error, isError: true };
|
|
5298
|
+
}
|
|
5299
|
+
|
|
5300
|
+
/**
|
|
5301
|
+
* A tool close of the channel `entry`, which its name resolved to
|
|
5302
|
+
* (handleToolClose).
|
|
5303
|
+
*/
|
|
5304
|
+
private async closeToolEntry(entry: ChannelEntry, input: ToolCloseInput, origin?: ChannelToolOrigin): Promise<ToolResult> {
|
|
5203
5305
|
// From here, the channel's own id, whichever name the call gave for it
|
|
5204
5306
|
// (a server-qualified one included).
|
|
5205
5307
|
input = { ...input, channelId: entry.descriptor.id };
|
|
@@ -5233,19 +5335,21 @@ export class ChannelRegistry {
|
|
|
5233
5335
|
// Closed already, perhaps only by default: the agent's close is still
|
|
5234
5336
|
// its decision, and is recorded as one (setDesiredState), so a later
|
|
5235
5337
|
// registration's lift of a default leaves the channel closed.
|
|
5236
|
-
const
|
|
5338
|
+
const hook = this.toolCloseHook(refOf(entry));
|
|
5339
|
+
if ('error' in hook) return { success: false, error: hook.error, isError: true };
|
|
5340
|
+
const ended = hook.text;
|
|
5237
5341
|
try {
|
|
5238
5342
|
this.setDesiredState(entry.serverId, input.channelId, 'closed', closeSource);
|
|
5239
5343
|
} catch (err) {
|
|
5240
5344
|
return {
|
|
5241
5345
|
success: false,
|
|
5242
|
-
error: `Channel ${shown} is already closed, but recording the decision to close it failed: ${(err as Error).message}${ended ? ` ${ended}` : ''}`,
|
|
5346
|
+
error: `Channel ${shown} is already closed, but recording the decision to close it failed: ${(err as Error).message}${ended ? ` ${ended()}` : ''}`,
|
|
5243
5347
|
isError: true,
|
|
5244
5348
|
};
|
|
5245
5349
|
}
|
|
5246
5350
|
return {
|
|
5247
5351
|
success: true,
|
|
5248
|
-
data: { channelId: input.channelId, serverId: entry.serverId, status: 'already closed' },
|
|
5352
|
+
data: { channelId: input.channelId, serverId: entry.serverId, status: 'already closed', ...(ended ? { note: ended() } : {}) },
|
|
5249
5353
|
};
|
|
5250
5354
|
}
|
|
5251
5355
|
|
|
@@ -5285,15 +5389,22 @@ export class ChannelRegistry {
|
|
|
5285
5389
|
// A hold around this channel — the all-except scope it is the excepted
|
|
5286
5390
|
// channel of — ends through the cancel flow: backlog released, end
|
|
5287
5391
|
// recorded, never by overwriting state (#232 gap 9).
|
|
5288
|
-
const
|
|
5392
|
+
const hook = this.toolCloseHook(refOf(entry));
|
|
5393
|
+
if ('error' in hook) return { success: false, error: hook.error, isError: true };
|
|
5394
|
+
const ended = hook.text;
|
|
5289
5395
|
|
|
5290
5396
|
// A decision the log refused changed nothing for the channel, and the
|
|
5291
|
-
// answer says so, and what the hook ended all the same.
|
|
5397
|
+
// answer says so, and what the hook ended all the same. Every answer
|
|
5398
|
+
// carries the hook's sentence, a success's as `note`: whether the close
|
|
5399
|
+
// went through says nothing of the focus, whose end may wait (an
|
|
5400
|
+
// operator's change to the store in progress). The sentence is read as
|
|
5401
|
+
// the answer is built, after the transport's round-trip, during which a
|
|
5402
|
+
// release that waited may have run.
|
|
5403
|
+
const withEnded = (text: string): string => (ended ? `${text} ${ended()}` : text);
|
|
5292
5404
|
try {
|
|
5293
5405
|
this.setDesiredState(entry.serverId, input.channelId, 'closed', closeSource);
|
|
5294
5406
|
} catch (err) {
|
|
5295
|
-
|
|
5296
|
-
return { success: false, error: ended ? `${text} ${ended}` : text, isError: true };
|
|
5407
|
+
return { success: false, error: withEnded(this.lifecycleFailureText('close', entry, unrecordedDecision(err))), isError: true };
|
|
5297
5408
|
}
|
|
5298
5409
|
// This channel's typing, not every channel with its id (stopTyping's
|
|
5299
5410
|
// fan-out of a bare name).
|
|
@@ -5302,7 +5413,7 @@ export class ChannelRegistry {
|
|
|
5302
5413
|
// Refused now, the close stays recorded for the server's next
|
|
5303
5414
|
// registration, and the text says so.
|
|
5304
5415
|
if (!this.directsLifecycle(entry.serverId)) {
|
|
5305
|
-
return { success: false, error: this.lifecycleFailureText('close', entry, this.lifecycleRefusal(entry.serverId)), isError: true };
|
|
5416
|
+
return { success: false, error: withEnded(this.lifecycleFailureText('close', entry, this.lifecycleRefusal(entry.serverId))), isError: true };
|
|
5306
5417
|
}
|
|
5307
5418
|
|
|
5308
5419
|
try {
|
|
@@ -5311,13 +5422,165 @@ export class ChannelRegistry {
|
|
|
5311
5422
|
if (applied.open || now !== 'closed') throw new ChannelLifecycleError(supersededClause('close', now), 'superseded');
|
|
5312
5423
|
return {
|
|
5313
5424
|
success: true,
|
|
5314
|
-
data: { channelId: input.channelId, serverId: entry.serverId, status: 'closed' },
|
|
5425
|
+
data: { channelId: input.channelId, serverId: entry.serverId, status: 'closed', ...(ended ? { note: ended() } : {}) },
|
|
5315
5426
|
};
|
|
5316
5427
|
} catch (err) {
|
|
5317
|
-
return { success: false, error: this.lifecycleFailureText('close', entry, err), isError: true };
|
|
5428
|
+
return { success: false, error: withEnded(this.lifecycleFailureText('close', entry, err)), isError: true };
|
|
5318
5429
|
}
|
|
5319
5430
|
}
|
|
5320
5431
|
|
|
5432
|
+
/**
|
|
5433
|
+
* The tool-close hook (onToolClose) for a channel, its throw taken as the
|
|
5434
|
+
* answer's text: a release of the hold that failed is the close's answer,
|
|
5435
|
+
* as the registry's other failures are, never a rejected call.
|
|
5436
|
+
*/
|
|
5437
|
+
private toolCloseHook(ref: ChannelRef, opts?: { unregistered?: true }): { text?: () => string } | { error: string } {
|
|
5438
|
+
try {
|
|
5439
|
+
return { text: this.onToolClose?.(ref.serverId, ref.channelId, opts) };
|
|
5440
|
+
} catch (err) {
|
|
5441
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
5442
|
+
}
|
|
5443
|
+
}
|
|
5444
|
+
|
|
5445
|
+
/**
|
|
5446
|
+
* A close naming the channel the all-except hold excepts, which its server
|
|
5447
|
+
* no longer registers: the close has nothing left to close, and the hold
|
|
5448
|
+
* nothing left to speak into, so it ends as a close of the registered
|
|
5449
|
+
* channel ends it, what it held released, and the answer says so, as a
|
|
5450
|
+
* failure, since no close ran. Undefined where no hold excepts the channel
|
|
5451
|
+
* (onToolClose answers nothing).
|
|
5452
|
+
*/
|
|
5453
|
+
private unregisteredFocusClose(gone: ChannelRef): ToolResult | undefined {
|
|
5454
|
+
const lead = noLongerRegistered(gone);
|
|
5455
|
+
const ended = this.toolCloseHook(gone, { unregistered: true });
|
|
5456
|
+
if ('error' in ended) return { success: false, error: `${lead} ${ended.error}`, isError: true };
|
|
5457
|
+
if (ended.text) return { success: false, error: `${lead} ${ended.text()}`, isError: true };
|
|
5458
|
+
return undefined;
|
|
5459
|
+
}
|
|
5460
|
+
|
|
5461
|
+
/**
|
|
5462
|
+
* The channel the all-except hold excepts (focusChannelResolver), where a
|
|
5463
|
+
* channel_close names it by its id or its server-qualified name alone: no
|
|
5464
|
+
* serverId, and no channel of the calling turn's own answering to the name
|
|
5465
|
+
* (ownChannelNamed, which a turn's name means first). Such a name means
|
|
5466
|
+
* the hold's channel, registered or not, and not another server's channel
|
|
5467
|
+
* with the id, as when the hold's channel's server stopped registering it
|
|
5468
|
+
* and another server registered the id: that one is closed only by a close
|
|
5469
|
+
* that names its server. With it, where other registered channels answer
|
|
5470
|
+
* to the name, the sentence saying they weren't closed (`notClosed`).
|
|
5471
|
+
*
|
|
5472
|
+
* Where the name is the hold channel's and a channel of the calling turn's
|
|
5473
|
+
* own, another channel, answers to it too, the name means neither: a
|
|
5474
|
+
* resident repeating a close that failed before its focus ended, in a turn
|
|
5475
|
+
* that has since opened another server's channel with the id, would
|
|
5476
|
+
* otherwise close that channel and leave the focus in force. That close is
|
|
5477
|
+
* refused, naming both (`refusal`, focusOrOwnRefusal), and a close naming
|
|
5478
|
+
* either's server reaches it. A turn's name for the hold's channel itself,
|
|
5479
|
+
* and a name the turn's channels on more than one server answer to
|
|
5480
|
+
* (ownNeitherRefusal), resolve as any other.
|
|
5481
|
+
*
|
|
5482
|
+
* Undefined where no hold is in force, where the name isn't the hold
|
|
5483
|
+
* channel's, and where the hold's record can't be read, which leaves the
|
|
5484
|
+
* name to resolve as any other (resolveToolChannelEntry).
|
|
5485
|
+
*
|
|
5486
|
+
* Another channel the name reaches that has a tune-out of its own is
|
|
5487
|
+
* refused by a close until that tune-out is cancelled (tunedOutRefusal),
|
|
5488
|
+
* so its sentence says it is tuned out, and names the cancel first, then
|
|
5489
|
+
* the close by its id and server.
|
|
5490
|
+
*/
|
|
5491
|
+
private focusNamedBare(
|
|
5492
|
+
name: string,
|
|
5493
|
+
serverId: string | undefined,
|
|
5494
|
+
own: readonly ChannelRef[] | undefined,
|
|
5495
|
+
): { focus: ChannelRef; notClosed?: string } | { refusal: string } | undefined {
|
|
5496
|
+
if (serverId) return undefined;
|
|
5497
|
+
const mine = this.ownChannelNamed(name, own);
|
|
5498
|
+
if (mine && 'neither' in mine) return undefined;
|
|
5499
|
+
let focus: ChannelRef | null | undefined;
|
|
5500
|
+
try {
|
|
5501
|
+
focus = this.focusChannelResolver?.();
|
|
5502
|
+
} catch {
|
|
5503
|
+
return undefined;
|
|
5504
|
+
}
|
|
5505
|
+
if (!focus || (name !== focus.channelId && name !== qualifiedChannelName(focus))) return undefined;
|
|
5506
|
+
if (mine) return sameChannel(mine.channel, focus) ? undefined : { refusal: this.focusOrOwnRefusal(name, focus, mine.channel, own) };
|
|
5507
|
+
const held = focus;
|
|
5508
|
+
const others = this.entriesNamed(name).filter((entry) => !sameChannel(refOf(entry), held));
|
|
5509
|
+
const tunedOut = others.filter((entry) => this.getDesiredState(entry.serverId, entry.descriptor.id) === 'tuned-out');
|
|
5510
|
+
const closable = others.filter((entry) => !tunedOut.includes(entry)).map(refOf);
|
|
5511
|
+
const sentences = [
|
|
5512
|
+
...(closable.length > 0 ? [notClosedText(closable)] : []),
|
|
5513
|
+
...tunedOut.map((entry) => {
|
|
5514
|
+
const ref = refOf(entry);
|
|
5515
|
+
return `Channel ${channelOnServer(ref)} was not closed: it is tuned out, and a close doesn't change that. ` +
|
|
5516
|
+
this.tuneOutEndText(entry, 'channel_close', `channelId ${quotedValue(ref.channelId)} and serverId ${quotedValue(ref.serverId)}`);
|
|
5517
|
+
}),
|
|
5518
|
+
];
|
|
5519
|
+
return { focus: held, ...(sentences.length > 0 ? { notClosed: sentences.join(' ') } : {}) };
|
|
5520
|
+
}
|
|
5521
|
+
|
|
5522
|
+
/**
|
|
5523
|
+
* The refusal of a channel_close's bare name (focusNamedBare) that both the
|
|
5524
|
+
* channel the all-except hold excepts (`focus`) and another channel of the
|
|
5525
|
+
* calling turn's own (`mine`, ownChannelNamed) answer to, each named by
|
|
5526
|
+
* what reaches it alone. The hold's channel is reached by its
|
|
5527
|
+
* server-qualified name, registered or not (focusNamedBare), unless a
|
|
5528
|
+
* channel of the turn's answers to that too; the turn's channel as a
|
|
5529
|
+
* refusal of the turn's names shows it (ownCandidates), unless another
|
|
5530
|
+
* channel of the turn's has its qualified name for its id. A serverId goes
|
|
5531
|
+
* with the channel's own id, so it's offered only where the name is the id
|
|
5532
|
+
* of both, for the hold's server (registered or not, the close reaches its
|
|
5533
|
+
* channel: unregisteredFocusNamed) and for the turn's channel's where that
|
|
5534
|
+
* server still registers it. Where the name is the hold channel's
|
|
5535
|
+
* server-qualified name, which another of the turn's channels has for its
|
|
5536
|
+
* id, the hold's channel is reached by its id and server.
|
|
5537
|
+
*/
|
|
5538
|
+
private focusOrOwnRefusal(name: string, focus: ChannelRef, mine: ChannelRef, own: readonly ChannelRef[] | undefined): string {
|
|
5539
|
+
const qualified = qualifiedChannelName(focus);
|
|
5540
|
+
const focusReaches = this.ownChannelNamed(qualified, own) === undefined;
|
|
5541
|
+
const [theirs] = this.ownCandidates([mine]);
|
|
5542
|
+
// The turn's channel's name, as the focus's: a bare id answers to the
|
|
5543
|
+
// turn's own channels first, so one of them with that id catches it.
|
|
5544
|
+
const theirsReaches = theirs!.reaches && this.ownChannelNamed(theirs!.text, own) === undefined;
|
|
5545
|
+
const parts = [
|
|
5546
|
+
{ text: focusReaches ? quotedValue(qualified) : channelOnServer(focus), reaches: focusReaches },
|
|
5547
|
+
{ text: theirsReaches ? quotedValue(theirs!.text) : theirs!.reaches ? channelOnServer(mine) : theirs!.text, reaches: theirsReaches },
|
|
5548
|
+
];
|
|
5549
|
+
const named = parts.filter((p) => p.reaches).map((p) => p.text);
|
|
5550
|
+
const byName = named.length === 2 ? 'channelId set to one of them' : named.length === 1 ? `channelId ${named[0]}` : '';
|
|
5551
|
+
const servers = name === focus.channelId ? [focus.serverId, ...(this.entryAt(mine) ? [mine.serverId] : [])] : [];
|
|
5552
|
+
const byServer = servers.length > 0 ? `serverId set to ${servers.map((id) => JSON.stringify(id)).join(' or ')}` : '';
|
|
5553
|
+
const how = [byName, byServer].filter((h) => h !== '').join(', or with ');
|
|
5554
|
+
const focusRoute = name !== focus.channelId && !focusReaches
|
|
5555
|
+
? ` For the channel the focus excepts, call again with channelId ${quotedValue(focus.channelId)} and serverId ${JSON.stringify(focus.serverId)}.`
|
|
5556
|
+
: '';
|
|
5557
|
+
return `Channel ${JSON.stringify(name)} names both the channel the all-except focus excepts (${parts[0]!.text}) ` +
|
|
5558
|
+
`and this turn's channel (${parts[1]!.text}), so nothing ran.${how ? ` Call again with ${how}.` : ''}${focusRoute}`;
|
|
5559
|
+
}
|
|
5560
|
+
|
|
5561
|
+
/**
|
|
5562
|
+
* The channel the all-except hold excepts (focusChannelResolver), where a
|
|
5563
|
+
* channel tool's name with a serverId, or the calling turn's own channel's
|
|
5564
|
+
* name, means it though its server no longer registers it: with a
|
|
5565
|
+
* serverId, that server's channel with the id; inside the calling turn,
|
|
5566
|
+
* the turn's own channel the name answers to by its id (ownChannelNamed).
|
|
5567
|
+
* A name given alone is focusNamedBare's. Undefined where no hold is in
|
|
5568
|
+
* force, where its channel is still registered, and where the hold's
|
|
5569
|
+
* record can't be read, which leaves the name's refusal as it was.
|
|
5570
|
+
*/
|
|
5571
|
+
private unregisteredFocusNamed(name: string, serverId: string | undefined, own: readonly ChannelRef[] | undefined): ChannelRef | undefined {
|
|
5572
|
+
let focus: ChannelRef | null | undefined;
|
|
5573
|
+
try {
|
|
5574
|
+
focus = this.focusChannelResolver?.();
|
|
5575
|
+
} catch {
|
|
5576
|
+
return undefined;
|
|
5577
|
+
}
|
|
5578
|
+
if (!focus || this.entryAt(focus)) return undefined;
|
|
5579
|
+
if (serverId) return sameChannel(focus, { serverId, channelId: name }) ? focus : undefined;
|
|
5580
|
+
const mine = this.ownChannelNamed(name, own);
|
|
5581
|
+
return mine && 'channel' in mine && sameChannel(mine.channel, focus) ? focus : undefined;
|
|
5582
|
+
}
|
|
5583
|
+
|
|
5321
5584
|
private async handleToolDecline(input: {
|
|
5322
5585
|
channelId: string;
|
|
5323
5586
|
serverId?: string;
|
|
@@ -5513,8 +5776,8 @@ export class ChannelRegistry {
|
|
|
5513
5776
|
`diverted, and then call channel_close.${serverBack}`
|
|
5514
5777
|
: `. If you still want it closed, call channel_close.${serverBack}`;
|
|
5515
5778
|
closeError = decided === 'tuned-out'
|
|
5516
|
-
? `The decline is recorded, but the channel was tuned out meanwhile, and that decision stands: its messages
|
|
5517
|
-
|
|
5779
|
+
? `The decline is recorded, but the channel was tuned out meanwhile, and that decision stands: its messages ` +
|
|
5780
|
+
`${this.tuneOutHeld()}${step}`
|
|
5518
5781
|
: `The decline is recorded, but a later decision to open the channel replaced it, and that decision stands${step}`;
|
|
5519
5782
|
} else if (closed) {
|
|
5520
5783
|
// Closed for certain, whatever became of the decline's own close: one
|
|
@@ -6267,8 +6530,13 @@ export class ChannelRegistry {
|
|
|
6267
6530
|
? 'Nothing was posted: name the channel with channelId. A subagent has no default channel, since its plain '
|
|
6268
6531
|
+ 'speech is returned to whoever called it, so channel_publish sends only to a channel it names.'
|
|
6269
6532
|
: caller !== undefined && inTurn && this.activeChannelResolver?.(caller) === null
|
|
6270
|
-
?
|
|
6271
|
-
|
|
6533
|
+
? (this.channellessTurn && !this.channellessTurn(caller)
|
|
6534
|
+
// The turn came from a channel, but its speech goes to none
|
|
6535
|
+
// (a locked turn held as drafts).
|
|
6536
|
+
? `Nothing was posted: name the channel with channelId. Your plain speech goes to no channel now, so `
|
|
6537
|
+
+ `channel_publish sends only to a channel it names. ${findIds}`
|
|
6538
|
+
: `Nothing was posted: name the channel with channelId. This turn has no channel of its own, so its plain speech `
|
|
6539
|
+
+ `goes to none and channel_publish sends only to a channel it names. ${findIds}`)
|
|
6272
6540
|
: `Nothing was posted: channel_publish needs a \`channelId\`, and there's no channel to default to yet. ${findIds}`,
|
|
6273
6541
|
isError: true,
|
|
6274
6542
|
};
|