@yaag/runtime 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/package.json +1 -1
- package/src/agent/agent-compaction.ts +80 -0
- package/src/agent/agent-usage.ts +11 -0
- package/src/agent/agent.ts +94 -2
- package/src/agent/fork.ts +74 -0
- package/src/agent/index.ts +1 -0
- package/src/agent/spawn-open.ts +249 -0
- package/src/agent/spawn-parent.ts +21 -0
- package/src/agent/spawn-request.ts +17 -2
- package/src/agent/spawn.ts +45 -191
- package/src/cassette/cassette-replay.ts +33 -1
- package/src/cassette/cassette-schema.ts +21 -0
- package/src/cassette/cassette.ts +75 -6
- package/src/cassette/index.ts +1 -0
- package/src/cassette/recording-transport.ts +13 -0
- package/src/cassette/replay-divergence.ts +58 -3
- package/src/cassette/replay-transport.ts +22 -1
- package/src/cassette/resume-transport.ts +27 -2
- package/src/errors.ts +4 -0
- package/src/events.ts +35 -0
- package/src/index.ts +7 -0
- package/src/summary/index.ts +2 -0
- package/src/summary/summary-agent.ts +55 -2
- package/src/summary/summary.ts +21 -0
- package/src/transport/fake-transport.ts +35 -1
- package/src/transport/index.ts +9 -1
- package/src/transport/live-transport.ts +9 -0
- package/src/transport/pi-state.ts +51 -1
- package/src/transport/transport.ts +68 -0
- package/src/transport/worktree-transport.ts +39 -6
- package/src/types.ts +54 -1
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { REPORT_RESULT_EXTENSION_PATH, REPORT_RESULT_TOOL_NAME } from "../ask-contract/index.ts";
|
|
2
2
|
import { SYSTEM_PROMPT_RECORDER_EXTENSION_PATH } from "./system-prompt-recorder.ts";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
AgentStats,
|
|
5
|
+
CompactionResult,
|
|
6
|
+
Frame,
|
|
7
|
+
OpenOptions,
|
|
8
|
+
TokenBreakdown,
|
|
9
|
+
} from "./transport.ts";
|
|
4
10
|
|
|
5
11
|
/** What the Agent reported about its own work, read from a `get_state` probe. */
|
|
6
12
|
export interface AgentProgress {
|
|
@@ -23,7 +29,10 @@ export function piCommand(options: OpenOptions, toolProbeExtensionPath?: string)
|
|
|
23
29
|
}
|
|
24
30
|
appendCapabilities(cmd, options, toolProbeExtensionPath);
|
|
25
31
|
if (options.sessionDir) cmd.push("--session-dir", options.sessionDir);
|
|
32
|
+
// `--session` and `--fork` are mutually exclusive in pi, and a resume is the
|
|
33
|
+
// stronger claim: it names the very session this Agent must continue.
|
|
26
34
|
if (options.sessionFile) cmd.push("--session", options.sessionFile);
|
|
35
|
+
else if (options.forkSession) cmd.push("--fork", options.forkSession);
|
|
27
36
|
// No --approve: Agents inherit the user's saved project-trust decision (ADR-0009).
|
|
28
37
|
return cmd;
|
|
29
38
|
}
|
|
@@ -154,6 +163,47 @@ export function readStats(response: Frame): AgentStats {
|
|
|
154
163
|
return { tokens: readBreakdown("tokens" in data ? data.tokens : null), cost };
|
|
155
164
|
}
|
|
156
165
|
|
|
166
|
+
/**
|
|
167
|
+
* What a `compact` response reports (ADR-0043).
|
|
168
|
+
*
|
|
169
|
+
* A missing field becomes null rather than 0, so "unknown" and "free" stay
|
|
170
|
+
* distinguishable: a custom compaction handler may answer without usage.
|
|
171
|
+
*/
|
|
172
|
+
export function readCompaction(response: Frame): CompactionResult {
|
|
173
|
+
const data: unknown = response.data;
|
|
174
|
+
if (typeof data !== "object" || data === null) {
|
|
175
|
+
return { tokensBefore: null, tokensAfter: null, tokens: null, cost: null };
|
|
176
|
+
}
|
|
177
|
+
const record: Record<string, unknown> = { ...data };
|
|
178
|
+
const usage: unknown = record.usage;
|
|
179
|
+
const usageRecord: Record<string, unknown> =
|
|
180
|
+
typeof usage === "object" && usage !== null ? { ...usage } : {};
|
|
181
|
+
const cost: unknown = usageRecord.cost;
|
|
182
|
+
return {
|
|
183
|
+
tokensBefore: typeof record.tokensBefore === "number" ? record.tokensBefore : null,
|
|
184
|
+
tokensAfter:
|
|
185
|
+
typeof record.estimatedTokensAfter === "number" ? record.estimatedTokensAfter : null,
|
|
186
|
+
tokens: readCompactionTokens(usageRecord),
|
|
187
|
+
cost:
|
|
188
|
+
typeof cost === "object" &&
|
|
189
|
+
cost !== null &&
|
|
190
|
+
typeof (cost as { total?: unknown }).total === "number"
|
|
191
|
+
? (cost as { total: number }).total
|
|
192
|
+
: null,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The summary call's own token breakdown, or null when pi reported none. */
|
|
197
|
+
function readCompactionTokens(usage: Record<string, unknown>): TokenBreakdown | null {
|
|
198
|
+
return readBreakdown({
|
|
199
|
+
input: usage.input,
|
|
200
|
+
output: usage.output,
|
|
201
|
+
cacheRead: usage.cacheRead,
|
|
202
|
+
cacheWrite: usage.cacheWrite,
|
|
203
|
+
total: usage.totalTokens,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
157
207
|
function readBreakdown(tokens: unknown): TokenBreakdown | null {
|
|
158
208
|
if (typeof tokens !== "object" || tokens === null) return null;
|
|
159
209
|
const record: Record<string, unknown> = { ...tokens };
|
|
@@ -31,6 +31,23 @@ export interface AgentStats {
|
|
|
31
31
|
readonly cost: number | null;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* What one compaction of an Agent's context reported (ADR-0043).
|
|
36
|
+
*
|
|
37
|
+
* Every field is null when pi reported nothing for it: a custom compaction
|
|
38
|
+
* handler may answer without usage, and "unknown" must stay distinct from 0.
|
|
39
|
+
*/
|
|
40
|
+
export interface CompactionResult {
|
|
41
|
+
/** Context tokens before the summary replaced the transcript. */
|
|
42
|
+
readonly tokensBefore: number | null;
|
|
43
|
+
/** Estimated context tokens after the summary replaced the transcript. */
|
|
44
|
+
readonly tokensAfter: number | null;
|
|
45
|
+
/** Token breakdown of the summary call itself. */
|
|
46
|
+
readonly tokens: TokenBreakdown | null;
|
|
47
|
+
/** Cost of the summary call itself. */
|
|
48
|
+
readonly cost: number | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
34
51
|
/** Recorded inputs used to explain an Ask-hash mismatch without changing identity. */
|
|
35
52
|
export interface AskMarkerContext {
|
|
36
53
|
readonly prompt: string;
|
|
@@ -93,6 +110,27 @@ export interface AskCompletion {
|
|
|
93
110
|
readonly recovered?: true;
|
|
94
111
|
}
|
|
95
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Opaque marker naming one compaction of an Agent's context.
|
|
115
|
+
*
|
|
116
|
+
* A compaction sits between two Asks, so it cannot ride the Agent-level frame
|
|
117
|
+
* streams: a Cassette replay switches those cursors to an Ask and never
|
|
118
|
+
* switches back. The marker gives the exchange its own recorded streams.
|
|
119
|
+
*/
|
|
120
|
+
export interface CompactionMarker {
|
|
121
|
+
/** Monotonic per Agent, from 0. */
|
|
122
|
+
readonly index: number;
|
|
123
|
+
/** sha256 of the settled-Ask count and the custom instructions, when given. */
|
|
124
|
+
readonly hash: string;
|
|
125
|
+
/** Settled Asks of this Agent at the compaction point. */
|
|
126
|
+
readonly afterAsks: number;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Presence identifies Cassette playback of one compaction. */
|
|
130
|
+
export interface CompactionPlayback {
|
|
131
|
+
readonly result: CompactionResult;
|
|
132
|
+
}
|
|
133
|
+
|
|
96
134
|
/** Presence identifies Cassette playback, including recorded successful Asks. */
|
|
97
135
|
export interface AskPlayback {
|
|
98
136
|
/** Limit outcome recorded for this Ask, if it rejected with ASK_LIMIT. */
|
|
@@ -143,6 +181,15 @@ export interface AgentTransport {
|
|
|
143
181
|
/** Reports surfaced live outcomes; replay ignores completion. */
|
|
144
182
|
finishAsk(completion: AskCompletion): void;
|
|
145
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Begins one compaction. Live transports return undefined; replay transports
|
|
186
|
+
* return the recorded result.
|
|
187
|
+
*/
|
|
188
|
+
beginCompaction(marker: CompactionMarker): CompactionPlayback | undefined;
|
|
189
|
+
|
|
190
|
+
/** Ends the compaction exchange, so later frames belong to the Agent again. */
|
|
191
|
+
finishCompaction(result: CompactionResult | undefined): void;
|
|
192
|
+
|
|
146
193
|
/**
|
|
147
194
|
* The extraction policy recorded for the Ask at `index`, when a Cassette
|
|
148
195
|
* backs it. An Ask recorded under an older policy keeps that policy, so its
|
|
@@ -191,10 +238,31 @@ export interface OpenOptions {
|
|
|
191
238
|
* Spawn identity only: it never becomes argv, and it is absent when empty.
|
|
192
239
|
*/
|
|
193
240
|
readonly declaredExtensions?: readonly string[];
|
|
241
|
+
/**
|
|
242
|
+
* Resolved name of the Agent named as this Agent's parent (Parent Link).
|
|
243
|
+
* Spawn identity only: it never becomes argv.
|
|
244
|
+
*/
|
|
245
|
+
readonly parent?: string;
|
|
194
246
|
/** Session storage directory. Used by the e2e suite to stay out of ~/.pi (ticket 06). */
|
|
195
247
|
readonly sessionDir?: string;
|
|
196
248
|
/** Resumes an existing pi session, translated to `--session <path>`. */
|
|
197
249
|
readonly sessionFile?: string;
|
|
250
|
+
/** How the Agent came to be; absent means an ordinary spawn. Spawn identity. */
|
|
251
|
+
readonly origin?: "fork";
|
|
252
|
+
/** Name of the Agent this one was forked from. Spawn identity; never argv. */
|
|
253
|
+
readonly forkOf?: string;
|
|
254
|
+
/** Settled Asks of the fork source at the fork point. Spawn identity. */
|
|
255
|
+
readonly forkAsks?: number;
|
|
256
|
+
/**
|
|
257
|
+
* The fork source's session file, translated to `pi --fork <path>`.
|
|
258
|
+
* Never spawn identity: the path is machine-specific.
|
|
259
|
+
*/
|
|
260
|
+
readonly forkSession?: string;
|
|
261
|
+
/**
|
|
262
|
+
* The fork source's worktree, so the fork branches from it instead of from a
|
|
263
|
+
* clean base tree. Never spawn identity.
|
|
264
|
+
*/
|
|
265
|
+
readonly worktreeFrom?: WorktreeResolution;
|
|
198
266
|
}
|
|
199
267
|
|
|
200
268
|
/** Nondeterministic worktree identity resolved below the transport seam. */
|
|
@@ -28,9 +28,17 @@ export function worktreeTransport(inner: TransportFactory): TransportFactory {
|
|
|
28
28
|
observeStartup?: TransportStartupObserver,
|
|
29
29
|
): Promise<AgentTransport> {
|
|
30
30
|
if (options.worktree !== true) return inner.open(options, observeStartup);
|
|
31
|
-
const
|
|
32
|
-
await
|
|
33
|
-
|
|
31
|
+
const from = options.worktreeFrom;
|
|
32
|
+
const root = await repositoryRoot(from?.cwd ?? options.cwd, options.name);
|
|
33
|
+
// A fork branches from what its source already committed, so the source's
|
|
34
|
+
// dirty files stay behind and no clean check applies (ADR-0044).
|
|
35
|
+
if (from === undefined) await requireClean(root, options.name);
|
|
36
|
+
const resolution = await addWorktree({
|
|
37
|
+
root,
|
|
38
|
+
name: options.name,
|
|
39
|
+
namingRoot: await namingRoot(root),
|
|
40
|
+
startPoint: from?.branch ?? "HEAD",
|
|
41
|
+
});
|
|
34
42
|
const { worktree: _worktree, ...innerOptions } = options;
|
|
35
43
|
let reported = false;
|
|
36
44
|
const transport = await inner.open(
|
|
@@ -69,18 +77,43 @@ async function requireClean(cwd: string, name: string): Promise<void> {
|
|
|
69
77
|
}
|
|
70
78
|
}
|
|
71
79
|
|
|
72
|
-
|
|
80
|
+
/**
|
|
81
|
+
* The repository the worktree directory is named after.
|
|
82
|
+
*
|
|
83
|
+
* For a worktree it is the main repository, not the worktree itself, so a fork
|
|
84
|
+
* of a Worktree Agent does not nest `repo-worktrees/a1-worktrees/a2`.
|
|
85
|
+
*/
|
|
86
|
+
async function namingRoot(root: string): Promise<string> {
|
|
87
|
+
const result = await git(root, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
88
|
+
const common = result.stdout.trim();
|
|
89
|
+
if (result.code !== 0 || common === "" || !common.endsWith("/.git")) return root;
|
|
90
|
+
return common.slice(0, -"/.git".length);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function addWorktree(options: {
|
|
94
|
+
readonly root: string;
|
|
95
|
+
readonly name: string;
|
|
96
|
+
readonly namingRoot: string;
|
|
97
|
+
readonly startPoint: string;
|
|
98
|
+
}): Promise<{
|
|
73
99
|
readonly cwd: string;
|
|
74
100
|
readonly branch: string;
|
|
75
101
|
}> {
|
|
76
|
-
const parent = join(dirname(options.
|
|
102
|
+
const parent = join(dirname(options.namingRoot), `${basename(options.namingRoot)}-worktrees`);
|
|
77
103
|
await mkdir(parent, { recursive: true });
|
|
78
104
|
for (let suffix = 1; ; suffix += 1) {
|
|
79
105
|
const leaf = suffix === 1 ? options.name : `${options.name}-${suffix}`;
|
|
80
106
|
const branch = `yaag/${leaf}`;
|
|
81
107
|
const cwd = join(parent, leaf);
|
|
82
108
|
if (await candidateTaken(options.root, branch, cwd)) continue;
|
|
83
|
-
const result = await git(options.root, [
|
|
109
|
+
const result = await git(options.root, [
|
|
110
|
+
"worktree",
|
|
111
|
+
"add",
|
|
112
|
+
"-b",
|
|
113
|
+
branch,
|
|
114
|
+
cwd,
|
|
115
|
+
options.startPoint,
|
|
116
|
+
]);
|
|
84
117
|
if (result.code === 0) return { cwd, branch };
|
|
85
118
|
if (isCollision(result)) continue;
|
|
86
119
|
throw new Error(`git worktree add failed: ${result.stderr.trim() || result.stdout.trim()}`);
|
package/src/types.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { Static, TSchema } from "typebox";
|
|
2
2
|
import type { ModelSpec, ThinkingLevel, ThinkingSpec } from "./model/index.ts";
|
|
3
|
+
import type { CompactionResult } from "./transport/index.ts";
|
|
3
4
|
|
|
4
5
|
export type { ThinkingLevel } from "./model/index.ts";
|
|
6
|
+
export type { CompactionResult } from "./transport/index.ts";
|
|
5
7
|
|
|
6
8
|
/** Options for spawning one Agent (ADR-0001, ADR-0009, ADR-0026). */
|
|
7
9
|
export interface SpawnOptions {
|
|
@@ -66,6 +68,11 @@ export interface SpawnOptions {
|
|
|
66
68
|
readonly name?: string;
|
|
67
69
|
/** Request a fresh Git worktree. The requested cwd remains the base until spawn resolves. */
|
|
68
70
|
readonly worktree?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Names this Agent's parent in the Run tree (Parent Link). Data only: it is
|
|
73
|
+
* no conversation channel and no lifetime rule.
|
|
74
|
+
*/
|
|
75
|
+
readonly parent?: Handle;
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
/**
|
|
@@ -73,12 +80,35 @@ export interface SpawnOptions {
|
|
|
73
80
|
*
|
|
74
81
|
* This is the settled shape, and it is what the Cassette identity hashes
|
|
75
82
|
* (ADR-0039).
|
|
83
|
+
*
|
|
84
|
+
* `parent` is omitted on purpose: a Handle must never reach this shape, because
|
|
85
|
+
* an Ask records these options into the Cassette. Spawn resolves the Parent
|
|
86
|
+
* Link to a name, which travels in `OpenOptions` and stays out of Ask identity.
|
|
76
87
|
*/
|
|
77
|
-
export interface ResolvedSpawnOptions extends Omit<SpawnOptions, "model" | "thinking"> {
|
|
88
|
+
export interface ResolvedSpawnOptions extends Omit<SpawnOptions, "model" | "thinking" | "parent"> {
|
|
78
89
|
readonly model?: string;
|
|
79
90
|
readonly thinking?: ThinkingLevel;
|
|
80
91
|
}
|
|
81
92
|
|
|
93
|
+
/**
|
|
94
|
+
* What a fork may change about the Agent it copies (ADR-0044).
|
|
95
|
+
*
|
|
96
|
+
* A fork inherits the source's resolved spawn options verbatim: model,
|
|
97
|
+
* thinking, tools, skills, system prompt, extensions and Ask defaults. `name`
|
|
98
|
+
* is the exception — the child always gets a fresh auto-name unless this object
|
|
99
|
+
* names one. The fork source becomes the child's default `parent`.
|
|
100
|
+
*
|
|
101
|
+
* The inherited model is the concrete one the source settled on, so the child
|
|
102
|
+
* never re-runs the source's fallback list; `fork({ model })` is the way out.
|
|
103
|
+
* A fork of a Worktree Agent gets its own fresh worktree, branched from the
|
|
104
|
+
* source's branch, so only committed state transfers. The child re-runs the
|
|
105
|
+
* ADR-0026 Tool Contract probe against its own effective options.
|
|
106
|
+
*/
|
|
107
|
+
export interface ForkOptions extends SpawnOptions {
|
|
108
|
+
/** true = pi's default compaction; a string = pi custom instructions. */
|
|
109
|
+
readonly compact?: boolean | string;
|
|
110
|
+
}
|
|
111
|
+
|
|
82
112
|
/** Topology-only fields that may change when spawning an Agent Definition. */
|
|
83
113
|
export interface SpawnOverrides {
|
|
84
114
|
/** Log and event label. Defaults to the definition's name; duplicates are suffixed. */
|
|
@@ -87,6 +117,8 @@ export interface SpawnOverrides {
|
|
|
87
117
|
readonly cwd?: string;
|
|
88
118
|
/** Request a fresh Git worktree. */
|
|
89
119
|
readonly worktree?: boolean;
|
|
120
|
+
/** Names this Agent's parent in the Run tree (Parent Link). Data only. */
|
|
121
|
+
readonly parent?: Handle;
|
|
90
122
|
}
|
|
91
123
|
|
|
92
124
|
/**
|
|
@@ -190,4 +222,25 @@ export interface Handle {
|
|
|
190
222
|
options: StructuredAskOptions<Schema>,
|
|
191
223
|
): Promise<Static<Schema>>;
|
|
192
224
|
ask(prompt: string, options?: AskOptions): Promise<string>;
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Spawns a new Agent from a copy of this Agent's session (ADR-0044).
|
|
228
|
+
*
|
|
229
|
+
* The fork point is a settled Ask boundary: a call while an Ask is in flight
|
|
230
|
+
* rejects with `FORK_DURING_ASK`. An Agent that died mid-Ask, or that holds
|
|
231
|
+
* no session file, rejects with `FORK_REFUSED`. An Agent that exited cleanly
|
|
232
|
+
* stays forkable, so a program can build context, let the Agent exit, then
|
|
233
|
+
* fork its final state many times.
|
|
234
|
+
*/
|
|
235
|
+
fork(overrides?: ForkOptions): Promise<Handle>;
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Replaces this Agent's context with a summary of it (ADR-0043).
|
|
239
|
+
*
|
|
240
|
+
* `instructions` becomes pi's `customInstructions`. A call while an Ask is in
|
|
241
|
+
* flight rejects with `COMPACT_DURING_ASK`; a refusal from pi rejects with
|
|
242
|
+
* `COMPACT_FAILED`. The result reports what the summary call cost; each field
|
|
243
|
+
* is null when pi reported nothing for it.
|
|
244
|
+
*/
|
|
245
|
+
compact(instructions?: string): Promise<CompactionResult>;
|
|
193
246
|
}
|