@zenera/neo 1.1.23 → 1.1.25

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 (45) 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 +4 -0
  4. package/dist/index.js +4 -0
  5. package/dist/inspect/client.d.ts +1 -1
  6. package/dist/inspect/client.js +7 -2
  7. package/dist/lock.d.ts +27 -0
  8. package/dist/lock.js +66 -0
  9. package/dist/match.d.ts +44 -0
  10. package/dist/match.js +100 -0
  11. package/dist/memory/diff.d.ts +39 -0
  12. package/dist/memory/diff.js +95 -0
  13. package/dist/memory/files.d.ts +1 -1
  14. package/dist/memory/grep.d.ts +57 -0
  15. package/dist/memory/grep.js +182 -0
  16. package/dist/memory/index.d.ts +10 -2
  17. package/dist/memory/index.js +15 -4
  18. package/dist/memory/merge.js +1 -31
  19. package/dist/memory/recall.d.ts +1 -1
  20. package/dist/memory/recall.js +1 -1
  21. package/dist/memory/store.d.ts +8 -5
  22. package/dist/memory/store.js +26 -55
  23. package/dist/memory/tools.js +105 -9
  24. package/dist/memory/twin.d.ts +6 -0
  25. package/dist/memory/twin.js +40 -0
  26. package/dist/memory/types.d.ts +7 -2
  27. package/dist/models/factory.d.ts +67 -2
  28. package/dist/models/factory.js +256 -10
  29. package/dist/project/config.js +1 -1
  30. package/dist/project/load.d.ts +7 -0
  31. package/dist/project/load.js +29 -6
  32. package/dist/runner.js +7 -0
  33. package/dist/tools/exa.js +1 -1
  34. package/dist/tools/files.d.ts +81 -0
  35. package/dist/tools/files.js +1472 -0
  36. package/dist/tools/index.d.ts +1 -1
  37. package/dist/tools/index.js +1 -1
  38. package/dist/tools/sandbox.d.ts +44 -0
  39. package/dist/tools/sandbox.js +162 -5
  40. package/dist/tools/workspace.js +81 -3
  41. package/dist/trajectory.d.ts +19 -1
  42. package/dist/trajectory.js +53 -0
  43. package/dist/types.d.ts +1 -0
  44. package/dist/types.js +1 -0
  45. package/package.json +1 -1
@@ -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";
@@ -113,6 +114,62 @@ export const providerKinds = Object.keys(KINDS);
113
114
  export function isProviderKind(name) {
114
115
  return Object.hasOwn(KINDS, name);
115
116
  }
