@zenera/neo 1.1.22 → 1.1.24
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/dist/embeddings/rate-limit.d.ts +7 -0
- package/dist/embeddings/rate-limit.js +33 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/inspect/client.d.ts +1 -1
- package/dist/inspect/client.js +7 -2
- package/dist/memory/graph.d.ts +14 -0
- package/dist/memory/graph.js +28 -0
- package/dist/memory/grep.d.ts +57 -0
- package/dist/memory/grep.js +182 -0
- package/dist/memory/index.d.ts +12 -2
- package/dist/memory/index.js +16 -5
- package/dist/memory/match.d.ts +18 -0
- package/dist/memory/match.js +61 -0
- package/dist/memory/merge.d.ts +65 -0
- package/dist/memory/merge.js +379 -0
- package/dist/memory/recall.d.ts +1 -1
- package/dist/memory/recall.js +1 -1
- package/dist/memory/store.d.ts +18 -0
- package/dist/memory/store.js +31 -4
- package/dist/memory/tools.js +105 -9
- package/dist/memory/types.d.ts +7 -2
- package/dist/memory/vectors.d.ts +2 -0
- package/dist/memory/vectors.js +7 -0
- package/dist/models/factory.d.ts +43 -2
- package/dist/models/factory.js +108 -1
- package/dist/project/config.js +1 -1
- package/dist/project/load.d.ts +7 -0
- package/dist/project/load.js +29 -6
- package/dist/tools/exa.js +1 -1
- package/dist/tools/files.d.ts +81 -0
- package/dist/tools/files.js +1472 -0
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +1 -1
- package/dist/tools/sandbox.d.ts +44 -0
- package/dist/tools/sandbox.js +122 -3
- package/dist/tools/workspace.js +81 -3
- package/dist/trajectory.d.ts +19 -1
- package/dist/trajectory.js +53 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.js +1 -0
- package/package.json +1 -1
package/dist/tools/index.d.ts
CHANGED
package/dist/tools/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Tools a host may hand to a project. Nothing here is wired in by default:
|
|
2
2
|
// what an agent can reach is a deployment decision, so it is opted into.
|
|
3
3
|
export * from "./exa.js";
|
|
4
|
+
export * from "./files.js";
|
|
4
5
|
export * from "./sandbox.js";
|
|
5
|
-
export * from "./workspace.js";
|
|
6
6
|
//# sourceMappingURL=index.js.map
|
package/dist/tools/sandbox.d.ts
CHANGED
|
@@ -120,6 +120,15 @@ export interface ProcResult {
|
|
|
120
120
|
}
|
|
121
121
|
export type Runner = (bin: string, args: readonly string[], opts?: ProcOptions) => Promise<ProcResult>;
|
|
122
122
|
export declare const runProcess: Runner;
|
|
123
|
+
/**
|
|
124
|
+
* `run`, retried while the engine could not be reached at all.
|
|
125
|
+
*
|
|
126
|
+
* Empty stdout is the guard that keeps this from retrying real work: a command
|
|
127
|
+
* that produced output ran, whatever its stderr says afterwards. The wait is
|
|
128
|
+
* jittered because the burst being backed off from is N clients arriving
|
|
129
|
+
* together, and a fixed delay would only reassemble it.
|
|
130
|
+
*/
|
|
131
|
+
export declare function resilient(run: Runner, attempts?: number): Runner;
|
|
123
132
|
interface Resolved extends Required<Omit<SandboxSpec, 'user'>> {
|
|
124
133
|
user?: string;
|
|
125
134
|
root: string;
|
|
@@ -143,12 +152,30 @@ export interface ExecResult {
|
|
|
143
152
|
truncated?: boolean;
|
|
144
153
|
timed_out?: boolean;
|
|
145
154
|
duration_ms: number;
|
|
155
|
+
out_of_memory?: OutOfMemory;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Why a command was killed for memory, which is a different question from
|
|
159
|
+
* whether it was. A container that overran its own `--memory` will do it again
|
|
160
|
+
* however long anyone waits; one the kernel sacrificed for a neighbour may
|
|
161
|
+
* simply succeed later. Exit code 137 is all either of them says.
|
|
162
|
+
*/
|
|
163
|
+
export interface OutOfMemory {
|
|
164
|
+
/** what this container was allowed */
|
|
165
|
+
limit_mib: number;
|
|
166
|
+
/** high-water mark for the container's whole life, not for this command */
|
|
167
|
+
peak_mib?: number;
|
|
168
|
+
/** false when the limit itself is the problem, so an unchanged rerun ends the same way */
|
|
169
|
+
retryable: boolean;
|
|
170
|
+
hint: string;
|
|
146
171
|
}
|
|
147
172
|
interface Job {
|
|
148
173
|
id: string;
|
|
149
174
|
command: string;
|
|
150
175
|
cwd: string;
|
|
151
176
|
startedAt: number;
|
|
177
|
+
/** worked out once, because a log is read many times and the evidence is consumed */
|
|
178
|
+
oom?: OutOfMemory;
|
|
152
179
|
}
|
|
153
180
|
/**
|
|
154
181
|
* The container, started when it is first needed and not before. A project
|
|
@@ -195,6 +222,23 @@ export declare class Sandbox {
|
|
|
195
222
|
*/
|
|
196
223
|
inside(path?: string): string;
|
|
197
224
|
}
|
|
225
|
+
export interface SandboxLimits {
|
|
226
|
+
/** MiB */
|
|
227
|
+
memory: number;
|
|
228
|
+
/** fractional cores */
|
|
229
|
+
cpus: number;
|
|
230
|
+
hardening: SandboxHardening;
|
|
231
|
+
/** whether the numbers above were written down, or are this profile's defaults */
|
|
232
|
+
declared: boolean;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* What a spec's ceilings come to, for a report that has no container to ask.
|
|
236
|
+
*
|
|
237
|
+
* It exists so `zen sandbox status` cannot disagree with what a run actually
|
|
238
|
+
* applies: the defaults are stated once, here and in `resolveSpec`, and
|
|
239
|
+
* nothing outside this module repeats them.
|
|
240
|
+
*/
|
|
241
|
+
export declare function sandboxLimits(spec?: Pick<SandboxSpec, 'cpus' | 'memory' | 'hardening'>): SandboxLimits;
|
|
198
242
|
export declare class SandboxPool {
|
|
199
243
|
#private;
|
|
200
244
|
constructor(opts: SandboxPoolOptions);
|
package/dist/tools/sandbox.js
CHANGED
|
@@ -39,6 +39,8 @@ const MAX_TIMEOUT_MS = 3_600_000;
|
|
|
39
39
|
const DEFAULT_PIDS = 1024;
|
|
40
40
|
/** How long a container gets to stop itself before it is killed. */
|
|
41
41
|
const STOP_GRACE_S = 2;
|
|
42
|
+
/** SIGKILL, which is how the kernel's out-of-memory killer arrives. */
|
|
43
|
+
const KILLED = 137;
|
|
42
44
|
// Standard mode's resource ceilings. Even without the strict profile, an
|
|
43
45
|
// unbounded container can exhaust host cores or memory during runaway builds.
|
|
44
46
|
const DEFAULT_CPUS = 4;
|
|
@@ -138,6 +140,57 @@ export const runProcess = (bin, args, opts = {}) => new Promise((settle, fail) =
|
|
|
138
140
|
child.stdin.end();
|
|
139
141
|
}
|
|
140
142
|
});
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Reaching the engine at all
|
|
145
|
+
//
|
|
146
|
+
// On macOS and Windows the client is remote: unless it has been pointed at the
|
|
147
|
+
// machine's forwarded endpoint, every invocation opens its own ssh connection
|
|
148
|
+
// into the virtual machine, and sshd refuses new ones past `MaxStartups` — ten
|
|
149
|
+
// by default, with a random drop above that. A wide batch therefore loses
|
|
150
|
+
// calls for a reason that has nothing to do with the command, on a different
|
|
151
|
+
// task each time.
|
|
152
|
+
//
|
|
153
|
+
// A refused connection is the one failure that can be retried without asking
|
|
154
|
+
// what the command was: it happened before authentication, so nothing ran.
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
const TRANSPORT = /kex_exchange_identification|ssh: handshake failed|ssh: unexpected packet|connection closed by remote host|connection reset by peer|connection refused/i;
|
|
157
|
+
const ATTEMPTS = 4;
|
|
158
|
+
/**
|
|
159
|
+
* `run`, retried while the engine could not be reached at all.
|
|
160
|
+
*
|
|
161
|
+
* Empty stdout is the guard that keeps this from retrying real work: a command
|
|
162
|
+
* that produced output ran, whatever its stderr says afterwards. The wait is
|
|
163
|
+
* jittered because the burst being backed off from is N clients arriving
|
|
164
|
+
* together, and a fixed delay would only reassemble it.
|
|
165
|
+
*/
|
|
166
|
+
export function resilient(run, attempts = ATTEMPTS) {
|
|
167
|
+
return async (bin, args, opts = {}) => {
|
|
168
|
+
for (let n = 1;; n++) {
|
|
169
|
+
const res = await run(bin, args, opts);
|
|
170
|
+
if (n >= attempts ||
|
|
171
|
+
res.code === 0 ||
|
|
172
|
+
res.stdout !== '' ||
|
|
173
|
+
opts.signal?.aborted ||
|
|
174
|
+
!TRANSPORT.test(res.stderr)) {
|
|
175
|
+
return res;
|
|
176
|
+
}
|
|
177
|
+
await new Promise((go) => setTimeout(go, 2 ** n * 50 + Math.random() * 100));
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Asked of the container after a kill. Builtins only: a slim image may have no
|
|
183
|
+
* `awk`, and strict hardening leaves nowhere writable to install one. `oom_kill`
|
|
184
|
+
* counts kills in this container's cgroup and no other, which is what separates
|
|
185
|
+
* the two cases. Unreadable values print empty, and the caller reads that as
|
|
186
|
+
* "could not tell".
|
|
187
|
+
*/
|
|
188
|
+
const CGROUP = [
|
|
189
|
+
`peak=; kills=`,
|
|
190
|
+
`read peak < /sys/fs/cgroup/memory.peak 2>/dev/null`,
|
|
191
|
+
`while read -r k v; do [ "$k" = oom_kill ] && kills=$v; done < /sys/fs/cgroup/memory.events 2>/dev/null`,
|
|
192
|
+
`echo "peak=$peak kills=$kills"`,
|
|
193
|
+
].join('\n');
|
|
141
194
|
/**
|
|
142
195
|
* The container, started when it is first needed and not before. A project
|
|
143
196
|
* whose agents never shell out never pays for an image pull, and a session
|
|
@@ -150,9 +203,11 @@ export class Sandbox {
|
|
|
150
203
|
#jobs = new Map();
|
|
151
204
|
#ready;
|
|
152
205
|
#created = false;
|
|
206
|
+
/** kills counted in this container's cgroup as of the last probe */
|
|
207
|
+
#kills;
|
|
153
208
|
constructor(opts) {
|
|
154
209
|
this.spec = resolveSpec(opts);
|
|
155
|
-
this.#run = opts.exec ?? runProcess;
|
|
210
|
+
this.#run = resilient(opts.exec ?? runProcess);
|
|
156
211
|
this.name = containerName(this.spec);
|
|
157
212
|
}
|
|
158
213
|
/** Whether anything was actually started — `dispose` is free when not. */
|
|
@@ -273,6 +328,7 @@ export class Sandbox {
|
|
|
273
328
|
truncated: res.truncated || undefined,
|
|
274
329
|
timed_out: res.timedOut || undefined,
|
|
275
330
|
duration_ms: Date.now() - startedAt,
|
|
331
|
+
out_of_memory: await this.#oom(res.code, res.timedOut, opts.signal),
|
|
276
332
|
};
|
|
277
333
|
}
|
|
278
334
|
/**
|
|
@@ -353,17 +409,24 @@ export class Sandbox {
|
|
|
353
409
|
const output = rest.join('\n---\n');
|
|
354
410
|
const lines = Number(meta.lines ?? 0);
|
|
355
411
|
const shown = output ? output.replace(/\n$/, '').split('\n').length : 0;
|
|
412
|
+
const exit = meta.exit === undefined ? undefined : Number(meta.exit);
|
|
413
|
+
// Kept on the job: a second read would find the kill counter unchanged
|
|
414
|
+
// and report this container's own overrun as the machine's doing.
|
|
415
|
+
if (exit === KILLED && !job.oom) {
|
|
416
|
+
job.oom = await this.#oom(exit, false);
|
|
417
|
+
}
|
|
356
418
|
return {
|
|
357
419
|
job_id: id,
|
|
358
420
|
command: job.command,
|
|
359
421
|
running: meta.exit === undefined,
|
|
360
|
-
exit_code:
|
|
422
|
+
exit_code: exit,
|
|
361
423
|
duration_ms: Date.now() - job.startedAt,
|
|
362
424
|
start_line: first,
|
|
363
425
|
end_line: first + shown - 1,
|
|
364
426
|
lines,
|
|
365
427
|
truncated: first + shown - 1 < lines || undefined,
|
|
366
428
|
output,
|
|
429
|
+
out_of_memory: job.oom,
|
|
367
430
|
};
|
|
368
431
|
}
|
|
369
432
|
/** Signals a job's whole process group, so its children go too. */
|
|
@@ -422,6 +485,45 @@ export class Sandbox {
|
|
|
422
485
|
const wanted = seconds && seconds > 0 ? seconds * 1000 : this.spec.timeout * 1000;
|
|
423
486
|
return Math.min(wanted || DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
|
|
424
487
|
}
|
|
488
|
+
/**
|
|
489
|
+
* Turns a bare 137 into a reason, by asking the container which cgroup did
|
|
490
|
+
* the killing. Only a kill counted against *this* container is the caller's
|
|
491
|
+
* to fix; the rest is the machine being busy, and telling someone to shrink
|
|
492
|
+
* a command that already fit would send them off after the wrong thing.
|
|
493
|
+
*/
|
|
494
|
+
async #oom(code, timedOut, signal) {
|
|
495
|
+
// A timeout or a cancel kills podman on this side, not the container,
|
|
496
|
+
// and an uncapped container has no limit it could have exceeded.
|
|
497
|
+
if (code !== KILLED || timedOut || signal?.aborted || this.spec.memory <= 0) {
|
|
498
|
+
return undefined;
|
|
499
|
+
}
|
|
500
|
+
const res = await this.#run(this.spec.engine, ['exec', '--interactive', this.name, '/bin/sh', '-s'], { input: CGROUP, timeoutMs: 15_000 }).catch(() => undefined);
|
|
501
|
+
const out = res?.stdout ?? '';
|
|
502
|
+
const peak = Number(/peak=(\d+)/.exec(out)?.[1]);
|
|
503
|
+
const kills = Number(/kills=(\d+)/.exec(out)?.[1]);
|
|
504
|
+
const counted = Number.isFinite(kills);
|
|
505
|
+
// Nothing readable is read as our own doing: that costs one wasted
|
|
506
|
+
// retry, where the other guess costs an agent retrying forever a
|
|
507
|
+
// command that was never going to fit.
|
|
508
|
+
const own = !counted || kills > (this.#kills ?? 0);
|
|
509
|
+
if (counted) {
|
|
510
|
+
this.#kills = kills;
|
|
511
|
+
}
|
|
512
|
+
const limit = this.spec.memory;
|
|
513
|
+
return {
|
|
514
|
+
limit_mib: limit,
|
|
515
|
+
peak_mib: Number.isFinite(peak) ? Math.round(peak / (1024 * 1024)) : undefined,
|
|
516
|
+
retryable: !own,
|
|
517
|
+
hint: own
|
|
518
|
+
? `this container is limited to ${limit} MiB and the command wanted more. ` +
|
|
519
|
+
`Running it again unchanged will end the same way: give the sandbox more ` +
|
|
520
|
+
`memory in the project configuration, or do the work in smaller pieces.`
|
|
521
|
+
: `the machine ran out of memory while this container was inside its ` +
|
|
522
|
+
`${limit} MiB limit, so it was killed for something another container did. ` +
|
|
523
|
+
`The command itself does not need to change, and may work once the machine ` +
|
|
524
|
+
`is less busy.`,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
425
527
|
#podman(args) {
|
|
426
528
|
return this.#run(this.spec.engine, args, { timeoutMs: 120_000 });
|
|
427
529
|
}
|
|
@@ -474,6 +576,23 @@ function resolveSpec(opts) {
|
|
|
474
576
|
engine: opts.engine ?? 'podman',
|
|
475
577
|
};
|
|
476
578
|
}
|
|
579
|
+
/**
|
|
580
|
+
* What a spec's ceilings come to, for a report that has no container to ask.
|
|
581
|
+
*
|
|
582
|
+
* It exists so `zen sandbox status` cannot disagree with what a run actually
|
|
583
|
+
* applies: the defaults are stated once, here and in `resolveSpec`, and
|
|
584
|
+
* nothing outside this module repeats them.
|
|
585
|
+
*/
|
|
586
|
+
export function sandboxLimits(spec = {}) {
|
|
587
|
+
const hardening = spec.hardening ?? 'standard';
|
|
588
|
+
const strict = hardening === 'strict';
|
|
589
|
+
return {
|
|
590
|
+
memory: spec.memory ?? (strict ? STRICT_MEMORY : DEFAULT_MEMORY),
|
|
591
|
+
cpus: spec.cpus ?? (strict ? STRICT_CPUS : DEFAULT_CPUS),
|
|
592
|
+
hardening,
|
|
593
|
+
declared: spec.memory !== undefined || spec.cpus !== undefined,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
477
596
|
/**
|
|
478
597
|
* The container's name is a function of its configuration. Change the image
|
|
479
598
|
* and a new container appears rather than an old one quietly persisting with
|
|
@@ -585,7 +704,7 @@ export function sandboxTools(opts) {
|
|
|
585
704
|
group: SANDBOX_GROUP,
|
|
586
705
|
description: 'Runs a shell command in a Linux container and waits for it to finish. The ' +
|
|
587
706
|
`workspace is mounted at ${SANDBOX_MOUNT} and is the working directory, so files you ` +
|
|
588
|
-
'read and write here are the same files the
|
|
707
|
+
'read and write here are the same files the file tools see. Everything ' +
|
|
589
708
|
'else is throwaway. Use this for builds, tests, and package installs; use ' +
|
|
590
709
|
'run_command_background for anything that does not end on its own, such as a ' +
|
|
591
710
|
'server or a watcher.',
|
package/dist/tools/workspace.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { realpathSync, statSync } from 'node:fs';
|
|
2
|
-
import { mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { cp, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { dirname, extname, join, relative, resolve, sep } from 'node:path';
|
|
4
4
|
import { tool } from "../types.js";
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
@@ -684,7 +684,7 @@ function patchLines(lines, chunks, file) {
|
|
|
684
684
|
const at = findHeading(out, heading, cursor);
|
|
685
685
|
if (at < 0) {
|
|
686
686
|
// A heading only narrows the search. Models invent them —
|
|
687
|
-
// `@@ class
|
|
687
|
+
// `@@ class RiskScanner` for a file of plain functions — and
|
|
688
688
|
// when the context occurs once the heading was never needed, so
|
|
689
689
|
// refusing spends a turn to learn nothing.
|
|
690
690
|
if (countMatches(out, chunk.before, cursor) === 1) {
|
|
@@ -1064,6 +1064,75 @@ export function workspaceTools(opts) {
|
|
|
1064
1064
|
return { from: ws.show(source), to: ws.show(target), moved: true };
|
|
1065
1065
|
},
|
|
1066
1066
|
});
|
|
1067
|
+
// The one mutating tool whose source may sit in a read-only tree, and the
|
|
1068
|
+
// reason it earns its place: reaching a mounted file otherwise means
|
|
1069
|
+
// reading it into the context and writing it back out, which costs a turn,
|
|
1070
|
+
// truncates past the read cap and cannot carry a binary at all.
|
|
1071
|
+
const copyPath = tool({
|
|
1072
|
+
name: 'copy_file',
|
|
1073
|
+
group: GROUP,
|
|
1074
|
+
description: 'Copies a file, making the destination parent directories as needed. The source ' +
|
|
1075
|
+
'may be in a read-only tree, so this is how something mounted for reference is ' +
|
|
1076
|
+
'brought into the workspace; the destination must be writable. The bytes are ' +
|
|
1077
|
+
'copied directly and are not read into the conversation, so size is no object ' +
|
|
1078
|
+
'and a binary copies as faithfully as text. Refuses to clobber an existing ' +
|
|
1079
|
+
'destination unless overwrite is set, and only copies a directory, with ' +
|
|
1080
|
+
'everything in it, when recursive is set.',
|
|
1081
|
+
parameters: {
|
|
1082
|
+
type: 'object',
|
|
1083
|
+
properties: {
|
|
1084
|
+
from: { type: 'string', description: `What to copy. ${inside}` },
|
|
1085
|
+
to: { type: 'string', description: `Where the copy should land. ${inside}` },
|
|
1086
|
+
overwrite: {
|
|
1087
|
+
type: 'boolean',
|
|
1088
|
+
description: 'Replace the destination if it exists.',
|
|
1089
|
+
},
|
|
1090
|
+
recursive: {
|
|
1091
|
+
type: 'boolean',
|
|
1092
|
+
description: 'Required to copy a directory and its contents.',
|
|
1093
|
+
},
|
|
1094
|
+
},
|
|
1095
|
+
required: ['from', 'to'],
|
|
1096
|
+
additionalProperties: false,
|
|
1097
|
+
},
|
|
1098
|
+
execute: async ({ from, to, overwrite, recursive }) => {
|
|
1099
|
+
ws.mutable();
|
|
1100
|
+
const source = ws.within(from);
|
|
1101
|
+
const target = ws.within(to, { write: true });
|
|
1102
|
+
if (ws.isRoot(target)) {
|
|
1103
|
+
return { error: 'refusing to overwrite the workspace root' };
|
|
1104
|
+
}
|
|
1105
|
+
if (source === target) {
|
|
1106
|
+
return { error: 'the source and the destination are the same path' };
|
|
1107
|
+
}
|
|
1108
|
+
const info = await stat(source); // ENOENT here is the honest error
|
|
1109
|
+
if (info.isDirectory()) {
|
|
1110
|
+
if (!recursive) {
|
|
1111
|
+
return {
|
|
1112
|
+
error: `${ws.show(source)} is a directory`,
|
|
1113
|
+
hint: 'pass recursive to copy it and everything in it',
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
if (target.startsWith(source + sep)) {
|
|
1117
|
+
return { error: `cannot copy ${ws.show(source)} into itself` };
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
if (await statOrNull(target)) {
|
|
1121
|
+
if (!overwrite) {
|
|
1122
|
+
return {
|
|
1123
|
+
error: `${ws.show(target)} already exists`,
|
|
1124
|
+
hint: 'pass overwrite to replace it',
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
// cp() will not replace a directory with a file, or the reverse.
|
|
1128
|
+
await rm(target, { recursive: true, force: true });
|
|
1129
|
+
}
|
|
1130
|
+
await mkdir(dirname(target), { recursive: true });
|
|
1131
|
+
await cp(source, target, { recursive: recursive === true });
|
|
1132
|
+
const out = { from: ws.show(source), to: ws.show(target), copied: true };
|
|
1133
|
+
return info.isDirectory() ? { ...out, directory: true } : { ...out, bytes: info.size };
|
|
1134
|
+
},
|
|
1135
|
+
});
|
|
1067
1136
|
const deleteFile = tool({
|
|
1068
1137
|
name: 'delete_file',
|
|
1069
1138
|
group: GROUP,
|
|
@@ -1154,7 +1223,16 @@ export function workspaceTools(opts) {
|
|
|
1154
1223
|
});
|
|
1155
1224
|
return named(ws, opts.readOnly
|
|
1156
1225
|
? [readFileTool, listDir, findFiles]
|
|
1157
|
-
: [
|
|
1226
|
+
: [
|
|
1227
|
+
readFileTool,
|
|
1228
|
+
listDir,
|
|
1229
|
+
findFiles,
|
|
1230
|
+
writeFileTool,
|
|
1231
|
+
applyPatch,
|
|
1232
|
+
copyPath,
|
|
1233
|
+
movePath,
|
|
1234
|
+
deleteFile,
|
|
1235
|
+
]);
|
|
1158
1236
|
}
|
|
1159
1237
|
/** What each errno means, said the way the rest of these tools say things. */
|
|
1160
1238
|
const ERRNO = {
|
package/dist/trajectory.d.ts
CHANGED
|
@@ -67,7 +67,7 @@ export interface MemoryRecallNode extends NodeBase {
|
|
|
67
67
|
}
|
|
68
68
|
export interface MemoryOpNode extends NodeBase {
|
|
69
69
|
type: 'memory_op';
|
|
70
|
-
op: 'commit' | 'forget' | 'load';
|
|
70
|
+
op: 'commit' | 'forget' | 'load' | 'grep';
|
|
71
71
|
/** sha256(runId, callId) — deterministic, so a replay is deduplicated */
|
|
72
72
|
opId: string;
|
|
73
73
|
nodes: {
|
|
@@ -202,6 +202,24 @@ export declare function projected(nodes: TrajectoryNode[]): TrajectoryNode[];
|
|
|
202
202
|
* added, since counting it too would double every branch.
|
|
203
203
|
*/
|
|
204
204
|
export declare function totalUsage(trajectory: TrajectoryNode[]): TokenUsage;
|
|
205
|
+
export interface ModelUsage {
|
|
206
|
+
/** the exact model id the call was served by, as recorded on the node */
|
|
207
|
+
model: string;
|
|
208
|
+
calls: number;
|
|
209
|
+
usage: TokenUsage;
|
|
210
|
+
}
|
|
211
|
+
/** A summarizer's tokens are real spend, but no node records which model burned them. */
|
|
212
|
+
export declare const COMPACTION_MODEL = "(compaction)";
|
|
213
|
+
/**
|
|
214
|
+
* `totalUsage`, split by the model that spent it. The same walk and the same
|
|
215
|
+
* recursion into branches, so the rows always add back up to the total — which
|
|
216
|
+
* is the point: a run that used three models must not report one number that
|
|
217
|
+
* belongs to none of them. Heaviest first, since that is the one being asked
|
|
218
|
+
* about.
|
|
219
|
+
*/
|
|
220
|
+
export declare function usageByModel(trajectory: TrajectoryNode[]): ModelUsage[];
|
|
221
|
+
/** Folds splits together: many turns of one run, or many items of a batch. */
|
|
222
|
+
export declare function mergeModelUsage(...lists: readonly (readonly ModelUsage[])[]): ModelUsage[];
|
|
205
223
|
export declare function lastOfType<T extends TrajectoryNode['type']>(trajectory: TrajectoryNode[], type: T): Extract<TrajectoryNode, {
|
|
206
224
|
type: T;
|
|
207
225
|
}> | undefined;
|
package/dist/trajectory.js
CHANGED
|
@@ -47,6 +47,59 @@ export function totalUsage(trajectory) {
|
|
|
47
47
|
return acc;
|
|
48
48
|
}, zeroUsage());
|
|
49
49
|
}
|
|
50
|
+
/** A summarizer's tokens are real spend, but no node records which model burned them. */
|
|
51
|
+
export const COMPACTION_MODEL = '(compaction)';
|
|
52
|
+
/**
|
|
53
|
+
* `totalUsage`, split by the model that spent it. The same walk and the same
|
|
54
|
+
* recursion into branches, so the rows always add back up to the total — which
|
|
55
|
+
* is the point: a run that used three models must not report one number that
|
|
56
|
+
* belongs to none of them. Heaviest first, since that is the one being asked
|
|
57
|
+
* about.
|
|
58
|
+
*/
|
|
59
|
+
export function usageByModel(trajectory) {
|
|
60
|
+
const into = new Map();
|
|
61
|
+
collect(trajectory, into);
|
|
62
|
+
return sorted(into);
|
|
63
|
+
}
|
|
64
|
+
/** Folds splits together: many turns of one run, or many items of a batch. */
|
|
65
|
+
export function mergeModelUsage(...lists) {
|
|
66
|
+
const into = new Map();
|
|
67
|
+
for (const list of lists) {
|
|
68
|
+
for (const one of list) {
|
|
69
|
+
fold(into, one);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return sorted(into);
|
|
73
|
+
}
|
|
74
|
+
function collect(trajectory, into) {
|
|
75
|
+
for (const n of trajectory) {
|
|
76
|
+
if (n.type === 'llm_call') {
|
|
77
|
+
fold(into, { model: n.model, calls: 1, usage: n.usage });
|
|
78
|
+
}
|
|
79
|
+
else if (n.type === 'compaction') {
|
|
80
|
+
fold(into, { model: COMPACTION_MODEL, calls: 1, usage: n.usage });
|
|
81
|
+
}
|
|
82
|
+
else if (n.type === 'join') {
|
|
83
|
+
for (const branch of n.branches) {
|
|
84
|
+
collect(branch.nodes, into);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function fold(into, one) {
|
|
90
|
+
const had = into.get(one.model);
|
|
91
|
+
into.set(one.model, had
|
|
92
|
+
? {
|
|
93
|
+
model: had.model,
|
|
94
|
+
calls: had.calls + one.calls,
|
|
95
|
+
usage: addUsage(had.usage, one.usage),
|
|
96
|
+
}
|
|
97
|
+
: { model: one.model, calls: one.calls, usage: one.usage });
|
|
98
|
+
}
|
|
99
|
+
function sorted(into) {
|
|
100
|
+
const spent = (m) => m.usage.inputTokens + m.usage.outputTokens;
|
|
101
|
+
return [...into.values()].sort((a, b) => spent(b) - spent(a) || a.model.localeCompare(b.model));
|
|
102
|
+
}
|
|
50
103
|
export function lastOfType(trajectory, type) {
|
|
51
104
|
for (let i = trajectory.length - 1; i >= 0; i--) {
|
|
52
105
|
if (trajectory[i].type === type) {
|
package/dist/types.d.ts
CHANGED
|
@@ -184,6 +184,7 @@ export declare const HANDOFF_PREFIX = "transfer_to_";
|
|
|
184
184
|
export declare const FORK_TOOL = "fork";
|
|
185
185
|
export declare const FINAL_OUTPUT_TOOL = "final_output";
|
|
186
186
|
export declare const MEMORY_SEARCH_TOOL = "memory_search";
|
|
187
|
+
export declare const MEMORY_GREP_TOOL = "memory_grep";
|
|
187
188
|
export declare const MEMORY_LOAD_TOOL = "memory_load";
|
|
188
189
|
export declare const MEMORY_COMMIT_TOOL = "memory_commit";
|
|
189
190
|
export declare const MEMORY_FORGET_TOOL = "memory_forget";
|
package/dist/types.js
CHANGED
|
@@ -144,6 +144,7 @@ export const HANDOFF_PREFIX = 'transfer_to_';
|
|
|
144
144
|
export const FORK_TOOL = 'fork';
|
|
145
145
|
export const FINAL_OUTPUT_TOOL = 'final_output';
|
|
146
146
|
export const MEMORY_SEARCH_TOOL = 'memory_search';
|
|
147
|
+
export const MEMORY_GREP_TOOL = 'memory_grep';
|
|
147
148
|
export const MEMORY_LOAD_TOOL = 'memory_load';
|
|
148
149
|
export const MEMORY_COMMIT_TOOL = 'memory_commit';
|
|
149
150
|
export const MEMORY_FORGET_TOOL = 'memory_forget';
|