@yaag/extension 0.10.0 → 0.11.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/docs/authoring.md CHANGED
@@ -114,6 +114,26 @@ program again and replays the recorded Asks. A resume needs the program too,
114
114
  because a Cassette holds the history of a Run and never the program. See
115
115
  [Examples](examples.md#05--record-and-resume).
116
116
 
117
+ ## Fork and compaction
118
+
119
+ `handle.compact()` replaces an Agent's context with a summary of it, and
120
+ reports the context size before and after. `handle.fork()` spawns a new Agent
121
+ from a copy of this Agent's conversation: use it when one expensive Agent
122
+ builds context that several cheap Agents need. Both need a settled Ask
123
+ boundary, so a call during an Ask fails with `COMPACT_DURING_ASK` or
124
+ `FORK_DURING_ASK`.
125
+
126
+ - An Agent that exited cleanly stays forkable: build context, close the Agent,
127
+ then fork its final state. An Agent killed during an Ask fails `FORK_REFUSED`.
128
+ - The fork inherits every spawn option of its source, including the concrete
129
+ model it settled on, but not its name. Pass any spawn option to change that.
130
+ The source becomes the fork's parent; pass `parent` to place it elsewhere.
131
+ - `fork({ compact: true })` compacts the copy before you get the Handle; a
132
+ string becomes the compaction instructions.
133
+ - A fork of a Worktree Agent gets its own worktree, branched from the source's
134
+ branch, so only committed work transfers. Forking needs a pi that supports
135
+ `pi --fork`. See [Examples](examples.md#07--fork-and-compaction).
136
+
117
137
  ## Lineage
118
138
 
119
139
  `parent` names the Agent a spawn belongs under. It is data only: it sets the
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Build context once, then work many times. The builder reads the repository;
3
+ * each worker forks the builder's conversation, so it starts with that context
4
+ * without paying to read the repository again.
5
+ */
6
+ import { defineRun, prompt } from "@yaag/runtime";
7
+
8
+ const TOPICS = ["error handling", "testing", "naming"] as const;
9
+
10
+ export default defineRun({
11
+ name: "fork",
12
+ description: "Forks one context-building Agent into several cheap workers.",
13
+ async run(ctx) {
14
+ const builder = await ctx.spawn({ name: "builder" });
15
+ await builder.ask(prompt`
16
+ Read this repository and describe how it is organized. Report one
17
+ paragraph.
18
+ `);
19
+ const notes = await Promise.all(
20
+ TOPICS.map(async (topic) => {
21
+ // The fork inherits the builder's options and its whole conversation.
22
+ // `compact` summarizes that conversation before the worker starts.
23
+ const worker = await builder.fork({ name: topic.split(" ")[0], compact: true });
24
+ return worker.ask(prompt`From what you already read, report on ${topic}.`);
25
+ }),
26
+ );
27
+ return notes.join("\n\n");
28
+ },
29
+ });
package/docs/examples.md CHANGED
@@ -117,3 +117,18 @@ ever spawns anything itself.
117
117
 
118
118
  Full file: [`examples/06-lineage.ts`](examples/06-lineage.ts). Run it with
119
119
  `yaag run examples/06-lineage.ts`.
120
+ ## 07 — fork and compaction
121
+
122
+ One Agent reads the repository. Each worker forks that Agent, so it inherits
123
+ the whole conversation instead of reading the repository again. `compact: true`
124
+ summarizes the inherited context before the worker starts.
125
+
126
+ <!-- embed: docs/examples/07-fork.ts -->
127
+
128
+ ```ts
129
+ const worker = await builder.fork({ name: topic.split(" ")[0], compact: true });
130
+ return worker.ask(prompt`From what you already read, report on ${topic}.`);
131
+ ```
132
+
133
+ Full file: [`examples/07-fork.ts`](examples/07-fork.ts). Run it with
134
+ `yaag run examples/07-fork.ts`.
@@ -103,6 +103,17 @@ Cause: `ASK_LIMIT` is a tripped soft budget (`maxTurns`, `maxToolCalls`,
103
103
  Fix: raise the budget, or add a `wrapUpPrompt`. After `ASK_LIMIT` the Handle is
104
104
  alive, so the program can ask again.
105
105
 
106
+ ## A fork or a compaction was refused
107
+
108
+ Cause: `COMPACT_DURING_ASK` and `FORK_DURING_ASK` mean an Ask of that Agent was
109
+ still in flight. Both operations need a settled Ask boundary. `FORK_REFUSED`
110
+ means the Agent cannot be forked at all: it was killed during an Ask, so the
111
+ end of its session is undefined, or it holds no session file, as a
112
+ Cassette-playback Agent does. `COMPACT_FAILED` means pi refused the summary.
113
+
114
+ Fix: await the Ask before you fork or compact. For `FORK_REFUSED`, fork an
115
+ Agent that settled its last Ask, or spawn a fresh Agent instead.
116
+
106
117
  ## describe executes the module top level
107
118
 
108
119
  <!-- quote: @yaag/cli/src/argv.ts -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yaag/extension",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -25,9 +25,9 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@earendil-works/pi-tui": "^0.84.0",
28
- "@yaag/cli": "0.10.0",
29
- "@yaag/runtime": "0.10.0",
30
- "@yaag/tui": "0.10.0",
28
+ "@yaag/cli": "0.11.0",
29
+ "@yaag/runtime": "0.11.0",
30
+ "@yaag/tui": "0.11.0",
31
31
  "nanoid": "^6.0.1"
32
32
  },
33
33
  "peerDependencies": {
@@ -26,6 +26,7 @@ export const EXAMPLE_FILES = [
26
26
  "04-controlled-ask.ts",
27
27
  "05-record-resume.ts",
28
28
  "06-lineage.ts",
29
+ "07-fork.ts",
29
30
  ] as const;
30
31
 
31
32
  /** Absolute path of one shipped doc file, named relative to the docs root. */
@@ -2,6 +2,7 @@ import type {
2
2
  AgentActivity,
3
3
  AgentInfo,
4
4
  AgentState,
5
+ CompactionInfo,
5
6
  ModelFallbackInfo,
6
7
  NodeInfo,
7
8
  NodeState,
@@ -58,6 +59,7 @@ interface SummaryBase {
58
59
  readonly durationMs: number;
59
60
  readonly worstFrameGapMs: number;
60
61
  readonly modelFallbacks: number;
62
+ readonly compactions: number;
61
63
  }
62
64
 
63
65
  function parseBase(stored: Record<string, unknown>): SummaryBase | null {
@@ -93,6 +95,7 @@ function parseBase(stored: Record<string, unknown>): SummaryBase | null {
93
95
  // Tolerant, unlike the strict fields above: a record written before the
94
96
  // fallback counter existed must still load instead of disappearing.
95
97
  modelFallbacks: typeof stored.modelFallbacks === "number" ? stored.modelFallbacks : 0,
98
+ compactions: typeof stored.compactions === "number" ? stored.compactions : 0,
96
99
  };
97
100
  }
98
101
 
@@ -157,6 +160,9 @@ function parseAgent(value: unknown): AgentInfo | null {
157
160
  // Tolerant for the same reason as the Run-level counter.
158
161
  modelFallbacksPruned:
159
162
  typeof stored.modelFallbacksPruned === "number" ? stored.modelFallbacksPruned : 0,
163
+ // Tolerant as well: a record written before compaction carries neither field.
164
+ compactions: typeof stored.compactions === "number" ? stored.compactions : 0,
165
+ lastCompaction: parseCompaction(stored.lastCompaction),
160
166
  };
161
167
  if (state === "asking") {
162
168
  if (typeof stored.askIndex !== "number") return null;
@@ -175,6 +181,26 @@ function parseAgent(value: unknown): AgentInfo | null {
175
181
  return { ...base, state, askIndex: stored.askIndex, promptGist: stored.promptGist };
176
182
  }
177
183
 
184
+ /** The newest compaction of one Agent, or null when the record holds none. */
185
+ function parseCompaction(value: unknown): CompactionInfo | null {
186
+ const stored = asRecord(value);
187
+ if (stored === null) return null;
188
+ if (
189
+ !isNullableNumber(stored.tokensBefore) ||
190
+ !isNullableNumber(stored.tokensAfter) ||
191
+ !isNullableNumber(stored.cost) ||
192
+ !isNullableNumber(stored.at)
193
+ ) {
194
+ return null;
195
+ }
196
+ return {
197
+ tokensBefore: stored.tokensBefore,
198
+ tokensAfter: stored.tokensAfter,
199
+ cost: stored.cost,
200
+ at: stored.at,
201
+ };
202
+ }
203
+
178
204
  function parseNodes(value: unknown): NodeInfo[] | null {
179
205
  if (!Array.isArray(value)) return null;
180
206
  const nodes: NodeInfo[] = [];
@@ -84,9 +84,17 @@ function normalizeAgents(value: RunSummary): RunSummary {
84
84
  modelFallbacksPruned: agent.modelFallbacksPruned ?? 0,
85
85
  parent: agent.parent ?? null,
86
86
  origin: normalizeOrigin(agent.origin),
87
+ // Absent in every message written before compaction shipped.
88
+ compactions: agent.compactions ?? 0,
89
+ lastCompaction: agent.lastCompaction ?? null,
87
90
  };
88
91
  }
89
- return { ...value, agents, modelFallbacks: value.modelFallbacks ?? 0 };
92
+ return {
93
+ ...value,
94
+ agents,
95
+ modelFallbacks: value.modelFallbacks ?? 0,
96
+ compactions: value.compactions ?? 0,
97
+ };
90
98
  }
91
99
 
92
100
  function optionalNodes(value: unknown): boolean {