117
+ export function vertexRoute(model) {
118
+ if (/^(anthropic\/)?claude-/.test(model)) {
119
+ return 'anthropic';
120
+ }
121
+ return model.includes('/') ? 'openai' : 'gemini';
122
+ }
123
+ /** The protocol one model on one kind is spoken in. Only Vertex has more than one. */
124
+ function protocolOf(kind, model) {
125
+ return kind === 'vertex' ? vertexRoute(model) : KINDS[kind].protocol;
126
+ }
127
+ /** The OpenAI-compatible endpoint on Vertex has chat completions and nothing else. */
128
+ function apisOf(kind, protocol) {
129
+ return kind === 'vertex' && protocol === 'openai' ? ['chat'] : KINDS[kind].apis;
130
+ }
131
+ // ---------------------------------------------------------------------------
132
+ // Tuning knobs
133
+ // ---------------------------------------------------------------------------
134
+ /**
135
+ * Which knobs each adapter actually reads.
136
+ *
137
+ * `ModelSpec` unions every vendor's option interface, so `reasoningEffort` on a
138
+ * Gemini model is type-legal and the yaml schema accepts it too — and until
139
+ * this table it was dropped in silence, which is the worst outcome available:
140
+ * the config claims a setting is in force, the request never carries it, and
141
+ * the run looks healthy. No static type could have caught it either, because
142
+ * `provider` is a free name and its kind is not known until resolution.
143
+ *
144
+ * Translating instead was the alternative, and it is worse. The vendors do not
145
+ * mean the same thing by the same word: effort is a coarse level on OpenAI, a
146
+ * token budget on Anthropic, and *both* on Google depending on the model
147
+ * generation. A silent remap would make one line mean four things and bury the
148
+ * 2.5/3 split that `gemini.ts` deliberately refuses to paper over.
149
+ */
150
+ const KNOBS = {
151
+ 'openai/chat': ['reasoningEffort'],
152
+ 'openai/responses': ['reasoningEffort', 'reasoningSummary', 'store'],
153
+ anthropic: ['maxTokens', 'thinkingBudgetTokens'],
154
+ gemini: ['maxTokens', 'thinkingBudget', 'thinkingLevel', 'includeThoughts'],
155
+ openrouter: [
156
+ 'maxTokens',
157
+ 'reasoningEffort',
158
+ 'reasoningSummary',
159
+ 'routing',
160
+ 'fallbacks',
161
+ 'plugins',
162
+ 'serviceTier',
163
+ ],
164
+ };
165
+ /** Every knob some adapter reads. A key outside it is a connection field, not tuning. */
166
+ const TUNING = new Set(Object.values(KNOBS).flat());
167
+ function unreadKnobs(spec, target) {
168
+ const supported = KNOBS[target];
169
+ return Object.entries(spec)
170
+ .filter(([k, v]) => v !== undefined && TUNING.has(k) && !supported.includes(k))
171
+ .map(([knob]) => ({ knob, target, supported }));
172
+ }
116
173
  // ---------------------------------------------------------------------------
117
174
  // Environment references
118
175
  // ---------------------------------------------------------------------------
@@ -171,7 +228,11 @@ export class ModelRegistry {
171
228
  this.#providers.set(name, spec);
172
229
  // Re-declaring after a client was handed out would otherwise leave the
173
230
  // old credentials in service for the rest of the process.
174
- this.#clients.delete(name);
231
+ for (const key of this.#clients.keys()) {
232
+ if (key === name || key.startsWith(`${name}\0`)) {
233
+ this.#clients.delete(key);
234
+ }
235
+ }
175
236
  this.#limiters.delete(name);
176
237
  return this;
177
238
  }
@@ -204,6 +265,17 @@ export class ModelRegistry {
204
265
  this.#clients.set(name, built);
205
266
  return built;
206
267
  }
