@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.
- package/dist/embeddings/rate-limit.d.ts +7 -0
- package/dist/embeddings/rate-limit.js +33 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/inspect/client.d.ts +1 -1
- package/dist/inspect/client.js +7 -2
- package/dist/lock.d.ts +27 -0
- package/dist/lock.js +66 -0
- package/dist/match.d.ts +44 -0
- package/dist/match.js +100 -0
- package/dist/memory/diff.d.ts +39 -0
- package/dist/memory/diff.js +95 -0
- package/dist/memory/files.d.ts +1 -1
- package/dist/memory/grep.d.ts +57 -0
- package/dist/memory/grep.js +182 -0
- package/dist/memory/index.d.ts +10 -2
- package/dist/memory/index.js +15 -4
- package/dist/memory/merge.js +1 -31
- package/dist/memory/recall.d.ts +1 -1
- package/dist/memory/recall.js +1 -1
- package/dist/memory/store.d.ts +8 -5
- package/dist/memory/store.js +26 -55
- package/dist/memory/tools.js +105 -9
- package/dist/memory/twin.d.ts +6 -0
- package/dist/memory/twin.js +40 -0
- package/dist/memory/types.d.ts +7 -2
- package/dist/models/factory.d.ts +67 -2
- package/dist/models/factory.js +256 -10
- package/dist/project/config.js +1 -1
- package/dist/project/load.d.ts +7 -0
- package/dist/project/load.js +29 -6
- package/dist/runner.js +7 -0
- 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 +162 -5
- 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/models/factory.js
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
-
(
|
|
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 (
|
|
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 (
|
|
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
|
|
260
|
-
|
|
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: ${
|
|
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
|
-
|
|
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;
|
package/dist/project/config.js
CHANGED
|
@@ -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(
|
|
196
|
+
.union([z.boolean(), z.object({ limit: z.int().positive().default(10) }).strict()])
|
|
197
197
|
.optional(),
|
|
198
198
|
})
|
|
199
199
|
.strict();
|
package/dist/project/load.d.ts
CHANGED
|
@@ -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;
|
package/dist/project/load.js
CHANGED
|
@@ -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 ??
|
|
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([
|
|
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 ?
|
|
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
|
|
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
|