@dynamicagents/plugins 0.11.1 → 0.13.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.
Files changed (134) hide show
  1. package/README.md +59 -79
  2. package/dist/browser/index.d.ts +0 -2
  3. package/dist/browser/index.js +21 -27
  4. package/dist/claude-code/cards.d.ts +53 -0
  5. package/dist/claude-code/cards.js +272 -0
  6. package/dist/claude-code/config.d.ts +73 -31
  7. package/dist/claude-code/config.js +0 -2
  8. package/dist/claude-code/credentials.d.ts +21 -9
  9. package/dist/claude-code/credentials.js +43 -9
  10. package/dist/claude-code/egress.d.ts +15 -14
  11. package/dist/claude-code/egress.js +51 -14
  12. package/dist/claude-code/events.d.ts +153 -9
  13. package/dist/claude-code/events.js +289 -58
  14. package/dist/claude-code/github.d.ts +39 -0
  15. package/dist/claude-code/github.js +82 -0
  16. package/dist/claude-code/index.d.ts +24 -80
  17. package/dist/claude-code/index.js +19 -234
  18. package/dist/claude-code/model.d.ts +152 -0
  19. package/dist/claude-code/model.js +342 -0
  20. package/dist/claude-code/run.d.ts +283 -92
  21. package/dist/claude-code/run.js +620 -114
  22. package/dist/claude-code/session.d.ts +131 -0
  23. package/dist/claude-code/session.js +285 -0
  24. package/dist/claude-code/spec.d.ts +46 -0
  25. package/dist/claude-code/spec.js +83 -0
  26. package/dist/claude-code/verified.d.ts +15 -0
  27. package/dist/claude-code/verified.js +15 -0
  28. package/dist/computer/context.d.ts +19 -0
  29. package/dist/computer/context.js +222 -0
  30. package/dist/computer/file-lock.d.ts +5 -0
  31. package/dist/computer/file-lock.js +40 -0
  32. package/dist/computer/gate.d.ts +12 -12
  33. package/dist/computer/gate.js +12 -12
  34. package/dist/computer/index.d.ts +57 -255
  35. package/dist/computer/index.js +98 -1039
  36. package/dist/computer/paths.d.ts +47 -22
  37. package/dist/computer/paths.js +64 -49
  38. package/dist/computer/proxy.d.ts +45 -0
  39. package/dist/computer/proxy.js +234 -0
  40. package/dist/computer/render.d.ts +27 -38
  41. package/dist/computer/render.js +32 -65
  42. package/dist/computer/tools-exec.d.ts +3 -0
  43. package/dist/computer/tools-exec.js +268 -0
  44. package/dist/computer/tools-file.d.ts +12 -0
  45. package/dist/computer/tools-file.js +64 -0
  46. package/dist/computer/tools-grep.d.ts +3 -0
  47. package/dist/computer/tools-grep.js +129 -0
  48. package/dist/repo/checkout.d.ts +35 -6
  49. package/dist/repo/checkout.js +64 -6
  50. package/dist/repo/context.d.ts +135 -0
  51. package/dist/repo/context.js +582 -0
  52. package/dist/repo/index.d.ts +79 -57
  53. package/dist/repo/index.js +118 -1112
  54. package/dist/repo/tools-checks.d.ts +3 -0
  55. package/dist/repo/tools-checks.js +112 -0
  56. package/dist/repo/tools-clone.d.ts +4 -0
  57. package/dist/repo/tools-clone.js +192 -0
  58. package/dist/repo/tools-pr.d.ts +14 -0
  59. package/dist/repo/tools-pr.js +196 -0
  60. package/dist/repo/tools-review.d.ts +12 -0
  61. package/dist/repo/tools-review.js +393 -0
  62. package/dist/repo/tools-worktree.d.ts +4 -0
  63. package/dist/repo/tools-worktree.js +305 -0
  64. package/dist/repo/tools-worktrees.d.ts +12 -0
  65. package/dist/repo/tools-worktrees.js +48 -0
  66. package/dist/repo/url.d.ts +2 -2
  67. package/dist/repo/url.js +7 -7
  68. package/dist/scratch/index.d.ts +4 -4
  69. package/dist/scratch/index.js +12 -11
  70. package/dist/{computer → workspace}/advisory.d.ts +33 -20
  71. package/dist/{computer → workspace}/advisory.js +32 -7
  72. package/dist/workspace/ca-trust.d.ts +95 -0
  73. package/dist/workspace/ca-trust.js +312 -0
  74. package/dist/workspace/container-deps.d.ts +54 -0
  75. package/dist/workspace/container-deps.js +224 -0
  76. package/dist/workspace/container-fault.d.ts +38 -0
  77. package/dist/workspace/container-fault.js +73 -0
  78. package/dist/workspace/exec.d.ts +94 -0
  79. package/dist/workspace/exec.js +153 -0
  80. package/dist/workspace/format.d.ts +36 -0
  81. package/dist/workspace/format.js +57 -0
  82. package/dist/workspace/git-host.d.ts +79 -0
  83. package/dist/workspace/git-host.js +226 -0
  84. package/dist/workspace/git-identity.d.ts +57 -0
  85. package/dist/workspace/git-identity.js +147 -0
  86. package/dist/workspace/index.d.ts +43 -47
  87. package/dist/workspace/index.js +52 -112
  88. package/dist/workspace/install-job.d.ts +121 -0
  89. package/dist/workspace/install-job.js +866 -0
  90. package/dist/{computer → workspace}/install.d.ts +15 -19
  91. package/dist/{computer → workspace}/install.js +8 -11
  92. package/dist/workspace/object.d.ts +502 -0
  93. package/dist/workspace/object.js +1427 -0
  94. package/dist/workspace/open.d.ts +95 -0
  95. package/dist/workspace/open.js +63 -0
  96. package/dist/workspace/read.d.ts +16 -0
  97. package/dist/workspace/read.js +22 -0
  98. package/dist/workspace/shell.d.ts +45 -0
  99. package/dist/workspace/shell.js +80 -0
  100. package/dist/workspace/sync.d.ts +111 -0
  101. package/dist/workspace/sync.js +124 -0
  102. package/dist/workspace/wake.d.ts +19 -0
  103. package/dist/workspace/wake.js +10 -0
  104. package/package.json +28 -42
  105. package/dist/arc-agi/analysis.d.ts +0 -336
  106. package/dist/arc-agi/analysis.js +0 -829
  107. package/dist/arc-agi/client.d.ts +0 -82
  108. package/dist/arc-agi/client.js +0 -113
  109. package/dist/arc-agi/game-tools.d.ts +0 -25
  110. package/dist/arc-agi/game-tools.js +0 -26
  111. package/dist/arc-agi/index.d.ts +0 -42
  112. package/dist/arc-agi/index.js +0 -175
  113. package/dist/arc-agi/main-agent.d.ts +0 -27
  114. package/dist/arc-agi/main-agent.js +0 -65
  115. package/dist/arc-agi/recipe.d.ts +0 -51
  116. package/dist/arc-agi/recipe.js +0 -100
  117. package/dist/arc-agi/scorecard.d.ts +0 -118
  118. package/dist/arc-agi/scorecard.js +0 -163
  119. package/dist/arc-agi/soul.d.ts +0 -40
  120. package/dist/arc-agi/soul.js +0 -90
  121. package/dist/arc-agi/store.d.ts +0 -154
  122. package/dist/arc-agi/store.js +0 -216
  123. package/dist/arc-agi/tools.d.ts +0 -36
  124. package/dist/arc-agi/tools.js +0 -576
  125. package/dist/arc-agi/types.d.ts +0 -212
  126. package/dist/arc-agi/types.js +0 -19
  127. package/dist/claude-code/recipe.d.ts +0 -53
  128. package/dist/claude-code/recipe.js +0 -100
  129. package/dist/computer/read.d.ts +0 -137
  130. package/dist/computer/read.js +0 -203
  131. package/dist/recall/index.d.ts +0 -99
  132. package/dist/recall/index.js +0 -171
  133. package/dist/triage/index.d.ts +0 -355
  134. package/dist/triage/index.js +0 -302
