@animalabs/agent-framework 0.32.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +204 -14
- package/dist/src/admission/admit.d.ts +18 -0
- package/dist/src/admission/admit.d.ts.map +1 -1
- package/dist/src/admission/admit.js +16 -3
- package/dist/src/admission/admit.js.map +1 -1
- package/dist/src/agent.d.ts +185 -78
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +279 -102
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/mcp-server-core.d.ts.map +1 -1
- package/dist/src/api/mcp-server-core.js +50 -12
- package/dist/src/api/mcp-server-core.js.map +1 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +4 -20
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/code-execution/isolate/scripts.d.ts +12 -8
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +24 -0
- package/dist/src/code-execution/isolate/scripts.js.map +1 -1
- package/dist/src/config-checks.d.ts +7 -0
- package/dist/src/config-checks.d.ts.map +1 -1
- package/dist/src/config-checks.js +14 -0
- package/dist/src/config-checks.js.map +1 -1
- package/dist/src/context-manager-errors.d.ts +25 -0
- package/dist/src/context-manager-errors.d.ts.map +1 -0
- package/dist/src/context-manager-errors.js +31 -0
- package/dist/src/context-manager-errors.js.map +1 -0
- package/dist/src/context-options.d.ts +67 -0
- package/dist/src/context-options.d.ts.map +1 -0
- package/dist/src/context-options.js +85 -0
- package/dist/src/context-options.js.map +1 -0
- package/dist/src/deferred-import.d.ts +93 -0
- package/dist/src/deferred-import.d.ts.map +1 -0
- package/dist/src/deferred-import.js +177 -0
- package/dist/src/deferred-import.js.map +1 -0
- package/dist/src/dendrite/audience.d.ts +223 -0
- package/dist/src/dendrite/audience.d.ts.map +1 -0
- package/dist/src/dendrite/audience.js +202 -0
- package/dist/src/dendrite/audience.js.map +1 -0
- package/dist/src/dendrite/index.d.ts +5 -3
- package/dist/src/dendrite/index.d.ts.map +1 -1
- package/dist/src/dendrite/index.js +2 -1
- package/dist/src/dendrite/index.js.map +1 -1
- package/dist/src/dendrite/presets.d.ts +10 -5
- package/dist/src/dendrite/presets.d.ts.map +1 -1
- package/dist/src/dendrite/presets.js +17 -5
- package/dist/src/dendrite/presets.js.map +1 -1
- package/dist/src/dendrite/registry.d.ts +62 -7
- package/dist/src/dendrite/registry.d.ts.map +1 -1
- package/dist/src/dendrite/registry.js +179 -10
- package/dist/src/dendrite/registry.js.map +1 -1
- package/dist/src/dendrite/types.d.ts +91 -8
- package/dist/src/dendrite/types.d.ts.map +1 -1
- package/dist/src/foreign-rounds.d.ts +69 -0
- package/dist/src/foreign-rounds.d.ts.map +1 -0
- package/dist/src/foreign-rounds.js +152 -0
- package/dist/src/foreign-rounds.js.map +1 -0
- package/dist/src/framework.d.ts +1976 -818
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +7566 -3422
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +85 -15
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +421 -47
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/index.d.ts +1 -1
- package/dist/src/gate/index.d.ts.map +1 -1
- package/dist/src/gate/index.js.map +1 -1
- package/dist/src/gate/rule-scope.d.ts +30 -0
- package/dist/src/gate/rule-scope.d.ts.map +1 -0
- package/dist/src/gate/rule-scope.js +41 -0
- package/dist/src/gate/rule-scope.js.map +1 -0
- package/dist/src/gate/suppress-script-worker.d.ts +2 -0
- package/dist/src/gate/suppress-script-worker.d.ts.map +1 -0
- package/dist/src/gate/suppress-script-worker.js +103 -0
- package/dist/src/gate/suppress-script-worker.js.map +1 -0
- package/dist/src/gate/suppress-scripts.d.ts +105 -0
- package/dist/src/gate/suppress-scripts.d.ts.map +1 -0
- package/dist/src/gate/suppress-scripts.js +403 -0
- package/dist/src/gate/suppress-scripts.js.map +1 -0
- package/dist/src/gate/suppression-review.d.ts +143 -0
- package/dist/src/gate/suppression-review.d.ts.map +1 -0
- package/dist/src/gate/suppression-review.js +463 -0
- package/dist/src/gate/suppression-review.js.map +1 -0
- package/dist/src/gate/suppression-stats.d.ts +125 -0
- package/dist/src/gate/suppression-stats.d.ts.map +1 -0
- package/dist/src/gate/suppression-stats.js +214 -0
- package/dist/src/gate/suppression-stats.js.map +1 -0
- package/dist/src/gate/types.d.ts +135 -7
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/gate/wake-onboarding.d.ts +119 -0
- package/dist/src/gate/wake-onboarding.d.ts.map +1 -0
- package/dist/src/gate/wake-onboarding.js +277 -0
- package/dist/src/gate/wake-onboarding.js.map +1 -0
- package/dist/src/held-writes.d.ts +31 -23
- package/dist/src/held-writes.d.ts.map +1 -1
- package/dist/src/held-writes.js +44 -59
- package/dist/src/held-writes.js.map +1 -1
- package/dist/src/index.d.ts +5 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/jsonrpc-envelope.d.ts +69 -0
- package/dist/src/jsonrpc-envelope.d.ts.map +1 -0
- package/dist/src/jsonrpc-envelope.js +66 -0
- package/dist/src/jsonrpc-envelope.js.map +1 -0
- package/dist/src/mcpl/channel-registry.d.ts +131 -15
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +290 -46
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.d.ts +2 -4
- package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
- package/dist/src/mcpl/feature-set-manager.d.ts +2 -4
- package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
- package/dist/src/mcpl/feature-set-manager.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +2 -1
- package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
- package/dist/src/mcpl/inbound-source.js +2 -1
- package/dist/src/mcpl/inbound-source.js.map +1 -1
- package/dist/src/mcpl/inference-router.d.ts +6 -6
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +132 -22
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +105 -15
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts +9 -7
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +13 -10
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +134 -56
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tool-classes.d.ts.map +1 -1
- package/dist/src/mcpl/tool-classes.js +3 -0
- package/dist/src/mcpl/tool-classes.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +8 -2
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +24 -1
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +68 -18
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts +45 -8
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +237 -86
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/history/index.d.ts +59 -0
- package/dist/src/modules/history/index.d.ts.map +1 -1
- package/dist/src/modules/history/index.js +432 -67
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/history/semantic.d.ts +6 -5
- package/dist/src/modules/history/semantic.d.ts.map +1 -1
- package/dist/src/modules/history/semantic.js +15 -22
- package/dist/src/modules/history/semantic.js.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.d.ts +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +1 -1
- package/dist/src/modules/workspace/index.d.ts +28 -7
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +76 -22
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/notice-tags.d.ts +10 -1
- package/dist/src/notice-tags.d.ts.map +1 -1
- package/dist/src/notice-tags.js +10 -1
- package/dist/src/notice-tags.js.map +1 -1
- package/dist/src/operator-log.d.ts +36 -3
- package/dist/src/operator-log.d.ts.map +1 -1
- package/dist/src/operator-log.js +2 -2
- package/dist/src/operator-log.js.map +1 -1
- package/dist/src/presentation.d.ts +746 -0
- package/dist/src/presentation.d.ts.map +1 -0
- package/dist/src/presentation.js +1435 -0
- package/dist/src/presentation.js.map +1 -0
- package/dist/src/prose-drafts.d.ts +15 -11
- package/dist/src/prose-drafts.d.ts.map +1 -1
- package/dist/src/prose-drafts.js +16 -12
- package/dist/src/prose-drafts.js.map +1 -1
- package/dist/src/read-marks.d.ts +192 -0
- package/dist/src/read-marks.d.ts.map +1 -0
- package/dist/src/read-marks.js +242 -0
- package/dist/src/read-marks.js.map +1 -0
- package/dist/src/recovery/offline-branch.d.ts +91 -6
- package/dist/src/recovery/offline-branch.d.ts.map +1 -1
- package/dist/src/recovery/offline-branch.js +273 -29
- package/dist/src/recovery/offline-branch.js.map +1 -1
- package/dist/src/recovery/recover-cli.js +22 -3
- package/dist/src/recovery/recover-cli.js.map +1 -1
- package/dist/src/send-tools.d.ts +5 -9
- package/dist/src/send-tools.d.ts.map +1 -1
- package/dist/src/send-tools.js +40 -7
- package/dist/src/send-tools.js.map +1 -1
- package/dist/src/stored-injections.d.ts +2 -2
- package/dist/src/stored-injections.js +2 -2
- package/dist/src/stored-removal.d.ts +17 -4
- package/dist/src/stored-removal.d.ts.map +1 -1
- package/dist/src/stored-removal.js +29 -5
- package/dist/src/stored-removal.js.map +1 -1
- package/dist/src/tool-result-guard.d.ts +12 -1
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +42 -8
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +1159 -79
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +3709 -363
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts +114 -17
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/tune-out/tools.js +87 -27
- package/dist/src/tune-out/tools.js.map +1 -1
- package/dist/src/tune-out/window-anchor.d.ts +143 -0
- package/dist/src/tune-out/window-anchor.d.ts.map +1 -0
- package/dist/src/tune-out/window-anchor.js +202 -0
- package/dist/src/tune-out/window-anchor.js.map +1 -0
- package/dist/src/types/agent.d.ts +82 -20
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +2 -0
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +32 -12
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +180 -69
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +510 -36
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/admission/admit.ts +32 -3
- package/src/agent.ts +323 -120
- package/src/api/mcp-server-core.ts +54 -17
- package/src/api/server.ts +4 -23
- package/src/code-execution/isolate/scripts.ts +28 -3
- package/src/config-checks.ts +14 -0
- package/src/context-manager-errors.ts +37 -0
- package/src/context-options.ts +96 -0
- package/src/deferred-import.ts +239 -0
- package/src/dendrite/audience.ts +315 -0
- package/src/dendrite/index.ts +14 -2
- package/src/dendrite/presets.ts +18 -6
- package/src/dendrite/registry.ts +209 -12
- package/src/dendrite/types.ts +92 -8
- package/src/foreign-rounds.ts +199 -0
- package/src/framework.ts +7804 -3546
- package/src/gate/event-gate.ts +428 -65
- package/src/gate/index.ts +3 -0
- package/src/gate/rule-scope.ts +45 -0
- package/src/gate/suppress-script-worker.ts +89 -0
- package/src/gate/suppress-scripts.ts +414 -0
- package/src/gate/suppression-review.ts +517 -0
- package/src/gate/suppression-stats.ts +314 -0
- package/src/gate/types.ts +141 -7
- package/src/gate/wake-onboarding.ts +309 -0
- package/src/held-writes.ts +49 -60
- package/src/index.ts +9 -2
- package/src/jsonrpc-envelope.ts +81 -0
- package/src/mcpl/channel-registry.ts +353 -85
- package/src/mcpl/checkpoint-manager.ts +3 -2
- package/src/mcpl/feature-set-manager.ts +2 -1
- package/src/mcpl/inbound-source.ts +2 -1
- package/src/mcpl/inference-router.ts +7 -4
- package/src/mcpl/push-coalescer.ts +193 -34
- package/src/mcpl/push-handler.ts +10 -5
- package/src/mcpl/server-connection.ts +141 -60
- package/src/mcpl/tool-classes.ts +3 -0
- package/src/mcpl/types.ts +8 -2
- package/src/module-registry.ts +73 -17
- package/src/modules/discord/index.ts +229 -81
- package/src/modules/history/index.ts +439 -70
- package/src/modules/history/semantic.ts +15 -21
- package/src/modules/workspace/disk-agreement.ts +1 -1
- package/src/modules/workspace/index.ts +85 -23
- package/src/notice-tags.ts +10 -1
- package/src/operator-log.ts +36 -3
- package/src/presentation.ts +1676 -0
- package/src/prose-drafts.ts +16 -12
- package/src/read-marks.ts +328 -0
- package/src/recovery/offline-branch.ts +346 -32
- package/src/recovery/recover-cli.ts +21 -3
- package/src/send-tools.ts +40 -7
- package/src/stored-injections.ts +2 -2
- package/src/stored-removal.ts +31 -7
- package/src/tool-result-guard.ts +41 -7
- package/src/tune-out/coordinator.ts +4089 -457
- package/src/tune-out/tools.ts +160 -43
- package/src/tune-out/window-anchor.ts +277 -0
- package/src/types/agent.ts +86 -20
- package/src/types/events.ts +2 -0
- package/src/types/framework.ts +32 -12
- package/src/types/module.ts +181 -68
- package/src/types/trace.ts +542 -40
|
@@ -32,6 +32,35 @@
|
|
|
32
32
|
* Kept deliberately read-only, same posture as HealthModule: no side
|
|
33
33
|
* effects, no message mutation. `onProcess` is a no-op.
|
|
34
34
|
*
|
|
35
|
+
* The tools read the record, not an agent's window (#291): an agent's
|
|
36
|
+
* window is the rows its requests were given, which once agents share a
|
|
37
|
+
* slot is not the whole record. Where the framework offers the window
|
|
38
|
+
* lookup (ModuleContext.windowStatusOf), each entry `extract`, `search` and
|
|
39
|
+
* `semantic_search` return for a row the caller isn't shown says so
|
|
40
|
+
* (`window: "not-shown" | "undone"`), relative to the agent whose call it
|
|
41
|
+
* is, or to the delivery agent for a call no agent made. A row the caller
|
|
42
|
+
* is shown is one its window holds and its view keeps
|
|
43
|
+
* (ModuleContext.viewKeeps): a window written down from the shared record
|
|
44
|
+
* can hold another agent's stored context injection or a held row, which
|
|
45
|
+
* its requests never carry. For an agent's call, another agent's own rows
|
|
46
|
+
* (its rounds, its notes, its stored injections) that it isn't shown are
|
|
47
|
+
* left out unless it observes that agent (decision 1), counted as
|
|
48
|
+
* `leftOut`; without an author filter extract's `offset` counts them, so a
|
|
49
|
+
* caller pages on by `returned + leftOut`, and with one it counts only the
|
|
50
|
+
* rows the filter keeps, which a caller pages on by `resume`. A row stored before the audience stamp, whose owner
|
|
51
|
+
* isn't recorded, is nobody's own and isn't left out: it carries `window`
|
|
52
|
+
* where the caller isn't shown it. The summaries `overview` and `semantic_search` read fold the
|
|
53
|
+
* bound context's agent's window (ModuleContext.agentNameOf says whose), so
|
|
54
|
+
* for another agent's call that doesn't observe that agent they are left
|
|
55
|
+
* out too: `overview` keeps each summarized span's time, level and counts
|
|
56
|
+
* and leaves out its text (`content: null`, `contentLeftOut: true`,
|
|
57
|
+
* counted as `summariesLeftOut`), and `semantic_search` leaves out summary
|
|
58
|
+
* hits, counted in `index.leftOut`. That is for now; serving such a caller
|
|
59
|
+
* its own context's summaries is later work. An operator's call stays
|
|
60
|
+
* record-wide, and so does the bound agent's own. `stats` stays an
|
|
61
|
+
* aggregate over the record, unmarked, as do overview's counts. Without
|
|
62
|
+
* the lookup every tool and every answer is as it was before the index.
|
|
63
|
+
*
|
|
35
64
|
* Every query dispatch collapses onto `queryMessagesByTimeAndChannel` — its
|
|
36
65
|
* own implementation already handles "only a time range", "only a channel",
|
|
37
66
|
* or "neither" by delegating to the single-filter native queries (see
|
|
@@ -41,9 +70,12 @@
|
|
|
41
70
|
import { Worker } from 'node:worker_threads';
|
|
42
71
|
import { fileURLToPath } from 'node:url';
|
|
43
72
|
import { dirname, join } from 'node:path';
|
|
44
|
-
import { EmbedServiceError, SemanticIndexClient, SemanticIndexer, messageIndexText, parseSummaryKey, summaryIndexText } from './semantic.js';
|
|
73
|
+
import { ERROR_MARK, EmbedServiceError, SemanticIndexClient, SemanticIndexer, messageIndexText, parseSummaryKey, summaryIndexText } from './semantic.js';
|
|
45
74
|
import { unknownToolMessage } from '../unknown-tool.js';
|
|
75
|
+
import { speechOf } from '../../send-tools.js';
|
|
46
76
|
import { safeSlice } from '../../safe-slice.js';
|
|
77
|
+
import { audienceOf } from '../../dendrite/audience.js';
|
|
78
|
+
import { storedInjectionMarkOf } from '../../stored-injections.js';
|
|
47
79
|
// ============================================================================
|
|
48
80
|
// Limits
|
|
49
81
|
// ============================================================================
|
|
@@ -546,6 +578,16 @@ export class HistoryModule {
|
|
|
546
578
|
return this.indexer ? this.indexer.catchUp(maxItems) : null;
|
|
547
579
|
}
|
|
548
580
|
getTools() {
|
|
581
|
+
// With the framework's window lookup (#291), entries say which rows the
|
|
582
|
+
// caller isn't being shown, and another agent's own rows are left out
|
|
583
|
+
// for an agent that doesn't observe it: one sentence more in each
|
|
584
|
+
// description that returns rows, and in overview's, whose summaries'
|
|
585
|
+
// text is left out the same way. Without it, the tools are as they were.
|
|
586
|
+
const windowed = !!this.ctx?.windowStatusOf;
|
|
587
|
+
const windowNote = windowed ? WINDOW_NOTE : '';
|
|
588
|
+
// extract's offset counts the rows left out for the caller, as the page's
|
|
589
|
+
// place in the window does.
|
|
590
|
+
const extractWindowNote = windowed ? WINDOW_NOTE + EXTRACT_OFFSET_NOTE : '';
|
|
549
591
|
return [
|
|
550
592
|
{
|
|
551
593
|
name: 'stats',
|
|
@@ -567,7 +609,9 @@ export class HistoryModule {
|
|
|
567
609
|
name: 'extract',
|
|
568
610
|
description: 'Fetch raw, uncompressed messages for a time range and/or channel, paginated oldest-first. ' +
|
|
569
611
|
'Use after `stats` to pull the actual content. format:"text" flattens each message to a short ' +
|
|
570
|
-
'readable string (
|
|
612
|
+
'readable string (a call of a tool the framework names for sending, editing or saying a message, such as ' +
|
|
613
|
+
'send_message, as its name and its words when it carries words, such as "[telegram--send_message] …", its name ending ", error" when its result, stored after it, is flagged as an error; ' +
|
|
614
|
+
'other tool_use/tool_result/thinking/images as bracketed labels, a tool_result flagged as an error ending ", error"); ' +
|
|
571
615
|
'format:"raw" returns the content blocks unmodified. ' +
|
|
572
616
|
'Two extra modes: (1) aroundId — pass a message id (e.g. from `search`) to get the conversation ' +
|
|
573
617
|
'around it: `before` messages before it, the message itself (anchor:true), and `after` messages ' +
|
|
@@ -576,7 +620,8 @@ export class HistoryModule {
|
|
|
576
620
|
'authors; this is filtered in-process over at most maxScan messages of the window, so a response ' +
|
|
577
621
|
'may report truncated:true with a `resume` object ({windowOffset, offset, afterId}) — repeat the call with ' +
|
|
578
622
|
'those fields added to continue exactly where it stopped. If messages were added or removed in the window ' +
|
|
579
|
-
'meanwhile, the resume re-anchors on afterId and says so (windowChanged), or fails loudly when it can\'t.'
|
|
623
|
+
'meanwhile, the resume re-anchors on afterId and says so (windowChanged), or fails loudly when it can\'t.' +
|
|
624
|
+
extractWindowNote,
|
|
580
625
|
inputSchema: {
|
|
581
626
|
type: 'object',
|
|
582
627
|
properties: {
|
|
@@ -613,7 +658,8 @@ export class HistoryModule {
|
|
|
613
658
|
'wholeWord:true stops "mission" from matching inside "uncommissioned". Each match carries an `id` ' +
|
|
614
659
|
'you can hand to extract({aroundId}) to read the conversation around it. ' +
|
|
615
660
|
'regex:true matching runs under a wall-clock deadline and is cleanly failed ' +
|
|
616
|
-
`(not silently empty) if a pattern is too slow — avoid nested-quantifier patterns like (a+)
|
|
661
|
+
`(not silently empty) if a pattern is too slow — avoid nested-quantifier patterns like (a+)+.` +
|
|
662
|
+
windowNote,
|
|
617
663
|
inputSchema: {
|
|
618
664
|
type: 'object',
|
|
619
665
|
properties: {
|
|
@@ -660,7 +706,8 @@ export class HistoryModule {
|
|
|
660
706
|
'with a clear error rather than silently returning no spans. An entry marked boundaryUncertain:true ' +
|
|
661
707
|
'had a millisecond-timestamp collision at its edge with another span (rare — two distinct messages ' +
|
|
662
708
|
'landing in the same millisecond); its messageCount is still exact but tokensEstimate may be off by ' +
|
|
663
|
-
'a small, bounded amount.'
|
|
709
|
+
'a small, bounded amount.' +
|
|
710
|
+
(windowed ? WINDOW_NOTE_OVERVIEW : ''),
|
|
664
711
|
inputSchema: {
|
|
665
712
|
type: 'object',
|
|
666
713
|
properties: {
|
|
@@ -685,6 +732,9 @@ export class HistoryModule {
|
|
|
685
732
|
];
|
|
686
733
|
}
|
|
687
734
|
semanticSearchTool() {
|
|
735
|
+
// An undo moves no branch since #291, so with the window lookup an
|
|
736
|
+
// undone message is a hit, marked, rather than dropped off the branch.
|
|
737
|
+
const windowed = !!this.ctx?.windowStatusOf;
|
|
688
738
|
return {
|
|
689
739
|
name: 'semantic_search',
|
|
690
740
|
description: 'Search your own history by MEANING, not exact words: an embedding index over your raw messages ' +
|
|
@@ -698,12 +748,13 @@ export class HistoryModule {
|
|
|
698
748
|
'searching (bounded, so a huge backlog is reported as `index.behind` rather than blocking; if a ' +
|
|
699
749
|
'background sync is already running, the search waits for that run to finish instead). ' +
|
|
700
750
|
'Purely a read: nothing is written to your history. Hits are checked against your current branch ' +
|
|
701
|
-
|
|
751
|
+
`before they come back: ${windowed ? 'a message or ' : 'a message you undid or '}a summary from a branch you left is dropped and ` +
|
|
702
752
|
'counted in index.droppedOffBranch; a message edited down to nothing searchable is dropped and ' +
|
|
703
753
|
'counted in index.droppedStale. When that leaves fewer than `limit` hits and the index may hold more, it ' +
|
|
704
754
|
'searches again, deeper, down to its first 200 hits; if a search after the first fails, the hits found so ' +
|
|
705
755
|
'far come back, with why in index.searchAgainError. Message snippets ' +
|
|
706
|
-
'always show the current text of the message.'
|
|
756
|
+
'always show the current text of the message.' +
|
|
757
|
+
(windowed ? WINDOW_NOTE_SEMANTIC : ''),
|
|
707
758
|
inputSchema: {
|
|
708
759
|
type: 'object',
|
|
709
760
|
properties: {
|
|
@@ -733,13 +784,13 @@ export class HistoryModule {
|
|
|
733
784
|
case 'stats':
|
|
734
785
|
return this.handleStats((call.input ?? {}));
|
|
735
786
|
case 'extract':
|
|
736
|
-
return this.handleExtract((call.input ?? {}));
|
|
787
|
+
return this.handleExtract((call.input ?? {}), this.viewFor(call));
|
|
737
788
|
case 'search':
|
|
738
|
-
return await this.handleSearch((call.input ?? {}));
|
|
789
|
+
return await this.handleSearch((call.input ?? {}), this.viewFor(call));
|
|
739
790
|
case 'overview':
|
|
740
|
-
return this.handleOverview((call.input ?? {}));
|
|
791
|
+
return this.handleOverview((call.input ?? {}), this.viewFor(call));
|
|
741
792
|
case 'semantic_search':
|
|
742
|
-
return await this.handleSemanticSearch((call.input ?? {}));
|
|
793
|
+
return await this.handleSemanticSearch((call.input ?? {}), this.viewFor(call));
|
|
743
794
|
default:
|
|
744
795
|
return { success: false, isError: true, error: unknownToolMessage(this.name, call.name, this.getTools()) };
|
|
745
796
|
}
|
|
@@ -766,6 +817,130 @@ export class HistoryModule {
|
|
|
766
817
|
async onProcess(_event, _state) {
|
|
767
818
|
return {};
|
|
768
819
|
}
|
|
820
|
+
/**
|
|
821
|
+
* How one call's caller sees the record (#291), when the framework offers
|
|
822
|
+
* the window lookup (ModuleContext.windowStatusOf); undefined otherwise,
|
|
823
|
+
* and then every entry is as it was before the presentation index. The
|
|
824
|
+
* caller is the agent whose call it is (callerAgentName), or, for a call
|
|
825
|
+
* no agent made (an operator's, a host's), the delivery agent, for marks
|
|
826
|
+
* only. A row the caller is shown is one its window holds and its view
|
|
827
|
+
* keeps (ModuleContext.viewKeeps): a window written down from the record
|
|
828
|
+
* the residents share can hold another agent's stored context injection,
|
|
829
|
+
* or a row a tune-out holds from the residents, which the caller's
|
|
830
|
+
* requests never carry. A row the caller isn't shown is marked
|
|
831
|
+
* (`window`); for an agent's call, another agent's own row (its rounds,
|
|
832
|
+
* its notes, its stored injections) that it isn't shown is left out,
|
|
833
|
+
* unless it observes that agent: seeing another agent's thread is a
|
|
834
|
+
* declared relationship (decision 1), an observe edge, such as a
|
|
835
|
+
* resident's `AgentConfig.observes`. Each row's status is read once per
|
|
836
|
+
* call.
|
|
837
|
+
*
|
|
838
|
+
* The bound context's summaries fold its agent's window, that agent's own
|
|
839
|
+
* rounds and notes among it, so for another agent's call that doesn't
|
|
840
|
+
* observe that agent they are left out too (`leavesSummaries`). That is
|
|
841
|
+
* for now: serving such a caller summaries of its own context is later
|
|
842
|
+
* work. An agent's call where the framework can't say whose context the
|
|
843
|
+
* module is bound to (ModuleContext.agentNameOf missing, or answering
|
|
844
|
+
* that it is no live agent's) leaves them out as well. A call no agent
|
|
845
|
+
* made, and the bound agent's own, read them as before.
|
|
846
|
+
*/
|
|
847
|
+
viewFor(call) {
|
|
848
|
+
const ctx = this.ctx;
|
|
849
|
+
if (!ctx?.windowStatusOf)
|
|
850
|
+
return undefined;
|
|
851
|
+
const caller = call.callerAgentName;
|
|
852
|
+
const forCaller = caller === undefined ? undefined : { forAgent: caller };
|
|
853
|
+
const statuses = new Map();
|
|
854
|
+
// Set once a lookup throws: an agent the framework no longer knows. Its
|
|
855
|
+
// window is taken as holding nothing, so each row is marked not shown.
|
|
856
|
+
let unknown = false;
|
|
857
|
+
const statusOf = (msg) => {
|
|
858
|
+
if (unknown)
|
|
859
|
+
return null;
|
|
860
|
+
const id = String(msg.id);
|
|
861
|
+
let status = statuses.get(id);
|
|
862
|
+
if (status === undefined) {
|
|
863
|
+
try {
|
|
864
|
+
status = ctx.windowStatusOf(msg.id, forCaller);
|
|
865
|
+
}
|
|
866
|
+
catch {
|
|
867
|
+
unknown = true;
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
870
|
+
statuses.set(id, status);
|
|
871
|
+
}
|
|
872
|
+
return status;
|
|
873
|
+
};
|
|
874
|
+
// Whether the caller's view keeps a row its window holds. Without the
|
|
875
|
+
// lookup, every row its window holds is taken as shown, as before it.
|
|
876
|
+
const kept = new Map();
|
|
877
|
+
const keeps = (msg) => {
|
|
878
|
+
if (!ctx.viewKeeps)
|
|
879
|
+
return true;
|
|
880
|
+
const id = String(msg.id);
|
|
881
|
+
let keep = kept.get(id);
|
|
882
|
+
if (keep === undefined) {
|
|
883
|
+
try {
|
|
884
|
+
keep = ctx.viewKeeps(msg, forCaller);
|
|
885
|
+
}
|
|
886
|
+
catch {
|
|
887
|
+
unknown = true;
|
|
888
|
+
return false;
|
|
889
|
+
}
|
|
890
|
+
kept.set(id, keep);
|
|
891
|
+
}
|
|
892
|
+
return keep;
|
|
893
|
+
};
|
|
894
|
+
const shown = (msg) => statusOf(msg) === 'in' && keeps(msg);
|
|
895
|
+
const observed = new Map();
|
|
896
|
+
const observes = (owner) => {
|
|
897
|
+
let sees = observed.get(owner);
|
|
898
|
+
if (sees === undefined) {
|
|
899
|
+
try {
|
|
900
|
+
sees = ctx.observes?.(caller, owner) ?? false;
|
|
901
|
+
}
|
|
902
|
+
catch {
|
|
903
|
+
sees = false;
|
|
904
|
+
}
|
|
905
|
+
observed.set(owner, sees);
|
|
906
|
+
}
|
|
907
|
+
return sees;
|
|
908
|
+
};
|
|
909
|
+
let leavesSummaries = false;
|
|
910
|
+
if (caller !== undefined) {
|
|
911
|
+
let bound;
|
|
912
|
+
try {
|
|
913
|
+
bound = this.cm ? ctx.agentNameOf?.(this.cm) : undefined;
|
|
914
|
+
}
|
|
915
|
+
catch {
|
|
916
|
+
bound = undefined;
|
|
917
|
+
}
|
|
918
|
+
leavesSummaries = bound === undefined || (bound !== caller && !observes(bound));
|
|
919
|
+
}
|
|
920
|
+
return {
|
|
921
|
+
mark: (msg) => {
|
|
922
|
+
const status = statusOf(msg);
|
|
923
|
+
if (unknown)
|
|
924
|
+
return 'not-shown';
|
|
925
|
+
if (status === 'undone')
|
|
926
|
+
return 'undone';
|
|
927
|
+
if (status === 'out' || status === 'absent')
|
|
928
|
+
return 'not-shown';
|
|
929
|
+
return status === 'in' && !keeps(msg) ? 'not-shown' : undefined;
|
|
930
|
+
},
|
|
931
|
+
leaves: (msg) => {
|
|
932
|
+
if (caller === undefined)
|
|
933
|
+
return false;
|
|
934
|
+
// A stored injection from before the audience stamp names its agent
|
|
935
|
+
// in its own mark: it is that agent's context, as every view reads it.
|
|
936
|
+
const owner = audienceOf(msg.metadata)?.ownRowOf ?? storedInjectionMarkOf(msg.metadata)?.agent;
|
|
937
|
+
if (owner === undefined || owner === caller)
|
|
938
|
+
return false;
|
|
939
|
+
return !shown(msg) && !observes(owner);
|
|
940
|
+
},
|
|
941
|
+
leavesSummaries,
|
|
942
|
+
};
|
|
943
|
+
}
|
|
769
944
|
// ==========================================================================
|
|
770
945
|
// stats
|
|
771
946
|
// ==========================================================================
|
|
@@ -801,9 +976,9 @@ export class HistoryModule {
|
|
|
801
976
|
// ==========================================================================
|
|
802
977
|
// extract
|
|
803
978
|
// ==========================================================================
|
|
804
|
-
handleExtract(input) {
|
|
979
|
+
handleExtract(input, view) {
|
|
805
980
|
if (input.aroundId !== undefined)
|
|
806
|
-
return this.handleExtractAround(input);
|
|
981
|
+
return this.handleExtractAround(input, view);
|
|
807
982
|
const aroundOnly = ['before', 'after', 'allChannels'].filter((k) => input[k] !== undefined);
|
|
808
983
|
if (aroundOnly.length > 0) {
|
|
809
984
|
const one = aroundOnly.length === 1;
|
|
@@ -828,6 +1003,17 @@ export class HistoryModule {
|
|
|
828
1003
|
'author filter, use "offset".');
|
|
829
1004
|
}
|
|
830
1005
|
if (authorFilter) {
|
|
1006
|
+
// A row left out for the caller (viewFor) is one the filter doesn't
|
|
1007
|
+
// keep, counted in leftOut.
|
|
1008
|
+
let leftOut = 0;
|
|
1009
|
+
const keeps = (m) => {
|
|
1010
|
+
if (!authorFilter(m))
|
|
1011
|
+
return false;
|
|
1012
|
+
if (!view?.leaves(m))
|
|
1013
|
+
return true;
|
|
1014
|
+
leftOut++;
|
|
1015
|
+
return false;
|
|
1016
|
+
};
|
|
831
1017
|
// No native author index: page through the time/channel window and
|
|
832
1018
|
// filter here. offset/limit apply to the FILTERED sequence. Stop as
|
|
833
1019
|
// soon as the requested page is full; otherwise scan to maxScan. Only
|
|
@@ -842,7 +1028,7 @@ export class HistoryModule {
|
|
|
842
1028
|
const maxScan = clampCount(input.maxScan, EXTRACT_FILTER_DEFAULT_MAX_SCAN, EXTRACT_FILTER_MAX_MAX_SCAN, 'maxScan', 'read', 1);
|
|
843
1029
|
const requestedOffset = clampCount(input.windowOffset, 0, NATIVE_OFFSET_MAX, 'windowOffset', 'read');
|
|
844
1030
|
// A position is only meaningful against the window it was taken in:
|
|
845
|
-
// a removal before it (every discord:delete, host hide
|
|
1031
|
+
// a removal before it (every discord:delete, host hide) shifts
|
|
846
1032
|
// later positions left, and a message inserted before it (a
|
|
847
1033
|
// time-ordered window with backfill) shifts them right. Either would
|
|
848
1034
|
// silently skip or repeat a message. afterId pins the position to the
|
|
@@ -880,9 +1066,13 @@ export class HistoryModule {
|
|
|
880
1066
|
let kept = 0;
|
|
881
1067
|
let scanned = 0;
|
|
882
1068
|
let lastScanned;
|
|
883
|
-
// Window position just past the last message put on the page,
|
|
1069
|
+
// Window position just past the last message put on the page, that
|
|
1070
|
+
// message, and the rows left out up to it: a page-full stop resumes
|
|
1071
|
+
// there, so the rows the lookahead passed after it are the next page's
|
|
1072
|
+
// to count.
|
|
884
1073
|
let afterPage = windowOffset;
|
|
885
1074
|
let lastOnPage;
|
|
1075
|
+
let leftOutAtPage = 0;
|
|
886
1076
|
let exhausted = false;
|
|
887
1077
|
let pageFull = false;
|
|
888
1078
|
outer: for (;;) {
|
|
@@ -899,12 +1089,13 @@ export class HistoryModule {
|
|
|
899
1089
|
for (const m of chunk) {
|
|
900
1090
|
scanned++;
|
|
901
1091
|
lastScanned = m;
|
|
902
|
-
if (!
|
|
1092
|
+
if (!keeps(m))
|
|
903
1093
|
continue;
|
|
904
1094
|
if (kept >= offset && page.length < limit) {
|
|
905
1095
|
page.push(m);
|
|
906
1096
|
afterPage = windowOffset + scanned;
|
|
907
1097
|
lastOnPage = m;
|
|
1098
|
+
leftOutAtPage = leftOut;
|
|
908
1099
|
}
|
|
909
1100
|
kept++;
|
|
910
1101
|
if (limit > 0 && page.length >= limit && kept > offset + limit) {
|
|
@@ -931,19 +1122,22 @@ export class HistoryModule {
|
|
|
931
1122
|
if (next.length === 0)
|
|
932
1123
|
exhausted = true;
|
|
933
1124
|
}
|
|
1125
|
+
const counted = pageFull ? leftOutAtPage : leftOut;
|
|
934
1126
|
return {
|
|
935
1127
|
success: true,
|
|
936
1128
|
data: {
|
|
937
1129
|
// A resumed scan (windowOffset > 0) only saw the window from that
|
|
938
1130
|
// position on, so its count is named for that — never a total. A
|
|
939
1131
|
// page-full stop's lookahead match lies past the resume point and
|
|
940
|
-
// is left out, so counts add up across a chain of resumes
|
|
1132
|
+
// is left out, so counts add up across a chain of resumes, and so
|
|
1133
|
+
// do the rows left out on the way to it, which the resume scans.
|
|
941
1134
|
...(() => {
|
|
942
1135
|
const n = pageFull ? kept - 1 : kept;
|
|
943
1136
|
const base = windowOffset > 0 ? 'matchedSinceWindowOffset' : 'matchedCount';
|
|
944
1137
|
return { [exhausted ? base : `${base}AtLeast`]: n };
|
|
945
1138
|
})(),
|
|
946
1139
|
returned: page.length,
|
|
1140
|
+
...(counted > 0 ? { leftOut: counted } : {}),
|
|
947
1141
|
scanned,
|
|
948
1142
|
truncated: !exhausted,
|
|
949
1143
|
...(shift !== 0 || missed.length > 0 || unverified
|
|
@@ -962,7 +1156,7 @@ export class HistoryModule {
|
|
|
962
1156
|
? {
|
|
963
1157
|
addedBefore: missed.length,
|
|
964
1158
|
// the ones this filter would have kept — fetch them with extract({aroundId})
|
|
965
|
-
missedIds: missed.filter(authorFilter).map((m) => String(m.id)),
|
|
1159
|
+
missedIds: missed.filter((m) => authorFilter(m) && !view?.leaves(m)).map((m) => String(m.id)),
|
|
966
1160
|
}
|
|
967
1161
|
: {}),
|
|
968
1162
|
note: missed.length > 0 || shift > 0
|
|
@@ -1002,7 +1196,7 @@ export class HistoryModule {
|
|
|
1002
1196
|
};
|
|
1003
1197
|
})()
|
|
1004
1198
|
: {}),
|
|
1005
|
-
messages: page.map((msg) => projectMessage(msg, format)),
|
|
1199
|
+
messages: page.map((msg) => projectMessage(msg, format, (m) => this.failedSpeech(m), view?.mark(msg))),
|
|
1006
1200
|
},
|
|
1007
1201
|
};
|
|
1008
1202
|
}
|
|
@@ -1017,14 +1211,19 @@ export class HistoryModule {
|
|
|
1017
1211
|
limit: limit + 1,
|
|
1018
1212
|
offset,
|
|
1019
1213
|
});
|
|
1020
|
-
const
|
|
1214
|
+
const fetched = result.messages.slice(0, limit);
|
|
1215
|
+
// Rows left out for the caller (viewFor) leave the page; the page's
|
|
1216
|
+
// place in the window, and so offset and hasMore, stay the window's.
|
|
1217
|
+
const page = view ? fetched.filter((msg) => !view.leaves(msg)) : fetched;
|
|
1218
|
+
const leftOut = fetched.length - page.length;
|
|
1021
1219
|
return {
|
|
1022
1220
|
success: true,
|
|
1023
1221
|
data: {
|
|
1024
1222
|
...(channelId !== undefined ? { matchedCount: result.matchedCount } : {}),
|
|
1025
1223
|
returned: page.length,
|
|
1224
|
+
...(leftOut > 0 ? { leftOut } : {}),
|
|
1026
1225
|
hasMore: result.messages.length > limit,
|
|
1027
|
-
messages: page.map((msg) => projectMessage(msg, format)),
|
|
1226
|
+
messages: page.map((msg) => projectMessage(msg, format, (m) => this.failedSpeech(m), view?.mark(msg))),
|
|
1028
1227
|
},
|
|
1029
1228
|
};
|
|
1030
1229
|
}
|
|
@@ -1035,7 +1234,7 @@ export class HistoryModule {
|
|
|
1035
1234
|
* side still needs, the nearest messages strictly before/after it (both
|
|
1036
1235
|
* via edgeWindow, which is exact by timestamp).
|
|
1037
1236
|
*/
|
|
1038
|
-
handleExtractAround(input) {
|
|
1237
|
+
handleExtractAround(input, view) {
|
|
1039
1238
|
const windowFields = ['from', 'to', 'offset', 'windowOffset', 'limit'].filter((k) => input[k] !== undefined);
|
|
1040
1239
|
if (windowFields.length > 0) {
|
|
1041
1240
|
throw new Error(`Nothing was read: "aroundId" picks its own window, so it doesn't take ${quotedList(windowFields, 'or')}. Size ` +
|
|
@@ -1063,7 +1262,9 @@ export class HistoryModule {
|
|
|
1063
1262
|
? 'the "messageId" of a history--semantic_search hit.'
|
|
1064
1263
|
: 'the "id" of a history--search or history--extract result.'));
|
|
1065
1264
|
}
|
|
1066
|
-
const
|
|
1265
|
+
const found = cm.getMessage(rawId.replace(/^msg:/, ''));
|
|
1266
|
+
// A row left out for the caller (viewFor) is answered as one not there.
|
|
1267
|
+
const anchor = found && !view?.leaves(found) ? found : null;
|
|
1067
1268
|
if (!anchor) {
|
|
1068
1269
|
throw new Error(`Nothing was read: no message in your history has the id ${shown(input.aroundId)}. Pass the "id" ` +
|
|
1069
1270
|
'of a history--search or history--extract result' +
|
|
@@ -1105,15 +1306,18 @@ export class HistoryModule {
|
|
|
1105
1306
|
const needAfter = after - tiesAfter.length;
|
|
1106
1307
|
const earlier = needBefore > 0 ? this.edgeWindow({ toMs: t - 1, channelId, n: needBefore, side: 'newest' }).messages.reverse() : [];
|
|
1107
1308
|
const later = needAfter > 0 ? this.edgeWindow({ fromMs: t + 1, channelId, n: needAfter, side: 'oldest' }).messages : [];
|
|
1108
|
-
const
|
|
1309
|
+
const around = [...earlier, ...tiesBefore, ties[idx], ...tiesAfter, ...later];
|
|
1310
|
+
const window = view ? around.filter((m) => !view.leaves(m)) : around;
|
|
1311
|
+
const leftOut = around.length - window.length;
|
|
1109
1312
|
return {
|
|
1110
1313
|
success: true,
|
|
1111
1314
|
data: {
|
|
1112
1315
|
anchorId,
|
|
1113
1316
|
channelId: channelId ?? null,
|
|
1114
1317
|
returned: window.length,
|
|
1318
|
+
...(leftOut > 0 ? { leftOut } : {}),
|
|
1115
1319
|
messages: window.map((m) => ({
|
|
1116
|
-
...projectMessage(m, format),
|
|
1320
|
+
...projectMessage(m, format, (n) => this.failedSpeech(n), view?.mark(m)),
|
|
1117
1321
|
...(String(m.id) === anchorId ? { anchor: true } : {}),
|
|
1118
1322
|
})),
|
|
1119
1323
|
},
|
|
@@ -1122,7 +1326,7 @@ export class HistoryModule {
|
|
|
1122
1326
|
// ==========================================================================
|
|
1123
1327
|
// search
|
|
1124
1328
|
// ==========================================================================
|
|
1125
|
-
async handleSearch(input) {
|
|
1329
|
+
async handleSearch(input, view) {
|
|
1126
1330
|
// A finite number is searched as its text, as a regular expression made
|
|
1127
1331
|
// from it reads it; any other value that isn't text is refused.
|
|
1128
1332
|
const given = input.query;
|
|
@@ -1203,11 +1407,32 @@ export class HistoryModule {
|
|
|
1203
1407
|
unscanned = w.total - (w.messages.length - pool.length) - windowMessages.length;
|
|
1204
1408
|
}
|
|
1205
1409
|
const candidatePoolSize = windowMessages.length;
|
|
1206
|
-
const
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1410
|
+
const byAuthor = authorFilter ? windowMessages.filter(authorFilter) : windowMessages;
|
|
1411
|
+
// A row left out for the caller (viewFor) is no candidate. Beside each
|
|
1412
|
+
// candidate, how many were left out before it.
|
|
1413
|
+
const candidates = [];
|
|
1414
|
+
const leftOutBefore = [];
|
|
1415
|
+
let leftOutOfPool = 0;
|
|
1416
|
+
for (const m of byAuthor) {
|
|
1417
|
+
if (view?.leaves(m)) {
|
|
1418
|
+
leftOutOfPool++;
|
|
1419
|
+
continue;
|
|
1420
|
+
}
|
|
1421
|
+
candidates.push(m);
|
|
1422
|
+
leftOutBefore.push(leftOutOfPool);
|
|
1423
|
+
}
|
|
1424
|
+
// A scan stopped at `limit` resumes after its last scanned candidate, so
|
|
1425
|
+
// the rows left out past it are the next call's to count, as extract's
|
|
1426
|
+
// page-full stop counts them (leftOutAtPage): counts add up across a
|
|
1427
|
+
// chain of resumes.
|
|
1428
|
+
const poolInfo = (scanned) => {
|
|
1429
|
+
const leftOut = scanned > 0 && scanned < candidates.length ? leftOutBefore[scanned - 1] : leftOutOfPool;
|
|
1430
|
+
return {
|
|
1431
|
+
candidatePoolSize,
|
|
1432
|
+
...(authorFilter ? { afterAuthorFilter: byAuthor.length } : {}),
|
|
1433
|
+
...(leftOut > 0 ? { leftOut } : {}),
|
|
1434
|
+
order,
|
|
1435
|
+
};
|
|
1211
1436
|
};
|
|
1212
1437
|
// Where the scan stopped, when it stopped before the end of the window
|
|
1213
1438
|
// the caller asked about: either `limit` matches came first (the rest of
|
|
@@ -1276,8 +1501,8 @@ export class HistoryModule {
|
|
|
1276
1501
|
// through a worker thread instead, which can be forcibly terminated
|
|
1277
1502
|
// on a deadline. See search-regex-worker.ts's header for the full
|
|
1278
1503
|
// rationale.
|
|
1279
|
-
const { matches, scanned } = await this.searchWithRegexWorker(candidates, query, flags, limit);
|
|
1280
|
-
return { success: true, data: { scanned, ...poolInfo, truncated, ...resumeInfo(scanned, matches.length), matches } };
|
|
1504
|
+
const { matches, scanned } = await this.searchWithRegexWorker(candidates, query, flags, limit, view);
|
|
1505
|
+
return { success: true, data: { scanned, ...poolInfo(scanned), truncated, ...resumeInfo(scanned, matches.length), matches } };
|
|
1281
1506
|
}
|
|
1282
1507
|
// Plain substring search (String.prototype.indexOf) is inherently
|
|
1283
1508
|
// linear in input length — no ReDoS-equivalent risk — so it stays
|
|
@@ -1304,14 +1529,73 @@ export class HistoryModule {
|
|
|
1304
1529
|
participant: msg.participant,
|
|
1305
1530
|
author: authorName(msg),
|
|
1306
1531
|
channelId: getChannelId(msg) ?? null,
|
|
1307
|
-
|
|
1532
|
+
...windowField(view?.mark(msg)),
|
|
1533
|
+
snippet: this.hitSnippet(msg, text, hit.index, hit.length),
|
|
1308
1534
|
});
|
|
1309
1535
|
}
|
|
1310
1536
|
return {
|
|
1311
1537
|
success: true,
|
|
1312
|
-
data: { scanned, ...poolInfo, truncated, ...resumeInfo(scanned, matches.length), matches },
|
|
1538
|
+
data: { scanned, ...poolInfo(scanned), truncated, ...resumeInfo(scanned, matches.length), matches },
|
|
1313
1539
|
};
|
|
1314
1540
|
}
|
|
1541
|
+
failedSpeech(msg) {
|
|
1542
|
+
const pending = new Set();
|
|
1543
|
+
for (const block of msg.content)
|
|
1544
|
+
if (block.type === 'tool_use' && speechOf(block.name, block.input))
|
|
1545
|
+
pending.add(block.id);
|
|
1546
|
+
if (!pending.size)
|
|
1547
|
+
return NO_FAILURES;
|
|
1548
|
+
const cm = this.cm;
|
|
1549
|
+
const fromMs = msg.timestamp.getTime();
|
|
1550
|
+
const failed = new Set();
|
|
1551
|
+
const answer = (blocks) => {
|
|
1552
|
+
for (const block of blocks) {
|
|
1553
|
+
if (block.type === 'tool_result' && pending.delete(block.toolUseId) && block.isError)
|
|
1554
|
+
failed.add(block.toolUseId);
|
|
1555
|
+
}
|
|
1556
|
+
};
|
|
1557
|
+
answer(msg.content);
|
|
1558
|
+
let past = false;
|
|
1559
|
+
let body = msg.bodyGroupId;
|
|
1560
|
+
for (let offset = 0; pending.size > 0; offset++) {
|
|
1561
|
+
const later = cm.queryMessagesByTime({ fromMs, limit: 1, offset }).messages[0];
|
|
1562
|
+
if (!later)
|
|
1563
|
+
break;
|
|
1564
|
+
if (!past) {
|
|
1565
|
+
if (later.timestamp.getTime() > fromMs)
|
|
1566
|
+
break;
|
|
1567
|
+
past = String(later.id) === String(msg.id);
|
|
1568
|
+
continue;
|
|
1569
|
+
}
|
|
1570
|
+
const blocks = later.content;
|
|
1571
|
+
if (later.bodyGroupId === undefined || later.bodyGroupId !== body) {
|
|
1572
|
+
if (blocks.some((block) => block.type === 'tool_use') || !blocks.some((block) => block.type === 'tool_result'))
|
|
1573
|
+
break;
|
|
1574
|
+
body = later.bodyGroupId;
|
|
1575
|
+
}
|
|
1576
|
+
answer(blocks);
|
|
1577
|
+
}
|
|
1578
|
+
return failed;
|
|
1579
|
+
}
|
|
1580
|
+
hitSnippet(msg, text, index, length) {
|
|
1581
|
+
const content = msg.content;
|
|
1582
|
+
const failed = this.failedSpeech(msg);
|
|
1583
|
+
const spans = blockSpans(content);
|
|
1584
|
+
for (const { block, at, label } of spans) {
|
|
1585
|
+
if (block.type !== 'tool_use')
|
|
1586
|
+
continue;
|
|
1587
|
+
const said = speechOf(block.name, block.input);
|
|
1588
|
+
const wordsAt = at + label.length - (said?.words.length ?? 0);
|
|
1589
|
+
if (said && index >= wordsAt && index < at + label.length && index + length <= at + label.length) {
|
|
1590
|
+
return `[${block.name}${failed.has(block.id) ? ERROR_MARK : ''}] ${snippetAround(said.words, index - wordsAt, length)}`;
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
if (!failed.size)
|
|
1594
|
+
return snippetAround(text, index, length);
|
|
1595
|
+
const marks = spans.flatMap(({ block, at }) => (block.type === 'tool_use' && failed.has(block.id) ? [at + 1 + block.name.length] : []));
|
|
1596
|
+
const moved = (i) => i + ERROR_MARK.length * marks.filter((mark) => mark <= i).length;
|
|
1597
|
+
return snippetAround(flattenContent(content, failed), moved(index), moved(index + length) - moved(index));
|
|
1598
|
+
}
|
|
1315
1599
|
/**
|
|
1316
1600
|
* Run regex matching for `search` on a worker thread with a hard
|
|
1317
1601
|
* wall-clock deadline, so a catastrophically-backtracking pattern can be
|
|
@@ -1326,7 +1610,7 @@ export class HistoryModule {
|
|
|
1326
1610
|
* module) rather than returning an empty match list — a timed-out search
|
|
1327
1611
|
* must never be indistinguishable from a clean "no matches" result.
|
|
1328
1612
|
*/
|
|
1329
|
-
async searchWithRegexWorker(candidates, pattern, flags, limit) {
|
|
1613
|
+
async searchWithRegexWorker(candidates, pattern, flags, limit, view) {
|
|
1330
1614
|
const texts = candidates.map((msg) => flattenContent(msg.content));
|
|
1331
1615
|
let worker;
|
|
1332
1616
|
try {
|
|
@@ -1365,7 +1649,8 @@ export class HistoryModule {
|
|
|
1365
1649
|
participant: msg.participant,
|
|
1366
1650
|
author: authorName(msg),
|
|
1367
1651
|
channelId: getChannelId(msg) ?? null,
|
|
1368
|
-
|
|
1652
|
+
...windowField(view?.mark(msg)),
|
|
1653
|
+
snippet: this.hitSnippet(msg, text, matchIndex, matchLength),
|
|
1369
1654
|
};
|
|
1370
1655
|
});
|
|
1371
1656
|
return { matches, scanned };
|
|
@@ -1381,7 +1666,7 @@ export class HistoryModule {
|
|
|
1381
1666
|
// ==========================================================================
|
|
1382
1667
|
// overview
|
|
1383
1668
|
// ==========================================================================
|
|
1384
|
-
async handleSemanticSearch(input) {
|
|
1669
|
+
async handleSemanticSearch(input, view) {
|
|
1385
1670
|
if (!this.semanticCfg || !this.semanticClient) {
|
|
1386
1671
|
throw new Error("Nothing was searched: this host has no semantic index set up, so history--semantic_search isn't available " +
|
|
1387
1672
|
"here, and calling it again won't change that. history--search finds messages by their words.");
|
|
@@ -1488,16 +1773,23 @@ export class HistoryModule {
|
|
|
1488
1773
|
};
|
|
1489
1774
|
};
|
|
1490
1775
|
// The remote index is append-only and knows nothing about branches: a
|
|
1491
|
-
// message removed from the agent's reach by
|
|
1492
|
-
// /newtopic keeps its id (a new message gets a fresh one) and
|
|
1493
|
-
// indexed. Check every hit against the CURRENT branch before it goes
|
|
1494
|
-
// to the model — getMessage/getSummary rebuild on branch switch, so
|
|
1495
|
-
//
|
|
1496
|
-
//
|
|
1497
|
-
//
|
|
1498
|
-
//
|
|
1499
|
-
// is
|
|
1500
|
-
//
|
|
1776
|
+
// message removed from the agent's reach by a rollback, /checkout,
|
|
1777
|
+
// /restore or /newtopic keeps its id (a new message gets a fresh one) and
|
|
1778
|
+
// stays indexed. Check every hit against the CURRENT branch before it goes
|
|
1779
|
+
// back to the model — getMessage/getSummary rebuild on branch switch, so a
|
|
1780
|
+
// message, or a summary, this branch doesn't hold answers null. An undo
|
|
1781
|
+
// works on the agent's window since #291 and moves no branch, so an
|
|
1782
|
+
// undone message stays on it and comes back, marked `window: "undone"`
|
|
1783
|
+
// for the caller when the framework offers the window lookup (viewFor),
|
|
1784
|
+
// as is one the caller wasn't shown; another agent's own row the caller
|
|
1785
|
+
// doesn't observe is left out (index.leftOut), and so is a summary hit
|
|
1786
|
+
// for another agent's call that doesn't observe the agent whose context
|
|
1787
|
+
// the summaries fold (viewFor's leavesSummaries).
|
|
1788
|
+
// A branch made from an earlier point, as a rollback makes one, can mint
|
|
1789
|
+
// a summary id another branch already used, so a summary's key carries
|
|
1790
|
+
// its creation time too, and a hit whose time isn't this branch's
|
|
1791
|
+
// summary's is another branch's. k is small, so this is at most `limit`
|
|
1792
|
+
// local lookups.
|
|
1501
1793
|
// A message hit whose current text has nothing left to index (edited
|
|
1502
1794
|
// blank, delete not yet applied) is dropped too, so its stale snippet is
|
|
1503
1795
|
// never served. Dropped hits are replaced by over-fetching: when the
|
|
@@ -1511,6 +1803,8 @@ export class HistoryModule {
|
|
|
1511
1803
|
// A summary hit's snippet is rebuilt from the local summary too, so a
|
|
1512
1804
|
// service that keeps only vectors and ids (no text) still yields one.
|
|
1513
1805
|
const currentText = new Map();
|
|
1806
|
+
// Each message hit's mark for the caller (viewFor), by the hit's id.
|
|
1807
|
+
const marks = new Map();
|
|
1514
1808
|
const keep = (h) => {
|
|
1515
1809
|
const msgId = /^msg:(.+)$/.exec(h.id)?.[1];
|
|
1516
1810
|
const sum = parseSummaryKey(h.id);
|
|
@@ -1518,10 +1812,16 @@ export class HistoryModule {
|
|
|
1518
1812
|
const m = cm.getMessage(msgId);
|
|
1519
1813
|
if (!m)
|
|
1520
1814
|
return 'offBranch';
|
|
1815
|
+
if (view?.leaves(m))
|
|
1816
|
+
return 'leftOut';
|
|
1817
|
+
const mark = view?.mark(m);
|
|
1818
|
+
if (mark !== undefined)
|
|
1819
|
+
marks.set(h.id, mark);
|
|
1521
1820
|
const text = safeSlice(messageIndexText(m, includePrivate), 0, this.semanticCfg.maxChars ?? 32_000);
|
|
1522
1821
|
if (!text)
|
|
1523
1822
|
return 'stale';
|
|
1524
|
-
|
|
1823
|
+
const failed = this.failedSpeech(m);
|
|
1824
|
+
currentText.set(h.id, { text, shown: failed.size ? safeSlice(messageIndexText(m, includePrivate, failed), 0, this.semanticCfg.maxChars ?? 32_000) : text });
|
|
1525
1825
|
return 'ok';
|
|
1526
1826
|
}
|
|
1527
1827
|
if (sum !== null) {
|
|
@@ -1539,6 +1839,8 @@ export class HistoryModule {
|
|
|
1539
1839
|
return 'superseded';
|
|
1540
1840
|
if (s.created !== sum.createdMs)
|
|
1541
1841
|
return 'offBranch';
|
|
1842
|
+
if (view?.leavesSummaries)
|
|
1843
|
+
return 'leftOut';
|
|
1542
1844
|
const text = summaryIndexText(s, this.semanticCfg);
|
|
1543
1845
|
// An empty text here is mostly defensive: context-manager drops a
|
|
1544
1846
|
// summary that's empty after trimming when it loads, and refuses to
|
|
@@ -1547,7 +1849,7 @@ export class HistoryModule {
|
|
|
1547
1849
|
// text.
|
|
1548
1850
|
if (!text)
|
|
1549
1851
|
return 'stale';
|
|
1550
|
-
currentText.set(h.id, text);
|
|
1852
|
+
currentText.set(h.id, { text, shown: text });
|
|
1551
1853
|
return 'ok';
|
|
1552
1854
|
}
|
|
1553
1855
|
return 'ok';
|
|
@@ -1563,6 +1865,7 @@ export class HistoryModule {
|
|
|
1563
1865
|
}
|
|
1564
1866
|
let droppedOffBranch = 0;
|
|
1565
1867
|
let droppedStale = 0;
|
|
1868
|
+
let leftOut = 0;
|
|
1566
1869
|
// Why searching again for more hits failed, when it did. The search
|
|
1567
1870
|
// before it answered, so its hits on this branch are returned.
|
|
1568
1871
|
let searchAgainError = null;
|
|
@@ -1571,18 +1874,24 @@ export class HistoryModule {
|
|
|
1571
1874
|
const check = () => {
|
|
1572
1875
|
droppedOffBranch = 0;
|
|
1573
1876
|
droppedStale = 0;
|
|
1877
|
+
leftOut = 0;
|
|
1574
1878
|
onBranch = res.hits.filter((h) => {
|
|
1575
1879
|
const v = keep(h);
|
|
1576
1880
|
if (v === 'offBranch')
|
|
1577
1881
|
droppedOffBranch++;
|
|
1578
1882
|
else if (v === 'stale')
|
|
1579
1883
|
droppedStale++;
|
|
1884
|
+
else if (v === 'leftOut')
|
|
1885
|
+
leftOut++;
|
|
1580
1886
|
return v === 'ok';
|
|
1581
1887
|
});
|
|
1582
1888
|
};
|
|
1889
|
+
// A search of summaries alone, for a caller they're left out for, keeps
|
|
1890
|
+
// no hit however deep it goes, so it isn't searched again.
|
|
1891
|
+
const keepsNone = wantsSummariesOnly && view?.leavesSummaries === true;
|
|
1583
1892
|
for (;;) {
|
|
1584
1893
|
check();
|
|
1585
|
-
if (onBranch.length >= limit || res.hits.length < k || k >= SEMANTIC_MAX_FETCH)
|
|
1894
|
+
if (keepsNone || onBranch.length >= limit || res.hits.length < k || k >= SEMANTIC_MAX_FETCH)
|
|
1586
1895
|
break;
|
|
1587
1896
|
k = Math.min(SEMANTIC_MAX_FETCH, k * 4);
|
|
1588
1897
|
try {
|
|
@@ -1628,10 +1937,11 @@ export class HistoryModule {
|
|
|
1628
1937
|
score: h.score,
|
|
1629
1938
|
timestamp,
|
|
1630
1939
|
channelId: h.channel,
|
|
1940
|
+
...windowField(marks.get(h.id)),
|
|
1631
1941
|
participant: meta?.participant ?? null,
|
|
1632
1942
|
author: meta?.author ?? null,
|
|
1633
|
-
snippet: currentText.has(h.id) ? snippetOf(currentText.get(h.id)) : (h.text ?? ''),
|
|
1634
|
-
chars: currentText.has(h.id) ? currentText.get(h.id).length : h.chars,
|
|
1943
|
+
snippet: currentText.has(h.id) ? snippetOf(currentText.get(h.id).shown) : (h.text ?? ''),
|
|
1944
|
+
chars: currentText.has(h.id) ? currentText.get(h.id).text.length : h.chars,
|
|
1635
1945
|
};
|
|
1636
1946
|
});
|
|
1637
1947
|
if (unread.length > 0) {
|
|
@@ -1653,6 +1963,8 @@ export class HistoryModule {
|
|
|
1653
1963
|
droppedOffBranch,
|
|
1654
1964
|
/** Hits for messages whose current text has nothing indexable (edited blank; the index keeps such a message until its delete goes through, and on a service without /delete it never does). */
|
|
1655
1965
|
droppedStale,
|
|
1966
|
+
/** Hits left out for the caller (#291): another agent's own rows it isn't shown and doesn't observe, and, for another agent's call that doesn't observe the agent whose context the summaries fold, summary hits; only when there were any. */
|
|
1967
|
+
...(leftOut > 0 ? { leftOut } : {}),
|
|
1656
1968
|
/** When too few hits were on this branch and searching again for more failed: why, and whether calling again can help. */
|
|
1657
1969
|
searchAgainError,
|
|
1658
1970
|
},
|
|
@@ -1660,7 +1972,7 @@ export class HistoryModule {
|
|
|
1660
1972
|
},
|
|
1661
1973
|
};
|
|
1662
1974
|
}
|
|
1663
|
-
handleOverview(input) {
|
|
1975
|
+
handleOverview(input, view) {
|
|
1664
1976
|
const channelId = this.resolveChannel(input.channelId);
|
|
1665
1977
|
const fromMs = parseIsoDate(input.from, 'from');
|
|
1666
1978
|
const toMs = parseIsoDate(input.to, 'to');
|
|
@@ -1971,6 +2283,14 @@ export class HistoryModule {
|
|
|
1971
2283
|
// `filtered.length - limit` has no such cliff, at limit:0 it's
|
|
1972
2284
|
// `slice(filtered.length)` -> `[]`, the honest answer.
|
|
1973
2285
|
const capped = truncated ? filtered.slice(filtered.length - limit) : filtered;
|
|
2286
|
+
// The summaries fold the bound context's agent's window, its own rounds
|
|
2287
|
+
// and notes among it (#291). For another agent's call that doesn't
|
|
2288
|
+
// observe that agent (viewFor's leavesSummaries) a summarized span keeps
|
|
2289
|
+
// its time, level and counts, which are the record's, and leaves out its
|
|
2290
|
+
// text: `content: null` and `contentLeftOut: true`, counted at the top as
|
|
2291
|
+
// `summariesLeftOut`.
|
|
2292
|
+
const leavesContent = view?.leavesSummaries === true;
|
|
2293
|
+
const summariesLeftOut = leavesContent ? capped.filter((e) => e.summarized).length : 0;
|
|
1974
2294
|
return {
|
|
1975
2295
|
success: true,
|
|
1976
2296
|
data: {
|
|
@@ -1987,6 +2307,7 @@ export class HistoryModule {
|
|
|
1987
2307
|
...(input.level !== undefined ? { level: input.level } : { maxLevelAvailable: cm.getMaxSummaryLevel() }),
|
|
1988
2308
|
truncated,
|
|
1989
2309
|
totalSpans,
|
|
2310
|
+
...(summariesLeftOut > 0 ? { summariesLeftOut } : {}),
|
|
1990
2311
|
entries: capped.map((e) => {
|
|
1991
2312
|
// When channelId narrows the response, the numbers an agent will
|
|
1992
2313
|
// actually reason over (messageCount/tokensEstimate) are scoped
|
|
@@ -2004,7 +2325,11 @@ export class HistoryModule {
|
|
|
2004
2325
|
from: new Date(e.startMs).toISOString(),
|
|
2005
2326
|
to: new Date(e.endMs).toISOString(),
|
|
2006
2327
|
summarized: e.summarized,
|
|
2007
|
-
...(e.summarized
|
|
2328
|
+
...(e.summarized
|
|
2329
|
+
? leavesContent
|
|
2330
|
+
? { content: null, contentLeftOut: true, level: e.level }
|
|
2331
|
+
: { content: e.content, level: e.level }
|
|
2332
|
+
: {}),
|
|
2008
2333
|
// Set only when this entry's boundary-tie disambiguation (see
|
|
2009
2334
|
// the correction pass above) found and corrected a
|
|
2010
2335
|
// millisecond-timestamp collision at its edge. messageCount is
|
|
@@ -2223,31 +2548,71 @@ function mergeForeignIntoGap(gaps, boundaryMs, side, foreign) {
|
|
|
2223
2548
|
gaps.push({ startMs: boundaryMs, endMs: boundaryMs, summarized: false, stats: addend, boundaryUncertain: true });
|
|
2224
2549
|
}
|
|
2225
2550
|
}
|
|
2226
|
-
|
|
2551
|
+
/** The sentence extract's and search's descriptions gain with the window lookup (getTools). */
|
|
2552
|
+
const WINDOW_NOTE = ' A message that isn\'t in your context carries `window`: "undone" for one an undo or a refusal rewind took ' +
|
|
2553
|
+
'out of it, "not-shown" for one not in your context now, never shown to you or returned by an undo or rollback ' +
|
|
2554
|
+
'to be shown again; another agent\'s own turns and notes that aren\'t in your context are left out unless you ' +
|
|
2555
|
+
'observe that agent, and how many were is `leftOut`.';
|
|
2556
|
+
/** extract's addition to it: without an author filter its offset counts the rows left out, as its page's place in the window does; with one, only the rows the filter keeps. */
|
|
2557
|
+
const EXTRACT_OFFSET_NOTE = ' Without an author filter, `offset` counts the messages left out too: to page on, advance it by returned + leftOut. ' +
|
|
2558
|
+
'With one, it counts only the messages the filter keeps: page on by `resume`.';
|
|
2559
|
+
/** semantic_search's form of it. */
|
|
2560
|
+
const WINDOW_NOTE_SEMANTIC = ' A message hit that isn\'t in your context carries `window` ("undone" for one an undo or a refusal rewind took ' +
|
|
2561
|
+
'out of it, "not-shown" for one not in your context now, never shown to you or returned by an undo or rollback ' +
|
|
2562
|
+
'to be shown again); another agent\'s own turns and notes that aren\'t in your ' +
|
|
2563
|
+
'context are left out unless you observe that agent, and so are summary hits when the summaries fold another ' +
|
|
2564
|
+
'agent\'s context (the one this history is bound to) and you don\'t observe that agent, all counted in ' +
|
|
2565
|
+
'index.leftOut.';
|
|
2566
|
+
/** overview's form of it: its entries are spans, and only a summary's text is the bound agent's. */
|
|
2567
|
+
const WINDOW_NOTE_OVERVIEW = ' The summaries fold one agent\'s context, the one this history is bound to: when that isn\'t yours and you don\'t ' +
|
|
2568
|
+
'observe that agent, a summarized span comes back with its time, level and counts but not its text ' +
|
|
2569
|
+
'(content:null, contentLeftOut:true), and summariesLeftOut says how many did.';
|
|
2570
|
+
/** `{ window }` for a defined mark, nothing otherwise, so an entry in the caller's window is as it always was. */
|
|
2571
|
+
function windowField(mark) {
|
|
2572
|
+
return mark === undefined ? {} : { window: mark };
|
|
2573
|
+
}
|
|
2574
|
+
function projectMessage(msg, format, failedOf, mark) {
|
|
2227
2575
|
return {
|
|
2228
2576
|
id: String(msg.id),
|
|
2229
2577
|
timestamp: msg.timestamp.toISOString(),
|
|
2230
2578
|
participant: msg.participant,
|
|
2231
2579
|
author: authorName(msg),
|
|
2232
2580
|
channelId: getChannelId(msg) ?? null,
|
|
2233
|
-
|
|
2581
|
+
...windowField(mark),
|
|
2582
|
+
content: format === 'raw' ? msg.content : flattenContent(msg.content, failedOf(msg)),
|
|
2234
2583
|
};
|
|
2235
2584
|
}
|
|
2585
|
+
const NO_FAILURES = new Set();
|
|
2236
2586
|
/** Flatten a message's content blocks to one short, readable string. Text
|
|
2237
|
-
* blocks verbatim;
|
|
2238
|
-
*
|
|
2239
|
-
* matching, not
|
|
2240
|
-
|
|
2241
|
-
|
|
2587
|
+
* blocks verbatim; a speech tool's call (send-tools.ts) as its name and
|
|
2588
|
+
* words; everything else (tool_use/tool_result/thinking/media) as a short
|
|
2589
|
+
* bracketed label — this is for agent readability and search matching, not
|
|
2590
|
+
* byte-faithful reconstruction. */
|
|
2591
|
+
function flattenContent(content, failed = NO_FAILURES) {
|
|
2592
|
+
return content.map((block) => blockLabel(block, failed)).join(' ').trim();
|
|
2242
2593
|
}
|
|
2243
|
-
function
|
|
2594
|
+
function blockSpans(content) {
|
|
2595
|
+
const labels = content.map((block) => blockLabel(block));
|
|
2596
|
+
const joined = labels.join(' ');
|
|
2597
|
+
let at = joined.trimStart().length - joined.length;
|
|
2598
|
+
return content.map((block, k) => {
|
|
2599
|
+
const span = { block, at, label: labels[k] };
|
|
2600
|
+
at += span.label.length + 1;
|
|
2601
|
+
return span;
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2604
|
+
function blockLabel(block, failed = NO_FAILURES) {
|
|
2244
2605
|
switch (block.type) {
|
|
2245
2606
|
case 'text':
|
|
2246
2607
|
return block.text;
|
|
2247
|
-
case 'tool_use':
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2608
|
+
case 'tool_use': {
|
|
2609
|
+
const said = speechOf(block.name, block.input);
|
|
2610
|
+
return said ? `[${block.name}${failed.has(block.id) ? ERROR_MARK : ''}] ${said.words}` : `[tool_use: ${block.name}]`;
|
|
2611
|
+
}
|
|
2612
|
+
case 'tool_result': {
|
|
2613
|
+
const mark = block.isError ? ERROR_MARK : '';
|
|
2614
|
+
return block.toolName ? `[tool_result: ${block.toolName}${mark}]` : `[tool_result${mark}]`;
|
|
2615
|
+
}
|
|
2251
2616
|
case 'thinking':
|
|
2252
2617
|
case 'redacted_thinking':
|
|
2253
2618
|
return '[thinking]';
|