@yaag/extension 0.9.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 +44 -0
- package/docs/examples/06-lineage.ts +33 -0
- package/docs/examples/07-fork.ts +29 -0
- package/docs/examples.md +32 -0
- package/docs/troubleshooting.md +11 -0
- package/package.json +4 -4
- package/src/docs/docs-root.ts +3 -1
- package/src/record/run-summary-parse.ts +30 -0
- package/src/view/run-details.ts +28 -3
package/docs/authoring.md
CHANGED
|
@@ -113,3 +113,47 @@ rules. ADR-0022 plans the human-in-the-loop verbs; they are not available yet.
|
|
|
113
113
|
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
|
+
|
|
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
|
+
|
|
137
|
+
## Lineage
|
|
138
|
+
|
|
139
|
+
`parent` names the Agent a spawn belongs under. It is data only: it sets the
|
|
140
|
+
Agent's place in the Run tree, so the TUI and the Run Summary draw the child
|
|
141
|
+
under its parent. It opens no channel between the two Agents, and it ends no
|
|
142
|
+
lifetime — every Agent still dies with the Run.
|
|
143
|
+
|
|
144
|
+
`parent` takes a Handle a previous spawn in this Run returned, and it works the
|
|
145
|
+
same way as an override on a definition: `ctx.spawn(reviewer, { parent })`. A
|
|
146
|
+
parent that already exited stays a valid parent.
|
|
147
|
+
|
|
148
|
+
An Agent cannot spawn. It can *ask* for a helper through `outputSchema`, and
|
|
149
|
+
the program decides:
|
|
150
|
+
|
|
151
|
+
<!-- embed: docs/examples/06-lineage.ts -->
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
for (const helper of wish.helpers) {
|
|
155
|
+
if (!ALLOWED_ROLES.has(helper.role)) continue;
|
|
156
|
+
const agent = await ctx.spawn({ name: helper.role, parent: implementer });
|
|
157
|
+
reports.push(await agent.ask(prompt`Review this report: ${wish.report}`));
|
|
158
|
+
}
|
|
159
|
+
```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mediated autonomy. The implementer reports which helpers it wants; the
|
|
3
|
+
* program is the gate that decides, spawns them, and places them in the tree
|
|
4
|
+
* under the implementer with `parent`.
|
|
5
|
+
*/
|
|
6
|
+
import { defineRun, prompt } from "@yaag/runtime";
|
|
7
|
+
import { Type } from "typebox";
|
|
8
|
+
|
|
9
|
+
const ALLOWED_ROLES = new Set(["reviewer", "summarizer"]);
|
|
10
|
+
|
|
11
|
+
const Wish = Type.Object({
|
|
12
|
+
report: Type.String(),
|
|
13
|
+
helpers: Type.Array(Type.Object({ role: Type.String(), reason: Type.String() })),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export default defineRun({
|
|
17
|
+
name: "lineage",
|
|
18
|
+
description: "Spawns the helpers an implementer asks for, under the implementer.",
|
|
19
|
+
async run(ctx) {
|
|
20
|
+
const implementer = await ctx.spawn({ name: "implementer" });
|
|
21
|
+
const wish = await implementer.ask(
|
|
22
|
+
prompt`Report one paragraph about this repository, and the helper roles you want.`,
|
|
23
|
+
{ outputSchema: Wish },
|
|
24
|
+
);
|
|
25
|
+
const reports: string[] = [];
|
|
26
|
+
for (const helper of wish.helpers) {
|
|
27
|
+
if (!ALLOWED_ROLES.has(helper.role)) continue;
|
|
28
|
+
const agent = await ctx.spawn({ name: helper.role, parent: implementer });
|
|
29
|
+
reports.push(await agent.ask(prompt`Review this report: ${wish.report}`));
|
|
30
|
+
}
|
|
31
|
+
return [wish.report, ...reports].join("\n\n");
|
|
32
|
+
},
|
|
33
|
+
});
|
|
@@ -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
|
@@ -100,3 +100,35 @@ holds it.
|
|
|
100
100
|
Full file: [`examples/05-record-resume.ts`](examples/05-record-resume.ts). Run
|
|
101
101
|
it with `yaag run examples/05-record-resume.ts --record run.json`, then resume
|
|
102
102
|
it with `yaag run examples/05-record-resume.ts --resume run.json`.
|
|
103
|
+
|
|
104
|
+
## 06 — lineage
|
|
105
|
+
|
|
106
|
+
`parent` places an Agent under another Agent in the Run tree. Here the
|
|
107
|
+
implementer reports the helper roles it wants, and the program decides which of
|
|
108
|
+
them to spawn. The helpers become a subtree of the implementer, and no Agent
|
|
109
|
+
ever spawns anything itself.
|
|
110
|
+
|
|
111
|
+
<!-- embed: docs/examples/06-lineage.ts -->
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
const agent = await ctx.spawn({ name: helper.role, parent: implementer });
|
|
115
|
+
reports.push(await agent.ask(prompt`Review this report: ${wish.report}`));
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Full file: [`examples/06-lineage.ts`](examples/06-lineage.ts). Run it with
|
|
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`.
|
package/docs/troubleshooting.md
CHANGED
|
@@ -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.
|
|
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.
|
|
29
|
-
"@yaag/runtime": "0.
|
|
30
|
-
"@yaag/tui": "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": {
|
package/src/docs/docs-root.ts
CHANGED
|
@@ -18,13 +18,15 @@ export const DOC_PAGES = [
|
|
|
18
18
|
"troubleshooting.md",
|
|
19
19
|
] as const;
|
|
20
20
|
|
|
21
|
-
/** The
|
|
21
|
+
/** The shipped example programs, in reading order. */
|
|
22
22
|
export const EXAMPLE_FILES = [
|
|
23
23
|
"01-minimal.ts",
|
|
24
24
|
"02-args.ts",
|
|
25
25
|
"03-fan-out.ts",
|
|
26
26
|
"04-controlled-ask.ts",
|
|
27
27
|
"05-record-resume.ts",
|
|
28
|
+
"06-lineage.ts",
|
|
29
|
+
"07-fork.ts",
|
|
28
30
|
] as const;
|
|
29
31
|
|
|
30
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
|
|
|
@@ -150,9 +153,16 @@ function parseAgent(value: unknown): AgentInfo | null {
|
|
|
150
153
|
nodes,
|
|
151
154
|
finishedNodesPruned: stored.finishedNodesPruned,
|
|
152
155
|
modelFallbacks: fallbacks,
|
|
156
|
+
// Lineage is tolerant for the same reason as the counters: a record written
|
|
157
|
+
// before the Parent Link carries neither field.
|
|
158
|
+
parent: isNullableString(stored.parent) ? stored.parent : null,
|
|
159
|
+
origin: stored.origin === "fork" ? ("fork" as const) : ("spawn" as const),
|
|
153
160
|
// Tolerant for the same reason as the Run-level counter.
|
|
154
161
|
modelFallbacksPruned:
|
|
155
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),
|
|
156
166
|
};
|
|
157
167
|
if (state === "asking") {
|
|
158
168
|
if (typeof stored.askIndex !== "number") return null;
|
|
@@ -171,6 +181,26 @@ function parseAgent(value: unknown): AgentInfo | null {
|
|
|
171
181
|
return { ...base, state, askIndex: stored.askIndex, promptGist: stored.promptGist };
|
|
172
182
|
}
|
|
173
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
|
+
|
|
174
204
|
function parseNodes(value: unknown): NodeInfo[] | null {
|
|
175
205
|
if (!Array.isArray(value)) return null;
|
|
176
206
|
const nodes: NodeInfo[] = [];
|
package/src/view/run-details.ts
CHANGED
|
@@ -82,9 +82,19 @@ function normalizeAgents(value: RunSummary): RunSummary {
|
|
|
82
82
|
finishedNodesPruned: agent.finishedNodesPruned ?? 0,
|
|
83
83
|
modelFallbacks: agent.modelFallbacks ?? [],
|
|
84
84
|
modelFallbacksPruned: agent.modelFallbacksPruned ?? 0,
|
|
85
|
+
parent: agent.parent ?? null,
|
|
86
|
+
origin: normalizeOrigin(agent.origin),
|
|
87
|
+
// Absent in every message written before compaction shipped.
|
|
88
|
+
compactions: agent.compactions ?? 0,
|
|
89
|
+
lastCompaction: agent.lastCompaction ?? null,
|
|
85
90
|
};
|
|
86
91
|
}
|
|
87
|
-
return {
|
|
92
|
+
return {
|
|
93
|
+
...value,
|
|
94
|
+
agents,
|
|
95
|
+
modelFallbacks: value.modelFallbacks ?? 0,
|
|
96
|
+
compactions: value.compactions ?? 0,
|
|
97
|
+
};
|
|
88
98
|
}
|
|
89
99
|
|
|
90
100
|
function optionalNodes(value: unknown): boolean {
|
|
@@ -166,10 +176,23 @@ function isAgentBase(value: Record<string, unknown>): boolean {
|
|
|
166
176
|
optionalNodes(value.nodes) &&
|
|
167
177
|
(value.finishedNodesPruned === undefined || natural(value.finishedNodesPruned)) &&
|
|
168
178
|
optionalFallbacks(value.modelFallbacks) &&
|
|
169
|
-
(value.modelFallbacksPruned === undefined || natural(value.modelFallbacksPruned))
|
|
179
|
+
(value.modelFallbacksPruned === undefined || natural(value.modelFallbacksPruned)) &&
|
|
180
|
+
// Lineage is tolerated as absent: a details blob persisted before the
|
|
181
|
+
// Parent Link carries neither field.
|
|
182
|
+
(value.parent === undefined || nullableString(value.parent)) &&
|
|
183
|
+
(value.origin === undefined || typeof value.origin === "string")
|
|
170
184
|
);
|
|
171
185
|
}
|
|
172
186
|
|
|
187
|
+
/**
|
|
188
|
+
* An origin a newer CLI may not have shipped yet reads as "spawn", the same way
|
|
189
|
+
* a stored Run record treats it. A value outside the contract must never drop
|
|
190
|
+
* the whole details blob and blank the Run view.
|
|
191
|
+
*/
|
|
192
|
+
function normalizeOrigin(value: unknown): "spawn" | "fork" {
|
|
193
|
+
return value === "fork" ? "fork" : "spawn";
|
|
194
|
+
}
|
|
195
|
+
|
|
173
196
|
function optionalEvent(value: unknown): value is LifecycleEvent | undefined {
|
|
174
197
|
return value === undefined || isEvent(value);
|
|
175
198
|
}
|
|
@@ -183,7 +206,9 @@ function isEvent(value: unknown): value is LifecycleEvent {
|
|
|
183
206
|
return (
|
|
184
207
|
strings(value.agent, value.model, value.cwd) &&
|
|
185
208
|
optionalString(value.branch) &&
|
|
186
|
-
optionalString(value.sessionFile)
|
|
209
|
+
optionalString(value.sessionFile) &&
|
|
210
|
+
optionalString(value.parent) &&
|
|
211
|
+
(value.origin === undefined || typeof value.origin === "string")
|
|
187
212
|
);
|
|
188
213
|
case "ask_start":
|
|
189
214
|
return (
|