268
+ /** A Vertex provider's Claude or partner client: built once, beside its GenAI one. */
269
+ #partner(name, protocol, spec) {
270
+ const key = `${name}\0${protocol}`;
271
+ const existing = this.#clients.get(key);
272
+ if (existing) {
273
+ return existing;
274
+ }
275
+ const built = buildVertexPartner(name, protocol, spec);
276
+ this.#clients.set(key, built);
277
+ return built;
278
+ }
207
279
  /**
208
280
  * The pacing shared by every embedder on a provider, so that what one of
209
281
  * them learns from a 429 the others do not have to learn again — and so
@@ -225,41 +297,71 @@ export class ModelRegistry {
225
297
  this.#limiters.set(name, built);
226
298
  return built;
227
299
  }
300
+ /**
301
+ * Knobs a ref carries that its adapter will never read — the same check
302
+ * `model()` refuses on, minus the client, so a report can name them before
303
+ * a credential is resolved or anything is contacted.
304
+ */
305
+ knobs(ref) {
306
+ const spec = typeof ref === 'string' ? this.parse(ref) : ref;
307
+ const kind = this.kindOf(spec.provider ?? this.#default);
308
+ const protocol = protocolOf(kind, spec.model);
309
+ if (protocol !== 'openai') {
310
+ return unreadKnobs(spec, protocol);
311
+ }
312
+ // An api this vendor does not speak already has an error of its own;
313
+ // answering about its knobs would only bury it.
314
+ const apis = apisOf(kind, protocol);
315
+ const api = spec.api ?? apis[0];
316
+ return api && apis.includes(api) ? unreadKnobs(spec, `openai/${api}`) : [];
317
+ }
228
318
  /** Turns a shorthand or a spec into a `Model`. */
229
319
  model(ref) {
230
320
  const spec = typeof ref === 'string' ? this.parse(ref) : ref;
231
321
  const name = spec.provider ?? this.#default;
232
322
  const provider = this.#spec(name);
233
323
  const kind = this.kindOf(name);
234
- const defaults = KINDS[kind];
324
+ const protocol = protocolOf(kind, spec.model);
325
+ // Before the client, so a knob for the wrong vendor is reported as the
326
+ // config mistake it is rather than behind a missing credential.
327
+ const [stray] = this.knobs(spec);
328
+ if (stray) {
329
+ throw new TypeError(`provider "${name}" (${kind}) does not read "${stray.knob}" — a ` +
330
+ `${stray.target} model takes: ${stray.supported.join(', ')}`);
331
+ }
235
332
  // Inline credentials opt out of the shared client: they describe a
236
333
  // different connection, and handing back the memoized one would
237
334
  // silently ignore them.
238
335
  const client = spec.client ??
239
- (hasCredentials(spec) ? buildClient(name, kind, spec, provider) : this.client(name));
336
+ (protocol !== KINDS[kind].protocol
337
+ ? this.#partner(name, protocol, provider)
338
+ : hasCredentials(spec)
339
+ ? buildClient(name, kind, spec, provider)
340
+ : this.client(name));
240
341
  // The *resolved* name, not `spec.provider`: a bare ref carries none,
241
342
  // and a failure that cannot say which connection refused it is the
242
343
  // thing this is here to prevent.
243
344
  const options = { ...spec, provider: name };
244
345
  // Every protocol but OpenAI's has exactly one API, so naming one is a
245
346
  // mistake worth reporting rather than a field to ignore.
246
- if (defaults.protocol !== 'openai') {
347
+ if (protocol !== 'openai') {
247
348
  if (spec.api) {
248
349
  throw new TypeError(`provider "${name}" (${kind}) has one api, so "${spec.api}" means nothing here`);
249
350
  }
250
- switch (defaults.protocol) {
351
+ switch (protocol) {
251
352
  case 'anthropic':
252
- return new AnthropicModel(spec.model, client, options);
353
+ return new AnthropicModel(spec.model.replace(/^anthropic\//, ''), client, options);
253
354
  case 'gemini':
254
355
  return new GeminiModel(spec.model, client, options);
255
356
  case 'openrouter':
256
357
  return new OpenRouterModel(spec.model, client, options);
257
358
  }
258
359
  }
259
- const api = spec.api ?? defaults.apis[0];
260
- if (!defaults.apis.includes(api)) {
360
+ const apis = apisOf(kind, protocol);
361
+ const api = spec.api ?? apis[0];
362
+ if (!apis.includes(api)) {
261
363
  throw new TypeError(`provider "${name}" (${kind}) does not speak the "${api}" api ` +
262
- `(supported: ${defaults.apis.join(', ')})`);
364
+ `(supported: ${apis.join(', ')})`);
263
365
  }
264
366
  return api === 'responses'
265
367
  ? new OpenAIResponsesModel(spec.model, client, options)
@@ -457,7 +559,7 @@ function buildClient(name, kind, creds, opts) {
457
559
  defaultHeaders: headers,
458
560
  timeout: opts.timeoutMs,
459
561
  maxRetries: retriesOf(opts),
460
- ...(opts.token ? { fetch: bearerFetch(opts.token) } : {}),
562
+ fetch: funded(opts.token ? bearerFetch(opts.token) : fetch),
461
563
  };
462
564
  if (defaults.protocol === 'anthropic') {
463
565
  const { Anthropic } = sdk('@anthropic-ai/sdk', kind);
@@ -592,6 +694,109 @@ function projectFromKeyFile() {
592
694
  return undefined;
593
695
  }
594
696
  }
697
+ // ---------------------------------------------------------------------------
698
+ // Vertex partner models
699
+ // ---------------------------------------------------------------------------
700
+ /** Gemini has these; the partner endpoints do not, and answer them with a bare 404. */
701
+ const MULTI_REGIONS = new Set(['us', 'eu']);
702
+ /**
703
+ * Where a Vertex provider's partner models are served, and how to authenticate
704
+ * there. The GenAI SDK does both for Gemini; nothing does them for Claude or
705
+ * the OpenAI-compatible endpoint, so they are resolved here the same way:
706
+ * `project`, then GOOGLE_CLOUD_PROJECT, then the service-account file.
707
+ */
708
+ export function vertexEndpoint(spec = {}, name = 'vertex') {
709
+ const where = `provider "${name}"`;
710
+ const project = expand(spec.project, `${where}: project`) ??
711
+ fromEnv('GOOGLE_CLOUD_PROJECT') ??
712
+ projectFromKeyFile();
713
+ if (!project) {
714
+ throw new Error(`${where}: Claude and partner models on vertex need \`project\`, or ` +
715
+ `GOOGLE_CLOUD_PROJECT, and a service account — an express-mode key reaches Gemini only`);
716
+ }
717
+ const named = expand(spec.location, `${where}: location`) ?? fromEnv('GOOGLE_CLOUD_LOCATION') ?? 'global';
718
+ const location = MULTI_REGIONS.has(named) ? 'global' : named;
719
+ const host = location === 'global'
720
+ ? 'https://aiplatform.googleapis.com'
721
+ : `https://${location}-aiplatform.googleapis.com`;
722
+ const token = spec.token;
723
+ return {
724
+ project,
725
+ location,
726
+ base: `${host}/v1/projects/${project}/locations/${location}`,
727
+ token: token ? async () => token() : adcToken(),
728
+ };
729
+ }
730
+ /** Mints and refreshes tokens the way the GenAI SDK does for Gemini. */
731
+ function adcToken() {
732
+ let auth;
733
+ return async () => {
734
+ if (!auth) {
735
+ // `@google/genai`'s own dependency, so it is resolved from there rather than declared twice.
736
+ const genai = sdkPath('@google/genai', 'vertex');
737
+ const { GoogleAuth } = createRequire(genai)('google-auth-library');
738
+ auth = new GoogleAuth({ scopes: ['https://www.googleapis.com/auth/cloud-platform'] });
739
+ }
740
+ const token = await auth.getAccessToken();
741
+ if (!token) {
742
+ throw new Error('vertex: Application Default Credentials gave no access token');
743
+ }
744
+ return token;
745
+ };
746
+ }
747
+ function sdkPath(pkg, kind) {
748
+ try {
749
+ return requirePeer.resolve(pkg);
750
+ }
751
+ catch (cause) {
752
+ throw new Error(`provider kind "${kind}" needs ${pkg}, which is not installed — run: npm i ${pkg}`, { cause });
753
+ }
754
+ }
755
+ function buildVertexPartner(name, protocol, opts) {
756
+ const at = vertexEndpoint(opts, name);
757
+ const common = {
758
+ apiKey: OAUTH_PLACEHOLDER,
759
+ defaultHeaders: expandHeaders(opts.headers, `provider "${name}"`),
760
+ timeout: opts.timeoutMs,
761
+ maxRetries: retriesOf(opts),
762
+ };
763
+ if (protocol === 'anthropic') {
764
+ const { Anthropic } = sdk('@anthropic-ai/sdk', 'vertex');
765
+ return new Anthropic({
766
+ ...common,
767
+ baseURL: at.base,
768
+ fetch: funded(rawPredictFetch(at.base, at.token)),
769
+ });
770
+ }
771
+ const { OpenAI } = sdk('openai', 'vertex');
772
+ return new OpenAI({
773
+ ...common,
774
+ baseURL: `${at.base}/endpoints/openapi`,
775
+ fetch: funded(bearerFetch(at.token)),
776
+ });
777
+ }
778
+ /**
779
+ * Anthropic's client posts to `/v1/messages`; Vertex wants the model in the
780
+ * path and its own `anthropic_version` in the body instead.
781
+ */
782
+ function rawPredictFetch(base, token) {
783
+ const authed = bearerFetch(token);
784
+ return async (input, init) => {
785
+ const url = input instanceof Request ? input.url : String(input);
786
+ if (!new URL(url).pathname.endsWith('/v1/messages') || typeof init?.body !== 'string') {
787
+ return authed(input, init);
788
+ }
789
+ const { model, ...body } = JSON.parse(init.body);
790
+ const headers = new Headers(init.headers);
791
+ headers.delete('x-api-key');
792
+ const verb = body.stream ? 'streamRawPredict' : 'rawPredict';
793
+ return authed(`${base}/publishers/anthropic/models/${model}:${verb}`, {
794
+ ...init,
795
+ headers,
796
+ body: JSON.stringify({ ...body, anthropic_version: 'vertex-2023-10-16' }),
797
+ });
798
+ };
799
+ }
595
800
  // Both SDKs refuse to construct without a key. When a token callback owns the
596
801
  // Authorization header this value is never sent, so it only has to be non-empty.
597
802
  const OAUTH_PLACEHOLDER = 'oauth';
@@ -608,6 +813,47 @@ function bearerFetch(token) {
608
813
  return fetch(input, { ...init, headers });
609
814
  };
610
815
  }
816
+ /**
817
+ * Tells the client not to wait out a 429 that no wait will clear.
818
+ *
819
+ * Both SDKs retry every 429, because a 429 is normally this minute's rate
820
+ * limit — but an exhausted balance answers with the same status, and the whole
821
+ * backoff schedule is then spent on a call that cannot succeed. The account is
822
+ * named in the body rather than the status, so the body is what decides, and
823
+ * `x-should-retry` is the SDKs' own override for exactly this.
824
+ */
825
+ function funded(inner) {
826
+ return async (input, init) => {
827
+ const res = await inner(input, init);
828
+ if (res.status !== 429) {
829
+ return res;
830
+ }
831
+ // A 429 body is a sentence long, and the client is about to read it
832
+ // anyway to build the error it throws.
833
+ const body = await res.text();
834
+ const headers = new Headers(res.headers);
835
+ // The transfer is already undone by the time a body is text.
836
+ headers.delete('content-encoding');
837
+ headers.delete('content-length');
838
+ if (unfunded(body)) {
839
+ headers.set('x-should-retry', 'false');
840
+ }
841
+ return new Response(body, { status: res.status, statusText: res.statusText, headers });
842
+ };
843
+ }
844
+ /** Whether a refusal body names the account rather than the rate. */
845
+ function unfunded(body) {
846
+ let parsed;
847
+ try {
848
+ parsed = JSON.parse(body);
849
+ }
850
+ catch {
851
+ return false;
852
+ }
853
+ const root = parsed;
854
+ const said = (root?.error ?? root);
855
+ return Boolean(said) && (unfundedSymbol(said?.code) || unfundedSymbol(said?.type));
856
+ }
611
857
  function expandHeaders(headers, where) {
612
858
  if (!headers) {
613
859
  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/runner.js CHANGED
@@ -449,6 +449,13 @@ export class AgentRunner {
449
449
  catch (e) {
450
450
  return fail(`error: arguments are not valid JSON: ${String(e)}`);
451
451
  }
452
+ if (typeof args !== 'object' || args === null || Array.isArray(args)) {
453
+ return fail(`error: arguments to ${def.name} must be a JSON object`);
454
+ }
455
+ const missing = (def.parameters.required ?? []).filter((k) => args[k] == null);
456
+ if (missing.length) {
457
+ return fail(`error: ${def.name} is missing required arguments: ${missing.join(', ')}`);
458
+ }
452
459
  try {
453
460
  const out = await def.execute(args, {
454
461
  ctx: state.context,
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