@dynamicagents/plugins 0.12.0 → 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 +58 -77
- 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 +28 -63
- package/dist/claude-code/config.js +0 -11
- package/dist/claude-code/egress.d.ts +8 -1
- package/dist/claude-code/egress.js +42 -4
- package/dist/claude-code/events.d.ts +106 -9
- package/dist/claude-code/events.js +201 -56
- package/dist/claude-code/github.d.ts +39 -0
- package/dist/claude-code/github.js +82 -0
- package/dist/claude-code/index.d.ts +16 -90
- package/dist/claude-code/index.js +14 -276
- package/dist/claude-code/model.d.ts +152 -0
- package/dist/claude-code/model.js +342 -0
- package/dist/claude-code/run.d.ts +209 -112
- package/dist/claude-code/run.js +339 -138
- 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 +1 -5
- package/dist/computer/context.js +7 -10
- package/dist/computer/file-lock.js +5 -4
- package/dist/computer/gate.d.ts +4 -4
- package/dist/computer/gate.js +5 -5
- package/dist/computer/index.d.ts +40 -258
- package/dist/computer/index.js +71 -324
- package/dist/computer/paths.d.ts +36 -16
- package/dist/computer/paths.js +42 -29
- package/dist/computer/proxy.d.ts +45 -0
- package/dist/computer/proxy.js +234 -0
- package/dist/computer/render.d.ts +1 -40
- package/dist/computer/render.js +2 -68
- package/dist/computer/tools-exec.js +12 -13
- package/dist/computer/tools-file.d.ts +9 -1
- package/dist/computer/tools-file.js +22 -82
- package/dist/computer/{tools-find.d.ts → tools-grep.d.ts} +1 -1
- package/dist/computer/tools-grep.js +129 -0
- package/dist/repo/checkout.d.ts +4 -5
- package/dist/repo/checkout.js +31 -8
- package/dist/repo/context.d.ts +35 -9
- package/dist/repo/context.js +101 -49
- package/dist/repo/index.d.ts +73 -28
- package/dist/repo/index.js +112 -96
- package/dist/repo/tools-checks.d.ts +3 -0
- package/dist/repo/tools-checks.js +112 -0
- package/dist/repo/tools-clone.js +1 -1
- package/dist/repo/tools-pr.d.ts +14 -0
- package/dist/repo/{tools-forge.js → tools-pr.js} +53 -36
- package/dist/repo/tools-review.d.ts +9 -0
- package/dist/repo/tools-review.js +67 -47
- package/dist/repo/tools-worktree.js +144 -60
- 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 +4 -4
- package/dist/{computer → workspace}/advisory.js +2 -2
- package/dist/{computer/host → workspace}/ca-trust.d.ts +8 -1
- package/dist/{computer/host → workspace}/ca-trust.js +12 -3
- package/dist/{computer/host → workspace}/container-fault.d.ts +3 -3
- package/dist/{computer/host → workspace}/container-fault.js +23 -8
- 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/{computer/host → workspace}/git-host.d.ts +21 -4
- package/dist/{computer/host → workspace}/git-host.js +72 -2
- package/dist/{computer/host → workspace}/git-identity.d.ts +1 -1
- package/dist/{computer/host → workspace}/git-identity.js +1 -1
- package/dist/workspace/index.d.ts +43 -47
- package/dist/workspace/index.js +52 -112
- package/dist/{computer/host → workspace}/install-job.d.ts +4 -4
- package/dist/{computer/host → workspace}/install-job.js +276 -176
- package/dist/{computer → workspace}/install.d.ts +8 -8
- package/dist/{computer → workspace}/install.js +3 -3
- package/dist/{computer/host/workspace.d.ts → workspace/object.d.ts} +105 -27
- package/dist/{computer/host/workspace.js → workspace/object.js} +217 -48
- 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/{computer → workspace}/shell.d.ts +5 -5
- package/dist/{computer → workspace}/shell.js +6 -6
- package/dist/{computer/host → workspace}/wake.d.ts +1 -1
- package/package.json +16 -36
- package/dist/arc-agi/analysis.d.ts +0 -322
- package/dist/arc-agi/analysis.js +0 -802
- 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 -24
- package/dist/arc-agi/game-tools.js +0 -26
- package/dist/arc-agi/index.d.ts +0 -40
- package/dist/arc-agi/index.js +0 -175
- package/dist/arc-agi/main-agent.d.ts +0 -24
- package/dist/arc-agi/main-agent.js +0 -62
- package/dist/arc-agi/recipe.d.ts +0 -48
- package/dist/arc-agi/recipe.js +0 -97
- 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 -29
- package/dist/arc-agi/soul.js +0 -79
- package/dist/arc-agi/store.d.ts +0 -153
- package/dist/arc-agi/store.js +0 -215
- package/dist/arc-agi/tools.d.ts +0 -36
- package/dist/arc-agi/tools.js +0 -574
- package/dist/arc-agi/types.d.ts +0 -211
- 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 -133
- package/dist/computer/read.js +0 -200
- package/dist/computer/tools-find.js +0 -216
- package/dist/recall/index.d.ts +0 -105
- package/dist/recall/index.js +0 -181
- package/dist/repo/tools-forge.d.ts +0 -4
- package/dist/triage/index.d.ts +0 -360
- package/dist/triage/index.js +0 -311
- /package/dist/{computer/host → workspace}/container-deps.d.ts +0 -0
- /package/dist/{computer/host → workspace}/container-deps.js +0 -0
- /package/dist/{computer/host → workspace}/sync.d.ts +0 -0
- /package/dist/{computer/host → workspace}/sync.js +0 -0
- /package/dist/{computer/host → workspace}/wake.js +0 -0
package/dist/computer/read.js
DELETED
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reading the workspace without pulling more of it across the boundary than the
|
|
3
|
-
* model will be shown.
|
|
4
|
-
*
|
|
5
|
-
* The obvious version of each function here reads everything and throws most of
|
|
6
|
-
* it away, which is the wrong shape against a Durable Object's 128 MB. So the
|
|
7
|
-
* budget is applied at the source, and the offsets handed back are the *source's*
|
|
8
|
-
* rather than the rendered subset's — which is what makes the next page exact
|
|
9
|
-
* instead of a guess.
|
|
10
|
-
*/
|
|
11
|
-
/**
|
|
12
|
-
* Read a file without pulling more of it across the boundary than the model will
|
|
13
|
-
* be shown.
|
|
14
|
-
*
|
|
15
|
-
* `readFile(path, "utf8")` materialises the whole file in the isolate before
|
|
16
|
-
* `truncateOutput` throws most of it away. Harmless on a source file, and the
|
|
17
|
-
* wrong shape against a Durable Object's 128 MB — `sb_read` is one model decision
|
|
18
|
-
* away from a lockfile, a bundle or a captured build log. The read is
|
|
19
|
-
* range-addressable, so the budget is enforced at the source.
|
|
20
|
-
*
|
|
21
|
-
* Two reads rather than one, because the budget is spent from both ends for the
|
|
22
|
-
* reason {@link truncateOutput} documents — the first error is at the top of a
|
|
23
|
-
* file and the summary is at the bottom.
|
|
24
|
-
*
|
|
25
|
-
* ## The one place the budget is spent in bytes
|
|
26
|
-
*
|
|
27
|
-
* `maxChars` is a character ceiling everywhere else — see
|
|
28
|
-
* {@link ComputerConfig.maxOutputChars} — and here it is spent against
|
|
29
|
-
* `byteOffset`/`byteLength`, which are the only units the transport has, and the
|
|
30
|
-
* only ones that bound what crosses it. Safe in the direction that matters: a
|
|
31
|
-
* UTF-8 byte is never more than one UTF-16 code unit, so *N* bytes decode to at
|
|
32
|
-
* most *N* characters. Reading the character budget as bytes therefore always
|
|
33
|
-
* honours the ceiling, and errs low on a file that is mostly non-ASCII — a CJK
|
|
34
|
-
* source file is cut at about a third of the characters it could have shown. The
|
|
35
|
-
* alternative is a second round trip on every read, to recover a bound the
|
|
36
|
-
* marker already announces.
|
|
37
|
-
*
|
|
38
|
-
* ## Why `stat` first
|
|
39
|
-
*
|
|
40
|
-
* Sizing off the returned string instead would be wrong in a way that hides
|
|
41
|
-
* itself. `byteLength` counts bytes and `String.length` counts UTF-16 code units,
|
|
42
|
-
* so a file of two-byte characters comes back *under* a character budget while
|
|
43
|
-
* still being over the byte budget — and the read would look complete with half
|
|
44
|
-
* the file missing and no marker saying so. One extra round trip buys the
|
|
45
|
-
* distinction.
|
|
46
|
-
*
|
|
47
|
-
* A slice boundary can land mid-codepoint, which the decoder resolves to a single
|
|
48
|
-
* replacement character. That is one glyph of noise at a cut that already
|
|
49
|
-
* announces itself as a cut.
|
|
50
|
-
*/
|
|
51
|
-
export async function readBounded(fs, path, maxChars) {
|
|
52
|
-
const { size } = await fs.stat(path);
|
|
53
|
-
if (size <= maxChars)
|
|
54
|
-
return fs.readFile(path, "utf8");
|
|
55
|
-
// Names the way out, rather than only the size of the hole. Before `offset`
|
|
56
|
-
// existed the honest answer was "use sb_exec with sed", which needs a live
|
|
57
|
-
// container — the dependency these tools exist to remove.
|
|
58
|
-
const marker = (dropped, at) => `\n\n… [${dropped} bytes omitted from the middle — read them with ` +
|
|
59
|
-
`\`offset: ${at}\`] …\n\n`;
|
|
60
|
-
const half = Math.floor((maxChars - marker(size, size).length) / 2);
|
|
61
|
-
// No budget for two ends plus the marker: keep the head, where the first error
|
|
62
|
-
// is. The same fallback `truncateOutput` makes at the same ceiling.
|
|
63
|
-
if (half < 1)
|
|
64
|
-
return fs.readFile(path, {
|
|
65
|
-
encoding: "utf8",
|
|
66
|
-
byteLength: Math.max(0, maxChars)
|
|
67
|
-
});
|
|
68
|
-
const [head, tail] = await Promise.all([
|
|
69
|
-
fs.readFile(path, { encoding: "utf8", byteOffset: 0, byteLength: half }),
|
|
70
|
-
fs.readFile(path, {
|
|
71
|
-
encoding: "utf8",
|
|
72
|
-
byteOffset: size - half,
|
|
73
|
-
byteLength: half
|
|
74
|
-
})
|
|
75
|
-
]);
|
|
76
|
-
return head + marker(size - half * 2, half) + tail;
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* A byte window the model asked for, with its coordinates stated.
|
|
80
|
-
*
|
|
81
|
-
* No middle-out here, deliberately: {@link readBounded} guesses at what matters when
|
|
82
|
-
* nobody said, but an explicit `offset` *is* the model saying. Trimming the middle
|
|
83
|
-
* of a region it chose would defeat the request and, worse, make the next offset
|
|
84
|
-
* unknowable.
|
|
85
|
-
*
|
|
86
|
-
* The window line is what makes paging work at all — the model needs to know where
|
|
87
|
-
* it landed and how much is left to compute the next `offset`.
|
|
88
|
-
*/
|
|
89
|
-
export async function readWindow(fs, path, offset, length, maxChars) {
|
|
90
|
-
const { size } = await fs.stat(path);
|
|
91
|
-
if (offset >= size)
|
|
92
|
-
return `(offset ${offset} is past the end of ${path}, which is ${size} bytes)`;
|
|
93
|
-
const byteLength = Math.min(length, maxChars, size - offset);
|
|
94
|
-
const body = await fs.readFile(path, {
|
|
95
|
-
encoding: "utf8",
|
|
96
|
-
byteOffset: offset,
|
|
97
|
-
byteLength
|
|
98
|
-
});
|
|
99
|
-
const end = offset + byteLength;
|
|
100
|
-
const rest = end < size ? `; ${size - end} bytes after this` : "";
|
|
101
|
-
return `${body}\n--- bytes ${offset}–${end} of ${size}${rest} ---`;
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Fetch a page, drop what is off-limits, and keep the source offsets that make the
|
|
105
|
-
* *next* page exact.
|
|
106
|
-
*
|
|
107
|
-
* The subtle part is why `rawIndex` exists at all. `offset` counts items at the
|
|
108
|
-
* source; the caller renders a filtered, budget-trimmed subset of them. So the
|
|
109
|
-
* obvious `nextOffset = offset + shown` is wrong — it silently skips or repeats
|
|
110
|
-
* whenever anything was dropped, which is precisely when a skipped directory is
|
|
111
|
-
* involved. Keeping each survivor's source index means the next page starts exactly
|
|
112
|
-
* after the last one the model actually saw.
|
|
113
|
-
*
|
|
114
|
-
* Retries because a skipped directory can outnumber a whole page on its own — a
|
|
115
|
-
* dependency tree runs to tens of thousands of files — so one fetch can come back
|
|
116
|
-
* entirely excluded. `maxRounds` is the caller's: a retry re-reads and re-scans
|
|
117
|
-
* every file the last one looked at. For a walk the store can prune instead, see
|
|
118
|
-
* `sb_ls`, which passes exclusions to `find` and needs none of this.
|
|
119
|
-
*
|
|
120
|
-
* Deliberately no slicing to `want`: the caller needs the extra item to know a next
|
|
121
|
-
* page exists, and its index to say where.
|
|
122
|
-
*/
|
|
123
|
-
export async function collectVisible(fetch, pathOf, skip, want, offset, maxRounds) {
|
|
124
|
-
const items = [];
|
|
125
|
-
const rawIndex = [];
|
|
126
|
-
let cursor = offset;
|
|
127
|
-
let exhausted = false;
|
|
128
|
-
let rounds = 0;
|
|
129
|
-
// One past `want`, so a next page is detected without a further round trip.
|
|
130
|
-
while (items.length <= want && rounds < maxRounds) {
|
|
131
|
-
rounds += 1;
|
|
132
|
-
const batch = await fetch(cursor, want + 1);
|
|
133
|
-
batch.forEach((item, i) => {
|
|
134
|
-
if (skip(pathOf(item)))
|
|
135
|
-
return;
|
|
136
|
-
items.push(item);
|
|
137
|
-
rawIndex.push(cursor + i);
|
|
138
|
-
});
|
|
139
|
-
cursor += batch.length;
|
|
140
|
-
if (batch.length <= want) {
|
|
141
|
-
exhausted = true;
|
|
142
|
-
break;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
return {
|
|
146
|
-
items,
|
|
147
|
-
rawIndex,
|
|
148
|
-
rawEnd: cursor,
|
|
149
|
-
exhausted,
|
|
150
|
-
crowded: !exhausted && items.length <= want
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
/**
|
|
154
|
-
* The "there is more, here is where" line, with an offset that is actually correct.
|
|
155
|
-
*
|
|
156
|
-
* `rawIndex[shown]` rather than `offset + shown`: the first item the model did *not*
|
|
157
|
-
* see, at its source coordinate. Anything else drifts by however many entries the
|
|
158
|
-
* skip filter removed, and drifts silently — the next page would repeat or skip
|
|
159
|
-
* with nothing to indicate it had.
|
|
160
|
-
*
|
|
161
|
-
* Narrowing is still offered, and still first-class, because it is cheaper than
|
|
162
|
-
* paging: `offset` does not skip the walk, it only discards its early results. It is
|
|
163
|
-
* offered as the better option rather than the only one, which is the thing the
|
|
164
|
-
* previous wording got wrong — "narrow it" is no answer at all when the model wants
|
|
165
|
-
* the next page of a list it is working through.
|
|
166
|
-
*/
|
|
167
|
-
export function listingNote(page, shown, noun, narrower) {
|
|
168
|
-
const next = shown < page.rawIndex.length
|
|
169
|
-
? page.rawIndex[shown]
|
|
170
|
-
: page.exhausted
|
|
171
|
-
? undefined
|
|
172
|
-
: page.rawEnd;
|
|
173
|
-
if (next === undefined)
|
|
174
|
-
return "";
|
|
175
|
-
return (`\n\n… showed ${shown} ${noun}; there are more. Continue with ` +
|
|
176
|
-
`\`offset: ${next}\`, or narrow with ${narrower} — narrowing is cheaper, ` +
|
|
177
|
-
`since an offset still walks everything it skips.`);
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* Does this path exist?
|
|
181
|
-
*
|
|
182
|
-
* `getWorkspace()` types `fs` as the in-process `WorkspaceFilesystem`, but the
|
|
183
|
-
* value on the far side of a Durable Object boundary is a
|
|
184
|
-
* `WorkspaceFilesystemStub`, which carries a few methods the local class does
|
|
185
|
-
* not — `exists` among them. So the fast path is asked for rather than cast to,
|
|
186
|
-
* and `stat` is the fallback for a host that hands back a local workspace.
|
|
187
|
-
*
|
|
188
|
-
* The fallback treats *any* `stat` failure as absence, which is the honest
|
|
189
|
-
* reading for a tool whose entire answer is a boolean: there is no error channel
|
|
190
|
-
* to distinguish "missing" from "unreadable", and the caller's next move — look
|
|
191
|
-
* somewhere else — is the same either way.
|
|
192
|
-
*/
|
|
193
|
-
export async function pathExists(fs, path) {
|
|
194
|
-
// Called as a method, never through `.call`: on an RPC stub `.call` is read as
|
|
195
|
-
// a remote method, and the stub it would pass as `this` cannot be serialised.
|
|
196
|
-
const remote = fs;
|
|
197
|
-
if (typeof remote.exists === "function")
|
|
198
|
-
return remote.exists(path);
|
|
199
|
-
return fs.stat(path).then(() => true, () => false);
|
|
200
|
-
}
|
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
import { tool } from "ai";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import { guardPath, isSkipped, skipNames, WALK_SKIPS } from "./paths.js";
|
|
4
|
-
import { humanBytes, packBlocks, renderGrepMatches } from "./render.js";
|
|
5
|
-
import { collectVisible, listingNote, pathExists } from "./read.js";
|
|
6
|
-
/** Finding things: what is in a directory, what a file contains, what exists. */
|
|
7
|
-
/**
|
|
8
|
-
* How many directory entries one `sb_ls` returns.
|
|
9
|
-
*
|
|
10
|
-
* A bound on the *listing*, not on the rendered text — `maxOutputChars` still
|
|
11
|
-
* applies on top, and the order matters. Bounding only the text means `readdir`
|
|
12
|
-
* returns every entry and the isolate holds all of them before the ceiling
|
|
13
|
-
* discards the tail; a dependency tree is 22,470 files, and a misbehaving build
|
|
14
|
-
* is exactly when someone lists one. `readdir` takes a `limit`, so the bound is
|
|
15
|
-
* applied where the entries are read.
|
|
16
|
-
*/
|
|
17
|
-
const DEFAULT_MAX_ENTRIES = 1000;
|
|
18
|
-
/**
|
|
19
|
-
* How many matches one `sb_grep` returns.
|
|
20
|
-
*
|
|
21
|
-
* Bounded at the source like {@link DEFAULT_MAX_ENTRIES}, and for a sharper reason
|
|
22
|
-
* than a listing: without a `limit`, `fs.grep` reads *every* file under the path
|
|
23
|
-
* looking for more. `.git` and the dependency tree are both in the workspace, so
|
|
24
|
-
* an unbounded search of `/workspace` streams every loose object and every
|
|
25
|
-
* vendored file through the isolate before answering. The limit is what stops
|
|
26
|
-
* that walk early, and the skip list is what keeps the matches worth reading.
|
|
27
|
-
*/
|
|
28
|
-
const DEFAULT_MAX_MATCHES = 200;
|
|
29
|
-
export function findTools(ctx) {
|
|
30
|
-
const { cwd, maxChars, inWorkspace } = ctx;
|
|
31
|
-
return {
|
|
32
|
-
sb_ls: tool({
|
|
33
|
-
description: "List files in a workspace directory, or find files by name. Without `pattern` it lists one level: directories with a trailing slash, files with their size — check that before reading a large one, since sb_read truncates. " +
|
|
34
|
-
"`pattern` is a glob matched against paths relative to `path`, and searches the whole subtree: `*` stays within one path segment, `**/` crosses directories, `?` matches one character. So `*.ts` finds top-level TypeScript files and `**/*.ts` finds them at any depth. " +
|
|
35
|
-
"A cut listing reports the `offset` that continues it. Subtree listings leave out `.git` and `node_modules`.",
|
|
36
|
-
inputSchema: z.object({
|
|
37
|
-
path: z.string().describe("Absolute directory path"),
|
|
38
|
-
recursive: z
|
|
39
|
-
.boolean()
|
|
40
|
-
.optional()
|
|
41
|
-
.describe("List the whole subtree rather than one level"),
|
|
42
|
-
pattern: z
|
|
43
|
-
.string()
|
|
44
|
-
.optional()
|
|
45
|
-
.describe("Glob relative to path, e.g. '**/*.spec.ts'. Searches the subtree, so `recursive` is not needed with it."),
|
|
46
|
-
offset: z
|
|
47
|
-
.number()
|
|
48
|
-
.int()
|
|
49
|
-
.min(0)
|
|
50
|
-
.optional()
|
|
51
|
-
.describe("Entries to skip — use the offset a cut listing reports")
|
|
52
|
-
}),
|
|
53
|
-
execute: async ({ path, recursive, pattern, offset }) => {
|
|
54
|
-
const refusal = guardPath(path, "sb_ls");
|
|
55
|
-
if (refusal)
|
|
56
|
-
return refusal;
|
|
57
|
-
const from = offset ?? 0;
|
|
58
|
-
return inWorkspace("listing", path, async (fs) => {
|
|
59
|
-
if (recursive || pattern) {
|
|
60
|
-
// `find` rather than `ls`, which took no bound: a prefix scan
|
|
61
|
-
// returned every path in the subtree and the ceiling then threw most
|
|
62
|
-
// of them away — the same read-everything-then-discard shape the
|
|
63
|
-
// `readdir` limit below exists to avoid. Two differences follow from
|
|
64
|
-
// the swap, both improvements: directories appear (rendered like the
|
|
65
|
-
// arm below renders them), and order is the walk's — pre-order, by
|
|
66
|
-
// name — rather than one flat sort.
|
|
67
|
-
//
|
|
68
|
-
// `.git` and `node_modules` are pruned by the store, not filtered
|
|
69
|
-
// here: at a repo root either one outnumbers a page on its own.
|
|
70
|
-
const entries = await fs.find(path, pattern, {
|
|
71
|
-
limit: DEFAULT_MAX_ENTRIES + 1,
|
|
72
|
-
offset: from,
|
|
73
|
-
exclude: WALK_SKIPS.map((segment) => `**/${segment}`)
|
|
74
|
-
});
|
|
75
|
-
if (entries.length === 0)
|
|
76
|
-
return pattern
|
|
77
|
-
? `(nothing under ${path} matches ${pattern})`
|
|
78
|
-
: `(${path} is empty)`;
|
|
79
|
-
const blocks = entries
|
|
80
|
-
.slice(0, DEFAULT_MAX_ENTRIES)
|
|
81
|
-
.map((e) => [e.type === "dir" ? `${e.path}/` : e.path]);
|
|
82
|
-
const { body, shown } = packBlocks(blocks, maxChars);
|
|
83
|
-
const page = {
|
|
84
|
-
rawIndex: entries.map((_, i) => from + i),
|
|
85
|
-
rawEnd: from + entries.length,
|
|
86
|
-
exhausted: entries.length <= DEFAULT_MAX_ENTRIES,
|
|
87
|
-
crowded: false
|
|
88
|
-
};
|
|
89
|
-
return body + listingNote(page, shown, "entries", "`pattern`");
|
|
90
|
-
}
|
|
91
|
-
// One over the ceiling: enough to know the listing was cut without a
|
|
92
|
-
// second round trip to find out, and the extra entry is not shown.
|
|
93
|
-
// Unfiltered on purpose — one line naming a directory that is really
|
|
94
|
-
// there is honest, and costs a line rather than a page. It is the
|
|
95
|
-
// *subtree* walk above that cannot afford to descend into them.
|
|
96
|
-
const entries = await fs.readdir(path, {
|
|
97
|
-
limit: DEFAULT_MAX_ENTRIES + 1,
|
|
98
|
-
offset: from
|
|
99
|
-
});
|
|
100
|
-
if (entries.length === 0)
|
|
101
|
-
return from > 0
|
|
102
|
-
? `(no entries in ${path} past offset ${from})`
|
|
103
|
-
: `(${path} is empty)`;
|
|
104
|
-
const blocks = entries
|
|
105
|
-
.slice(0, DEFAULT_MAX_ENTRIES)
|
|
106
|
-
.map((e) => [
|
|
107
|
-
e.isDirectory ? `${e.name}/` : `${e.name}\t${humanBytes(e.size)}`
|
|
108
|
-
]);
|
|
109
|
-
const { body, shown } = packBlocks(blocks, maxChars);
|
|
110
|
-
const more = entries.length > shown;
|
|
111
|
-
return (body +
|
|
112
|
-
(more
|
|
113
|
-
? `\n… showed ${shown} entries; there are more. Continue with \`offset: ${from + shown}\`, or narrow with \`pattern\`.`
|
|
114
|
-
: ""));
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
}),
|
|
118
|
-
/**
|
|
119
|
-
* Search, without the container.
|
|
120
|
-
*
|
|
121
|
-
* The tool this replaces is `sb_exec("grep -rn …")`, and the case for a native
|
|
122
|
-
* one is not that shelling out fails — it is where the search runs. `fs.grep`
|
|
123
|
-
* reads the Durable Object's SQLite, so it answers while the container is
|
|
124
|
-
* being replaced or an install is still running, which is exactly the window
|
|
125
|
-
* the install gate leaves a subagent with nothing to do. It is also why this
|
|
126
|
-
* tool is not gated: see {@link file://./context.ts awaitAdvisories}.
|
|
127
|
-
*
|
|
128
|
-
* Two lesser reasons that still matter. The query arrives as a value rather
|
|
129
|
-
* than through `shellQuote` and a shell that would re-parse it. And the result
|
|
130
|
-
* is bounded by a `limit` at the source instead of being middle-truncated
|
|
131
|
-
* afterwards, which for a match list means losing whole files silently.
|
|
132
|
-
*/
|
|
133
|
-
sb_grep: tool({
|
|
134
|
-
description: "Search file contents across the workspace. Returns matching lines grouped by file, each with its line number. " +
|
|
135
|
-
"The query is matched literally — set `regex` to interpret it as a regular expression. " +
|
|
136
|
-
"Pass `include` to limit which files are searched, e.g. '**/*.ts' — without it every file under `path` is read, which is slower and rarely what you meant. " +
|
|
137
|
-
"A cut result reports the `offset` that continues it. Use `context` to see the lines around a match. " +
|
|
138
|
-
"Results from `.git` and `node_modules` are left out — search node_modules with sb_exec.",
|
|
139
|
-
inputSchema: z.object({
|
|
140
|
-
query: z.string().describe("Text to find, e.g. 'buildComputerTools'"),
|
|
141
|
-
path: z
|
|
142
|
-
.string()
|
|
143
|
-
.optional()
|
|
144
|
-
.describe(`Absolute file or directory to search (default: ${cwd})`),
|
|
145
|
-
include: z
|
|
146
|
-
.string()
|
|
147
|
-
.optional()
|
|
148
|
-
.describe("Glob relative to path limiting which files are searched, e.g. '**/*.ts'"),
|
|
149
|
-
regex: z
|
|
150
|
-
.boolean()
|
|
151
|
-
.optional()
|
|
152
|
-
.describe("Interpret query as a regular expression"),
|
|
153
|
-
ignoreCase: z.boolean().optional().describe("Ignore letter case"),
|
|
154
|
-
context: z
|
|
155
|
-
.number()
|
|
156
|
-
.int()
|
|
157
|
-
.min(0)
|
|
158
|
-
.max(3)
|
|
159
|
-
.optional()
|
|
160
|
-
.describe("Lines of surrounding context to include with each match"),
|
|
161
|
-
offset: z
|
|
162
|
-
.number()
|
|
163
|
-
.int()
|
|
164
|
-
.min(0)
|
|
165
|
-
.optional()
|
|
166
|
-
.describe("Matches to skip — use the offset a cut result reports")
|
|
167
|
-
}),
|
|
168
|
-
execute: async ({ query, path, include, regex, ignoreCase, context, offset }) => {
|
|
169
|
-
const target = path ?? cwd;
|
|
170
|
-
const refusal = guardPath(target, "sb_grep");
|
|
171
|
-
if (refusal)
|
|
172
|
-
return refusal;
|
|
173
|
-
const from = offset ?? 0;
|
|
174
|
-
return inWorkspace("searching", target, async (fs) => {
|
|
175
|
-
// Two rounds, because a `grep` retry re-reads and re-scans every file
|
|
176
|
-
// it already looked at. `.git` rarely floods a page here — its bulk is
|
|
177
|
-
// compressed objects, which a text query does not match — where a
|
|
178
|
-
// `node_modules` left in the workspace is source and matches.
|
|
179
|
-
const skips = WALK_SKIPS;
|
|
180
|
-
const page = await collectVisible((at, limit) => fs.grep(query, target, {
|
|
181
|
-
include,
|
|
182
|
-
regex,
|
|
183
|
-
ignoreCase,
|
|
184
|
-
context,
|
|
185
|
-
limit,
|
|
186
|
-
offset: at
|
|
187
|
-
}), (m) => m.path, (p) => isSkipped(skips, p), DEFAULT_MAX_MATCHES, from, 2);
|
|
188
|
-
if (page.items.length === 0)
|
|
189
|
-
return page.crowded
|
|
190
|
-
? `every match for ${JSON.stringify(query)} from offset ${from} is inside ${skipNames(skips)}, which ${skips.length > 1 ? "are" : "is"} not searched. Add \`include\` (e.g. '**/*.ts') to search the working tree instead, or use sb_exec to search \`node_modules\`.`
|
|
191
|
-
: `no matches for ${JSON.stringify(query)} in ${target}${include ? ` (${include})` : ""}${from > 0 ? ` past offset ${from}` : ""}`;
|
|
192
|
-
const { body, shown, capped } = renderGrepMatches(page.items.slice(0, DEFAULT_MAX_MATCHES), maxChars);
|
|
193
|
-
return (body +
|
|
194
|
-
listingNote(page, shown, "matches", "`include`") +
|
|
195
|
-
(capped
|
|
196
|
-
? `\n(Some lines were shortened. Read one in full with \`sb_read\` and an \`offset\`.)`
|
|
197
|
-
: ""));
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
}),
|
|
201
|
-
sb_exists: tool({
|
|
202
|
-
description: "Check whether a path exists in the workspace.",
|
|
203
|
-
inputSchema: z.object({ path: z.string().describe("Absolute path") }),
|
|
204
|
-
execute: async ({ path }) => {
|
|
205
|
-
const refusal = guardPath(path, "sb_exists");
|
|
206
|
-
if (refusal)
|
|
207
|
-
return refusal;
|
|
208
|
-
return inWorkspace("checking", path, async (fs) => {
|
|
209
|
-
return (await pathExists(fs, path))
|
|
210
|
-
? `${path} exists`
|
|
211
|
-
: `${path} does not exist`;
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
})
|
|
215
|
-
};
|
|
216
|
-
}
|
package/dist/recall/index.d.ts
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import type { AgentPlugin } from "@dynamicagents/core";
|
|
2
|
-
import type { SessionMessage } from "@dynamicagents/core/agent";
|
|
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,
|
|
61
|
-
* agentName: host.agentName, ...RECALL })
|
|
62
|
-
* ```
|
|
63
|
-
*
|
|
64
|
-
* Field-by-field re-typing is not just verbose — it silently drops any option
|
|
65
|
-
* added later, so a new default lands everywhere except the deployments that
|
|
66
|
-
* enumerated the fields by hand.
|
|
67
|
-
*/
|
|
68
|
-
export interface RecallTuning {
|
|
69
|
-
/**
|
|
70
|
-
* AI Gateway id. Pass the host's **resolved** value (`PluginHost.aiGatewayId`),
|
|
71
|
-
* so embedding calls stay correlated with chat calls in one gateway. Reading it
|
|
72
|
-
* off a config-overrides object instead yields `undefined` the moment that
|
|
73
|
-
* override is dropped in favour of core's default.
|
|
74
|
-
*/
|
|
75
|
-
aiGatewayId?: string;
|
|
76
|
-
/**
|
|
77
|
-
* The `agent` key on embedding calls' AI Gateway log rows. Pass the host's
|
|
78
|
-
* `PluginHost.agentName`, for the reason given on {@link aiGatewayId}.
|
|
79
|
-
*/
|
|
80
|
-
agentName?: string;
|
|
81
|
-
/** Workers-AI embedding model. Must match the index's dimension. */
|
|
82
|
-
embeddingModelId?: string;
|
|
83
|
-
/** Matches returned by a search. Defaults to 5. */
|
|
84
|
-
topK?: number;
|
|
85
|
-
/** Stored-snippet cap, guarding Vectorize's ~10 KiB/vector. Defaults to 2000. */
|
|
86
|
-
metadataTextMax?: number;
|
|
87
|
-
}
|
|
88
|
-
export interface RecallConfig extends RecallTuning {
|
|
89
|
-
/** The `AI` binding, for embeddings. Read lazily — see below. */
|
|
90
|
-
ai: Ai;
|
|
91
|
-
/** The `VECTORIZE` binding. Must be a 1024-dim, cosine index. */
|
|
92
|
-
index: RecallIndex;
|
|
93
|
-
/**
|
|
94
|
-
* The archive namespace for this agent instance — **a thunk, deliberately.**
|
|
95
|
-
*
|
|
96
|
-
* It derives from the verified caller's identity, which is not known when
|
|
97
|
-
* `plugins(host)` runs in `onStart`. The Durable Object is keyed 1:1 by that
|
|
98
|
-
* caller, so the value is constant once known; a thunk is what lets the host
|
|
99
|
-
* supply it late while both hooks here read the same one.
|
|
100
|
-
*/
|
|
101
|
-
namespace: () => string;
|
|
102
|
-
/** Test override; skips the provider entirely. */
|
|
103
|
-
embed?: Embed;
|
|
104
|
-
}
|
|
105
|
-
export declare function recall(config: RecallConfig): AgentPlugin;
|