@zenera/cli 1.1.10 → 1.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -15
- package/dist/banner.js +69 -47
- package/dist/cache.d.ts +98 -0
- package/dist/cache.js +301 -0
- package/dist/catalog.d.ts +3 -0
- package/dist/catalog.js +35 -11
- package/dist/commands/cache.d.ts +7 -0
- package/dist/commands/cache.js +245 -0
- package/dist/commands/check.js +6 -3
- package/dist/commands/index.js +5 -0
- package/dist/commands/init.js +8 -4
- package/dist/commands/key.js +68 -14
- package/dist/commands/memory.d.ts +3 -0
- package/dist/commands/memory.js +375 -0
- package/dist/commands/models.js +17 -1
- package/dist/commands/run.js +11 -3
- package/dist/commands/sandbox.js +70 -23
- package/dist/engine.js +14 -9
- package/dist/home.d.ts +2 -2
- package/dist/home.js +2 -2
- package/dist/keys.d.ts +22 -0
- package/dist/keys.js +105 -2
- package/dist/lib.d.ts +1 -0
- package/dist/lib.js +1 -0
- package/dist/liveness.js +11 -0
- package/dist/main.js +9 -0
- package/dist/podman.js +43 -9
- package/dist/projects.js +10 -2
- package/dist/resolve.d.ts +4 -0
- package/dist/resolve.js +43 -17
- package/dist/scaffold.js +22 -1
- package/dist/term.d.ts +23 -2
- package/dist/term.js +215 -8
- package/dist/tui/app.d.ts +10 -0
- package/dist/tui/app.js +478 -54
- package/dist/tui/theme.d.ts +13 -2
- package/dist/tui/theme.js +26 -13
- package/dist/tui/wrap.d.ts +92 -0
- package/dist/tui/wrap.js +147 -2
- package/dist/validate.d.ts +2 -0
- package/dist/validate.js +87 -2
- package/package.json +2 -2
- package/templates/editor/.github/copilot-instructions.md +200 -55
- package/templates/editor/.github/prompts/new-agent.prompt.md +5 -2
- package/templates/editor/.github/prompts/review-project.prompt.md +5 -0
- package/templates/editor/.github/prompts/sync-with-spec.prompt.md +11 -4
- package/templates/editor/.github/skills/zen-cli/SKILL.md +3 -0
- package/templates/editor/.github/skills/zen-cli/references/faker.md +18 -8
- package/templates/editor/.github/skills/zen-cli/references/keys.md +7 -7
- package/templates/editor/.github/skills/zen-cli/references/memory.md +113 -0
- package/templates/editor/.github/skills/zen-cli/references/rag.md +2 -2
- package/templates/editor/.github/skills/zen-cli/references/run.md +3 -1
- package/templates/editor/.github/skills/zen-memory/SKILL.md +555 -0
- package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +575 -0
- package/templates/editor/.github/skills/{api-schema-index → zen-rag-schema}/SKILL.md +4 -4
- package/templates/editor/.vscode/settings.json +4 -2
- package/templates/parts/exa.prompt.md.tmpl +4 -0
- package/templates/parts/exa.spec.md.tmpl +2 -0
- package/templates/project/INSTRUCTIONS.md +8 -0
- package/templates/project/SPECIFICATION.md.tmpl +47 -0
- package/templates/project/agents/prompts/{default.md → default.md.tmpl} +4 -0
- package/templates/project/agents.yaml.tmpl +4 -0
- package/templates/project/gitignore +7 -1
- package/templates/project/scripts/_setup.sh +101 -0
- package/templates/project/SPECIFICATION.md +0 -6
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: zen-memory
|
|
3
|
+
description: How agent memory is organised and how to configure it in `agents.yaml` — the `memory:` block, per-agent `access`/`sees`/`writes`/`autoRecall`, the four `memory_*` tools, kinds and relations, how recall ranks and stitches a subgraph, and how to design a memory strategy for a project. Includes what to commit and what never to, how to keep a working file under `/memory` and re-run it later, and how to put memory in front of a `zen rag schema` or `zen rag docs` index so a search that already succeeded once is not paid for again.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Memory
|
|
7
|
+
|
|
8
|
+
Memory is what an agent knows that nobody wrote down for it. A skill is
|
|
9
|
+
authored, reviewed and versioned; a memory is **produced by a run** and read by
|
|
10
|
+
the runs that follow. Both are knowledge, and they are not interchangeable —
|
|
11
|
+
anything authoritative belongs in a skill, where a human owns it.
|
|
12
|
+
|
|
13
|
+
It is a **graph**, not a list of rows. A node is one thing worth keeping — a
|
|
14
|
+
request, a plan, a fact, a script, a call that was made — and an edge says how
|
|
15
|
+
one led to another. That shape is the point: a script an agent wrote is useless
|
|
16
|
+
without the request that asked for it and the endpoint it calls, and a store
|
|
17
|
+
that can only return a ranked list leaves the joining to the model, every time.
|
|
18
|
+
|
|
19
|
+
It lives with the **project**, not the session. A memory that died with the
|
|
20
|
+
session would be a cache.
|
|
21
|
+
|
|
22
|
+
## Turning it on
|
|
23
|
+
|
|
24
|
+
Two keys, and the second one is what actually enables anything.
|
|
25
|
+
|
|
26
|
+
```yaml
|
|
27
|
+
memory: # where the graph lives, and what vectorises it
|
|
28
|
+
dir: memory
|
|
29
|
+
embedding: main
|
|
30
|
+
|
|
31
|
+
agents:
|
|
32
|
+
- name: auditor
|
|
33
|
+
memory: true # this agent gets the tools and auto-recall
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
That is the whole feature working the obvious way: the agent can search, load
|
|
37
|
+
and commit, and it recalls automatically before answering new user input.
|
|
38
|
+
|
|
39
|
+
The top-level block is optional — an agent that says `memory: true` gets the
|
|
40
|
+
defaults without one. A project that mentions memory nowhere opens no store and
|
|
41
|
+
creates no directory.
|
|
42
|
+
|
|
43
|
+
### `memory:` — the graph itself
|
|
44
|
+
|
|
45
|
+
| Field | Type | Default | Meaning |
|
|
46
|
+
| ----------- | ------------- | --------------- | ----------------------------------- |
|
|
47
|
+
| `dir` | path | `memory` | Relative to the project root |
|
|
48
|
+
| `embedding` | embedding ref | the top-level | The vectoriser recall searches with |
|
|
49
|
+
| `kinds` | name[] | the seven below | **Replaces** the node vocabulary |
|
|
50
|
+
| `relations` | name[] | the four below | **Replaces** the edge vocabulary |
|
|
51
|
+
|
|
52
|
+
**Set `embedding`.** Without one, recall falls back to term overlap: it finds a
|
|
53
|
+
memory phrased the way the query happened to be phrased and misses the rest,
|
|
54
|
+
which is the failure mode that makes people conclude memory does not work. The
|
|
55
|
+
width is learned from the first response rather than declared, and changing the
|
|
56
|
+
model afterwards is **refused** rather than silently mixing two vector spaces —
|
|
57
|
+
so choose it before the graph has anything in it.
|
|
58
|
+
|
|
59
|
+
```yaml
|
|
60
|
+
embeddings:
|
|
61
|
+
main: openai:text-embedding-3-small
|
|
62
|
+
memory:
|
|
63
|
+
embedding: main
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`zen memory stats` says `embedding none — recall falls back to term overlap`
|
|
67
|
+
when this was skipped, and a vector count below the node count means some nodes
|
|
68
|
+
were committed before an embedder existed.
|
|
69
|
+
|
|
70
|
+
### `agents[].memory` — who may see and write what
|
|
71
|
+
|
|
72
|
+
```yaml
|
|
73
|
+
agents:
|
|
74
|
+
- name: auditor
|
|
75
|
+
memory:
|
|
76
|
+
access: read-write
|
|
77
|
+
sees: [audit]
|
|
78
|
+
writes: [audit]
|
|
79
|
+
autoRecall: { limit: 3 }
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
| Field | Type | Default | Meaning |
|
|
83
|
+
| ------------ | -------------------------- | --------------- | -------------------------------------------- |
|
|
84
|
+
| `access` | `read`/`read-write`/`full` | `read-write` | Which of the four tools this agent gets |
|
|
85
|
+
| `sees` | name[] | `[]` | Private slices it may read, on top of `*` |
|
|
86
|
+
| `writes` | name[] | `[*]` | Labels it may commit under |
|
|
87
|
+
| `autoRecall` | boolean or `{ limit }` | `true`, limit 5 | Recall before a turn that follows user input |
|
|
88
|
+
|
|
89
|
+
`memory: true` is shorthand for all four defaults.
|
|
90
|
+
|
|
91
|
+
## The four tools
|
|
92
|
+
|
|
93
|
+
They are **not** listed in an agent's `tools:`. They are derived from the
|
|
94
|
+
binding and appear when it exists, so `access` is the only thing that decides
|
|
95
|
+
which of them the model is offered:
|
|
96
|
+
|
|
97
|
+
| Level | Tools |
|
|
98
|
+
| ------------ | ------------------------------- |
|
|
99
|
+
| `read` | `memory_search`, `memory_load` |
|
|
100
|
+
| `read-write` | the above, plus `memory_commit` |
|
|
101
|
+
| `full` | the above, plus `memory_forget` |
|
|
102
|
+
|
|
103
|
+
| Tool | Does |
|
|
104
|
+
| --------------- | -------------------------------------------------------------------------- |
|
|
105
|
+
| `memory_search` | Ranks, then stitches. Returns an outline of a subgraph and a legend of ids |
|
|
106
|
+
| `memory_load` | Reads whole nodes by id. **The only call that counts as use** |
|
|
107
|
+
| `memory_commit` | Writes a subgraph — nodes and the edges between them — in one transaction |
|
|
108
|
+
| `memory_forget` | Removes nodes, their vectors and their file bytes together |
|
|
109
|
+
|
|
110
|
+
Search and load are split on purpose. Search hands back context the model never
|
|
111
|
+
asked for; counting that as use would poison recency ranking. Commit is one
|
|
112
|
+
call because a remembered thing is a subgraph, and building it with three calls
|
|
113
|
+
leaves the graph half-formed when the model stops early.
|
|
114
|
+
|
|
115
|
+
The system prompt already explains all of this to the agent — how to read a
|
|
116
|
+
recollection, when committing is worthwhile, why correction is a new node. That
|
|
117
|
+
text is **derived from `access`**, so an agent that cannot write is never told
|
|
118
|
+
how to. Do not restate it in a prompt; state project-specific policy instead.
|
|
119
|
+
|
|
120
|
+
## How it is organised
|
|
121
|
+
|
|
122
|
+
### The vocabulary
|
|
123
|
+
|
|
124
|
+
Seven kinds and four relations, both closed sets, offered to the model as enums.
|
|
125
|
+
That is what keeps a graph built by a language model still queryable a month
|
|
126
|
+
later.
|
|
127
|
+
|
|
128
|
+
| Kind | Is |
|
|
129
|
+
| ------------ | -------------------------------------------------------------- |
|
|
130
|
+
| `task` | a request that started a piece of work |
|
|
131
|
+
| `plan` | an approach that worked, worth reusing on a similar request |
|
|
132
|
+
| `fact` | a durable truth about the environment or the domain |
|
|
133
|
+
| `snippet` | a short piece of code or configuration, too small to be a file |
|
|
134
|
+
| `file` | an artifact kept whole under `/memory` and re-runnable |
|
|
135
|
+
| `operation` | an external call that was made — an endpoint, command or query |
|
|
136
|
+
| `preference` | a standing instruction from the user that applies to every run |
|
|
137
|
+
|
|
138
|
+
| Relation | Reads as | Effect on recall |
|
|
139
|
+
| ------------ | --------------------------------------------------------- | ------------------------------- |
|
|
140
|
+
| `PRODUCED` | the source led to the target being made (task → artifact) | spine — pulled in first |
|
|
141
|
+
| `CALLS` | the source invokes the target | spine |
|
|
142
|
+
| `SUPERSEDES` | the source replaces the target; the target is stale | spine, and **hides** the target |
|
|
143
|
+
| `INFORMED` | the source was context the target was built from | background — dropped first |
|
|
144
|
+
|
|
145
|
+
Override the vocabularies only if the defaults genuinely do not fit the domain.
|
|
146
|
+
A wider vocabulary is a vaguer one, and a kind nothing else understands is a
|
|
147
|
+
kind no other agent can search for. `kinds` **replaces** the built-in set, with
|
|
148
|
+
one exception: `preference` is always available, because the engine itself reads
|
|
149
|
+
that kind to build the system prompt.
|
|
150
|
+
|
|
151
|
+
### Every node
|
|
152
|
+
|
|
153
|
+
| Field | Why it matters |
|
|
154
|
+
| ------------------------ | ----------------------------------------------------------- |
|
|
155
|
+
| `text` | the only thing ranked, and the only thing embedded |
|
|
156
|
+
| `kind` | filters a search, and decides how it renders |
|
|
157
|
+
| `audience` | who may see it. `['*']` is everyone; denied by default |
|
|
158
|
+
| `createdAt` | provenance, and what `newer_than` filters on |
|
|
159
|
+
| `lastUsedAt`, `useCount` | bumped by `memory_load` alone — the input to recency |
|
|
160
|
+
| `revision` | optimistic concurrency, via `expected_revision` on a commit |
|
|
161
|
+
| `file` | the remembered bytes, if any |
|
|
162
|
+
|
|
163
|
+
### How recall actually works
|
|
164
|
+
|
|
165
|
+
1. **Rank.** The query is embedded and compared against node texts. Anything
|
|
166
|
+
scoring below a fixed floor of `0.15` is noise and is dropped. Superseded
|
|
167
|
+
nodes are excluded, and so are `preference` nodes unless asked for by kind.
|
|
168
|
+
2. **Order by recency, but do not admit by it.** Relevance alone clears the
|
|
169
|
+
threshold; decay by `lastUsedAt` only breaks the tie afterwards. Memory that
|
|
170
|
+
keeps proving useful surfaces ahead of memory written once and never read —
|
|
171
|
+
without anything ageing out of reach however well it matches.
|
|
172
|
+
3. **Stitch.** Breadth-first from every seed at once, ignoring edge direction,
|
|
173
|
+
because a memory is as often reached from the artifact back to the request as
|
|
174
|
+
the other way.
|
|
175
|
+
4. **Spend the budget in tiers**: seeds, then the spine (`PRODUCED`, `CALLS`,
|
|
176
|
+
`SUPERSEDES`), then `INFORMED` context. A tight budget therefore degrades by
|
|
177
|
+
dropping background rather than by truncating the answer.
|
|
178
|
+
|
|
179
|
+
Defaults: 5 seeds, 2 hops, 25 nodes. `memory_search` takes `limit`, `max_hops`,
|
|
180
|
+
`max_nodes`, `kinds` and `newer_than` to move them.
|
|
181
|
+
|
|
182
|
+
What comes back is a `<memory-recollection>` block: an indented outline where
|
|
183
|
+
the indentation _is_ the graph, each line reading `score kind id` or
|
|
184
|
+
`→relation kind id`, with each node's text under it **clipped to 160
|
|
185
|
+
characters**. That clip is a design constraint on how you write a node — see
|
|
186
|
+
below.
|
|
187
|
+
|
|
188
|
+
### On disk
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
<project>/memory/
|
|
192
|
+
├── manifest.json written LAST — a half-written memory reads as empty
|
|
193
|
+
├── graph.json nodes and edges, parsed whole on open
|
|
194
|
+
├── vectors.f32 raw little-endian float32, a quarter the size of JSON
|
|
195
|
+
├── vectors.json the ids those vectors belong to
|
|
196
|
+
├── files/ remembered bytes, one per node that has any
|
|
197
|
+
└── .lock held while a run is writing; a dead pid's lock is stale
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
It is project state, not source, and it is not committed. The directory is
|
|
201
|
+
mounted **read-only** at `/memory` in the agent's namespace.
|
|
202
|
+
|
|
203
|
+
## Designing a memory strategy
|
|
204
|
+
|
|
205
|
+
### The test for what to remember
|
|
206
|
+
|
|
207
|
+
**Would the next run otherwise have to work this out again?** That is the whole
|
|
208
|
+
question. A script that ran, an interface that behaved differently from its
|
|
209
|
+
documentation, a call sequence that turned out to be necessary — yes. The
|
|
210
|
+
request restated, progress notes, anything unverified, anything a search already
|
|
211
|
+
returned — no.
|
|
212
|
+
|
|
213
|
+
The second test is **would it still be true next month**. Memory has no expiry
|
|
214
|
+
and no reviewer. A fact about the environment survives; a fact about today's
|
|
215
|
+
data does not, and committing one is how a graph becomes confidently wrong.
|
|
216
|
+
|
|
217
|
+
### Write the text for the question, not for the answer
|
|
218
|
+
|
|
219
|
+
Recall ranks a node's `text` against **the user's next message**, because that
|
|
220
|
+
is what auto-recall embeds. So the text has to be phrased the way somebody will
|
|
221
|
+
ask for it, not the way the system that produced it names it.
|
|
222
|
+
|
|
223
|
+
| Instead of | Write |
|
|
224
|
+
| --------------------------------- | ------------------------------------------------------------------------------- |
|
|
225
|
+
| `POST /v2/subs/{id}:cancel — 204` | `Cancelling a subscription: POST /v2/subs/{id}:cancel, 204, no body` |
|
|
226
|
+
| `fix for the 409` | `A duplicate customer email returns 409, not 422 — retry is wrong` |
|
|
227
|
+
| `script.py` | `Weekly invoice reconciliation script — reads billing CSV, writes a diff table` |
|
|
228
|
+
|
|
229
|
+
Front-load the identifying facts: the first 160 characters are what appears in
|
|
230
|
+
a recollection, and everything after that costs a `memory_load` to see.
|
|
231
|
+
|
|
232
|
+
### The shape of a good commit
|
|
233
|
+
|
|
234
|
+
One call, several nodes, and the edges that explain them:
|
|
235
|
+
|
|
236
|
+
```
|
|
237
|
+
task "reconcile invoices against the ledger for a month"
|
|
238
|
+
PRODUCED → file the script that did it
|
|
239
|
+
INFORMED ← fact "ledger exports use minor units; invoices use decimals"
|
|
240
|
+
file
|
|
241
|
+
CALLS → operation "GET /v2/invoices?created[gte]= — cursor paging"
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Committed separately, a later run finds a script with no idea why it exists, or
|
|
245
|
+
a fact with nothing that acts on it. Committed together, one hit on any of them
|
|
246
|
+
brings the rest.
|
|
247
|
+
|
|
248
|
+
**Never edit a memory to correct it.** Commit the new one and link it to the old
|
|
249
|
+
with `SUPERSEDES`. Recall follows the edge forward and hides what it points at,
|
|
250
|
+
so the wrong answer stops being served without being destroyed — and the reason
|
|
251
|
+
a thing changed is often the useful part. `zen memory ls --stale` is the list of
|
|
252
|
+
corrections that have happened.
|
|
253
|
+
|
|
254
|
+
A new node whose text is a near-duplicate (cosine ≥ 0.97) of one already there
|
|
255
|
+
is **folded into it**, and the existing id comes back under the model's ref, so
|
|
256
|
+
the links still land on the memory that was already present.
|
|
257
|
+
|
|
258
|
+
### Audiences: one graph, masked — not one graph each
|
|
259
|
+
|
|
260
|
+
Every node carries an `audience`; `sees` is the set an agent reads. `*` is the
|
|
261
|
+
public slice and is always included, so a binding can only ever **widen** what
|
|
262
|
+
an agent sees, never hide the common ground. An invisible node is
|
|
263
|
+
indistinguishable from a missing one, deliberately, so a mask cannot be probed
|
|
264
|
+
by id.
|
|
265
|
+
|
|
266
|
+
Decide audiences before you decide agents. The useful question is not "which
|
|
267
|
+
agent wrote this" but "which agent would be misled by this":
|
|
268
|
+
|
|
269
|
+
| Situation | Configuration |
|
|
270
|
+
| ------------------------------------------------- | ----------------------------------------- |
|
|
271
|
+
| Everything is common ground | `memory: true` everywhere. Do this first |
|
|
272
|
+
| A specialist accumulates jargon nobody else needs | `sees: [x]`, `writes: [x]` on that agent |
|
|
273
|
+
| A reviewer should read a slice but not add to it | `sees: [x]`, `writes: []`, `access: read` |
|
|
274
|
+
| A summariser should never write at all | `access: read` |
|
|
275
|
+
| One curator may delete | `access: full` on exactly one agent |
|
|
276
|
+
|
|
277
|
+
When `writes` names exactly one label, the tool schema omits the field and
|
|
278
|
+
applies it — there is nothing to choose and nothing for the model to get wrong.
|
|
279
|
+
Neither `sees` nor `writes` is ever a tool parameter: an agent that could name
|
|
280
|
+
its own audience could read another agent's slice just by asking.
|
|
281
|
+
|
|
282
|
+
Keep `access: full` rare. Forgetting is almost never the right correction, and a
|
|
283
|
+
model that can delete will occasionally tidy.
|
|
284
|
+
|
|
285
|
+
### Auto-recall
|
|
286
|
+
|
|
287
|
+
On by default, because an agent that has to remember to go looking mostly does
|
|
288
|
+
not. It fires **after new user input and after a hand-off** — not before every
|
|
289
|
+
call, because recalling on every turn costs tokens and defeats prompt caching
|
|
290
|
+
for a marginal gain. A turn following a tool result does not re-recall.
|
|
291
|
+
|
|
292
|
+
| Setting | For |
|
|
293
|
+
| ---------------- | ------------------------------------------------------------------------ |
|
|
294
|
+
| `true` (default) | Almost everything |
|
|
295
|
+
| `{ limit: 2 }` | A cheap model, or a graph big enough that five seeds is a page |
|
|
296
|
+
| `{ limit: 8 }` | A research agent where breadth beats prompt size |
|
|
297
|
+
| `false` | An agent that should decide for itself when to look — it keeps the tools |
|
|
298
|
+
|
|
299
|
+
`autoRecall: false` is right for a tool-driven agent whose first turn is
|
|
300
|
+
mechanical, and for anything where the user's message is a payload rather than a
|
|
301
|
+
question.
|
|
302
|
+
|
|
303
|
+
### Preferences
|
|
304
|
+
|
|
305
|
+
A `preference` node is a standing instruction — "always report findings as a
|
|
306
|
+
table", "use ISO dates". These are **not recalled by similarity**, because they
|
|
307
|
+
are not about the request in front of the agent: an instruction on how to report
|
|
308
|
+
is nowhere near a question about gateway rules in embedding space, and a search
|
|
309
|
+
would never surface it.
|
|
310
|
+
|
|
311
|
+
So they are listed rather than ranked, and rendered into the system prompt with
|
|
312
|
+
their ids:
|
|
313
|
+
|
|
314
|
+
```
|
|
315
|
+
<memory-preferences>
|
|
316
|
+
- report findings as a table with a severity column [01JD9Q7X8N2K4M6P8R0T2V4W6Y]
|
|
317
|
+
</memory-preferences>
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
They land in the cached prefix, so they cost once per run rather than once per
|
|
321
|
+
turn, and the id is what lets a model replace one with `SUPERSEDES` instead of
|
|
322
|
+
quietly ignoring it. They are left out of ordinary recall on purpose — they are
|
|
323
|
+
in the prompt already.
|
|
324
|
+
|
|
325
|
+
Commit one only when the user's own words generalise: "always", "from now on",
|
|
326
|
+
"I prefer", "never". A single request being fulfilled is not a standing
|
|
327
|
+
instruction, and a graph full of false preferences is a system prompt nobody
|
|
328
|
+
wrote.
|
|
329
|
+
|
|
330
|
+
## Remembering files, and running them again
|
|
331
|
+
|
|
332
|
+
The workspace is disposable — it may be a container about to be discarded — so a
|
|
333
|
+
reference to a path in it dangles the moment the run ends. `memory_commit` takes
|
|
334
|
+
a `file` argument on a node instead, and copies the **bytes** out:
|
|
335
|
+
|
|
336
|
+
```json
|
|
337
|
+
{
|
|
338
|
+
"nodes": [
|
|
339
|
+
{
|
|
340
|
+
"ref": "s",
|
|
341
|
+
"kind": "file",
|
|
342
|
+
"text": "Invoice reconciliation script — reads the billing CSV, writes a diff table",
|
|
343
|
+
"file": "reconcile.py"
|
|
344
|
+
},
|
|
345
|
+
{ "ref": "t", "kind": "task", "text": "reconcile invoices against the ledger for March" }
|
|
346
|
+
],
|
|
347
|
+
"edges": [{ "from": "t", "to": "s", "relation": "PRODUCED" }]
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
The path is the one the **file tools** use, resolved the same way. The copy
|
|
352
|
+
lands at `<project>/memory/files/<id>.<ext>` and appears to the agent as:
|
|
353
|
+
|
|
354
|
+
```
|
|
355
|
+
/memory/01JD9Q7X8N2K4M6P8R0T2V4W6Y.py
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
| Rule | Value |
|
|
359
|
+
| --------- | ---------------------------------------------------------------- |
|
|
360
|
+
| Size cap | 2 MiB. Over it, keep the artifact and commit a `snippet` instead |
|
|
361
|
+
| Inlined | under 32 KiB — `memory_load` returns the contents |
|
|
362
|
+
| Larger | `memory_load` returns the path; open or run it |
|
|
363
|
+
| Mount | `/memory`, **read-only** |
|
|
364
|
+
| Extension | preserved, because it decides how the file runs and renders |
|
|
365
|
+
|
|
366
|
+
Reusing one is the point of keeping it:
|
|
367
|
+
|
|
368
|
+
```
|
|
369
|
+
run_command python3 /memory/01JD9Q7X8N2K4M6P8R0T2V4W6Y.py
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Which means the file has to be **self-contained**. A script that reads
|
|
373
|
+
`../data/latest.csv` or imports from the workspace will not run next week; one
|
|
374
|
+
that takes its inputs as arguments will. If it needs a credential, it should
|
|
375
|
+
read it from the environment and say so in the node text.
|
|
376
|
+
|
|
377
|
+
Nothing writes to `/memory` through the file tools, so every file under it has a
|
|
378
|
+
node explaining what it is and the graph cannot acquire orphans. To change a
|
|
379
|
+
remembered file: copy it into the workspace, edit it there, commit the new one
|
|
380
|
+
and link it with `SUPERSEDES` to the old.
|
|
381
|
+
|
|
382
|
+
| Commit as | When |
|
|
383
|
+
| --------- | ------------------------------------------------------------------------- |
|
|
384
|
+
| `file` | It is runnable, or it is the artifact itself: a script, a config, a query |
|
|
385
|
+
| `snippet` | It is a few lines that only make sense read: a request body, a fragment |
|
|
386
|
+
| `fact` | It is a sentence. A file containing one sentence is a fact in a costume |
|
|
387
|
+
|
|
388
|
+
## Memory in front of a `zen rag` index
|
|
389
|
+
|
|
390
|
+
This is the highest-value thing memory does in a project that has an index, and
|
|
391
|
+
it is entirely a matter of **what you commit**.
|
|
392
|
+
|
|
393
|
+
### The division of labour
|
|
394
|
+
|
|
395
|
+
| | A schema/docs index | Memory |
|
|
396
|
+
| --------------- | --------------------------------------------------------------- | --------------------------------- |
|
|
397
|
+
| Holds | the API or the documents, completely | what earlier runs worked out |
|
|
398
|
+
| Authority | the source of truth | a record, never a substitute |
|
|
399
|
+
| Completeness | exact and countable | a ranked, masked slice |
|
|
400
|
+
| Freshness | as fresh as the last `index` | as stale as it was allowed to get |
|
|
401
|
+
| Cost of a query | one embedding round trip (`search`), or nothing (`list`/`grep`) | already in the prompt |
|
|
402
|
+
|
|
403
|
+
So: **never copy index content into memory.** A schema pasted into a node goes
|
|
404
|
+
stale silently and the index already answers in milliseconds; a documentation
|
|
405
|
+
passage quoted into a node outlives the release it was true for and will look
|
|
406
|
+
exactly as authoritative when it is wrong.
|
|
407
|
+
|
|
408
|
+
What memory should hold is the **route** — the resolution, not the source:
|
|
409
|
+
|
|
410
|
+
| Commit | Kind | Because |
|
|
411
|
+
| --------------------------------------------------------------------------------- | ------------------ | --------------------------------------------------------------------------------- |
|
|
412
|
+
| The endpoint that turned out to be the answer, and its shape | `operation` | A `search_api` costs a round trip and returns five ranked guesses; this names one |
|
|
413
|
+
| The sequence of calls that was necessary | `plan` | No ranking finds an ordering. It is not in the document |
|
|
414
|
+
| Where the answer lives in the docs — document name, heading, line range | `fact` | `zen rag docs show` reads it back verbatim and current |
|
|
415
|
+
| A convention the index cannot state — auth, casing, base path, the error envelope | `fact` | It is not in the graph, and it is wrong to re-derive it every run |
|
|
416
|
+
| Where the index itself is | `fact` | `-d` or `$ZEN_SCHEMA_DB`. An agent cannot guess a path |
|
|
417
|
+
| A working request body, or a script that drives the API | `snippet` / `file` | The thing you would otherwise write again |
|
|
418
|
+
| That a search returned nothing, and `grep` confirmed absence | `fact` | Proving a negative twice is pure waste |
|
|
419
|
+
|
|
420
|
+
### Why it makes search cheaper
|
|
421
|
+
|
|
422
|
+
Auto-recall fires **before the model's first tool call of the turn**. So the
|
|
423
|
+
recollection arrives before the decision to search is made — and a recalled
|
|
424
|
+
`operation` node naming `POST /v2/subs/{id}:cancel` removes the search
|
|
425
|
+
entirely, rather than making it faster. When the recollection is only close, it
|
|
426
|
+
still narrows: the model reaches for `list_api --name` or `grep_api` instead of
|
|
427
|
+
`search_api`, which are local, exact, and cost nothing.
|
|
428
|
+
|
|
429
|
+
Phrase these nodes accordingly. The next question will arrive as _"how do I
|
|
430
|
+
cancel someone's subscription?"_, not as `cancelSubscription`, so the node text
|
|
431
|
+
must contain the human phrasing **and** the identifiers. Put the question first
|
|
432
|
+
and the answer second.
|
|
433
|
+
|
|
434
|
+
### Keeping it honest against re-indexing
|
|
435
|
+
|
|
436
|
+
An index is rebuilt when the source changes; memory is not. Two habits keep the
|
|
437
|
+
two from drifting:
|
|
438
|
+
|
|
439
|
+
1. **Name the version in the node text** — "Billing API v2", "acme\_4.1.0 docs".
|
|
440
|
+
A recollection about a version the project no longer uses is then visibly
|
|
441
|
+
about the wrong thing rather than invisibly wrong.
|
|
442
|
+
2. **Supersede after a re-index.** When a route changes, commit the new
|
|
443
|
+
`operation` node with `SUPERSEDES` pointing at the old one. `zen memory ls
|
|
444
|
+
--kind operation` is the review list.
|
|
445
|
+
|
|
446
|
+
And state the rule in the project's own skill for the index: **verify against
|
|
447
|
+
the index before acting on a recalled route.** Recall is a shortcut past the
|
|
448
|
+
search, not past the truth. One `list_api --name` or `zen rag docs show` is
|
|
449
|
+
cheap and settles it.
|
|
450
|
+
|
|
451
|
+
### The project skill is where this is written down
|
|
452
|
+
|
|
453
|
+
An index used by a project needs a skill in that project describing it — see
|
|
454
|
+
`zen-rag-schema` and `zen-rag-docs`. Memory policy belongs in the same skill,
|
|
455
|
+
because it is the same subject:
|
|
456
|
+
|
|
457
|
+
```md
|
|
458
|
+
- Recalled routes are a shortcut, not authority. Confirm with `list_api --name`
|
|
459
|
+
before writing a call against one.
|
|
460
|
+
- After solving something the index made you work for, commit it: the
|
|
461
|
+
`operation` you ended on, linked to the `task` that asked and any `fact` you
|
|
462
|
+
had to discover. Phrase the text as the question, not as the operation id.
|
|
463
|
+
- Never commit schema text or documentation passages. Commit the pointer —
|
|
464
|
+
document, heading, line range — and read it back with `zen rag docs show`.
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
## Inspecting and repairing it
|
|
468
|
+
|
|
469
|
+
```
|
|
470
|
+
zen memory [stats|ls|show|export|forget] [args] [options]
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
`zen memory export --open` is the one to reach for: one self-contained HTML
|
|
474
|
+
page with every node, the graph as a diagram, and the remembered file's actual
|
|
475
|
+
content in the detail pane. It reads **unmasked** — when the mask is what is
|
|
476
|
+
wrong, the hidden part is exactly the part you need — and it contacts no model,
|
|
477
|
+
so inspection is free and offline.
|
|
478
|
+
|
|
479
|
+
| Question | Command |
|
|
480
|
+
| ---------------------------------------- | -------------------------------- |
|
|
481
|
+
| Is memory even on, and is it embedded? | `zen memory stats` |
|
|
482
|
+
| What has this project learned? | `zen memory export --open` |
|
|
483
|
+
| What is in one private slice? | `zen memory ls --audience audit` |
|
|
484
|
+
| What has been corrected? | `zen memory ls --stale` |
|
|
485
|
+
| What files are being kept? | `zen memory ls --files` |
|
|
486
|
+
| Why was that recalled? | `zen memory show <id>` |
|
|
487
|
+
| That should never have been written down | `zen memory forget <id>` |
|
|
488
|
+
|
|
489
|
+
`zen inspect` answers the other half: a run report shows the recollection block
|
|
490
|
+
exactly as the model received it, which is how you tell "memory had nothing"
|
|
491
|
+
apart from "memory had it and the model ignored it".
|
|
492
|
+
|
|
493
|
+
## A worked configuration
|
|
494
|
+
|
|
495
|
+
```yaml
|
|
496
|
+
embeddings:
|
|
497
|
+
main: openai:text-embedding-3-small
|
|
498
|
+
|
|
499
|
+
memory:
|
|
500
|
+
dir: memory
|
|
501
|
+
embedding: main
|
|
502
|
+
|
|
503
|
+
agents:
|
|
504
|
+
- name: triage # front door: reads everything, writes the common ground
|
|
505
|
+
memory: true
|
|
506
|
+
|
|
507
|
+
- name: api-worker # does the work; keeps its own routes and scripts
|
|
508
|
+
memory:
|
|
509
|
+
access: read-write
|
|
510
|
+
sees: [api]
|
|
511
|
+
writes: [api]
|
|
512
|
+
autoRecall: { limit: 8 }
|
|
513
|
+
|
|
514
|
+
- name: reporter # formats the answer; must not invent memories
|
|
515
|
+
memory:
|
|
516
|
+
access: read
|
|
517
|
+
autoRecall: false
|
|
518
|
+
|
|
519
|
+
- name: curator # the only thing that may delete
|
|
520
|
+
memory:
|
|
521
|
+
access: full
|
|
522
|
+
sees: [api]
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
## Review checklist
|
|
526
|
+
|
|
527
|
+
- [ ] `memory.embedding` is set, and was set before the graph had content.
|
|
528
|
+
- [ ] Every agent that should learn has a `memory:` binding — the top-level
|
|
529
|
+
block alone enables nothing.
|
|
530
|
+
- [ ] `access: full` appears at most once.
|
|
531
|
+
- [ ] `sees`/`writes` exist because an agent would be **misled** by the other
|
|
532
|
+
slice, not merely because it does not need it.
|
|
533
|
+
- [ ] `autoRecall: false` on any agent whose user message is a payload.
|
|
534
|
+
- [ ] Nothing authoritative is expected to live here — that is a skill's job.
|
|
535
|
+
- [ ] The project's index skill says to verify a recalled route.
|
|
536
|
+
- [ ] `zen memory export --open` after a week of use, to see what it actually
|
|
537
|
+
learned rather than what you hoped.
|
|
538
|
+
|
|
539
|
+
## When it goes wrong
|
|
540
|
+
|
|
541
|
+
| Symptom | Cause |
|
|
542
|
+
| ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
543
|
+
| The `memory_*` tools are missing | No `memory:` on the agent. They come from the binding, never from `tools:` |
|
|
544
|
+
| `zen memory` says the project has none | Neither a `memory:` block nor an agent binding — nothing is opened and no directory is made |
|
|
545
|
+
| Recall finds a memory only when reworded | No embedder. `zen memory stats` says `embedding none` |
|
|
546
|
+
| Recall finds nothing after changing the embedder | Refused rather than mixed — a manifest records the model. Re-embed or change it back |
|
|
547
|
+
| Vectors fewer than nodes | Some nodes were committed with no embedder; they are only reachable by term overlap |
|
|
548
|
+
| The graph fills with restated requests | The commit rule is not in a prompt or skill. State the "would a later run redo this" test |
|
|
549
|
+
| A wrong memory keeps coming back | It was edited instead of superseded, or superseded in the wrong direction — the **new** node is the source |
|
|
550
|
+
| An agent cannot see a node you can | Its `audience` is not in that agent's `sees`. Invisible and missing are the same thing, on purpose |
|
|
551
|
+
| "this agent cannot remember files" | The agent has no workspace, so a path cannot be resolved. Commit without `file` |
|
|
552
|
+
| A remembered file will not run | It is not self-contained — it referenced the workspace it was written in |
|
|
553
|
+
| Recollections are large and unhelpful | `autoRecall.limit` is too high, or node texts describe answers rather than questions |
|
|
554
|
+
| The model recalls a route the API no longer has | Nothing re-checks memory against a rebuilt index. Supersede, and require verification |
|
|
555
|
+
| A second run of the same project refuses to start | The directory lock. A lock whose process is gone is stale and is taken over |
|