@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
package/src/agent/spawn.ts
CHANGED
|
@@ -1,28 +1,13 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
1
|
import type { ConfigExtension } from "../config/index.ts";
|
|
3
2
|
import { YaagError } from "../errors.ts";
|
|
4
3
|
import type { EventSink } from "../events.ts";
|
|
5
|
-
import {
|
|
6
|
-
ModelErrorHistory,
|
|
7
|
-
type ModelSelection,
|
|
8
|
-
normalizeModelResolution,
|
|
9
|
-
resolveModel,
|
|
10
|
-
resolveRecordedModel,
|
|
11
|
-
} from "../model/index.ts";
|
|
12
4
|
import type { RunContext } from "../run/index.ts";
|
|
13
|
-
import type {
|
|
14
|
-
import type {
|
|
15
|
-
|
|
16
|
-
Handle,
|
|
17
|
-
ResolvedSpawnOptions,
|
|
18
|
-
SpawnOptions,
|
|
19
|
-
SpawnOverrides,
|
|
20
|
-
} from "../types.ts";
|
|
21
|
-
import { Agent } from "./agent.ts";
|
|
22
|
-
import { uniqueAgentName } from "./agent-names.ts";
|
|
5
|
+
import type { TransportFactory } from "../transport/index.ts";
|
|
6
|
+
import type { Handle, SpawnOptions, SpawnOverrides } from "../types.ts";
|
|
7
|
+
import type { Agent } from "./agent.ts";
|
|
23
8
|
import { type AgentDefinition, agentDefinitionConfig, isAgentDefinition } from "./define-agent.ts";
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
9
|
+
import { type ForkSpawner, forkRequest } from "./fork.ts";
|
|
10
|
+
import { type OpenAgentContext, type OpenAgentRequest, openAgent } from "./spawn-open.ts";
|
|
26
11
|
|
|
27
12
|
/** Dependencies for one Run's Agent-spawn gate. */
|
|
28
13
|
export interface SpawnDependencies {
|
|
@@ -50,125 +35,30 @@ export interface SpawnGate {
|
|
|
50
35
|
export function makeSpawn(deps: SpawnDependencies): SpawnGate {
|
|
51
36
|
const taken = new Set<string>();
|
|
52
37
|
let closed = false;
|
|
38
|
+
// Declared before use: every Agent this gate opens is handed the same fork
|
|
39
|
+
// spawner, so a fork is gated and named exactly like a spawn.
|
|
40
|
+
const forkSpawner: ForkSpawner = async (source, overrides) => {
|
|
41
|
+
if (closed) throw new YaagError("RUN_CLOSED", "run has already settled");
|
|
42
|
+
const request = forkRequest(source, overrides);
|
|
43
|
+
const handle = await openAgent(context, {
|
|
44
|
+
spawnOptions: request.spawnOptions,
|
|
45
|
+
parent: request.parent,
|
|
46
|
+
askDefaults: undefined,
|
|
47
|
+
definitionName: undefined,
|
|
48
|
+
fork: request.fork,
|
|
49
|
+
});
|
|
50
|
+
if (request.compact !== undefined && request.compact !== false) {
|
|
51
|
+
await handle.compact(typeof request.compact === "string" ? request.compact : undefined);
|
|
52
|
+
}
|
|
53
|
+
return handle;
|
|
54
|
+
};
|
|
55
|
+
const context: OpenAgentContext = { ...deps, taken, forkSpawner };
|
|
53
56
|
const spawn: RunContext["spawn"] = async (
|
|
54
57
|
definitionOrOptions: SpawnOptions | AgentDefinition = {},
|
|
55
58
|
overrides?: SpawnOverrides,
|
|
56
59
|
): Promise<Handle> => {
|
|
57
60
|
if (closed) throw new YaagError("RUN_CLOSED", "run has already settled");
|
|
58
|
-
|
|
59
|
-
const cwd = resolve(request.spawnOptions.cwd ?? process.cwd());
|
|
60
|
-
const name = uniqueAgentName(request.spawnOptions.name, deps.agents.length, taken);
|
|
61
|
-
const resolution = normalizeModelResolution(request.spawnOptions);
|
|
62
|
-
// One history per Agent: its spawn loop and every mid-Ask fallback loop
|
|
63
|
-
// append to it, so a resolver sees every candidate that already failed.
|
|
64
|
-
const history = new ModelErrorHistory();
|
|
65
|
-
try {
|
|
66
|
-
// Extension paths do not vary per candidate, so a bad path fails once, generically.
|
|
67
|
-
const extensions = await resolveSpawnExtensions({
|
|
68
|
-
configExtensions: deps.configExtensions,
|
|
69
|
-
declared: request.spawnOptions.extensions,
|
|
70
|
-
useConfigExtensions: request.spawnOptions.configExtensions !== false,
|
|
71
|
-
...(deps.programFile === undefined ? {} : { programFile: deps.programFile }),
|
|
72
|
-
projectRoot: cwd,
|
|
73
|
-
}).catch((error: unknown) => {
|
|
74
|
-
if (error instanceof YaagError) throw error;
|
|
75
|
-
throw new YaagError("SPAWN_FAILED", `agent "${name}": ${String(error)}`, name);
|
|
76
|
-
});
|
|
77
|
-
// One local for both open sites, so the launch arguments and the recorded
|
|
78
|
-
// identity cannot drift apart.
|
|
79
|
-
const extensionFields =
|
|
80
|
-
extensions === undefined
|
|
81
|
-
? {}
|
|
82
|
-
: {
|
|
83
|
-
resolvedExtensionPaths: extensions.resolvedPaths,
|
|
84
|
-
declaredExtensions: extensions.declared,
|
|
85
|
-
};
|
|
86
|
-
const attempt = async (
|
|
87
|
-
selection: ModelSelection,
|
|
88
|
-
): Promise<{ opened: OpenedTransport; spawnOptions: ResolvedSpawnOptions }> => {
|
|
89
|
-
const settled = withSelection(request.spawnOptions, selection);
|
|
90
|
-
return {
|
|
91
|
-
opened: await openTransport({
|
|
92
|
-
factory: deps.factory,
|
|
93
|
-
name,
|
|
94
|
-
cwd,
|
|
95
|
-
spawnOptions: settled,
|
|
96
|
-
...extensionFields,
|
|
97
|
-
sessionDir: deps.sessionDir,
|
|
98
|
-
}),
|
|
99
|
-
spawnOptions: settled,
|
|
100
|
-
};
|
|
101
|
-
};
|
|
102
|
-
// The peek must stay in the same synchronous block as the first open: a
|
|
103
|
-
// Cassette-backed factory claims Agents in open order (ADR-0013).
|
|
104
|
-
const recorded = deps.factory.recordedSpawn?.(
|
|
105
|
-
openRequest({
|
|
106
|
-
name,
|
|
107
|
-
cwd,
|
|
108
|
-
spawnOptions: request.spawnOptions,
|
|
109
|
-
...extensionFields,
|
|
110
|
-
sessionDir: deps.sessionDir,
|
|
111
|
-
}),
|
|
112
|
-
);
|
|
113
|
-
// A Cassette-backed spawn adopts the recorded resolved selection and skips
|
|
114
|
-
// the loop, so a replayed Run emits no spawn-time fallback (ADR-0039).
|
|
115
|
-
const adopted =
|
|
116
|
-
recorded === undefined ? undefined : resolveRecordedModel({ resolution, recorded });
|
|
117
|
-
// The adopted outcome carries the attempts the recording already spent, so
|
|
118
|
-
// a later mid-Ask fallback re-resolves from that attempt index (ADR-0039).
|
|
119
|
-
for (const skipped of adopted?.skipped ?? []) {
|
|
120
|
-
history.record(skipped.reason, skipped.failedModel);
|
|
121
|
-
}
|
|
122
|
-
const { opened, spawnOptions } =
|
|
123
|
-
adopted === undefined
|
|
124
|
-
? await resolveModel({
|
|
125
|
-
resolution,
|
|
126
|
-
agent: name,
|
|
127
|
-
history,
|
|
128
|
-
onFallback: (fallback) => {
|
|
129
|
-
deps.emit({ type: "model_fallback", agent: name, ...fallback });
|
|
130
|
-
},
|
|
131
|
-
attempt,
|
|
132
|
-
})
|
|
133
|
-
: await attempt(adopted.selection);
|
|
134
|
-
const resolvedCwd = opened.startup.worktree?.cwd ?? cwd;
|
|
135
|
-
const branch = opened.startup.worktree?.branch;
|
|
136
|
-
const sessionFile = opened.startup.sessionFile;
|
|
137
|
-
const agent = new Agent({
|
|
138
|
-
name,
|
|
139
|
-
cwd: resolvedCwd,
|
|
140
|
-
branch,
|
|
141
|
-
transport: opened.transport,
|
|
142
|
-
emit: deps.emit,
|
|
143
|
-
spawnOptions,
|
|
144
|
-
// An Agent that named no candidate inherits pi's default model, so a
|
|
145
|
-
// failing Ask has nothing to fall back from and stays an Ask failure.
|
|
146
|
-
...(spawnOptions.model === undefined
|
|
147
|
-
? {}
|
|
148
|
-
: { modelFallback: { resolution, history, candidate: spawnOptions.model } }),
|
|
149
|
-
...(request.askDefaults === undefined ? {} : { askDefaults: request.askDefaults }),
|
|
150
|
-
...(request.definitionName === undefined ? {} : { definitionName: request.definitionName }),
|
|
151
|
-
});
|
|
152
|
-
deps.agents.push(agent);
|
|
153
|
-
deps.emit({
|
|
154
|
-
type: "agent_spawn",
|
|
155
|
-
agent: name,
|
|
156
|
-
model: agent.model,
|
|
157
|
-
cwd: resolvedCwd,
|
|
158
|
-
...(branch === undefined ? {} : { branch }),
|
|
159
|
-
...(sessionFile === undefined ? {} : { sessionFile }),
|
|
160
|
-
});
|
|
161
|
-
return agent;
|
|
162
|
-
} catch (error) {
|
|
163
|
-
if (request.definitionName !== undefined && isSpawnFailure(error)) {
|
|
164
|
-
throw new YaagError(
|
|
165
|
-
"SPAWN_FAILED",
|
|
166
|
-
`definition "${request.definitionName}": ${error.message}`,
|
|
167
|
-
name,
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
throw error;
|
|
171
|
-
}
|
|
61
|
+
return openAgent(context, resolveRequest(definitionOrOptions, overrides));
|
|
172
62
|
};
|
|
173
63
|
return {
|
|
174
64
|
spawn,
|
|
@@ -178,23 +68,26 @@ export function makeSpawn(deps: SpawnDependencies): SpawnGate {
|
|
|
178
68
|
};
|
|
179
69
|
}
|
|
180
70
|
|
|
181
|
-
|
|
71
|
+
/** Topology overrides after validation; `parent` stays raw for `resolveParent`. */
|
|
72
|
+
type SpawnTopology = Omit<SpawnOverrides, "parent">;
|
|
73
|
+
|
|
74
|
+
type MutableSpawnOverrides = { -readonly [Key in keyof SpawnTopology]: SpawnTopology[Key] };
|
|
182
75
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
readonly
|
|
186
|
-
|
|
187
|
-
readonly definitionName: string | undefined;
|
|
76
|
+
/** One validated override object: checked topology plus the unchecked Parent Link. */
|
|
77
|
+
interface ValidatedOverrides {
|
|
78
|
+
readonly topology: SpawnTopology;
|
|
79
|
+
readonly parent: unknown;
|
|
188
80
|
}
|
|
189
81
|
|
|
190
82
|
function resolveRequest(
|
|
191
83
|
definitionOrOptions: SpawnOptions | AgentDefinition,
|
|
192
84
|
overrides: SpawnOverrides | undefined,
|
|
193
|
-
):
|
|
85
|
+
): OpenAgentRequest {
|
|
194
86
|
if (!isAgentDefinition(definitionOrOptions)) {
|
|
195
|
-
|
|
87
|
+
const { parent, ...spawnOptions } = definitionOrOptions;
|
|
88
|
+
return { spawnOptions, parent, askDefaults: undefined, definitionName: undefined };
|
|
196
89
|
}
|
|
197
|
-
const topology = validateOverrides(overrides);
|
|
90
|
+
const { topology, parent } = validateOverrides(overrides);
|
|
198
91
|
const config = agentDefinitionConfig(definitionOrOptions);
|
|
199
92
|
return {
|
|
200
93
|
spawnOptions: {
|
|
@@ -218,27 +111,32 @@ function resolveRequest(
|
|
|
218
111
|
? { systemPrompt: config.prompt }
|
|
219
112
|
: { appendSystemPrompt: config.prompt }),
|
|
220
113
|
},
|
|
114
|
+
parent,
|
|
221
115
|
askDefaults: config.askDefaults,
|
|
222
116
|
definitionName: config.name,
|
|
223
117
|
};
|
|
224
118
|
}
|
|
225
119
|
|
|
226
|
-
function validateOverrides(overrides: unknown):
|
|
227
|
-
if (overrides === undefined) return {};
|
|
120
|
+
function validateOverrides(overrides: unknown): ValidatedOverrides {
|
|
121
|
+
if (overrides === undefined) return { topology: {}, parent: undefined };
|
|
228
122
|
if (typeof overrides !== "object" || overrides === null || Array.isArray(overrides)) {
|
|
229
123
|
throw new TypeError(
|
|
230
124
|
"spawn overrides must be an object: definitions own policy and spawn overrides own topology",
|
|
231
125
|
);
|
|
232
126
|
}
|
|
233
127
|
for (const key of Object.keys(overrides)) {
|
|
234
|
-
if (key !== "name" && key !== "cwd" && key !== "worktree") {
|
|
128
|
+
if (key !== "name" && key !== "cwd" && key !== "worktree" && key !== "parent") {
|
|
235
129
|
throw new TypeError(
|
|
236
130
|
`spawn override "${key}" is not allowed: definitions own policy and spawn overrides own topology`,
|
|
237
131
|
);
|
|
238
132
|
}
|
|
239
133
|
}
|
|
240
134
|
const topology: MutableSpawnOverrides = {};
|
|
135
|
+
// `parent` is never narrowed here: `resolveParent` owns that check, so the
|
|
136
|
+
// error text stays in one place and no unchecked value becomes a Handle.
|
|
137
|
+
let parent: unknown;
|
|
241
138
|
for (const [key, value] of Object.entries(overrides)) {
|
|
139
|
+
if (key === "parent") parent = value;
|
|
242
140
|
if (key === "name") {
|
|
243
141
|
if (value !== undefined && typeof value !== "string") {
|
|
244
142
|
throw new TypeError('spawn override "name" must be a string when present');
|
|
@@ -258,49 +156,5 @@ function validateOverrides(overrides: unknown): SpawnOverrides {
|
|
|
258
156
|
topology.worktree = value;
|
|
259
157
|
}
|
|
260
158
|
}
|
|
261
|
-
return topology;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function isSpawnFailure(error: unknown): error is YaagError {
|
|
265
|
-
return error instanceof YaagError && error.code === "SPAWN_FAILED";
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
interface OpenTransportOptions {
|
|
269
|
-
readonly factory: TransportFactory;
|
|
270
|
-
readonly name: string;
|
|
271
|
-
readonly cwd: string;
|
|
272
|
-
readonly spawnOptions: ResolvedSpawnOptions;
|
|
273
|
-
readonly resolvedExtensionPaths?: readonly string[];
|
|
274
|
-
readonly declaredExtensions?: readonly string[];
|
|
275
|
-
readonly sessionDir: string | undefined;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
interface OpenedTransport {
|
|
279
|
-
readonly transport: AgentTransport;
|
|
280
|
-
readonly startup: TransportStartup;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
async function openTransport(options: OpenTransportOptions): Promise<OpenedTransport> {
|
|
284
|
-
const startup: TransportStartup = {};
|
|
285
|
-
try {
|
|
286
|
-
const transport = await options.factory.open(
|
|
287
|
-
openRequest({
|
|
288
|
-
name: options.name,
|
|
289
|
-
cwd: options.cwd,
|
|
290
|
-
spawnOptions: options.spawnOptions,
|
|
291
|
-
...(options.resolvedExtensionPaths === undefined
|
|
292
|
-
? {}
|
|
293
|
-
: { resolvedExtensionPaths: options.resolvedExtensionPaths }),
|
|
294
|
-
...(options.declaredExtensions === undefined
|
|
295
|
-
? {}
|
|
296
|
-
: { declaredExtensions: options.declaredExtensions }),
|
|
297
|
-
sessionDir: options.sessionDir,
|
|
298
|
-
}),
|
|
299
|
-
(report) => Object.assign(startup, report),
|
|
300
|
-
);
|
|
301
|
-
return { transport, startup };
|
|
302
|
-
} catch (error) {
|
|
303
|
-
if (error instanceof YaagError) throw error;
|
|
304
|
-
throw new YaagError("SPAWN_FAILED", `agent "${options.name}": ${String(error)}`, options.name);
|
|
305
|
-
}
|
|
159
|
+
return { topology, parent };
|
|
306
160
|
}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import type { ReportedResult } from "../ask-contract/index.ts";
|
|
2
2
|
import { isReportResultCommandFrame, ReportResultCall } from "../ask-contract/index.ts";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
AgentStats,
|
|
5
|
+
AskMarker,
|
|
6
|
+
AskPlayback,
|
|
7
|
+
CompactionMarker,
|
|
8
|
+
CompactionPlayback,
|
|
9
|
+
Frame,
|
|
10
|
+
} from "../transport/index.ts";
|
|
4
11
|
import { FrameQueue } from "../transport/index.ts";
|
|
5
12
|
import type { CassetteAgent } from "./cassette.ts";
|
|
6
13
|
|
|
@@ -92,6 +99,31 @@ export class CassetteReplay {
|
|
|
92
99
|
};
|
|
93
100
|
}
|
|
94
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Moves playback to a compaction its caller has already identity-checked.
|
|
104
|
+
*
|
|
105
|
+
* A compaction lives between two Asks, and `beginAsk` never switches the
|
|
106
|
+
* cursors back to the Agent-level streams, so the recorded compaction owns
|
|
107
|
+
* its own streams (ADR-0043).
|
|
108
|
+
*/
|
|
109
|
+
beginCompaction(marker: CompactionMarker): CompactionPlayback {
|
|
110
|
+
const compaction = this.#agent.compactions?.[marker.index];
|
|
111
|
+
if (!compaction) throw new Error("unreachable replay compaction cursor");
|
|
112
|
+
this.#sentCursor = 0;
|
|
113
|
+
this.#receivedCursor = 0;
|
|
114
|
+
this.#sent = compaction.sentFrames;
|
|
115
|
+
this.#received = compaction.receivedFrames;
|
|
116
|
+
this.#openAttempt(0);
|
|
117
|
+
return {
|
|
118
|
+
result: compaction.result ?? {
|
|
119
|
+
tokensBefore: null,
|
|
120
|
+
tokensAfter: null,
|
|
121
|
+
tokens: null,
|
|
122
|
+
cost: null,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
95
127
|
/** Ends the stream cleanly, allowing a caller to switch to another source. */
|
|
96
128
|
finish(): void {
|
|
97
129
|
this.#queue.end();
|
|
@@ -38,6 +38,10 @@ const CassetteSpawnSchema = Type.Object({
|
|
|
38
38
|
...spawnPolicyFields,
|
|
39
39
|
worktree: Type.Optional(Type.Literal(true)),
|
|
40
40
|
declaredExtensions: Type.Optional(StringArray),
|
|
41
|
+
parent: Type.Optional(Type.String()),
|
|
42
|
+
origin: Type.Optional(Type.Literal("fork")),
|
|
43
|
+
forkOf: Type.Optional(Type.String()),
|
|
44
|
+
forkAsks: Type.Optional(Type.Number()),
|
|
41
45
|
});
|
|
42
46
|
|
|
43
47
|
const ContextSpawnSchema = Type.Object({
|
|
@@ -118,6 +122,22 @@ const TokenBreakdownSchema = Type.Object({
|
|
|
118
122
|
total: Type.Number(),
|
|
119
123
|
});
|
|
120
124
|
|
|
125
|
+
const CompactionResultSchema = Type.Object({
|
|
126
|
+
tokensBefore: Type.Union([Type.Null(), Type.Number()]),
|
|
127
|
+
tokensAfter: Type.Union([Type.Null(), Type.Number()]),
|
|
128
|
+
tokens: Type.Union([Type.Null(), TokenBreakdownSchema]),
|
|
129
|
+
cost: Type.Union([Type.Null(), Type.Number()]),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const CassetteCompactionSchema = Type.Object({
|
|
133
|
+
index: Type.Number(),
|
|
134
|
+
hash: Type.String(),
|
|
135
|
+
afterAsks: Type.Number(),
|
|
136
|
+
result: Type.Optional(CompactionResultSchema),
|
|
137
|
+
sentFrames: Type.Array(FrameSchema),
|
|
138
|
+
receivedFrames: Type.Array(FrameSchema),
|
|
139
|
+
});
|
|
140
|
+
|
|
121
141
|
const CassetteAgentSchema = Type.Object({
|
|
122
142
|
spawn: CassetteSpawnSchema,
|
|
123
143
|
model: Type.String(),
|
|
@@ -127,6 +147,7 @@ const CassetteAgentSchema = Type.Object({
|
|
|
127
147
|
sentFrames: Type.Array(FrameSchema),
|
|
128
148
|
receivedFrames: Type.Array(FrameSchema),
|
|
129
149
|
asks: Type.Array(CassetteAskSchema),
|
|
150
|
+
compactions: Type.Optional(Type.Array(CassetteCompactionSchema)),
|
|
130
151
|
stats: Type.Object({
|
|
131
152
|
tokens: Type.Union([Type.Null(), TokenBreakdownSchema]),
|
|
132
153
|
cost: Type.Union([Type.Null(), Type.Number()]),
|
package/src/cassette/cassette.ts
CHANGED
|
@@ -7,6 +7,8 @@ import type {
|
|
|
7
7
|
AskInvalidOutputPlayback,
|
|
8
8
|
AskMarker,
|
|
9
9
|
AskMarkerContext,
|
|
10
|
+
CompactionMarker,
|
|
11
|
+
CompactionResult,
|
|
10
12
|
Frame,
|
|
11
13
|
OpenOptions,
|
|
12
14
|
WorktreeResolution,
|
|
@@ -60,9 +62,23 @@ export interface CassetteAgent {
|
|
|
60
62
|
readonly sentFrames: readonly Frame[];
|
|
61
63
|
readonly receivedFrames: readonly Frame[];
|
|
62
64
|
readonly asks: readonly CassetteAsk[];
|
|
65
|
+
/** Compactions between this Agent's Asks; absent for an Agent that compacted none. */
|
|
66
|
+
readonly compactions?: readonly CassetteCompaction[];
|
|
63
67
|
readonly stats: AgentStats;
|
|
64
68
|
}
|
|
65
69
|
|
|
70
|
+
/** The frames attributed to one compaction marker (ADR-0043). */
|
|
71
|
+
export interface CassetteCompaction {
|
|
72
|
+
readonly index: number;
|
|
73
|
+
readonly hash: string;
|
|
74
|
+
/** Settled Asks of this Agent at the compaction point. */
|
|
75
|
+
readonly afterAsks: number;
|
|
76
|
+
/** What the recorded compaction reported; absent when the exchange failed. */
|
|
77
|
+
readonly result?: CompactionResult;
|
|
78
|
+
readonly sentFrames: readonly Frame[];
|
|
79
|
+
readonly receivedFrames: readonly Frame[];
|
|
80
|
+
}
|
|
81
|
+
|
|
66
82
|
/** Behavioural identity supplied when opening a recorded Agent. */
|
|
67
83
|
export interface CassetteSpawn {
|
|
68
84
|
readonly name: string;
|
|
@@ -88,6 +104,17 @@ export interface CassetteSpawn {
|
|
|
88
104
|
* across machines (ADR-0040). Absent when the Agent ran no extension.
|
|
89
105
|
*/
|
|
90
106
|
readonly declaredExtensions?: readonly string[];
|
|
107
|
+
/**
|
|
108
|
+
* Resolved name of the Agent named as this Agent's parent (Parent Link).
|
|
109
|
+
* Part of spawn identity; absent for a root Agent and for older Cassettes.
|
|
110
|
+
*/
|
|
111
|
+
readonly parent?: string;
|
|
112
|
+
/** How the Agent came to be; absent means an ordinary spawn. Spawn identity. */
|
|
113
|
+
readonly origin?: "fork";
|
|
114
|
+
/** Name of the Agent this one was forked from. Spawn identity. */
|
|
115
|
+
readonly forkOf?: string;
|
|
116
|
+
/** Settled Asks of the fork source at the fork point. Spawn identity. */
|
|
117
|
+
readonly forkAsks?: number;
|
|
91
118
|
}
|
|
92
119
|
|
|
93
120
|
/** The frames attributed to one Ask marker. */
|
|
@@ -137,6 +164,8 @@ export interface CassetteRecorder {
|
|
|
137
164
|
received(frame: Frame): void;
|
|
138
165
|
beginAsk(marker: AskMarker): void;
|
|
139
166
|
finishAsk(completion: AskCompletion): void;
|
|
167
|
+
beginCompaction(marker: CompactionMarker): void;
|
|
168
|
+
finishCompaction(result: CompactionResult | undefined): void;
|
|
140
169
|
closed(stats: AgentStats): void;
|
|
141
170
|
}
|
|
142
171
|
|
|
@@ -156,6 +185,15 @@ interface MutableAsk {
|
|
|
156
185
|
recovered?: true;
|
|
157
186
|
}
|
|
158
187
|
|
|
188
|
+
interface MutableCompaction {
|
|
189
|
+
readonly index: number;
|
|
190
|
+
readonly hash: string;
|
|
191
|
+
readonly afterAsks: number;
|
|
192
|
+
readonly sentFrames: Frame[];
|
|
193
|
+
readonly receivedFrames: Frame[];
|
|
194
|
+
result?: CompactionResult;
|
|
195
|
+
}
|
|
196
|
+
|
|
159
197
|
interface MutableAgent {
|
|
160
198
|
readonly spawn: CassetteSpawn;
|
|
161
199
|
readonly model: string;
|
|
@@ -165,8 +203,10 @@ interface MutableAgent {
|
|
|
165
203
|
readonly sentFrames: Frame[];
|
|
166
204
|
readonly receivedFrames: Frame[];
|
|
167
205
|
readonly asks: MutableAsk[];
|
|
206
|
+
readonly compactions: MutableCompaction[];
|
|
168
207
|
stats: AgentStats;
|
|
169
|
-
|
|
208
|
+
/** The slot later frames belong to: one Ask, one compaction, or the Agent. */
|
|
209
|
+
active: MutableAsk | MutableCompaction | null;
|
|
170
210
|
}
|
|
171
211
|
|
|
172
212
|
/** Collects a Cassette in memory; `executeRun` serializes it at Run settlement. */
|
|
@@ -189,6 +229,7 @@ export class CassetteCollector implements CassetteSink {
|
|
|
189
229
|
sentFrames: [],
|
|
190
230
|
receivedFrames: [],
|
|
191
231
|
asks: [],
|
|
232
|
+
compactions: [],
|
|
192
233
|
stats: { tokens: null, cost: null },
|
|
193
234
|
active: null,
|
|
194
235
|
};
|
|
@@ -206,11 +247,24 @@ export class CassetteCollector implements CassetteSink {
|
|
|
206
247
|
agent.active = ask;
|
|
207
248
|
},
|
|
208
249
|
finishAsk: (completion): void => {
|
|
209
|
-
|
|
210
|
-
if (
|
|
211
|
-
if (completion.
|
|
212
|
-
if (completion.
|
|
213
|
-
if (completion.
|
|
250
|
+
const ask = activeAsk(agent);
|
|
251
|
+
if (!ask) return;
|
|
252
|
+
if (completion.limit !== undefined) ask.limit = completion.limit;
|
|
253
|
+
if (completion.stalled !== undefined) ask.stalled = completion.stalled;
|
|
254
|
+
if (completion.invalidOutput !== undefined) ask.outcome = completion.invalidOutput;
|
|
255
|
+
if (completion.recovered === true) ask.recovered = true;
|
|
256
|
+
},
|
|
257
|
+
beginCompaction: (marker): void => {
|
|
258
|
+
if (!agent) return;
|
|
259
|
+
const compaction = { ...marker, sentFrames: [], receivedFrames: [] };
|
|
260
|
+
agent.compactions.push(compaction);
|
|
261
|
+
agent.active = compaction;
|
|
262
|
+
},
|
|
263
|
+
finishCompaction: (result): void => {
|
|
264
|
+
const compaction = agent?.compactions.at(-1);
|
|
265
|
+
if (compaction !== undefined && result !== undefined) compaction.result = result;
|
|
266
|
+
// Later frames belong to the Agent again, not to the compaction.
|
|
267
|
+
if (agent) agent.active = null;
|
|
214
268
|
},
|
|
215
269
|
closed: (stats): void => {
|
|
216
270
|
if (agent) agent.stats = stats;
|
|
@@ -234,6 +288,7 @@ export class CassetteCollector implements CassetteSink {
|
|
|
234
288
|
sentFrames,
|
|
235
289
|
receivedFrames,
|
|
236
290
|
asks,
|
|
291
|
+
compactions,
|
|
237
292
|
stats,
|
|
238
293
|
}) => ({
|
|
239
294
|
spawn,
|
|
@@ -244,6 +299,9 @@ export class CassetteCollector implements CassetteSink {
|
|
|
244
299
|
sentFrames,
|
|
245
300
|
receivedFrames,
|
|
246
301
|
asks,
|
|
302
|
+
// Absent for an Agent that compacted nothing, so every Cassette
|
|
303
|
+
// recorded before ADR-0043 keeps its byte-identical shape.
|
|
304
|
+
...(compactions.length === 0 ? {} : { compactions }),
|
|
247
305
|
stats,
|
|
248
306
|
}),
|
|
249
307
|
),
|
|
@@ -272,10 +330,21 @@ function spawnIdentity(options: OpenOptions): CassetteSpawn {
|
|
|
272
330
|
...(options.declaredExtensions === undefined || options.declaredExtensions.length === 0
|
|
273
331
|
? {}
|
|
274
332
|
: { declaredExtensions: [...options.declaredExtensions] }),
|
|
333
|
+
...(options.parent === undefined ? {} : { parent: options.parent }),
|
|
334
|
+
...(options.origin === undefined ? {} : { origin: options.origin }),
|
|
335
|
+
...(options.forkOf === undefined ? {} : { forkOf: options.forkOf }),
|
|
336
|
+
...(options.forkAsks === undefined ? {} : { forkAsks: options.forkAsks }),
|
|
275
337
|
...(options.worktree === true ? { worktree: true } : {}),
|
|
276
338
|
};
|
|
277
339
|
}
|
|
278
340
|
|
|
341
|
+
/** The Ask that later frames belong to, or nothing when a compaction owns them. */
|
|
342
|
+
function activeAsk(agent: MutableAgent | null): MutableAsk | null {
|
|
343
|
+
const active = agent?.active;
|
|
344
|
+
if (!active || "afterAsks" in active) return null;
|
|
345
|
+
return active;
|
|
346
|
+
}
|
|
347
|
+
|
|
279
348
|
function append(
|
|
280
349
|
agent: MutableAgent | null,
|
|
281
350
|
direction: "sentFrames" | "receivedFrames",
|
package/src/cassette/index.ts
CHANGED
|
@@ -4,6 +4,9 @@ import type {
|
|
|
4
4
|
AskCompletion,
|
|
5
5
|
AskMarker,
|
|
6
6
|
AskPlayback,
|
|
7
|
+
CompactionMarker,
|
|
8
|
+
CompactionPlayback,
|
|
9
|
+
CompactionResult,
|
|
7
10
|
Frame,
|
|
8
11
|
OpenOptions,
|
|
9
12
|
RecordedSpawnSelection,
|
|
@@ -87,6 +90,16 @@ class RecordingTransport implements AgentTransport {
|
|
|
87
90
|
this.#inner.finishAsk(completion);
|
|
88
91
|
}
|
|
89
92
|
|
|
93
|
+
beginCompaction(marker: CompactionMarker): CompactionPlayback | undefined {
|
|
94
|
+
this.#recorder.beginCompaction(marker);
|
|
95
|
+
return this.#inner.beginCompaction(marker);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
finishCompaction(result: CompactionResult | undefined): void {
|
|
99
|
+
this.#recorder.finishCompaction(result);
|
|
100
|
+
this.#inner.finishCompaction(result);
|
|
101
|
+
}
|
|
102
|
+
|
|
90
103
|
recordedExtractionPolicy(index: number): string | undefined {
|
|
91
104
|
return this.#inner.recordedExtractionPolicy?.(index);
|
|
92
105
|
}
|