@zenera/neo 1.1.23 → 1.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/embeddings/rate-limit.d.ts +7 -0
- package/dist/embeddings/rate-limit.js +33 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/inspect/client.d.ts +1 -1
- package/dist/inspect/client.js +7 -2
- 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/match.d.ts +18 -0
- package/dist/memory/match.js +61 -0
- package/dist/memory/recall.d.ts +1 -1
- package/dist/memory/recall.js +1 -1
- package/dist/memory/store.d.ts +7 -0
- package/dist/memory/store.js +22 -4
- package/dist/memory/tools.js +105 -9
- package/dist/memory/types.d.ts +7 -2
- package/dist/models/factory.d.ts +43 -2
- package/dist/models/factory.js +108 -1
- package/dist/project/config.js +1 -1
- package/dist/project/load.d.ts +7 -0
- package/dist/project/load.js +29 -6
- package/dist/tools/exa.js +1 -1
- package/dist/tools/files.d.ts +81 -0
- package/dist/tools/files.js +1472 -0
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +1 -1
- package/dist/tools/sandbox.d.ts +44 -0
- package/dist/tools/sandbox.js +122 -3
- package/dist/tools/workspace.js +81 -3
- package/dist/trajectory.d.ts +19 -1
- package/dist/trajectory.js +53 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.js +1 -0
- package/package.json +1 -1
package/dist/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";
|
|
@@ -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
|
-
|
|
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;
|
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/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
|