@zenera/cli 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 +239 -0
- package/dist/args.d.ts +40 -0
- package/dist/args.js +99 -0
- package/dist/audit.d.ts +53 -0
- package/dist/audit.js +144 -0
- package/dist/banner.d.ts +13 -0
- package/dist/banner.js +103 -0
- package/dist/command.d.ts +14 -0
- package/dist/command.js +12 -0
- package/dist/commands/check.d.ts +3 -0
- package/dist/commands/check.js +287 -0
- package/dist/commands/index.d.ts +22 -0
- package/dist/commands/index.js +56 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +157 -0
- package/dist/commands/inspect.d.ts +3 -0
- package/dist/commands/inspect.js +158 -0
- package/dist/commands/key.d.ts +3 -0
- package/dist/commands/key.js +335 -0
- package/dist/commands/list.d.ts +3 -0
- package/dist/commands/list.js +101 -0
- package/dist/commands/models.d.ts +9 -0
- package/dist/commands/models.js +120 -0
- package/dist/commands/open.d.ts +9 -0
- package/dist/commands/open.js +270 -0
- package/dist/commands/run.d.ts +3 -0
- package/dist/commands/run.js +167 -0
- package/dist/commands/sandbox.d.ts +3 -0
- package/dist/commands/sandbox.js +112 -0
- package/dist/commands/version.d.ts +6 -0
- package/dist/commands/version.js +39 -0
- package/dist/engine.d.ts +49 -0
- package/dist/engine.js +208 -0
- package/dist/external.d.ts +10 -0
- package/dist/external.js +56 -0
- package/dist/home.d.ts +31 -0
- package/dist/home.js +108 -0
- package/dist/ids.d.ts +12 -0
- package/dist/ids.js +44 -0
- package/dist/keys.d.ts +124 -0
- package/dist/keys.js +309 -0
- package/dist/lib.d.ts +9 -0
- package/dist/lib.js +31 -0
- package/dist/liveness.d.ts +23 -0
- package/dist/liveness.js +221 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.js +155 -0
- package/dist/narrate.d.ts +19 -0
- package/dist/narrate.js +124 -0
- package/dist/podman.d.ts +46 -0
- package/dist/podman.js +254 -0
- package/dist/projects.d.ts +70 -0
- package/dist/projects.js +232 -0
- package/dist/resolve.d.ts +27 -0
- package/dist/resolve.js +138 -0
- package/dist/sandbox.d.ts +36 -0
- package/dist/sandbox.js +104 -0
- package/dist/scaffold.d.ts +29 -0
- package/dist/scaffold.js +220 -0
- package/dist/session.d.ts +77 -0
- package/dist/session.js +156 -0
- package/dist/term.d.ts +69 -0
- package/dist/term.js +242 -0
- package/dist/tui/app.d.ts +8 -0
- package/dist/tui/app.js +257 -0
- package/dist/tui/theme.d.ts +23 -0
- package/dist/tui/theme.js +134 -0
- package/dist/tui/wrap.d.ts +12 -0
- package/dist/tui/wrap.js +62 -0
- package/dist/validate.d.ts +145 -0
- package/dist/validate.js +959 -0
- package/package.json +76 -0
- package/templates/.github/copilot-instructions.md +1579 -0
- package/templates/.github/prompts/new-agent.prompt.md +38 -0
- package/templates/.github/prompts/new-skill.prompt.md +37 -0
- package/templates/.github/prompts/review-project.prompt.md +31 -0
- package/templates/.github/skills/zen-cli/SKILL.md +110 -0
|
@@ -0,0 +1,1579 @@
|
|
|
1
|
+
# Copilot instructions — agent projects
|
|
2
|
+
|
|
3
|
+
> Written by `zen init` into `.github/copilot-instructions.md`, alongside the
|
|
4
|
+
> prompt files and skills in the same tree (§2.4). VS Code loads it
|
|
5
|
+
> automatically for every request in this folder, so it is the standing brief
|
|
6
|
+
> for anyone — human or agent — editing this project.
|
|
7
|
+
>
|
|
8
|
+
> It describes the Zenera Neo runtime, not this particular project, and `zen`
|
|
9
|
+
> rewrites the whole tree on `init` and `open` — so put this project's own
|
|
10
|
+
> conventions in `INSTRUCTIONS.md`, where they will survive. The runtime's own
|
|
11
|
+
> reference is `docs/agents-yaml.md`, `docs/projects.md` and `DESIGN.md` in the
|
|
12
|
+
> @zenera/neo repository — where they disagree with this file, they win.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 0. What this repository is
|
|
17
|
+
|
|
18
|
+
This is **not a normal application repository**. There is no application code
|
|
19
|
+
here. It is a folder of declarative artefacts — YAML and Markdown — that
|
|
20
|
+
assemble into a running multi-agent system, driven by the `zen` CLI.
|
|
21
|
+
|
|
22
|
+
The centre of gravity is **prose**. Every valuable change here is an edit to a
|
|
23
|
+
prompt, a skill, or one line of `agents.yaml`. Behaviour is configured, not
|
|
24
|
+
programmed: what an agent knows, which model answers, and which of the tools
|
|
25
|
+
`zen run` provides it may reach for.
|
|
26
|
+
|
|
27
|
+
**Default posture when working in this repo:**
|
|
28
|
+
|
|
29
|
+
1. Find which artefact owns the behaviour before editing anything (§10).
|
|
30
|
+
2. Prefer editing a prompt or skill over adding an agent.
|
|
31
|
+
3. Prefer adding a skill over lengthening a prompt.
|
|
32
|
+
4. Prefer granting a tool over asking the model to compute or remember.
|
|
33
|
+
5. Never add an agent to solve a problem that is really a prompt problem.
|
|
34
|
+
6. Every change must still load: `agents.yaml` is validated strictly at load,
|
|
35
|
+
and `zen check` says so before a model is ever called.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 1. Mental model
|
|
40
|
+
|
|
41
|
+
### 1.1 What an agent actually is
|
|
42
|
+
|
|
43
|
+
An agent is four things and nothing more:
|
|
44
|
+
|
|
45
|
+
| Part | Where it lives | What it decides |
|
|
46
|
+
| --------------- | ---------------------------------------------- | ----------------------------- |
|
|
47
|
+
| **Instruction** | `INSTRUCTIONS.md` + `agents/prompts/<name>.md` | How it behaves |
|
|
48
|
+
| **Model** | `agents.yaml` → `model:` | How well and how expensively |
|
|
49
|
+
| **Tools** | `agents.yaml` → `tools:` | What it can _do_ |
|
|
50
|
+
| **Knowledge** | `agents/skills/*` + memory | What it can _know_, on demand |
|
|
51
|
+
|
|
52
|
+
Plus two relations: **handoffs** — which other agents it may transfer control to
|
|
53
|
+
— and **fork** — whether it may split into parallel branches at all (§6.4).
|
|
54
|
+
|
|
55
|
+
There is no hidden orchestration layer. If behaviour is wrong, one of those six
|
|
56
|
+
things is wrong.
|
|
57
|
+
|
|
58
|
+
### 1.2 The loop
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
user input
|
|
62
|
+
↓
|
|
63
|
+
[system prompt][tool schemas][transcript] → model
|
|
64
|
+
↓
|
|
65
|
+
model returns: text → done
|
|
66
|
+
tools → execute → append results → loop
|
|
67
|
+
handoff→ switch agent, re-render system prompt → loop
|
|
68
|
+
fork → run N branches in parallel → join → loop
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Every step appends to an **append-only trajectory**. Nothing is ever mutated or
|
|
72
|
+
deleted; compaction _covers_ older nodes rather than removing them, so the audit
|
|
73
|
+
trail survives context pressure. A conversation _is_ its `AgentState`; the loaded
|
|
74
|
+
project is immutable and shared by every conversation in the process.
|
|
75
|
+
|
|
76
|
+
### 1.3 The single most important idea: context is the product
|
|
77
|
+
|
|
78
|
+
The model sees exactly one thing: a token sequence. Your entire job as an author
|
|
79
|
+
is deciding **what is in that sequence and in what order**.
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
┌─────────────────────────────────────────────┐
|
|
83
|
+
│ tool schemas fixed at load │ ← stable, cacheable
|
|
84
|
+
│ INSTRUCTIONS.md shared by all agents │ ← stable, cacheable
|
|
85
|
+
│ agent prompt this agent's brief │ ← stable per agent
|
|
86
|
+
│ skill index names + descriptions │ ← stable per agent
|
|
87
|
+
│ preloaded skills activated turn 0 │ ← stable, in the cached prefix
|
|
88
|
+
├─────────────────────────────────────────────┤
|
|
89
|
+
│ transcript grows │ ← the volatile part
|
|
90
|
+
└─────────────────────────────────────────────┘
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Two consequences that drive nearly every design rule in this document:
|
|
94
|
+
|
|
95
|
+
- **Stable prefix = cache hit.** Providers cache by prefix. Anything appended
|
|
96
|
+
mid-run _after_ the first reply — a tool schema, an instruction, a late skill
|
|
97
|
+
activation — invalidates the cache from that point on. This is why tool schemas
|
|
98
|
+
are fixed at load and skill-owned tools are declared from turn 0 and merely
|
|
99
|
+
_gated_ at call time.
|
|
100
|
+
- **Everything in the prefix is paid for on every call.** A 4 000-token prompt
|
|
101
|
+
covering twelve perils costs on every turn, whether or not the case involves
|
|
102
|
+
any of them. Progressive disclosure (§5) is not an optimisation; it is the
|
|
103
|
+
organising principle.
|
|
104
|
+
|
|
105
|
+
### 1.4 What a good system looks like
|
|
106
|
+
|
|
107
|
+
- Each agent has **one job you can state in one sentence**.
|
|
108
|
+
- The prompt says what to do, not what the software is.
|
|
109
|
+
- Facts live in skills or tools, never in a prompt that must be edited to change
|
|
110
|
+
a number.
|
|
111
|
+
- Numbers come from tools; the model narrates, it does not compute.
|
|
112
|
+
- The failure mode of every instruction is stated ("if X is absent, say so and
|
|
113
|
+
stop") — an unstated failure mode is an invented one.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 2. Repository layout
|
|
118
|
+
|
|
119
|
+
### 2.1 Canonical
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
my-project/
|
|
123
|
+
├── .github/ the editor's brief — §2.4
|
|
124
|
+
│ ├── copilot-instructions.md this file
|
|
125
|
+
│ ├── prompts/*.prompt.md tasks you invoke by name
|
|
126
|
+
│ └── skills/*/SKILL.md reference the editor loads on demand
|
|
127
|
+
├── .env credentials — NEVER committed
|
|
128
|
+
├── INSTRUCTIONS.md house rules, prepended to every agent
|
|
129
|
+
├── agents.yaml who exists, what they may reach for
|
|
130
|
+
├── agents/
|
|
131
|
+
│ ├── prompts/
|
|
132
|
+
│ │ ├── intake.md one agent's own brief
|
|
133
|
+
│ │ └── adjuster.md
|
|
134
|
+
│ └── skills/
|
|
135
|
+
│ ├── house_style/
|
|
136
|
+
│ │ ├── SKILL.md folder skill
|
|
137
|
+
│ │ └── examples.md sibling files become `resources`
|
|
138
|
+
│ ├── water_damage/
|
|
139
|
+
│ │ └── SKILL.md
|
|
140
|
+
│ └── shipping_delays.md flat skill (frontmatter + body)
|
|
141
|
+
└── sessions/ run state, memory, whatever the agent wrote
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Only `agents.yaml` is required, and only `agents:` is required inside it.
|
|
145
|
+
The config is found by name, in order: `agents.yaml`, `agents.yml`,
|
|
146
|
+
`agents/agents.yaml`, `agents/agents.yml`.
|
|
147
|
+
|
|
148
|
+
### 2.2 Variants
|
|
149
|
+
|
|
150
|
+
**Single agent, knowledge-heavy** — the most under-used shape. One agent, one
|
|
151
|
+
prompt, a large skill catalog. Prefer this until routing is genuinely needed.
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
INSTRUCTIONS.md · agents.yaml · agents/prompts/assistant.md · agents/skills/** (20 skills)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Router + specialists** — a cheap intake agent that classifies and hands off.
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
agents/prompts/{intake,billing,technical,escalation}.md
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Pipeline** — fixed stages, each handing to the next; the last one answers.
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
research → draft → review
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Shared skills across projects** — `skills:` accepts a list, merged into one
|
|
170
|
+
catalog:
|
|
171
|
+
|
|
172
|
+
```yaml
|
|
173
|
+
skills:
|
|
174
|
+
- agents/skills
|
|
175
|
+
- ../shared/compliance-skills
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Paths may not escape the project root unless the root is set to the common
|
|
179
|
+
ancestor. Several directories become **one** provider (id `project`) — "which
|
|
180
|
+
folder is this skill in?" is not a question an author should have to answer.
|
|
181
|
+
|
|
182
|
+
### 2.3 Naming rules (enforced)
|
|
183
|
+
|
|
184
|
+
Agent names, provider names and model alias keys must match:
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
^[a-z0-9]+(?:[-_][a-z0-9]+)*$
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
They reach the model as `transfer_to_<name>` and the file system as directory
|
|
191
|
+
names, so: `intake`, `order-triage`, `house_style`. No spaces, no capitals, no
|
|
192
|
+
dots.
|
|
193
|
+
|
|
194
|
+
### 2.4 The two audiences
|
|
195
|
+
|
|
196
|
+
There are two sets of instructions in this repository and they are not for the
|
|
197
|
+
same reader. Keeping them apart is the single easiest thing to get wrong.
|
|
198
|
+
|
|
199
|
+
| Tree | Read by | About |
|
|
200
|
+
| ---------------------------- | ------------------------------------------ | ----------------------------------- |
|
|
201
|
+
| `INSTRUCTIONS.md`, `agents/` | the **project's** agents, at run time | the domain this system works in |
|
|
202
|
+
| `.github/` | the **editor's** assistant, while you edit | how a project of this kind is built |
|
|
203
|
+
|
|
204
|
+
The `.github/` tree follows the same progressive-disclosure discipline the
|
|
205
|
+
agents do, for the same reason — it is a prefix somebody pays for:
|
|
206
|
+
|
|
207
|
+
- **`copilot-instructions.md`** is always on. Everything in it is loaded for
|
|
208
|
+
every request in this folder, so it holds only what is true of every task.
|
|
209
|
+
- **`.github/skills/<name>/SKILL.md`** is reference the editor loads when its
|
|
210
|
+
`description` matches what you asked. Put long, occasional material here —
|
|
211
|
+
a command surface, a vendor's quirks, a format spec — not in the file above.
|
|
212
|
+
The `description` is the routing key; §3.4 applies to these as much as to the
|
|
213
|
+
project's own skills.
|
|
214
|
+
- **`.github/prompts/<name>.prompt.md`** is a task you invoke by name (`/name`),
|
|
215
|
+
with `mode: agent` and a `description` in its frontmatter. Write one when a
|
|
216
|
+
job is done repeatedly and has a right order — adding an agent, adding a
|
|
217
|
+
skill, reviewing before a commit.
|
|
218
|
+
|
|
219
|
+
`zen init` and `zen open` rewrite this whole tree from the version of `zen` in
|
|
220
|
+
hand, so **edits inside `.github/` do not survive**. Project-specific conventions
|
|
221
|
+
belong in `INSTRUCTIONS.md` and the agent prompts, which are never overwritten.
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## 3. File formats
|
|
226
|
+
|
|
227
|
+
### 3.1 `agents.yaml`
|
|
228
|
+
|
|
229
|
+
The schema is **strict**: an unknown key is a load error, not a value silently
|
|
230
|
+
ignored. Errors name the path — `agents.yaml: agents[1].skills.discovery — …`.
|
|
231
|
+
|
|
232
|
+
```yaml
|
|
233
|
+
version: 1 # schema version, defaults to 1
|
|
234
|
+
default: intake # entry agent; wins over any `default: true`
|
|
235
|
+
|
|
236
|
+
providers: {} # named connections (credentials + endpoint)
|
|
237
|
+
provider: openai # the provider an unprefixed model id belongs to — §7.3
|
|
238
|
+
models: {} # named model configurations
|
|
239
|
+
model: fast # fallback for agents that do not pin their own
|
|
240
|
+
embeddings: {} # named vectorisers — §3.1.1
|
|
241
|
+
embedding: small # the one `AgentProject.embedder()` returns when asked for no name
|
|
242
|
+
skills: agents/skills # one directory, or a list
|
|
243
|
+
|
|
244
|
+
agents: # the only required key; at least one entry
|
|
245
|
+
- name: intake
|
|
246
|
+
description: Takes the first message, gets the reference, routes the case.
|
|
247
|
+
system: agents/prompts/intake.md
|
|
248
|
+
model: router
|
|
249
|
+
tools: [policy_lookup]
|
|
250
|
+
handoffs: [adjuster]
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Agent fields**
|
|
254
|
+
|
|
255
|
+
| Field | Meaning |
|
|
256
|
+
| ------------- | ------------------------------------------------------------------------------------ |
|
|
257
|
+
| `name` | **Required.** See §2.3 |
|
|
258
|
+
| `description` | What a sibling's `transfer_to_<name>` tool tells the model. Write it _for the model_ |
|
|
259
|
+
| `system` | Prompt path, relative to root. Defaults to `agents/prompts/<name>.md` if present |
|
|
260
|
+
| `model` | A `models:` alias or shorthand. Falls back to top-level `model:` |
|
|
261
|
+
| `tools` | Selectors over the tools `zen run` provides — see §3.6 |
|
|
262
|
+
| `handoffs` | Agent names this one may transfer to. Bare strings; no per-edge config |
|
|
263
|
+
| `skills` | Skill binding — see §5.2 |
|
|
264
|
+
| `fork` | `true`, or `{ agents, maxBranches }` — opt-in to parallel branches; see §6.4 |
|
|
265
|
+
| `default` | `true` marks the entry point when no top-level `default:` |
|
|
266
|
+
|
|
267
|
+
**Providers** — a provider is a _connection_, not a model. One client is built
|
|
268
|
+
per name and shared, so five agents on one key open one connection pool.
|
|
269
|
+
|
|
270
|
+
| Field | Meaning |
|
|
271
|
+
| ------------------------- | --------------------------------------------------------------------------------------------------------- |
|
|
272
|
+
| `kind` | `openai` \| `google` \| `vertex` \| `anthropic` \| `openrouter` \| `openai-compatible` (default `openai`) |
|
|
273
|
+
| `apiKey` / `apiKeyEnv` | Literal, `${VAR}`, or the name of the env var holding it |
|
|
274
|
+
| `baseURL` / `baseURLEnv` | For gateways and compatible endpoints |
|
|
275
|
+
| `project` / `location` | **vertex only** — GCP project id and region (or `global`) |
|
|
276
|
+
| `headers` | Sent on every request: routing, attribution, api versions |
|
|
277
|
+
| `timeoutMs`, `maxRetries` | Per-request timeout and retry count |
|
|
278
|
+
|
|
279
|
+
`openai`, `google`, `vertex`, `anthropic`, `openrouter` and `openai-compatible`
|
|
280
|
+
are usable as provider _names_ with no declaration at all. Declare a
|
|
281
|
+
`providers:` entry only when it says something the default does not — a second
|
|
282
|
+
key, a region, a base url.
|
|
283
|
+
|
|
284
|
+
`${VAR}` and `${VAR:-fallback}` expand from the environment, compose inside
|
|
285
|
+
longer values (`https://${GATEWAY}/v1`), and are **lazy**: a declared-but-unused
|
|
286
|
+
provider with a missing key does not fail loading.
|
|
287
|
+
|
|
288
|
+
**Errors caught at load** — rely on these instead of defensive checks:
|
|
289
|
+
|
|
290
|
+
- any unknown key; any name breaking the pattern
|
|
291
|
+
- `models.<alias>.provider` naming an undeclared provider
|
|
292
|
+
- `embeddings.<alias>.provider` naming an undeclared provider
|
|
293
|
+
- `agents[].tools` naming a tool the runtime does not provide, or a group with nothing in it
|
|
294
|
+
- `agents[].handoffs` naming an unknown agent, or the agent itself
|
|
295
|
+
- `agents[].skills.provider` / `.allow` / `.preload` naming something absent
|
|
296
|
+
- a `preload:` entry missing from `allow:`
|
|
297
|
+
- `agents[].fork.agents` naming an unknown agent, or being empty; `maxBranches` below 2
|
|
298
|
+
- `system:` pointing at a missing file, or outside the project root
|
|
299
|
+
|
|
300
|
+
**Not caught at load** — a model id whose prefix is missing and so resolves to the
|
|
301
|
+
wrong provider (§7.3), and any combination of knobs the vendor rejects at request
|
|
302
|
+
time, such as OpenAI reasoning on chat completions (§7.6). Both surface on the
|
|
303
|
+
first call, so read §7 before writing a `models:` entry.
|
|
304
|
+
|
|
305
|
+
**Comments in `agents.yaml`** — this file is the architecture diagram of the
|
|
306
|
+
project, and its comments are read by whoever has to change it next. Write them
|
|
307
|
+
at that level:
|
|
308
|
+
|
|
309
|
+
- Say **what a block is for** and **why it exists**: what this agent owns, why
|
|
310
|
+
this one is on the deep tier, why this handoff edge is there.
|
|
311
|
+
- Keep them **short** — one line above a block, a few words at the end of a line.
|
|
312
|
+
A comment longer than the thing it describes is a design doc in the wrong file.
|
|
313
|
+
- Do **not** restate the runtime. How skills are discovered, how the trajectory
|
|
314
|
+
is appended, how prompt caching works, what the loader validates — none of that
|
|
315
|
+
belongs here. It is documented in this file and in `docs/agents-yaml.md`.
|
|
316
|
+
- Do **not** restate the key. `# the model this agent uses` above `model:` is
|
|
317
|
+
noise; `# cheap: it only classifies` is not.
|
|
318
|
+
- Do not narrate edits (`# added 2026-08`, `# was gpt-4o`). Git owns that.
|
|
319
|
+
|
|
320
|
+
```yaml
|
|
321
|
+
# Intake classifies and routes; it never answers.
|
|
322
|
+
- name: intake
|
|
323
|
+
model: router # cheap tier — one sentence in, one handoff out
|
|
324
|
+
handoffs: [adjuster, escalation]
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### 3.1.1 `embeddings:`
|
|
328
|
+
|
|
329
|
+
A vectoriser turns text into a vector, for retrieval rather than for answering.
|
|
330
|
+
It resolves through the **same `providers:`**, so a key declared once generates
|
|
331
|
+
and embeds without being written twice.
|
|
332
|
+
|
|
333
|
+
```yaml
|
|
334
|
+
providers:
|
|
335
|
+
house:
|
|
336
|
+
apiKey: ${ACME_OPENAI_KEY}
|
|
337
|
+
|
|
338
|
+
embeddings:
|
|
339
|
+
small: openai:text-embedding-3-small # a shorthand string...
|
|
340
|
+
large: # ...or the object form
|
|
341
|
+
provider: house
|
|
342
|
+
model: text-embedding-3-large
|
|
343
|
+
dimensions: 256
|
|
344
|
+
|
|
345
|
+
embedding: small
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
| Field | Meaning |
|
|
349
|
+
| ------------------------ | ------------------------------------------------------------------------ |
|
|
350
|
+
| `provider` | A `providers:` name or a built-in kind. Defaults to the default provider |
|
|
351
|
+
| `model` | **Required.** The bare id — the object form never re-parses a shorthand |
|
|
352
|
+
| `dimensions` | Truncate to this width, where the model supports it |
|
|
353
|
+
| `apiKey` / `baseURL` / … | The same credential fields a provider takes, for a one-off connection |
|
|
354
|
+
| `title` | **gemini only** — a document title the retrieval task type weighs |
|
|
355
|
+
| `maxBatch` | **gemini only** — texts per request; see below |
|
|
356
|
+
| `routing` | **openrouter only** — which upstream provider serves the request |
|
|
357
|
+
|
|
358
|
+
Four things differ from `models:` and are worth knowing before you write one:
|
|
359
|
+
|
|
360
|
+
- **No `api:` field.** `/v1/responses` has no embeddings endpoint, so naming an
|
|
361
|
+
api means nothing on any protocol. The shorthand is `[provider:]model`.
|
|
362
|
+
- **`kind: anthropic` has no embeddings API at all.** Anthropic publishes none
|
|
363
|
+
and points at third parties; an `embeddings:` entry on an Anthropic provider
|
|
364
|
+
fails at load. Use another provider — the connection need not be the one the
|
|
365
|
+
agents talk through.
|
|
366
|
+
- **Not a per-agent key.** Nothing in the runtime consumes a vectoriser yet, so
|
|
367
|
+
there is no `agents[].embedding:`. A TypeScript host reaches one with
|
|
368
|
+
`project.embedder()` for the default, or `project.embedder('large')` by name.
|
|
369
|
+
- **Vectors come back unit length**, so cosine and dot product agree. This is a
|
|
370
|
+
guarantee of the runtime, not of the vendor: truncating with `dimensions:` is a
|
|
371
|
+
raw slice and only some models rescale afterwards — `gemini-embedding-2` does,
|
|
372
|
+
`gemini-embedding-001` returns |v| ≈ 0.58 at 768 of its 3072. Pass
|
|
373
|
+
`normalize: false` on a call to see what the model actually said.
|
|
374
|
+
|
|
375
|
+
Google's `embedContent` takes one document per request for every
|
|
376
|
+
`gemini-embedding-*` model, and the adapter splits a batch across requests to
|
|
377
|
+
hide that. `maxBatch` therefore defaults to `1`; raise it only for a
|
|
378
|
+
`text-embedding-*` model, which accepts more.
|
|
379
|
+
|
|
380
|
+
`zen check` and `zen models` report every declared embedding beside the models,
|
|
381
|
+
with the credential each one would need.
|
|
382
|
+
|
|
383
|
+
### 3.2 `INSTRUCTIONS.md`
|
|
384
|
+
|
|
385
|
+
House rules, read **once** and prepended to every agent's system prompt. It is
|
|
386
|
+
the stable head of the cached prefix, so it should change rarely. The name is
|
|
387
|
+
deliberately not `AGENTS.md` — that one belongs to the coding assistant reading
|
|
388
|
+
this file, and these rules address the project's own agents.
|
|
389
|
+
|
|
390
|
+
Put here only what is true for **every** agent:
|
|
391
|
+
|
|
392
|
+
- identity and domain ("You work the property claims desk")
|
|
393
|
+
- non-negotiable prohibitions (regulatory, legal, safety)
|
|
394
|
+
- global format and tone constraints
|
|
395
|
+
- domain vocabulary and identifier formats
|
|
396
|
+
|
|
397
|
+
Do **not** put here: anything one agent needs and another does not; anything that
|
|
398
|
+
changes weekly; long reference data (that is a skill).
|
|
399
|
+
|
|
400
|
+
**When writing it, remember what it is: a prefix on every agent's system prompt.**
|
|
401
|
+
It is not a README and not a design document — every line is paid for on every
|
|
402
|
+
call, by every agent, and each one is an instruction the model will try to
|
|
403
|
+
follow. So:
|
|
404
|
+
|
|
405
|
+
- It is a **prompt**: §4.2 applies in full — second person, imperative, no
|
|
406
|
+
hedging, no meta-talk about the runtime, failure paths stated.
|
|
407
|
+
- Carry the **shared model of the project**: what this system is, what the agents
|
|
408
|
+
are collectively for, how the work flows between them, and the vocabulary and
|
|
409
|
+
identifier formats they all use. Enough for any agent to know where it sits;
|
|
410
|
+
not a tour of the codebase.
|
|
411
|
+
- Describe the architecture in **one short paragraph or a handful of lines**, in
|
|
412
|
+
terms the agents can act on ("the adjuster owns coverage decisions; you do
|
|
413
|
+
not"), not in terms of files, YAML keys or the runtime.
|
|
414
|
+
- Anything only one agent needs goes in that agent's prompt instead. If you find
|
|
415
|
+
yourself writing "if you are the router…", you are in the wrong file.
|
|
416
|
+
|
|
417
|
+
Target 20–60 lines. If it exceeds ~100, split the stable half out into a
|
|
418
|
+
preloaded skill.
|
|
419
|
+
|
|
420
|
+
### 3.3 `agents/prompts/<name>.md`
|
|
421
|
+
|
|
422
|
+
Plain Markdown, no frontmatter. This is the agent's _job description_, appended
|
|
423
|
+
after `INSTRUCTIONS.md`.
|
|
424
|
+
|
|
425
|
+
Structure that works:
|
|
426
|
+
|
|
427
|
+
```markdown
|
|
428
|
+
<one sentence: who this agent is and what it owns>
|
|
429
|
+
|
|
430
|
+
<what it does, as a short numbered procedure or 3–5 rules>
|
|
431
|
+
|
|
432
|
+
<what it must NOT do — especially the neighbouring agent's job>
|
|
433
|
+
|
|
434
|
+
<how to finish: the shape of the answer, or which handoff ends the turn>
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
Real example (an intake agent, complete):
|
|
438
|
+
|
|
439
|
+
```markdown
|
|
440
|
+
You are the first person a claimant reaches.
|
|
441
|
+
|
|
442
|
+
Your job is small and you should finish it fast:
|
|
443
|
+
|
|
444
|
+
1. Read what happened.
|
|
445
|
+
2. Call `policy_lookup` with the claim reference to confirm the policy exists
|
|
446
|
+
and see what it covers.
|
|
447
|
+
3. Hand the case to the `adjuster` agent, which owns the peril policies.
|
|
448
|
+
|
|
449
|
+
Do not quote coverage rules yourself — you do not have them. Do not ask the
|
|
450
|
+
claimant for anything the message already contains.
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
Note what makes it work: it is 9 lines; it names the tool and the handoff
|
|
454
|
+
literally; it states the boundary ("you do not have them") with the _reason_; it
|
|
455
|
+
forbids the specific failure that agent actually exhibits.
|
|
456
|
+
|
|
457
|
+
Write it as a prompt, under the rules in §4.2 — **instructive and concise**. It
|
|
458
|
+
tells one agent what to do; it never explains the system (`INSTRUCTIONS.md`
|
|
459
|
+
already did, §3.2), never repeats a house rule, and never describes the runtime.
|
|
460
|
+
Every line should be an instruction the model can act on or a boundary it can
|
|
461
|
+
check itself against.
|
|
462
|
+
|
|
463
|
+
Target 10–40 lines. A 200-line prompt is a skill catalog that has not been split
|
|
464
|
+
yet.
|
|
465
|
+
|
|
466
|
+
### 3.4 Skills
|
|
467
|
+
|
|
468
|
+
A skill is curated, reusable instruction content — plus optional tools — loaded
|
|
469
|
+
**on demand** instead of permanently occupying the system prompt. Two layouts,
|
|
470
|
+
discovered in the same scan:
|
|
471
|
+
|
|
472
|
+
```
|
|
473
|
+
agents/skills/refund_policy.md flat: frontmatter + body
|
|
474
|
+
agents/skills/refund_policy/SKILL.md folder: sibling files become `resources`
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
Frontmatter is a deliberately small subset of YAML — `key: value`, plus `[a, b]`
|
|
478
|
+
flow lists for `tags` and `tools`. **Every key is optional**: `name` defaults to
|
|
479
|
+
the file/folder name, `description` to the first non-empty line of the body.
|
|
480
|
+
|
|
481
|
+
```markdown
|
|
482
|
+
---
|
|
483
|
+
name: refund_policy
|
|
484
|
+
description: When a refund is owed, and how much.
|
|
485
|
+
version: 2.0.0
|
|
486
|
+
tags: [billing, policy]
|
|
487
|
+
tools: [issue_refund]
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
A parcel counts as late once it passes its promised delivery date by 48 hours.
|
|
491
|
+
|
|
492
|
+
- **48h to 7 days late** — apologise, confirm the parcel is still moving, and
|
|
493
|
+
refund the shipping fee. Do not refund the goods.
|
|
494
|
+
- **More than 7 days late** — treat the parcel as lost. Offer a replacement at
|
|
495
|
+
no cost, or a full refund, whichever the customer prefers.
|
|
496
|
+
|
|
497
|
+
Never give a new delivery date. Say "still in transit" instead.
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
**The `description` is the routing key.** It is the only thing the model sees
|
|
501
|
+
before deciding to load the skill. Write it as the _condition under which this
|
|
502
|
+
skill is needed_, not as a title:
|
|
503
|
+
|
|
504
|
+
| Bad | Good |
|
|
505
|
+
| --------------------------- | ------------------------------------------------------------------------------------ |
|
|
506
|
+
| `description: Refunds` | `description: When a refund is owed, and how much.` |
|
|
507
|
+
| `description: Water policy` | `description: Escape of water from plumbing and tanks — and the freezing exclusion.` |
|
|
508
|
+
|
|
509
|
+
**`tools:` in frontmatter** names skill-owned tools. They are declared to the
|
|
510
|
+
provider from turn 0 (the schema never changes) but **refuse to execute** until
|
|
511
|
+
the skill is active. This is how a tool can be gated without breaking the cache.
|
|
512
|
+
|
|
513
|
+
Use a folder skill when the content needs companions — a CSV rate table, an
|
|
514
|
+
example letter, a JSON schema. Siblings become `resources` the model can read.
|
|
515
|
+
|
|
516
|
+
### 3.5 Tools
|
|
517
|
+
|
|
518
|
+
A tool is what an agent can _do_ rather than say. `zen run` provides three
|
|
519
|
+
groups — the workspace tools (§3.6), the sandbox tools (§3.7) and the web tools
|
|
520
|
+
(§3.8) — and `agents.yaml` decides which agent holds which. Nothing else reaches
|
|
521
|
+
the machine, so `tools:` is the whole permission model: an agent that does not
|
|
522
|
+
name a tool cannot use it, whatever its prompt says.
|
|
523
|
+
|
|
524
|
+
Two rules follow:
|
|
525
|
+
|
|
526
|
+
- **Grant the narrowest set the job needs.** An agent that only reviews should
|
|
527
|
+
not be holding the tools that overwrite files.
|
|
528
|
+
- **Say in the prompt when to reach for what.** A granted tool the prompt never
|
|
529
|
+
mentions is used at the model's discretion, which is not the same as never.
|
|
530
|
+
|
|
531
|
+
Skills can own tools too — `tools:` in a skill's frontmatter (§3.4) names tools
|
|
532
|
+
that refuse to run until that skill is active.
|
|
533
|
+
|
|
534
|
+
### 3.6 The workspace tools (`workspace:*`)
|
|
535
|
+
|
|
536
|
+
`zen run` builds this set for you, rooted at the session's workspace directory.
|
|
537
|
+
Nothing else reaches the file system, so an agent whose `tools:` does not name
|
|
538
|
+
them cannot see a file at all.
|
|
539
|
+
|
|
540
|
+
| Tool | What it does |
|
|
541
|
+
| ------------- | ---------------------------------------------------------------------------------------- |
|
|
542
|
+
| `read_file` | Reads text; `start_line`/`end_line` for a range. Reports total lines and if it truncated |
|
|
543
|
+
| `list_dir` | Entries with kind, format (text/binary/image/…), size, and line count for text |
|
|
544
|
+
| `find_files` | Paths containing a substring, case-insensitive |
|
|
545
|
+
| `write_file` | Creates or overwrites a whole file, making parent directories |
|
|
546
|
+
| `apply_patch` | Edits by surrounding context rather than line numbers; several files atomically |
|
|
547
|
+
| `move_file` | Moves or renames; refuses to clobber without `overwrite` |
|
|
548
|
+
| `delete_file` | Deletes; a directory needs `recursive` |
|
|
549
|
+
|
|
550
|
+
Every path is resolved through one containment gate — symlinks followed, then
|
|
551
|
+
checked — so nothing outside the workspace root is reachable. Reads are capped
|
|
552
|
+
and listings bounded, so no single call can flood the context.
|
|
553
|
+
|
|
554
|
+
Paths are written relative to the root, and `/workspace/...` — the name the same
|
|
555
|
+
directory has inside the sandbox (§3.7) — is accepted as well, so a path copied
|
|
556
|
+
out of a command's output does not have to be translated first. When a mount is
|
|
557
|
+
configured the tools also _report_ that name, so the two toolsets speak one
|
|
558
|
+
vocabulary and a path can be passed from either to the other unchanged; with no
|
|
559
|
+
container involved there is no second name and everything stays relative.
|
|
560
|
+
|
|
561
|
+
**Selecting them.** A `tools:` entry is a selector, not only a name:
|
|
562
|
+
|
|
563
|
+
| Selector | Selects |
|
|
564
|
+
| ------------- | ------------------------------------------------- |
|
|
565
|
+
| `read_file` | that one tool |
|
|
566
|
+
| `workspace:*` | every tool in the group |
|
|
567
|
+
| `'*'` | every tool the runtime provides |
|
|
568
|
+
| `-<any>` | removes what it matches from the selection so far |
|
|
569
|
+
|
|
570
|
+
```yaml
|
|
571
|
+
agents:
|
|
572
|
+
- name: editor
|
|
573
|
+
tools: [workspace:*]
|
|
574
|
+
|
|
575
|
+
- name: reviewer
|
|
576
|
+
# Everything except the four that can change something.
|
|
577
|
+
tools: [workspace:*, -write_file, -apply_patch, -move_file, -delete_file]
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
Selectors apply in the order written, so a `-` line reads as an exception to the
|
|
581
|
+
line above it. Quote a lone `'*'`: unquoted, YAML reads it as an alias and
|
|
582
|
+
refuses the file. `workspace:*` needs no quoting. There is no name globbing —
|
|
583
|
+
`read_*` is an unknown tool, because a selector should track a declared set, not
|
|
584
|
+
a naming habit.
|
|
585
|
+
|
|
586
|
+
`zen run --read-only` withholds the four mutating tools whatever `agents.yaml`
|
|
587
|
+
asks for: the deployment overriding the repository, as everywhere else.
|
|
588
|
+
|
|
589
|
+
**Prompting for them.** Three lines earn their place in any prompt that grants
|
|
590
|
+
this group:
|
|
591
|
+
|
|
592
|
+
- Read before editing — `apply_patch` matches on exact text, so a patch built
|
|
593
|
+
from memory fails.
|
|
594
|
+
- Prefer `apply_patch` to `write_file` for an existing file. Rewriting a file to
|
|
595
|
+
change one line costs the whole file in output tokens and loses everything the
|
|
596
|
+
model did not think to repeat.
|
|
597
|
+
- `list_dir` before guessing a path; `find_files` when the name is known but the
|
|
598
|
+
location is not.
|
|
599
|
+
|
|
600
|
+
### 3.7 The sandbox tools (`sandbox:*`)
|
|
601
|
+
|
|
602
|
+
The second group `zen run` builds. These run a shell command in a Linux
|
|
603
|
+
container with the same workspace mounted at `/workspace`, so an agent can
|
|
604
|
+
build, test, install and inspect rather than only read and write.
|
|
605
|
+
|
|
606
|
+
| Tool | What it does |
|
|
607
|
+
| ------------------------ | ------------------------------------------------------------------------ |
|
|
608
|
+
| `run_command` | Runs to completion. Returns exit code, stdout, stderr, duration |
|
|
609
|
+
| `run_command_background` | Starts a long process (a server, a watch build) and returns a `job_id` |
|
|
610
|
+
| `read_command_output` | A window of a job's output from `start_line`, plus whether it still runs |
|
|
611
|
+
| `stop_command` | Signals a job's process group |
|
|
612
|
+
|
|
613
|
+
The container is the boundary. Nothing inspects the command — there is no
|
|
614
|
+
allow-list of binaries and no pattern matching on what the model wrote, because
|
|
615
|
+
both are trivially defeated and neither survives a shell. What holds is that
|
|
616
|
+
only the workspace and the session's `/home/agent` are mounted, the container
|
|
617
|
+
is removed at the end of the session, and the command is never a shell argument
|
|
618
|
+
on the host: it travels on stdin to `/bin/sh` inside.
|
|
619
|
+
|
|
620
|
+
**Configuring it.** A top-level `sandbox:` block in `agents.yaml` describes the
|
|
621
|
+
container. Every field has a default, so the block is optional — write only the
|
|
622
|
+
lines that differ:
|
|
623
|
+
|
|
624
|
+
```yaml
|
|
625
|
+
sandbox:
|
|
626
|
+
persist: true # recommended — see below
|
|
627
|
+
image: docker.io/library/python:3.14-slim-bookworm # the default
|
|
628
|
+
cpus: 4 # fractional cores
|
|
629
|
+
memory: 4096 # MiB
|
|
630
|
+
network: bridge # `none` for a project that must not reach out
|
|
631
|
+
timeout: 300 # seconds per command
|
|
632
|
+
env: [HTTPS_PROXY, NO_PROXY] # host variables to forward, by NAME
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
| Field | Default | Meaning |
|
|
636
|
+
| --------- | --------------------------------------------- | -------------------------------------------- |
|
|
637
|
+
| `image` | `docker.io/library/python:3.14-slim-bookworm` | The base image commands run in |
|
|
638
|
+
| `cpus` | the host's | Fractional cores |
|
|
639
|
+
| `memory` | the host's | MiB |
|
|
640
|
+
| `network` | `bridge` | `bridge` / `none` / `host` |
|
|
641
|
+
| `workdir` | `/workspace` | Mount point and default cwd |
|
|
642
|
+
| `timeout` | `120` | Seconds per command |
|
|
643
|
+
| `user` | the image's | uid, name, or `uid:gid` |
|
|
644
|
+
| `persist` | `false` — **set it to `true`** | Keep the container between runs of a session |
|
|
645
|
+
| `env` | none | Host variables to forward, **names** |
|
|
646
|
+
|
|
647
|
+
`env:` takes **names, never values** — a value here would be a secret in the
|
|
648
|
+
repository — and anything credential-shaped (`KEY`, `TOKEN`, `SECRET`,
|
|
649
|
+
`PASSWORD`, `CREDENTIAL`) is refused at load.
|
|
650
|
+
|
|
651
|
+
Agents share one container, because they share the workspace and a hand-off is
|
|
652
|
+
meant to be continuous. An agent that needs something else says so and gets its
|
|
653
|
+
own, with its block merged over the top-level one:
|
|
654
|
+
|
|
655
|
+
```yaml
|
|
656
|
+
sandbox:
|
|
657
|
+
image: docker.io/library/python:3.14-slim-bookworm
|
|
658
|
+
|
|
659
|
+
agents:
|
|
660
|
+
- name: builder
|
|
661
|
+
tools: [workspace:*, sandbox:*]
|
|
662
|
+
sandbox:
|
|
663
|
+
image: docker.io/library/node:22-bookworm-slim
|
|
664
|
+
memory: 8192
|
|
665
|
+
- name: analyst
|
|
666
|
+
tools: [workspace:*, sandbox:*] # shares the project's container
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
**Write `persist: true` unless you have a reason not to.** By default the
|
|
670
|
+
container is _removed_ when the session closes, and only two paths survive it:
|
|
671
|
+
`/workspace`, and `/home/agent` — which is `$HOME` inside, backed by the session
|
|
672
|
+
directory. That covers `pip install --user`, `npm config` and `~/.cache`, but it
|
|
673
|
+
does **not** cover the ordinary thing an agent actually does: `pip install X` or
|
|
674
|
+
`apt-get install X` as root writes to the container's system paths, and those
|
|
675
|
+
are gone on the next `zen run`. The agent then reinstalls, silently, every
|
|
676
|
+
single time — and usually does not realise it has, because the previous run's
|
|
677
|
+
transcript says it succeeded.
|
|
678
|
+
|
|
679
|
+
```yaml
|
|
680
|
+
sandbox:
|
|
681
|
+
persist: true
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
With it, the container is _stopped_ rather than removed, and the next run of
|
|
685
|
+
that session starts the same one back up with everything still installed. The
|
|
686
|
+
cost is containers that outlive their sessions — `zen sandbox status` lists them
|
|
687
|
+
and `zen sandbox clean` removes them.
|
|
688
|
+
|
|
689
|
+
Changing any field renames the container, so bumping the image gets a fresh one
|
|
690
|
+
rather than an old one quietly persisting with the wrong contents. That is also
|
|
691
|
+
the one sharp edge of `persist: true`: a config change abandons the old
|
|
692
|
+
container with whatever was installed in it, so a long-lived setup still belongs
|
|
693
|
+
in `image:` rather than in an accumulated rootfs.
|
|
694
|
+
|
|
695
|
+
Granting the group is what makes the project need Podman: `zen run` checks the
|
|
696
|
+
engine before the first turn and exits `5` with an install command if it is
|
|
697
|
+
missing. `zen sandbox status` answers the same question on its own. Full
|
|
698
|
+
reference: `docs/agents-yaml.md`.
|
|
699
|
+
|
|
700
|
+
**Prompting for them.** Two lines earn their place:
|
|
701
|
+
|
|
702
|
+
- The workspace is at `/workspace` and is the same directory the file tools
|
|
703
|
+
see — an edit made with `apply_patch` is what a command will compile, and a
|
|
704
|
+
path from either side works on both.
|
|
705
|
+
- Anything that does not return, returns — use `run_command_background` for a
|
|
706
|
+
server, not `run_command` with a large timeout.
|
|
707
|
+
|
|
708
|
+
### 3.8 The web tools (`exa:*`)
|
|
709
|
+
|
|
710
|
+
The third group `zen run` builds. These reach the live web through
|
|
711
|
+
[Exa](https://exa.ai) — a search index built for models rather than for people,
|
|
712
|
+
so a query is a sentence describing what is wanted, not a bag of keywords.
|
|
713
|
+
|
|
714
|
+
| Tool | What it does |
|
|
715
|
+
| ------------ | ------------------------------------------------------------------------------- |
|
|
716
|
+
| `web_search` | Ranked pages for a described query, each with a short excerpt of why it matched |
|
|
717
|
+
| `web_read` | The readable text of pages, several at once, boilerplate stripped |
|
|
718
|
+
| `web_answer` | A written answer to a question, with the sources it was drawn from |
|
|
719
|
+
|
|
720
|
+
The three are meant to be used in that order: **search to find, read to quote.**
|
|
721
|
+
An excerpt is enough to judge which source to trust and never enough to cite
|
|
722
|
+
from — `web_search` returns the sentences that made a page match, not the page.
|
|
723
|
+
`web_answer` runs a search _and_ a model on the other side, so it is the slowest
|
|
724
|
+
and dearest of the three; it earns its cost when the answer is a fact spread
|
|
725
|
+
over several pages, and wastes it when a specific document is wanted.
|
|
726
|
+
|
|
727
|
+
```yaml
|
|
728
|
+
agents:
|
|
729
|
+
- name: researcher
|
|
730
|
+
tools: [exa:*, workspace:*]
|
|
731
|
+
|
|
732
|
+
- name: fact-checker
|
|
733
|
+
# Find and read, but never let a model on the far side do the reasoning.
|
|
734
|
+
tools: [web_search, web_read]
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
**The key.** All three read `$EXA_API_KEY` **when they are called**, not when the
|
|
738
|
+
project loads. So a project naming `exa:*` still loads on a machine that has no
|
|
739
|
+
key — the tools simply refuse, on the turn that tried, saying which variable is
|
|
740
|
+
missing. Get a key from <https://dashboard.exa.ai/api-keys> and hold it in
|
|
741
|
+
either place:
|
|
742
|
+
|
|
743
|
+
```
|
|
744
|
+
zen key add exa # the keyring; materialised into the environment per run
|
|
745
|
+
EXA_API_KEY=... # or .env, which wins over the keyring
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
`zen check` warns when an agent selects one of these tools and neither place
|
|
749
|
+
holds a key. Unlike a model credential this is a warning, not an error: the
|
|
750
|
+
project is still valid, it just cannot search yet.
|
|
751
|
+
|
|
752
|
+
**Notable arguments.** Defaults are chosen so that the common call is
|
|
753
|
+
`{ "query": "…" }` and nothing else:
|
|
754
|
+
|
|
755
|
+
| Argument | On | Why it exists |
|
|
756
|
+
| ------------------------------------------ | ------------ | ------------------------------------------------------------------------------- |
|
|
757
|
+
| `num_results` | `web_search` | 8 by default, 25 at most |
|
|
758
|
+
| `include_domains` / `exclude_domains` | `web_search` | The replacement for `site:` — operators in the query text do not work here |
|
|
759
|
+
| `start_published_date` / `end_published_…` | `web_search` | ISO 8601. The only reliable way to exclude a stale answer |
|
|
760
|
+
| `category` | `web_search` | `company`, `publication`, `news`, `personal site`, `financial report`, `people` |
|
|
761
|
+
| `max_characters` | `web_read` | 4 000 by default, 10 000 at most; `truncated` says when a page was cut |
|
|
762
|
+
| `max_age_hours` | `web_read` | `0` forces a live crawl. Omit it unless the page changes by the hour |
|
|
763
|
+
|
|
764
|
+
Every reply is bounded — pages are cut at the cap and the whole call at 128 KiB
|
|
765
|
+
of text — so one call cannot flood the context. Each carries `cost_usd`, which
|
|
766
|
+
is what the vendor charged for that call.
|
|
767
|
+
|
|
768
|
+
A failure is **reported, not raised**: a refused key, an exhausted balance, a
|
|
769
|
+
url nothing serves all come back as `{ error, hint }` for the model to read and
|
|
770
|
+
act on. `web_read` reports per-url failures in `failed` alongside the pages that
|
|
771
|
+
did load, so one bad link does not lose the rest.
|
|
772
|
+
|
|
773
|
+
**Prompting for them.** Three lines earn their place in any prompt granting this
|
|
774
|
+
group:
|
|
775
|
+
|
|
776
|
+
- Search with a sentence, not keywords — the query is read by a model.
|
|
777
|
+
- Never quote an excerpt. `web_search` says which page to open; `web_read` says
|
|
778
|
+
what it contains.
|
|
779
|
+
- Say when the web is allowed to override what the model already believes, and
|
|
780
|
+
when it is not. Without that line, a retrieved page and a memorised fact carry
|
|
781
|
+
equal weight.
|
|
782
|
+
|
|
783
|
+
### 3.9 Running it
|
|
784
|
+
|
|
785
|
+
`zen run` is what turns this folder into a running system. It reads the
|
|
786
|
+
directory, checks it, builds the workspace and sandbox tools against the
|
|
787
|
+
session's workspace, and starts the conversation:
|
|
788
|
+
|
|
789
|
+
```
|
|
790
|
+
zen run open the entry agent on this project
|
|
791
|
+
zen run "what changed?" one shot; stdout is the answer
|
|
792
|
+
zen run --session <id> continue a session
|
|
793
|
+
zen run --workspace ./repo what the agent may read and write
|
|
794
|
+
zen run --model careful override the default model for this run
|
|
795
|
+
zen run --image <ref> override the sandbox image for this run
|
|
796
|
+
zen run --read-only withhold every tool that can write
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
The project is read **once, up front**: a missing prompt, an unknown tool name,
|
|
800
|
+
a handoff to nobody all fail before the first call, with the offending key
|
|
801
|
+
named. Flags always win over the file — the repository states intent, the
|
|
802
|
+
invocation overrides it.
|
|
803
|
+
|
|
804
|
+
A session owns a workspace, a trajectory, memory and whatever the agent wrote,
|
|
805
|
+
under `sessions/`. None of it is source; none of it is committed.
|
|
806
|
+
|
|
807
|
+
### 3.10 `.env`
|
|
808
|
+
|
|
809
|
+
```
|
|
810
|
+
OPENAI_API_KEY=...
|
|
811
|
+
ANTHROPIC_API_KEY=...
|
|
812
|
+
GOOGLE_APPLICATION_CREDENTIALS=./.keys/vertex.json
|
|
813
|
+
EXA_API_KEY=...
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
Never commit. Never inline a key into `agents.yaml` — use `${VAR}`. Never print a
|
|
817
|
+
key in a log line, a test fixture, or a chat message.
|
|
818
|
+
|
|
819
|
+
---
|
|
820
|
+
|
|
821
|
+
## 4. Writing prompts
|
|
822
|
+
|
|
823
|
+
### 4.1 Composition order
|
|
824
|
+
|
|
825
|
+
```
|
|
826
|
+
1. INSTRUCTIONS.md (shared, once, all agents)
|
|
827
|
+
2. agents/prompts/<n>.md (this agent)
|
|
828
|
+
3. skill index (rendered by the runtime — do not hand-write it)
|
|
829
|
+
4. preloaded skills (activated before the first call)
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
Never duplicate `INSTRUCTIONS.md` content into an agent prompt; never hand-render
|
|
833
|
+
a list of skills into a prompt (the runtime does it, and a hand-written one goes
|
|
834
|
+
stale silently).
|
|
835
|
+
|
|
836
|
+
### 4.2 Rules
|
|
837
|
+
|
|
838
|
+
1. **Second person, imperative, present tense.** "Call `policy_lookup` with the
|
|
839
|
+
reference." Not "The agent should be able to look up policies."
|
|
840
|
+
2. **Name things literally.** Tools in backticks by their exact name; agents by
|
|
841
|
+
their exact `name:`. The model matches strings.
|
|
842
|
+
3. **State the boundary and the reason.** "Do not quote coverage rules — you do
|
|
843
|
+
not have them." A prohibition without a reason gets rationalised away.
|
|
844
|
+
4. **Prefer prohibitions that are specific.** "Never say 'approved'" beats "be
|
|
845
|
+
careful about commitments".
|
|
846
|
+
5. **Give the shape of the output, not a template to fill.** "Three short
|
|
847
|
+
paragraphs: what happened, what the policy says, what happens next."
|
|
848
|
+
6. **State the failure path.** What to do when the tool errors, the reference is
|
|
849
|
+
missing, the case does not fit. Unstated failure modes get invented.
|
|
850
|
+
7. **One instruction per line.** Contradictions become visible when they are
|
|
851
|
+
adjacent.
|
|
852
|
+
8. **No meta-talk.** Do not explain the runtime, the trajectory, tokens, or that
|
|
853
|
+
it is an AI. That is context the model must pay for and cannot act on.
|
|
854
|
+
9. **No hedging.** "Try to", "if possible", "generally" are read as permission
|
|
855
|
+
to skip.
|
|
856
|
+
10. **Facts do not belong in prompts.** A number, a rate, a policy clause belongs
|
|
857
|
+
in a skill (changeable, versioned, loaded when relevant) or a tool
|
|
858
|
+
(authoritative, computed). A prompt that must be edited to change a fee is
|
|
859
|
+
mis-factored.
|
|
860
|
+
|
|
861
|
+
### 4.3 Voice
|
|
862
|
+
|
|
863
|
+
Write like a competent colleague briefing a new hire on their first day: short
|
|
864
|
+
sentences, concrete nouns, the reason behind each rule. The model mirrors the
|
|
865
|
+
register it is given — a prompt written in bureaucratic hedging produces
|
|
866
|
+
bureaucratic hedging.
|
|
867
|
+
|
|
868
|
+
### 4.4 Debugging a prompt
|
|
869
|
+
|
|
870
|
+
When behaviour is wrong, in this order:
|
|
871
|
+
|
|
872
|
+
1. **Read the actual assembled prompt**, not the file. Use the inspect report.
|
|
873
|
+
2. Is the instruction _present_? (Missed skill load, wrong agent, compaction.)
|
|
874
|
+
3. Is it _contradicted_ by `INSTRUCTIONS.md` or a skill? Adjacent contradictions
|
|
875
|
+
win over distant ones; later text usually wins over earlier.
|
|
876
|
+
4. Is it _specific enough to be checkable_? Rewrite as a testable assertion.
|
|
877
|
+
5. Only then consider a stronger model.
|
|
878
|
+
|
|
879
|
+
Changing the model to fix an instruction-following bug hides the bug and pays for
|
|
880
|
+
it forever.
|
|
881
|
+
|
|
882
|
+
---
|
|
883
|
+
|
|
884
|
+
## 5. Context engineering
|
|
885
|
+
|
|
886
|
+
### 5.1 Progressive disclosure
|
|
887
|
+
|
|
888
|
+
The decision for every piece of knowledge:
|
|
889
|
+
|
|
890
|
+
| Where | Cost | Use when |
|
|
891
|
+
| ------------------- | ----------------------- | ----------------------------------------------------- |
|
|
892
|
+
| `INSTRUCTIONS.md` | every call, every agent | true always, for everyone |
|
|
893
|
+
| agent prompt | every call, one agent | true always, for this job |
|
|
894
|
+
| **preloaded skill** | every call, one agent | always needed, but versioned/shared separately |
|
|
895
|
+
| **indexed skill** | one line until loaded | needed _sometimes_, model can tell when from one line |
|
|
896
|
+
| **searched skill** | nothing until searched | catalog too large to index (>~30 entries) |
|
|
897
|
+
| **tool** | schema only | needs live data, exact arithmetic, or a side effect |
|
|
898
|
+
| **memory** | recall block when hit | learned across runs, not authored |
|
|
899
|
+
|
|
900
|
+
### 5.2 Skill bindings
|
|
901
|
+
|
|
902
|
+
```yaml
|
|
903
|
+
agents:
|
|
904
|
+
- name: adjuster
|
|
905
|
+
skills:
|
|
906
|
+
provider: project # which catalog; defaults to the sole one
|
|
907
|
+
discovery: index # index | search | none
|
|
908
|
+
allow: [water_damage, storm_damage, house_style] # restrict the catalog
|
|
909
|
+
preload: [house_style] # active before the first call
|
|
910
|
+
maxIndexEntries: 20 # cap the rendered index
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
- **`index`** — names and descriptions rendered into the system prompt. The
|
|
914
|
+
default and right answer for most catalogs.
|
|
915
|
+
- **`search`** — no index, only a `skill_search` tool. For catalogs too large to
|
|
916
|
+
render.
|
|
917
|
+
- **`none`** — preloads only.
|
|
918
|
+
- **`preload`** — for content there is no case for the model to decline: house
|
|
919
|
+
tone, a formatting contract. Making it choose is a wasted round trip.
|
|
920
|
+
Preloaded skills are filtered _out_ of the rendered index, and their activation
|
|
921
|
+
lands at the head of the transcript, inside the cached prefix.
|
|
922
|
+
- **`allow`** — how one catalog serves several agents without each seeing all of
|
|
923
|
+
it. A `preload` entry must also be in `allow`.
|
|
924
|
+
|
|
925
|
+
### 5.3 Memory
|
|
926
|
+
|
|
927
|
+
Memory is the read-write twin of skills: written by the run, not authored. A
|
|
928
|
+
**scope** is a namespace string; agents bound to the same scope share what is
|
|
929
|
+
in it, and the default scope is the agent's own — so memory is private until
|
|
930
|
+
something says otherwise. Recall is automatic: matches for the current input
|
|
931
|
+
are injected before each call, which is what makes memory work without the
|
|
932
|
+
model remembering to look, and an agent with write access also gets tools to
|
|
933
|
+
search, add, update and delete entries.
|
|
934
|
+
|
|
935
|
+
It is a session-level facility rather than an `agents.yaml` key: `zen run`
|
|
936
|
+
binds each session's store under `sessions/`, so memory travels with the
|
|
937
|
+
session and is not part of this repository.
|
|
938
|
+
|
|
939
|
+
Do not treat memory as a database. It is for things learned that should
|
|
940
|
+
persist. Anything authoritative — a rate, a policy clause, a procedure — belongs
|
|
941
|
+
in a skill, where it is versioned and reviewable.
|
|
942
|
+
|
|
943
|
+
### 5.4 Cache discipline
|
|
944
|
+
|
|
945
|
+
Rules that follow directly from "stable prefix = cache hit":
|
|
946
|
+
|
|
947
|
+
- Do not reorder `agents.yaml` for cosmetic reasons — tool order is prompt order.
|
|
948
|
+
- Do not put timestamps, run ids, or "today is …" in `INSTRUCTIONS.md` or a
|
|
949
|
+
prompt. A changing prefix is a permanent cache miss. Put volatile facts in a
|
|
950
|
+
tool result.
|
|
951
|
+
- Prefer `preload` over an instruction telling the model to load a skill first.
|
|
952
|
+
- Keep the volatile half of an instruction in the agent prompt and the stable
|
|
953
|
+
half in `INSTRUCTIONS.md`, not the reverse.
|
|
954
|
+
|
|
955
|
+
---
|
|
956
|
+
|
|
957
|
+
## 6. Organising agents
|
|
958
|
+
|
|
959
|
+
### 6.1 When to split
|
|
960
|
+
|
|
961
|
+
Split into a second agent when at least one is true:
|
|
962
|
+
|
|
963
|
+
- The two jobs want **different models** (a cheap router, a careful writer).
|
|
964
|
+
- The two jobs want **different tools**, and giving both to one agent invites
|
|
965
|
+
misuse.
|
|
966
|
+
- The two jobs want **contradictory instructions** ("be exhaustive" vs "be brief").
|
|
967
|
+
- One job needs a **large knowledge slice** the other never touches.
|
|
968
|
+
- The boundary is a real **handoff in the business process** ("passed to an
|
|
969
|
+
adjuster").
|
|
970
|
+
|
|
971
|
+
Do **not** split because:
|
|
972
|
+
|
|
973
|
+
- The prompt got long — split it into skills instead.
|
|
974
|
+
- It feels tidier — every handoff costs a full re-render of the system prompt and
|
|
975
|
+
a fresh cache prefix.
|
|
976
|
+
- You want "a planner and an executor" with no distinct tools or models — that is
|
|
977
|
+
one agent with a numbered procedure.
|
|
978
|
+
|
|
979
|
+
**Start with one agent. Add the second when a specific case forces it.**
|
|
980
|
+
|
|
981
|
+
### 6.2 Patterns
|
|
982
|
+
|
|
983
|
+
**Router + specialists.** A cheap, fast agent whose only job is classification
|
|
984
|
+
and handoff. Its prompt is short, it holds few tools, and it must be forbidden
|
|
985
|
+
from answering. Specialists never hand back to it.
|
|
986
|
+
|
|
987
|
+
```yaml
|
|
988
|
+
agents:
|
|
989
|
+
- { name: intake, model: router, handoffs: [billing, technical, escalation] }
|
|
990
|
+
- { name: billing, model: balanced, skills: { allow: [refund_policy, invoicing] } }
|
|
991
|
+
- { name: technical, model: balanced, tools: [search_logs, restart_service] }
|
|
992
|
+
- { name: escalation, model: careful }
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
**Pipeline.** Fixed stages, each handing to the next; only the last answers the
|
|
996
|
+
user. Encode the order in `handoffs:` so a stage cannot skip ahead.
|
|
997
|
+
|
|
998
|
+
**Fan-out / join.** For independent parallel work — ten regions, four review
|
|
999
|
+
lenses, six candidate suppliers — declare `fork:` on the agent that owns the
|
|
1000
|
+
work and let the model split it. See §6.4.
|
|
1001
|
+
|
|
1002
|
+
**Single agent + rich catalog.** One agent, `discovery: index`, twenty skills.
|
|
1003
|
+
Cheapest to run, cheapest to reason about, and correct far more often than the
|
|
1004
|
+
multi-agent instinct suggests.
|
|
1005
|
+
|
|
1006
|
+
### 6.3 Handoffs
|
|
1007
|
+
|
|
1008
|
+
- `description:` on the target agent is what the model reads when deciding.
|
|
1009
|
+
Write it as a routing condition: _"Applies the written peril policies to a
|
|
1010
|
+
claim and explains the outcome."_ — not _"The adjuster agent."_
|
|
1011
|
+
- Handoffs are bare name strings; there is no per-edge configuration.
|
|
1012
|
+
- Self-handoff is a load error. Cycles are legal but usually a bug — a router in
|
|
1013
|
+
the `handoffs` of its own specialists produces ping-pong.
|
|
1014
|
+
- Handoff collapses history by policy: the receiving agent sees a selection, not
|
|
1015
|
+
the full transcript. Do not assume it saw a detail three turns back; if it
|
|
1016
|
+
matters, put it in the handoff.
|
|
1017
|
+
|
|
1018
|
+
### 6.4 Forking (fan-out / join)
|
|
1019
|
+
|
|
1020
|
+
Forking is **opt-in per agent**. Without the key the agent is never offered the
|
|
1021
|
+
`fork` tool and cannot split, however obviously parallel the work looks:
|
|
1022
|
+
|
|
1023
|
+
```yaml
|
|
1024
|
+
agents:
|
|
1025
|
+
- name: trunk
|
|
1026
|
+
fork: true # unrestricted: any agent, any number of branches
|
|
1027
|
+
|
|
1028
|
+
- name: sweep
|
|
1029
|
+
fork:
|
|
1030
|
+
agents: [prober] # every branch runs the specialist
|
|
1031
|
+
maxBranches: 6
|
|
1032
|
+
```
|
|
1033
|
+
|
|
1034
|
+
| Field | Default | Meaning |
|
|
1035
|
+
| ------------- | -------------------- | ------------------------------------- |
|
|
1036
|
+
| `agents` | every declared agent | Which agents a branch may run |
|
|
1037
|
+
| `maxBranches` | unlimited | Cap on branches per call; minimum `2` |
|
|
1038
|
+
|
|
1039
|
+
The **model** decides the split: it names N branches, each with self-contained
|
|
1040
|
+
instructions. They run truly concurrently and rejoin as **one tool call and one
|
|
1041
|
+
tool result** in the parent's history — so N branches cost the parent
|
|
1042
|
+
O(N × summary), not O(N × full history).
|
|
1043
|
+
|
|
1044
|
+
Rules worth knowing before you write the key:
|
|
1045
|
+
|
|
1046
|
+
- `agents:` **may include the forking agent itself** — unlike `handoffs:`, that
|
|
1047
|
+
is not an error, and one role fanned out over ten items is the common shape.
|
|
1048
|
+
The list reaches the model as an `enum`, so a name outside it cannot even be
|
|
1049
|
+
decoded.
|
|
1050
|
+
- A fork always needs **at least two** branches. A one-branch call is refused
|
|
1051
|
+
with a message telling the model to do the work itself instead.
|
|
1052
|
+
- **Branches cannot talk to each other.** If branch B needs branch A's answer,
|
|
1053
|
+
it is a sequence, not a fork — keep it in one conversation.
|
|
1054
|
+
- Nesting is capped by the run's `maxForkDepth` (2 by default), so a branch may
|
|
1055
|
+
fork again but not without bound.
|
|
1056
|
+
- Fork vs handoff: a handoff is _one_ conversation changing owner; a fork is the
|
|
1057
|
+
_same_ question asked N times at once and merged.
|
|
1058
|
+
|
|
1059
|
+
Choose `context:` deliberately — the model sets it per call, so say in the
|
|
1060
|
+
agent's prompt which one this work wants:
|
|
1061
|
+
|
|
1062
|
+
- `inherit` — branch gets the full prefix. For work that depends on the case.
|
|
1063
|
+
- `compact` — prefix minus tool calls, tool results and recalls: _what was
|
|
1064
|
+
decided_, not the raw noise. **The default for wide fan-outs.**
|
|
1065
|
+
- `none` — system prompt plus instructions only. Cheapest; for independent
|
|
1066
|
+
lookups.
|
|
1067
|
+
|
|
1068
|
+
Declaring `fork:` only makes the tool available. Say in the agent's prompt when
|
|
1069
|
+
to reach for it, in the terms of this domain — _"When the request covers more
|
|
1070
|
+
than one region, fork one branch per region and merge their findings"_ — or a
|
|
1071
|
+
weaker model will work through the list serially and never call it.
|
|
1072
|
+
|
|
1073
|
+
### 6.5 Termination
|
|
1074
|
+
|
|
1075
|
+
A turn ends when the model replies with no tool calls — that text is the answer.
|
|
1076
|
+
There is no turn limit, so an agent that must stop somewhere needs a prompt that
|
|
1077
|
+
says where: what "done" looks like, and what to do when it cannot get there.
|
|
1078
|
+
|
|
1079
|
+
So say in the prompt which handoff ends the turn, or what the final answer
|
|
1080
|
+
should contain. An agent with no stated finish keeps working the problem.
|
|
1081
|
+
|
|
1082
|
+
---
|
|
1083
|
+
|
|
1084
|
+
## 7. Model selection
|
|
1085
|
+
|
|
1086
|
+
### 7.1 Tiers
|
|
1087
|
+
|
|
1088
|
+
Think in three tiers, and give them **role names, not vendor names** — so the
|
|
1089
|
+
mapping changes in one place.
|
|
1090
|
+
|
|
1091
|
+
| Alias | Role | Reasoning |
|
|
1092
|
+
| ---------- | ----------------------------------------------------------- | -------------- |
|
|
1093
|
+
| `router` | Classify, extract, route, validate. High volume, low stakes | minimal / none |
|
|
1094
|
+
| `balanced` | The default worker: apply written rules, use tools, answer | low / medium |
|
|
1095
|
+
| `careful` | Ambiguity, multi-step planning, anything a human will sign | high |
|
|
1096
|
+
|
|
1097
|
+
Add `writer` only if tone matters enough to justify a separate model.
|
|
1098
|
+
|
|
1099
|
+
### 7.2 Worked example
|
|
1100
|
+
|
|
1101
|
+
```yaml
|
|
1102
|
+
providers:
|
|
1103
|
+
# `vertex` needs no key: the GenAI SDK resolves Application Default
|
|
1104
|
+
# Credentials itself. Declared here only to pin the region.
|
|
1105
|
+
vertex-eu:
|
|
1106
|
+
kind: vertex
|
|
1107
|
+
location: europe-west4
|
|
1108
|
+
claude:
|
|
1109
|
+
kind: anthropic
|
|
1110
|
+
apiKey: ${ANTHROPIC_API_KEY}
|
|
1111
|
+
|
|
1112
|
+
models:
|
|
1113
|
+
router:
|
|
1114
|
+
provider: vertex-eu
|
|
1115
|
+
model: gemini-3.5-flash-lite
|
|
1116
|
+
thinkingLevel: minimal
|
|
1117
|
+
balanced:
|
|
1118
|
+
provider: vertex-eu
|
|
1119
|
+
model: gemini-3.5-flash
|
|
1120
|
+
thinkingLevel: high
|
|
1121
|
+
careful:
|
|
1122
|
+
provider: openai
|
|
1123
|
+
api: responses
|
|
1124
|
+
model: gpt-5.4-mini
|
|
1125
|
+
reasoningEffort: high
|
|
1126
|
+
reasoningSummary: auto # so the reasoning is visible while it works
|
|
1127
|
+
writer:
|
|
1128
|
+
provider: claude
|
|
1129
|
+
model: claude-sonnet-4-5
|
|
1130
|
+
maxTokens: 16000
|
|
1131
|
+
|
|
1132
|
+
model: balanced # fallback for agents that do not pin their own
|
|
1133
|
+
|
|
1134
|
+
agents:
|
|
1135
|
+
- { name: intake, model: router, handoffs: [adjuster] }
|
|
1136
|
+
- { name: adjuster, model: balanced, handoffs: [escalation] }
|
|
1137
|
+
- { name: escalation, model: careful }
|
|
1138
|
+
```
|
|
1139
|
+
|
|
1140
|
+
### 7.3 Shorthand
|
|
1141
|
+
|
|
1142
|
+
```
|
|
1143
|
+
[provider[/api]:]model
|
|
1144
|
+
```
|
|
1145
|
+
|
|
1146
|
+
`gpt-4o` · `openai:gpt-4o` · `openai/responses:o3` · `vertex:gemini-3.5-flash`
|
|
1147
|
+
|
|
1148
|
+
Only the **first** colon separates, so a fine-tuned id must name its provider:
|
|
1149
|
+
`openai:ft:gpt-4o:acme::a1b2`.
|
|
1150
|
+
|
|
1151
|
+
**Always write the prefix.** The first segment is a provider _name_, not a vendor
|
|
1152
|
+
hint — nothing reads `gemini-3.5-flash` and infers Google. An unprefixed id goes
|
|
1153
|
+
to the default provider, which is `openai` unless a top-level `provider:` says
|
|
1154
|
+
otherwise, so a bare `gemini-3.5-flash` asks OpenAI for a Google model and fails
|
|
1155
|
+
with `OPENAI_API_KEY is not set`. The message names the provider it resolved to;
|
|
1156
|
+
read it as "the prefix is missing", not "the key is missing".
|
|
1157
|
+
|
|
1158
|
+
Anything the shorthand cannot express (keys, base urls, `api:`, reasoning knobs)
|
|
1159
|
+
needs the object form. The object form does **not** re-parse a shorthand: its
|
|
1160
|
+
`model:` is the bare id and the provider goes in `provider:` beside it. Writing
|
|
1161
|
+
`model: openai:gpt-5.4-mini` there sends that whole string to the API.
|
|
1162
|
+
|
|
1163
|
+
Resolution order for any `model:` value: `zen run --model` → this file's
|
|
1164
|
+
`models:` → the shorthand parser. Two agents naming `balanced` share one model
|
|
1165
|
+
over one connection.
|
|
1166
|
+
|
|
1167
|
+
### 7.4 Vendor knobs
|
|
1168
|
+
|
|
1169
|
+
| Field | Applies to | Notes |
|
|
1170
|
+
| ----------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
1171
|
+
| `reasoningEffort` | openai, openrouter | Free string on purpose — the API is the authority on validity. On openai **needs `api: responses`** — §7.6 |
|
|
1172
|
+
| `reasoningSummary` | openai, openrouter | `auto` \| `concise` \| `detailed`. On openai **needs `api: responses`** — §7.6 |
|
|
1173
|
+
| `maxTokens` | anthropic, gemini, openrouter | Cap on **output** tokens, not context. Anthropic requires one (default 8192) |
|
|
1174
|
+
| `thinkingBudgetTokens` | anthropic | Extended thinking budget |
|
|
1175
|
+
| `thinkingBudget` | gemini 2.5 | Tokens: `0` off, `-1` auto |
|
|
1176
|
+
| `thinkingLevel` | gemini 3 | `minimal` \| `low` \| `medium` \| `high` |
|
|
1177
|
+
| `includeThoughts` | gemini | Thought summaries; default `true` |
|
|
1178
|
+
| `routing` / `fallbacks` | openrouter | Upstream provider preferences, and models to fall back to — §7.5 |
|
|
1179
|
+
|
|
1180
|
+
Knobs that do not apply to the chosen vendor are ignored, not rejected.
|
|
1181
|
+
`api:` exists only for the OpenAI protocol — naming it on a Gemini or Anthropic
|
|
1182
|
+
model is an error.
|
|
1183
|
+
|
|
1184
|
+
Each vendor's own SDK is used rather than its OpenAI-compatible endpoint, because
|
|
1185
|
+
those endpoints drop exactly what this runtime is built on: thinking budgets,
|
|
1186
|
+
thought signatures, cache accounting. `openai-compatible` is the shim kind — vLLM,
|
|
1187
|
+
a self-hosted gateway. `openrouter` used to be that shim with its base url
|
|
1188
|
+
(`https://openrouter.ai/api/v1`) and key env (`OPENROUTER_API_KEY`) filled in; it
|
|
1189
|
+
now has its own SDK, which is what makes provider routing and fallback chains
|
|
1190
|
+
available — §7.5.
|
|
1191
|
+
|
|
1192
|
+
### 7.5 OpenRouter
|
|
1193
|
+
|
|
1194
|
+
A gateway: one key and one endpoint in front of several hundred models from
|
|
1195
|
+
every vendor. Useful when a project wants to compare families without holding
|
|
1196
|
+
four accounts, and when a cheap tier should be swappable by editing one id.
|
|
1197
|
+
|
|
1198
|
+
The whole declaration is the kind — `baseURL` and `OPENROUTER_API_KEY` are its
|
|
1199
|
+
defaults, and built-in kinds are usable as provider names, so a `providers:`
|
|
1200
|
+
entry is only worth writing when it adds something:
|
|
1201
|
+
|
|
1202
|
+
```yaml
|
|
1203
|
+
agents:
|
|
1204
|
+
- name: triage
|
|
1205
|
+
model: openrouter:anthropic/claude-sonnet-4.5
|
|
1206
|
+
```
|
|
1207
|
+
|
|
1208
|
+
**Model ids** are `vendor/model` and may carry a variant suffix after a colon —
|
|
1209
|
+
`:free`, `:nitro` (throughput-routed), `:floor` (price-routed), `:online` (web
|
|
1210
|
+
search). Both survive the shorthand, because only the _first_ colon separates:
|
|
1211
|
+
|
|
1212
|
+
| Ref | Provider | Model |
|
|
1213
|
+
| -------------------------------- | ------------ | --------------------- |
|
|
1214
|
+
| `openrouter:openai/gpt-5.4-nano` | `openrouter` | `openai/gpt-5.4-nano` |
|
|
1215
|
+
| `openrouter:z-ai/glm-5.2:free` | `openrouter` | `z-ai/glm-5.2:free` |
|
|
1216
|
+
|
|
1217
|
+
The `vendor/` prefix is part of the _id_, not a provider name: what precedes the
|
|
1218
|
+
first colon is the provider, and the `provider/api` slash is only read there.
|
|
1219
|
+
|
|
1220
|
+
**No api to choose.** This kind speaks one protocol, its own, so `api: responses`
|
|
1221
|
+
and `openrouter/responses:…` are both a load error ("has one api, so … means
|
|
1222
|
+
nothing here") rather than a 404 from the gateway at the first request. Reasoning
|
|
1223
|
+
arrives on the message and is read into `thinking` deltas; `reasoningEffort` and
|
|
1224
|
+
`reasoningSummary` are both forwarded, and the gateway maps effort onto whatever
|
|
1225
|
+
the destination model understands.
|
|
1226
|
+
|
|
1227
|
+
**Routing and fallbacks** are the reason this kind has an SDK. `routing` picks
|
|
1228
|
+
the upstream provider (OpenRouter's `provider` field, renamed because `provider:`
|
|
1229
|
+
already means the connection); `fallbacks` lists other _models_ to try when none
|
|
1230
|
+
can serve it (its `models` field). `allowFallbacks`, below, is a third thing
|
|
1231
|
+
again — whether the gateway may look past `order`:
|
|
1232
|
+
|
|
1233
|
+
```yaml
|
|
1234
|
+
models:
|
|
1235
|
+
routed:
|
|
1236
|
+
provider: openrouter
|
|
1237
|
+
model: openai/gpt-5.4-nano
|
|
1238
|
+
routing:
|
|
1239
|
+
order: [azure, openai]
|
|
1240
|
+
requireParameters: true
|
|
1241
|
+
sort: throughput
|
|
1242
|
+
fallbacks: [anthropic/claude-sonnet-4.5]
|
|
1243
|
+
```
|
|
1244
|
+
|
|
1245
|
+
`routing` belongs to a **model**, not to a `providers:` entry: it is chosen per
|
|
1246
|
+
request, not per connection, and the provider schema is strict, so writing it
|
|
1247
|
+
there is a load error.
|
|
1248
|
+
|
|
1249
|
+
| Key | Value |
|
|
1250
|
+
| ------------------- | ---------------------------------------------------------------------------- |
|
|
1251
|
+
| `order` | Providers to try first — a _preference_, not a restriction |
|
|
1252
|
+
| `only` / `ignore` | Restrict serving to, or away from, these |
|
|
1253
|
+
| `allowFallbacks` | May the gateway go beyond `order` — **on** unless set `false` |
|
|
1254
|
+
| `sort` | `price`, `throughput`, `latency`, `exacto` |
|
|
1255
|
+
| `requireParameters` | Skip a provider that would drop a parameter rather than serve it |
|
|
1256
|
+
| `dataCollection` | `allow` \| `deny` |
|
|
1257
|
+
| `quantizations` | `int4` `int8` `fp4` `mxfp4` `nvfp4` `fp6` `fp8` `mxfp8` `fp16` `bf16` `fp32` |
|
|
1258
|
+
| `zdr` | Zero-data-retention endpoints only |
|
|
1259
|
+
|
|
1260
|
+
`serviceTier` (`auto` \| `default` \| `fast` \| `flex` \| `priority` \| `scale`)
|
|
1261
|
+
sits alongside `routing`, not inside it.
|
|
1262
|
+
|
|
1263
|
+
**A typo in `order` is invisible.** Provider names and `sort` are free strings
|
|
1264
|
+
for the same reason `reasoningEffort` is — the gateway's list moves faster than a
|
|
1265
|
+
schema would — so nothing local rejects them, and since `allowFallbacks` defaults
|
|
1266
|
+
on, an unknown name is skipped and the request quietly succeeds somewhere else.
|
|
1267
|
+
What _is_ checked is checked by the API rather than at load: a bad `sort` or
|
|
1268
|
+
`quantizations` returns `400 provider.sort: Invalid input`. Use `only`, or
|
|
1269
|
+
`allowFallbacks: false`, when the constraint is meant to bind — an unroutable
|
|
1270
|
+
request is then a 404 instead of a silent reroute.
|
|
1271
|
+
|
|
1272
|
+
**Attribution** goes in `headers:`; there is no dedicated field because that one
|
|
1273
|
+
already means "sent on every request":
|
|
1274
|
+
|
|
1275
|
+
```yaml
|
|
1276
|
+
providers:
|
|
1277
|
+
openrouter:
|
|
1278
|
+
kind: openrouter
|
|
1279
|
+
headers:
|
|
1280
|
+
HTTP-Referer: https://example.com
|
|
1281
|
+
X-Title: My Agent
|
|
1282
|
+
```
|
|
1283
|
+
|
|
1284
|
+
**Check capabilities before pinning an id.** A gateway routes to whoever serves
|
|
1285
|
+
that model, so a request can fail on a capability rather than on the model
|
|
1286
|
+
existing (`404 No endpoints found that support image input`). The catalog is
|
|
1287
|
+
public and needs no key:
|
|
1288
|
+
|
|
1289
|
+
```bash
|
|
1290
|
+
curl -s https://openrouter.ai/api/v1/models | jq -r '
|
|
1291
|
+
.data[] | select(.id == "z-ai/glm-5.2:free")
|
|
1292
|
+
| "modalities: \(.architecture.input_modalities | join("+"))",
|
|
1293
|
+
"params: \(.supported_parameters | join(","))"'
|
|
1294
|
+
```
|
|
1295
|
+
|
|
1296
|
+
`input_modalities` decides whether images may be sent at all;
|
|
1297
|
+
`supported_parameters` decides whether `tools`, `tool_choice` and
|
|
1298
|
+
`reasoning_effort` are honoured. An agent with tools needs `tools` in that list.
|
|
1299
|
+
|
|
1300
|
+
**Not modelled yet:** `transforms`, `usage.include`, and per-call cost — the
|
|
1301
|
+
gateway reports a price on every response, but it is not surfaced in the token
|
|
1302
|
+
accounting. `plugins` exists in code only. Four `provider` fields the SDK accepts
|
|
1303
|
+
have no yaml spelling either — `maxPrice`, `preferredMaxLatency`,
|
|
1304
|
+
`preferredMinThroughput`, `enforceDistillableText` — and `sort` takes the string
|
|
1305
|
+
form only, not the `{ by, partition }` object. `models:` entries are strict, so
|
|
1306
|
+
writing any of these is a load error rather than a key that is silently dropped.
|
|
1307
|
+
|
|
1308
|
+
**`maxRetries` is honoured only as `0`.** This SDK takes a retry _strategy_, not
|
|
1309
|
+
a count, so `0` disables retries and any other number leaves the default backoff
|
|
1310
|
+
in place. `timeoutMs` and `headers` behave normally.
|
|
1311
|
+
|
|
1312
|
+
**Keys:** `zen key add openrouter` stores it under `OPENROUTER_API_KEY`.
|
|
1313
|
+
|
|
1314
|
+
### 7.6 Turning reasoning on
|
|
1315
|
+
|
|
1316
|
+
Ask two separate questions: does the model **reason**, and does it **say what it
|
|
1317
|
+
reasoned**. They are different knobs, and the second is off by default on every
|
|
1318
|
+
vendor except Gemini — which is why a reasoning model can burn thousands of
|
|
1319
|
+
thinking tokens while the CLI shows no progress at all.
|
|
1320
|
+
|
|
1321
|
+
**OpenAI** — reasoning text only exists on the **responses** API, and only as a
|
|
1322
|
+
summary. `api: responses` is not optional here, and not only for visibility:
|
|
1323
|
+
chat completions is the default, and it **refuses `reasoningEffort` together with
|
|
1324
|
+
function tools** — `400 Function tools with reasoning_effort are not supported
|
|
1325
|
+
for <model> in /v1/chat/completions`. So any OpenAI agent that both reasons and
|
|
1326
|
+
holds tools — which is nearly all of them — must name the api.
|
|
1327
|
+
|
|
1328
|
+
```yaml
|
|
1329
|
+
models:
|
|
1330
|
+
default:
|
|
1331
|
+
provider: openai
|
|
1332
|
+
api: responses # required — chat completions refuses tools + reasoning
|
|
1333
|
+
model: gpt-5.4-nano
|
|
1334
|
+
reasoningEffort: medium # how hard it thinks
|
|
1335
|
+
reasoningSummary: auto # whether you get to see it
|
|
1336
|
+
```
|
|
1337
|
+
|
|
1338
|
+
**Anthropic** — `thinkingBudgetTokens` turns extended thinking on, and it is spent
|
|
1339
|
+
_out of_ `maxTokens`, so raise that too or the answer has no room left after the
|
|
1340
|
+
thinking. The runtime keeps 1024 tokens of headroom whatever you write, so a cap
|
|
1341
|
+
below the budget is corrected rather than rejected. Read §7.7 first: this
|
|
1342
|
+
combination is unsafe for tool-using agents.
|
|
1343
|
+
|
|
1344
|
+
```yaml
|
|
1345
|
+
models:
|
|
1346
|
+
careful:
|
|
1347
|
+
provider: anthropic
|
|
1348
|
+
model: claude-sonnet-4-5
|
|
1349
|
+
maxTokens: 16000
|
|
1350
|
+
thinkingBudgetTokens: 8000
|
|
1351
|
+
```
|
|
1352
|
+
|
|
1353
|
+
**Gemini** — thought summaries are on by default (`includeThoughts: true`); what
|
|
1354
|
+
varies is the budget. Gemini 3 takes `thinkingLevel`, Gemini 2.5 takes
|
|
1355
|
+
`thinkingBudget` in tokens.
|
|
1356
|
+
|
|
1357
|
+
```yaml
|
|
1358
|
+
models:
|
|
1359
|
+
balanced:
|
|
1360
|
+
provider: vertex
|
|
1361
|
+
model: gemini-3.5-flash
|
|
1362
|
+
thinkingLevel: high
|
|
1363
|
+
```
|
|
1364
|
+
|
|
1365
|
+
What this buys, in both views: `zen run` in the TUI streams the reasoning as a dim
|
|
1366
|
+
running tail above the answer, and the one-shot path prints it under `--live`.
|
|
1367
|
+
The full chain is kept in the trajectory either way and is in the inspect report,
|
|
1368
|
+
so turning summaries off costs visibility, not the audit trail.
|
|
1369
|
+
|
|
1370
|
+
The cost is real — a summary is extra output tokens on every call — so leave it
|
|
1371
|
+
on where someone is watching and reach for a cheaper tier before turning effort
|
|
1372
|
+
up (§7.8).
|
|
1373
|
+
|
|
1374
|
+
### 7.7 Known traps
|
|
1375
|
+
|
|
1376
|
+
- **A model id with no provider prefix** — `gemini-3.5-flash` resolves to the
|
|
1377
|
+
default provider, not to Google, and the failure reads as a missing OpenAI key.
|
|
1378
|
+
Write `google:gemini-3.5-flash` — §7.3.
|
|
1379
|
+
- **OpenAI reasoning without `api: responses`** — chat completions rejects
|
|
1380
|
+
`reasoningEffort` alongside function tools with a `400`, and exposes no
|
|
1381
|
+
reasoning summary even without tools — §7.6.
|
|
1382
|
+
- **Anthropic + `thinkingBudgetTokens` + multi-turn tool use** — thinking-block
|
|
1383
|
+
signatures are not replayed, and the API rejects the follow-up. Leave extended
|
|
1384
|
+
thinking off for tool-using agents.
|
|
1385
|
+
- **`reasoningEffort: minimal`** is rejected by the `gpt-5.4-*` family; use `low`.
|
|
1386
|
+
- **Vertex** needs a project id, resolved from `project:` → `GOOGLE_CLOUD_PROJECT`
|
|
1387
|
+
→ the `project_id` inside the key file named by `GOOGLE_APPLICATION_CREDENTIALS`.
|
|
1388
|
+
gcloud user credentials and metadata-server credentials carry no project id, so
|
|
1389
|
+
those deployments must set the variable.
|
|
1390
|
+
- **OpenRouter + a capability the route does not have** — a valid id and a valid
|
|
1391
|
+
key still fail at the first request (`404 No endpoints found that support image
|
|
1392
|
+
input`, or tools quietly unused). This is not a config error and `zen check`
|
|
1393
|
+
cannot see it: check the catalog (§7.5). Cheap `:free` tiers are the usual
|
|
1394
|
+
offenders — they are frequently text-only.
|
|
1395
|
+
- **Swapping an OpenRouter id is not a like-for-like change.** Two models behind
|
|
1396
|
+
one gateway differ in modalities, tool support and reasoning; re-run the case
|
|
1397
|
+
that uses the capability, not just any case.
|
|
1398
|
+
- **An `embeddings:` entry on an Anthropic provider** fails at load: that vendor
|
|
1399
|
+
publishes no embeddings API. Point it at another provider — §3.1.1.
|
|
1400
|
+
- **Changing `dimensions:` on an embedding invalidates every stored vector.**
|
|
1401
|
+
Widths are not comparable, so anything already indexed has to be re-embedded.
|
|
1402
|
+
|
|
1403
|
+
### 7.8 How to choose, in practice
|
|
1404
|
+
|
|
1405
|
+
1. Start every agent on `balanced`.
|
|
1406
|
+
2. Demote to `router` any agent whose job is classification, extraction, or a
|
|
1407
|
+
fixed handoff — measure, do not guess.
|
|
1408
|
+
3. Promote to `careful` only after seeing a concrete failure that a stronger
|
|
1409
|
+
model actually fixes.
|
|
1410
|
+
4. Never fix an instruction-following bug with a model upgrade (§4.4).
|
|
1411
|
+
5. Raise reasoning effort before switching model families: it is a smaller,
|
|
1412
|
+
reversible change.
|
|
1413
|
+
|
|
1414
|
+
---
|
|
1415
|
+
|
|
1416
|
+
## 8. Evaluating changes
|
|
1417
|
+
|
|
1418
|
+
There is no compiler for prose. Substitutes, in order of value:
|
|
1419
|
+
|
|
1420
|
+
1. **`zen check`.** Most structural mistakes are load errors, and this is the
|
|
1421
|
+
fastest way to see all of them: it validates `agents.yaml`, checks that every
|
|
1422
|
+
prompt, skill and catalog it names is on disk, that hand-offs and tool
|
|
1423
|
+
selectors resolve, and that the models have credentials — without stopping at
|
|
1424
|
+
the first problem and without calling anything. `zen check --json` if you are
|
|
1425
|
+
parsing it.
|
|
1426
|
+
2. **Run the case that motivated the change**, plus one that must _not_ change.
|
|
1427
|
+
3. **Read the inspect report** — it shows the assembled prompt, every request and
|
|
1428
|
+
response, tool calls, skill activations and cost. Behaviour questions are
|
|
1429
|
+
answered there, not by re-reading the YAML.
|
|
1430
|
+
4. **Re-run `zen check` after any rename.** It is what catches the handoff, the
|
|
1431
|
+
skill or the prompt path that a rename silently unlinked.
|
|
1432
|
+
5. **Watch the token accounting.** A change that doubles prefix size is a
|
|
1433
|
+
regression even if the answer improved.
|
|
1434
|
+
6. **After changing a model id, exercise the capability it was chosen for** —
|
|
1435
|
+
send an image, force a tool call, ask for reasoning. `zen check` proves the
|
|
1436
|
+
credential resolves, not that the route serves images or honours `tools`; on a
|
|
1437
|
+
gateway that gap is a request-time 404 (§7.5).
|
|
1438
|
+
|
|
1439
|
+
CLI (`zen --help` for the authoritative list): `zen init`, `zen run`, `zen check`,
|
|
1440
|
+
`zen inspect`, `zen models`, `zen key`, `zen list`. **stdout is the answer, stderr
|
|
1441
|
+
is the narration**; every command takes `--json`. Exit codes: `0` ok, `1` failed,
|
|
1442
|
+
`2` usage, `3` invalid project, `4` no usable credential.
|
|
1443
|
+
|
|
1444
|
+
---
|
|
1445
|
+
|
|
1446
|
+
## 9. Review checklist
|
|
1447
|
+
|
|
1448
|
+
Before finishing any change here:
|
|
1449
|
+
|
|
1450
|
+
**Structure**
|
|
1451
|
+
|
|
1452
|
+
- [ ] `agents.yaml` still loads; no unknown keys, no dangling names
|
|
1453
|
+
- [ ] Top-level `default:` names the entry agent explicitly
|
|
1454
|
+
- [ ] Every agent has a `description:` written as a routing condition
|
|
1455
|
+
- [ ] No self-handoff; no accidental cycle back to the router
|
|
1456
|
+
- [ ] Names match `^[a-z0-9]+(?:[-_][a-z0-9]+)*$`
|
|
1457
|
+
- [ ] An agent expected to fan out has `fork:`, and its prompt says when to use it
|
|
1458
|
+
- [ ] Comments explain the design, not the runtime or the key they sit above
|
|
1459
|
+
|
|
1460
|
+
**Prompts**
|
|
1461
|
+
|
|
1462
|
+
- [ ] `INSTRUCTIONS.md` carries the shared architecture and nothing agent-specific
|
|
1463
|
+
- [ ] Nothing duplicated between `INSTRUCTIONS.md` and an agent prompt
|
|
1464
|
+
- [ ] Every tool and agent referenced by its exact name
|
|
1465
|
+
- [ ] Failure paths stated for every instruction that can fail
|
|
1466
|
+
- [ ] No facts, rates or figures embedded in a prompt
|
|
1467
|
+
- [ ] No hedging, no meta-talk about the runtime
|
|
1468
|
+
|
|
1469
|
+
**Skills**
|
|
1470
|
+
|
|
1471
|
+
- [ ] Every skill has a `description` that says _when it is needed_
|
|
1472
|
+
- [ ] `preload` is reserved for content the model would never decline
|
|
1473
|
+
- [ ] `preload` entries also appear in `allow` where `allow` is used
|
|
1474
|
+
- [ ] Catalog >~30 entries → `discovery: search`
|
|
1475
|
+
|
|
1476
|
+
**Tools**
|
|
1477
|
+
|
|
1478
|
+
- [ ] Every agent holds the narrowest set its job needs
|
|
1479
|
+
- [ ] An agent that only reads is not holding `write_file`, `apply_patch`,
|
|
1480
|
+
`move_file` or `delete_file` — subtract them from `workspace:*`
|
|
1481
|
+
- [ ] `sandbox:*` is granted only where a shell is actually needed
|
|
1482
|
+
- [ ] `sandbox.persist: true`, unless a throwaway rootfs is wanted on purpose
|
|
1483
|
+
- [ ] The `sandbox:` image carries what the work needs, rather than the prompt
|
|
1484
|
+
installing it every run
|
|
1485
|
+
- [ ] `sandbox.env` lists names only, and nothing credential-shaped
|
|
1486
|
+
- [ ] `exa:*` is granted only where the live web is actually needed, and the
|
|
1487
|
+
prompt says when to trust it over what the model already believes
|
|
1488
|
+
- [ ] Every granted tool the prompt expects is named in that prompt
|
|
1489
|
+
|
|
1490
|
+
**Models**
|
|
1491
|
+
|
|
1492
|
+
- [ ] Aliases are role names, not vendor names
|
|
1493
|
+
- [ ] Vendor knobs valid for the chosen vendor (no `api:` on gemini/anthropic)
|
|
1494
|
+
- [ ] No agent silently on the fallback `model:` when it needed a specific tier
|
|
1495
|
+
|
|
1496
|
+
**Cache and cost**
|
|
1497
|
+
|
|
1498
|
+
- [ ] No timestamps or run-specific text in the prefix
|
|
1499
|
+
- [ ] Prefix growth is intentional and worth it
|
|
1500
|
+
|
|
1501
|
+
**Secrets**
|
|
1502
|
+
|
|
1503
|
+
- [ ] No key literal in YAML, prompt, skill or log — `${VAR}` only
|
|
1504
|
+
- [ ] `.env` is git-ignored
|
|
1505
|
+
|
|
1506
|
+
---
|
|
1507
|
+
|
|
1508
|
+
## 10. Where to change what
|
|
1509
|
+
|
|
1510
|
+
| Symptom | Change this |
|
|
1511
|
+
| ------------------------------------------ | ---------------------------------------------------------- |
|
|
1512
|
+
| Wrong tone, wrong format, wrong length | `INSTRUCTIONS.md` (all agents) or the agent prompt |
|
|
1513
|
+
| Says something forbidden | `INSTRUCTIONS.md` prohibition, stated specifically |
|
|
1514
|
+
| Ignores a rule that only applies sometimes | Move the rule into a skill with a sharp description |
|
|
1515
|
+
| Never loads the skill it should | The skill's `description`; or `preload` it |
|
|
1516
|
+
| Loads too much, answers slowly | `allow:`, `maxIndexEntries:`, or `discovery: search` |
|
|
1517
|
+
| Invents a number | A skill holding the figure, or a command that computes it |
|
|
1518
|
+
| Rewrites a whole file to change one line | A prompt line preferring `apply_patch` — §3.6 |
|
|
1519
|
+
| Edits files it should only be reading | Subtract the mutating tools, or `zen run --read-only` |
|
|
1520
|
+
| Cannot run the build or the tests | Grant `sandbox:*`; pick an `image:` that has the toolchain |
|
|
1521
|
+
| Installs the same packages on every run | `sandbox.persist: true`, or set `sandbox.image` — §3.7 |
|
|
1522
|
+
| Answers from stale knowledge of the world | Grant `web_search` + `web_read`, and say when — §3.8 |
|
|
1523
|
+
| Cites a page it only saw the excerpt of | A prompt line: `web_read` before quoting — §3.8 |
|
|
1524
|
+
| Every web call refuses | No Exa key: `zen key add exa` — `zen check` warns — §3.8 |
|
|
1525
|
+
| Answers instead of routing | Router prompt prohibition; check `handoffs:` |
|
|
1526
|
+
| Routes to the wrong specialist | The target agents' `description:` fields |
|
|
1527
|
+
| Loses a detail after a handoff | Say it in the handoff; check the collapse policy |
|
|
1528
|
+
| Works through N independent items serially | `fork:` on that agent, and a prompt line — §6.4 |
|
|
1529
|
+
| Forks when the steps actually depend | Prompt line: branches cannot see each other |
|
|
1530
|
+
| Slow and expensive on trivial cases | Demote that agent's model tier / reasoning effort |
|
|
1531
|
+
| Fails only on genuinely hard cases | Promote that agent's tier, or split the hard path out |
|
|
1532
|
+
| Shows no reasoning while it works | Turn summaries on for that model — §7.6 |
|
|
1533
|
+
| Forgets across conversations | Continue the session rather than starting a new one |
|
|
1534
|
+
| Breaks at load with a named path | Read the message — it names the exact key |
|
|
1535
|
+
|
|
1536
|
+
---
|
|
1537
|
+
|
|
1538
|
+
## 11. Anti-patterns
|
|
1539
|
+
|
|
1540
|
+
- **The mega-prompt.** 300 lines covering twelve scenarios. Split into skills.
|
|
1541
|
+
- **Agent sprawl.** Eight agents that share one model, one tool set and one
|
|
1542
|
+
prompt style. Collapse into one with a catalog.
|
|
1543
|
+
- **Facts in prompts.** A fee schedule inside `INSTRUCTIONS.md`. It cannot be
|
|
1544
|
+
versioned, cannot be shared, and is paid for on every call.
|
|
1545
|
+
- **Politeness padding.** "Please try your best to be helpful." Costs tokens,
|
|
1546
|
+
changes nothing.
|
|
1547
|
+
- **Commented implementation notes.** `agents.yaml` explaining how skill
|
|
1548
|
+
discovery or prompt caching works. That is this file's job — §3.1.
|
|
1549
|
+
- **Fixing prompts with models.** See §4.4.
|
|
1550
|
+
- **The chatty router.** A router that answers before handing off, because its
|
|
1551
|
+
prompt never forbade it.
|
|
1552
|
+
- **Ping-pong handoffs.** Specialists that hand back to the router, which hands
|
|
1553
|
+
back to a specialist.
|
|
1554
|
+
- **Forking a chain.** Branches never see each other, so a fork whose second
|
|
1555
|
+
branch needs the first branch's answer is a sequence wearing a fork's clothes.
|
|
1556
|
+
- **Installing the toolchain every run.** A prompt that begins with `apt-get
|
|
1557
|
+
install` is an `image:` that was never set — §3.7.
|
|
1558
|
+
- **Volatile prefix.** "Current date: …" in `INSTRUCTIONS.md`. Permanent cache
|
|
1559
|
+
miss.
|
|
1560
|
+
|
|
1561
|
+
---
|
|
1562
|
+
|
|
1563
|
+
## 12. Maintaining this file
|
|
1564
|
+
|
|
1565
|
+
This file is loaded on every request in this repository, so it is subject to its
|
|
1566
|
+
own rules: stable, factual, no hedging. Before adding a section, ask whether it
|
|
1567
|
+
is true of every task — if it is not, it is a skill under `.github/skills/`, or
|
|
1568
|
+
a prompt file under `.github/prompts/` if it is a procedure (§2.4).
|
|
1569
|
+
|
|
1570
|
+
Update it when:
|
|
1571
|
+
|
|
1572
|
+
- the layout changes (a new directory, a moved catalog)
|
|
1573
|
+
- a model alias is added, retired or repointed — §7.2 must match `agents.yaml`
|
|
1574
|
+
- a vendor trap is discovered — add it to §7.7
|
|
1575
|
+
- a recurring review comment appears twice — turn it into a checklist line in §9
|
|
1576
|
+
- a runtime capability is added — describe it here, or it will not be used
|
|
1577
|
+
|
|
1578
|
+
Do not let it grow without pruning. When a section is only true of one agent, it
|
|
1579
|
+
belongs in that agent's prompt, not here.
|