@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.
- package/README.md +59 -79
- package/dist/browser/index.d.ts +0 -2
- package/dist/browser/index.js +21 -27
- package/dist/claude-code/cards.d.ts +53 -0
- package/dist/claude-code/cards.js +272 -0
- package/dist/claude-code/config.d.ts +73 -31
- package/dist/claude-code/config.js +0 -2
- package/dist/claude-code/credentials.d.ts +21 -9
- package/dist/claude-code/credentials.js +43 -9
- package/dist/claude-code/egress.d.ts +15 -14
- package/dist/claude-code/egress.js +51 -14
- package/dist/claude-code/events.d.ts +153 -9
- package/dist/claude-code/events.js +289 -58
- package/dist/claude-code/github.d.ts +39 -0
- package/dist/claude-code/github.js +82 -0
- package/dist/claude-code/index.d.ts +24 -80
- package/dist/claude-code/index.js +19 -234
- package/dist/claude-code/model.d.ts +152 -0
- package/dist/claude-code/model.js +342 -0
- package/dist/claude-code/run.d.ts +283 -92
- package/dist/claude-code/run.js +620 -114
- package/dist/claude-code/session.d.ts +131 -0
- package/dist/claude-code/session.js +285 -0
- package/dist/claude-code/spec.d.ts +46 -0
- package/dist/claude-code/spec.js +83 -0
- package/dist/claude-code/verified.d.ts +15 -0
- package/dist/claude-code/verified.js +15 -0
- package/dist/computer/context.d.ts +19 -0
- package/dist/computer/context.js +222 -0
- package/dist/computer/file-lock.d.ts +5 -0
- package/dist/computer/file-lock.js +40 -0
- package/dist/computer/gate.d.ts +12 -12
- package/dist/computer/gate.js +12 -12
- package/dist/computer/index.d.ts +57 -255
- package/dist/computer/index.js +98 -1039
- package/dist/computer/paths.d.ts +47 -22
- package/dist/computer/paths.js +64 -49
- package/dist/computer/proxy.d.ts +45 -0
- package/dist/computer/proxy.js +234 -0
- package/dist/computer/render.d.ts +27 -38
- package/dist/computer/render.js +32 -65
- package/dist/computer/tools-exec.d.ts +3 -0
- package/dist/computer/tools-exec.js +268 -0
- package/dist/computer/tools-file.d.ts +12 -0
- package/dist/computer/tools-file.js +64 -0
- package/dist/computer/tools-grep.d.ts +3 -0
- package/dist/computer/tools-grep.js +129 -0
- package/dist/repo/checkout.d.ts +35 -6
- package/dist/repo/checkout.js +64 -6
- package/dist/repo/context.d.ts +135 -0
- package/dist/repo/context.js +582 -0
- package/dist/repo/index.d.ts +79 -57
- package/dist/repo/index.js +118 -1112
- package/dist/repo/tools-checks.d.ts +3 -0
- package/dist/repo/tools-checks.js +112 -0
- package/dist/repo/tools-clone.d.ts +4 -0
- package/dist/repo/tools-clone.js +192 -0
- package/dist/repo/tools-pr.d.ts +14 -0
- package/dist/repo/tools-pr.js +196 -0
- package/dist/repo/tools-review.d.ts +12 -0
- package/dist/repo/tools-review.js +393 -0
- package/dist/repo/tools-worktree.d.ts +4 -0
- package/dist/repo/tools-worktree.js +305 -0
- package/dist/repo/tools-worktrees.d.ts +12 -0
- package/dist/repo/tools-worktrees.js +48 -0
- package/dist/repo/url.d.ts +2 -2
- package/dist/repo/url.js +7 -7
- package/dist/scratch/index.d.ts +4 -4
- package/dist/scratch/index.js +12 -11
- package/dist/{computer → workspace}/advisory.d.ts +33 -20
- package/dist/{computer → workspace}/advisory.js +32 -7
- package/dist/workspace/ca-trust.d.ts +95 -0
- package/dist/workspace/ca-trust.js +312 -0
- package/dist/workspace/container-deps.d.ts +54 -0
- package/dist/workspace/container-deps.js +224 -0
- package/dist/workspace/container-fault.d.ts +38 -0
- package/dist/workspace/container-fault.js +73 -0
- package/dist/workspace/exec.d.ts +94 -0
- package/dist/workspace/exec.js +153 -0
- package/dist/workspace/format.d.ts +36 -0
- package/dist/workspace/format.js +57 -0
- package/dist/workspace/git-host.d.ts +79 -0
- package/dist/workspace/git-host.js +226 -0
- package/dist/workspace/git-identity.d.ts +57 -0
- package/dist/workspace/git-identity.js +147 -0
- package/dist/workspace/index.d.ts +43 -47
- package/dist/workspace/index.js +52 -112
- package/dist/workspace/install-job.d.ts +121 -0
- package/dist/workspace/install-job.js +866 -0
- package/dist/{computer → workspace}/install.d.ts +15 -19
- package/dist/{computer → workspace}/install.js +8 -11
- package/dist/workspace/object.d.ts +502 -0
- package/dist/workspace/object.js +1427 -0
- package/dist/workspace/open.d.ts +95 -0
- package/dist/workspace/open.js +63 -0
- package/dist/workspace/read.d.ts +16 -0
- package/dist/workspace/read.js +22 -0
- package/dist/workspace/shell.d.ts +45 -0
- package/dist/workspace/shell.js +80 -0
- package/dist/workspace/sync.d.ts +111 -0
- package/dist/workspace/sync.js +124 -0
- package/dist/workspace/wake.d.ts +19 -0
- package/dist/workspace/wake.js +10 -0
- package/package.json +28 -42
- package/dist/arc-agi/analysis.d.ts +0 -336
- package/dist/arc-agi/analysis.js +0 -829
- package/dist/arc-agi/client.d.ts +0 -82
- package/dist/arc-agi/client.js +0 -113
- package/dist/arc-agi/game-tools.d.ts +0 -25
- package/dist/arc-agi/game-tools.js +0 -26
- package/dist/arc-agi/index.d.ts +0 -42
- package/dist/arc-agi/index.js +0 -175
- package/dist/arc-agi/main-agent.d.ts +0 -27
- package/dist/arc-agi/main-agent.js +0 -65
- package/dist/arc-agi/recipe.d.ts +0 -51
- package/dist/arc-agi/recipe.js +0 -100
- package/dist/arc-agi/scorecard.d.ts +0 -118
- package/dist/arc-agi/scorecard.js +0 -163
- package/dist/arc-agi/soul.d.ts +0 -40
- package/dist/arc-agi/soul.js +0 -90
- package/dist/arc-agi/store.d.ts +0 -154
- package/dist/arc-agi/store.js +0 -216
- package/dist/arc-agi/tools.d.ts +0 -36
- package/dist/arc-agi/tools.js +0 -576
- package/dist/arc-agi/types.d.ts +0 -212
- package/dist/arc-agi/types.js +0 -19
- package/dist/claude-code/recipe.d.ts +0 -53
- package/dist/claude-code/recipe.js +0 -100
- package/dist/computer/read.d.ts +0 -137
- package/dist/computer/read.js +0 -203
- package/dist/recall/index.d.ts +0 -99
- package/dist/recall/index.js +0 -171
- package/dist/triage/index.d.ts +0 -355
- package/dist/triage/index.js +0 -302
package/dist/computer/read.js
DELETED
|
@@ -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
|
-
}
|
package/dist/recall/index.d.ts
DELETED
|
@@ -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;
|
package/dist/recall/index.js
DELETED
|
@@ -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
|
-
}
|