@animalabs/agent-framework 0.28.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -25
- package/dist/src/admission/admit.d.ts +90 -0
- package/dist/src/admission/admit.d.ts.map +1 -0
- package/dist/src/admission/admit.js +43 -0
- package/dist/src/admission/admit.js.map +1 -0
- package/dist/src/agent.d.ts +21 -0
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +47 -7
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/mcp-server-core.d.ts.map +1 -1
- package/dist/src/api/mcp-server-core.js +3 -12
- package/dist/src/api/mcp-server-core.js.map +1 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +7 -3
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/author-presentation.d.ts +50 -0
- package/dist/src/author-presentation.d.ts.map +1 -0
- package/dist/src/author-presentation.js +58 -0
- package/dist/src/author-presentation.js.map +1 -0
- package/dist/src/auto-continue.d.ts +2 -0
- package/dist/src/auto-continue.d.ts.map +1 -1
- package/dist/src/auto-continue.js +5 -2
- package/dist/src/auto-continue.js.map +1 -1
- package/dist/src/code-execution/isolate/runner.d.ts +7 -5
- package/dist/src/code-execution/isolate/runner.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/runner.js +7 -5
- package/dist/src/code-execution/isolate/runner.js.map +1 -1
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +3 -2
- package/dist/src/code-execution/isolate/scripts.js.map +1 -1
- package/dist/src/dendrite/index.d.ts +6 -0
- package/dist/src/dendrite/index.d.ts.map +1 -0
- package/dist/src/dendrite/index.js +3 -0
- package/dist/src/dendrite/index.js.map +1 -0
- package/dist/src/dendrite/presets.d.ts +60 -0
- package/dist/src/dendrite/presets.d.ts.map +1 -0
- package/dist/src/dendrite/presets.js +117 -0
- package/dist/src/dendrite/presets.js.map +1 -0
- package/dist/src/dendrite/registry.d.ts +260 -0
- package/dist/src/dendrite/registry.d.ts.map +1 -0
- package/dist/src/dendrite/registry.js +742 -0
- package/dist/src/dendrite/registry.js.map +1 -0
- package/dist/src/dendrite/types.d.ts +392 -0
- package/dist/src/dendrite/types.d.ts.map +1 -0
- package/dist/src/dendrite/types.js +24 -0
- package/dist/src/dendrite/types.js.map +1 -0
- package/dist/src/framework.d.ts +846 -67
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +4213 -924
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +46 -14
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +143 -53
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/index.d.ts +1 -1
- package/dist/src/gate/index.d.ts.map +1 -1
- package/dist/src/gate/index.js +1 -1
- package/dist/src/gate/index.js.map +1 -1
- package/dist/src/gate/types.d.ts +12 -2
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/heartbeat/heartbeat.d.ts +18 -0
- package/dist/src/heartbeat/heartbeat.d.ts.map +1 -1
- package/dist/src/heartbeat/heartbeat.js +34 -8
- package/dist/src/heartbeat/heartbeat.js.map +1 -1
- package/dist/src/held-writes.d.ts +3 -3
- package/dist/src/held-writes.js +3 -3
- package/dist/src/host-presentation.d.ts +174 -0
- package/dist/src/host-presentation.d.ts.map +1 -0
- package/dist/src/host-presentation.js +565 -0
- package/dist/src/host-presentation.js.map +1 -0
- package/dist/src/index.d.ts +7 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/line-reader.d.ts +4 -0
- package/dist/src/line-reader.d.ts.map +1 -0
- package/dist/src/line-reader.js +59 -0
- package/dist/src/line-reader.js.map +1 -0
- package/dist/src/mcpl/channel-registry.d.ts +118 -15
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +266 -110
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +2 -1
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/convert-block.d.ts +1 -0
- package/dist/src/mcpl/convert-block.d.ts.map +1 -1
- package/dist/src/mcpl/convert-block.js +16 -4
- package/dist/src/mcpl/convert-block.js.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.js +4 -3
- package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +3 -1
- package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
- package/dist/src/mcpl/inbound-source.js +3 -1
- package/dist/src/mcpl/inbound-source.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
- package/dist/src/mcpl/modern-connection.js +11 -7
- package/dist/src/mcpl/modern-connection.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts +3 -2
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +25 -3
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +9 -0
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +21 -0
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/push-handler.js +2 -2
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +12 -40
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +29 -23
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +20 -20
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/skip-reply-wake.d.ts +25 -0
- package/dist/src/mcpl/skip-reply-wake.d.ts.map +1 -1
- package/dist/src/mcpl/skip-reply-wake.js +25 -0
- package/dist/src/mcpl/skip-reply-wake.js.map +1 -1
- package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
- package/dist/src/mcpl/tool-presence.js +18 -17
- package/dist/src/mcpl/tool-presence.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
- package/dist/src/mcpl/tool-signature.js +17 -16
- package/dist/src/mcpl/tool-signature.js.map +1 -1
- package/dist/src/mcpl/toolset-host.d.ts +8 -26
- package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
- package/dist/src/mcpl/toolset-host.js +16 -6
- package/dist/src/mcpl/toolset-host.js.map +1 -1
- package/dist/src/mcpl/toolsets-module.d.ts +14 -25
- package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
- package/dist/src/mcpl/toolsets-module.js +187 -50
- package/dist/src/mcpl/toolsets-module.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +1 -1
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +16 -16
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +28 -12
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +6 -7
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +6 -7
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/api/index.d.ts.map +1 -1
- package/dist/src/modules/api/index.js +4 -2
- package/dist/src/modules/api/index.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +33 -25
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/history/index.js +8 -6
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/sender-name.d.ts +7 -0
- package/dist/src/modules/sender-name.d.ts.map +1 -0
- package/dist/src/modules/sender-name.js +10 -0
- package/dist/src/modules/sender-name.js.map +1 -0
- package/dist/src/modules/workspace/answers.d.ts +5 -1
- package/dist/src/modules/workspace/answers.d.ts.map +1 -1
- package/dist/src/modules/workspace/answers.js +16 -1
- package/dist/src/modules/workspace/answers.js.map +1 -1
- package/dist/src/modules/workspace/direct-write.d.ts.map +1 -1
- package/dist/src/modules/workspace/direct-write.js +6 -4
- package/dist/src/modules/workspace/direct-write.js.map +1 -1
- package/dist/src/modules/workspace/effects.d.ts +4 -2
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +50 -40
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +1 -0
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +66 -36
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +51 -20
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +120 -82
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts +7 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +133 -37
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/notice-tags.d.ts +81 -0
- package/dist/src/notice-tags.d.ts.map +1 -0
- package/dist/src/notice-tags.js +85 -0
- package/dist/src/notice-tags.js.map +1 -0
- package/dist/src/reasoning-loss-notice.d.ts.map +1 -1
- package/dist/src/reasoning-loss-notice.js +5 -3
- package/dist/src/reasoning-loss-notice.js.map +1 -1
- package/dist/src/silent-heartbeat.d.ts +5 -55
- package/dist/src/silent-heartbeat.d.ts.map +1 -1
- package/dist/src/silent-heartbeat.js +63 -7
- package/dist/src/silent-heartbeat.js.map +1 -1
- package/dist/src/stored-injections.d.ts +4 -0
- package/dist/src/stored-injections.d.ts.map +1 -1
- package/dist/src/stored-injections.js +6 -0
- package/dist/src/stored-injections.js.map +1 -1
- package/dist/src/supplied-text.d.ts +35 -0
- package/dist/src/supplied-text.d.ts.map +1 -0
- package/dist/src/supplied-text.js +141 -0
- package/dist/src/supplied-text.js.map +1 -0
- package/dist/src/tool-args.d.ts.map +1 -1
- package/dist/src/tool-args.js +9 -8
- package/dist/src/tool-args.js.map +1 -1
- package/dist/src/tool-image-ledger.d.ts.map +1 -1
- package/dist/src/tool-image-ledger.js +2 -1
- package/dist/src/tool-image-ledger.js.map +1 -1
- package/dist/src/tool-presentation.d.ts.map +1 -1
- package/dist/src/tool-presentation.js +5 -4
- package/dist/src/tool-presentation.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +382 -11
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +1100 -119
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts +101 -5
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/tune-out/tools.js +55 -18
- package/dist/src/tune-out/tools.js.map +1 -1
- package/dist/src/types/agent.d.ts +48 -9
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +142 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +22 -11
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +136 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/admission/admit.ts +113 -0
- package/src/agent.ts +52 -7
- package/src/api/mcp-server-core.ts +3 -14
- package/src/api/server.ts +7 -3
- package/src/author-presentation.ts +81 -0
- package/src/auto-continue.ts +8 -2
- package/src/code-execution/isolate/runner.ts +7 -5
- package/src/code-execution/isolate/scripts.ts +3 -2
- package/src/dendrite/index.ts +32 -0
- package/src/dendrite/presets.ts +149 -0
- package/src/dendrite/registry.ts +808 -0
- package/src/dendrite/types.ts +397 -0
- package/src/framework.ts +4362 -852
- package/src/gate/event-gate.ts +159 -52
- package/src/gate/index.ts +1 -1
- package/src/gate/types.ts +12 -2
- package/src/heartbeat/heartbeat.ts +48 -9
- package/src/held-writes.ts +3 -3
- package/src/host-presentation.ts +612 -0
- package/src/index.ts +17 -4
- package/src/line-reader.ts +58 -0
- package/src/mcpl/channel-registry.ts +306 -111
- package/src/mcpl/conversation-router.ts +2 -1
- package/src/mcpl/convert-block.ts +17 -4
- package/src/mcpl/hook-orchestrator.ts +4 -3
- package/src/mcpl/inbound-source.ts +3 -1
- package/src/mcpl/modern-connection.ts +11 -7
- package/src/mcpl/protocol-family.ts +24 -3
- package/src/mcpl/push-coalescer.ts +19 -0
- package/src/mcpl/push-handler.ts +2 -2
- package/src/mcpl/references.ts +31 -23
- package/src/mcpl/server-connection.ts +20 -18
- package/src/mcpl/skip-reply-wake.ts +35 -0
- package/src/mcpl/tool-presence.ts +18 -17
- package/src/mcpl/tool-signature.ts +17 -16
- package/src/mcpl/toolset-host.ts +18 -6
- package/src/mcpl/toolsets-module.ts +149 -50
- package/src/mcpl/transport.ts +18 -14
- package/src/mcpl/types.ts +29 -12
- package/src/module-registry.ts +6 -7
- package/src/modules/api/index.ts +4 -2
- package/src/modules/discord/index.ts +37 -25
- package/src/modules/history/index.ts +8 -6
- package/src/modules/sender-name.ts +9 -0
- package/src/modules/workspace/answers.ts +20 -2
- package/src/modules/workspace/direct-write.ts +4 -3
- package/src/modules/workspace/effects.ts +50 -40
- package/src/modules/workspace/index.ts +58 -34
- package/src/modules/workspace/observe.ts +139 -88
- package/src/modules/workspace/reconcile.ts +159 -36
- package/src/notice-tags.ts +86 -0
- package/src/reasoning-loss-notice.ts +5 -3
- package/src/silent-heartbeat.ts +11 -7
- package/src/stored-injections.ts +7 -0
- package/src/supplied-text.ts +154 -0
- package/src/tool-args.ts +9 -8
- package/src/tool-image-ledger.ts +2 -1
- package/src/tool-presentation.ts +5 -4
- package/src/tune-out/coordinator.ts +1252 -123
- package/src/tune-out/tools.ts +152 -11
- package/src/types/agent.ts +51 -9
- package/src/types/framework.ts +146 -0
- package/src/types/module.ts +23 -11
- package/src/types/trace.ts +150 -0
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
* - Expose synthesized tools: channel_list, channel_open, channel_close, channel_publish
|
|
13
13
|
* - Build channel context for beforeInference params
|
|
14
14
|
*/
|
|
15
|
-
import {
|
|
15
|
+
import { NOTICE_TAG } from '../notice-tags.js';
|
|
16
|
+
import { convertBlocks } from './convert-block.js';
|
|
17
|
+
import { cutToFit, markedText, markNoticeOpenings, nameToTypeBack, quotedValue, showsAsItself, suppliedValue } from '../supplied-text.js';
|
|
16
18
|
import { isDeepStrictEqual } from 'node:util';
|
|
17
19
|
import { ensureStateRegistered } from '../module-registry.js';
|
|
18
20
|
import { expandCoreTags, isMarkerTagged } from './tags.js';
|
|
@@ -22,7 +24,7 @@ import { logDetail, logValue } from './log-value.js';
|
|
|
22
24
|
import { RefusalLines } from './refusal-lines.js';
|
|
23
25
|
import { CapabilityGrant } from './capability-grant.js';
|
|
24
26
|
import { McplRequestError } from './server-connection.js';
|
|
25
|
-
import { readSkipReplyWake } from './skip-reply-wake.js';
|
|
27
|
+
import { readSkipReplyWake, skipReplyNote } from './skip-reply-wake.js';
|
|
26
28
|
import { isProseTargetToken } from './prose-grammar.js';
|
|
27
29
|
// ============================================================================
|
|
28
30
|
// Typing indicator interval (Discord typing lasts ~10s, so 7s keeps it alive)
|
|
@@ -337,11 +339,11 @@ const CHANNEL_TOOL_DEFINITIONS = [
|
|
|
337
339
|
},
|
|
338
340
|
{
|
|
339
341
|
name: 'channel_publish',
|
|
340
|
-
description: "Publish a message to a channel. If channelId is omitted, publishes
|
|
342
|
+
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.",
|
|
341
343
|
inputSchema: {
|
|
342
344
|
type: 'object',
|
|
343
345
|
properties: {
|
|
344
|
-
channelId: { type: 'string', description: "ID of the channel to publish to (defaults to your
|
|
346
|
+
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)" },
|
|
345
347
|
content: { type: 'string', description: 'Text content to publish' },
|
|
346
348
|
text: { type: 'string', description: 'Alias for content' },
|
|
347
349
|
},
|
|
@@ -410,20 +412,22 @@ const CHANNEL_TOOL_DEFINITIONS = [
|
|
|
410
412
|
// ============================================================================
|
|
411
413
|
// Constructor Options
|
|
412
414
|
// ============================================================================
|
|
413
|
-
/** Bound connector values for notices, logs and an outcome's reason
|
|
414
|
-
*
|
|
415
|
-
*
|
|
415
|
+
/** Bound connector values for notices, logs and an outcome's reason, on one
|
|
416
|
+
* line with their notice openings marked (suppliedValue). A PublishOutcome
|
|
417
|
+
* keeps the connector's structured data (`detail`) as the connection gives
|
|
418
|
+
* it, its strings redacted (redactedData). */
|
|
416
419
|
export function connectorText(value) {
|
|
417
420
|
let text;
|
|
418
421
|
try {
|
|
419
422
|
text = typeof value === 'string' ? value : JSON.stringify(value) ?? String(value);
|
|
420
423
|
}
|
|
421
424
|
catch {
|
|
422
|
-
return
|
|
425
|
+
return `[${NOTICE_TAG.unserializableConnectorValue}]`;
|
|
423
426
|
}
|
|
424
427
|
const limit = 2000;
|
|
425
|
-
const suffix =
|
|
426
|
-
|
|
428
|
+
const suffix = `… [${NOTICE_TAG.truncated}]`;
|
|
429
|
+
const shown = suppliedValue(text);
|
|
430
|
+
return shown.length > limit ? cutToFit(shown, limit - suffix.length) + suffix : shown;
|
|
427
431
|
}
|
|
428
432
|
/** One key per channel, its server and id: unlike joining the two with a
|
|
429
433
|
* separator, no two channels share one, whatever their ids hold. */
|
|
@@ -437,6 +441,8 @@ export function sameChannel(a, b) {
|
|
|
437
441
|
return !a && !b;
|
|
438
442
|
return a.serverId === b.serverId && a.channelId === b.channelId;
|
|
439
443
|
}
|
|
444
|
+
export const NO_CHANNEL_EXAMPLE = '#<channel>';
|
|
445
|
+
export const NO_PERSON_EXAMPLE = '@<person>';
|
|
440
446
|
/**
|
|
441
447
|
* A channel's server-qualified name, `serverId:channelId`. A name given as a
|
|
442
448
|
* string (a tool's channelId, a `>>` target, a host's call) may take this
|
|
@@ -447,11 +453,32 @@ export function sameChannel(a, b) {
|
|
|
447
453
|
export function qualifiedChannelName(ref) {
|
|
448
454
|
return `${ref.serverId}:${ref.channelId}`;
|
|
449
455
|
}
|
|
456
|
+
function markedHistory(history) {
|
|
457
|
+
return history.map((item) => {
|
|
458
|
+
if (item === null || typeof item !== 'object' || Array.isArray(item) || !('content' in item))
|
|
459
|
+
return item;
|
|
460
|
+
const { content } = item;
|
|
461
|
+
if (Array.isArray(content))
|
|
462
|
+
return { ...item, content: markNoticeOpenings(content.map(withEmbeddedTextMarked)) };
|
|
463
|
+
return typeof content === 'string' ? { ...item, content: markedText(content) } : item;
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
function withEmbeddedTextMarked(block) {
|
|
467
|
+
if (block === null || typeof block !== 'object' || block.type !== 'resource')
|
|
468
|
+
return block;
|
|
469
|
+
const { resource } = block;
|
|
470
|
+
if (resource === null || typeof resource !== 'object' || typeof resource.text !== 'string')
|
|
471
|
+
return block;
|
|
472
|
+
return { ...block, resource: { ...resource, text: markedText(resource.text) } };
|
|
473
|
+
}
|
|
450
474
|
/** A channel told apart by its id and its server, for one no string picks
|
|
451
|
-
* alone: `"b:room" on server "a"
|
|
475
|
+
* alone or whose name wouldn't show as itself: `"b:room" on server "a"`,
|
|
476
|
+
* each a JSON literal a channel tool's channelId and serverId take back
|
|
477
|
+
* (quotedValue). No input takes the whole form for a name. */
|
|
452
478
|
function channelOnServer(ref) {
|
|
453
|
-
return `${
|
|
479
|
+
return `${quotedValue(ref.channelId)} on server ${quotedValue(ref.serverId)}`;
|
|
454
480
|
}
|
|
481
|
+
const CHANNEL_ON_SERVER_AT_END = /("(?:[^"\\]|\\.)*") on server ("(?:[^"\\]|\\.)*")$/;
|
|
455
482
|
/**
|
|
456
483
|
* A failed request's words, bounded, for a reason that a caller composes.
|
|
457
484
|
* This host's own words, for a request that wasn't sent or wasn't answered
|
|
@@ -650,6 +677,9 @@ export class ChannelRegistry {
|
|
|
650
677
|
emitTraceFn;
|
|
651
678
|
sendTypingFn;
|
|
652
679
|
shouldTriggerInference;
|
|
680
|
+
latestInboundLocus;
|
|
681
|
+
onToolClose;
|
|
682
|
+
focusChannelResolver;
|
|
653
683
|
onRouteFailure;
|
|
654
684
|
onChannelAutoOpened;
|
|
655
685
|
homeChannelResolver;
|
|
@@ -1020,6 +1050,9 @@ export class ChannelRegistry {
|
|
|
1020
1050
|
this.emitTraceFn = emitTraceFn;
|
|
1021
1051
|
this.sendTypingFn = options?.sendTypingFn;
|
|
1022
1052
|
this.shouldTriggerInference = options?.shouldTriggerInference;
|
|
1053
|
+
this.latestInboundLocus = options?.latestInboundLocus === true;
|
|
1054
|
+
this.onToolClose = options?.onToolClose;
|
|
1055
|
+
this.focusChannelResolver = options?.focusChannelResolver;
|
|
1023
1056
|
this.onRouteFailure = options?.onRouteFailure;
|
|
1024
1057
|
this.onChannelAutoOpened = options?.onChannelAutoOpened;
|
|
1025
1058
|
this.homeChannelResolver = options?.homeChannelResolver;
|
|
@@ -1228,9 +1261,6 @@ export class ChannelRegistry {
|
|
|
1228
1261
|
// The batch came on the transport open now: a close after this point
|
|
1229
1262
|
// (transportClosed) leaves its traffic nothing to confirm.
|
|
1230
1263
|
const closesAtArrival = this.transportCloses.get(serverId) ?? 0;
|
|
1231
|
-
// And came from a server registered now: one removed since (removeServer)
|
|
1232
|
-
// gives no message the agent replies to (markAccepted).
|
|
1233
|
-
const removalsAtArrival = this.serverRemovals.get(serverId) ?? 0;
|
|
1234
1264
|
for (const message of params.messages) {
|
|
1235
1265
|
if (!message || typeof message.channelId !== 'string' || !message.channelId
|
|
1236
1266
|
|| typeof message.messageId !== 'string' || !message.messageId) {
|
|
@@ -1328,33 +1358,24 @@ export class ChannelRegistry {
|
|
|
1328
1358
|
}
|
|
1329
1359
|
if (message.tags)
|
|
1330
1360
|
message.tags = expandCoreTags(message.tags);
|
|
1331
|
-
const convertedContent = message.content
|
|
1361
|
+
const convertedContent = convertBlocks(message.content, 'attachment on channel message', serverId);
|
|
1332
1362
|
if (!emptyAllowed && isVisiblyEmptyConversion(message.content, convertedContent)) {
|
|
1333
1363
|
rejectEmpty();
|
|
1334
1364
|
continue;
|
|
1335
1365
|
}
|
|
1336
|
-
//
|
|
1337
|
-
//
|
|
1338
|
-
//
|
|
1339
|
-
//
|
|
1340
|
-
//
|
|
1341
|
-
//
|
|
1366
|
+
// A server sending channels/incoming is evidence that the transport
|
|
1367
|
+
// is open. This repairs transient status only; durable desired state
|
|
1368
|
+
// still changes exclusively through lifecycle operations. Nothing else
|
|
1369
|
+
// is decided here: whether this message is delivered, wakes anyone or
|
|
1370
|
+
// counts as the latest inbound is the framework's admission decision,
|
|
1371
|
+
// taken once the hold is known (see `noteDelivered`; issue #232 root
|
|
1372
|
+
// cause A). A coalesced item is "accepted" only once the coalescer
|
|
1373
|
+
// admits it, so for those this runs after the hook (below).
|
|
1342
1374
|
const incomingEntry = this.channels.get(incomingKey);
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
// must not retarget the default publish locus or become
|
|
1348
|
-
// `context.incoming`'s reply anchor. An edit the coalescer delivered
|
|
1349
|
-
// as the agent's first view counts as the message it replaced.
|
|
1350
|
-
// A message the coalescer admits after its server was removed sets no
|
|
1351
|
-
// default: the removal cleared that server's message, and a server
|
|
1352
|
-
// that's gone mustn't displace a live one's default.
|
|
1353
|
-
if (!isMarkerTagged(message.tags, firstView) && (this.serverRemovals.get(serverId) ?? 0) === removalsAtArrival) {
|
|
1354
|
-
this.defaultPublishChannel = { serverId, channelId: message.channelId };
|
|
1355
|
-
this.defaultPublishMessageId = message.messageId;
|
|
1356
|
-
this.defaultPublishThreadId = message.threadId;
|
|
1357
|
-
}
|
|
1375
|
+
// `firstView` is the coalescer's mark that an edit was the agent's first
|
|
1376
|
+
// view of its message; the registry no longer records the publish locus
|
|
1377
|
+
// here (admission does, through noteDelivered, with the same marker rule).
|
|
1378
|
+
const markAccepted = (_firstView = false) => {
|
|
1358
1379
|
// Where the host directs this server's lifecycle, traffic shows the
|
|
1359
1380
|
// transport open only for a channel still decided open or tuned out,
|
|
1360
1381
|
// and, but for one exception, only for a confirmed target: after a
|
|
@@ -1392,7 +1413,7 @@ export class ChannelRegistry {
|
|
|
1392
1413
|
};
|
|
1393
1414
|
if (!coalesced)
|
|
1394
1415
|
markAccepted();
|
|
1395
|
-
//
|
|
1416
|
+
// The server-level veto only; the wake gate runs at admission.
|
|
1396
1417
|
let triggerInference = true;
|
|
1397
1418
|
if (this.shouldTriggerInference) {
|
|
1398
1419
|
// The text the agent's context gets, as the push lane's filter reads
|
|
@@ -1674,15 +1695,58 @@ export class ChannelRegistry {
|
|
|
1674
1695
|
// ==========================================================================
|
|
1675
1696
|
// Accessors
|
|
1676
1697
|
// ==========================================================================
|
|
1698
|
+
/**
|
|
1699
|
+
* The most recent DELIVERED inbound message (channel, message, thread).
|
|
1700
|
+
* Recorded by the framework after admission, so a held message never
|
|
1701
|
+
* becomes "the latest inbound" (issue #232 gap 2), and never for a marker
|
|
1702
|
+
* (a reaction, reaction removal, edit or deletion, apart from an edit the
|
|
1703
|
+
* coalescer delivered as the agent's first view: isMarkerTagged). Read as
|
|
1704
|
+
* the `incoming` reference in the channel context and, under
|
|
1705
|
+
* `speech.locus: 'latest-inbound'`, as the fallback speech locus. The
|
|
1706
|
+
* channel is on the server the message came from. A message whose server
|
|
1707
|
+
* no longer registers its channel sets nothing, as one the coalescer admits
|
|
1708
|
+
* after its server was removed: the removal cleared that server's message,
|
|
1709
|
+
* and a server that's gone mustn't displace a live one's anchor. That's
|
|
1710
|
+
* read at admission, where main's guard compared the server's removal
|
|
1711
|
+
* count at arrival and at acceptance (markAccepted): a message that
|
|
1712
|
+
* arrived before its server's removal and is admitted once the server
|
|
1713
|
+
* registers the channel again sets the anchor here, as a message delivered
|
|
1714
|
+
* on a live channel, where main's would set nothing.
|
|
1715
|
+
*/
|
|
1716
|
+
noteDelivered(channel, messageId, threadId, tags, firstView = false) {
|
|
1717
|
+
// A marker is ABOUT a message, not one the agent replies to: it leaves
|
|
1718
|
+
// the anchor where it was (#157), and so does a chat:system notice (a
|
|
1719
|
+
// join, the weather), which is about the place (isMarkerTagged). An edit
|
|
1720
|
+
// the coalescer delivered as the agent's first view of its message counts
|
|
1721
|
+
// as that message.
|
|
1722
|
+
if (isMarkerTagged(tags, firstView))
|
|
1723
|
+
return;
|
|
1724
|
+
if (!this.entryAt(channel))
|
|
1725
|
+
return;
|
|
1726
|
+
this.defaultPublishChannel = { serverId: channel.serverId, channelId: channel.channelId };
|
|
1727
|
+
this.defaultPublishMessageId = messageId;
|
|
1728
|
+
this.defaultPublishThreadId = threadId;
|
|
1729
|
+
}
|
|
1677
1730
|
/**
|
|
1678
1731
|
* Get the default publish channel ID: the channel of the most recent
|
|
1679
|
-
* incoming message that isn't a reaction, reaction removal, edit
|
|
1680
|
-
* deletion marker, apart from an edit the coalescer delivers on
|
|
1681
|
-
* as the agent's first view.
|
|
1732
|
+
* delivered incoming message that isn't a reaction, reaction removal, edit
|
|
1733
|
+
* or deletion marker, apart from an edit the coalescer delivers on
|
|
1734
|
+
* acceptance as the agent's first view.
|
|
1682
1735
|
*/
|
|
1683
1736
|
getDefaultPublishChannel() {
|
|
1684
1737
|
return this.defaultPublishChannel;
|
|
1685
1738
|
}
|
|
1739
|
+
/**
|
|
1740
|
+
* Public form of the tool-input channel resolution (resolveToolChannelEntry):
|
|
1741
|
+
* optional server, the calling turn's own channels (`own`), ambiguity is an
|
|
1742
|
+
* error.
|
|
1743
|
+
*/
|
|
1744
|
+
resolveChannel(channelId, serverId, own) {
|
|
1745
|
+
const resolved = this.resolveToolChannelEntry(channelId, serverId, own);
|
|
1746
|
+
if (!resolved.entry)
|
|
1747
|
+
return { error: resolved.error ?? `Channel not found: ${channelId}` };
|
|
1748
|
+
return { serverId: resolved.entry.serverId, channelId: resolved.entry.descriptor.id };
|
|
1749
|
+
}
|
|
1686
1750
|
/**
|
|
1687
1751
|
* RFC-006 §3.2: is `channelId` registered BY `serverId` through a server
|
|
1688
1752
|
* declaration (channels/register, channels/changed) or an authorized open?
|
|
@@ -1764,7 +1828,7 @@ export class ChannelRegistry {
|
|
|
1764
1828
|
case 'journal':
|
|
1765
1829
|
return this.handleToolJournal(input);
|
|
1766
1830
|
case 'skip_reply':
|
|
1767
|
-
return this.handleToolSkipReply(input);
|
|
1831
|
+
return this.handleToolSkipReply(input, origin?.kind === 'agent' ? origin.earlierWake : undefined);
|
|
1768
1832
|
default:
|
|
1769
1833
|
// Not every agent is given every channel tool, so the refusal points
|
|
1770
1834
|
// to the ones this agent has rather than listing them all.
|
|
@@ -1780,7 +1844,7 @@ export class ChannelRegistry {
|
|
|
1780
1844
|
// ==========================================================================
|
|
1781
1845
|
/**
|
|
1782
1846
|
* The message the agent is replying to, with its channel: the most recent
|
|
1783
|
-
* inbound message that isn't a reaction, reaction removal, edit or deletion
|
|
1847
|
+
* delivered inbound message (noteDelivered) that isn't a reaction, reaction removal, edit or deletion
|
|
1784
1848
|
* marker, apart from an edit the coalescer delivers on acceptance as the
|
|
1785
1849
|
* agent's first view. Left process-global: per-channel inbound tracking
|
|
1786
1850
|
* (the right messageId for a fork's own channel) is a separate concern from
|
|
@@ -1797,10 +1861,14 @@ export class ChannelRegistry {
|
|
|
1797
1861
|
if (agentName !== undefined && this.activeChannelResolver?.(agentName) === null && !this.homeChannelResolver?.(agentName)) {
|
|
1798
1862
|
return undefined;
|
|
1799
1863
|
}
|
|
1800
|
-
|
|
1801
|
-
|
|
1864
|
+
// The latest delivered inbound itself, not the speech fallback
|
|
1865
|
+
// (globalFallbackFor), which under `speech.locus: 'delivery'` is the
|
|
1866
|
+
// all-except hold's excepted channel while one is open, and none
|
|
1867
|
+
// otherwise.
|
|
1868
|
+
const anchor = agentName !== undefined && this.takesGlobalFallback?.(agentName) === false ? null : this.defaultPublishChannel;
|
|
1869
|
+
if (!anchor || !this.defaultPublishMessageId)
|
|
1802
1870
|
return undefined;
|
|
1803
|
-
return { channel:
|
|
1871
|
+
return { channel: anchor, messageId: this.defaultPublishMessageId, threadId: this.defaultPublishThreadId };
|
|
1804
1872
|
}
|
|
1805
1873
|
/**
|
|
1806
1874
|
* The channel context one server is told for an agent's inference
|
|
@@ -1817,21 +1885,25 @@ export class ChannelRegistry {
|
|
|
1817
1885
|
// declared it inbound-only, since speech isn't posted there): a conversation
|
|
1818
1886
|
// fork's home channel, else this turn's channel (single trunk agent — item-3
|
|
1819
1887
|
// redux: its locus, else its triggering channel), else the global default
|
|
1820
|
-
// (
|
|
1888
|
+
// (a no-trigger turn's, but not a heartbeat's, whose turn takes none).
|
|
1889
|
+
// Without this, a fork
|
|
1821
1890
|
// was advertised the global locus but published somewhere else; and a trunk
|
|
1822
1891
|
// agent was told the wrong channel under concurrency.
|
|
1823
1892
|
// Each lookup is keyed on whether a name was given, not its truthiness:
|
|
1824
1893
|
// '' is a name an ephemeral run can have, and resolveLocus reads it too.
|
|
1825
1894
|
const home = agentName !== undefined ? this.homeChannelResolver?.(agentName) : undefined;
|
|
1826
1895
|
const active = agentName !== undefined ? this.activeChannelResolver?.(agentName) : undefined;
|
|
1827
|
-
// An ephemeral run takes no global fallback (takesGlobalFallback):
|
|
1828
|
-
// the latest inbound message's channel as
|
|
1829
|
-
// message as the one it is replying to.
|
|
1830
|
-
const fallback = agentName !== undefined ? this.globalFallbackFor(agentName) : this.
|
|
1896
|
+
// An ephemeral run takes no global fallback (takesGlobalFallback): not the
|
|
1897
|
+
// all-except hold's channel, not the latest inbound message's channel as
|
|
1898
|
+
// its outgoing one, nor that message as the one it is replying to.
|
|
1899
|
+
const fallback = agentName !== undefined ? this.globalFallbackFor(agentName) : this.sharedFallback();
|
|
1831
1900
|
// A turn that deliberately has no channel (null) takes no fallback.
|
|
1832
1901
|
const outgoing = home ?? (active !== undefined ? active : fallback);
|
|
1833
1902
|
const context = {};
|
|
1834
|
-
// Incoming: the message the agent is replying to (incomingFor).
|
|
1903
|
+
// Incoming: the message the agent is replying to (incomingFor). The
|
|
1904
|
+
// anchor is independent of the speech rule: under `speech.locus:
|
|
1905
|
+
// 'delivery'` the latest inbound is no fallback for where prose lands,
|
|
1906
|
+
// but it is still the message the agent is replying to.
|
|
1835
1907
|
const incoming = this.incomingFor(agentName);
|
|
1836
1908
|
if (incoming && incoming.channel.serverId === serverId) {
|
|
1837
1909
|
context.incoming = {
|
|
@@ -3215,13 +3287,20 @@ export class ChannelRegistry {
|
|
|
3215
3287
|
* name, only where no registered channel takes that name, as a channel
|
|
3216
3288
|
* tool reads it or a `>>` target does (by label or raw id too), so the
|
|
3217
3289
|
* name can't send anything to another channel; else by its id and server.
|
|
3290
|
+
* A name that wouldn't show as itself (showsAsItself: one holding a line
|
|
3291
|
+
* break, a notice opening, a quote, a backslash or a lone surrogate, or
|
|
3292
|
+
* whitespace at its edges) is shown by its id and its server too, which
|
|
3293
|
+
* the channel tools take exactly and the prose resolver doesn't, so no name
|
|
3294
|
+
* shown can be another channel's.
|
|
3218
3295
|
*/
|
|
3219
3296
|
nameShown(ref) {
|
|
3220
3297
|
const entry = this.entryAt(ref);
|
|
3221
|
-
if (entry)
|
|
3222
|
-
|
|
3298
|
+
if (entry) {
|
|
3299
|
+
const name = this.nameFor(entry);
|
|
3300
|
+
return name !== undefined && showsAsItself(name) ? name : channelOnServer(ref);
|
|
3301
|
+
}
|
|
3223
3302
|
for (const name of [ref.channelId, qualifiedChannelName(ref)]) {
|
|
3224
|
-
if (this.entriesNamed(name).length === 0 && !('channelId' in this.resolveProseTarget(name)))
|
|
3303
|
+
if (showsAsItself(name) && this.entriesNamed(name).length === 0 && !('channelId' in this.resolveProseTarget(name)))
|
|
3225
3304
|
return name;
|
|
3226
3305
|
}
|
|
3227
3306
|
return channelOnServer(ref);
|
|
@@ -3244,8 +3323,8 @@ export class ChannelRegistry {
|
|
|
3244
3323
|
/**
|
|
3245
3324
|
* How a tuned-out channel's tune-out ends, for the texts that tell an agent
|
|
3246
3325
|
* channel_open doesn't change it: a tune_out cancel, by the name that
|
|
3247
|
-
* picks the channel (nameFor), or, where no name picks it alone, its
|
|
3248
|
-
*
|
|
3326
|
+
* picks the channel (nameFor), or, where no name picks it alone, by its id
|
|
3327
|
+
* and server, since tune_out takes a serverId.
|
|
3249
3328
|
* For channel_close, which refuses a tuned-out channel too, it adds that
|
|
3250
3329
|
* the close comes after that.
|
|
3251
3330
|
*/
|
|
@@ -3257,12 +3336,15 @@ export class ChannelRegistry {
|
|
|
3257
3336
|
}
|
|
3258
3337
|
tuneOutEndText(entry, tool) {
|
|
3259
3338
|
const close = tool === 'channel_close';
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3339
|
+
// A name is written as a literal tune_out's channelId takes back
|
|
3340
|
+
// (quotedValue, main's #852); one no name picks alone, by its id and
|
|
3341
|
+
// server, since this branch's tune_out takes a serverId.
|
|
3342
|
+
const name = this.nameFor(entry);
|
|
3343
|
+
const names = name !== undefined
|
|
3344
|
+
? `channelId ${quotedValue(name)}`
|
|
3345
|
+
: `channelId ${quotedValue(entry.descriptor.id)}, serverId ${JSON.stringify(entry.serverId)}`;
|
|
3346
|
+
return `Call tune_out with ${names} and mode "cancel" for it to resume normal ` +
|
|
3347
|
+
`attention; cancelling delivers the diverted backlog${close ? ', and channel_close closes it after that.' : '.'}`;
|
|
3266
3348
|
}
|
|
3267
3349
|
/**
|
|
3268
3350
|
* A channel's name as nameShown gives it, quoted for a sentence: the name
|
|
@@ -3270,7 +3352,7 @@ export class ChannelRegistry {
|
|
|
3270
3352
|
*/
|
|
3271
3353
|
quotedShown(ref) {
|
|
3272
3354
|
const shown = this.nameShown(ref);
|
|
3273
|
-
return shown === channelOnServer(ref) ? shown :
|
|
3355
|
+
return shown === channelOnServer(ref) ? shown : quotedValue(shown);
|
|
3274
3356
|
}
|
|
3275
3357
|
/**
|
|
3276
3358
|
* The string that names exactly this entry by entriesNamed: its id when no
|
|
@@ -3303,7 +3385,7 @@ export class ChannelRegistry {
|
|
|
3303
3385
|
const nothing = opts.nothing ?? 'nothing ran';
|
|
3304
3386
|
const names = entries.map((e) => this.nameFor(e));
|
|
3305
3387
|
if (names.every((n) => n !== undefined)) {
|
|
3306
|
-
const listed = names.map((n) =>
|
|
3388
|
+
const listed = names.map((n) => quotedValue(n)).join(', ');
|
|
3307
3389
|
const servers = [...new Set(entries.map((e) => JSON.stringify(e.serverId)))];
|
|
3308
3390
|
// One server's two channels can answer to one name too, as `s`'s
|
|
3309
3391
|
// `s:room` and `room` both do to `s:room`; its server picks neither.
|
|
@@ -3325,8 +3407,8 @@ export class ChannelRegistry {
|
|
|
3325
3407
|
if (own === undefined && !opts.takesServerId)
|
|
3326
3408
|
return `- ${where}: no channelId reaches it alone.`;
|
|
3327
3409
|
return own !== undefined
|
|
3328
|
-
? `- ${where}: call again with channelId ${
|
|
3329
|
-
: `- ${where}: call again with channelId ${
|
|
3410
|
+
? `- ${where}: call again with channelId ${quotedValue(own)}.`
|
|
3411
|
+
: `- ${where}: call again with channelId ${quotedValue(e.descriptor.id)} and serverId ${JSON.stringify(e.serverId)}.`;
|
|
3330
3412
|
});
|
|
3331
3413
|
const count = entries.length === 2 ? 'either of two channels' : `any of ${entries.length} channels`;
|
|
3332
3414
|
return `Channel ${JSON.stringify(name)} could mean ${count}, so ${nothing}:\n${lines.join('\n')}`;
|
|
@@ -3364,7 +3446,7 @@ export class ChannelRegistry {
|
|
|
3364
3446
|
if (!alone)
|
|
3365
3447
|
return 'channel_list shows the channels and their servers.';
|
|
3366
3448
|
const one = named[0];
|
|
3367
|
-
const names = `${JSON.stringify(channelId)} alone names channel ${
|
|
3449
|
+
const names = `${JSON.stringify(channelId)} alone names channel ${quotedValue(one.descriptor.id)} on server `
|
|
3368
3450
|
+ JSON.stringify(one.serverId);
|
|
3369
3451
|
return takesServerId ? `${names}: for that one, call again with serverId left out.` : `${names}.`;
|
|
3370
3452
|
}
|
|
@@ -3376,7 +3458,7 @@ export class ChannelRegistry {
|
|
|
3376
3458
|
const places = elsewhere.map((e) => {
|
|
3377
3459
|
const at = `on server ${JSON.stringify(e.serverId)}`;
|
|
3378
3460
|
const name = this.nameFor(e);
|
|
3379
|
-
return name === undefined ? `${at} (no channelId reaches it alone)` : `${at} as ${
|
|
3461
|
+
return name === undefined ? `${at} (no channelId reaches it alone)` : `${at} as ${quotedValue(name)}`;
|
|
3380
3462
|
});
|
|
3381
3463
|
return `It's ${places.length === 1 ? places[0] : `${places.slice(0, -1).join(', ')} and ${places.at(-1)}`}.`;
|
|
3382
3464
|
}
|
|
@@ -3639,19 +3721,22 @@ export class ChannelRegistry {
|
|
|
3639
3721
|
* before any other spelling (ownChannelNamed).
|
|
3640
3722
|
*/
|
|
3641
3723
|
resolveProseTarget(spec, opts) {
|
|
3642
|
-
const
|
|
3724
|
+
const name = spec.trim();
|
|
3725
|
+
if (name === NO_CHANNEL_EXAMPLE || name === NO_PERSON_EXAMPLE)
|
|
3726
|
+
return { error: `${name} is the example's placeholder: write the name it stands for in its place` };
|
|
3727
|
+
const own = this.ownChannelNamed(name, opts?.own);
|
|
3643
3728
|
let resolved;
|
|
3644
3729
|
if (own === undefined) {
|
|
3645
|
-
resolved = this.resolveTargetByName(
|
|
3730
|
+
resolved = this.resolveTargetByName(name);
|
|
3646
3731
|
}
|
|
3647
3732
|
else if ('neither' in own) {
|
|
3648
|
-
resolved = { error: `"${
|
|
3733
|
+
resolved = { error: `"${name}" is the id of this turn's channels on more than one server`, candidates: this.ownCandidates(own.neither).map((c) => c.prose) };
|
|
3649
3734
|
}
|
|
3650
3735
|
else {
|
|
3651
3736
|
const entry = this.entryAt(own.channel);
|
|
3652
3737
|
resolved = entry
|
|
3653
3738
|
? { channelId: entry.descriptor.id, serverId: entry.serverId, label: entry.descriptor.label }
|
|
3654
|
-
: { error: `${this.ownChannelGoneText(
|
|
3739
|
+
: { error: `${this.ownChannelGoneText(name, own.channel)}, and nothing can be sent there until that server registers it again` };
|
|
3655
3740
|
}
|
|
3656
3741
|
if (!opts?.forSpeech || 'error' in resolved)
|
|
3657
3742
|
return resolved;
|
|
@@ -3704,7 +3789,7 @@ export class ChannelRegistry {
|
|
|
3704
3789
|
return undefined;
|
|
3705
3790
|
if ('neither' in mine) {
|
|
3706
3791
|
const done = tool === 'publish' ? 'was posted' : tool === 'deliver' ? 'was delivered' : 'ran';
|
|
3707
|
-
return { error: this.ownNeitherRefusal(name, mine.neither, done) };
|
|
3792
|
+
return { error: this.ownNeitherRefusal(name, mine.neither, done, false) };
|
|
3708
3793
|
}
|
|
3709
3794
|
return this.entryAt(mine.channel) ? { channel: mine.channel } : { error: this.ownChannelGoneRefusal(name, mine.channel, tool) };
|
|
3710
3795
|
}
|
|
@@ -3721,32 +3806,73 @@ export class ChannelRegistry {
|
|
|
3721
3806
|
/**
|
|
3722
3807
|
* The turn's channels a name means neither of (ownChannelNamed), each by a
|
|
3723
3808
|
* string that reaches it outside the turn's rule: its server-qualified
|
|
3724
|
-
* name, where that picks it alone, since its id is the name refused.
|
|
3725
|
-
*
|
|
3726
|
-
*
|
|
3809
|
+
* name, where that picks it alone, since its id is the name refused. `text`
|
|
3810
|
+
* is that name, which a refusal writes as JSON; `prose`, which a `>>`
|
|
3811
|
+
* target reads, is that name where it shows as itself, else its id and
|
|
3812
|
+
* server. One no string picks alone, and one its server no longer
|
|
3813
|
+
* registers, are named by id and server in both, saying so. `reaches` says
|
|
3814
|
+
* which a name reaches.
|
|
3727
3815
|
*/
|
|
3728
3816
|
ownCandidates(channels) {
|
|
3729
3817
|
return channels.map((c) => {
|
|
3730
3818
|
const entry = this.entryAt(c);
|
|
3731
|
-
if (!entry)
|
|
3732
|
-
|
|
3819
|
+
if (!entry) {
|
|
3820
|
+
const text = `${channelOnServer(c)} (its server no longer registers it)`;
|
|
3821
|
+
return { text, prose: text, reaches: false, byServer: false };
|
|
3822
|
+
}
|
|
3733
3823
|
const qualified = qualifiedChannelName(c);
|
|
3734
3824
|
const picked = this.entriesNamed(qualified);
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3825
|
+
if (picked.length !== 1 || picked[0] !== entry) {
|
|
3826
|
+
const text = `${channelOnServer(c)} (no channelId reaches it alone)`;
|
|
3827
|
+
return { text, prose: text, reaches: false, byServer: true };
|
|
3828
|
+
}
|
|
3829
|
+
return { text: qualified, prose: showsAsItself(qualified) ? qualified : channelOnServer(c), reaches: true, byServer: false };
|
|
3738
3830
|
});
|
|
3739
3831
|
}
|
|
3832
|
+
/**
|
|
3833
|
+
* What a refusal listing these candidates says of each one that is a
|
|
3834
|
+
* channel shown by its id and server: the channelId channel_publish reaches
|
|
3835
|
+
* it by, or that no channelId reaches it alone. That is its server-qualified
|
|
3836
|
+
* name where that picks it alone, since a bare id is what a turn's own
|
|
3837
|
+
* channels answer to first, whether or not their server still registers
|
|
3838
|
+
* them (ownChannelNamed), and else its id where that picks it alone and
|
|
3839
|
+
* isn't empty (channel_publish refuses an empty one). It leaves out a channel no
|
|
3840
|
+
* longer registered and a candidate that already says no channelId reaches
|
|
3841
|
+
* it alone, and is empty when nothing is left.
|
|
3842
|
+
*/
|
|
3843
|
+
publishRouteFor(candidates) {
|
|
3844
|
+
const parts = new Set();
|
|
3845
|
+
for (const candidate of candidates) {
|
|
3846
|
+
const shown = CHANNEL_ON_SERVER_AT_END.exec(candidate);
|
|
3847
|
+
if (!shown)
|
|
3848
|
+
continue;
|
|
3849
|
+
const ref = { channelId: JSON.parse(shown[1]), serverId: JSON.parse(shown[2]) };
|
|
3850
|
+
const entry = this.entryAt(ref);
|
|
3851
|
+
if (!entry)
|
|
3852
|
+
continue;
|
|
3853
|
+
const name = [qualifiedChannelName(ref), ref.channelId].find((n) => {
|
|
3854
|
+
const named = n === '' ? [] : this.entriesNamed(n);
|
|
3855
|
+
return named.length === 1 && named[0] === entry;
|
|
3856
|
+
});
|
|
3857
|
+
parts.add(name !== undefined
|
|
3858
|
+
? `channel_publish reaches ${channelOnServer(ref)} with channelId ${quotedValue(name)}`
|
|
3859
|
+
: `no channelId reaches ${channelOnServer(ref)} alone`);
|
|
3860
|
+
}
|
|
3861
|
+
return parts.size ? ` For a channel shown by its id and server: ${[...parts].join('; ')}.` : '';
|
|
3862
|
+
}
|
|
3740
3863
|
/** A tool's refusal of a name the turn's channels on more than one server
|
|
3741
3864
|
* answer to by their id (ownChannelNamed's neither). */
|
|
3742
|
-
ownNeitherRefusal(name, channels, done) {
|
|
3865
|
+
ownNeitherRefusal(name, channels, done, takesServer = true) {
|
|
3743
3866
|
const candidates = this.ownCandidates(channels);
|
|
3744
|
-
const named = candidates.filter((c) => c.reaches).map((c) =>
|
|
3867
|
+
const named = candidates.filter((c) => c.reaches).map((c) => quotedValue(c.text));
|
|
3745
3868
|
const pick = named.length === candidates.length ? ' Call again with channelId set to one of them.'
|
|
3746
3869
|
: named.length === 1 ? ` Call again with channelId ${named[0]}.`
|
|
3747
3870
|
: named.length > 1 ? ` Call again with channelId set to one of ${named.join(', ')}.` : '';
|
|
3748
|
-
const listed = candidates.map((c) => (c.reaches ?
|
|
3749
|
-
|
|
3871
|
+
const listed = candidates.map((c) => (c.reaches ? quotedValue(c.text) : c.text)).join(', ');
|
|
3872
|
+
const byServer = takesServer && candidates.some((c) => c.byServer)
|
|
3873
|
+
? ' For a channel shown by its id and server, call again with that channelId and that serverId.'
|
|
3874
|
+
: '';
|
|
3875
|
+
return `"${name}" is the id of this turn's channels on more than one server (${listed}), so nothing ${done}.${pick}${byServer}`;
|
|
3750
3876
|
}
|
|
3751
3877
|
resolveTargetByName(spec) {
|
|
3752
3878
|
const entries = [...this.channels.values()];
|
|
@@ -3757,7 +3883,7 @@ export class ChannelRegistry {
|
|
|
3757
3883
|
// A candidate is named by the token that picks it alone (nameFor), so
|
|
3758
3884
|
// two servers' channels with one id or one label can be told apart, or,
|
|
3759
3885
|
// where no string picks it alone, by its id and its server.
|
|
3760
|
-
const token = (e) => this.
|
|
3886
|
+
const token = (e) => this.nameShown(refOf(e));
|
|
3761
3887
|
// A DM is shown by its label, the person, where that label, given as a
|
|
3762
3888
|
// target, picks that DM. It doesn't where another channel answers to that
|
|
3763
3889
|
// label too, as two servers' DMs with one person do (a label is matched
|
|
@@ -3774,7 +3900,7 @@ export class ChannelRegistry {
|
|
|
3774
3900
|
};
|
|
3775
3901
|
const dmShown = (list) => list.map((e) => {
|
|
3776
3902
|
const label = e.descriptor.label ?? e.descriptor.id;
|
|
3777
|
-
return labelPicks(e, label) ? label : token(e);
|
|
3903
|
+
return showsAsItself(label) && labelPicks(e, label) ? label : token(e);
|
|
3778
3904
|
});
|
|
3779
3905
|
// A channel id, or its server-qualified name (entriesNamed).
|
|
3780
3906
|
const exact = this.entriesNamed(trimmed);
|
|
@@ -3879,7 +4005,7 @@ export class ChannelRegistry {
|
|
|
3879
4005
|
if (byName.length > 1) {
|
|
3880
4006
|
return {
|
|
3881
4007
|
error: `"${trimmed}" matches several channels`,
|
|
3882
|
-
candidates: byName.map((e) => `${e.descriptor.label} = ${token(e)}`),
|
|
4008
|
+
candidates: byName.map((e) => `${nameToTypeBack(e.descriptor.label ?? '')} = ${token(e)}`),
|
|
3883
4009
|
};
|
|
3884
4010
|
}
|
|
3885
4011
|
// No match: offer near-candidates (labels containing the name) so the
|
|
@@ -3887,7 +4013,7 @@ export class ChannelRegistry {
|
|
|
3887
4013
|
const near = entries
|
|
3888
4014
|
.filter((e) => norm(e.descriptor.label ?? '').includes(norm(trimmed)))
|
|
3889
4015
|
.slice(0, 5)
|
|
3890
|
-
.map((e) => `${e.descriptor.label} = ${token(e)}`);
|
|
4016
|
+
.map((e) => `${nameToTypeBack(e.descriptor.label ?? '')} = ${token(e)}`);
|
|
3891
4017
|
return { error: `no channel matches "${trimmed}"`, ...(near.length ? { candidates: near } : {}) };
|
|
3892
4018
|
}
|
|
3893
4019
|
/**
|
|
@@ -3924,7 +4050,7 @@ export class ChannelRegistry {
|
|
|
3924
4050
|
qualifiedChannelName(ref),
|
|
3925
4051
|
];
|
|
3926
4052
|
for (const c of candidates) {
|
|
3927
|
-
if (!isProseTargetToken(c) || c === '#')
|
|
4053
|
+
if (!isProseTargetToken(c) || c === '#' || !showsAsItself(c))
|
|
3928
4054
|
continue;
|
|
3929
4055
|
const r = this.resolveProseTarget(c, { forSpeech: true });
|
|
3930
4056
|
if ('channelId' in r && sameChannel(r, ref))
|
|
@@ -4192,7 +4318,7 @@ export class ChannelRegistry {
|
|
|
4192
4318
|
channelId: input.channelId,
|
|
4193
4319
|
serverId: entry.serverId,
|
|
4194
4320
|
status: alreadyDesiredOpen ? 'reconciled' : 'opened',
|
|
4195
|
-
...(result.history ? { history: result.history } : {}),
|
|
4321
|
+
...(result.history ? { history: markedHistory(result.history) } : {}),
|
|
4196
4322
|
...(result.historyTruncated ? { historyTruncated: true } : {}),
|
|
4197
4323
|
},
|
|
4198
4324
|
};
|
|
@@ -4249,13 +4375,14 @@ export class ChannelRegistry {
|
|
|
4249
4375
|
// Closed already, perhaps only by default: the agent's close is still
|
|
4250
4376
|
// its decision, and is recorded as one (setDesiredState), so a later
|
|
4251
4377
|
// registration's lift of a default leaves the channel closed.
|
|
4378
|
+
const ended = this.onToolClose?.(entry.serverId, input.channelId);
|
|
4252
4379
|
try {
|
|
4253
4380
|
this.setDesiredState(entry.serverId, input.channelId, 'closed', closeSource);
|
|
4254
4381
|
}
|
|
4255
4382
|
catch (err) {
|
|
4256
4383
|
return {
|
|
4257
4384
|
success: false,
|
|
4258
|
-
error: `Channel ${shown} is already closed, but recording the decision to close it failed: ${err.message}`,
|
|
4385
|
+
error: `Channel ${shown} is already closed, but recording the decision to close it failed: ${err.message}${ended ? ` ${ended}` : ''}`,
|
|
4259
4386
|
isError: true,
|
|
4260
4387
|
};
|
|
4261
4388
|
}
|
|
@@ -4295,12 +4422,18 @@ export class ChannelRegistry {
|
|
|
4295
4422
|
const refusal = this.tunedOutRefusal('channel_close', entry);
|
|
4296
4423
|
return isModuleOrigin ? { ...refusal, isError: false } : refusal;
|
|
4297
4424
|
}
|
|
4298
|
-
// A
|
|
4425
|
+
// A hold around this channel — the all-except scope it is the excepted
|
|
4426
|
+
// channel of — ends through the cancel flow: backlog released, end
|
|
4427
|
+
// recorded, never by overwriting state (#232 gap 9).
|
|
4428
|
+
const ended = this.onToolClose?.(entry.serverId, input.channelId);
|
|
4429
|
+
// A decision the log refused changed nothing for the channel, and the
|
|
4430
|
+
// answer says so, and what the hook ended all the same.
|
|
4299
4431
|
try {
|
|
4300
4432
|
this.setDesiredState(entry.serverId, input.channelId, 'closed', closeSource);
|
|
4301
4433
|
}
|
|
4302
4434
|
catch (err) {
|
|
4303
|
-
|
|
4435
|
+
const text = this.lifecycleFailureText('close', entry, unrecordedDecision(err));
|
|
4436
|
+
return { success: false, error: ended ? `${text} ${ended}` : text, isError: true };
|
|
4304
4437
|
}
|
|
4305
4438
|
// This channel's typing, not every channel with its id (stopTyping's
|
|
4306
4439
|
// fan-out of a bare name).
|
|
@@ -4649,13 +4782,14 @@ export class ChannelRegistry {
|
|
|
4649
4782
|
* tool, so any trailing prose this turn is NOT posted. Replaces the old
|
|
4650
4783
|
* overloaded use of `think` for staying silent.
|
|
4651
4784
|
*/
|
|
4652
|
-
handleToolSkipReply(input) {
|
|
4785
|
+
handleToolSkipReply(input, earlierWake) {
|
|
4653
4786
|
// The self-wake itself is armed by the framework (which owns the
|
|
4654
4787
|
// EventGate) BEFORE its dispatch reaches this handler, from the same
|
|
4655
4788
|
// reading, and the framework refuses a wake the host can't set before
|
|
4656
4789
|
// dispatching. A caller of this registry's handleChannelToolCall
|
|
4657
4790
|
// (AgentFramework.channels) goes past both, so its note names a wake
|
|
4658
|
-
// nothing armed.
|
|
4791
|
+
// nothing armed. The framework, which also owns the heartbeat, says
|
|
4792
|
+
// which of its wakes is due before the self-wake (`earlierWake`).
|
|
4659
4793
|
const wake = readSkipReplyWake(input);
|
|
4660
4794
|
const selfWake = wake.kind === 'wake' ? wake.seconds : undefined;
|
|
4661
4795
|
return {
|
|
@@ -4671,9 +4805,7 @@ export class ChannelRegistry {
|
|
|
4671
4805
|
// on Fable: ~60% of skip_reply result bytes were verbatim input echo).
|
|
4672
4806
|
data: {
|
|
4673
4807
|
skipped: true,
|
|
4674
|
-
note: selfWake !== undefined
|
|
4675
|
-
? `Turn ended; nothing sent. Self-wake in ~${selfWake}s unless something wakes you first.`
|
|
4676
|
-
: 'Turn ended; nothing sent.',
|
|
4808
|
+
note: skipReplyNote(selfWake, selfWake !== undefined ? earlierWake : undefined),
|
|
4677
4809
|
},
|
|
4678
4810
|
};
|
|
4679
4811
|
}
|
|
@@ -4765,9 +4897,27 @@ export class ChannelRegistry {
|
|
|
4765
4897
|
const active = this.activeChannelResolver?.(conversationId);
|
|
4766
4898
|
return active !== undefined ? active : this.globalFallbackFor(conversationId);
|
|
4767
4899
|
}
|
|
4768
|
-
/**
|
|
4900
|
+
/**
|
|
4901
|
+
* The process-global fallback for this conversation, or null when it takes
|
|
4902
|
+
* none (takesGlobalFallback): an ephemeral run's prose is its caller's
|
|
4903
|
+
* result, so neither the all-except hold's channel nor the latest inbound
|
|
4904
|
+
* message's is ever its destination.
|
|
4905
|
+
*/
|
|
4769
4906
|
globalFallbackFor(conversationId) {
|
|
4770
|
-
return this.takesGlobalFallback?.(conversationId) === false ? null : this.
|
|
4907
|
+
return this.takesGlobalFallback?.(conversationId) === false ? null : this.sharedFallback();
|
|
4908
|
+
}
|
|
4909
|
+
/**
|
|
4910
|
+
* What every conversation that takes the global fallback falls back to when
|
|
4911
|
+
* neither a home channel nor its own delivery names one: the all-except
|
|
4912
|
+
* hold's channel while one is open, else the latest delivered inbound
|
|
4913
|
+
* channel under the legacy locus rule, else none — the prose stays private
|
|
4914
|
+
* (architecture channel §3 carve-out, 2026-10-06).
|
|
4915
|
+
*/
|
|
4916
|
+
sharedFallback() {
|
|
4917
|
+
return this.focusChannelResolver?.() ?? this.fallbackLocus();
|
|
4918
|
+
}
|
|
4919
|
+
fallbackLocus() {
|
|
4920
|
+
return this.latestInboundLocus ? this.defaultPublishChannel : null;
|
|
4771
4921
|
}
|
|
4772
4922
|
/**
|
|
4773
4923
|
* Resolve a publish target to exactly one registered channel: `serverId`
|
|
@@ -5185,17 +5335,20 @@ export class ChannelRegistry {
|
|
|
5185
5335
|
if (input.channelId === '' && tool === 'channel_publish') {
|
|
5186
5336
|
return { success: false, error: `Nothing was posted: channelId is empty. ${findIds}`, isError: true };
|
|
5187
5337
|
}
|
|
5188
|
-
// Resolve channelId:
|
|
5189
|
-
//
|
|
5190
|
-
//
|
|
5191
|
-
//
|
|
5338
|
+
// Resolve channelId: a call made inside the agent's turn (inTurn) has
|
|
5339
|
+
// the turn's default applied already, where its plain speech goes, and
|
|
5340
|
+
// takes none here. Any other defaults to what the caller falls back to:
|
|
5341
|
+
// the all-except hold's channel, else the latest delivered inbound
|
|
5342
|
+
// channel under the legacy locus rule (a marker — reaction, edit,
|
|
5343
|
+
// deletion — is never that channel), which an ephemeral run never takes
|
|
5192
5344
|
// (takesGlobalFallback), so it names one.
|
|
5193
5345
|
// A channel named by a string means what it means to every channel tool
|
|
5194
5346
|
// (entriesNamed): one channel, by its id or its server-qualified name,
|
|
5195
5347
|
// and a name two servers' channels answer to is refused with them as
|
|
5196
5348
|
// candidates, never sent to the first.
|
|
5197
5349
|
const caller = origin?.kind === 'agent' ? origin.agentName : undefined;
|
|
5198
|
-
const
|
|
5350
|
+
const inTurn = origin?.kind === 'agent' && origin.inTurn === true;
|
|
5351
|
+
const fallback = caller === undefined ? this.sharedFallback() : inTurn ? null : this.globalFallbackFor(caller);
|
|
5199
5352
|
if (!(input.channelId ?? fallback)) {
|
|
5200
5353
|
return {
|
|
5201
5354
|
success: false,
|
|
@@ -5204,7 +5357,10 @@ export class ChannelRegistry {
|
|
|
5204
5357
|
: caller !== undefined && this.takesGlobalFallback?.(caller) === false
|
|
5205
5358
|
? 'Nothing was posted: name the channel with channelId. A subagent has no default channel, since its plain '
|
|
5206
5359
|
+ 'speech is returned to whoever called it, so channel_publish sends only to a channel it names.'
|
|
5207
|
-
:
|
|
5360
|
+
: caller !== undefined && inTurn && this.activeChannelResolver?.(caller) === null
|
|
5361
|
+
? `Nothing was posted: name the channel with channelId. This turn has no channel of its own, so its plain speech `
|
|
5362
|
+
+ `goes to none and channel_publish sends only to a channel it names. ${findIds}`
|
|
5363
|
+
: `Nothing was posted: channel_publish needs a \`channelId\`, and there's no channel to default to yet. ${findIds}`,
|
|
5208
5364
|
isError: true,
|
|
5209
5365
|
};
|
|
5210
5366
|
}
|
|
@@ -5228,7 +5384,7 @@ export class ChannelRegistry {
|
|
|
5228
5384
|
else if (own && 'neither' in own) {
|
|
5229
5385
|
if (seen)
|
|
5230
5386
|
seen.refusal = 'shared-id';
|
|
5231
|
-
return { success: false, error: this.ownNeitherRefusal(input.channelId, own.neither, 'was posted'), isError: true };
|
|
5387
|
+
return { success: false, error: this.ownNeitherRefusal(input.channelId, own.neither, 'was posted', false), isError: true };
|
|
5232
5388
|
}
|
|
5233
5389
|
else if (own) {
|
|
5234
5390
|
entry = this.entryAt(own.channel);
|
|
@@ -5265,7 +5421,7 @@ export class ChannelRegistry {
|
|
|
5265
5421
|
// channel with its id isn't where its message came from.
|
|
5266
5422
|
error: input.channelId === undefined
|
|
5267
5423
|
? serverGiven
|
|
5268
|
-
? `Server "${input.serverId}" has no channel
|
|
5424
|
+
? `Server "${input.serverId}" has no channel ${quotedValue(fallback.channelId)}, the id of the channel this call would default to, so nothing was posted.`
|
|
5269
5425
|
: `The channel this would default to, ${this.quotedShown(fallback)}, isn't registered now, so nothing was posted.`
|
|
5270
5426
|
// This tool's schema has no serverId, so one given here is a host's
|
|
5271
5427
|
// ref (publishForAgent), or one an agent added unasked, and the
|