@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
@@ -39,6 +39,13 @@ export class VectorBlock {
39
39
  has(id) {
40
40
  return this.#index.has(id);
41
41
  }
42
+ /** A copy, not a view: the row it came from moves the next time something is deleted. */
43
+ get(id) {
44
+ const row = this.#index.get(id);
45
+ return row === undefined
46
+ ? undefined
47
+ : this.#data.slice(row * this.#dims, (row + 1) * this.#dims);
48
+ }
42
49
  set(id, vector) {
43
50
  if (vector.length !== this.#dims) {
44
51
  throw new Error(`expected a ${this.#dims}-dimension vector, got ${vector.length}`);
@@ -83,8 +83,9 @@ export interface ProviderSpec extends Credentials {
83
83
  }
84
84
  /**
85
85
  * What to ask for, and of whom. Vendor differences live in the *provider*, so
86
- * there is nothing here to discriminate on: the option interfaces are unioned
87
- * and the knobs that do not apply to the chosen vendor are ignored.
86
+ * there is nothing here to discriminate on: the option interfaces are unioned,
87
+ * which makes every vendor's knobs type-legal on every model. `model()` rejects
88
+ * the ones the resolved adapter would not read — see `KNOBS`.
88
89
  *
89
90
  * `Credentials` are still accepted inline, for a one-off model that does not
90
91
  * warrant a declared provider. Using them opts out of the shared client.
@@ -149,6 +150,39 @@ export interface ModelRequirement {
149
150
  }
150
151
  export declare const providerKinds: ProviderKind[];
151
152
  export declare function isProviderKind(name: string): name is ProviderKind;
153
+ /**
154
+ * Which knobs each adapter actually reads.
155
+ *
156
+ * `ModelSpec` unions every vendor's option interface, so `reasoningEffort` on a
157
+ * Gemini model is type-legal and the yaml schema accepts it too — and until
158
+ * this table it was dropped in silence, which is the worst outcome available:
159
+ * the config claims a setting is in force, the request never carries it, and
160
+ * the run looks healthy. No static type could have caught it either, because
161
+ * `provider` is a free name and its kind is not known until resolution.
162
+ *
163
+ * Translating instead was the alternative, and it is worse. The vendors do not
164
+ * mean the same thing by the same word: effort is a coarse level on OpenAI, a
165
+ * token budget on Anthropic, and *both* on Google depending on the model
166
+ * generation. A silent remap would make one line mean four things and bury the
167
+ * 2.5/3 split that `gemini.ts` deliberately refuses to paper over.
168
+ */
169
+ declare const KNOBS: {
170
+ readonly 'openai/chat': readonly ["reasoningEffort"];
171
+ readonly 'openai/responses': readonly ["reasoningEffort", "reasoningSummary", "store"];
172
+ readonly anthropic: readonly ["maxTokens", "thinkingBudgetTokens"];
173
+ readonly gemini: readonly ["maxTokens", "thinkingBudget", "thinkingLevel", "includeThoughts"];
174
+ readonly openrouter: readonly ["maxTokens", "reasoningEffort", "reasoningSummary", "routing", "fallbacks", "plugins", "serviceTier"];
175
+ };
176
+ /** An adapter, in the terms `KNOBS` is keyed by: a protocol, split by api where there are two. */
177
+ export type KnobTarget = keyof typeof KNOBS;
178
+ /** A knob a spec carries that the adapter it resolves to will never read. */
179
+ export interface KnobIssue {
180
+ knob: string;
181
+ /** the adapter that would have had to read it */
182
+ target: KnobTarget;
183
+ /** what that adapter does read */
184
+ supported: readonly string[];
185
+ }
152
186
  /**
153
187
  * Substitutes `${VAR}` references. An unset variable with no fallback is an
154
188
  * error naming both the variable and where it was written, because the
@@ -176,6 +210,12 @@ export declare class ModelRegistry {
176
210
  names(): string[];
177
211
  /** The shared client for a provider, built on first use. */
178
212
  client(name: string): ProviderClient;
213
+ /**
214
+ * Knobs a ref carries that its adapter will never read — the same check
215
+ * `model()` refuses on, minus the client, so a report can name them before
216
+ * a credential is resolved or anything is contacted.
217
+ */
218
+ knobs(ref: ModelRef): KnobIssue[];
179
219
  /** Turns a shorthand or a spec into a `Model`. */
180
220
  model(ref: ModelRef): Model;
181
221
  /**
@@ -215,4 +255,5 @@ export declare const defaultModels: ModelRegistry;
215
255
  export declare function createModel(ref: ModelRef, registry?: ModelRegistry): Model;
216
256
  /** `createModel`'s counterpart for vectors, over the same registry and clients. */
217
257
  export declare function createEmbedder(ref: EmbeddingRef, registry?: ModelRegistry): Embedder;
258
+ export {};
218
259
  //# sourceMappingURL=factory.d.ts.map
@@ -4,6 +4,7 @@ import { GeminiEmbedder } from "../embeddings/gemini.js";
4
4
  import { DEFAULT_START, RateLimiter } from "../embeddings/limiter.js";
5
5
  import { OpenAIEmbedder } from "../embeddings/openai.js";
6
6
  import { OpenRouterEmbedder } from "../embeddings/openrouter.js";
7
+ import { unfundedSymbol } from "../embeddings/rate-limit.js";
7
8
  import { AnthropicModel } from "./anthropic.js";
8
9
  import { GeminiModel } from "./gemini.js";
9
10
  import { OpenAIModel } from "./openai-chat.js";
@@ -114,6 +115,48 @@ export function isProviderKind(name) {
114
115
  return Object.hasOwn(KINDS, name);
115
116
  }
116
117
  // ---------------------------------------------------------------------------
118
+ // Tuning knobs
119
+ // ---------------------------------------------------------------------------
120
+ /**
121
+ * Which knobs each adapter actually reads.
122
+ *
123
+ * `ModelSpec` unions every vendor's option interface, so `reasoningEffort` on a
124
+ * Gemini model is type-legal and the yaml schema accepts it too — and until
125
+ * this table it was dropped in silence, which is the worst outcome available:
126
+ * the config claims a setting is in force, the request never carries it, and
127
+ * the run looks healthy. No static type could have caught it either, because
128
+ * `provider` is a free name and its kind is not known until resolution.
129
+ *
130
+ * Translating instead was the alternative, and it is worse. The vendors do not
131
+ * mean the same thing by the same word: effort is a coarse level on OpenAI, a
132
+ * token budget on Anthropic, and *both* on Google depending on the model
133
+ * generation. A silent remap would make one line mean four things and bury the
134
+ * 2.5/3 split that `gemini.ts` deliberately refuses to paper over.
135
+ */
136
+ const KNOBS = {
137
+ 'openai/chat': ['reasoningEffort'],
138
+ 'openai/responses': ['reasoningEffort', 'reasoningSummary', 'store'],
139
+ anthropic: ['maxTokens', 'thinkingBudgetTokens'],
140
+ gemini: ['maxTokens', 'thinkingBudget', 'thinkingLevel', 'includeThoughts'],
141
+ openrouter: [
142
+ 'maxTokens',
143
+ 'reasoningEffort',
144
+ 'reasoningSummary',
145
+ 'routing',
146
+ 'fallbacks',
147
+ 'plugins',
148
+ 'serviceTier',
149
+ ],
150
+ };
151
+ /** Every knob some adapter reads. A key outside it is a connection field, not tuning. */
152
+ const TUNING = new Set(Object.values(KNOBS).flat());
153
+ function unreadKnobs(spec, target) {
154
+ const supported = KNOBS[target];
155
+ return Object.entries(spec)
156
+ .filter(([k, v]) => v !== undefined && TUNING.has(k) && !supported.includes(k))
157
+ .map(([knob]) => ({ knob, target, supported }));
158
+ }
159
+ // ---------------------------------------------------------------------------
117
160
  // Environment references
118
161
  // ---------------------------------------------------------------------------
119
162
  // `${VAR}` and `${VAR:-fallback}`. Chosen over a bare `env.VAR` token because a
@@ -225,6 +268,22 @@ export class ModelRegistry {
225
268
  this.#limiters.set(name, built);
226
269
  return built;
227
270
  }
271
+ /**
272
+ * Knobs a ref carries that its adapter will never read — the same check
273
+ * `model()` refuses on, minus the client, so a report can name them before
274
+ * a credential is resolved or anything is contacted.
275
+ */
276
+ knobs(ref) {
277
+ const spec = typeof ref === 'string' ? this.parse(ref) : ref;
278
+ const defaults = KINDS[this.kindOf(spec.provider ?? this.#default)];
279
+ if (defaults.protocol !== 'openai') {
280
+ return unreadKnobs(spec, defaults.protocol);
281
+ }
282
+ // An api this vendor does not speak already has an error of its own;
283
+ // answering about its knobs would only bury it.
284
+ const api = spec.api ?? defaults.apis[0];
285
+ return api && defaults.apis.includes(api) ? unreadKnobs(spec, `openai/${api}`) : [];
286
+ }
228
287
  /** Turns a shorthand or a spec into a `Model`. */
229
288
  model(ref) {
230
289
  const spec = typeof ref === 'string' ? this.parse(ref) : ref;
@@ -232,6 +291,13 @@ export class ModelRegistry {
232
291
  const provider = this.#spec(name);
233
292
  const kind = this.kindOf(name);
234
293
  const defaults = KINDS[kind];
294
+ // Before the client, so a knob for the wrong vendor is reported as the
295
+ // config mistake it is rather than behind a missing credential.
296
+ const [stray] = this.knobs(spec);
297
+ if (stray) {
298
+ throw new TypeError(`provider "${name}" (${kind}) does not read "${stray.knob}" — a ` +
299
+ `${stray.target} model takes: ${stray.supported.join(', ')}`);
300
+ }
235
301
  // Inline credentials opt out of the shared client: they describe a
236
302
  // different connection, and handing back the memoized one would
237
303
  // silently ignore them.
@@ -457,7 +523,7 @@ function buildClient(name, kind, creds, opts) {
457
523
  defaultHeaders: headers,
458
524
  timeout: opts.timeoutMs,
459
525
  maxRetries: retriesOf(opts),
460
- ...(opts.token ? { fetch: bearerFetch(opts.token) } : {}),
526
+ fetch: funded(opts.token ? bearerFetch(opts.token) : fetch),
461
527
  };
462
528
  if (defaults.protocol === 'anthropic') {
463
529
  const { Anthropic } = sdk('@anthropic-ai/sdk', kind);
@@ -608,6 +674,47 @@ function bearerFetch(token) {
608
674
  return fetch(input, { ...init, headers });
609
675
  };
610
676
  }
677
+ /**
678
+ * Tells the client not to wait out a 429 that no wait will clear.
679
+ *
680
+ * Both SDKs retry every 429, because a 429 is normally this minute's rate
681
+ * limit — but an exhausted balance answers with the same status, and the whole
682
+ * backoff schedule is then spent on a call that cannot succeed. The account is
683
+ * named in the body rather than the status, so the body is what decides, and
684
+ * `x-should-retry` is the SDKs' own override for exactly this.
685
+ */
686
+ function funded(inner) {
687
+ return async (input, init) => {
688
+ const res = await inner(input, init);
689
+ if (res.status !== 429) {
690
+ return res;
691
+ }
692
+ // A 429 body is a sentence long, and the client is about to read it
693
+ // anyway to build the error it throws.
694
+ const body = await res.text();
695
+ const headers = new Headers(res.headers);
696
+ // The transfer is already undone by the time a body is text.
697
+ headers.delete('content-encoding');
698
+ headers.delete('content-length');
699
+ if (unfunded(body)) {
700
+ headers.set('x-should-retry', 'false');
701
+ }
702
+ return new Response(body, { status: res.status, statusText: res.statusText, headers });
703
+ };
704
+ }
705
+ /** Whether a refusal body names the account rather than the rate. */
706
+ function unfunded(body) {
707
+ let parsed;
708
+ try {
709
+ parsed = JSON.parse(body);
710
+ }
711
+ catch {
712
+ return false;
713
+ }
714
+ const root = parsed;
715
+ const said = (root?.error ?? root);
716
+ return Boolean(said) && (unfundedSymbol(said?.code) || unfundedSymbol(said?.type));
717
+ }
611
718
  function expandHeaders(headers, where) {
612
719
  if (!headers) {
613
720
  return undefined;
@@ -193,7 +193,7 @@ const memoryBinding = z
193
193
  * lets it decide when to look.
194
194
  */
195
195
  autoRecall: z
196
- .union([z.boolean(), z.object({ limit: z.int().positive().default(5) }).strict()])
196
+ .union([z.boolean(), z.object({ limit: z.int().positive().default(10) }).strict()])
197
197
  .optional(),
198
198
  })
199
199
  .strict();
@@ -63,6 +63,13 @@ export interface ProjectOptions<TCtx = unknown> {
63
63
  * and writes to another.
64
64
  */
65
65
  memoryDir?: string;
66
+ /**
67
+ * Recall only: the graph is opened read-only and every agent's `access` is
68
+ * clamped to `read`, so the writing tools are never offered and the house
69
+ * rules never claim they are. It is what lets several runs share one
70
+ * warmed memory at once, which the directory lock otherwise forbids.
71
+ */
72
+ memoryReadOnly?: boolean;
66
73
  /** turns an agent-visible path into a host path, so files can be remembered */
67
74
  resolveFile?: (path: string) => string;
68
75
  payloads?: PayloadStore;
@@ -4,6 +4,7 @@ import { Agent, AgentRegistry } from "../agent.js";
4
4
  import { RunStream } from "../events.js";
5
5
  import { frontmatter, toList } from "../frontmatter.js";
6
6
  import { MemoryIndex } from "../memory/index.js";
7
+ import { DEFAULT_SEEDS } from "../memory/recall.js";
7
8
  import { MemoryStore } from "../memory/store.js";
8
9
  import { ModelRegistry, } from "../models/factory.js";
9
10
  import { promptFile } from "../prompt.js";
@@ -42,7 +43,10 @@ export function readProjectConfig(dir) {
42
43
  * error nobody can act on.
43
44
  */
44
45
  export async function loadProject(dir, opts = {}) {
45
- const { root, source, config } = readProjectConfig(dir);
46
+ const { root, source, config: declared } = readProjectConfig(dir);
47
+ // Clamped here rather than at each use, so the tools, the house rules and
48
+ // the report all read the same access and cannot disagree about it.
49
+ const config = opts.memoryReadOnly ? recallOnly(declared) : declared;
46
50
  // Read once and share the objects: every agent's prompt then reports the
47
51
  // same paths and the same content hashes, so the report says "one document,
48
52
  // five prompts" instead of showing five identical blobs.
@@ -73,7 +77,8 @@ export async function loadProject(dir, opts = {}) {
73
77
  // The default is resolved here rather than on first use, so a broken
74
78
  // `embedding:` fails at load like a broken `model:` does.
75
79
  embedders(config.embedding, 'embedding');
76
- const memory = opts.memory ?? (await openMemory(root, config, embedders, opts.memoryDir));
80
+ const memory = opts.memory ??
81
+ (await openMemory(root, config, embedders, opts.memoryDir, opts.memoryReadOnly));
77
82
  return new AgentProject({
78
83
  root,
79
84
  source,
@@ -240,10 +245,17 @@ const FILE_TOOLS = [
240
245
  'find_files',
241
246
  'write_file',
242
247
  'apply_patch',
248
+ 'copy_file',
243
249
  'move_file',
244
250
  'delete_file',
245
251
  ];
246
- const FILE_WRITE_TOOLS = new Set(['write_file', 'apply_patch', 'move_file', 'delete_file']);
252
+ const FILE_WRITE_TOOLS = new Set([
253
+ 'write_file',
254
+ 'apply_patch',
255
+ 'copy_file',
256
+ 'move_file',
257
+ 'delete_file',
258
+ ]);
247
259
  function activeFileTools(selectors) {
248
260
  const active = new Set();
249
261
  if (!selectors?.length) {
@@ -503,7 +515,7 @@ function memoryFor(spec) {
503
515
  sees: b.sees,
504
516
  writes: b.writes,
505
517
  autoRecall: auto
506
- ? { query: 'last_user_input', limit: auto === true ? 5 : auto.limit }
518
+ ? { query: 'last_user_input', limit: auto === true ? DEFAULT_SEEDS : auto.limit }
507
519
  : undefined,
508
520
  };
509
521
  }
@@ -527,13 +539,24 @@ export function memoryDir(root, config, override) {
527
539
  }
528
540
  return join(root, config.memory?.dir ?? 'memory');
529
541
  }
542
+ /**
543
+ * The same project with every memory binding demoted to `read`. A read-only
544
+ * graph refuses a commit, so an agent offered `memory_commit` would only find
545
+ * that out by having one fail mid-run.
546
+ */
547
+ function recallOnly(config) {
548
+ return {
549
+ ...config,
550
+ agents: config.agents.map((spec) => spec.memory ? { ...spec, memory: { ...spec.memory, access: 'read' } } : spec),
551
+ };
552
+ }
530
553
  /**
531
554
  * Opened only when something will use it, so an ordinary project neither
532
555
  * creates a directory nor takes a lock. A `memory:` block with no agent bound
533
556
  * to it still counts: it is how a project declares the graph `zen memory`
534
557
  * inspects between runs.
535
558
  */
536
- async function openMemory(root, config, embedders, override) {
559
+ async function openMemory(root, config, embedders, override, readOnly) {
537
560
  const dir = memoryDir(root, config, override);
538
561
  if (!dir) {
539
562
  return undefined;
@@ -541,7 +564,7 @@ async function openMemory(root, config, embedders, override) {
541
564
  const declared = config.memory;
542
565
  const embedder = embedders(declared?.embedding ?? config.embedding, 'memory.embedding');
543
566
  return new MemoryIndex({
544
- store: await MemoryStore.open(dir),
567
+ store: await MemoryStore.open(dir, { readOnly }),
545
568
  embedder,
546
569
  kinds: declared?.kinds,
547
570
  relations: declared?.relations,
package/dist/tools/exa.js CHANGED
@@ -41,7 +41,7 @@ const MAX_TIMEOUT_MS = 120_000;
41
41
  /**
42
42
  * A failure the model should read rather than one the run should die of.
43
43
  * Module-private: it never escapes a tool, which turns it into `{error, hint}`
44
- * the same way the workspace tools turn a bad patch into one.
44
+ * the same way the file tools turn a bad patch into one.
45
45
  */
46
46
  class ExaError extends Error {
47
47
  hint;
@@ -0,0 +1,81 @@
1
+ import { type AnyTool } from '../types.ts';
2
+ /** Where a project's `assets/` directory appears in the agent's namespace. */
3
+ export declare const ASSETS_MOUNT = "/assets";
4
+ /** ...and where its skill catalog does, so a skill's scripts can be run. */
5
+ export declare const SKILLS_MOUNT = "/skills";
6
+ /**
7
+ * A second tree the agent can reach, under a name of its own. Structurally the
8
+ * same as a `SandboxMount` on purpose: one directory is one mount, and the host
9
+ * hands the identical array to the file tools and to the container so that both
10
+ * call it by the same name.
11
+ */
12
+ export interface WorkspaceMount {
13
+ /** absolute host path; must already exist */
14
+ host: string;
15
+ /** absolute path the agent names it by, e.g. `/assets` */
16
+ at: string;
17
+ /** defaults to true — an extra tree is reference material unless said otherwise */
18
+ readOnly?: boolean;
19
+ }
20
+ export interface WorkspaceOptions {
21
+ root: string;
22
+ /** refuse every mutating tool; `zen run --read-only` */
23
+ readOnly?: boolean;
24
+ /**
25
+ * An absolute path the root is *also* known by, because something else has
26
+ * mounted it there — the sandbox bind-mounts the workspace at `/workspace`,
27
+ * so `run_command` returns compiler errors, stack traces and `pwd` output
28
+ * naming files under a prefix the file tools would otherwise refuse. Set
29
+ * it and both spellings reach the same file.
30
+ */
31
+ mount?: string;
32
+ /** further trees, each under its own name; read-only unless one says otherwise */
33
+ mounts?: readonly WorkspaceMount[];
34
+ }
35
+ export declare class Workspace {
36
+ #private;
37
+ readonly root: string;
38
+ readonly readOnly: boolean;
39
+ readonly mount: string | undefined;
40
+ /** the extra trees, resolved — the primary root is not among them */
41
+ readonly mounts: readonly Required<WorkspaceMount>[];
42
+ constructor(opts: WorkspaceOptions);
43
+ /**
44
+ * The one gate. Returns an absolute path inside one of the roots, or throws.
45
+ *
46
+ * A path that does not exist yet cannot be realpath'd, so the nearest
47
+ * existing ancestor is resolved instead and the remainder appended — which
48
+ * is exactly what a create needs and closes the same hole a create opens.
49
+ */
50
+ within(input: string, opts?: {
51
+ write?: boolean;
52
+ }): string;
53
+ /** Where a path sits under its own root, in posix form. `.` is that root. */
54
+ rel(path: string): string;
55
+ /**
56
+ * How a path is written back to the model, and the reason `mount` is not
57
+ * only about what comes in. A shell in the sandbox prints the mounted name
58
+ * — `find` returns /workspace/src/a.ts — so if the file tools answered with
59
+ * `src/a.ts` the model would be holding two vocabularies for one tree and
60
+ * would have to translate between them to feed one tool's output to the
61
+ * other. With a mount configured every path in and every path out is the
62
+ * mounted one, and copying a path from a command's output into read_file
63
+ * is exact. Without one, everything stays relative as before.
64
+ */
65
+ show(path: string): string;
66
+ /** Whether a resolved path *is* one of the roots. Nothing may move or delete one. */
67
+ isRoot(path: string): boolean;
68
+ mutable(): void;
69
+ }
70
+ export type FileFormat = 'text' | 'binary' | 'image' | 'audio' | 'video' | 'pdf' | 'archive';
71
+ export interface FileInfo {
72
+ name: string;
73
+ kind: 'file' | 'dir' | 'link' | 'other';
74
+ format?: FileFormat;
75
+ bytes?: number;
76
+ lines?: number;
77
+ }
78
+ export declare function fileTools<TCtx = unknown>(opts: WorkspaceOptions): AnyTool<TCtx>[];
79
+ /** @deprecated The group has been `files` for a while; this is the old spelling. */
80
+ export declare const workspaceTools: typeof fileTools;
81
+ //# sourceMappingURL=files.d.ts.map