@@ -1,203 +0,0 @@
1
- import { isGitInternal } from "./paths.js";
2
- /**
3
- * Reading the workspace without pulling more of it across the boundary than the
4
- * model will be shown.
5
- *
6
- * The obvious version of each function here reads everything and throws most of
7
- * it away, which is the wrong shape against a Durable Object's 128 MB. So the
8
- * budget is applied at the source, and the offsets handed back are the *source's*
9
- * rather than the rendered subset's — which is what makes the next page exact
10
- * instead of a guess.
11
- */
12
- /**
13
- * Read a file without pulling more of it across the boundary than the model will
14
- * be shown.
15
- *
16
- * `readFile(path, "utf8")` materialises the whole file in the isolate before
17
- * `truncateOutput` throws most of it away. Harmless on a source file, and the
18
- * wrong shape against a Durable Object's 128 MB — `sb_read` is one model decision
19
- * away from a lockfile, a bundle or a captured build log. The read is
20
- * range-addressable, so the budget is enforced at the source.
21
- *
22
- * Two reads rather than one, because the budget is spent from both ends for the
23
- * reason {@link truncateOutput} documents — the first error is at the top of a
24
- * file and the summary is at the bottom.
25
- *
26
- * ## The one place the budget is spent in bytes
27
- *
28
- * `maxChars` is a character ceiling everywhere else — see
29
- * {@link ComputerConfig.maxOutputChars} — and here it is spent against
30
- * `byteOffset`/`byteLength`, which are the only units the transport has. That is
31
- * deliberate and it is safe in the direction that matters: a UTF-8 byte is never
32
- * more than one UTF-16 code unit, so *N* bytes decode to at most *N* characters.
33
- * Reading the character budget as bytes therefore always honours the ceiling,
34
- * and errs low on a file that is mostly non-ASCII — a CJK source file is cut at
35
- * about a third of the characters it could have shown. The alternative is a
36
- * second round trip to measure what the first one returned, on every read, to
37
- * recover a bound the marker already announces.
38
- *
39
- * The other reason it cannot be characters is the one this function exists for:
40
- * bounding the *transport*. A byte range is what stops a 4 MB lockfile being
41
- * materialised in a 128 MB isolate before anything is thrown away.
42
- *
43
- * ## Why `stat` first
44
- *
45
- * Sizing off the returned string instead would be wrong in a way that hides
46
- * itself. `byteLength` counts bytes and `String.length` counts UTF-16 code units,
47
- * so a file of two-byte characters comes back *under* a character budget while
48
- * still being over the byte budget — and the read would look complete with half
49
- * the file missing and no marker saying so. One extra round trip buys the
50
- * distinction.
51
- *
52
- * A slice boundary can land mid-codepoint, which the decoder resolves to a single
53
- * replacement character. That is one glyph of noise at a cut that already
54
- * announces itself as a cut.
55
- */
56
- export async function readBounded(fs, path, maxChars) {
57
- const { size } = await fs.stat(path);
58
- if (size <= maxChars)
59
- return fs.readFile(path, "utf8");
60
- // Names the way out, rather than only the size of the hole. Before `offset`
61
- // existed the honest answer was "use sb_exec with sed", which needs a live
62
- // container — the dependency these tools exist to remove.
63
- const marker = (dropped, at) => `\n\n… [${dropped} bytes omitted from the middle — read them with ` +
64
- `\`offset: ${at}\`] …\n\n`;
65
- const half = Math.floor((maxChars - marker(size, size).length) / 2);
66
- // No budget for two ends plus the marker: keep the head, where the first error
67
- // is. The same fallback `truncateOutput` makes at the same ceiling.
68
- if (half < 1)
69
- return fs.readFile(path, {
70
- encoding: "utf8",
71
- byteLength: Math.max(0, maxChars)
72
- });
73
- const [head, tail] = await Promise.all([
74
- fs.readFile(path, { encoding: "utf8", byteOffset: 0, byteLength: half }),
75
- fs.readFile(path, {
76
- encoding: "utf8",
77
- byteOffset: size - half,
78
- byteLength: half
79
- })
80
- ]);
81
- return head + marker(size - half * 2, half) + tail;
82
- }
83
- /**
84
- * A byte window the model asked for, with its coordinates stated.
85
- *
86
- * No middle-out here, deliberately: {@link readBounded} guesses at what matters when
87
- * nobody said, but an explicit `offset` *is* the model saying. Trimming the middle
88
- * of a region it chose would defeat the request and, worse, make the next offset
89
- * unknowable.
90
- *
91
- * The window line is what makes paging work at all — the model needs to know where
92
- * it landed and how much is left to compute the next `offset`.
93
- */
94
- export async function readWindow(fs, path, offset, length, maxChars) {
95
- const { size } = await fs.stat(path);
96
- if (offset >= size)
97
- return `(offset ${offset} is past the end of ${path}, which is ${size} bytes)`;
98
- const byteLength = Math.min(length, maxChars, size - offset);
99
- const body = await fs.readFile(path, {
100
- encoding: "utf8",
101
- byteOffset: offset,
102
- byteLength
103
- });
104
- const end = offset + byteLength;
105
- const rest = end < size ? `; ${size - end} bytes after this` : "";
106
- return `${body}\n--- bytes ${offset}–${end} of ${size}${rest} ---`;
107
- }
108
- /**
109
- * Fetch a page, drop what is off-limits, and keep the source offsets that make the
110
- * *next* page exact.
111
- *
112
- * The subtle part is why `rawIndex` exists at all. `offset` counts items at the
113
- * source; the caller renders a filtered, budget-trimmed subset of them. So the
114
- * obvious `nextOffset = offset + shown` is wrong — it silently skips or repeats
115
- * whenever anything was dropped, which is precisely when `.git` is involved. Keeping
116
- * each survivor's source index means the next page starts exactly after the last one
117
- * the model actually saw.
118
- *
119
- * Retries because `.git` can outnumber a whole page on its own: at a repo root it is
120
- * walked *first* (`.` sorts before alphanumerics) and holds thousands of objects, so
121
- * one fetch can come back entirely excluded. `maxRounds` is the caller's, and the
122
- * asymmetry is deliberate — a `find` retry re-walks dirents, which is cheap SQLite
123
- * reads, while a `grep` retry re-reads and re-scans every file it already looked at.
124
- *
125
- * Deliberately no slicing to `want`: the caller needs the extra item to know a next
126
- * page exists, and its index to say where.
127
- */
128
- export async function collectVisible(fetch, pathOf, want, offset, maxRounds) {
129
- const items = [];
130
- const rawIndex = [];
131
- let cursor = offset;
132
- let exhausted = false;
133
- let rounds = 0;
134
- // One past `want`, so a next page is detected without a further round trip.
135
- while (items.length <= want && rounds < maxRounds) {
136
- rounds += 1;
137
- const batch = await fetch(cursor, want + 1);
138
- batch.forEach((item, i) => {
139
- if (isGitInternal(pathOf(item)))
140
- return;
141
- items.push(item);
142
- rawIndex.push(cursor + i);
143
- });
144
- cursor += batch.length;
145
- if (batch.length <= want) {
146
- exhausted = true;
147
- break;
148
- }
149
- }
150
- return {
151
- items,
152
- rawIndex,
153
- rawEnd: cursor,
154
- exhausted,
155
- crowded: !exhausted && items.length <= want
156
- };
157
- }
158
- /**
159
- * The "there is more, here is where" line, with an offset that is actually correct.
160
- *
161
- * `rawIndex[shown]` rather than `offset + shown`: the first item the model did *not*
162
- * see, at its source coordinate. Anything else drifts by however many entries the
163
- * `.git` filter removed, and drifts silently — the next page would repeat or skip
164
- * with nothing to indicate it had.
165
- *
166
- * Narrowing is still offered, and still first-class, because it is cheaper than
167
- * paging: `offset` does not skip the walk, it only discards its early results. It is
168
- * offered as the better option rather than the only one, which is the thing the
169
- * previous wording got wrong — "narrow it" is no answer at all when the model wants
170
- * the next page of a list it is working through.
171
- */
172
- export function listingNote(page, shown, noun, narrower) {
173
- const next = shown < page.rawIndex.length
174
- ? page.rawIndex[shown]
175
- : page.exhausted
176
- ? undefined
177
- : page.rawEnd;
178
- if (next === undefined)
179
- return "";
180
- return (`\n\n… showed ${shown} ${noun}; there are more. Continue with ` +
181
- `\`offset: ${next}\`, or narrow with ${narrower} — narrowing is cheaper, ` +
182
- `since an offset still walks everything it skips.`);
183
- }
184
- /**
185
- * Does this path exist?
186
- *
187
- * `getWorkspace()` types `fs` as the in-process `WorkspaceFilesystem`, but the
188
- * value on the far side of a Durable Object boundary is a
189
- * `WorkspaceFilesystemStub`, which carries a few methods the local class does
190
- * not — `exists` among them. So the fast path is asked for rather than cast to,
191
- * and `stat` is the fallback for a host that hands back a local workspace.
192
- *
193
- * The fallback treats *any* `stat` failure as absence, which is the honest
194
- * reading for a tool whose entire answer is a boolean: there is no error channel
195
- * to distinguish "missing" from "unreadable", and the caller's next move — look
196
- * somewhere else — is the same either way.
197
- */
198
- export async function pathExists(fs, path) {
199
- const probe = fs.exists;
200
- if (typeof probe === "function")
201
- return probe.call(fs, path);
202
- return fs.stat(path).then(() => true, () => false);
203
- }
@@ -1,99 +0,0 @@
1
- import type { SessionMessage } from "agents/experimental/memory/session";
2
- import type { AgentPlugin } from "@dynamicagents/core";
3
- /**
4
- * The subset of the Vectorize binding recall drives — structurally satisfied by
5
- * `Vectorize`/`VectorizeIndex`, so nothing here names the binding type and a
6
- * test can pass a plain object.
7
- */
8
- export interface RecallIndex {
9
- query(vector: number[], options?: VectorizeQueryOptions): Promise<VectorizeMatches>;
10
- upsert(vectors: VectorizeVector[]): Promise<unknown>;
11
- }
12
- /** Embed a batch of texts. Output dimension must match the Vectorize index. */
13
- export type Embed = (texts: string[]) => Promise<number[][]>;
14
- /** A single archived message returned by a recall search. */
15
- export interface RecallResult {
16
- score: number;
17
- role: string;
18
- /** The stored message snippet (truncated to `metadataTextMax`). */
19
- text: string;
20
- /** Display name of the human speaker, when the message carried a `<turn>` wrapper. */
21
- author?: string;
22
- /** Speaker's platform user id, when present. */
23
- authorId?: string;
24
- /** Channel the turn came from, when present. */
25
- channel?: string;
26
- /** ISO-8601 instant of the turn, when present. */
27
- at?: string;
28
- }
29
- /**
30
- * Build the Vectorize vectors for a batch of archived messages. Pure:
31
- * `embeddings` are supplied 1:1 with `messages` (both already filtered to
32
- * non-empty text). The vector `id` is the message's own id, so re-archiving on a
33
- * compaction retry is an idempotent upsert; `<turn>` provenance is lifted into
34
- * metadata when present.
35
- */
36
- export declare function toRecallVectors(messages: SessionMessage[], namespace: string, embeddings: number[][], metadataTextMax?: number): VectorizeVector[];
37
- /**
38
- * Archive a range of displaced messages into Vectorize. Embeds the non-empty
39
- * texts in one batch and upserts them under `namespace`. No-op on empty input.
40
- */
41
- export declare function archiveMessages(index: RecallIndex, namespace: string, messages: SessionMessage[], embed: Embed, metadataTextMax?: number): Promise<void>;
42
- /**
43
- * Semantically search this instance's archive. Embeds the query and returns the
44
- * top matches within `namespace`, mapping each vector's metadata to a compact
45
- * {@link RecallResult}. Constrained to `namespace` — never a caller-supplied one.
46
- */
47
- export declare function recallSearch(index: RecallIndex, namespace: string, query: string, embed: Embed, topK?: number): Promise<RecallResult[]>;
48
- /**
49
- * The tuning half of {@link RecallConfig}: plain values, no bindings.
50
- *
51
- * Split out so a host can declare its settings in one place and spread them,
52
- * rather than re-typing each field at the call site:
53
- *
54
- * ```ts
55
- * // config.ts — the values, typed
56
- * export const RECALL: RecallTuning = { embeddingModelId: "@cf/baai/bge-m3", topK: 5 };
57
- *
58
- * // plugins.ts — the bindings, and the values spread
59
- * recall({ ai: host.env.AI, index: host.env.VECTORIZE,
60
- * namespace: host.callerKey, aiGatewayId: host.aiGatewayId, ...RECALL })
61
- * ```
62
- *
63
- * Field-by-field re-typing is not just verbose — it silently drops any option
64
- * added later, so a new default lands everywhere except the deployments that
65
- * enumerated the old set.
66
- */
67
- export interface RecallTuning {
68
- /**
69
- * AI Gateway id. Pass the host's **resolved** value (`PluginHost.aiGatewayId`),
70
- * so embedding calls stay correlated with chat calls in one gateway. Reading it
71
- * off a config-overrides object instead yields `undefined` the moment that
72
- * override is dropped in favour of core's default.
73
- */
74
- aiGatewayId?: string;
75
- /** Workers-AI embedding model. Must match the index's dimension. */
76
- embeddingModelId?: string;
77
- /** Matches returned by a search. Defaults to 5. */
78
- topK?: number;
79
- /** Stored-snippet cap, guarding Vectorize's ~10 KiB/vector. Defaults to 2000. */
80
- metadataTextMax?: number;
81
- }
82
- export interface RecallConfig extends RecallTuning {
83
- /** The `AI` binding, for embeddings. Read lazily — see below. */
84
- ai: Ai;
85
- /** The `VECTORIZE` binding. Must be a 1024-dim, cosine index. */
86
- index: RecallIndex;
87
- /**
88
- * The archive namespace for this agent instance — **a thunk, deliberately.**
89
- *
90
- * It derives from the verified caller's identity, which is not known when
91
- * `plugins(host)` runs in `onStart`. The Durable Object is keyed 1:1 by that
92
- * caller, so the value is constant once known; a thunk is what lets the host
93
- * supply it late while both hooks here read the same one.
94
- */
95
- namespace: () => string;
96
- /** Test override; skips the provider entirely. */
97
- embed?: Embed;
98
- }
99
- export declare function recall(config: RecallConfig): AgentPlugin;
@@ -1,171 +0,0 @@
1
- import { embedMany, tool } from "ai";
2
- import { z } from "zod";
3
- import { createWorkersAI } from "workers-ai-provider";
4
- import { definePlugin } from "@dynamicagents/core";
5
- import { parseTurn, sessionText } from "@dynamicagents/core/agent";
6
- /**
7
- * `@dynamicagents/plugins/recall` — episodic memory over Cloudflare Vectorize.
8
- *
9
- * When the Session compacts, the raw messages it folds into a summary would
10
- * otherwise be lost. Core announces that moment (`onMessagesDisplaced`) because
11
- * core is what performs the lossy operation; this plugin is what does something
12
- * about it — embedding the displaced range and upserting it, so a `recall` tool
13
- * can later semantically search history that has scrolled out of the live
14
- * context window.
15
- *
16
- * Core carries **no embedding code at all**: no `Embed` type, no
17
- * `embeddingModelId`, no recall config. It used to, without a single caller, and
18
- * that had fixed the shape of embedding for every future plugin — one model, one
19
- * dimension, `string[] → number[][]` — while `ModelConfig` could only say "the
20
- * agent has an embedding model" when the true statement is "*this plugin* uses
21
- * this embedding model". So the whole embedding path lives here.
22
- *
23
- * The namespace is always bound in code from the caller's verified identity,
24
- * never model input, so one caller can never read another's archive.
25
- */
26
- /** Workers-AI embedding model. 1024-dim/cosine, matching the Vectorize index. */
27
- const DEFAULT_EMBEDDING_MODEL_ID = "@cf/baai/bge-m3";
28
- /** Matches to return from a search. */
29
- const DEFAULT_TOP_K = 5;
30
- /**
31
- * Stored-snippet cap. Vectorize allows roughly 10 KiB of metadata per vector,
32
- * and the snippet is nearly all of it — a message longer than this is truncated
33
- * rather than costing the whole upsert.
34
- */
35
- const DEFAULT_METADATA_TEXT_MAX = 2000;
36
- /**
37
- * Build the Vectorize vectors for a batch of archived messages. Pure:
38
- * `embeddings` are supplied 1:1 with `messages` (both already filtered to
39
- * non-empty text). The vector `id` is the message's own id, so re-archiving on a
40
- * compaction retry is an idempotent upsert; `<turn>` provenance is lifted into
41
- * metadata when present.
42
- */
43
- export function toRecallVectors(messages, namespace, embeddings, metadataTextMax = DEFAULT_METADATA_TEXT_MAX) {
44
- return messages.map((m, i) => {
45
- const text = sessionText(m);
46
- const metadata = {
47
- role: m.role,
48
- text: text.slice(0, metadataTextMax)
49
- };
50
- const turn = parseTurn(text);
51
- if (turn) {
52
- metadata.author = turn.from;
53
- metadata.authorId = turn.id;
54
- metadata.channel = turn.channel;
55
- metadata.at = turn.at;
56
- }
57
- return { id: m.id, namespace, values: embeddings[i], metadata };
58
- });
59
- }
60
- /**
61
- * Archive a range of displaced messages into Vectorize. Embeds the non-empty
62
- * texts in one batch and upserts them under `namespace`. No-op on empty input.
63
- */
64
- export async function archiveMessages(index, namespace, messages, embed, metadataTextMax = DEFAULT_METADATA_TEXT_MAX) {
65
- const withText = messages.filter((m) => sessionText(m).trim().length > 0);
66
- if (withText.length === 0)
67
- return;
68
- const embeddings = await embed(withText.map((m) => sessionText(m)));
69
- await index.upsert(toRecallVectors(withText, namespace, embeddings, metadataTextMax));
70
- }
71
- /**
72
- * Semantically search this instance's archive. Embeds the query and returns the
73
- * top matches within `namespace`, mapping each vector's metadata to a compact
74
- * {@link RecallResult}. Constrained to `namespace` — never a caller-supplied one.
75
- */
76
- export async function recallSearch(index, namespace, query, embed, topK = DEFAULT_TOP_K) {
77
- const [vector] = await embed([query]);
78
- if (!vector)
79
- return [];
80
- const { matches } = await index.query(vector, {
81
- namespace,
82
- topK,
83
- returnMetadata: "all"
84
- });
85
- return matches.map((match) => {
86
- const md = (match.metadata ?? {});
87
- const result = {
88
- score: match.score,
89
- role: md.role ?? "unknown",
90
- text: md.text ?? ""
91
- };
92
- if (md.author)
93
- result.author = md.author;
94
- if (md.authorId)
95
- result.authorId = md.authorId;
96
- if (md.channel)
97
- result.channel = md.channel;
98
- if (md.at)
99
- result.at = md.at;
100
- return result;
101
- });
102
- }
103
- export function recall(config) {
104
- const { ai, index, namespace, aiGatewayId, embeddingModelId = DEFAULT_EMBEDDING_MODEL_ID, topK = DEFAULT_TOP_K, metadataTextMax = DEFAULT_METADATA_TEXT_MAX } = config;
105
- /**
106
- * Built on first use, never at module scope. Cloudflare evaluates module scope
107
- * during `wrangler deploy` to validate the new version, and bindings are not
108
- * populated at that point — constructing eagerly makes `createWorkersAI` throw
109
- * "you must provide either a binding or credentials".
110
- */
111
- let provider;
112
- const embed = config.embed ??
113
- (async (texts) => {
114
- if (texts.length === 0)
115
- return [];
116
- provider ??= createWorkersAI({
117
- binding: ai,
118
- ...(aiGatewayId ? { gateway: { id: aiGatewayId } } : {})
119
- });
120
- const { embeddings } = await embedMany({
121
- model: provider.embedding(embeddingModelId),
122
- values: texts
123
- });
124
- return embeddings;
125
- });
126
- return definePlugin({
127
- key: "recall",
128
- /**
129
- * Offered only once this caller's history has actually been compacted.
130
- *
131
- * Before the first compaction the archive is empty by construction, and a
132
- * tool whose only possible answer is "nothing here yet" costs the model a
133
- * call to discover that — plus the tokens to describe it, every round.
134
- */
135
- mainAgentTools: async (ctx) => {
136
- if ((await ctx.session.getCompactions()).length === 0)
137
- return {};
138
- return {
139
- recall: tool({
140
- description: "Search your own older conversation history with this caller that has scrolled out of the live context window (it was summarized during compaction). Use this to recall specific past details — quotes, decisions, facts — that you no longer have verbatim. Returns the most semantically similar archived messages with their author/channel/timestamp when known.",
141
- inputSchema: z.object({
142
- query: z
143
- .string()
144
- .describe("What to look for in the archived history"),
145
- limit: z
146
- .number()
147
- .int()
148
- .min(1)
149
- .max(20)
150
- .optional()
151
- .describe(`Max number of matches to return (default ${topK})`)
152
- }),
153
- // The index, the namespace and the embedder all come from the closure.
154
- // The model can only ever supply a query string and a count.
155
- execute: async ({ query, limit }) => ({
156
- results: await recallSearch(index, namespace(), query, embed, limit ?? topK)
157
- })
158
- })
159
- };
160
- },
161
- capability: "You can search your own older history with this caller — everything that has scrolled out of your live context window and been summarized. Use `recall` when you need a specific past detail verbatim (a quote, a decision, a number) rather than the gist you already have in your summary. It searches by meaning, so describe what you are looking for rather than guessing exact words.",
162
- /**
163
- * Compaction is about to fold these into a summary; embed and store them
164
- * first. Failures are the runtime's to swallow — it fans out with
165
- * `allSettled` and never lets a listener abort compaction, because history
166
- * must still shorten when a side store is briefly unavailable.
167
- */
168
- onMessagesDisplaced: (messages) => archiveMessages(index, namespace(), messages, embed, metadataTextMax),
169
- requires: { bindings: ["VECTORIZE"] }
170
- });
171
- }