@animalabs/agent-framework 0.28.0 → 0.29.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/agent.d.ts.map +1 -1
- package/dist/src/agent.js +4 -1
- 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/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/framework.d.ts +164 -36
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1120 -479
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +34 -10
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +118 -48
- 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 +170 -0
- package/dist/src/host-presentation.d.ts.map +1 -0
- package/dist/src/host-presentation.js +561 -0
- package/dist/src/host-presentation.js.map +1 -0
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- 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 +34 -6
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +131 -53
- 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-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 +180 -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 +79 -0
- package/dist/src/notice-tags.d.ts.map +1 -0
- package/dist/src/notice-tags.js +83 -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 +1 -53
- package/dist/src/silent-heartbeat.d.ts.map +1 -1
- package/dist/src/silent-heartbeat.js +56 -3
- package/dist/src/silent-heartbeat.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 +3 -2
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +24 -26
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/types/agent.d.ts +23 -9
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +43 -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/package.json +2 -2
- package/src/agent.ts +4 -1
- package/src/api/mcp-server-core.ts +3 -14
- package/src/api/server.ts +7 -3
- 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/framework.ts +1132 -492
- package/src/gate/event-gate.ts +128 -47
- 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 +605 -0
- package/src/index.ts +2 -2
- package/src/line-reader.ts +58 -0
- package/src/mcpl/channel-registry.ts +137 -52
- 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-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 +142 -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 +84 -0
- package/src/reasoning-loss-notice.ts +5 -3
- package/src/silent-heartbeat.ts +4 -3
- 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 +25 -27
- package/src/types/agent.ts +23 -9
- package/src/types/framework.ts +44 -0
- package/src/types/module.ts +23 -11
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Readable } from 'node:stream';
|
|
2
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
3
|
+
|
|
4
|
+
const LF = 0x0a;
|
|
5
|
+
const CR = 0x0d;
|
|
6
|
+
|
|
7
|
+
export function readLines(input: Readable, onLine: (line: string) => void, onEnd?: () => void): () => void {
|
|
8
|
+
const decoder = new StringDecoder('utf8');
|
|
9
|
+
let partial = '';
|
|
10
|
+
let lineEndedAtCr = false;
|
|
11
|
+
let stopped = false;
|
|
12
|
+
|
|
13
|
+
const take = (text: string) => {
|
|
14
|
+
let from = 0;
|
|
15
|
+
if (lineEndedAtCr && text.length > 0) {
|
|
16
|
+
lineEndedAtCr = false;
|
|
17
|
+
if (text.charCodeAt(0) === LF) from = 1;
|
|
18
|
+
}
|
|
19
|
+
for (let i = from; i < text.length && !stopped; i++) {
|
|
20
|
+
const code = text.charCodeAt(i);
|
|
21
|
+
if (code !== LF && code !== CR) continue;
|
|
22
|
+
const line = partial + text.slice(from, i);
|
|
23
|
+
partial = '';
|
|
24
|
+
if (code === CR) {
|
|
25
|
+
if (i + 1 === text.length) lineEndedAtCr = true;
|
|
26
|
+
else if (text.charCodeAt(i + 1) === LF) i++;
|
|
27
|
+
}
|
|
28
|
+
from = i + 1;
|
|
29
|
+
onLine(line);
|
|
30
|
+
}
|
|
31
|
+
if (!stopped) partial += text.slice(from);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const onData = (chunk: Buffer | string) => take(decoder.write(chunk));
|
|
35
|
+
const onStreamEnd = () => {
|
|
36
|
+
take(decoder.end());
|
|
37
|
+
if (!stopped && partial.length > 0) {
|
|
38
|
+
const last = partial;
|
|
39
|
+
partial = '';
|
|
40
|
+
onLine(last);
|
|
41
|
+
}
|
|
42
|
+
if (stopped) return;
|
|
43
|
+
stop();
|
|
44
|
+
onEnd?.();
|
|
45
|
+
};
|
|
46
|
+
const stop = () => {
|
|
47
|
+
stopped = true;
|
|
48
|
+
input.off('data', onData);
|
|
49
|
+
input.off('end', onStreamEnd);
|
|
50
|
+
};
|
|
51
|
+
input.on('data', onData);
|
|
52
|
+
input.on('end', onStreamEnd);
|
|
53
|
+
return stop;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function escapeLineSeparators(json: string): string {
|
|
57
|
+
return json.replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
|
|
58
|
+
}
|
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
* - Build channel context for beforeInference params
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { NOTICE_TAG } from '../notice-tags.js';
|
|
16
17
|
import type { ContentBlock } from '@animalabs/membrane';
|
|
17
|
-
import {
|
|
18
|
+
import { convertBlocks } from './convert-block.js';
|
|
19
|
+
import { cutToFit, markedText, markNoticeOpenings, nameToTypeBack, quotedValue, showsAsItself, suppliedValue } from '../supplied-text.js';
|
|
18
20
|
import type { JsStore } from '@animalabs/chronicle';
|
|
19
21
|
import { isDeepStrictEqual } from 'node:util';
|
|
20
22
|
import { ensureStateRegistered } from '../module-registry.js';
|
|
@@ -45,7 +47,7 @@ import { RefusalLines } from './refusal-lines.js';
|
|
|
45
47
|
import { CapabilityGrant } from './capability-grant.js';
|
|
46
48
|
import type { InboundSource } from './inbound-source.js';
|
|
47
49
|
import { McplRequestError, type McplServerConnection } from './server-connection.js';
|
|
48
|
-
import { readSkipReplyWake } from './skip-reply-wake.js';
|
|
50
|
+
import { readSkipReplyWake, skipReplyNote, type SkipReplyEarlierWake } from './skip-reply-wake.js';
|
|
49
51
|
import { isProseTargetToken } from './prose-grammar.js';
|
|
50
52
|
|
|
51
53
|
// ============================================================================
|
|
@@ -643,19 +645,21 @@ const CHANNEL_TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
643
645
|
// Constructor Options
|
|
644
646
|
// ============================================================================
|
|
645
647
|
|
|
646
|
-
/** Bound connector values for notices, logs and an outcome's reason
|
|
647
|
-
*
|
|
648
|
-
*
|
|
648
|
+
/** Bound connector values for notices, logs and an outcome's reason, on one
|
|
649
|
+
* line with their notice openings marked (suppliedValue). A PublishOutcome
|
|
650
|
+
* keeps the connector's structured data (`detail`) as the connection gives
|
|
651
|
+
* it, its strings redacted (redactedData). */
|
|
649
652
|
export function connectorText(value: unknown): string {
|
|
650
653
|
let text: string;
|
|
651
654
|
try {
|
|
652
655
|
text = typeof value === 'string' ? value : JSON.stringify(value) ?? String(value);
|
|
653
656
|
} catch {
|
|
654
|
-
return
|
|
657
|
+
return `[${NOTICE_TAG.unserializableConnectorValue}]`;
|
|
655
658
|
}
|
|
656
659
|
const limit = 2000;
|
|
657
|
-
const suffix =
|
|
658
|
-
|
|
660
|
+
const suffix = `… [${NOTICE_TAG.truncated}]`;
|
|
661
|
+
const shown = suppliedValue(text);
|
|
662
|
+
return shown.length > limit ? cutToFit(shown, limit - suffix.length) + suffix : shown;
|
|
659
663
|
}
|
|
660
664
|
|
|
661
665
|
/**
|
|
@@ -688,6 +692,9 @@ export function sameChannel(a: ChannelRef | null | undefined, b: ChannelRef | nu
|
|
|
688
692
|
return a.serverId === b.serverId && a.channelId === b.channelId;
|
|
689
693
|
}
|
|
690
694
|
|
|
695
|
+
export const NO_CHANNEL_EXAMPLE = '#<channel>';
|
|
696
|
+
export const NO_PERSON_EXAMPLE = '@<person>';
|
|
697
|
+
|
|
691
698
|
/**
|
|
692
699
|
* A channel's server-qualified name, `serverId:channelId`. A name given as a
|
|
693
700
|
* string (a tool's channelId, a `>>` target, a host's call) may take this
|
|
@@ -699,12 +706,32 @@ export function qualifiedChannelName(ref: ChannelRef): string {
|
|
|
699
706
|
return `${ref.serverId}:${ref.channelId}`;
|
|
700
707
|
}
|
|
701
708
|
|
|
709
|
+
function markedHistory(history: readonly unknown[]): unknown[] {
|
|
710
|
+
return history.map((item) => {
|
|
711
|
+
if (item === null || typeof item !== 'object' || Array.isArray(item) || !('content' in item)) return item;
|
|
712
|
+
const { content } = item as { content: unknown };
|
|
713
|
+
if (Array.isArray(content)) return { ...item, content: markNoticeOpenings(content.map(withEmbeddedTextMarked) as object[]) };
|
|
714
|
+
return typeof content === 'string' ? { ...item, content: markedText(content) } : item;
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function withEmbeddedTextMarked(block: unknown): unknown {
|
|
719
|
+
if (block === null || typeof block !== 'object' || (block as { type?: unknown }).type !== 'resource') return block;
|
|
720
|
+
const { resource } = block as { resource?: unknown };
|
|
721
|
+
if (resource === null || typeof resource !== 'object' || typeof (resource as { text?: unknown }).text !== 'string') return block;
|
|
722
|
+
return { ...block, resource: { ...resource, text: markedText((resource as { text: string }).text) } };
|
|
723
|
+
}
|
|
724
|
+
|
|
702
725
|
/** A channel told apart by its id and its server, for one no string picks
|
|
703
|
-
* alone: `"b:room" on server "a"
|
|
726
|
+
* alone or whose name wouldn't show as itself: `"b:room" on server "a"`,
|
|
727
|
+
* each a JSON literal a channel tool's channelId and serverId take back
|
|
728
|
+
* (quotedValue). No input takes the whole form for a name. */
|
|
704
729
|
function channelOnServer(ref: ChannelRef): string {
|
|
705
|
-
return `${
|
|
730
|
+
return `${quotedValue(ref.channelId)} on server ${quotedValue(ref.serverId)}`;
|
|
706
731
|
}
|
|
707
732
|
|
|
733
|
+
const CHANNEL_ON_SERVER_AT_END = /("(?:[^"\\]|\\.)*") on server ("(?:[^"\\]|\\.)*")$/;
|
|
734
|
+
|
|
708
735
|
/**
|
|
709
736
|
* What one channels/publish attempt established (ChannelRegistry.publish).
|
|
710
737
|
* - `delivered`: the connector returned `delivered: true`. A historical
|
|
@@ -1089,6 +1116,10 @@ export type ChannelToolOrigin =
|
|
|
1089
1116
|
* name one of them answers to by its id means that channel
|
|
1090
1117
|
* (ChannelRegistry.ownChannelNamed). */
|
|
1091
1118
|
own?: readonly ChannelRef[];
|
|
1119
|
+
/** For a skip_reply whose self-wake the framework armed: the heartbeat's
|
|
1120
|
+
* wake due before it, when there is one, which its confirmation names
|
|
1121
|
+
* (skipReplyNote). */
|
|
1122
|
+
earlierWake?: SkipReplyEarlierWake;
|
|
1092
1123
|
};
|
|
1093
1124
|
|
|
1094
1125
|
/** The machine decision sources a module-origin channel_close may record.
|
|
@@ -1894,7 +1925,7 @@ export class ChannelRegistry {
|
|
|
1894
1925
|
}
|
|
1895
1926
|
}
|
|
1896
1927
|
if (message.tags) message.tags = expandCoreTags(message.tags);
|
|
1897
|
-
const convertedContent: ContentBlock[] = message.content
|
|
1928
|
+
const convertedContent: ContentBlock[] = convertBlocks(message.content, 'attachment on channel message', serverId);
|
|
1898
1929
|
if (!emptyAllowed && isVisiblyEmptyConversion(message.content, convertedContent)) {
|
|
1899
1930
|
rejectEmpty();
|
|
1900
1931
|
continue;
|
|
@@ -2386,7 +2417,10 @@ export class ChannelRegistry {
|
|
|
2386
2417
|
return this.handleToolJournal(input as { content?: string });
|
|
2387
2418
|
|
|
2388
2419
|
case 'skip_reply':
|
|
2389
|
-
return this.handleToolSkipReply(
|
|
2420
|
+
return this.handleToolSkipReply(
|
|
2421
|
+
input as { reason?: string; wake_in_seconds?: unknown },
|
|
2422
|
+
origin?.kind === 'agent' ? origin.earlierWake : undefined,
|
|
2423
|
+
);
|
|
2390
2424
|
|
|
2391
2425
|
default:
|
|
2392
2426
|
// Not every agent is given every channel tool, so the refusal points
|
|
@@ -2442,7 +2476,8 @@ export class ChannelRegistry {
|
|
|
2442
2476
|
// declared it inbound-only, since speech isn't posted there): a conversation
|
|
2443
2477
|
// fork's home channel, else this turn's channel (single trunk agent — item-3
|
|
2444
2478
|
// redux: its locus, else its triggering channel), else the global default
|
|
2445
|
-
// (
|
|
2479
|
+
// (a no-trigger turn's, but not a heartbeat's, whose turn takes none).
|
|
2480
|
+
// Without this, a fork
|
|
2446
2481
|
// was advertised the global locus but published somewhere else; and a trunk
|
|
2447
2482
|
// agent was told the wrong channel under concurrency.
|
|
2448
2483
|
// Each lookup is keyed on whether a name was given, not its truthiness:
|
|
@@ -3921,12 +3956,20 @@ export class ChannelRegistry {
|
|
|
3921
3956
|
* name, only where no registered channel takes that name, as a channel
|
|
3922
3957
|
* tool reads it or a `>>` target does (by label or raw id too), so the
|
|
3923
3958
|
* name can't send anything to another channel; else by its id and server.
|
|
3959
|
+
* A name that wouldn't show as itself (showsAsItself: one holding a line
|
|
3960
|
+
* break, a notice opening, a quote, a backslash or a lone surrogate, or
|
|
3961
|
+
* whitespace at its edges) is shown by its id and its server too, which
|
|
3962
|
+
* the channel tools take exactly and the prose resolver doesn't, so no name
|
|
3963
|
+
* shown can be another channel's.
|
|
3924
3964
|
*/
|
|
3925
3965
|
nameShown(ref: ChannelRef): string {
|
|
3926
3966
|
const entry = this.entryAt(ref);
|
|
3927
|
-
if (entry)
|
|
3967
|
+
if (entry) {
|
|
3968
|
+
const name = this.nameFor(entry);
|
|
3969
|
+
return name !== undefined && showsAsItself(name) ? name : channelOnServer(ref);
|
|
3970
|
+
}
|
|
3928
3971
|
for (const name of [ref.channelId, qualifiedChannelName(ref)]) {
|
|
3929
|
-
if (this.entriesNamed(name).length === 0 && !('channelId' in this.resolveProseTarget(name))) return name;
|
|
3972
|
+
if (showsAsItself(name) && this.entriesNamed(name).length === 0 && !('channelId' in this.resolveProseTarget(name))) return name;
|
|
3930
3973
|
}
|
|
3931
3974
|
return channelOnServer(ref);
|
|
3932
3975
|
}
|
|
@@ -3966,7 +4009,7 @@ export class ChannelRegistry {
|
|
|
3966
4009
|
private tuneOutEndText(entry: ChannelEntry, tool?: string): string {
|
|
3967
4010
|
const close = tool === 'channel_close';
|
|
3968
4011
|
return this.nameFor(entry) !== undefined
|
|
3969
|
-
? `Call tune_out with channelId ${this.
|
|
4012
|
+
? `Call tune_out with channelId ${quotedValue(this.nameFor(entry)!)} and mode "cancel" for it to resume normal ` +
|
|
3970
4013
|
`attention; cancelling delivers the diverted backlog${close ? ', and channel_close closes it after that.' : '.'}`
|
|
3971
4014
|
: 'No channelId names it alone, so tune_out can\'t cancel it: its tune-out ends by itself when its duration ' +
|
|
3972
4015
|
'runs out, or at the first wake past its wake budget, and its backlog is delivered then.' +
|
|
@@ -3979,7 +4022,7 @@ export class ChannelRegistry {
|
|
|
3979
4022
|
*/
|
|
3980
4023
|
quotedShown(ref: ChannelRef): string {
|
|
3981
4024
|
const shown = this.nameShown(ref);
|
|
3982
|
-
return shown === channelOnServer(ref) ? shown :
|
|
4025
|
+
return shown === channelOnServer(ref) ? shown : quotedValue(shown);
|
|
3983
4026
|
}
|
|
3984
4027
|
|
|
3985
4028
|
/**
|
|
@@ -4017,7 +4060,7 @@ export class ChannelRegistry {
|
|
|
4017
4060
|
const nothing = opts.nothing ?? 'nothing ran';
|
|
4018
4061
|
const names = entries.map((e) => this.nameFor(e));
|
|
4019
4062
|
if (names.every((n) => n !== undefined)) {
|
|
4020
|
-
const listed = names.map((n) =>
|
|
4063
|
+
const listed = names.map((n) => quotedValue(n!)).join(', ');
|
|
4021
4064
|
const servers = [...new Set(entries.map((e) => JSON.stringify(e.serverId)))];
|
|
4022
4065
|
// One server's two channels can answer to one name too, as `s`'s
|
|
4023
4066
|
// `s:room` and `room` both do to `s:room`; its server picks neither.
|
|
@@ -4038,8 +4081,8 @@ export class ChannelRegistry {
|
|
|
4038
4081
|
const own = names[i];
|
|
4039
4082
|
if (own === undefined && !opts.takesServerId) return `- ${where}: no channelId reaches it alone.`;
|
|
4040
4083
|
return own !== undefined
|
|
4041
|
-
? `- ${where}: call again with channelId ${
|
|
4042
|
-
: `- ${where}: call again with channelId ${
|
|
4084
|
+
? `- ${where}: call again with channelId ${quotedValue(own)}.`
|
|
4085
|
+
: `- ${where}: call again with channelId ${quotedValue(e.descriptor.id)} and serverId ${JSON.stringify(e.serverId)}.`;
|
|
4043
4086
|
});
|
|
4044
4087
|
const count = entries.length === 2 ? 'either of two channels' : `any of ${entries.length} channels`;
|
|
4045
4088
|
return `Channel ${JSON.stringify(name)} could mean ${count}, so ${nothing}:\n${lines.join('\n')}`;
|
|
@@ -4078,7 +4121,7 @@ export class ChannelRegistry {
|
|
|
4078
4121
|
// so the text says whose.
|
|
4079
4122
|
if (!alone) return 'channel_list shows the channels and their servers.';
|
|
4080
4123
|
const one = named[0]!;
|
|
4081
|
-
const names = `${JSON.stringify(channelId)} alone names channel ${
|
|
4124
|
+
const names = `${JSON.stringify(channelId)} alone names channel ${quotedValue(one.descriptor.id)} on server `
|
|
4082
4125
|
+ JSON.stringify(one.serverId);
|
|
4083
4126
|
return takesServerId ? `${names}: for that one, call again with serverId left out.` : `${names}.`;
|
|
4084
4127
|
}
|
|
@@ -4090,7 +4133,7 @@ export class ChannelRegistry {
|
|
|
4090
4133
|
const places = elsewhere.map((e) => {
|
|
4091
4134
|
const at = `on server ${JSON.stringify(e.serverId)}`;
|
|
4092
4135
|
const name = this.nameFor(e);
|
|
4093
|
-
return name === undefined ? `${at} (no channelId reaches it alone)` : `${at} as ${
|
|
4136
|
+
return name === undefined ? `${at} (no channelId reaches it alone)` : `${at} as ${quotedValue(name)}`;
|
|
4094
4137
|
});
|
|
4095
4138
|
return `It's ${places.length === 1 ? places[0] : `${places.slice(0, -1).join(', ')} and ${places.at(-1)}`}.`;
|
|
4096
4139
|
}
|
|
@@ -4394,17 +4437,19 @@ export class ChannelRegistry {
|
|
|
4394
4437
|
spec: string,
|
|
4395
4438
|
opts?: { forSpeech?: boolean; own?: readonly ChannelRef[] },
|
|
4396
4439
|
): PublishDestination | { error: string; candidates?: string[]; refusedChannelId?: string } {
|
|
4397
|
-
const
|
|
4440
|
+
const name = spec.trim();
|
|
4441
|
+
if (name === NO_CHANNEL_EXAMPLE || name === NO_PERSON_EXAMPLE) return { error: `${name} is the example's placeholder: write the name it stands for in its place` };
|
|
4442
|
+
const own = this.ownChannelNamed(name, opts?.own);
|
|
4398
4443
|
let resolved: PublishDestination | { error: string; candidates?: string[] };
|
|
4399
4444
|
if (own === undefined) {
|
|
4400
|
-
resolved = this.resolveTargetByName(
|
|
4445
|
+
resolved = this.resolveTargetByName(name);
|
|
4401
4446
|
} else if ('neither' in own) {
|
|
4402
|
-
resolved = { error: `"${
|
|
4447
|
+
resolved = { error: `"${name}" is the id of this turn's channels on more than one server`, candidates: this.ownCandidates(own.neither).map((c) => c.prose) };
|
|
4403
4448
|
} else {
|
|
4404
4449
|
const entry = this.entryAt(own.channel);
|
|
4405
4450
|
resolved = entry
|
|
4406
4451
|
? { channelId: entry.descriptor.id, serverId: entry.serverId, label: entry.descriptor.label }
|
|
4407
|
-
: { error: `${this.ownChannelGoneText(
|
|
4452
|
+
: { error: `${this.ownChannelGoneText(name, own.channel)}, and nothing can be sent there until that server registers it again` };
|
|
4408
4453
|
}
|
|
4409
4454
|
if (!opts?.forSpeech || 'error' in resolved) return resolved;
|
|
4410
4455
|
const entry = this.entryAt(resolved);
|
|
@@ -4463,7 +4508,7 @@ export class ChannelRegistry {
|
|
|
4463
4508
|
if (!mine) return undefined;
|
|
4464
4509
|
if ('neither' in mine) {
|
|
4465
4510
|
const done = tool === 'publish' ? 'was posted' : tool === 'deliver' ? 'was delivered' : 'ran';
|
|
4466
|
-
return { error: this.ownNeitherRefusal(name, mine.neither, done) };
|
|
4511
|
+
return { error: this.ownNeitherRefusal(name, mine.neither, done, false) };
|
|
4467
4512
|
}
|
|
4468
4513
|
return this.entryAt(mine.channel) ? { channel: mine.channel } : { error: this.ownChannelGoneRefusal(name, mine.channel, tool) };
|
|
4469
4514
|
}
|
|
@@ -4482,32 +4527,73 @@ export class ChannelRegistry {
|
|
|
4482
4527
|
/**
|
|
4483
4528
|
* The turn's channels a name means neither of (ownChannelNamed), each by a
|
|
4484
4529
|
* string that reaches it outside the turn's rule: its server-qualified
|
|
4485
|
-
* name, where that picks it alone, since its id is the name refused.
|
|
4486
|
-
*
|
|
4487
|
-
*
|
|
4530
|
+
* name, where that picks it alone, since its id is the name refused. `text`
|
|
4531
|
+
* is that name, which a refusal writes as JSON; `prose`, which a `>>`
|
|
4532
|
+
* target reads, is that name where it shows as itself, else its id and
|
|
4533
|
+
* server. One no string picks alone, and one its server no longer
|
|
4534
|
+
* registers, are named by id and server in both, saying so. `reaches` says
|
|
4535
|
+
* which a name reaches.
|
|
4488
4536
|
*/
|
|
4489
|
-
private ownCandidates(channels: readonly ChannelRef[]): Array<{ text: string; reaches: boolean }> {
|
|
4537
|
+
private ownCandidates(channels: readonly ChannelRef[]): Array<{ text: string; prose: string; reaches: boolean; byServer: boolean }> {
|
|
4490
4538
|
return channels.map((c) => {
|
|
4491
4539
|
const entry = this.entryAt(c);
|
|
4492
|
-
if (!entry)
|
|
4540
|
+
if (!entry) {
|
|
4541
|
+
const text = `${channelOnServer(c)} (its server no longer registers it)`;
|
|
4542
|
+
return { text, prose: text, reaches: false, byServer: false };
|
|
4543
|
+
}
|
|
4493
4544
|
const qualified = qualifiedChannelName(c);
|
|
4494
4545
|
const picked = this.entriesNamed(qualified);
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4546
|
+
if (picked.length !== 1 || picked[0] !== entry) {
|
|
4547
|
+
const text = `${channelOnServer(c)} (no channelId reaches it alone)`;
|
|
4548
|
+
return { text, prose: text, reaches: false, byServer: true };
|
|
4549
|
+
}
|
|
4550
|
+
return { text: qualified, prose: showsAsItself(qualified) ? qualified : channelOnServer(c), reaches: true, byServer: false };
|
|
4498
4551
|
});
|
|
4499
4552
|
}
|
|
4500
4553
|
|
|
4554
|
+
/**
|
|
4555
|
+
* What a refusal listing these candidates says of each one that is a
|
|
4556
|
+
* channel shown by its id and server: the channelId channel_publish reaches
|
|
4557
|
+
* it by, or that no channelId reaches it alone. That is its server-qualified
|
|
4558
|
+
* name where that picks it alone, since a bare id is what a turn's own
|
|
4559
|
+
* channels answer to first, whether or not their server still registers
|
|
4560
|
+
* them (ownChannelNamed), and else its id where that picks it alone and
|
|
4561
|
+
* isn't empty (channel_publish refuses an empty one). It leaves out a channel no
|
|
4562
|
+
* longer registered and a candidate that already says no channelId reaches
|
|
4563
|
+
* it alone, and is empty when nothing is left.
|
|
4564
|
+
*/
|
|
4565
|
+
publishRouteFor(candidates: readonly string[]): string {
|
|
4566
|
+
const parts = new Set<string>();
|
|
4567
|
+
for (const candidate of candidates) {
|
|
4568
|
+
const shown = CHANNEL_ON_SERVER_AT_END.exec(candidate);
|
|
4569
|
+
if (!shown) continue;
|
|
4570
|
+
const ref: ChannelRef = { channelId: JSON.parse(shown[1]!) as string, serverId: JSON.parse(shown[2]!) as string };
|
|
4571
|
+
const entry = this.entryAt(ref);
|
|
4572
|
+
if (!entry) continue;
|
|
4573
|
+
const name = [qualifiedChannelName(ref), ref.channelId].find((n) => {
|
|
4574
|
+
const named = n === '' ? [] : this.entriesNamed(n);
|
|
4575
|
+
return named.length === 1 && named[0] === entry;
|
|
4576
|
+
});
|
|
4577
|
+
parts.add(name !== undefined
|
|
4578
|
+
? `channel_publish reaches ${channelOnServer(ref)} with channelId ${quotedValue(name)}`
|
|
4579
|
+
: `no channelId reaches ${channelOnServer(ref)} alone`);
|
|
4580
|
+
}
|
|
4581
|
+
return parts.size ? ` For a channel shown by its id and server: ${[...parts].join('; ')}.` : '';
|
|
4582
|
+
}
|
|
4583
|
+
|
|
4501
4584
|
/** A tool's refusal of a name the turn's channels on more than one server
|
|
4502
4585
|
* answer to by their id (ownChannelNamed's neither). */
|
|
4503
|
-
private ownNeitherRefusal(name: string, channels: readonly ChannelRef[], done: string): string {
|
|
4586
|
+
private ownNeitherRefusal(name: string, channels: readonly ChannelRef[], done: string, takesServer = true): string {
|
|
4504
4587
|
const candidates = this.ownCandidates(channels);
|
|
4505
|
-
const named = candidates.filter((c) => c.reaches).map((c) =>
|
|
4588
|
+
const named = candidates.filter((c) => c.reaches).map((c) => quotedValue(c.text));
|
|
4506
4589
|
const pick = named.length === candidates.length ? ' Call again with channelId set to one of them.'
|
|
4507
4590
|
: named.length === 1 ? ` Call again with channelId ${named[0]}.`
|
|
4508
4591
|
: named.length > 1 ? ` Call again with channelId set to one of ${named.join(', ')}.` : '';
|
|
4509
|
-
const listed = candidates.map((c) => (c.reaches ?
|
|
4510
|
-
|
|
4592
|
+
const listed = candidates.map((c) => (c.reaches ? quotedValue(c.text) : c.text)).join(', ');
|
|
4593
|
+
const byServer = takesServer && candidates.some((c) => c.byServer)
|
|
4594
|
+
? ' For a channel shown by its id and server, call again with that channelId and that serverId.'
|
|
4595
|
+
: '';
|
|
4596
|
+
return `"${name}" is the id of this turn's channels on more than one server (${listed}), so nothing ${done}.${pick}${byServer}`;
|
|
4511
4597
|
}
|
|
4512
4598
|
|
|
4513
4599
|
private resolveTargetByName(
|
|
@@ -4521,7 +4607,7 @@ export class ChannelRegistry {
|
|
|
4521
4607
|
// A candidate is named by the token that picks it alone (nameFor), so
|
|
4522
4608
|
// two servers' channels with one id or one label can be told apart, or,
|
|
4523
4609
|
// where no string picks it alone, by its id and its server.
|
|
4524
|
-
const token = (e: ChannelEntry): string => this.
|
|
4610
|
+
const token = (e: ChannelEntry): string => this.nameShown(refOf(e));
|
|
4525
4611
|
// A DM is shown by its label, the person, where that label, given as a
|
|
4526
4612
|
// target, picks that DM. It doesn't where another channel answers to that
|
|
4527
4613
|
// label too, as two servers' DMs with one person do (a label is matched
|
|
@@ -4537,7 +4623,7 @@ export class ChannelRegistry {
|
|
|
4537
4623
|
};
|
|
4538
4624
|
const dmShown = (list: ChannelEntry[]): string[] => list.map((e) => {
|
|
4539
4625
|
const label = e.descriptor.label ?? e.descriptor.id;
|
|
4540
|
-
return labelPicks(e, label) ? label : token(e);
|
|
4626
|
+
return showsAsItself(label) && labelPicks(e, label) ? label : token(e);
|
|
4541
4627
|
});
|
|
4542
4628
|
|
|
4543
4629
|
// A channel id, or its server-qualified name (entriesNamed).
|
|
@@ -4655,7 +4741,7 @@ export class ChannelRegistry {
|
|
|
4655
4741
|
if (byName.length > 1) {
|
|
4656
4742
|
return {
|
|
4657
4743
|
error: `"${trimmed}" matches several channels`,
|
|
4658
|
-
candidates: byName.map((e) => `${e.descriptor.label} = ${token(e)}`),
|
|
4744
|
+
candidates: byName.map((e) => `${nameToTypeBack(e.descriptor.label ?? '')} = ${token(e)}`),
|
|
4659
4745
|
};
|
|
4660
4746
|
}
|
|
4661
4747
|
|
|
@@ -4664,7 +4750,7 @@ export class ChannelRegistry {
|
|
|
4664
4750
|
const near = entries
|
|
4665
4751
|
.filter((e) => norm(e.descriptor.label ?? '').includes(norm(trimmed)))
|
|
4666
4752
|
.slice(0, 5)
|
|
4667
|
-
.map((e) => `${e.descriptor.label} = ${token(e)}`);
|
|
4753
|
+
.map((e) => `${nameToTypeBack(e.descriptor.label ?? '')} = ${token(e)}`);
|
|
4668
4754
|
return { error: `no channel matches "${trimmed}"`, ...(near.length ? { candidates: near } : {}) };
|
|
4669
4755
|
}
|
|
4670
4756
|
|
|
@@ -4701,7 +4787,7 @@ export class ChannelRegistry {
|
|
|
4701
4787
|
qualifiedChannelName(ref),
|
|
4702
4788
|
];
|
|
4703
4789
|
for (const c of candidates) {
|
|
4704
|
-
if (!isProseTargetToken(c) || c === '#') continue;
|
|
4790
|
+
if (!isProseTargetToken(c) || c === '#' || !showsAsItself(c)) continue;
|
|
4705
4791
|
const r = this.resolveProseTarget(c, { forSpeech: true });
|
|
4706
4792
|
if ('channelId' in r && sameChannel(r, ref)) return c;
|
|
4707
4793
|
}
|
|
@@ -4990,7 +5076,7 @@ export class ChannelRegistry {
|
|
|
4990
5076
|
channelId: input.channelId,
|
|
4991
5077
|
serverId: entry.serverId,
|
|
4992
5078
|
status: alreadyDesiredOpen ? 'reconciled' : 'opened',
|
|
4993
|
-
...(result.history ? { history: result.history } : {}),
|
|
5079
|
+
...(result.history ? { history: markedHistory(result.history) } : {}),
|
|
4994
5080
|
...(result.historyTruncated ? { historyTruncated: true } : {}),
|
|
4995
5081
|
},
|
|
4996
5082
|
};
|
|
@@ -5479,13 +5565,14 @@ export class ChannelRegistry {
|
|
|
5479
5565
|
* tool, so any trailing prose this turn is NOT posted. Replaces the old
|
|
5480
5566
|
* overloaded use of `think` for staying silent.
|
|
5481
5567
|
*/
|
|
5482
|
-
private handleToolSkipReply(input: { reason?: string; wake_in_seconds?: unknown }): ToolResult {
|
|
5568
|
+
private handleToolSkipReply(input: { reason?: string; wake_in_seconds?: unknown }, earlierWake?: SkipReplyEarlierWake): ToolResult {
|
|
5483
5569
|
// The self-wake itself is armed by the framework (which owns the
|
|
5484
5570
|
// EventGate) BEFORE its dispatch reaches this handler, from the same
|
|
5485
5571
|
// reading, and the framework refuses a wake the host can't set before
|
|
5486
5572
|
// dispatching. A caller of this registry's handleChannelToolCall
|
|
5487
5573
|
// (AgentFramework.channels) goes past both, so its note names a wake
|
|
5488
|
-
// nothing armed.
|
|
5574
|
+
// nothing armed. The framework, which also owns the heartbeat, says
|
|
5575
|
+
// which of its wakes is due before the self-wake (`earlierWake`).
|
|
5489
5576
|
const wake = readSkipReplyWake(input);
|
|
5490
5577
|
const selfWake = wake.kind === 'wake' ? wake.seconds : undefined;
|
|
5491
5578
|
return {
|
|
@@ -5501,9 +5588,7 @@ export class ChannelRegistry {
|
|
|
5501
5588
|
// on Fable: ~60% of skip_reply result bytes were verbatim input echo).
|
|
5502
5589
|
data: {
|
|
5503
5590
|
skipped: true,
|
|
5504
|
-
note: selfWake !== undefined
|
|
5505
|
-
? `Turn ended; nothing sent. Self-wake in ~${selfWake}s unless something wakes you first.`
|
|
5506
|
-
: 'Turn ended; nothing sent.',
|
|
5591
|
+
note: skipReplyNote(selfWake, selfWake !== undefined ? earlierWake : undefined),
|
|
5507
5592
|
},
|
|
5508
5593
|
};
|
|
5509
5594
|
}
|
|
@@ -6096,7 +6181,7 @@ export class ChannelRegistry {
|
|
|
6096
6181
|
entry = this.entryAt(serverGiven ? { serverId: input.serverId!, channelId: fallback!.channelId } : fallback!);
|
|
6097
6182
|
} else if (own && 'neither' in own) {
|
|
6098
6183
|
if (seen) seen.refusal = 'shared-id';
|
|
6099
|
-
return { success: false, error: this.ownNeitherRefusal(input.channelId, own.neither, 'was posted'), isError: true };
|
|
6184
|
+
return { success: false, error: this.ownNeitherRefusal(input.channelId, own.neither, 'was posted', false), isError: true };
|
|
6100
6185
|
} else if (own) {
|
|
6101
6186
|
entry = this.entryAt(own.channel);
|
|
6102
6187
|
if (!entry) {
|
|
@@ -6127,7 +6212,7 @@ export class ChannelRegistry {
|
|
|
6127
6212
|
// channel with its id isn't where its message came from.
|
|
6128
6213
|
error: input.channelId === undefined
|
|
6129
6214
|
? serverGiven
|
|
6130
|
-
? `Server "${input.serverId}" has no channel
|
|
6215
|
+
? `Server "${input.serverId}" has no channel ${quotedValue(fallback!.channelId)}, the id of the channel this call would default to, so nothing was posted.`
|
|
6131
6216
|
: `The channel this would default to, ${this.quotedShown(fallback!)}, isn't registered now, so nothing was posted.`
|
|
6132
6217
|
// This tool's schema has no serverId, so one given here is a host's
|
|
6133
6218
|
// ref (publishForAgent), or one an agent added unasked, and the
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
* follow-up.
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
|
+
import { NOTICE_TAG } from '../notice-tags.js';
|
|
37
38
|
import { createHash } from 'node:crypto';
|
|
38
39
|
import type { ContextStrategy } from '@animalabs/context-manager';
|
|
39
40
|
import type { ChannelDescriptor } from './types.js';
|
|
@@ -144,7 +145,7 @@ export type RoutingDecision =
|
|
|
144
145
|
const DEFAULT_IDLE_TTL_MS = 12 * 60 * 60 * 1000;
|
|
145
146
|
|
|
146
147
|
export const DEFAULT_CLOSURE_PROMPT =
|
|
147
|
-
|
|
148
|
+
`[${NOTICE_TAG.system}] This engagement is closing due to inactivity. Finalize your ` +
|
|
148
149
|
'work: post any promised results to the channel and record outstanding ' +
|
|
149
150
|
'threads as unresolved. Do not ask follow-up questions.';
|
|
150
151
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* about a malformed block holds on all three.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { NOTICE_TAG } from '../notice-tags.js';
|
|
7
8
|
import type { ContentBlock } from '@animalabs/membrane';
|
|
8
9
|
import {
|
|
9
10
|
AUDIO_NO_DATA_TEXT,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
} from './references.js';
|
|
19
20
|
import { NON_STRING_TEXT_PLACEHOLDER } from './visible-content.js';
|
|
20
21
|
import type { McplContentBlock } from './types.js';
|
|
22
|
+
import { markNoticeOpenings } from '../supplied-text.js';
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
25
|
* Convert a single MCPL wire-format content block to a membrane ContentBlock.
|
|
@@ -50,7 +52,7 @@ export function convertBlock(block: McplContentBlock, provenance: string, server
|
|
|
50
52
|
// is a claim, one the reference rejects included, as classifyBlock
|
|
51
53
|
// and isInlineContradiction read it: a falsy one such as 0, false
|
|
52
54
|
// or '' stubs too, rather than sending the URI as the image's source.
|
|
53
|
-
return { type: 'text', text: referenceStubOrNull(block, provenance, serverId) ??
|
|
55
|
+
return { type: 'text', text: referenceStubOrNull(block, provenance, serverId) ?? `[${NOTICE_TAG.reference}]` };
|
|
54
56
|
}
|
|
55
57
|
if (isFilledString(block.data) && isFilledString(block.mimeType)) {
|
|
56
58
|
return {
|
|
@@ -76,7 +78,7 @@ export function convertBlock(block: McplContentBlock, provenance: string, server
|
|
|
76
78
|
// RFC-005 uri-form media with a disposition claim, a falsy one
|
|
77
79
|
// included, as for an image (above): stub, do not hand the URI to
|
|
78
80
|
// the provider or inline it.
|
|
79
|
-
return { type: 'text', text: referenceStubOrNull(block, provenance, serverId) ??
|
|
81
|
+
return { type: 'text', text: referenceStubOrNull(block, provenance, serverId) ?? `[${NOTICE_TAG.reference}]` };
|
|
80
82
|
}
|
|
81
83
|
if (isFilledString(block.data) && isFilledString(block.mimeType)) {
|
|
82
84
|
return {
|
|
@@ -95,14 +97,14 @@ export function convertBlock(block: McplContentBlock, provenance: string, server
|
|
|
95
97
|
}
|
|
96
98
|
// RFC-005: reference blocks become bounded stubs — never raw URIs
|
|
97
99
|
// (a signed URL is a bearer credential that looks like a location).
|
|
98
|
-
return { type: 'text', text: referenceStubOrNull(block, provenance, serverId) ??
|
|
100
|
+
return { type: 'text', text: referenceStubOrNull(block, provenance, serverId) ?? `[${NOTICE_TAG.reference}]` };
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
default: {
|
|
102
104
|
// MCP's link to a resource isn't one of MCPL's own blocks, but a server
|
|
103
105
|
// can send one, and RFC-005 §8.1 makes it a reference, shown as one.
|
|
104
106
|
if ((block as { type?: unknown }).type === 'resource_link') {
|
|
105
|
-
return { type: 'text', text: referenceStubOrNull(block, provenance, serverId) ??
|
|
107
|
+
return { type: 'text', text: referenceStubOrNull(block, provenance, serverId) ?? `[${NOTICE_TAG.reference}]` };
|
|
106
108
|
}
|
|
107
109
|
// Unknown wire block types previously fell off the exhaustive switch
|
|
108
110
|
// and propagated `undefined` into ContentBlock[]. Fail visibly. A type
|
|
@@ -113,6 +115,17 @@ export function convertBlock(block: McplContentBlock, provenance: string, server
|
|
|
113
115
|
}
|
|
114
116
|
}
|
|
115
117
|
|
|
118
|
+
export function convertBlocks(blocks: readonly McplContentBlock[], provenance: string, serverId: string): ContentBlock[] {
|
|
119
|
+
const converted = blocks.map((block) => convertBlock(block, provenance, serverId));
|
|
120
|
+
return markNoticeOpenings(converted, (index) => sendersText(blocks[index]));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function sendersText(block: McplContentBlock | undefined): boolean {
|
|
124
|
+
if (block?.type === 'text') return typeof block.text === 'string';
|
|
125
|
+
const c = classifyBlock(block);
|
|
126
|
+
return c.kind === 'embedded' && c.contents.kind === 'block' && c.contents.block.type === 'text';
|
|
127
|
+
}
|
|
128
|
+
|
|
116
129
|
/** A string with something in it: the only data, mimeType or uri a block's
|
|
117
130
|
* source is built from, and the only uri that makes it a reference. Anything
|
|
118
131
|
* else a sender puts there leaves the block without it, so conversion never
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import type { ContentBlock } from '@animalabs/membrane';
|
|
17
|
-
import {
|
|
17
|
+
import { convertBlocks } from './convert-block.js';
|
|
18
|
+
import { markedText } from '../supplied-text.js';
|
|
18
19
|
import type { ContextInjection } from '@animalabs/context-manager';
|
|
19
20
|
|
|
20
21
|
import type {
|
|
@@ -64,9 +65,9 @@ function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise
|
|
|
64
65
|
*/
|
|
65
66
|
function convertContent(content: string | McplContentBlock[], serverId: string): ContentBlock[] {
|
|
66
67
|
if (typeof content === 'string') {
|
|
67
|
-
return [{ type: 'text', text: content }];
|
|
68
|
+
return [{ type: 'text', text: markedText(content) }];
|
|
68
69
|
}
|
|
69
|
-
return content
|
|
70
|
+
return convertBlocks(content, 'attachment on context injection', serverId);
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
/**
|
|
@@ -217,7 +217,9 @@ function storedBlock(block: unknown): unknown {
|
|
|
217
217
|
* The version identity of a delivered body (agreed with the receipts lane,
|
|
218
218
|
* room-220 #46752–#47210): SHA-256 hex of `canonicalJson([blocks])` over the
|
|
219
219
|
* ContentBlock[] the framework stores for the body — after MCPL conversion,
|
|
220
|
-
*
|
|
220
|
+
* which marks the server's text at the framework's notice openings
|
|
221
|
+
* (supplied-text.ts), before any host decoration is added and before storage
|
|
222
|
+
* shards it — with
|
|
221
223
|
* each block as the store keeps it, which context-manager's `blockAsStored`
|
|
222
224
|
* gives. So a body hashes alike as delivered, as handed to storage, and as
|
|
223
225
|
* read back unsharded from the store with its blobs resolved, except one
|