@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andrey Ryabov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# @zenera/neo
|
|
2
|
+
|
|
3
|
+
**A multi-agent runtime for Node.js: agents, models, tools, skills, memory and
|
|
4
|
+
an append-only trajectory.**
|
|
5
|
+
|
|
6
|
+
[](https://www.npmjs.com/package/@zenera/neo)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](https://nodejs.org)
|
|
9
|
+
|
|
10
|
+
> Part of [ZeneraNeo](https://github.com/andreyryabov/ZeneraNeo). This is an
|
|
11
|
+
> open-source side project for experimentation and chore work — **not** the
|
|
12
|
+
> official Zenera AI Platform. It carries no support or stability promises.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
Node.js 24+, ESM only. Runtime dependencies are `yaml` and `zod` and nothing
|
|
17
|
+
else; the four vendor SDKs are **optional peer dependencies**, not loaded until
|
|
18
|
+
a client for that vendor is first built, so an OpenAI-only application never
|
|
19
|
+
pays for the others.
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm i @zenera/neo openai
|
|
23
|
+
npm i @zenera/neo @anthropic-ai/sdk
|
|
24
|
+
npm i @zenera/neo @google/genai
|
|
25
|
+
npm i @zenera/neo @openrouter/sdk
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Use
|
|
29
|
+
|
|
30
|
+
There are two ways in, and they meet in the middle: declare the agents in code,
|
|
31
|
+
or load them from a folder.
|
|
32
|
+
|
|
33
|
+
### In code
|
|
34
|
+
|
|
35
|
+
An agent is an instruction, a model, some tools, and who it may hand work to.
|
|
36
|
+
That is the whole list — there is no orchestration layer underneath.
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { AgentRunner, createModel, tool } from '@zenera/neo';
|
|
40
|
+
|
|
41
|
+
const getWeather = tool<{ city: string }>({
|
|
42
|
+
name: 'get_weather',
|
|
43
|
+
description: 'Current weather for a city.',
|
|
44
|
+
parameters: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
properties: { city: { type: 'string' } },
|
|
47
|
+
required: ['city'],
|
|
48
|
+
additionalProperties: false,
|
|
49
|
+
},
|
|
50
|
+
execute: ({ city }) => ({ city, tempC: 21, sky: 'clear' }),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const runner = new AgentRunner({ model: createModel('openai:gpt-5.4-mini') });
|
|
54
|
+
|
|
55
|
+
const planner = runner.agent({
|
|
56
|
+
name: 'planner',
|
|
57
|
+
description: 'Turns a request into a concrete day-by-day plan.',
|
|
58
|
+
instructions: 'Answer with a plan. Check the weather before you commit to a day.',
|
|
59
|
+
tools: [getWeather],
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const result = await runner.run(planner, 'Two days in Rome, outdoors where possible.');
|
|
63
|
+
console.log(result.output);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`run()` returns a handle: `await` it for the answer, or iterate it for the
|
|
67
|
+
stream — token deltas plus a checkpoint for every model call, tool call,
|
|
68
|
+
handoff, fork and join.
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
for await (const ev of runner.run(planner, 'Two days in Rome.')) {
|
|
72
|
+
if (ev.type === 'text_delta') process.stdout.write(ev.delta);
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
A second agent and a handoff is one more `runner.agent({ … })` plus
|
|
77
|
+
`handoffs: [planner]` on the first. Skills, memory scopes and payload stores are
|
|
78
|
+
declared once on the runner and referenced by id from each agent allowed to use
|
|
79
|
+
them.
|
|
80
|
+
|
|
81
|
+
### From a folder
|
|
82
|
+
|
|
83
|
+
An agentic project is a folder — prompts, agent wiring, skills and tool
|
|
84
|
+
selections are Markdown and YAML files, not code buried inside an application.
|
|
85
|
+
Load one and run it:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { loadProject } from '@zenera/neo';
|
|
89
|
+
|
|
90
|
+
const project = await loadProject('./my-project', { tools: [lookupPolicy] });
|
|
91
|
+
|
|
92
|
+
for await (const ev of project.run('Water damage, policy NM-448127.')) {
|
|
93
|
+
// stream events: thinking, text, tool calls, handoffs, usage
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The whole system is an `agents.yaml`:
|
|
98
|
+
|
|
99
|
+
```yaml
|
|
100
|
+
default: intake
|
|
101
|
+
model: anthropic:claude-sonnet-4-5
|
|
102
|
+
skills: agents/skills
|
|
103
|
+
|
|
104
|
+
agents:
|
|
105
|
+
- name: intake
|
|
106
|
+
description: Takes the first message and routes the case.
|
|
107
|
+
system: agents/prompts/intake.md
|
|
108
|
+
tools: [policy_lookup]
|
|
109
|
+
handoffs: [adjuster]
|
|
110
|
+
|
|
111
|
+
- name: adjuster
|
|
112
|
+
description: Weighs the written policy against the case and explains the outcome.
|
|
113
|
+
system: agents/prompts/adjuster.md
|
|
114
|
+
model: openai:gpt-5.4
|
|
115
|
+
tools: [workspace:*, sandbox:*]
|
|
116
|
+
skills:
|
|
117
|
+
discovery: index # the model loads what the case needs
|
|
118
|
+
preload: [house_style] # always on, from turn one
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
It is validated strictly at load: an unknown tool, a handoff to nobody, a
|
|
122
|
+
missing prompt file — each fails immediately, naming the offending key, instead
|
|
123
|
+
of surfacing three turns into a run as a confused model.
|
|
124
|
+
|
|
125
|
+
## What it gives you
|
|
126
|
+
|
|
127
|
+
- **Agents** — an instruction, a model, tools, skills, plus handoffs and fork.
|
|
128
|
+
There is no hidden orchestration layer.
|
|
129
|
+
- **Models** — OpenAI, Anthropic, Google/Vertex and OpenRouter behind one
|
|
130
|
+
interface, each through its own SDK, plus any OpenAI-compatible endpoint.
|
|
131
|
+
Named providers and model aliases resolve from config or from the host.
|
|
132
|
+
- **Tools** — plain typed functions, selectable per agent by name, group or
|
|
133
|
+
wildcard. Workspace file tools and a Podman sandbox ship with the library.
|
|
134
|
+
- **Skills** — instruction bundles discovered and loaded on demand instead of
|
|
135
|
+
permanently occupying the prompt, optionally owning their own tools.
|
|
136
|
+
- **Memory** — pluggable `MemoryStore`s, with scopes deciding what is private to
|
|
137
|
+
an agent and what is shared.
|
|
138
|
+
- **Trajectory** — an append-only log of everything that happened. Provider
|
|
139
|
+
messages are a projection of it, and compaction _covers_ nodes rather than
|
|
140
|
+
deleting them, so the audit trail survives context pressure.
|
|
141
|
+
- **Inspection** — `renderReportHtml` turns any run into one self-contained HTML
|
|
142
|
+
file: the graph, every request, every token.
|
|
143
|
+
|
|
144
|
+
## Entry points
|
|
145
|
+
|
|
146
|
+
| Specifier | Contents |
|
|
147
|
+
| ----------------------------- | ----------------------------------------- |
|
|
148
|
+
| `@zenera/neo` | agents, models, tools, kernel, inspection |
|
|
149
|
+
| `@zenera/neo/project` | loading a project folder |
|
|
150
|
+
| `@zenera/neo/skill-providers` | skill discovery and loading |
|
|
151
|
+
| `@zenera/neo/memory-stores` | `MemoryStore` implementations |
|
|
152
|
+
| `@zenera/neo/payload-stores` | payload persistence |
|
|
153
|
+
|
|
154
|
+
## Documentation
|
|
155
|
+
|
|
156
|
+
[DESIGN.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/DESIGN.md) ·
|
|
157
|
+
[docs/projects.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/projects.md) ·
|
|
158
|
+
[docs/agents-yaml.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/agents-yaml.md)
|
|
159
|
+
· and
|
|
160
|
+
[`examples/`](https://github.com/andreyryabov/ZeneraNeo/tree/main/examples) for
|
|
161
|
+
eight worked demos, from a single agent to a project loaded entirely from a
|
|
162
|
+
folder.
|
|
163
|
+
|
|
164
|
+
## The rest of the family
|
|
165
|
+
|
|
166
|
+
| Package | What it is |
|
|
167
|
+
| ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
|
|
168
|
+
| [`@zenera/cli`](https://github.com/andreyryabov/ZeneraNeo/blob/main/packages/cli/README.md) | `zen` — this runtime on the command line, with a TUI |
|
|
169
|
+
| [`@zenera/faker`](https://github.com/andreyryabov/ZeneraNeo/blob/main/packages/faker/README.md) | `zen faker` — a mock API from an openapi/swagger document |
|
|
170
|
+
| [`@zenera/rag`](https://github.com/andreyryabov/ZeneraNeo/blob/main/packages/rag/README.md) | `zen rag` — an API description as a searchable graph, + tools |
|
|
171
|
+
|
|
172
|
+
`@zenera/rag` also exports tools you can hand straight to `loadProject`, so an
|
|
173
|
+
agent can search an API it has never read.
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
Early days and moving fast — issues, questions and pull requests are welcome.
|
|
178
|
+
[MIT](LICENSE).
|
package/dist/agent.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { MemoryBinding, ResolvedBinding } from './memory.ts';
|
|
2
|
+
import type { Model } from './model.ts';
|
|
3
|
+
import type { Instructions } from './prompt.ts';
|
|
4
|
+
import type { SkillBinding } from './skills.ts';
|
|
5
|
+
import { type AnyTool } from './types.ts';
|
|
6
|
+
export interface ForkOptions {
|
|
7
|
+
/** agents a branch may run; defaults to any registered agent */
|
|
8
|
+
agents?: string[];
|
|
9
|
+
maxBranches?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface AgentOptions<TCtx = unknown> {
|
|
12
|
+
name: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
/**
|
|
15
|
+
* A string, a function of the run's immutable config, or an ordered list of
|
|
16
|
+
* both plus files. Composed into one prompt — and the files it names are
|
|
17
|
+
* recorded on the `system_prompt` node, so the trajectory says which
|
|
18
|
+
* document to edit.
|
|
19
|
+
*/
|
|
20
|
+
instructions?: Instructions<TCtx>;
|
|
21
|
+
model?: Model;
|
|
22
|
+
tools?: AnyTool<TCtx>[];
|
|
23
|
+
/** agents this one may hand the conversation over to (by name or instance) */
|
|
24
|
+
handoffs?: (string | Agent<TCtx>)[];
|
|
25
|
+
/** long-lived memory spaces this agent may read and/or write */
|
|
26
|
+
memory?: MemoryBinding<TCtx>[];
|
|
27
|
+
/** on-demand instruction bundles */
|
|
28
|
+
skills?: SkillBinding;
|
|
29
|
+
/** opt-in to parallel sub-agents */
|
|
30
|
+
fork?: ForkOptions;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Note what is *not* here any more: `maxTurns`. A run ends on a final answer,
|
|
34
|
+
* an abort or an unrecoverable error — never because a counter ran out.
|
|
35
|
+
*/
|
|
36
|
+
export declare class Agent<TCtx = unknown> {
|
|
37
|
+
readonly name: string;
|
|
38
|
+
readonly description?: string;
|
|
39
|
+
readonly instructions?: Instructions<TCtx>;
|
|
40
|
+
readonly model?: Model;
|
|
41
|
+
readonly tools: AnyTool<TCtx>[];
|
|
42
|
+
readonly handoffs: string[];
|
|
43
|
+
readonly memory: MemoryBinding<TCtx>[];
|
|
44
|
+
readonly skills?: SkillBinding;
|
|
45
|
+
readonly fork?: ForkOptions;
|
|
46
|
+
constructor(opts: AgentOptions<TCtx>);
|
|
47
|
+
/**
|
|
48
|
+
* Memory bindings with their scope resolved against the run context. The
|
|
49
|
+
* resolved value is what gets recorded in the trajectory, so a resumed run
|
|
50
|
+
* cannot drift into another user's space.
|
|
51
|
+
*/
|
|
52
|
+
memoryBindings(ctx: TCtx): ResolvedBinding[];
|
|
53
|
+
}
|
|
54
|
+
export declare class AgentRegistry<TCtx = unknown> {
|
|
55
|
+
readonly agents: Map<string, Agent<TCtx>>;
|
|
56
|
+
/** Registers agents; chainable. */
|
|
57
|
+
add(...agents: Agent<TCtx>[]): this;
|
|
58
|
+
/** Creates + registers + returns an agent in one call. */
|
|
59
|
+
agent(opts: AgentOptions<TCtx>): Agent<TCtx>;
|
|
60
|
+
find(name: string): Agent<TCtx> | undefined;
|
|
61
|
+
get(name: string): Agent<TCtx>;
|
|
62
|
+
names(): string[];
|
|
63
|
+
}
|
|
64
|
+
export declare function handoffTool<TCtx>(target: string, description?: string): AnyTool<TCtx>;
|
|
65
|
+
export declare function handoffTarget(toolName: string): string | undefined;
|
|
66
|
+
//# sourceMappingURL=agent.d.ts.map
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { HANDOFF_PREFIX } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Note what is *not* here any more: `maxTurns`. A run ends on a final answer,
|
|
4
|
+
* an abort or an unrecoverable error — never because a counter ran out.
|
|
5
|
+
*/
|
|
6
|
+
export class Agent {
|
|
7
|
+
name;
|
|
8
|
+
description;
|
|
9
|
+
instructions;
|
|
10
|
+
model;
|
|
11
|
+
tools;
|
|
12
|
+
handoffs;
|
|
13
|
+
memory;
|
|
14
|
+
skills;
|
|
15
|
+
fork;
|
|
16
|
+
constructor(opts) {
|
|
17
|
+
this.name = opts.name;
|
|
18
|
+
this.description = opts.description;
|
|
19
|
+
this.instructions = opts.instructions;
|
|
20
|
+
this.model = opts.model;
|
|
21
|
+
this.tools = opts.tools ?? [];
|
|
22
|
+
this.handoffs = (opts.handoffs ?? []).map((h) => (typeof h === 'string' ? h : h.name));
|
|
23
|
+
this.memory = opts.memory ?? [];
|
|
24
|
+
this.skills = opts.skills;
|
|
25
|
+
this.fork = opts.fork;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Memory bindings with their scope resolved against the run context. The
|
|
29
|
+
* resolved value is what gets recorded in the trajectory, so a resumed run
|
|
30
|
+
* cannot drift into another user's space.
|
|
31
|
+
*/
|
|
32
|
+
memoryBindings(ctx) {
|
|
33
|
+
return this.memory.map((b) => ({
|
|
34
|
+
store: b.store,
|
|
35
|
+
scope: typeof b.scope === 'function' ? b.scope(ctx) : (b.scope ?? `agent:${this.name}`),
|
|
36
|
+
access: b.access,
|
|
37
|
+
autoRecall: b.autoRecall,
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Registry
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
export class AgentRegistry {
|
|
45
|
+
agents = new Map();
|
|
46
|
+
/** Registers agents; chainable. */
|
|
47
|
+
add(...agents) {
|
|
48
|
+
for (const a of agents) {
|
|
49
|
+
if (this.agents.has(a.name)) {
|
|
50
|
+
throw new Error(`duplicate agent name: ${a.name}`);
|
|
51
|
+
}
|
|
52
|
+
this.agents.set(a.name, a);
|
|
53
|
+
}
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
/** Creates + registers + returns an agent in one call. */
|
|
57
|
+
agent(opts) {
|
|
58
|
+
const a = new Agent(opts);
|
|
59
|
+
this.add(a);
|
|
60
|
+
return a;
|
|
61
|
+
}
|
|
62
|
+
find(name) {
|
|
63
|
+
return this.agents.get(name);
|
|
64
|
+
}
|
|
65
|
+
get(name) {
|
|
66
|
+
const a = this.agents.get(name);
|
|
67
|
+
if (!a) {
|
|
68
|
+
throw new Error(`unknown agent: ${name} (known: ${[...this.agents.keys()].join(', ')})`);
|
|
69
|
+
}
|
|
70
|
+
return a;
|
|
71
|
+
}
|
|
72
|
+
names() {
|
|
73
|
+
return [...this.agents.keys()];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Hand-offs are modelled as ordinary tools named `transfer_to_<agent>`, so the
|
|
77
|
+
// model picks the next agent with the same mechanism it uses for everything
|
|
78
|
+
// else. The kernel recognises them purely by this prefix.
|
|
79
|
+
export function handoffTool(target, description) {
|
|
80
|
+
return {
|
|
81
|
+
name: `${HANDOFF_PREFIX}${target}`,
|
|
82
|
+
description: description ?? `Hand the conversation over to the "${target}" agent.`,
|
|
83
|
+
parameters: {
|
|
84
|
+
type: 'object',
|
|
85
|
+
properties: { reason: { type: 'string', description: 'Why the hand-off is needed.' } },
|
|
86
|
+
required: [],
|
|
87
|
+
additionalProperties: false,
|
|
88
|
+
},
|
|
89
|
+
// The switch itself happens in the kernel; this body only produces the
|
|
90
|
+
// tool message that acknowledges the call to the model.
|
|
91
|
+
execute: () => `transferred to ${target}`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export function handoffTarget(toolName) {
|
|
95
|
+
return toolName.startsWith(HANDOFF_PREFIX) ? toolName.slice(HANDOFF_PREFIX.length) : undefined;
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=agent.js.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { AgentRegistry } from './agent.ts';
|
|
2
|
+
import type { Model } from './model.ts';
|
|
3
|
+
import type { Services } from './services.ts';
|
|
4
|
+
import type { SkillBinding } from './skills.ts';
|
|
5
|
+
export interface ArchTool {
|
|
6
|
+
name: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
/** the skill that unlocks it; absent for tools the agent always carries */
|
|
9
|
+
skill?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ArchSkill {
|
|
12
|
+
name: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
version?: string;
|
|
15
|
+
/** loaded before the first model call, so it is never in the rendered index */
|
|
16
|
+
preload?: boolean;
|
|
17
|
+
/** tools this skill unlocks, by name */
|
|
18
|
+
toolNames?: string[];
|
|
19
|
+
}
|
|
20
|
+
export interface ArchMemory {
|
|
21
|
+
store: string;
|
|
22
|
+
/** `(per run)` when the binding resolves its scope from the run context */
|
|
23
|
+
scope: string;
|
|
24
|
+
access: 'read' | 'read-write';
|
|
25
|
+
autoRecall?: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface ArchAgent {
|
|
28
|
+
name: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
/** model id, from the agent or inherited from the runner */
|
|
31
|
+
model?: string;
|
|
32
|
+
/** true when the id above came from the runner rather than the agent */
|
|
33
|
+
inheritedModel?: boolean;
|
|
34
|
+
tools: ArchTool[];
|
|
35
|
+
/** agent names this one may transfer to */
|
|
36
|
+
handoffs: string[];
|
|
37
|
+
skills?: {
|
|
38
|
+
provider: string;
|
|
39
|
+
discovery: SkillBinding['discovery'];
|
|
40
|
+
/** empty when the provider is not registered with the services */
|
|
41
|
+
catalog: ArchSkill[];
|
|
42
|
+
/** the binding names a provider the services do not know */
|
|
43
|
+
unresolved?: boolean;
|
|
44
|
+
};
|
|
45
|
+
memory: ArchMemory[];
|
|
46
|
+
fork?: {
|
|
47
|
+
agents?: string[];
|
|
48
|
+
maxBranches?: number;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export interface Architecture {
|
|
52
|
+
/**
|
|
53
|
+
* `declared` comes from the registry and is complete. `observed` is
|
|
54
|
+
* reconstructed from a trajectory and only contains what a run touched —
|
|
55
|
+
* the fallback when nobody handed the report a runner.
|
|
56
|
+
*/
|
|
57
|
+
source: 'declared' | 'observed';
|
|
58
|
+
agents: ArchAgent[];
|
|
59
|
+
}
|
|
60
|
+
export interface DescribeOptions<TCtx = unknown> {
|
|
61
|
+
/** needed to enumerate skill catalogs */
|
|
62
|
+
services?: Services;
|
|
63
|
+
/** the runner's fallback model, for agents that do not pin one */
|
|
64
|
+
defaultModel?: Model;
|
|
65
|
+
/** resolves context-dependent memory scopes; omitted leaves them symbolic */
|
|
66
|
+
context?: TCtx;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Projects a registry into a snapshot. Async only because a skill catalog is
|
|
70
|
+
* an index a provider has to be asked for — everything else is already in
|
|
71
|
+
* memory.
|
|
72
|
+
*/
|
|
73
|
+
export declare function describeArchitecture<TCtx = unknown>(registry: AgentRegistry<TCtx>, opts?: DescribeOptions<TCtx>): Promise<Architecture>;
|
|
74
|
+
//# sourceMappingURL=architecture.d.ts.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { HANDOFF_PREFIX } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Projects a registry into a snapshot. Async only because a skill catalog is
|
|
4
|
+
* an index a provider has to be asked for — everything else is already in
|
|
5
|
+
* memory.
|
|
6
|
+
*/
|
|
7
|
+
export async function describeArchitecture(registry, opts = {}) {
|
|
8
|
+
const agents = [];
|
|
9
|
+
for (const agent of registry.agents.values()) {
|
|
10
|
+
agents.push(await describeAgent(agent, opts));
|
|
11
|
+
}
|
|
12
|
+
return { source: 'declared', agents };
|
|
13
|
+
}
|
|
14
|
+
async function describeAgent(agent, opts) {
|
|
15
|
+
const skills = await describeSkills(agent, opts.services);
|
|
16
|
+
const own = new Set(agent.tools.map((t) => t.name));
|
|
17
|
+
const tools = agent.tools
|
|
18
|
+
// A hand-off is already an edge between two agents; listing its
|
|
19
|
+
// synthetic `transfer_to_*` tool as well would draw it twice.
|
|
20
|
+
.filter((t) => !t.name.startsWith(HANDOFF_PREFIX))
|
|
21
|
+
.map((t) => ({ name: t.name, description: t.description }));
|
|
22
|
+
for (const skill of skills?.catalog ?? []) {
|
|
23
|
+
for (const name of skill.toolNames ?? []) {
|
|
24
|
+
if (own.has(name)) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
own.add(name);
|
|
28
|
+
tools.push({
|
|
29
|
+
name,
|
|
30
|
+
description: opts.services?.skillProvider(skills.provider).tool(name)?.description,
|
|
31
|
+
skill: skill.name,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const model = agent.model?.id ?? opts.defaultModel?.id;
|
|
36
|
+
return {
|
|
37
|
+
name: agent.name,
|
|
38
|
+
description: agent.description,
|
|
39
|
+
model,
|
|
40
|
+
inheritedModel: model !== undefined && !agent.model ? true : undefined,
|
|
41
|
+
tools,
|
|
42
|
+
handoffs: [...agent.handoffs],
|
|
43
|
+
skills,
|
|
44
|
+
memory: describeMemory(agent, opts.context),
|
|
45
|
+
fork: agent.fork
|
|
46
|
+
? { agents: agent.fork.agents, maxBranches: agent.fork.maxBranches }
|
|
47
|
+
: undefined,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async function describeSkills(agent, services) {
|
|
51
|
+
const binding = agent.skills;
|
|
52
|
+
if (!binding) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const head = { provider: binding.provider, discovery: binding.discovery };
|
|
56
|
+
if (!services?.hasSkills(binding.provider)) {
|
|
57
|
+
return { ...head, catalog: [], unresolved: true };
|
|
58
|
+
}
|
|
59
|
+
const preload = new Set(binding.preload ?? []);
|
|
60
|
+
const keep = allowFilter(binding);
|
|
61
|
+
const summaries = (await services.skillProvider(binding.provider).list()).filter(keep);
|
|
62
|
+
return {
|
|
63
|
+
...head,
|
|
64
|
+
catalog: summaries.map((s) => ({
|
|
65
|
+
name: s.name,
|
|
66
|
+
description: s.description,
|
|
67
|
+
version: s.version,
|
|
68
|
+
preload: preload.has(s.name) ? true : undefined,
|
|
69
|
+
toolNames: s.toolNames,
|
|
70
|
+
})),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function allowFilter(binding) {
|
|
74
|
+
const allow = binding.allow;
|
|
75
|
+
if (!allow) {
|
|
76
|
+
return () => true;
|
|
77
|
+
}
|
|
78
|
+
if (typeof allow === 'function') {
|
|
79
|
+
return allow;
|
|
80
|
+
}
|
|
81
|
+
const set = new Set(allow);
|
|
82
|
+
return (s) => set.has(s.name);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* With a context in hand this is exactly what the run will record. Without
|
|
86
|
+
* one, a scope function is left symbolic rather than called: it would be
|
|
87
|
+
* handed `undefined` and either throw or invent a space no run ever uses.
|
|
88
|
+
*/
|
|
89
|
+
function describeMemory(agent, context) {
|
|
90
|
+
if (context !== undefined) {
|
|
91
|
+
return agent.memoryBindings(context).map((b) => ({
|
|
92
|
+
store: b.store,
|
|
93
|
+
scope: b.scope,
|
|
94
|
+
access: b.access,
|
|
95
|
+
autoRecall: b.autoRecall ? true : undefined,
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
return agent.memory.map((b) => ({
|
|
99
|
+
store: b.store,
|
|
100
|
+
scope: typeof b.scope === 'function' ? '(per run)' : (b.scope ?? `agent:${agent.name}`),
|
|
101
|
+
access: b.access,
|
|
102
|
+
autoRecall: b.autoRecall ? true : undefined,
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=architecture.js.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { TokenUsage } from './types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Which side of a retrieval this text is.
|
|
4
|
+
*
|
|
5
|
+
* The only asymmetry worth a place in the contract: the current generation of
|
|
6
|
+
* embedding models is trained so that a question and the passage answering it
|
|
7
|
+
* are encoded *differently*, and mixing the two up costs recall silently rather
|
|
8
|
+
* than loudly. Google spells it `taskType`, OpenRouter `inputType`, and OpenAI
|
|
9
|
+
* has no such knob at all — which is exactly why it belongs on the request
|
|
10
|
+
* rather than on any one adapter.
|
|
11
|
+
*/
|
|
12
|
+
export type EmbeddingTaskType = 'query' | 'document';
|
|
13
|
+
export interface EmbeddingRequest {
|
|
14
|
+
/** the texts to encode, in the order the vectors come back */
|
|
15
|
+
input: string[];
|
|
16
|
+
taskType?: EmbeddingTaskType;
|
|
17
|
+
/** truncate to this width, where the model supports it */
|
|
18
|
+
dimensions?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Scale each vector to unit length. On by default — see `vectors` below.
|
|
21
|
+
* Turn it off to see what the model actually returned.
|
|
22
|
+
*/
|
|
23
|
+
normalize?: boolean;
|
|
24
|
+
signal?: AbortSignal;
|
|
25
|
+
}
|
|
26
|
+
export interface EmbeddingResponse {
|
|
27
|
+
/**
|
|
28
|
+
* One vector per input, in the same order, and of unit length unless the
|
|
29
|
+
* request asked otherwise.
|
|
30
|
+
*
|
|
31
|
+
* Normalising here rather than leaving it to the caller is the one place
|
|
32
|
+
* this contract does more than translate. Whether a model returns unit
|
|
33
|
+
* vectors is a property of the *model*, not the vendor, and it changes
|
|
34
|
+
* between generations: `gemini-embedding-2` is unit at every width,
|
|
35
|
+
* `gemini-embedding-001` comes back at |v| ≈ 0.58 when truncated to 768 of
|
|
36
|
+
* its 3072, and `text-embedding-005` at ≈ 0.67 for 256 of its 768 — because
|
|
37
|
+
* truncation is a raw slice and only some models rescale afterwards.
|
|
38
|
+
*
|
|
39
|
+
* Cosine similarity divides the magnitude out and so cannot see any of
|
|
40
|
+
* that; a dot-product index, which is what pgvector and most vector stores
|
|
41
|
+
* default to, can see nothing else. So a caller that did not normalise
|
|
42
|
+
* would not get an error, it would get quietly worse results — and it
|
|
43
|
+
* cannot know which model it was handed.
|
|
44
|
+
*/
|
|
45
|
+
vectors: number[][];
|
|
46
|
+
/** the width actually returned, which a truncating request may have chosen */
|
|
47
|
+
dimensions: number;
|
|
48
|
+
/** input tokens only; some providers report nothing at all */
|
|
49
|
+
usage?: TokenUsage;
|
|
50
|
+
}
|
|
51
|
+
export interface Embedder {
|
|
52
|
+
readonly id: string;
|
|
53
|
+
embed(req: EmbeddingRequest): Promise<EmbeddingResponse>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The one way an adapter finishes, so the unit-length guarantee holds for every
|
|
57
|
+
* provider without three copies of the same loop.
|
|
58
|
+
*/
|
|
59
|
+
export declare function embeddingResponse(vectors: number[][], req: EmbeddingRequest, usage?: TokenUsage): EmbeddingResponse;
|
|
60
|
+
//# sourceMappingURL=embedding.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one way an adapter finishes, so the unit-length guarantee holds for every
|
|
3
|
+
* provider without three copies of the same loop.
|
|
4
|
+
*/
|
|
5
|
+
export function embeddingResponse(vectors, req, usage) {
|
|
6
|
+
return {
|
|
7
|
+
vectors: req.normalize === false ? vectors : vectors.map(unit),
|
|
8
|
+
dimensions: vectors[0]?.length ?? 0,
|
|
9
|
+
usage,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function unit(vector) {
|
|
13
|
+
let sum = 0;
|
|
14
|
+
for (const x of vector) {
|
|
15
|
+
sum += x * x;
|
|
16
|
+
}
|
|
17
|
+
const length = Math.sqrt(sum);
|
|
18
|
+
// A zero vector has no direction to preserve, and dividing would make it NaN.
|
|
19
|
+
return length === 0 ? vector : vector.map((x) => x / length);
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=embedding.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { GoogleGenAI } from '@google/genai';
|
|
2
|
+
import { type Embedder, type EmbeddingRequest, type EmbeddingResponse } from '../embedding.ts';
|
|
3
|
+
/** Provider-specific knobs. */
|
|
4
|
+
export interface GeminiEmbedderOptions {
|
|
5
|
+
/** default width; a request may override it */
|
|
6
|
+
dimensions?: number;
|
|
7
|
+
/** a document's title, which the retrieval task type takes into account */
|
|
8
|
+
title?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Texts this model accepts per request. One is the default because that is
|
|
11
|
+
* what every `gemini-embedding-*` model takes — `embedContent` is a
|
|
12
|
+
* single-document endpoint there, and the batch path is a separate,
|
|
13
|
+
* job-based API. The older `text-embedding-*` models accept far more and
|
|
14
|
+
* can say so.
|
|
15
|
+
*/
|
|
16
|
+
maxBatch?: number;
|
|
17
|
+
}
|
|
18
|
+
export declare class GeminiEmbedder implements Embedder {
|
|
19
|
+
#private;
|
|
20
|
+
readonly id: string;
|
|
21
|
+
constructor(id: string, client: GoogleGenAI, options?: GeminiEmbedderOptions);
|
|
22
|
+
embed(req: EmbeddingRequest): Promise<EmbeddingResponse>;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=gemini.d.ts.map
|