@zenera/neo 1.1.0
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/LICENSE +21 -0
- package/README.md +178 -0
- package/dist/agent.d.ts +66 -0
- package/dist/agent.js +97 -0
- package/dist/architecture.d.ts +74 -0
- package/dist/architecture.js +105 -0
- package/dist/embedding.d.ts +60 -0
- package/dist/embedding.js +21 -0
- package/dist/embeddings/gemini.d.ts +24 -0
- package/dist/embeddings/gemini.js +71 -0
- package/dist/embeddings/index.d.ts +4 -0
- package/dist/embeddings/index.js +4 -0
- package/dist/embeddings/openai.d.ts +18 -0
- package/dist/embeddings/openai.js +27 -0
- package/dist/embeddings/openrouter.d.ts +21 -0
- package/dist/embeddings/openrouter.js +54 -0
- package/dist/events.d.ts +144 -0
- package/dist/events.js +110 -0
- package/dist/ids.d.ts +14 -0
- package/dist/ids.js +33 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +25 -0
- package/dist/inspect/client.d.ts +2 -0
- package/dist/inspect/client.js +1326 -0
- package/dist/inspect/css.d.ts +2 -0
- package/dist/inspect/css.js +164 -0
- package/dist/inspect/index.d.ts +9 -0
- package/dist/inspect/index.js +12 -0
- package/dist/inspect/page.d.ts +3 -0
- package/dist/inspect/page.js +93 -0
- package/dist/inspect/report.d.ts +54 -0
- package/dist/inspect/report.js +62 -0
- package/dist/kernel.d.ts +178 -0
- package/dist/kernel.js +929 -0
- package/dist/memory-stores/factory.d.ts +15 -0
- package/dist/memory-stores/factory.js +29 -0
- package/dist/memory-stores/file.d.ts +37 -0
- package/dist/memory-stores/file.js +174 -0
- package/dist/memory-stores/in-memory.d.ts +17 -0
- package/dist/memory-stores/in-memory.js +80 -0
- package/dist/memory-stores/index.d.ts +4 -0
- package/dist/memory-stores/index.js +5 -0
- package/dist/memory.d.ts +115 -0
- package/dist/memory.js +213 -0
- package/dist/model.d.ts +25 -0
- package/dist/model.js +2 -0
- package/dist/models/anthropic.d.ts +36 -0
- package/dist/models/anthropic.js +293 -0
- package/dist/models/factory.d.ts +202 -0
- package/dist/models/factory.js +533 -0
- package/dist/models/gemini.d.ts +45 -0
- package/dist/models/gemini.js +368 -0
- package/dist/models/index.d.ts +7 -0
- package/dist/models/index.js +7 -0
- package/dist/models/openai-chat.d.ts +19 -0
- package/dist/models/openai-chat.js +203 -0
- package/dist/models/openai-responses.d.ts +31 -0
- package/dist/models/openai-responses.js +258 -0
- package/dist/models/openrouter.d.ts +44 -0
- package/dist/models/openrouter.js +230 -0
- package/dist/payload-stores/factory.d.ts +15 -0
- package/dist/payload-stores/factory.js +30 -0
- package/dist/payload-stores/file.d.ts +29 -0
- package/dist/payload-stores/file.js +112 -0
- package/dist/payload-stores/in-memory.d.ts +17 -0
- package/dist/payload-stores/in-memory.js +40 -0
- package/dist/payload-stores/index.d.ts +4 -0
- package/dist/payload-stores/index.js +5 -0
- package/dist/payload.d.ts +89 -0
- package/dist/payload.js +181 -0
- package/dist/project/config.d.ts +302 -0
- package/dist/project/config.js +290 -0
- package/dist/project/index.d.ts +4 -0
- package/dist/project/index.js +4 -0
- package/dist/project/load.d.ts +142 -0
- package/dist/project/load.js +430 -0
- package/dist/project/refs.d.ts +23 -0
- package/dist/project/refs.js +70 -0
- package/dist/prompt.d.ts +66 -0
- package/dist/prompt.js +62 -0
- package/dist/runner.d.ts +103 -0
- package/dist/runner.js +537 -0
- package/dist/services.d.ts +23 -0
- package/dist/services.js +44 -0
- package/dist/skill-providers/factory.d.ts +16 -0
- package/dist/skill-providers/factory.js +26 -0
- package/dist/skill-providers/file.d.ts +44 -0
- package/dist/skill-providers/file.js +209 -0
- package/dist/skill-providers/index.d.ts +4 -0
- package/dist/skill-providers/index.js +5 -0
- package/dist/skill-providers/static.d.ts +13 -0
- package/dist/skill-providers/static.js +47 -0
- package/dist/skills.d.ts +102 -0
- package/dist/skills.js +232 -0
- package/dist/state.d.ts +86 -0
- package/dist/state.js +49 -0
- package/dist/tools/exa.d.ts +23 -0
- package/dist/tools/exa.js +389 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.js +6 -0
- package/dist/tools/sandbox.d.ts +172 -0
- package/dist/tools/sandbox.js +602 -0
- package/dist/tools/workspace.d.ts +53 -0
- package/dist/tools/workspace.js +1034 -0
- package/dist/trajectory.d.ts +213 -0
- package/dist/trajectory.js +232 -0
- package/dist/types.d.ts +195 -0
- package/dist/types.js +134 -0
- package/package.json +98 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { Agent, AgentRegistry } from '../agent.ts';
|
|
2
|
+
import type { Embedder } from '../embedding.ts';
|
|
3
|
+
import { RunStream } from '../events.ts';
|
|
4
|
+
import type { MemoryStore } from '../memory.ts';
|
|
5
|
+
import { ModelRegistry, type EmbeddingRef, type ModelRef, type ProviderSpec } from '../models/factory.ts';
|
|
6
|
+
import type { PayloadStore } from '../payload.ts';
|
|
7
|
+
import { AgentRunner, type RunOptions, type RunnerOptions } from '../runner.ts';
|
|
8
|
+
import type { SkillProvider } from '../skills.ts';
|
|
9
|
+
import { type AnyTool, type Input } from '../types.ts';
|
|
10
|
+
import { type ProjectConfig } from './config.ts';
|
|
11
|
+
/**
|
|
12
|
+
* A folder is an agentic system:
|
|
13
|
+
*
|
|
14
|
+
* ```
|
|
15
|
+
* INSTRUCTIONS.md house rules, prepended to every agent
|
|
16
|
+
* agents.yaml who exists, what they may reach for
|
|
17
|
+
* agents/prompts/<role>.md one agent's own instructions
|
|
18
|
+
* agents/skills/<name>/SKILL.md
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* The loader turns that into the objects an author would otherwise write by
|
|
22
|
+
* hand — it introduces no runtime concept of its own. Everything below is
|
|
23
|
+
* assembly, which is the point: a project is a *serialisation* of a registry,
|
|
24
|
+
* not a second way to run agents.
|
|
25
|
+
*/
|
|
26
|
+
export interface ProjectOptions<TCtx = unknown> {
|
|
27
|
+
/**
|
|
28
|
+
* Tool implementations, keyed by name. Config can name a tool; it cannot
|
|
29
|
+
* contain one, so this is the seam where code re-enters a declarative
|
|
30
|
+
* system. Both `agents[].tools` and a skill's `tools:` frontmatter resolve
|
|
31
|
+
* against it.
|
|
32
|
+
*/
|
|
33
|
+
tools?: AnyTool<TCtx>[];
|
|
34
|
+
/**
|
|
35
|
+
* Credentials for the names the config declares — or names it does not, so
|
|
36
|
+
* a deployment can add a provider the repository never mentions. Merged
|
|
37
|
+
* over `providers:`, host last, which is how ops repoints a key without a
|
|
38
|
+
* commit.
|
|
39
|
+
*/
|
|
40
|
+
providers?: Record<string, ProviderSpec>;
|
|
41
|
+
/**
|
|
42
|
+
* Aliases for `model:` values, so a project can say `model: fast` and the
|
|
43
|
+
* host decides what fast means this week. Merged over the config's
|
|
44
|
+
* `models:` map; a name in neither falls through to the shorthand.
|
|
45
|
+
*/
|
|
46
|
+
models?: Record<string, ModelRef>;
|
|
47
|
+
/** the same, for `embeddings:` — host last, for the same reason */
|
|
48
|
+
embeddings?: Record<string, EmbeddingRef>;
|
|
49
|
+
/**
|
|
50
|
+
* A registry to populate and use instead of a fresh one. Share it across
|
|
51
|
+
* projects and they share clients; pre-load it with a stub provider and the
|
|
52
|
+
* project never reaches the network.
|
|
53
|
+
*/
|
|
54
|
+
registry?: ModelRegistry;
|
|
55
|
+
memory?: MemoryStore[];
|
|
56
|
+
payloads?: PayloadStore;
|
|
57
|
+
/** extra providers merged with the ones the project declares */
|
|
58
|
+
skills?: SkillProvider[];
|
|
59
|
+
}
|
|
60
|
+
/** A project's declaration, before anything is assembled from it. */
|
|
61
|
+
export interface ProjectSource {
|
|
62
|
+
root: string;
|
|
63
|
+
/** the config file that was read */
|
|
64
|
+
source: string;
|
|
65
|
+
config: ProjectConfig;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Finds and validates `agents.yaml`, and stops there. Loading a project builds
|
|
69
|
+
* its model clients, which needs credentials — so anything that wants to *look*
|
|
70
|
+
* at what a project declares, credentials included, has to be able to read it
|
|
71
|
+
* without that.
|
|
72
|
+
*/
|
|
73
|
+
export declare function readProjectConfig(dir: string): ProjectSource;
|
|
74
|
+
/**
|
|
75
|
+
* Reads a project into memory. Every file it will ever need is read here, so a
|
|
76
|
+
* broken path, an unknown tool or a hand-off to nobody fails at startup with
|
|
77
|
+
* the offending key named — not on some later turn, in production, as a model
|
|
78
|
+
* error nobody can act on.
|
|
79
|
+
*/
|
|
80
|
+
export declare function loadProject<TCtx = unknown>(dir: string, opts?: ProjectOptions<TCtx>): Promise<AgentProject<TCtx>>;
|
|
81
|
+
interface ProjectParts<TCtx> {
|
|
82
|
+
root: string;
|
|
83
|
+
source: string;
|
|
84
|
+
config: ProjectConfig;
|
|
85
|
+
entry: string;
|
|
86
|
+
registry: AgentRegistry<TCtx>;
|
|
87
|
+
skillProviders: SkillProvider[];
|
|
88
|
+
models: ModelRegistry;
|
|
89
|
+
embedders: (ref: string | undefined, where: string) => Embedder | undefined;
|
|
90
|
+
options: ProjectOptions<TCtx>;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* A loaded project: immutable, and therefore shareable.
|
|
94
|
+
*
|
|
95
|
+
* Load once per process and let every chat use the same instance. Nothing
|
|
96
|
+
* per-conversation lives here — a conversation *is* its `AgentState`, and the
|
|
97
|
+
* caller's data reaches tools through `RunOptions.context`. Two chats holding
|
|
98
|
+
* this object cannot observe each other.
|
|
99
|
+
*
|
|
100
|
+
* There is a second, quieter payoff. Because tool schemas no longer change as
|
|
101
|
+
* skills activate, every chat on a shared project sends a byte-identical
|
|
102
|
+
* `[tool schemas][system prompt]` prefix. That is a provider cache hit *across*
|
|
103
|
+
* conversations, not merely within one.
|
|
104
|
+
*/
|
|
105
|
+
export declare class AgentProject<TCtx = unknown> {
|
|
106
|
+
#private;
|
|
107
|
+
readonly root: string;
|
|
108
|
+
/** the config file that was read, for error messages and reloads */
|
|
109
|
+
readonly source: string;
|
|
110
|
+
readonly config: ProjectConfig;
|
|
111
|
+
/** agent the project starts with */
|
|
112
|
+
readonly entry: string;
|
|
113
|
+
readonly registry: AgentRegistry<TCtx>;
|
|
114
|
+
readonly skillProviders: readonly SkillProvider[];
|
|
115
|
+
/** the providers this project declared, and the clients behind them */
|
|
116
|
+
readonly models: ModelRegistry;
|
|
117
|
+
constructor(parts: ProjectParts<TCtx>);
|
|
118
|
+
get agents(): readonly Agent<TCtx>[];
|
|
119
|
+
/**
|
|
120
|
+
* A declared vectoriser, memoized. Named or not: with no argument this is
|
|
121
|
+
* the project's `embedding:`, and `undefined` when it declares none.
|
|
122
|
+
*/
|
|
123
|
+
embedder(name?: string): Embedder | undefined;
|
|
124
|
+
/**
|
|
125
|
+
* The shared runner, or a fresh one when a caller needs different wiring.
|
|
126
|
+
*
|
|
127
|
+
* Memoized without overrides because a runner holds only immutable
|
|
128
|
+
* configuration; handing the same one to every chat is safe and saves
|
|
129
|
+
* re-registering the agents.
|
|
130
|
+
*/
|
|
131
|
+
runner(overrides?: RunnerOptions<TCtx>): AgentRunner<TCtx>;
|
|
132
|
+
/** Starts a run on the entry agent with the shared runner. */
|
|
133
|
+
run<T = string>(input?: Input, opts?: RunOptions<T, TCtx>): RunStream<T>;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Assembles the project's providers. Declaring one contacts nothing and reads
|
|
137
|
+
* no environment: a project may name a vendor a given deployment has no key
|
|
138
|
+
* for, and only pay for it if an agent actually reaches for it.
|
|
139
|
+
*/
|
|
140
|
+
export declare function projectRegistry<TCtx>(config: ProjectConfig, opts?: ProjectOptions<TCtx>): ModelRegistry;
|
|
141
|
+
export {};
|
|
142
|
+
//# sourceMappingURL=load.d.ts.map
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { Agent, AgentRegistry } from "../agent.js";
|
|
4
|
+
import { RunStream } from "../events.js";
|
|
5
|
+
import { ModelRegistry, } from "../models/factory.js";
|
|
6
|
+
import { promptFile } from "../prompt.js";
|
|
7
|
+
import { AgentRunner } from "../runner.js";
|
|
8
|
+
import { FileSkillProvider } from "../skill-providers/file.js";
|
|
9
|
+
import { selectTools } from "../types.js";
|
|
10
|
+
import { parseConfig } from "./config.js";
|
|
11
|
+
import { projectDir, projectFile, projectRoot } from "./refs.js";
|
|
12
|
+
const PROMPTS_DIR = 'agents/prompts';
|
|
13
|
+
const SKILLS_DIR = 'agents/skills';
|
|
14
|
+
const CONFIG_NAMES = ['agents.yaml', 'agents.yml', 'agents/agents.yaml', 'agents/agents.yml'];
|
|
15
|
+
// Deliberately not `AGENTS.md`: every coding assistant now reads that name out
|
|
16
|
+
// of an open folder, and `zen open` opens exactly this directory. The house
|
|
17
|
+
// rules here address *this project's* agents, not the editor's.
|
|
18
|
+
const HOUSE_RULES = 'INSTRUCTIONS.md';
|
|
19
|
+
/**
|
|
20
|
+
* Finds and validates `agents.yaml`, and stops there. Loading a project builds
|
|
21
|
+
* its model clients, which needs credentials — so anything that wants to *look*
|
|
22
|
+
* at what a project declares, credentials included, has to be able to read it
|
|
23
|
+
* without that.
|
|
24
|
+
*/
|
|
25
|
+
export function readProjectConfig(dir) {
|
|
26
|
+
const root = projectRoot(dir);
|
|
27
|
+
const source = findConfig(root);
|
|
28
|
+
return { root, source, config: parseConfig(readFileSync(source, 'utf8'), source) };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Reads a project into memory. Every file it will ever need is read here, so a
|
|
32
|
+
* broken path, an unknown tool or a hand-off to nobody fails at startup with
|
|
33
|
+
* the offending key named — not on some later turn, in production, as a model
|
|
34
|
+
* error nobody can act on.
|
|
35
|
+
*/
|
|
36
|
+
export async function loadProject(dir, opts = {}) {
|
|
37
|
+
const { root, source, config } = readProjectConfig(dir);
|
|
38
|
+
// Read once and share the object: every agent's prompt then reports the
|
|
39
|
+
// same path and the same content hash, so the report says "one document,
|
|
40
|
+
// five prompts" instead of showing five identical blobs.
|
|
41
|
+
const houseRules = readHouseRules(root);
|
|
42
|
+
const providers = [...skillProviders(root, config, opts), ...(opts.skills ?? [])];
|
|
43
|
+
const catalogs = await indexOf(providers);
|
|
44
|
+
const models = projectRegistry(config, opts);
|
|
45
|
+
const registry = new AgentRegistry();
|
|
46
|
+
const resolve = modelResolver(config, opts, models);
|
|
47
|
+
for (const spec of config.agents) {
|
|
48
|
+
registry.agent({
|
|
49
|
+
name: spec.name,
|
|
50
|
+
description: spec.description,
|
|
51
|
+
instructions: instructionsFor(root, spec, houseRules),
|
|
52
|
+
model: resolve(spec.model ?? config.model, `agents.${spec.name}.model`),
|
|
53
|
+
tools: toolsFor(spec, opts.tools ?? []),
|
|
54
|
+
handoffs: spec.handoffs,
|
|
55
|
+
skills: bindingFor(spec, providers),
|
|
56
|
+
fork: forkFor(spec),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
validate(config, registry, providers, catalogs, models);
|
|
60
|
+
const embedders = embedderResolver(config, opts, models);
|
|
61
|
+
// The default is resolved here rather than on first use, so a broken
|
|
62
|
+
// `embedding:` fails at load like a broken `model:` does.
|
|
63
|
+
embedders(config.embedding, 'embedding');
|
|
64
|
+
return new AgentProject({
|
|
65
|
+
root,
|
|
66
|
+
source,
|
|
67
|
+
config,
|
|
68
|
+
entry: entrypoint(config),
|
|
69
|
+
registry,
|
|
70
|
+
skillProviders: providers,
|
|
71
|
+
models,
|
|
72
|
+
embedders,
|
|
73
|
+
options: opts,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* A loaded project: immutable, and therefore shareable.
|
|
78
|
+
*
|
|
79
|
+
* Load once per process and let every chat use the same instance. Nothing
|
|
80
|
+
* per-conversation lives here — a conversation *is* its `AgentState`, and the
|
|
81
|
+
* caller's data reaches tools through `RunOptions.context`. Two chats holding
|
|
82
|
+
* this object cannot observe each other.
|
|
83
|
+
*
|
|
84
|
+
* There is a second, quieter payoff. Because tool schemas no longer change as
|
|
85
|
+
* skills activate, every chat on a shared project sends a byte-identical
|
|
86
|
+
* `[tool schemas][system prompt]` prefix. That is a provider cache hit *across*
|
|
87
|
+
* conversations, not merely within one.
|
|
88
|
+
*/
|
|
89
|
+
export class AgentProject {
|
|
90
|
+
root;
|
|
91
|
+
/** the config file that was read, for error messages and reloads */
|
|
92
|
+
source;
|
|
93
|
+
config;
|
|
94
|
+
/** agent the project starts with */
|
|
95
|
+
entry;
|
|
96
|
+
registry;
|
|
97
|
+
skillProviders;
|
|
98
|
+
/** the providers this project declared, and the clients behind them */
|
|
99
|
+
models;
|
|
100
|
+
#embedders;
|
|
101
|
+
#options;
|
|
102
|
+
#runner;
|
|
103
|
+
constructor(parts) {
|
|
104
|
+
this.root = parts.root;
|
|
105
|
+
this.source = parts.source;
|
|
106
|
+
this.config = parts.config;
|
|
107
|
+
this.entry = parts.entry;
|
|
108
|
+
this.registry = parts.registry;
|
|
109
|
+
this.skillProviders = parts.skillProviders;
|
|
110
|
+
this.models = parts.models;
|
|
111
|
+
this.#embedders = parts.embedders;
|
|
112
|
+
this.#options = parts.options;
|
|
113
|
+
}
|
|
114
|
+
get agents() {
|
|
115
|
+
return [...this.registry.agents.values()];
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* A declared vectoriser, memoized. Named or not: with no argument this is
|
|
119
|
+
* the project's `embedding:`, and `undefined` when it declares none.
|
|
120
|
+
*/
|
|
121
|
+
embedder(name) {
|
|
122
|
+
const ref = name ?? this.config.embedding;
|
|
123
|
+
return this.#embedders(ref, name ? `embeddings.${name}` : 'embedding');
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The shared runner, or a fresh one when a caller needs different wiring.
|
|
127
|
+
*
|
|
128
|
+
* Memoized without overrides because a runner holds only immutable
|
|
129
|
+
* configuration; handing the same one to every chat is safe and saves
|
|
130
|
+
* re-registering the agents.
|
|
131
|
+
*/
|
|
132
|
+
runner(overrides) {
|
|
133
|
+
if (overrides) {
|
|
134
|
+
return this.#build(overrides);
|
|
135
|
+
}
|
|
136
|
+
return (this.#runner ??= this.#build());
|
|
137
|
+
}
|
|
138
|
+
/** Starts a run on the entry agent with the shared runner. */
|
|
139
|
+
run(input, opts = {}) {
|
|
140
|
+
return this.runner().run(this.entry, input, opts);
|
|
141
|
+
}
|
|
142
|
+
#build(overrides = {}) {
|
|
143
|
+
const runner = new AgentRunner({
|
|
144
|
+
payloads: this.#options.payloads,
|
|
145
|
+
memory: this.#options.memory,
|
|
146
|
+
skills: [...this.skillProviders],
|
|
147
|
+
...overrides,
|
|
148
|
+
});
|
|
149
|
+
runner.add(...this.agents);
|
|
150
|
+
return runner;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Assembly
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
function findConfig(root) {
|
|
157
|
+
for (const name of CONFIG_NAMES) {
|
|
158
|
+
const path = join(root, name);
|
|
159
|
+
if (existsSync(path)) {
|
|
160
|
+
return path;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
throw new Error(`no project configuration in ${root} (looked for ${CONFIG_NAMES.join(', ')})`);
|
|
164
|
+
}
|
|
165
|
+
function readHouseRules(root) {
|
|
166
|
+
const path = join(root, HOUSE_RULES);
|
|
167
|
+
// Optional on purpose: a one-agent project whose whole prompt is its role
|
|
168
|
+
// file should not need a second, empty document.
|
|
169
|
+
return existsSync(path) ? promptFile(path, 'house_rules') : undefined;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* `INSTRUCTIONS.md` first, the agent's own file second — shared context before
|
|
173
|
+
* the specific job, and the stable half of the prompt in front where a cache
|
|
174
|
+
* can reuse it.
|
|
175
|
+
*/
|
|
176
|
+
function instructionsFor(root, spec, houseRules) {
|
|
177
|
+
const parts = [];
|
|
178
|
+
if (houseRules) {
|
|
179
|
+
parts.push(houseRules);
|
|
180
|
+
}
|
|
181
|
+
if (spec.system) {
|
|
182
|
+
parts.push(promptFile(projectFile(root, spec.system, `agents.${spec.name}.system`), 'role'));
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
const conventional = join(root, PROMPTS_DIR, `${spec.name}.md`);
|
|
186
|
+
if (existsSync(conventional)) {
|
|
187
|
+
parts.push(promptFile(conventional, 'role'));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return parts;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Assembles the project's providers. Declaring one contacts nothing and reads
|
|
194
|
+
* no environment: a project may name a vendor a given deployment has no key
|
|
195
|
+
* for, and only pay for it if an agent actually reaches for it.
|
|
196
|
+
*/
|
|
197
|
+
export function projectRegistry(config, opts = {}) {
|
|
198
|
+
const models = opts.registry ?? new ModelRegistry();
|
|
199
|
+
for (const [name, spec] of Object.entries(config.providers ?? {})) {
|
|
200
|
+
models.provider(name, spec);
|
|
201
|
+
}
|
|
202
|
+
// Host last: a deployment overrides the repository, never the other way.
|
|
203
|
+
for (const [name, spec] of Object.entries(opts.providers ?? {})) {
|
|
204
|
+
models.provider(name, spec);
|
|
205
|
+
}
|
|
206
|
+
if (config.provider) {
|
|
207
|
+
models.setDefault(config.provider);
|
|
208
|
+
}
|
|
209
|
+
return models;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* `model:` values, resolved through the alias tables and memoized.
|
|
213
|
+
*
|
|
214
|
+
* Memoized because two agents naming `fast` should be two references to one
|
|
215
|
+
* model over one client, not two of each — and because the error a bad ref
|
|
216
|
+
* raises should be raised once, at the agent that wrote it.
|
|
217
|
+
*/
|
|
218
|
+
function modelResolver(config, opts, models) {
|
|
219
|
+
const cache = new Map();
|
|
220
|
+
return (ref, where) => {
|
|
221
|
+
if (!ref) {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
const cached = cache.get(ref);
|
|
225
|
+
if (cached) {
|
|
226
|
+
return cached;
|
|
227
|
+
}
|
|
228
|
+
const resolved = opts.models?.[ref] ?? config.models?.[ref] ?? ref;
|
|
229
|
+
try {
|
|
230
|
+
// `reasoningEffort` is widened to `string` by the schema on purpose
|
|
231
|
+
// (see config.ts); the request is the authority on rejecting a bad
|
|
232
|
+
// one, so this cast hands it over rather than guessing the vendor's
|
|
233
|
+
// current set.
|
|
234
|
+
const model = models.model(resolved);
|
|
235
|
+
cache.set(ref, model);
|
|
236
|
+
return model;
|
|
237
|
+
}
|
|
238
|
+
catch (e) {
|
|
239
|
+
throw new Error(`${where}: ${e.message}`);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* `embeddings:` values, resolved the same way `model:` values are — through the
|
|
245
|
+
* alias tables, memoized, and with the offending key named when a ref will not
|
|
246
|
+
* resolve.
|
|
247
|
+
*/
|
|
248
|
+
function embedderResolver(config, opts, models) {
|
|
249
|
+
const cache = new Map();
|
|
250
|
+
return (ref, where) => {
|
|
251
|
+
if (!ref) {
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
const cached = cache.get(ref);
|
|
255
|
+
if (cached) {
|
|
256
|
+
return cached;
|
|
257
|
+
}
|
|
258
|
+
const resolved = opts.embeddings?.[ref] ?? config.embeddings?.[ref] ?? ref;
|
|
259
|
+
try {
|
|
260
|
+
// `routing` is widened to plain strings by the schema on purpose
|
|
261
|
+
// (see config.ts); the request is the authority on rejecting one.
|
|
262
|
+
const embedder = models.embedder(resolved);
|
|
263
|
+
cache.set(ref, embedder);
|
|
264
|
+
return embedder;
|
|
265
|
+
}
|
|
266
|
+
catch (e) {
|
|
267
|
+
throw new Error(`${where}: ${e.message}`);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function toolsFor(spec, available) {
|
|
272
|
+
if (!spec.tools?.length) {
|
|
273
|
+
return [];
|
|
274
|
+
}
|
|
275
|
+
return selectTools(available, spec.tools, {
|
|
276
|
+
where: `agents.${spec.name}.tools`,
|
|
277
|
+
hint: 'pass it in ProjectOptions.tools',
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
function bindingFor(spec, providers) {
|
|
281
|
+
if (!spec.skills) {
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
const provider = spec.skills.provider ?? providers[0]?.id;
|
|
285
|
+
if (!provider) {
|
|
286
|
+
throw new Error(`agents.${spec.name}.skills: no skill provider — add a top-level ` +
|
|
287
|
+
`\`skills:\` directory or pass one in ProjectOptions.skills`);
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
provider,
|
|
291
|
+
discovery: spec.skills.discovery,
|
|
292
|
+
allow: spec.skills.allow,
|
|
293
|
+
preload: spec.skills.preload,
|
|
294
|
+
maxIndexEntries: spec.skills.maxIndexEntries,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* `true` and `{}` both mean "opt in, no limits", because the kernel offers the
|
|
299
|
+
* tool on the binding's presence alone. `false` folds back to absent, so a
|
|
300
|
+
* project can write the "no" it was going to write anyway without that being a
|
|
301
|
+
* different state from omitting the key.
|
|
302
|
+
*/
|
|
303
|
+
function forkFor(spec) {
|
|
304
|
+
if (!spec.fork) {
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
307
|
+
return spec.fork === true ? {} : spec.fork;
|
|
308
|
+
}
|
|
309
|
+
function skillProviders(root, config, opts) {
|
|
310
|
+
const declared = config.skills
|
|
311
|
+
? Array.isArray(config.skills)
|
|
312
|
+
? config.skills
|
|
313
|
+
: [config.skills]
|
|
314
|
+
: existsSync(join(root, SKILLS_DIR))
|
|
315
|
+
? [SKILLS_DIR]
|
|
316
|
+
: [];
|
|
317
|
+
if (!declared.length) {
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
// One provider over several directories rather than several providers: a
|
|
321
|
+
// binding names exactly one, and "which folder is this skill in?" is not a
|
|
322
|
+
// question an agent author should have to answer.
|
|
323
|
+
return [
|
|
324
|
+
new FileSkillProvider({
|
|
325
|
+
id: 'project',
|
|
326
|
+
dir: declared.map((d, i) => projectDir(root, d, `skills[${i}]`)),
|
|
327
|
+
tools: opts.tools,
|
|
328
|
+
}),
|
|
329
|
+
];
|
|
330
|
+
}
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
// Validation
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
async function indexOf(providers) {
|
|
335
|
+
const out = new Map();
|
|
336
|
+
for (const p of providers) {
|
|
337
|
+
out.set(p.id, new Set((await p.list()).map((s) => s.name)));
|
|
338
|
+
}
|
|
339
|
+
return out;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Cross-references the config against what was actually loaded.
|
|
343
|
+
*
|
|
344
|
+
* The schema can only check shape. Whether `handoffs: [resolver]` names a real
|
|
345
|
+
* agent, or `preload: [house_style]` a real skill, is knowable only once
|
|
346
|
+
* everything is in memory — and getting it wrong is the failure that would
|
|
347
|
+
* otherwise surface as a confused model halfway through a run.
|
|
348
|
+
*/
|
|
349
|
+
function validate(config, registry, providers, catalogs, models) {
|
|
350
|
+
const ids = new Set(providers.map((p) => p.id));
|
|
351
|
+
// Provider *names* are checkable without credentials, so a typo in an entry
|
|
352
|
+
// no agent happens to use is still caught — unlike the key it would need.
|
|
353
|
+
for (const [alias, spec] of Object.entries(config.models ?? {})) {
|
|
354
|
+
const named = typeof spec === 'string' ? undefined : spec.provider;
|
|
355
|
+
if (named && !models.has(named)) {
|
|
356
|
+
throw new Error(`models.${alias}.provider: unknown provider "${named}" ` +
|
|
357
|
+
`(known: ${models.names().join(', ')})`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
for (const [alias, spec] of Object.entries(config.embeddings ?? {})) {
|
|
361
|
+
const named = typeof spec === 'string' ? undefined : spec.provider;
|
|
362
|
+
if (named && !models.has(named)) {
|
|
363
|
+
throw new Error(`embeddings.${alias}.provider: unknown provider "${named}" ` +
|
|
364
|
+
`(known: ${models.names().join(', ')})`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
for (const spec of config.agents) {
|
|
368
|
+
for (const target of spec.handoffs ?? []) {
|
|
369
|
+
if (!registry.find(target)) {
|
|
370
|
+
throw new Error(`agents.${spec.name}.handoffs: unknown agent "${target}" ` +
|
|
371
|
+
`(known: ${registry.names().join(', ')})`);
|
|
372
|
+
}
|
|
373
|
+
if (target === spec.name) {
|
|
374
|
+
throw new Error(`agents.${spec.name}.handoffs: an agent cannot hand off to itself`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
// Unlike a hand-off, naming yourself is the common case here — one role
|
|
378
|
+
// fanned out over ten regions — so only unknown names are an error.
|
|
379
|
+
for (const target of registry.get(spec.name).fork?.agents ?? []) {
|
|
380
|
+
if (!registry.find(target)) {
|
|
381
|
+
throw new Error(`agents.${spec.name}.fork.agents: unknown agent "${target}" ` +
|
|
382
|
+
`(known: ${registry.names().join(', ')})`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
const binding = registry.get(spec.name).skills;
|
|
386
|
+
if (!binding) {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (!ids.has(binding.provider)) {
|
|
390
|
+
throw new Error(`agents.${spec.name}.skills.provider: unknown provider "${binding.provider}" ` +
|
|
391
|
+
`(known: ${[...ids].join(', ') || 'none'})`);
|
|
392
|
+
}
|
|
393
|
+
const known = catalogs.get(binding.provider) ?? new Set();
|
|
394
|
+
for (const [key, names] of [
|
|
395
|
+
['allow', spec.skills?.allow],
|
|
396
|
+
['preload', spec.skills?.preload],
|
|
397
|
+
]) {
|
|
398
|
+
for (const skill of names ?? []) {
|
|
399
|
+
if (!known.has(skill)) {
|
|
400
|
+
throw new Error(`agents.${spec.name}.skills.${key}: unknown skill "${skill}" ` +
|
|
401
|
+
`(catalog "${binding.provider}" has: ${[...known].join(', ') || 'none'})`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
// A skill that is preloaded but not allowed would be activated by the
|
|
406
|
+
// runner and then hidden from the index — contradictory, and silent.
|
|
407
|
+
for (const skill of spec.skills?.preload ?? []) {
|
|
408
|
+
if (spec.skills?.allow && !spec.skills.allow.includes(skill)) {
|
|
409
|
+
throw new Error(`agents.${spec.name}.skills.preload: "${skill}" is not in \`allow\``);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
/** Explicit `default:` wins, then an agent that claims it, then the first declared. */
|
|
415
|
+
function entrypoint(config) {
|
|
416
|
+
if (config.default) {
|
|
417
|
+
const named = config.agents.find((a) => a.name === config.default);
|
|
418
|
+
if (!named) {
|
|
419
|
+
throw new Error(`default: unknown agent "${config.default}" ` +
|
|
420
|
+
`(declared: ${config.agents.map((a) => a.name).join(', ')})`);
|
|
421
|
+
}
|
|
422
|
+
return named.name;
|
|
423
|
+
}
|
|
424
|
+
const claimed = config.agents.filter((a) => a.default);
|
|
425
|
+
if (claimed.length > 1) {
|
|
426
|
+
throw new Error(`more than one agent claims \`default: true\`: ${claimed.map((a) => a.name).join(', ')}`);
|
|
427
|
+
}
|
|
428
|
+
return claimed[0]?.name ?? config.agents[0].name;
|
|
429
|
+
}
|
|
430
|
+
//# sourceMappingURL=load.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every path a project file names goes through here.
|
|
3
|
+
*
|
|
4
|
+
* A project is data someone else may have written, so `system: "../../.ssh/id_rsa"`
|
|
5
|
+
* has to be a load-time error rather than a prompt containing a private key.
|
|
6
|
+
* One chokepoint instead of a check at each call site, because the call sites
|
|
7
|
+
* are where such a check gets forgotten.
|
|
8
|
+
*
|
|
9
|
+
* `file://` is accepted for the spec's sake and normalised away immediately;
|
|
10
|
+
* everything else is read relative to the project root, never to `process.cwd()`,
|
|
11
|
+
* so a project loads the same from any working directory.
|
|
12
|
+
*/
|
|
13
|
+
export declare function projectPath(root: string, ref: string, what: string): string;
|
|
14
|
+
/** As `projectPath`, and the file must exist. */
|
|
15
|
+
export declare function projectFile(root: string, ref: string, what: string): string;
|
|
16
|
+
/** As `projectPath`, and the directory must exist. */
|
|
17
|
+
export declare function projectDir(root: string, ref: string, what: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* `root` itself, resolved and required to exist — the one path that cannot be
|
|
20
|
+
* checked against the root, since it is the root.
|
|
21
|
+
*/
|
|
22
|
+
export declare function projectRoot(dir: string): string;
|
|
23
|
+
//# sourceMappingURL=refs.d.ts.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { existsSync, statSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Path references
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
/**
|
|
8
|
+
* Every path a project file names goes through here.
|
|
9
|
+
*
|
|
10
|
+
* A project is data someone else may have written, so `system: "../../.ssh/id_rsa"`
|
|
11
|
+
* has to be a load-time error rather than a prompt containing a private key.
|
|
12
|
+
* One chokepoint instead of a check at each call site, because the call sites
|
|
13
|
+
* are where such a check gets forgotten.
|
|
14
|
+
*
|
|
15
|
+
* `file://` is accepted for the spec's sake and normalised away immediately;
|
|
16
|
+
* everything else is read relative to the project root, never to `process.cwd()`,
|
|
17
|
+
* so a project loads the same from any working directory.
|
|
18
|
+
*/
|
|
19
|
+
export function projectPath(root, ref, what) {
|
|
20
|
+
const path = resolve(root, unprefix(ref, what));
|
|
21
|
+
const rel = relative(root, path);
|
|
22
|
+
// `relative` gives '' for the root itself, and something starting with '..'
|
|
23
|
+
// for anything above it. An absolute result means a different drive.
|
|
24
|
+
if (rel.startsWith('..') || isAbsolute(rel)) {
|
|
25
|
+
throw new Error(`${what}: "${ref}" resolves outside the project root (${root})`);
|
|
26
|
+
}
|
|
27
|
+
return path;
|
|
28
|
+
}
|
|
29
|
+
/** As `projectPath`, and the file must exist. */
|
|
30
|
+
export function projectFile(root, ref, what) {
|
|
31
|
+
const path = projectPath(root, ref, what);
|
|
32
|
+
if (!existsSync(path) || !statSync(path).isFile()) {
|
|
33
|
+
throw new Error(`${what}: no such file: ${path}`);
|
|
34
|
+
}
|
|
35
|
+
return path;
|
|
36
|
+
}
|
|
37
|
+
/** As `projectPath`, and the directory must exist. */
|
|
38
|
+
export function projectDir(root, ref, what) {
|
|
39
|
+
const path = projectPath(root, ref, what);
|
|
40
|
+
if (!existsSync(path) || !statSync(path).isDirectory()) {
|
|
41
|
+
throw new Error(`${what}: no such directory: ${path}`);
|
|
42
|
+
}
|
|
43
|
+
return path;
|
|
44
|
+
}
|
|
45
|
+
function unprefix(ref, what) {
|
|
46
|
+
if (!ref.startsWith('file:')) {
|
|
47
|
+
return ref;
|
|
48
|
+
}
|
|
49
|
+
// `file:///abs/path` is a URL and has percent-encoding to undo; `file://x`
|
|
50
|
+
// names a remote host, which we cannot read and will not pretend to.
|
|
51
|
+
if (ref.startsWith('file:///')) {
|
|
52
|
+
return fileURLToPath(ref);
|
|
53
|
+
}
|
|
54
|
+
if (ref.startsWith('file://')) {
|
|
55
|
+
throw new Error(`${what}: "${ref}" names a host; only local paths are supported`);
|
|
56
|
+
}
|
|
57
|
+
return ref.slice('file:'.length);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* `root` itself, resolved and required to exist — the one path that cannot be
|
|
61
|
+
* checked against the root, since it is the root.
|
|
62
|
+
*/
|
|
63
|
+
export function projectRoot(dir) {
|
|
64
|
+
const path = resolve(dir);
|
|
65
|
+
if (!existsSync(path) || !statSync(path).isDirectory()) {
|
|
66
|
+
throw new Error(`project root is not a directory: ${path}`);
|
|
67
|
+
}
|
|
68
|
+
return path;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=refs.js.map
|