@animalabs/agent-framework 0.27.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 +145 -41
- package/dist/src/agent.d.ts +29 -2
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +73 -12
- 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 +11 -5
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/auto-continue.d.ts +5 -2
- package/dist/src/auto-continue.d.ts.map +1 -1
- package/dist/src/auto-continue.js +9 -5
- 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/cut-off-round.d.ts +24 -0
- package/dist/src/cut-off-round.d.ts.map +1 -0
- package/dist/src/cut-off-round.js +87 -0
- package/dist/src/cut-off-round.js.map +1 -0
- package/dist/src/framework.d.ts +468 -125
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +3651 -1111
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +133 -13
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +654 -59
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/index.d.ts +3 -2
- package/dist/src/gate/index.d.ts.map +1 -1
- package/dist/src/gate/index.js +2 -1
- package/dist/src/gate/index.js.map +1 -1
- package/dist/src/gate/reply-judge.d.ts +116 -0
- package/dist/src/gate/reply-judge.d.ts.map +1 -0
- package/dist/src/gate/reply-judge.js +467 -0
- package/dist/src/gate/reply-judge.js.map +1 -0
- package/dist/src/gate/types.d.ts +173 -2
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/heartbeat/heartbeat.d.ts +202 -0
- package/dist/src/heartbeat/heartbeat.d.ts.map +1 -0
- package/dist/src/heartbeat/heartbeat.js +778 -0
- package/dist/src/heartbeat/heartbeat.js.map +1 -0
- package/dist/src/heartbeat/reminder-time.d.ts +5 -0
- package/dist/src/heartbeat/reminder-time.d.ts.map +1 -0
- package/dist/src/heartbeat/reminder-time.js +107 -0
- package/dist/src/heartbeat/reminder-time.js.map +1 -0
- 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 +5 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -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/capability-mask.d.ts.map +1 -1
- package/dist/src/mcpl/capability-mask.js +3 -2
- package/dist/src/mcpl/capability-mask.js.map +1 -1
- 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 +133 -54
- 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 +29 -11
- package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
- package/dist/src/mcpl/inbound-source.js +43 -70
- 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 +18 -8
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +70 -30
- 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/tags.d.ts +9 -2
- package/dist/src/mcpl/tags.d.ts.map +1 -1
- package/dist/src/mcpl/tags.js +12 -2
- package/dist/src/mcpl/tags.js.map +1 -1
- package/dist/src/mcpl/tool-presence.d.ts +66 -14
- package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
- package/dist/src/mcpl/tool-presence.js +150 -80
- package/dist/src/mcpl/tool-presence.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts +10 -3
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
- package/dist/src/mcpl/tool-signature.js +60 -30
- package/dist/src/mcpl/tool-signature.js.map +1 -1
- package/dist/src/mcpl/toolset-host.d.ts +88 -55
- package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
- package/dist/src/mcpl/toolset-host.js +220 -111
- package/dist/src/mcpl/toolset-host.js.map +1 -1
- package/dist/src/mcpl/toolsets-module.d.ts +27 -35
- package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
- package/dist/src/mcpl/toolsets-module.js +313 -91
- 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 +31 -13
- 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 +10 -8
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +15 -9
- 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.d.ts.map +1 -1
- package/dist/src/modules/history/index.js +30 -17
- 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 +26 -0
- package/dist/src/modules/workspace/answers.d.ts.map +1 -0
- package/dist/src/modules/workspace/answers.js +138 -0
- package/dist/src/modules/workspace/answers.js.map +1 -0
- package/dist/src/modules/workspace/branch-intent.d.ts +5 -3
- package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
- package/dist/src/modules/workspace/branch-intent.js +6 -2
- package/dist/src/modules/workspace/branch-intent.js.map +1 -1
- package/dist/src/modules/workspace/direct-write.d.ts +3 -0
- package/dist/src/modules/workspace/direct-write.d.ts.map +1 -1
- package/dist/src/modules/workspace/direct-write.js +59 -24
- package/dist/src/modules/workspace/direct-write.js.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.d.ts +33 -11
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +48 -17
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/edit-text.d.ts +2 -1
- package/dist/src/modules/workspace/edit-text.d.ts.map +1 -1
- package/dist/src/modules/workspace/edit-text.js +4 -2
- package/dist/src/modules/workspace/edit-text.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 +67 -50
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +63 -51
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +980 -547
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +125 -82
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +465 -241
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/read-page.d.ts.map +1 -1
- package/dist/src/modules/workspace/read-page.js +7 -1
- package/dist/src/modules/workspace/read-page.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 +156 -50
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/result-text.d.ts +1 -0
- package/dist/src/modules/workspace/result-text.d.ts.map +1 -1
- package/dist/src/modules/workspace/result-text.js.map +1 -1
- package/dist/src/modules/workspace/sync.d.ts +9 -0
- package/dist/src/modules/workspace/sync.d.ts.map +1 -1
- package/dist/src/modules/workspace/sync.js +14 -0
- package/dist/src/modules/workspace/sync.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +96 -43
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +8 -3
- package/dist/src/modules/workspace/watcher.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/prose-drafts.d.ts +4 -3
- package/dist/src/prose-drafts.d.ts.map +1 -1
- package/dist/src/prose-drafts.js +3 -2
- package/dist/src/prose-drafts.js.map +1 -1
- package/dist/src/reasoning-loss-notice.d.ts +23 -0
- package/dist/src/reasoning-loss-notice.d.ts.map +1 -0
- package/dist/src/reasoning-loss-notice.js +52 -0
- package/dist/src/reasoning-loss-notice.js.map +1 -0
- package/dist/src/reasoning-loss.d.ts +24 -0
- package/dist/src/reasoning-loss.d.ts.map +1 -0
- package/dist/src/reasoning-loss.js +102 -0
- package/dist/src/reasoning-loss.js.map +1 -0
- package/dist/src/send-tools.d.ts +3 -2
- package/dist/src/send-tools.d.ts.map +1 -1
- package/dist/src/send-tools.js +3 -2
- package/dist/src/send-tools.js.map +1 -1
- package/dist/src/sent-rows.js +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/thinking-drops.d.ts +4 -11
- package/dist/src/thinking-drops.d.ts.map +1 -1
- package/dist/src/thinking-drops.js +7 -14
- package/dist/src/thinking-drops.js.map +1 -1
- package/dist/src/tool-args.d.ts +12 -0
- package/dist/src/tool-args.d.ts.map +1 -1
- package/dist/src/tool-args.js +39 -4
- 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/tool-result-guard.d.ts +8 -4
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +23 -11
- package/dist/src/tool-result-guard.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/tune-out/tools.d.ts +4 -1
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/types/agent.d.ts +63 -18
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +18 -8
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +58 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +27 -13
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +34 -1
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agent.ts +76 -12
- package/src/api/mcp-server-core.ts +3 -14
- package/src/api/server.ts +11 -5
- package/src/auto-continue.ts +12 -5
- package/src/code-execution/isolate/runner.ts +7 -5
- package/src/code-execution/isolate/scripts.ts +3 -2
- package/src/cut-off-round.ts +98 -0
- package/src/framework.ts +3772 -1138
- package/src/gate/event-gate.ts +705 -60
- package/src/gate/index.ts +9 -1
- package/src/gate/reply-judge.ts +512 -0
- package/src/gate/types.ts +176 -3
- package/src/heartbeat/heartbeat.ts +843 -0
- package/src/heartbeat/reminder-time.ts +107 -0
- package/src/held-writes.ts +3 -3
- package/src/host-presentation.ts +605 -0
- package/src/index.ts +5 -1
- package/src/line-reader.ts +58 -0
- package/src/mcpl/capability-mask.ts +3 -2
- package/src/mcpl/channel-registry.ts +139 -53
- 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 +45 -70
- 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 +76 -34
- package/src/mcpl/skip-reply-wake.ts +35 -0
- package/src/mcpl/tags.ts +13 -2
- package/src/mcpl/tool-presence.ts +149 -82
- package/src/mcpl/tool-signature.ts +64 -32
- package/src/mcpl/toolset-host.ts +212 -110
- package/src/mcpl/toolsets-module.ts +277 -91
- package/src/mcpl/transport.ts +18 -14
- package/src/mcpl/types.ts +32 -13
- package/src/module-registry.ts +14 -9
- package/src/modules/api/index.ts +4 -2
- package/src/modules/discord/index.ts +37 -25
- package/src/modules/history/index.ts +30 -18
- package/src/modules/sender-name.ts +9 -0
- package/src/modules/workspace/answers.ts +151 -0
- package/src/modules/workspace/branch-intent.ts +10 -5
- package/src/modules/workspace/direct-write.ts +59 -23
- package/src/modules/workspace/disk-agreement.ts +52 -21
- package/src/modules/workspace/edit-text.ts +7 -5
- package/src/modules/workspace/effects.ts +69 -50
- package/src/modules/workspace/index.ts +966 -556
- package/src/modules/workspace/observe.ts +497 -242
- package/src/modules/workspace/read-page.ts +7 -1
- package/src/modules/workspace/reconcile.ts +184 -49
- package/src/modules/workspace/result-text.ts +2 -0
- package/src/modules/workspace/sync.ts +16 -0
- package/src/modules/workspace/types.ts +96 -43
- package/src/modules/workspace/watcher.ts +8 -3
- package/src/notice-tags.ts +84 -0
- package/src/prose-drafts.ts +4 -3
- package/src/reasoning-loss-notice.ts +64 -0
- package/src/reasoning-loss.ts +120 -0
- package/src/send-tools.ts +3 -2
- package/src/sent-rows.ts +1 -1
- package/src/silent-heartbeat.ts +4 -3
- package/src/supplied-text.ts +154 -0
- package/src/thinking-drops.ts +10 -16
- package/src/tool-args.ts +38 -4
- package/src/tool-image-ledger.ts +2 -1
- package/src/tool-presentation.ts +5 -4
- package/src/tool-result-guard.ts +26 -11
- package/src/tune-out/coordinator.ts +25 -27
- package/src/tune-out/tools.ts +4 -1
- package/src/types/agent.ts +63 -18
- package/src/types/events.ts +18 -8
- package/src/types/framework.ts +59 -0
- package/src/types/module.ts +28 -13
- package/src/types/trace.ts +36 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text the framework didn't write (what a participant or a server supplied),
|
|
3
|
+
* put beside structure of the framework's own: the reply judge's document,
|
|
4
|
+
* and notices the framework writes. A value that can end its line can start
|
|
5
|
+
* a line that reads as the framework's own.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { CONTEXT_MANAGER_TAGS, MEMBRANE_TAGS, NOTICE_TAG } from './notice-tags.js';
|
|
9
|
+
|
|
10
|
+
/** Every form of line break a reader might take for one: CR LF, CR, LF,
|
|
11
|
+
* vertical tab, form feed, NEL, and the Unicode line and paragraph separators. */
|
|
12
|
+
const LINE_BREAK = /\r\n|[\r\n\u000b\u000c\u0085\u2028\u2029]/g;
|
|
13
|
+
|
|
14
|
+
/** Text with each line break, in whatever form, as `\n`. */
|
|
15
|
+
export function normalizeLineBreaks(text: string): string {
|
|
16
|
+
return text.replace(LINE_BREAK, '\n');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A value kept to one line: each run of line breaks, in whatever form, as one
|
|
20
|
+
* space, so it can't end the line it's put in or start one of its own. */
|
|
21
|
+
export function oneLine(text: string): string {
|
|
22
|
+
return normalizeLineBreaks(text).replace(/\n+/g, ' ');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const INVISIBLE = '\\p{Cf}\\p{Default_Ignorable_Code_Point}';
|
|
26
|
+
const SKIPPED = `[\\s\\p{Cc}${INVISIBLE}]`;
|
|
27
|
+
const tagWord = (word: string): string =>
|
|
28
|
+
[...word].map((c) => (/[\\^$.*+?()[\]{}|/]/.test(c) ? `\\${c}` : c)).join(`${SKIPPED}*`);
|
|
29
|
+
const NOTICE_OPENING = new RegExp(
|
|
30
|
+
`\\[(?=${SKIPPED}*(?:${[...Object.values(NOTICE_TAG), ...CONTEXT_MANAGER_TAGS, ...MEMBRANE_TAGS].map((tag) => tag.split(' ').map(tagWord).join(`${SKIPPED}+`)).join('|')})(?![\\p{L}\\p{N}]))`,
|
|
31
|
+
'giu',
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Texts read as one string, joined by line breaks, with a backslash put
|
|
36
|
+
* before the bracket of each opening of a listed tag (NOTICE_TAG,
|
|
37
|
+
* CONTEXT_MANAGER_TAGS, MEMBRANE_TAGS) that hasn't one before it in its own
|
|
38
|
+
* text. A text `marks` says false of is read and never changed.
|
|
39
|
+
*
|
|
40
|
+
* Every opening, not only one that starts a line: membrane's XML formatter
|
|
41
|
+
* writes `participant: ` before each message, so the framework's own
|
|
42
|
+
* openings stand mid-line there too. It joins a message's text blocks with
|
|
43
|
+
* line breaks and sets its images apart from that text, while the native
|
|
44
|
+
* formatter sends each block on its own, so an opening split across a
|
|
45
|
+
* message's text blocks is one either way. In any case, by Unicode's case folding, and
|
|
46
|
+
* through whitespace, control and invisible characters anywhere inside the
|
|
47
|
+
* bracket, which a model reads past.
|
|
48
|
+
*/
|
|
49
|
+
export function markedTexts(texts: readonly string[], marks: readonly boolean[] = []): string[] {
|
|
50
|
+
const starts: number[] = [];
|
|
51
|
+
let at = 0;
|
|
52
|
+
for (const text of texts) {
|
|
53
|
+
starts.push(at);
|
|
54
|
+
at += text.length + 1;
|
|
55
|
+
}
|
|
56
|
+
const offsets: number[][] = texts.map(() => []);
|
|
57
|
+
for (const match of texts.join('\n').matchAll(NOTICE_OPENING)) {
|
|
58
|
+
let holder = texts.length - 1;
|
|
59
|
+
while (starts[holder]! > match.index) holder--;
|
|
60
|
+
const offset = match.index - starts[holder]!;
|
|
61
|
+
if (marks[holder] !== false && texts[holder]![offset - 1] !== '\\') offsets[holder]!.push(offset);
|
|
62
|
+
}
|
|
63
|
+
return texts.map((text, i) => withMarks(text, offsets[i]!));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function withMarks(text: string, offsets: readonly number[]): string {
|
|
67
|
+
let marked = '';
|
|
68
|
+
let from = 0;
|
|
69
|
+
for (const offset of offsets) {
|
|
70
|
+
marked += `${text.slice(from, offset)}\\`;
|
|
71
|
+
from = offset;
|
|
72
|
+
}
|
|
73
|
+
return marked + text.slice(from);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function closedWithin(text: string, at: number): boolean {
|
|
77
|
+
let depth = 0;
|
|
78
|
+
for (let i = at; i < text.length; i++) {
|
|
79
|
+
if (text[i] === '[') depth++;
|
|
80
|
+
else if (text[i] === ']' && --depth === 0) return true;
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function unmarkedOpenings(text: string): number[] {
|
|
86
|
+
return [...text.matchAll(NOTICE_OPENING)].map((match) => match.index).filter((at) => text[at - 1] !== '\\');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function markedText(text: string): string {
|
|
90
|
+
return markedTexts([text])[0]!;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function markNoticeOpenings<T extends object>(blocks: readonly T[], marks: (index: number) => boolean = () => true): T[] {
|
|
94
|
+
const at: number[] = [];
|
|
95
|
+
blocks.forEach((block, index) => {
|
|
96
|
+
const { type, text } = (block ?? {}) as { type?: unknown; text?: unknown };
|
|
97
|
+
if (type === 'text' && typeof text === 'string') at.push(index);
|
|
98
|
+
});
|
|
99
|
+
const textOf = (index: number): string => (blocks[index] as unknown as { text: string }).text;
|
|
100
|
+
const marked = markedTexts(at.map(textOf), at.map(marks));
|
|
101
|
+
const out = [...blocks];
|
|
102
|
+
at.forEach((index, i) => {
|
|
103
|
+
if (marked[i] !== textOf(index)) out[index] = { ...blocks[index]!, text: marked[i] };
|
|
104
|
+
});
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function suppliedValue(value: unknown): string {
|
|
109
|
+
return markedText(oneLine(String(value)));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function cutToFit(text: string, length: number): string {
|
|
113
|
+
let end = Math.max(0, Math.floor(length));
|
|
114
|
+
if (end >= text.length) return text;
|
|
115
|
+
for (;;) {
|
|
116
|
+
const last = text.charCodeAt(end - 1);
|
|
117
|
+
if (end < text.length && last >= 0xd800 && last <= 0xdbff) end--;
|
|
118
|
+
else if (text[end - 1] === '\\' && text[end] === '[') end--;
|
|
119
|
+
else {
|
|
120
|
+
const cut = text.slice(0, end);
|
|
121
|
+
const open = unmarkedOpenings(cut).find((at) => !closedWithin(cut, at));
|
|
122
|
+
if (open === undefined) return cut;
|
|
123
|
+
end = open;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function showsAsItself(value: string): boolean {
|
|
129
|
+
return value !== '' && suppliedValue(value) === value && value.isWellFormed() && !EDGE_OR_QUOTE.test(value);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const EDGE_OR_QUOTE = /^\s|\s$|["\\]/;
|
|
133
|
+
|
|
134
|
+
export function quotedValue(value: unknown, space?: number): string {
|
|
135
|
+
return (JSON.stringify(value, null, space) ?? 'null').replace(/"(?:[^"\\]|\\.)*"/g, (literal) => quotedString(JSON.parse(literal)));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function quotedString(text: string): string {
|
|
139
|
+
let quoted = '';
|
|
140
|
+
let from = 0;
|
|
141
|
+
for (const match of text.matchAll(NOTICE_OPENING)) {
|
|
142
|
+
quoted += `${jsonText(text.slice(from, match.index))}\\u005b`;
|
|
143
|
+
from = match.index + 1;
|
|
144
|
+
}
|
|
145
|
+
return `"${quoted}${jsonText(text.slice(from))}"`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// JSON.stringify leaves NEL, U+2028 and U+2029 as they are.
|
|
149
|
+
const jsonText = (text: string): string =>
|
|
150
|
+
JSON.stringify(text).slice(1, -1).replace(/[\u0085\u2028\u2029]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
151
|
+
|
|
152
|
+
export function nameToTypeBack(name: string): string {
|
|
153
|
+
return showsAsItself(name) ? name : quotedValue(name);
|
|
154
|
+
}
|
package/src/thinking-drops.ts
CHANGED
|
@@ -29,10 +29,11 @@ export interface CompiledForRequest {
|
|
|
29
29
|
recordedAfter?: number;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/** What a stream's driver reads a call's dropped blocks against (`StartStreamResult`'s
|
|
32
|
+
/** What a stream's driver reads a call's dropped blocks against (`StartStreamResult`'s fields, together). */
|
|
33
33
|
export interface StreamThinkingDrops {
|
|
34
34
|
place: (entries: readonly ThinkingBindingEntry[]) => PlacedDrop[];
|
|
35
35
|
recordedAfter?: number;
|
|
36
|
+
branch?: string;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
/** A signed block's identity on the wire: a thinking block's signature, a redacted block's data. */
|
|
@@ -112,26 +113,19 @@ export function splitThinkingDrops(
|
|
|
112
113
|
/** Binding failures the context manager's record doesn't track: the account's, not the conversation's. */
|
|
113
114
|
const UNTRACKED_REASONS = new Set(['organization_binding_mismatch', 'end_user_binding_mismatch']);
|
|
114
115
|
|
|
115
|
-
/**
|
|
116
|
-
* When an agent's unexpected drops call for an operator alert: at the first call that shows any, and again only
|
|
117
|
-
* when a call shows more than the alert last counted. Each call of a stream, and each stream until a strip takes
|
|
118
|
-
* them out, drops the same blocks again, so an unchanged count isn't news. A call that shows no unexpected drop, its
|
|
119
|
-
* report empty, absent or naming only expected ones, ends the incident: the blocks it counted no longer drop. The next
|
|
120
|
-
* one alerts afresh. It's held in memory, so a restart mid-incident alerts once more, and an agent's removal ends its
|
|
121
|
-
* incident (`forget`).
|
|
122
|
-
*/
|
|
123
116
|
export class ThinkingLossIncidents {
|
|
124
|
-
private readonly alerted = new Map<string, number>();
|
|
117
|
+
private readonly alerted = new Map<string, { count: number; scope: string | undefined }>();
|
|
125
118
|
|
|
126
|
-
/** A call's report of `
|
|
127
|
-
observe(agent: string,
|
|
128
|
-
|
|
119
|
+
/** A call's report of `count` drops for `agent` in `scope`: the count to alert with, or undefined for no alert. */
|
|
120
|
+
observe(agent: string, count: number, scope?: string): number | undefined {
|
|
121
|
+
const open = this.alerted.get(agent);
|
|
122
|
+
if (count === 0) {
|
|
129
123
|
this.alerted.delete(agent);
|
|
130
124
|
return undefined;
|
|
131
125
|
}
|
|
132
|
-
if (
|
|
133
|
-
this.alerted.set(agent,
|
|
134
|
-
return
|
|
126
|
+
if (open && open.scope === scope && count <= open.count) return undefined;
|
|
127
|
+
this.alerted.set(agent, { count, scope });
|
|
128
|
+
return count;
|
|
135
129
|
}
|
|
136
130
|
|
|
137
131
|
/** The agent is gone: its incident, if one is open, ends with it. */
|
package/src/tool-args.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { nameToTypeBack, quotedValue } from './supplied-text.js';
|
|
1
2
|
import type { ToolDefinition } from './types/index.js';
|
|
2
3
|
|
|
3
4
|
/** A run of 16 or more digits, which membrane's XML parser passes as text for
|
|
@@ -67,6 +68,39 @@ function shown(value: unknown): string {
|
|
|
67
68
|
return head;
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
function notAnObject(input: unknown, field = 'input'): string {
|
|
72
|
+
return `its ${field} must be an object, and this call gave ${withArticle(kindOf(input))}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function markedInputRefusal(call: { name: string; unparseableInput?: string }): string | undefined {
|
|
76
|
+
const text = call.unparseableInput;
|
|
77
|
+
if (text === undefined) return undefined;
|
|
78
|
+
// When a stream ends before a call's block stops, membrane marks the call
|
|
79
|
+
// with what had arrived of its arguments: nothing, a part, or a whole
|
|
80
|
+
// object. A block still open when a stream breaks the API's block order
|
|
81
|
+
// is marked the same way, and one that had stopped with its object's JSON,
|
|
82
|
+
// so the arguments may be another call's (ToolUseContent.unparseableInput).
|
|
83
|
+
if (/^[ \t\n\r]*$/.test(text)) return `Nothing was done: ${nameToTypeBack(call.name)} its arguments never arrived; call it again with them whole.`;
|
|
84
|
+
let parsed: unknown;
|
|
85
|
+
try {
|
|
86
|
+
parsed = JSON.parse(text);
|
|
87
|
+
} catch {
|
|
88
|
+
return `Nothing was done: ${nameToTypeBack(call.name)} its arguments must be one JSON object, and this call gave text that isn't JSON, which may have been cut off: ${shown(text)}; call it again with one JSON object.`;
|
|
89
|
+
}
|
|
90
|
+
if (parsed === null) return undefined;
|
|
91
|
+
if (typeof parsed !== 'object' || Array.isArray(parsed)) return `Nothing was done: ${nameToTypeBack(call.name)} ${notAnObject(parsed)}.`;
|
|
92
|
+
return `Nothing was done: ${nameToTypeBack(call.name)} its arguments came in a response that didn't arrive whole and in order, so they can't be taken as this call's; call it again.`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function cutOffRefusal(call: { name: string }): string {
|
|
96
|
+
return `Nothing was done: ${nameToTypeBack(call.name)} didn't run, because the response it was in was cut off at the output limit. ` +
|
|
97
|
+
'To make the call, send less in one response: shorter text before it, shorter arguments, or fewer calls at once.';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function asCalled<T extends { name: string; input?: unknown; unparseableInput?: string }>(call: T): T {
|
|
101
|
+
return call.unparseableInput !== undefined && markedInputRefusal(call) === undefined ? { ...call, input: {} } : call;
|
|
102
|
+
}
|
|
103
|
+
|
|
70
104
|
/** A list in prose: `a`, `a or b`, `a, b or c`. */
|
|
71
105
|
function oneOf(items: string[]): string {
|
|
72
106
|
return items.length <= 1 ? items.join('') : `${items.slice(0, -1).join(', ')} or ${items[items.length - 1]}`;
|
|
@@ -169,7 +203,7 @@ export function toolArgsProblems(
|
|
|
169
203
|
if (!schema) return [];
|
|
170
204
|
const absent = input === undefined || input === null;
|
|
171
205
|
if (!absent && (typeof input !== 'object' || Array.isArray(input))) {
|
|
172
|
-
return [
|
|
206
|
+
return [notAnObject(input, options.field)];
|
|
173
207
|
}
|
|
174
208
|
const obj = (absent ? {} : input) as Record<string, unknown>;
|
|
175
209
|
const properties = (isMap(schema.properties) ? schema.properties : {}) as Record<string, { type?: unknown; nullable?: unknown; enum?: unknown } | undefined>;
|
|
@@ -183,7 +217,7 @@ export function toolArgsProblems(
|
|
|
183
217
|
const kinds = admittedKinds(declared(key));
|
|
184
218
|
if (value !== undefined && (value !== null || takesNull(declared(key)))) continue;
|
|
185
219
|
missing.add(key);
|
|
186
|
-
const wanted = kinds.length > 0 ?
|
|
220
|
+
const wanted = kinds.length > 0 ? `${quotedValue(key)} (${oneOf(kinds.map(withArticle))})` : quotedValue(key);
|
|
187
221
|
problems.push(`needs ${wanted}, and this call gave ${value === null ? 'null' : 'none'}`);
|
|
188
222
|
}
|
|
189
223
|
if (missing.size > 0 && declaresKeysPlainly(schema as Record<string, unknown>)) {
|
|
@@ -202,11 +236,11 @@ export function toolArgsProblems(
|
|
|
202
236
|
const kind = kindOf(value);
|
|
203
237
|
const largeInteger = kind === 'string' && kinds.includes('number') && LARGE_INTEGER_TEXT.test(value as string);
|
|
204
238
|
if (kinds.length > 0 && !kinds.includes(kind) && !largeInteger) {
|
|
205
|
-
problems.push(
|
|
239
|
+
problems.push(`${quotedValue(key)} must be ${oneOf(kinds.map(withArticle))}, and this call gave ${withArticle(kind)}`);
|
|
206
240
|
continue;
|
|
207
241
|
}
|
|
208
242
|
if (Array.isArray(param.enum) && !param.enum.includes(value)) {
|
|
209
|
-
problems.push(
|
|
243
|
+
problems.push(`${quotedValue(key)} must be one of ${oneOf(param.enum.map((e) => quotedValue(e)))}, and this call gave ${shown(value)}`);
|
|
210
244
|
}
|
|
211
245
|
}
|
|
212
246
|
return problems;
|
package/src/tool-image-ledger.ts
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* of bytes from another surface is the failure this exists to end.
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
+
import { NOTICE_TAG } from './notice-tags.js';
|
|
29
30
|
import { createHash, randomBytes } from 'node:crypto';
|
|
30
31
|
import { REFERENCE_STUB_MAX_CHARS, referenceTypeLabel } from './mcpl/references.js';
|
|
31
32
|
import { safeSlice } from './safe-slice.js';
|
|
@@ -256,7 +257,7 @@ export type ParsedImagePlaceholder =
|
|
|
256
257
|
const PLACEHOLDER_RE = /\[image: ([a-z0-9!#$&^_.+-]{1,127}\/[a-z0-9!#$&^_.+-]{1,127}), (~?[\d.]{1,16}(?:B|KB|MB))(?:, ref (img_[0-9a-z]{1,16}_\d{1,16}))?\]/g;
|
|
257
258
|
/** `[ref_1_ab3d] cam.png — image/png, 1.2MB claimed — from tool result — …`
|
|
258
259
|
* (one line, at most REFERENCE_STUB_MAX_CHARS long: see parseImagePlaceholders). */
|
|
259
|
-
const REFERENCE_STUB_RE =
|
|
260
|
+
const REFERENCE_STUB_RE = new RegExp(`^\\[(${NOTICE_TAG.referenceId}_[0-9a-z]+_[0-9a-z]+)\\] ([^\\n]*)$`, 'gm');
|
|
260
261
|
const IMAGE_MIME_RE = /\bimage\/[a-z0-9!#$&^_.+-]+/i;
|
|
261
262
|
|
|
262
263
|
/**
|
package/src/tool-presentation.ts
CHANGED
|
@@ -32,7 +32,8 @@ const MAX_BYTES = 256 * 1024;
|
|
|
32
32
|
*/
|
|
33
33
|
const NONBLOCK = typeof constants.O_NONBLOCK === 'number' ? constants.O_NONBLOCK : 0;
|
|
34
34
|
const MAX_DESCRIPTION_CHARS = 32768;
|
|
35
|
-
|
|
35
|
+
// The toolsets meta-tools are how a hidden tool is reached again, so they stay.
|
|
36
|
+
const protectedNames = new Set(['set_tool_visibility', 'set_tool_description', 'workspace--read', 'toolsets--open', 'toolsets--act', 'toolsets--look', 'toolsets--close']);
|
|
36
37
|
export const isPresentationTool = (name: string): boolean => name === 'set_tool_visibility' || name === 'set_tool_description';
|
|
37
38
|
const hash = (value: string): string => createHash('sha256').update(value).digest('hex');
|
|
38
39
|
|
|
@@ -175,7 +176,7 @@ function lockLeftBehind(error: unknown): string {
|
|
|
175
176
|
}
|
|
176
177
|
|
|
177
178
|
export function presentationTools(path: string): ToolDefinition[] {
|
|
178
|
-
const signpost = ` Read the generated catalogue with workspace--read {"path":${JSON.stringify(path)},"limit":140}.
|
|
179
|
+
const signpost = ` Read the generated catalogue with workspace--read {"path":${JSON.stringify(path)},"limit":140}. A hidden tool moves out of your tool list into the host's "settings" toolset (an MCPL server's tool, into that server's tab): toolsets--look lists it there, and toolsets--act calls it once that toolset is open. Changes affect the next newly compiled request, not an in-flight stream.`;
|
|
179
180
|
return [
|
|
180
181
|
{name:'set_tool_visibility', description:'Show or hide a tool definition. Hiding does not revoke permission.' + signpost,
|
|
181
182
|
inputSchema:{type:'object',properties:{name:{type:'string'},visible:{type:'boolean'}},required:['name','visible'],additionalProperties:false}},
|
|
@@ -341,7 +342,7 @@ export class ToolPresentation {
|
|
|
341
342
|
if (field === 'description' && typeof args.description === 'string' && characters(args.description) > MAX_DESCRIPTION_CHARS)
|
|
342
343
|
throw new PresentationRefusal(`Nothing was changed: the description is ${count(characters(args.description))} characters, over the ${count(MAX_DESCRIPTION_CHARS)} allowed. Shorten it and call set_tool_description again.`);
|
|
343
344
|
if (field === 'visible' && args.visible === false && protectedNames.has(name))
|
|
344
|
-
throw new PresentationRefusal(`Nothing was changed: ${name} can't be hidden, since workspace--read, set_tool_visibility and
|
|
345
|
+
throw new PresentationRefusal(`Nothing was changed: ${name} can't be hidden, since workspace--read, set_tool_visibility, set_tool_description and the toolsets-- tools are how you read the catalogue, reach hidden tools and undo these changes. Calling this again won't change that.`);
|
|
345
346
|
return name;
|
|
346
347
|
}
|
|
347
348
|
}
|
|
@@ -356,7 +357,7 @@ export function renderCatalogue(snapshot: PresentationSnapshot): string {
|
|
|
356
357
|
'# Tool catalogue — generated from currently available tools', '',
|
|
357
358
|
`Revision: ${snapshot.revision}`, '',
|
|
358
359
|
`${snapshot.entries.length} tools: ${visible} visible, ${snapshot.entries.length-visible} hidden.`,
|
|
359
|
-
'Visible =
|
|
360
|
+
'Visible = not hidden by you: in your tool list, unless the host keeps it in its "settings" toolset or a server\'s tab (toolsets--look shows those). Hidden = moved into the "settings" toolset, or its MCPL server\'s tab: toolsets--look shows it, toolsets--act calls it once that toolset is open.',
|
|
360
361
|
'These are your visibility choices, not fixed primary/secondary rankings.',
|
|
361
362
|
'Copy exact names from the index; similar-sounding names may not exist.',
|
|
362
363
|
'Show a hidden tool: set_tool_visibility {"name":"EXACT_NAME","visible":true}.',
|
package/src/tool-result-guard.ts
CHANGED
|
@@ -9,6 +9,14 @@ import { resultClaimsOf, rfc005TextOf, toolResultDataToHistoryString } from './t
|
|
|
9
9
|
import { safeSlice } from './safe-slice.js';
|
|
10
10
|
|
|
11
11
|
export const TOOL_RESULT_GUARD_NOTICE = 'Tool result withheld by the guard. The tool has already executed.';
|
|
12
|
+
export type WithheldNoticeKind = 'ran' | 'refused' | 'error';
|
|
13
|
+
const GUARD_NOTICES: Record<WithheldNoticeKind, string> = {
|
|
14
|
+
ran: TOOL_RESULT_GUARD_NOTICE,
|
|
15
|
+
refused: "Tool result withheld by the guard. The tool didn't run.",
|
|
16
|
+
error: "Tool result withheld by the guard. The call returned an error, and this notice can't say whether the tool ran.",
|
|
17
|
+
};
|
|
18
|
+
const noticeKind = (result: CompletedToolCall['result'] | undefined): WithheldNoticeKind =>
|
|
19
|
+
result?.refused === true ? 'refused' : result?.isError === true ? 'error' : 'ran';
|
|
12
20
|
export const TOOL_RESULT_GUARD_AUDIT_STATE = 'framework/tool-result-guard';
|
|
13
21
|
|
|
14
22
|
/** Where a withheld result's original went: a workspace path; the path a
|
|
@@ -54,9 +62,12 @@ export interface WithheldHost {
|
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
/**
|
|
57
|
-
* The stub a withheld result settles to: #159's neutral notice
|
|
58
|
-
*
|
|
59
|
-
*
|
|
65
|
+
* The stub a withheld result settles to: #159's neutral notice for its
|
|
66
|
+
* `kind` (a result that isn't an error, a refused call's, or another error's),
|
|
67
|
+
* then where the original is, then, for an error whose original is in a file
|
|
68
|
+
* known written, a line telling the agent to read it, then, when its tool
|
|
69
|
+
* marked any, the fields kept from its result (`kept`, from
|
|
70
|
+
* keptWhenWithheld). It names no refusal and no
|
|
60
71
|
* category (#159 keeps those in operational logs), so reading the rest back
|
|
61
72
|
* is the agent's deliberate choice (agent-framework #277). The place holds
|
|
62
73
|
* the result as stored, which for one over the inline cap is its capped
|
|
@@ -67,7 +78,7 @@ export interface WithheldHost {
|
|
|
67
78
|
* save failed is still queued in memory, and a restart before its retry
|
|
68
79
|
* loses it.
|
|
69
80
|
*/
|
|
70
|
-
export function withheldResultNotice(spill: WithheldSpill, auditDurable: boolean, kept?: string): string {
|
|
81
|
+
export function withheldResultNotice(spill: WithheldSpill, auditDurable: boolean, kept?: string, kind: WithheldNoticeKind = 'ran'): string {
|
|
71
82
|
const audit = `the guard's audit record (${TOOL_RESULT_GUARD_AUDIT_STATE}), for an operator`;
|
|
72
83
|
const keptIn = (only: string) => auditDurable
|
|
73
84
|
? `is kept ${only}in ${audit}`
|
|
@@ -80,8 +91,10 @@ export function withheldResultNotice(spill: WithheldSpill, auditDurable: boolean
|
|
|
80
91
|
: spill.error === undefined
|
|
81
92
|
? `Its result is in workspace file ${spill.path}.`
|
|
82
93
|
: `Writing its result to workspace file ${spill.path} failed, so it ${keptIn('only ')}.`;
|
|
94
|
+
const readable = kind === 'error' && !!spill && 'path' in spill && spill.error === undefined && !spill.unfinished;
|
|
95
|
+
const readFirst = readable ? ' Read it before calling again.' : '';
|
|
83
96
|
const fields = kept === undefined ? '' : ` Kept from its result: ${kept}`;
|
|
84
|
-
return `${
|
|
97
|
+
return `${GUARD_NOTICES[kind]} ${place}${readFirst}${fields}`;
|
|
85
98
|
}
|
|
86
99
|
|
|
87
100
|
/**
|
|
@@ -140,6 +153,7 @@ interface PendingBatch {
|
|
|
140
153
|
/** What each result's stub keeps if the batch is withheld, by its index in
|
|
141
154
|
* content: the fields its tool marked (keptWhenWithheld). */
|
|
142
155
|
kept: Map<number, string>;
|
|
156
|
+
notices: Map<string, WithheldNoticeKind>;
|
|
143
157
|
}
|
|
144
158
|
|
|
145
159
|
interface AuditOperation {
|
|
@@ -248,7 +262,7 @@ export class ToolResultGuard {
|
|
|
248
262
|
if (block.type === 'image') images += 1;
|
|
249
263
|
else chars += JSON.stringify(block).length;
|
|
250
264
|
}
|
|
251
|
-
chars -=
|
|
265
|
+
chars -= GUARD_NOTICES[pending.notices.get(result.toolUseId) ?? 'ran'].length;
|
|
252
266
|
}
|
|
253
267
|
return Math.max(0, Math.ceil(chars / 4) + images * 1600);
|
|
254
268
|
}
|
|
@@ -361,12 +375,13 @@ export class ToolResultGuard {
|
|
|
361
375
|
// with a placeholder if the audit cannot become durable below.
|
|
362
376
|
console.error(`[tool-result-guard] agent=${this.agentName} staged audit write queued:`, error);
|
|
363
377
|
}
|
|
378
|
+
const results = new Map(originals.map((call) => [call.id, call.result]));
|
|
379
|
+
const notices = new Map([...results].map(([id, result]) => [id, noticeKind(result)]));
|
|
364
380
|
const withheld: ContentBlock[] = content.map((block) => block.type === 'tool_result'
|
|
365
|
-
? { type: 'tool_result', toolUseId: block.toolUseId, content:
|
|
381
|
+
? { type: 'tool_result', toolUseId: block.toolUseId, content: GUARD_NOTICES[notices.get(block.toolUseId) ?? 'ran'], isError: block.isError }
|
|
366
382
|
: block);
|
|
367
383
|
// What each stub keeps if the batch is withheld, matched to its tool's
|
|
368
384
|
// result by tool use id, keyed by index as the stubs and files are.
|
|
369
|
-
const results = new Map(originals.map((call) => [call.id, call.result]));
|
|
370
385
|
const kept = new Map<number, string>();
|
|
371
386
|
for (const [index, block] of content.entries()) {
|
|
372
387
|
const result = block.type === 'tool_result' ? results.get(block.toolUseId) : undefined;
|
|
@@ -379,7 +394,7 @@ export class ToolResultGuard {
|
|
|
379
394
|
// which is correct since a reopened guard has no pending batch.
|
|
380
395
|
const messageId = this.cm.addMessage('user', withheld, metadata, undefined, { holdCompression: true });
|
|
381
396
|
this.pending = { id, messageId, content, withheld, wireResults,
|
|
382
|
-
branch: this.cm.currentBranch().name, submitted: false, durable: false, kept };
|
|
397
|
+
branch: this.cm.currentBranch().name, submitted: false, durable: false, kept, notices };
|
|
383
398
|
// Durability barrier: the audit must reach Chronicle's chain heads before
|
|
384
399
|
// the originals can go to a provider. On a failed sync the batch fails
|
|
385
400
|
// CLOSED: the placeholders go on the wire instead (the turn continues,
|
|
@@ -406,7 +421,7 @@ export class ToolResultGuard {
|
|
|
406
421
|
if (pending.durable) { pending.submitted = true; return results; }
|
|
407
422
|
const ids = new Set(pending.wireResults.map((result) => result.toolUseId));
|
|
408
423
|
return results.map((result) => ids.has(result.toolUseId)
|
|
409
|
-
? { ...result, content:
|
|
424
|
+
? { ...result, content: GUARD_NOTICES[pending.notices.get(result.toolUseId) ?? 'ran'] } : result);
|
|
410
425
|
}
|
|
411
426
|
|
|
412
427
|
/** The stream carrying this batch ended without a clean round (abort or
|
|
@@ -603,7 +618,7 @@ export class ToolResultGuard {
|
|
|
603
618
|
spills.set(index, spill);
|
|
604
619
|
}
|
|
605
620
|
const annotated: ContentBlock[] = pending.withheld.map((block, index) => block.type === 'tool_result'
|
|
606
|
-
? { ...block, content: withheldResultNotice(spills.get(index) ?? null, pending.durable, pending.kept.get(index)) }
|
|
621
|
+
? { ...block, content: withheldResultNotice(spills.get(index) ?? null, pending.durable, pending.kept.get(index), pending.notices.get(block.toolUseId)) }
|
|
607
622
|
: block);
|
|
608
623
|
try {
|
|
609
624
|
this.append({
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* lifecycle facts (the [Gate: …] precedent).
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { NOTICE_TAG } from '../notice-tags.js';
|
|
19
|
+
import { cutToFit, suppliedValue } from '../supplied-text.js';
|
|
18
20
|
import { randomUUID } from 'node:crypto';
|
|
19
21
|
import type { ContentBlock } from '@animalabs/membrane';
|
|
20
22
|
import type { StoredMessage } from '@animalabs/context-manager';
|
|
@@ -38,14 +40,14 @@ const WAKE_COALESCE_MS = 2_000;
|
|
|
38
40
|
const EXPIRY_RETRY_MS = 60_000;
|
|
39
41
|
|
|
40
42
|
/** Ends a backlog line cut to fit in one of the residents' messages. */
|
|
41
|
-
const CUT_MARK =
|
|
43
|
+
const CUT_MARK = ` [${NOTICE_TAG.cutToFit}; the whole message is in the channel's history]`;
|
|
42
44
|
|
|
43
45
|
/**
|
|
44
46
|
* tune_out's refusal of a channel already tuned out: its settings, then what
|
|
45
47
|
* its transport is now where the repeat tried to open it again (`transport`,
|
|
46
48
|
* from the framework's dispatch), then how to change the settings.
|
|
47
49
|
*/
|
|
48
|
-
export function alreadyTunedOutText(
|
|
50
|
+
export function alreadyTunedOutText(quoted: string, p: TuneOutParams, transport?: string): string {
|
|
49
51
|
// A duration so long its end is past what a Date can hold has no time to show.
|
|
50
52
|
const expires = p.expiresAtMs !== undefined ? new Date(p.expiresAtMs) : undefined;
|
|
51
53
|
const until = expires && Number.isFinite(expires.getTime())
|
|
@@ -53,21 +55,13 @@ export function alreadyTunedOutText(shown: string, p: TuneOutParams, transport?:
|
|
|
53
55
|
: '';
|
|
54
56
|
// A thrown error's words may end without a stop.
|
|
55
57
|
const now = transport === undefined ? '' : /[.!?]$/.test(transport) ? `${transport} ` : `${transport}. `;
|
|
56
|
-
return `Nothing changed${transport !== undefined ? ' in the tune-out' : ''}: ${
|
|
58
|
+
return `Nothing changed${transport !== undefined ? ' in the tune-out' : ''}: ${quoted} is already `
|
|
57
59
|
+ `tuned out, with cadenceSeconds ${p.cadenceSeconds}, backlogCap ${p.backlogCap} and maxWakes ${p.maxWakes}${until}. `
|
|
58
60
|
+ now
|
|
59
61
|
+ 'To change those, cancel it first with tune_out mode "cancel", which delivers its backlog to you now, then call '
|
|
60
62
|
+ 'tune_out again with the new settings.';
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
/** The first `length` code units of `text`, one fewer where the last would be
|
|
64
|
-
* the first half of a surrogate pair. */
|
|
65
|
-
function prefixOf(text: string, length: number): string {
|
|
66
|
-
const cut = text.slice(0, Math.max(0, length));
|
|
67
|
-
const last = cut.charCodeAt(cut.length - 1);
|
|
68
|
-
return last >= 0xd800 && last <= 0xdbff ? cut.slice(0, -1) : cut;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
65
|
/** What a thrown value says, without throwing: an Error's message, or the
|
|
72
66
|
* value as text, which String() can't give for an object with no prototype. */
|
|
73
67
|
function thrownReason(error: unknown): string {
|
|
@@ -153,6 +147,10 @@ export class TuneOutCoordinator {
|
|
|
153
147
|
return this.channelRegistry.nameShown({ serverId, channelId });
|
|
154
148
|
}
|
|
155
149
|
|
|
150
|
+
private quotedShown(serverId: string, channelId: string): string {
|
|
151
|
+
return this.channelRegistry.quotedShown({ serverId, channelId });
|
|
152
|
+
}
|
|
153
|
+
|
|
156
154
|
/** Restart cadence + expiry timers and re-anchor after a process restart. */
|
|
157
155
|
resumeActiveEpochs(): void {
|
|
158
156
|
let oldestAnchor: number | null = null;
|
|
@@ -244,14 +242,14 @@ export class TuneOutCoordinator {
|
|
|
244
242
|
if (this.hooks.isForkBound({ serverId, channelId })) {
|
|
245
243
|
return {
|
|
246
244
|
ok: false,
|
|
247
|
-
error: `Nothing changed: a conversation is bound to ${
|
|
245
|
+
error: `Nothing changed: a conversation is bound to ${this.quotedShown(serverId, channelId)}, so its messages go to that `
|
|
248
246
|
+ "conversation, and tune_out won't divert a channel while one is. Calling tune_out on it again won't change "
|
|
249
247
|
+ 'that until the conversation ends, which happens only once the channel has been quiet for a while. Tune '
|
|
250
248
|
+ 'out a different channel, or ask whoever runs this host if this one should be diverted.',
|
|
251
249
|
};
|
|
252
250
|
}
|
|
253
251
|
const current = this.channelRegistry.getTuneOutState(serverId, channelId);
|
|
254
|
-
if (current) return { ok: false, error: alreadyTunedOutText(this.
|
|
252
|
+
if (current) return { ok: false, error: alreadyTunedOutText(this.quotedShown(serverId, channelId), current.params), alreadyTunedOut: true };
|
|
255
253
|
// A tool's input reaches here as the model wrote it, and the store can't
|
|
256
254
|
// keep a NaN or an Infinity: its append would throw after the registry
|
|
257
255
|
// had marked the channel tuned out. So each option is read as a number
|
|
@@ -337,7 +335,7 @@ export class TuneOutCoordinator {
|
|
|
337
335
|
const ended = this.channelRegistry.cancelTuneOut(serverId, channelId, 'open', source);
|
|
338
336
|
if (!ended) {
|
|
339
337
|
// Its reader is the resident's tune_out: cancel_tuneout finds the channel first.
|
|
340
|
-
return { ok: false, error: this.notTunedOut(this.
|
|
338
|
+
return { ok: false, error: this.notTunedOut(this.quotedShown(serverId, channelId), 'you') };
|
|
341
339
|
}
|
|
342
340
|
this.clearTimers(serverId, channelId);
|
|
343
341
|
this.divertedSinceInvocation.delete(this.key(serverId, channelId));
|
|
@@ -360,7 +358,7 @@ export class TuneOutCoordinator {
|
|
|
360
358
|
// start at 0 and take them all.
|
|
361
359
|
const cap = Math.floor(ended.params.backlogCap);
|
|
362
360
|
const authorOf = (m: Pick<StoredMessage, 'participant' | 'metadata'>): string =>
|
|
363
|
-
(m.metadata as { author?: { name?: string } } | undefined)?.author?.name ?? m.participant;
|
|
361
|
+
suppliedValue((m.metadata as { author?: { name?: string } } | undefined)?.author?.name ?? m.participant);
|
|
364
362
|
const lineOf = (m: Pick<StoredMessage, 'participant' | 'content' | 'metadata'>): string => {
|
|
365
363
|
const text = m.content
|
|
366
364
|
.filter((b): b is Extract<ContentBlock, { type: 'text' }> => b.type === 'text')
|
|
@@ -370,10 +368,10 @@ export class TuneOutCoordinator {
|
|
|
370
368
|
};
|
|
371
369
|
// The channel as the resident and the subconscious are shown it.
|
|
372
370
|
const name = this.shown(serverId, channelId);
|
|
373
|
-
const lifecycle = `[
|
|
371
|
+
const lifecycle = `[${NOTICE_TAG.tuneOut}: ${name} cancelled — ${reason}, ${ended.wakeCount} wake${ended.wakeCount === 1 ? '' : 's'}]`;
|
|
374
372
|
const render = (lines: string[], truncated: number): string => {
|
|
375
373
|
const attrs = [
|
|
376
|
-
`channel
|
|
374
|
+
`channel=${this.quotedShown(serverId, channelId)}`,
|
|
377
375
|
`messages=${backlog.length}`,
|
|
378
376
|
...(truncated > 0 ? [`truncated=${truncated} (oldest; fetch_history covers the rest)`] : []),
|
|
379
377
|
];
|
|
@@ -417,7 +415,7 @@ export class TuneOutCoordinator {
|
|
|
417
415
|
// What of the newest line's text fits beside the frame and the mark. A
|
|
418
416
|
// cut shows some of it or none: a room under one character, or one
|
|
419
417
|
// that would split a surrogate pair, leaves the head empty.
|
|
420
|
-
const head =
|
|
418
|
+
const head = cutToFit(lines.at(-1) ?? '', room - frameShowing(1) - CUT_MARK.length);
|
|
421
419
|
if (kept === 0 && head !== '') {
|
|
422
420
|
lines = [head + CUT_MARK];
|
|
423
421
|
shown = shown.slice(-1);
|
|
@@ -480,7 +478,7 @@ export class TuneOutCoordinator {
|
|
|
480
478
|
: '';
|
|
481
479
|
this.hooks.addMessage(
|
|
482
480
|
'user',
|
|
483
|
-
[{ type: 'text', text: `[
|
|
481
|
+
[{ type: 'text', text: `[${NOTICE_TAG.tuneOutCancelled}: ${this.shown(serverId, channelId)} — ${reason}.${truncNote}${heldNote}]` }],
|
|
484
482
|
{ system: true, kind: 'tune-out-cancel-report', channelId },
|
|
485
483
|
subName,
|
|
486
484
|
);
|
|
@@ -664,8 +662,8 @@ export class TuneOutCoordinator {
|
|
|
664
662
|
|
|
665
663
|
const shown = this.shown(serverId, channelId);
|
|
666
664
|
const notice = trigger === 'wake'
|
|
667
|
-
? `[
|
|
668
|
-
: `[
|
|
665
|
+
? `[${NOTICE_TAG.tuneOutWake}: ${shown} — ${count} addressed message${count === 1 ? '' : 's'}]`
|
|
666
|
+
: `[${NOTICE_TAG.tuneOutCadence}: ${shown} — ${count} diverted message${count === 1 ? '' : 's'} since your last look]`;
|
|
669
667
|
this.hooks.addMessage(
|
|
670
668
|
'user',
|
|
671
669
|
[{ type: 'text', text: notice }],
|
|
@@ -709,7 +707,7 @@ export class TuneOutCoordinator {
|
|
|
709
707
|
position: 'system',
|
|
710
708
|
content: [{
|
|
711
709
|
type: 'text',
|
|
712
|
-
text: `[
|
|
710
|
+
text: `[${NOTICE_TAG.standingDispositions}]\n${entries.map(([k, v]) => `- ${k}: ${v}`).join('\n')}`,
|
|
713
711
|
}],
|
|
714
712
|
};
|
|
715
713
|
}
|
|
@@ -809,7 +807,7 @@ export class TuneOutCoordinator {
|
|
|
809
807
|
if (shared) return { success: false, error: shared, isError: true };
|
|
810
808
|
const channel = named[0];
|
|
811
809
|
if (!channel || !this.channelRegistry.getTuneOutState(channel.serverId, channel.channelId)) {
|
|
812
|
-
return { success: false, error: this.notTunedOut(name, 'the resident'), isError: true };
|
|
810
|
+
return { success: false, error: this.notTunedOut(JSON.stringify(name), 'the resident'), isError: true };
|
|
813
811
|
}
|
|
814
812
|
const note = typeof input.text === 'string' && input.text.trim() ? input.text.trim() : undefined;
|
|
815
813
|
const result = this.cancel(channel.serverId, channel.channelId, 'subconscious', 'subconscious judgment', note);
|
|
@@ -874,7 +872,7 @@ export class TuneOutCoordinator {
|
|
|
874
872
|
}
|
|
875
873
|
|
|
876
874
|
/**
|
|
877
|
-
* A cancel of a channel that isn't tuned out, by the name the caller gave
|
|
875
|
+
* A cancel of a channel that isn't tuned out, by the name, quoted, the caller gave
|
|
878
876
|
* or knows it by: which channels are, so the next call can name one, and
|
|
879
877
|
* how a tune-out ends without one. `backlogTo`
|
|
880
878
|
* names the resident as the message's reader knows it: "you" to the
|
|
@@ -884,11 +882,11 @@ export class TuneOutCoordinator {
|
|
|
884
882
|
* neither this list nor the name's lookup (channelsNamed, which reads the
|
|
885
883
|
* registered channels too), and the text claims nothing about it.
|
|
886
884
|
*/
|
|
887
|
-
private notTunedOut(
|
|
888
|
-
const active = this.listActive().map((e) =>
|
|
885
|
+
private notTunedOut(quoted: string, backlogTo: 'you' | 'the resident'): string {
|
|
886
|
+
const active = this.listActive().map((e) => this.quotedShown(e.serverId, e.channelId));
|
|
889
887
|
const opening = active.length === 0
|
|
890
888
|
? 'no connected channel is tuned out now.'
|
|
891
|
-
: `no connected channel ${
|
|
889
|
+
: `no connected channel ${quoted} is tuned out. Connected channels tuned out now: `
|
|
892
890
|
+ `${active.join(', ')}.`;
|
|
893
891
|
return `Nothing was cancelled: ${opening} A tune-out also ends by itself when its duration runs out, or at the `
|
|
894
892
|
+ `first wake past its wake budget, and its backlog goes to ${backlogTo} then.`;
|
package/src/tune-out/tools.ts
CHANGED
|
@@ -113,7 +113,10 @@ export interface SubconsciousConfig {
|
|
|
113
113
|
* agent-prefixed identifier.
|
|
114
114
|
*/
|
|
115
115
|
name?: string;
|
|
116
|
-
/** Model id; defaults to the primary agent's (same-model side-process).
|
|
116
|
+
/** Model id; defaults to the primary agent's (same-model side-process).
|
|
117
|
+
* Another id, compared exactly, makes the subconscious leave out the
|
|
118
|
+
* fields that describe the primary's model, its window and its thinking
|
|
119
|
+
* binding (SUBCONSCIOUS_MODEL_FIELDS); nothing here sets its own. */
|
|
117
120
|
model?: string;
|
|
118
121
|
/**
|
|
119
122
|
* The voice/criteria mode block — recipe-side and co-authored with the
|