@zenera/neo 1.1.0 → 1.1.3
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/README.md +0 -1
- package/dist/models/factory.d.ts +2 -1
- package/dist/models/factory.js +10 -2
- package/dist/project/config.d.ts +29 -0
- package/dist/project/config.js +59 -12
- package/dist/project/index.d.ts +2 -2
- package/dist/project/index.js +1 -1
- package/dist/project/load.d.ts +40 -0
- package/dist/project/load.js +67 -10
- package/dist/skill-providers/file.d.ts +20 -5
- package/dist/skill-providers/file.js +24 -27
- package/dist/skills.d.ts +8 -1
- package/dist/skills.js +11 -1
- package/dist/tools/sandbox.d.ts +11 -0
- package/dist/tools/sandbox.js +15 -0
- package/dist/tools/workspace.d.ts +29 -3
- package/dist/tools/workspace.js +253 -73
- package/dist/types.d.ts +4 -3
- package/dist/types.js +21 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -153,7 +153,6 @@ of surfacing three turns into a run as a confused model.
|
|
|
153
153
|
|
|
154
154
|
## Documentation
|
|
155
155
|
|
|
156
|
-
[DESIGN.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/DESIGN.md) ·
|
|
157
156
|
[docs/projects.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/projects.md) ·
|
|
158
157
|
[docs/agents-yaml.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/agents-yaml.md)
|
|
159
158
|
· and
|
package/dist/models/factory.d.ts
CHANGED
|
@@ -46,7 +46,8 @@ export interface ProviderSpec extends Credentials {
|
|
|
46
46
|
/**
|
|
47
47
|
* vertex only: GCP project id. Falls back to `GOOGLE_CLOUD_PROJECT`, then
|
|
48
48
|
* to the `project_id` inside the service-account key file named by
|
|
49
|
-
* `GOOGLE_APPLICATION_CREDENTIALS`.
|
|
49
|
+
* `GOOGLE_APPLICATION_CREDENTIALS`. Ignored when an `apiKey` is given —
|
|
50
|
+
* express mode addresses no project.
|
|
50
51
|
*/
|
|
51
52
|
project?: string;
|
|
52
53
|
/** vertex only: a region, or `global` (env: `GOOGLE_CLOUD_LOCATION`, default `global`) */
|
package/dist/models/factory.js
CHANGED
|
@@ -429,6 +429,12 @@ function buildOpenRouter(kind, apiKey, baseURL, headers, opts) {
|
|
|
429
429
|
* application-default login` — and refreshes them itself. That is why `vertex`
|
|
430
430
|
* is `keyOptional`, and why it needs none of the bearer-token machinery the
|
|
431
431
|
* OpenAI protocol uses for the same job.
|
|
432
|
+
*
|
|
433
|
+
* A key and a project are alternatives, not a pair. An express-mode key
|
|
434
|
+
* authenticates against `aiplatform.googleapis.com` with no project in the
|
|
435
|
+
* path; naming a project as well makes the SDK build a project-scoped url and
|
|
436
|
+
* then authenticate it with the key header — a combination the service refuses,
|
|
437
|
+
* and one nothing in the resulting 403 attributes to having set both.
|
|
432
438
|
*/
|
|
433
439
|
function buildGenAI(kind, apiKey, baseURL, headers, opts, where) {
|
|
434
440
|
const { GoogleGenAI } = sdk('@google/genai', kind);
|
|
@@ -436,16 +442,18 @@ function buildGenAI(kind, apiKey, baseURL, headers, opts, where) {
|
|
|
436
442
|
if (kind !== 'vertex') {
|
|
437
443
|
return new GoogleGenAI({ apiKey, httpOptions });
|
|
438
444
|
}
|
|
445
|
+
if (apiKey) {
|
|
446
|
+
return new GoogleGenAI({ vertexai: true, apiKey, httpOptions });
|
|
447
|
+
}
|
|
439
448
|
const project = expand(opts.project, `${where}: project`) ??
|
|
440
449
|
fromEnv('GOOGLE_CLOUD_PROJECT') ??
|
|
441
450
|
projectFromKeyFile();
|
|
442
|
-
if (!project
|
|
451
|
+
if (!project) {
|
|
443
452
|
throw new Error(`${where}: vertex needs \`project\`, or GOOGLE_CLOUD_PROJECT ` +
|
|
444
453
|
`(or \`apiKey\` for express mode)`);
|
|
445
454
|
}
|
|
446
455
|
return new GoogleGenAI({
|
|
447
456
|
vertexai: true,
|
|
448
|
-
apiKey,
|
|
449
457
|
project,
|
|
450
458
|
location: expand(opts.location, `${where}: location`) ??
|
|
451
459
|
fromEnv('GOOGLE_CLOUD_LOCATION') ??
|
package/dist/project/config.d.ts
CHANGED
|
@@ -112,8 +112,24 @@ declare const embeddingSpec: z.ZodObject<{
|
|
|
112
112
|
baseURL: z.ZodOptional<z.ZodString>;
|
|
113
113
|
baseURLEnv: z.ZodOptional<z.ZodString>;
|
|
114
114
|
}, z.core.$strict>;
|
|
115
|
+
/**
|
|
116
|
+
* A Dockerfile to build instead of an image to pull, for the ordinary case the
|
|
117
|
+
* `image:` key cannot serve: a project that needs two runtimes, or a pinned
|
|
118
|
+
* toolchain, and would otherwise have its agents reinstall it on every run.
|
|
119
|
+
*
|
|
120
|
+
* Building is a host concern, so nothing in this library does it — the front
|
|
121
|
+
* end resolves this to an image reference before a container is ever named.
|
|
122
|
+
*/
|
|
123
|
+
declare const build: z.ZodObject<{
|
|
124
|
+
dockerfile: z.ZodString;
|
|
125
|
+
context: z.ZodOptional<z.ZodString>;
|
|
126
|
+
}, z.core.$strict>;
|
|
115
127
|
declare const sandbox: z.ZodObject<{
|
|
116
128
|
image: z.ZodOptional<z.ZodString>;
|
|
129
|
+
build: z.ZodOptional<z.ZodObject<{
|
|
130
|
+
dockerfile: z.ZodString;
|
|
131
|
+
context: z.ZodOptional<z.ZodString>;
|
|
132
|
+
}, z.core.$strict>>;
|
|
117
133
|
cpus: z.ZodOptional<z.ZodNumber>;
|
|
118
134
|
memory: z.ZodOptional<z.ZodInt>;
|
|
119
135
|
network: z.ZodOptional<z.ZodEnum<{
|
|
@@ -126,6 +142,7 @@ declare const sandbox: z.ZodObject<{
|
|
|
126
142
|
user: z.ZodOptional<z.ZodString>;
|
|
127
143
|
persist: z.ZodOptional<z.ZodBoolean>;
|
|
128
144
|
env: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
145
|
+
keys: z.ZodOptional<z.ZodBoolean>;
|
|
129
146
|
}, z.core.$strict>;
|
|
130
147
|
export declare const projectSchema: z.ZodObject<{
|
|
131
148
|
version: z.ZodDefault<z.ZodLiteral<1>>;
|
|
@@ -230,8 +247,13 @@ export declare const projectSchema: z.ZodObject<{
|
|
|
230
247
|
}, z.core.$strict>]>>>;
|
|
231
248
|
embedding: z.ZodOptional<z.ZodString>;
|
|
232
249
|
skills: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
250
|
+
assets: z.ZodOptional<z.ZodString>;
|
|
233
251
|
sandbox: z.ZodOptional<z.ZodObject<{
|
|
234
252
|
image: z.ZodOptional<z.ZodString>;
|
|
253
|
+
build: z.ZodOptional<z.ZodObject<{
|
|
254
|
+
dockerfile: z.ZodString;
|
|
255
|
+
context: z.ZodOptional<z.ZodString>;
|
|
256
|
+
}, z.core.$strict>>;
|
|
235
257
|
cpus: z.ZodOptional<z.ZodNumber>;
|
|
236
258
|
memory: z.ZodOptional<z.ZodInt>;
|
|
237
259
|
network: z.ZodOptional<z.ZodEnum<{
|
|
@@ -244,6 +266,7 @@ export declare const projectSchema: z.ZodObject<{
|
|
|
244
266
|
user: z.ZodOptional<z.ZodString>;
|
|
245
267
|
persist: z.ZodOptional<z.ZodBoolean>;
|
|
246
268
|
env: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
269
|
+
keys: z.ZodOptional<z.ZodBoolean>;
|
|
247
270
|
}, z.core.$strict>>;
|
|
248
271
|
agents: z.ZodArray<z.ZodObject<{
|
|
249
272
|
name: z.ZodString;
|
|
@@ -269,6 +292,10 @@ export declare const projectSchema: z.ZodObject<{
|
|
|
269
292
|
}, z.core.$strict>]>>;
|
|
270
293
|
sandbox: z.ZodOptional<z.ZodObject<{
|
|
271
294
|
image: z.ZodOptional<z.ZodString>;
|
|
295
|
+
build: z.ZodOptional<z.ZodObject<{
|
|
296
|
+
dockerfile: z.ZodString;
|
|
297
|
+
context: z.ZodOptional<z.ZodString>;
|
|
298
|
+
}, z.core.$strict>>;
|
|
272
299
|
cpus: z.ZodOptional<z.ZodNumber>;
|
|
273
300
|
memory: z.ZodOptional<z.ZodInt>;
|
|
274
301
|
network: z.ZodOptional<z.ZodEnum<{
|
|
@@ -281,6 +308,7 @@ export declare const projectSchema: z.ZodObject<{
|
|
|
281
308
|
user: z.ZodOptional<z.ZodString>;
|
|
282
309
|
persist: z.ZodOptional<z.ZodBoolean>;
|
|
283
310
|
env: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
311
|
+
keys: z.ZodOptional<z.ZodBoolean>;
|
|
284
312
|
}, z.core.$strict>>;
|
|
285
313
|
default: z.ZodOptional<z.ZodBoolean>;
|
|
286
314
|
}, z.core.$strict>>;
|
|
@@ -291,6 +319,7 @@ export type ProviderConfig = z.infer<typeof provider>;
|
|
|
291
319
|
export type ModelConfig = z.infer<typeof modelSpec>;
|
|
292
320
|
export type EmbeddingConfig = z.infer<typeof embeddingSpec>;
|
|
293
321
|
export type SandboxConfig = z.infer<typeof sandbox>;
|
|
322
|
+
export type SandboxBuildConfig = z.infer<typeof build>;
|
|
294
323
|
/**
|
|
295
324
|
* Re-renders a zod failure as `agents.yaml: agents[1].skills.discovery — …`.
|
|
296
325
|
*
|
package/dist/project/config.js
CHANGED
|
@@ -165,15 +165,35 @@ const forkBinding = z
|
|
|
165
165
|
*
|
|
166
166
|
* `env` names host variables to forward, and only names: values never appear
|
|
167
167
|
* in the config, so a repository never carries one. Names that read like a
|
|
168
|
-
* credential are refused outright
|
|
169
|
-
*
|
|
170
|
-
*
|
|
168
|
+
* credential are refused outright — not because credentials never belong in
|
|
169
|
+
* the sandbox, but because `env` is the wrong door for them. Model keys go
|
|
170
|
+
* through `keys`, which forwards a known set by name and can be turned off in
|
|
171
|
+
* one place; a hand-written `env: [OPENAI_API_KEY]` would be a second, silent
|
|
172
|
+
* way in that no single switch could close.
|
|
171
173
|
*/
|
|
172
174
|
const SECRETISH = /(KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL)/i;
|
|
175
|
+
/**
|
|
176
|
+
* A Dockerfile to build instead of an image to pull, for the ordinary case the
|
|
177
|
+
* `image:` key cannot serve: a project that needs two runtimes, or a pinned
|
|
178
|
+
* toolchain, and would otherwise have its agents reinstall it on every run.
|
|
179
|
+
*
|
|
180
|
+
* Building is a host concern, so nothing in this library does it — the front
|
|
181
|
+
* end resolves this to an image reference before a container is ever named.
|
|
182
|
+
*/
|
|
183
|
+
const build = z
|
|
184
|
+
.object({
|
|
185
|
+
/** path to the Dockerfile, relative to the project root */
|
|
186
|
+
dockerfile: z.string().min(1),
|
|
187
|
+
/** what the build may COPY from; the Dockerfile's own directory by default */
|
|
188
|
+
context: z.string().min(1).optional(),
|
|
189
|
+
})
|
|
190
|
+
.strict();
|
|
173
191
|
const sandbox = z
|
|
174
192
|
.object({
|
|
175
193
|
/** the base image, e.g. `docker.io/library/python:3.13-slim` */
|
|
176
194
|
image: z.string().min(1).optional(),
|
|
195
|
+
/** a Dockerfile to build, instead of an image to pull */
|
|
196
|
+
build: build.optional(),
|
|
177
197
|
/** fractional cores the container may use */
|
|
178
198
|
cpus: z.number().positive().optional(),
|
|
179
199
|
/** MiB the container may use */
|
|
@@ -189,19 +209,40 @@ const sandbox = z
|
|
|
189
209
|
persist: z.boolean().optional(),
|
|
190
210
|
/** host variables to forward, by name */
|
|
191
211
|
env: z.array(z.string().min(1)).optional(),
|
|
212
|
+
/**
|
|
213
|
+
* Whether the model credentials reach the sandbox. On by default,
|
|
214
|
+
* because the scaffolded image installs `zen` itself and an agent that
|
|
215
|
+
* cannot run `zen models` inside its own sandbox is a surprise.
|
|
216
|
+
*
|
|
217
|
+
* Turning it off is the honest answer for a project whose agents write
|
|
218
|
+
* code they then execute: the container runs whatever the model wrote,
|
|
219
|
+
* so a forwarded key is a key that code can read and, on a `bridge`
|
|
220
|
+
* network, send somewhere.
|
|
221
|
+
*/
|
|
222
|
+
keys: z.boolean().optional(),
|
|
192
223
|
})
|
|
193
224
|
.strict()
|
|
194
225
|
.superRefine((value, ctx) => {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
});
|
|
203
|
-
}
|
|
226
|
+
if (value.image !== undefined && value.build !== undefined) {
|
|
227
|
+
ctx.addIssue({
|
|
228
|
+
code: 'custom',
|
|
229
|
+
path: ['build'],
|
|
230
|
+
message: 'image and build cannot both be set — a Dockerfile names its own base ' +
|
|
231
|
+
'in its FROM line, so one of the two would be silently ignored',
|
|
232
|
+
});
|
|
204
233
|
}
|
|
234
|
+
// Disable key blacklisting for now.
|
|
235
|
+
// for (const [i, name] of (value.env ?? []).entries()) {
|
|
236
|
+
// if (SECRETISH.test(name)) {
|
|
237
|
+
// ctx.addIssue({
|
|
238
|
+
// code: 'custom',
|
|
239
|
+
// path: ['env', i],
|
|
240
|
+
// message:
|
|
241
|
+
// `refusing to forward ${name} into the sandbox — it reads like a ` +
|
|
242
|
+
// 'credential, and the sandbox runs whatever the model writes',
|
|
243
|
+
// });
|
|
244
|
+
// }
|
|
245
|
+
// }
|
|
205
246
|
});
|
|
206
247
|
const agent = z
|
|
207
248
|
.object({
|
|
@@ -255,6 +296,12 @@ export const projectSchema = z
|
|
|
255
296
|
embedding: modelRef.optional(),
|
|
256
297
|
/** one directory, or several merged into one catalog */
|
|
257
298
|
skills: z.union([z.string().min(1), z.array(z.string().min(1))]).optional(),
|
|
299
|
+
/**
|
|
300
|
+
* Reference material every agent can read and none can write, mounted
|
|
301
|
+
* at /assets. One directory, project-wide: an agent that may only see
|
|
302
|
+
* some of it is a different project, not a different key.
|
|
303
|
+
*/
|
|
304
|
+
assets: z.string().min(1).optional(),
|
|
258
305
|
/** the container `run_command` and friends execute in */
|
|
259
306
|
sandbox: sandbox.optional(),
|
|
260
307
|
agents: z.array(agent).min(1),
|
package/dist/project/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { parseConfig, projectSchema, type AgentConfig, type EmbeddingConfig, type ModelConfig, type ProjectConfig, type ProviderConfig, type SandboxConfig, } from './config.ts';
|
|
2
|
-
export { AgentProject, loadProject, projectRegistry, readProjectConfig, type ProjectOptions, type ProjectSource, } from './load.ts';
|
|
1
|
+
export { parseConfig, projectSchema, type AgentConfig, type EmbeddingConfig, type ModelConfig, type ProjectConfig, type ProviderConfig, type SandboxBuildConfig, type SandboxConfig, } from './config.ts';
|
|
2
|
+
export { AgentProject, assetsDir, loadProject, projectRegistry, readProjectConfig, skillDirs, skillMounts, type ProjectOptions, type ProjectSource, } from './load.ts';
|
|
3
3
|
export { projectDir, projectFile, projectPath, projectRoot } from './refs.ts';
|
|
4
4
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/project/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { parseConfig, projectSchema, } from "./config.js";
|
|
2
|
-
export { AgentProject, loadProject, projectRegistry, readProjectConfig, } from "./load.js";
|
|
2
|
+
export { AgentProject, assetsDir, loadProject, projectRegistry, readProjectConfig, skillDirs, skillMounts, } from "./load.js";
|
|
3
3
|
export { projectDir, projectFile, projectPath, projectRoot } from "./refs.js";
|
|
4
4
|
//# sourceMappingURL=index.js.map
|
package/dist/project/load.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { MemoryStore } from '../memory.ts';
|
|
|
5
5
|
import { ModelRegistry, type EmbeddingRef, type ModelRef, type ProviderSpec } from '../models/factory.ts';
|
|
6
6
|
import type { PayloadStore } from '../payload.ts';
|
|
7
7
|
import { AgentRunner, type RunOptions, type RunnerOptions } from '../runner.ts';
|
|
8
|
+
import { type SkillDir } from '../skill-providers/file.ts';
|
|
8
9
|
import type { SkillProvider } from '../skills.ts';
|
|
9
10
|
import { type AnyTool, type Input } from '../types.ts';
|
|
10
11
|
import { type ProjectConfig } from './config.ts';
|
|
@@ -16,6 +17,7 @@ import { type ProjectConfig } from './config.ts';
|
|
|
16
17
|
* agents.yaml who exists, what they may reach for
|
|
17
18
|
* agents/prompts/<role>.md one agent's own instructions
|
|
18
19
|
* agents/skills/<name>/SKILL.md
|
|
20
|
+
* assets/ reference material, mounted read-only at /assets
|
|
19
21
|
* ```
|
|
20
22
|
*
|
|
21
23
|
* The loader turns that into the objects an author would otherwise write by
|
|
@@ -56,6 +58,16 @@ export interface ProjectOptions<TCtx = unknown> {
|
|
|
56
58
|
payloads?: PayloadStore;
|
|
57
59
|
/** extra providers merged with the ones the project declares */
|
|
58
60
|
skills?: SkillProvider[];
|
|
61
|
+
/**
|
|
62
|
+
* Where the host has mounted the skill catalog for the agent to reach —
|
|
63
|
+
* `/skills` from the CLI, which bind-mounts it into the container.
|
|
64
|
+
*
|
|
65
|
+
* Set it and a loaded skill carries the path of its own folder, so one that
|
|
66
|
+
* ships a script can say where the script is. Left unset, a skill is text
|
|
67
|
+
* and nothing claims otherwise: a promise of a directory that is not there
|
|
68
|
+
* is worse than no directory.
|
|
69
|
+
*/
|
|
70
|
+
skillsAt?: string;
|
|
59
71
|
}
|
|
60
72
|
/** A project's declaration, before anything is assembled from it. */
|
|
61
73
|
export interface ProjectSource {
|
|
@@ -85,6 +97,7 @@ interface ProjectParts<TCtx> {
|
|
|
85
97
|
entry: string;
|
|
86
98
|
registry: AgentRegistry<TCtx>;
|
|
87
99
|
skillProviders: SkillProvider[];
|
|
100
|
+
assets: string | undefined;
|
|
88
101
|
models: ModelRegistry;
|
|
89
102
|
embedders: (ref: string | undefined, where: string) => Embedder | undefined;
|
|
90
103
|
options: ProjectOptions<TCtx>;
|
|
@@ -112,6 +125,8 @@ export declare class AgentProject<TCtx = unknown> {
|
|
|
112
125
|
readonly entry: string;
|
|
113
126
|
readonly registry: AgentRegistry<TCtx>;
|
|
114
127
|
readonly skillProviders: readonly SkillProvider[];
|
|
128
|
+
/** reference material to mount read-only, if the project has any */
|
|
129
|
+
readonly assets: string | undefined;
|
|
115
130
|
/** the providers this project declared, and the clients behind them */
|
|
116
131
|
readonly models: ModelRegistry;
|
|
117
132
|
constructor(parts: ProjectParts<TCtx>);
|
|
@@ -138,5 +153,30 @@ export declare class AgentProject<TCtx = unknown> {
|
|
|
138
153
|
* for, and only pay for it if an agent actually reaches for it.
|
|
139
154
|
*/
|
|
140
155
|
export declare function projectRegistry<TCtx>(config: ProjectConfig, opts?: ProjectOptions<TCtx>): ModelRegistry;
|
|
156
|
+
/**
|
|
157
|
+
* Where each skill directory appears in the agent's namespace.
|
|
158
|
+
*
|
|
159
|
+
* One catalog is the whole of `/skills`, which is what a project normally has.
|
|
160
|
+
* Several cannot all be it, so each takes its own last segment underneath — and
|
|
161
|
+
* two directories ending in the same name would mean one hid the other, which
|
|
162
|
+
* is a load error rather than a catalog that silently went missing.
|
|
163
|
+
*/
|
|
164
|
+
export declare function skillMounts(dirs: readonly string[], at: string): SkillDir[];
|
|
165
|
+
/**
|
|
166
|
+
* The directories the skill catalog is built from, resolved. `skills:` if it is
|
|
167
|
+
* there, otherwise the conventional folder if it exists, otherwise none — a
|
|
168
|
+
* project without skills is a project, not an error.
|
|
169
|
+
*/
|
|
170
|
+
export declare function skillDirs(root: string, config: ProjectConfig): string[];
|
|
171
|
+
/**
|
|
172
|
+
* The project's reference material, resolved, or undefined if it has none.
|
|
173
|
+
*
|
|
174
|
+
* Convention first — `assets/` next to `agents.yaml` is enough, and a project
|
|
175
|
+
* that only wants somewhere to put a handbook writes no config at all. The key
|
|
176
|
+
* exists for the project that keeps its material elsewhere in the tree; a key
|
|
177
|
+
* naming a directory that is not there is a mistake in the file and fails at
|
|
178
|
+
* load, where the conventional folder merely being absent does not.
|
|
179
|
+
*/
|
|
180
|
+
export declare function assetsDir(root: string, config: ProjectConfig): string | undefined;
|
|
141
181
|
export {};
|
|
142
182
|
//# sourceMappingURL=load.d.ts.map
|
package/dist/project/load.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
1
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
3
|
import { Agent, AgentRegistry } from "../agent.js";
|
|
4
4
|
import { RunStream } from "../events.js";
|
|
5
5
|
import { ModelRegistry, } from "../models/factory.js";
|
|
@@ -11,6 +11,8 @@ import { parseConfig } from "./config.js";
|
|
|
11
11
|
import { projectDir, projectFile, projectRoot } from "./refs.js";
|
|
12
12
|
const PROMPTS_DIR = 'agents/prompts';
|
|
13
13
|
const SKILLS_DIR = 'agents/skills';
|
|
14
|
+
/** Reference material, by convention, when `assets:` says nothing. */
|
|
15
|
+
const ASSETS_DIR = 'assets';
|
|
14
16
|
const CONFIG_NAMES = ['agents.yaml', 'agents.yml', 'agents/agents.yaml', 'agents/agents.yml'];
|
|
15
17
|
// Deliberately not `AGENTS.md`: every coding assistant now reads that name out
|
|
16
18
|
// of an open folder, and `zen open` opens exactly this directory. The house
|
|
@@ -42,6 +44,9 @@ export async function loadProject(dir, opts = {}) {
|
|
|
42
44
|
const providers = [...skillProviders(root, config, opts), ...(opts.skills ?? [])];
|
|
43
45
|
const catalogs = await indexOf(providers);
|
|
44
46
|
const models = projectRegistry(config, opts);
|
|
47
|
+
// Resolved here rather than where it is mounted, so an `assets:` naming a
|
|
48
|
+
// directory that is not there fails at load like every other bad path.
|
|
49
|
+
const assets = assetsDir(root, config);
|
|
45
50
|
const registry = new AgentRegistry();
|
|
46
51
|
const resolve = modelResolver(config, opts, models);
|
|
47
52
|
for (const spec of config.agents) {
|
|
@@ -68,6 +73,7 @@ export async function loadProject(dir, opts = {}) {
|
|
|
68
73
|
entry: entrypoint(config),
|
|
69
74
|
registry,
|
|
70
75
|
skillProviders: providers,
|
|
76
|
+
assets,
|
|
71
77
|
models,
|
|
72
78
|
embedders,
|
|
73
79
|
options: opts,
|
|
@@ -95,6 +101,8 @@ export class AgentProject {
|
|
|
95
101
|
entry;
|
|
96
102
|
registry;
|
|
97
103
|
skillProviders;
|
|
104
|
+
/** reference material to mount read-only, if the project has any */
|
|
105
|
+
assets;
|
|
98
106
|
/** the providers this project declared, and the clients behind them */
|
|
99
107
|
models;
|
|
100
108
|
#embedders;
|
|
@@ -107,6 +115,7 @@ export class AgentProject {
|
|
|
107
115
|
this.entry = parts.entry;
|
|
108
116
|
this.registry = parts.registry;
|
|
109
117
|
this.skillProviders = parts.skillProviders;
|
|
118
|
+
this.assets = parts.assets;
|
|
110
119
|
this.models = parts.models;
|
|
111
120
|
this.#embedders = parts.embedders;
|
|
112
121
|
this.#options = parts.options;
|
|
@@ -307,13 +316,7 @@ function forkFor(spec) {
|
|
|
307
316
|
return spec.fork === true ? {} : spec.fork;
|
|
308
317
|
}
|
|
309
318
|
function skillProviders(root, config, opts) {
|
|
310
|
-
const declared = config
|
|
311
|
-
? Array.isArray(config.skills)
|
|
312
|
-
? config.skills
|
|
313
|
-
: [config.skills]
|
|
314
|
-
: existsSync(join(root, SKILLS_DIR))
|
|
315
|
-
? [SKILLS_DIR]
|
|
316
|
-
: [];
|
|
319
|
+
const declared = skillDirs(root, config);
|
|
317
320
|
if (!declared.length) {
|
|
318
321
|
return [];
|
|
319
322
|
}
|
|
@@ -323,11 +326,65 @@ function skillProviders(root, config, opts) {
|
|
|
323
326
|
return [
|
|
324
327
|
new FileSkillProvider({
|
|
325
328
|
id: 'project',
|
|
326
|
-
dir:
|
|
329
|
+
dir: opts.skillsAt ? skillMounts(declared, opts.skillsAt) : declared,
|
|
327
330
|
tools: opts.tools,
|
|
328
331
|
}),
|
|
329
332
|
];
|
|
330
333
|
}
|
|
334
|
+
/**
|
|
335
|
+
* Where each skill directory appears in the agent's namespace.
|
|
336
|
+
*
|
|
337
|
+
* One catalog is the whole of `/skills`, which is what a project normally has.
|
|
338
|
+
* Several cannot all be it, so each takes its own last segment underneath — and
|
|
339
|
+
* two directories ending in the same name would mean one hid the other, which
|
|
340
|
+
* is a load error rather than a catalog that silently went missing.
|
|
341
|
+
*/
|
|
342
|
+
export function skillMounts(dirs, at) {
|
|
343
|
+
if (dirs.length === 1) {
|
|
344
|
+
return [{ path: dirs[0], at }];
|
|
345
|
+
}
|
|
346
|
+
const taken = new Map();
|
|
347
|
+
return dirs.map((dir) => {
|
|
348
|
+
const name = basename(dir);
|
|
349
|
+
const clash = taken.get(name);
|
|
350
|
+
if (clash) {
|
|
351
|
+
throw new Error(`skills: ${dir} and ${clash} would both be mounted at ${at}/${name}`);
|
|
352
|
+
}
|
|
353
|
+
taken.set(name, dir);
|
|
354
|
+
return { path: dir, at: `${at}/${name}` };
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* The directories the skill catalog is built from, resolved. `skills:` if it is
|
|
359
|
+
* there, otherwise the conventional folder if it exists, otherwise none — a
|
|
360
|
+
* project without skills is a project, not an error.
|
|
361
|
+
*/
|
|
362
|
+
export function skillDirs(root, config) {
|
|
363
|
+
const declared = config.skills
|
|
364
|
+
? Array.isArray(config.skills)
|
|
365
|
+
? config.skills
|
|
366
|
+
: [config.skills]
|
|
367
|
+
: existsSync(join(root, SKILLS_DIR))
|
|
368
|
+
? [SKILLS_DIR]
|
|
369
|
+
: [];
|
|
370
|
+
return declared.map((d, i) => projectDir(root, d, `skills[${i}]`));
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* The project's reference material, resolved, or undefined if it has none.
|
|
374
|
+
*
|
|
375
|
+
* Convention first — `assets/` next to `agents.yaml` is enough, and a project
|
|
376
|
+
* that only wants somewhere to put a handbook writes no config at all. The key
|
|
377
|
+
* exists for the project that keeps its material elsewhere in the tree; a key
|
|
378
|
+
* naming a directory that is not there is a mistake in the file and fails at
|
|
379
|
+
* load, where the conventional folder merely being absent does not.
|
|
380
|
+
*/
|
|
381
|
+
export function assetsDir(root, config) {
|
|
382
|
+
if (config.assets) {
|
|
383
|
+
return projectDir(root, config.assets, 'assets');
|
|
384
|
+
}
|
|
385
|
+
const at = join(root, ASSETS_DIR);
|
|
386
|
+
return existsSync(at) && statSync(at).isDirectory() ? at : undefined;
|
|
387
|
+
}
|
|
331
388
|
// ---------------------------------------------------------------------------
|
|
332
389
|
// Validation
|
|
333
390
|
// ---------------------------------------------------------------------------
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import type { Skill, SkillProvider, SkillSummary } from '../skills.ts';
|
|
2
2
|
import { type AnyTool } from '../types.ts';
|
|
3
|
+
/**
|
|
4
|
+
* A directory of skills, and the name the *agent* reaches it by — which is not
|
|
5
|
+
* the host path when the folder is bind-mounted into a container. Give the
|
|
6
|
+
* plain string and a skill is text; give the pair and a skill that ships a
|
|
7
|
+
* script can say where the script is.
|
|
8
|
+
*/
|
|
9
|
+
export interface SkillDir {
|
|
10
|
+
path: string;
|
|
11
|
+
at: string;
|
|
12
|
+
}
|
|
3
13
|
export interface FileSkillProviderOptions {
|
|
4
|
-
/** one or more root directories holding `<name
|
|
5
|
-
dir: string | string[];
|
|
14
|
+
/** one or more root directories holding `<name>/SKILL.md` folders, or bare `<name>.md` files */
|
|
15
|
+
dir: string | SkillDir | (string | SkillDir)[];
|
|
6
16
|
/** logical provider id agents bind to; defaults to `file` */
|
|
7
17
|
id?: string;
|
|
8
18
|
/**
|
|
@@ -14,13 +24,18 @@ export interface FileSkillProviderOptions {
|
|
|
14
24
|
tools?: AnyTool<any>[];
|
|
15
25
|
}
|
|
16
26
|
/**
|
|
17
|
-
* Reads skills from disk.
|
|
27
|
+
* Reads skills from disk. A skill is a directory holding `SKILL.md`, and the
|
|
28
|
+
* directory is what lets it ship a rate table or a script beside its text:
|
|
18
29
|
*
|
|
19
30
|
* ```
|
|
20
|
-
* <dir>/budget_travel.md
|
|
21
|
-
* <dir>/budget_travel/
|
|
31
|
+
* <dir>/budget_travel/SKILL.md the instructions
|
|
32
|
+
* <dir>/budget_travel/cities.md whatever they refer to
|
|
22
33
|
* ```
|
|
23
34
|
*
|
|
35
|
+
* A bare `<dir>/budget_travel.md` is indexed too, for a host with nothing but
|
|
36
|
+
* prose to offer. `zen check` refuses it in a project, since a skill with no
|
|
37
|
+
* directory of its own can never grow one.
|
|
38
|
+
*
|
|
24
39
|
* Frontmatter is a small subset of YAML — `key: value`, plus `[a, b]` lists
|
|
25
40
|
* for `tags` and `tools`. `name` defaults to the file or folder name and
|
|
26
41
|
* `description` to the first line of the body, so the minimum viable skill is
|
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
import { readFile, readdir } from 'node:fs/promises';
|
|
2
|
-
import { join, resolve } from 'node:path';
|
|
2
|
+
import { basename, join, resolve } from 'node:path';
|
|
3
3
|
import { selectTools } from "../types.js";
|
|
4
4
|
// ---------------------------------------------------------------------------
|
|
5
5
|
// Filesystem-backed skill provider
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
7
7
|
const SKILL_FILE = 'SKILL.md';
|
|
8
8
|
/**
|
|
9
|
-
* Reads skills from disk.
|
|
9
|
+
* Reads skills from disk. A skill is a directory holding `SKILL.md`, and the
|
|
10
|
+
* directory is what lets it ship a rate table or a script beside its text:
|
|
10
11
|
*
|
|
11
12
|
* ```
|
|
12
|
-
* <dir>/budget_travel.md
|
|
13
|
-
* <dir>/budget_travel/
|
|
13
|
+
* <dir>/budget_travel/SKILL.md the instructions
|
|
14
|
+
* <dir>/budget_travel/cities.md whatever they refer to
|
|
14
15
|
* ```
|
|
15
16
|
*
|
|
17
|
+
* A bare `<dir>/budget_travel.md` is indexed too, for a host with nothing but
|
|
18
|
+
* prose to offer. `zen check` refuses it in a project, since a skill with no
|
|
19
|
+
* directory of its own can never grow one.
|
|
20
|
+
*
|
|
16
21
|
* Frontmatter is a small subset of YAML — `key: value`, plus `[a, b]` lists
|
|
17
22
|
* for `tags` and `tools`. `name` defaults to the file or folder name and
|
|
18
23
|
* `description` to the first line of the body, so the minimum viable skill is
|
|
@@ -26,7 +31,9 @@ export class FileSkillProvider {
|
|
|
26
31
|
constructor(opts) {
|
|
27
32
|
const o = typeof opts === 'string' ? { dir: opts } : opts;
|
|
28
33
|
const raw = Array.isArray(o.dir) ? o.dir : [o.dir];
|
|
29
|
-
this.#dirs = raw.map((d) =>
|
|
34
|
+
this.#dirs = raw.map((d) => typeof d === 'string'
|
|
35
|
+
? { path: resolve(d), at: '' }
|
|
36
|
+
: { path: resolve(d.path), at: d.at });
|
|
30
37
|
this.id = o.id ?? 'file';
|
|
31
38
|
for (const t of o.tools ?? []) {
|
|
32
39
|
this.#tools.set(t.name, t);
|
|
@@ -34,11 +41,11 @@ export class FileSkillProvider {
|
|
|
34
41
|
}
|
|
35
42
|
/** The first configured directory (kept for backwards compatibility). */
|
|
36
43
|
get dir() {
|
|
37
|
-
return this.#dirs[0];
|
|
44
|
+
return this.#dirs[0].path;
|
|
38
45
|
}
|
|
39
46
|
/** All configured directories. */
|
|
40
47
|
get dirs() {
|
|
41
|
-
return
|
|
48
|
+
return this.#dirs.map((d) => d.path);
|
|
42
49
|
}
|
|
43
50
|
/** Drops the cached scan; call after editing skills on disk. */
|
|
44
51
|
refresh() {
|
|
@@ -77,7 +84,7 @@ export class FileSkillProvider {
|
|
|
77
84
|
content: entry.content,
|
|
78
85
|
file: entry.file,
|
|
79
86
|
...(tools.length ? { tools } : {}),
|
|
80
|
-
...(entry.
|
|
87
|
+
...(entry.at ? { path: entry.at } : {}),
|
|
81
88
|
};
|
|
82
89
|
}
|
|
83
90
|
tool(name) {
|
|
@@ -93,7 +100,7 @@ export class FileSkillProvider {
|
|
|
93
100
|
for (const dir of this.#dirs) {
|
|
94
101
|
let items;
|
|
95
102
|
try {
|
|
96
|
-
items = await readdir(dir, { withFileTypes: true });
|
|
103
|
+
items = await readdir(dir.path, { withFileTypes: true });
|
|
97
104
|
}
|
|
98
105
|
catch (err) {
|
|
99
106
|
if (err.code === 'ENOENT') {
|
|
@@ -102,8 +109,8 @@ export class FileSkillProvider {
|
|
|
102
109
|
throw err;
|
|
103
110
|
}
|
|
104
111
|
for (const item of items) {
|
|
105
|
-
const folder = item.isDirectory() ? join(dir, item.name) : undefined;
|
|
106
|
-
const file = folder ? join(folder, SKILL_FILE) : join(dir, item.name);
|
|
112
|
+
const folder = item.isDirectory() ? join(dir.path, item.name) : undefined;
|
|
113
|
+
const file = folder ? join(folder, SKILL_FILE) : join(dir.path, item.name);
|
|
107
114
|
if (!folder && !(item.isFile() && item.name.endsWith('.md'))) {
|
|
108
115
|
continue;
|
|
109
116
|
}
|
|
@@ -112,20 +119,24 @@ export class FileSkillProvider {
|
|
|
112
119
|
continue;
|
|
113
120
|
}
|
|
114
121
|
const base = folder ? item.name : item.name.slice(0, -'.md'.length);
|
|
115
|
-
|
|
122
|
+
// Only a folder skill has files of its own to point at, and
|
|
123
|
+
// only when the host said where the directory can be reached.
|
|
124
|
+
const at = folder && dir.at ? `${dir.at}/${basename(folder)}` : undefined;
|
|
125
|
+
const entry = parse(raw, base, file, folder, at);
|
|
116
126
|
index.set(entry.summary.name, entry);
|
|
117
127
|
}
|
|
118
128
|
}
|
|
119
129
|
return index;
|
|
120
130
|
}
|
|
121
131
|
}
|
|
122
|
-
function parse(raw, base, file, folder) {
|
|
132
|
+
function parse(raw, base, file, folder, at) {
|
|
123
133
|
const { data, body } = frontmatter(raw);
|
|
124
134
|
const tags = toList(data.tags);
|
|
125
135
|
const tools = toList(data.tools);
|
|
126
136
|
return {
|
|
127
137
|
file,
|
|
128
138
|
folder,
|
|
139
|
+
...(at ? { at } : {}),
|
|
129
140
|
content: body,
|
|
130
141
|
summary: {
|
|
131
142
|
name: data.name || base,
|
|
@@ -136,20 +147,6 @@ function parse(raw, base, file, folder) {
|
|
|
136
147
|
},
|
|
137
148
|
};
|
|
138
149
|
}
|
|
139
|
-
/** Sibling files of a `SKILL.md`, top level only, keyed by filename. */
|
|
140
|
-
async function readResources(folder) {
|
|
141
|
-
const out = {};
|
|
142
|
-
for (const item of await readdir(folder, { withFileTypes: true })) {
|
|
143
|
-
if (!item.isFile() || item.name === SKILL_FILE) {
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
const value = await readOptional(join(folder, item.name));
|
|
147
|
-
if (value !== undefined) {
|
|
148
|
-
out[item.name] = value;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
return out;
|
|
152
|
-
}
|
|
153
150
|
async function readOptional(path) {
|
|
154
151
|
try {
|
|
155
152
|
return await readFile(path, 'utf8');
|
package/dist/skills.d.ts
CHANGED
|
@@ -23,7 +23,14 @@ export interface Skill extends SkillSummary {
|
|
|
23
23
|
content: string;
|
|
24
24
|
/** tools unlocked while the skill is active; the context type is the host agent's */
|
|
25
25
|
tools?: AnyTool<any>[];
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Where the skill's own files are, *under a name the agent can use* — not a
|
|
28
|
+
* host path. A skill that ships a script is instructions plus that script,
|
|
29
|
+
* and its text says "run scripts/fill.py" without saying where `scripts/`
|
|
30
|
+
* is; this is the answer, and it is only set when the host has actually put
|
|
31
|
+
* the folder somewhere the agent can reach.
|
|
32
|
+
*/
|
|
33
|
+
path?: string;
|
|
27
34
|
/** absolute path of the source file, if known (set by file-backed providers) */
|
|
28
35
|
file?: string;
|
|
29
36
|
}
|