@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.
Files changed (42) hide show
  1. package/dist/embeddings/rate-limit.d.ts +7 -0
  2. package/dist/embeddings/rate-limit.js +33 -0
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.js +2 -0
  5. package/dist/inspect/client.d.ts +1 -1
  6. package/dist/inspect/client.js +7 -2
  7. package/dist/memory/graph.d.ts +14 -0
  8. package/dist/memory/graph.js +28 -0
  9. package/dist/memory/grep.d.ts +57 -0
  10. package/dist/memory/grep.js +182 -0
  11. package/dist/memory/index.d.ts +12 -2
  12. package/dist/memory/index.js +16 -5
  13. package/dist/memory/match.d.ts +18 -0
  14. package/dist/memory/match.js +61 -0
  15. package/dist/memory/merge.d.ts +65 -0
  16. package/dist/memory/merge.js +379 -0
  17. package/dist/memory/recall.d.ts +1 -1
  18. package/dist/memory/recall.js +1 -1
  19. package/dist/memory/store.d.ts +18 -0
  20. package/dist/memory/store.js +31 -4
  21. package/dist/memory/tools.js +105 -9
  22. package/dist/memory/types.d.ts +7 -2
  23. package/dist/memory/vectors.d.ts +2 -0
  24. package/dist/memory/vectors.js +7 -0
  25. package/dist/models/factory.d.ts +43 -2
  26. package/dist/models/factory.js +108 -1
  27. package/dist/project/config.js +1 -1
  28. package/dist/project/load.d.ts +7 -0
  29. package/dist/project/load.js +29 -6
  30. package/dist/tools/exa.js +1 -1
  31. package/dist/tools/files.d.ts +81 -0
  32. package/dist/tools/files.js +1472 -0
  33. package/dist/tools/index.d.ts +1 -1
  34. package/dist/tools/index.js +1 -1
  35. package/dist/tools/sandbox.d.ts +44 -0
  36. package/dist/tools/sandbox.js +122 -3
  37. package/dist/tools/workspace.js +81 -3
  38. package/dist/trajectory.d.ts +19 -1
  39. package/dist/trajectory.js +53 -0
  40. package/dist/types.d.ts +1 -0
  41. package/dist/types.js +1 -0
  42. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
1
  export * from './exa.ts';
2
+ export * from './files.ts';
2
3
  export * from './sandbox.ts';
3
- export * from './workspace.ts';
4
4
  //# sourceMappingURL=index.d.ts.map
@@ -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
@@ -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);
@@ -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: meta.exit === undefined ? undefined : Number(meta.exit),
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 workspace tools see. Everything ' +
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.',
@@ -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 DfwRiskScanner` for a file of plain functions — and
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
- : [readFileTool, listDir, findFiles, writeFileTool, applyPatch, movePath, deleteFile]);
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 = {
@@ -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;
@@ -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';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenera/neo",
3
- "version": "1.1.22",
3
+ "version": "1.1.24",
4
4
  "description": "A multi-agent runtime for Node.js: agents, models, tools, skills, memory and an append-only trajectory.",
5
5
  "keywords": [
6
6
  "agents",