@yaag/extension 0.3.0 → 0.5.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/src/run-record.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { initialSummary, type RunSummary } from "@yaag/runtime";
2
2
  import type { ProcessIdentity } from "./process-liveness.ts";
3
+ import { parseRunSummary } from "./run-summary-parse.ts";
3
4
 
4
5
  /**
5
6
  * The durable state of a Run.
@@ -16,21 +17,26 @@ export type PersistedOutcome =
16
17
  | { readonly kind: "fulfilled"; readonly code: number | null; readonly result: string }
17
18
  | { readonly kind: "rejected"; readonly reason: string };
18
19
 
19
- /** What starting a Run said about it; enough to describe it in a later session. */
20
- export interface RunLaunch {
21
- /**
22
- * Exactly one of `file` and `script` is present: `file` for a program file,
23
- * `script` for an Inline Program (ADR-0033). The pair stays a tolerant
24
- * optional pair rather than a union, because a record written by an older
25
- * yaag must still narrow.
26
- */
27
- readonly file?: string;
28
- readonly script?: string;
20
+ /** Everything a launch carries besides the program itself. */
21
+ export interface RunLaunchContext {
29
22
  readonly args?: string;
30
23
  readonly record?: string;
31
24
  readonly resume?: string;
32
25
  }
33
26
 
27
+ /**
28
+ * What starting a Run said about it; enough to describe it in a later session.
29
+ *
30
+ * The program is one of exactly two kinds (ADR-0033): a program file, or an
31
+ * Inline Program source. The tolerance for a record an older yaag wrote without
32
+ * a `kind` lives in {@link parseRunRecord}, not in this type. A new record does
33
+ * carry `kind` on disk, which an older yaag ignores, so the format drifts in
34
+ * both directions without loss.
35
+ */
36
+ export type RunLaunch =
37
+ | (RunLaunchContext & { readonly kind: "file"; readonly file: string })
38
+ | (RunLaunchContext & { readonly kind: "inline"; readonly script: string });
39
+
34
40
  /** One Run as stored on disk, at `<runs dir>/<id>.json`. */
35
41
  export interface RunRecord {
36
42
  readonly id: string;
@@ -44,22 +50,45 @@ export interface RunRecord {
44
50
  }
45
51
 
46
52
  /**
47
- * Narrows an untrusted parsed JSON value to a {@link RunRecord}.
53
+ * Parses an untrusted parsed JSON value into a {@link RunRecord}, or `null`.
48
54
  *
49
55
  * A record may have been written by an older yaag, or truncated by a crash, so
50
56
  * every field the extension reads is checked here and a record that fails is
51
- * dropped rather than repaired.
57
+ * dropped rather than repaired. The record is rebuilt from checked fields, and
58
+ * the launch is normalized to its tagged kind, which is why this is a parser
59
+ * and not a type guard. The Summary shape is checked by {@link parseRunSummary}.
52
60
  */
53
- export function isRunRecord(value: unknown): value is RunRecord {
54
- if (typeof value !== "object" || value === null) return false;
61
+ export function parseRunRecord(value: unknown): RunRecord | null {
62
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
55
63
  const candidate: Record<string, unknown> = { ...value };
56
- return (
57
- typeof candidate["id"] === "string" &&
58
- typeof candidate["startedAt"] === "string" &&
59
- isState(candidate["state"]) &&
60
- isLaunch(candidate["launch"]) &&
61
- isSummary(candidate["summary"])
62
- );
64
+ const launch = parseLaunch(candidate["launch"]);
65
+ const identity = parseProcess(candidate["process"]);
66
+ const summary = parseRunSummary(candidate["summary"]);
67
+ const outcome = parseOutcome(candidate["outcome"]);
68
+ if (
69
+ launch === null ||
70
+ identity === undefined ||
71
+ summary === null ||
72
+ outcome === undefined ||
73
+ typeof candidate["id"] !== "string" ||
74
+ typeof candidate["startedAt"] !== "string" ||
75
+ !isNullableString(candidate["endedAt"]) ||
76
+ !isState(candidate["state"])
77
+ ) {
78
+ return null;
79
+ }
80
+ // The record is rebuilt field by field, so nothing untrusted is cast: every
81
+ // field a consumer reads has the type this parser proved (rule 0001 §3).
82
+ return {
83
+ id: candidate["id"],
84
+ launch,
85
+ process: identity,
86
+ startedAt: candidate["startedAt"],
87
+ endedAt: candidate["endedAt"],
88
+ state: candidate["state"],
89
+ summary,
90
+ outcome,
91
+ };
63
92
  }
64
93
 
65
94
  /** A Run record for a Run that has just started. */
@@ -87,15 +116,65 @@ function isState(value: unknown): value is PersistedRunState {
87
116
  );
88
117
  }
89
118
 
90
- function isLaunch(value: unknown): value is RunLaunch {
91
- return (
92
- typeof value === "object" &&
93
- value !== null &&
94
- (("file" in value && typeof value.file === "string") ||
95
- ("script" in value && typeof value.script === "string"))
96
- );
119
+ /** Normalizes the tolerant on-disk launch shape; a record with no `kind` still parses. */
120
+ function parseLaunch(value: unknown): RunLaunch | null {
121
+ if (typeof value !== "object" || value === null) return null;
122
+ const stored: Record<string, unknown> = { ...value };
123
+ const { file, script } = stored;
124
+ const context = launchContext(stored);
125
+ // Both present is the state `programTarget` refuses, so a record holding it
126
+ // is dropped rather than repaired, like a record holding neither.
127
+ if (typeof file === "string" && typeof script === "string") return null;
128
+ if (typeof file === "string") return { kind: "file", file, ...context };
129
+ if (typeof script === "string") return { kind: "inline", script, ...context };
130
+ return null;
131
+ }
132
+
133
+ function launchContext(stored: Record<string, unknown>): RunLaunchContext {
134
+ return {
135
+ ...(typeof stored.args === "string" ? { args: stored.args } : {}),
136
+ ...(typeof stored.record === "string" ? { record: stored.record } : {}),
137
+ ...(typeof stored.resume === "string" ? { resume: stored.resume } : {}),
138
+ };
139
+ }
140
+
141
+ /**
142
+ * `undefined` means the stored value is malformed or missing; a stored `null`
143
+ * is a Run whose process was never known.
144
+ */
145
+ function parseProcess(value: unknown): ProcessIdentity | null | undefined {
146
+ if (value === null) return null;
147
+ if (typeof value !== "object" || Array.isArray(value)) return undefined;
148
+ const stored: Record<string, unknown> = { ...value };
149
+ if (typeof stored.pid !== "number" || !isNullableString(stored.startedAt)) {
150
+ return undefined;
151
+ }
152
+ return { pid: stored.pid, startedAt: stored.startedAt };
153
+ }
154
+
155
+ /**
156
+ * `undefined` means the stored value is malformed or missing; a stored `null`
157
+ * is a Run that has not ended.
158
+ */
159
+ function parseOutcome(value: unknown): PersistedOutcome | null | undefined {
160
+ if (value === null) return null;
161
+ if (typeof value !== "object" || Array.isArray(value)) return undefined;
162
+ const stored: Record<string, unknown> = { ...value };
163
+ if (stored.kind === "fulfilled") {
164
+ if (typeof stored.result !== "string" || !isNullableNumber(stored.code)) return undefined;
165
+ return { kind: "fulfilled", code: stored.code, result: stored.result };
166
+ }
167
+ if (stored.kind === "rejected") {
168
+ if (typeof stored.reason !== "string") return undefined;
169
+ return { kind: "rejected", reason: stored.reason };
170
+ }
171
+ return undefined;
172
+ }
173
+
174
+ function isNullableString(value: unknown): value is string | null {
175
+ return value === null || typeof value === "string";
97
176
  }
98
177
 
99
- function isSummary(value: unknown): value is RunSummary {
100
- return typeof value === "object" && value !== null && "runState" in value;
178
+ function isNullableNumber(value: unknown): value is number | null {
179
+ return value === null || typeof value === "number";
101
180
  }
@@ -1,5 +1,6 @@
1
1
  import type { EndedRunSummary, RunSummary } from "@yaag/runtime";
2
2
  import type { ProcessIdentity } from "./process-liveness.ts";
3
+ import { pickInlineResumeSource } from "./resume-source.ts";
3
4
  import { type RunLaunch, type RunRecord, startedRecord } from "./run-record.ts";
4
5
  import { restoreRecords } from "./run-restore.ts";
5
6
  import type { RunStore } from "./run-store.ts";
@@ -135,6 +136,21 @@ export class RunRegistry {
135
136
  return this.#runs.get(id) ?? { state: "unknown" };
136
137
  }
137
138
 
139
+ /**
140
+ * The Inline Program source a Checkpoint resumes, read back from the durable
141
+ * records (ADR-0033). Null when no record holds one, and for a memory-only
142
+ * registry.
143
+ *
144
+ * It waits for every queued write first: a record write is asynchronous and
145
+ * queued, so a resume of a Run that just settled would otherwise read the
146
+ * directory before its own record is there and refuse a resumable Run.
147
+ */
148
+ async inlineSourceFor(artifact: string): Promise<string | null> {
149
+ if (this.#store === undefined) return null;
150
+ await this.#store.settled();
151
+ return pickInlineResumeSource(await this.#store.load(), artifact);
152
+ }
153
+
138
154
  /** Reads a Run this session never started back from the store, by id. */
139
155
  async recall(id: string): Promise<RunStatus> {
140
156
  const known = this.lookup(id);
package/src/run-store.ts CHANGED
@@ -1,12 +1,18 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { mkdir, readdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
2
+ import { chmod, mkdir, readdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
- import { isRunRecord, type RunRecord } from "./run-record.ts";
4
+ import { parseRunRecord, type RunRecord } from "./run-record.ts";
5
5
  import { resolveRunsDirectory } from "./runs-dir.ts";
6
6
 
7
7
  /** How many finished records are kept before the oldest ones are pruned. */
8
8
  export const FINISHED_RECORD_CAP = 200;
9
9
 
10
+ /** Owner-only directory: a Run record can hold an Inline Program source (ADR-0033). */
11
+ export const RECORD_DIRECTORY_MODE = 0o700;
12
+
13
+ /** Owner-only record file, for the same reason as {@link RECORD_DIRECTORY_MODE}. */
14
+ export const RECORD_FILE_MODE = 0o600;
15
+
10
16
  export interface RunStoreOptions {
11
17
  readonly directory?: string;
12
18
  readonly cap?: number;
@@ -19,6 +25,11 @@ export interface RunStoreOptions {
19
25
  * working when the state directory is read-only or full. Writes go to a temp
20
26
  * file and are renamed into place, so a reader never sees a half-written record
21
27
  * (ADR-0021).
28
+ *
29
+ * A record holds the whole Inline Program source (an inline `RunLaunch`), so the
30
+ * directory and each record file are private to the user who owns them
31
+ * ({@link RECORD_DIRECTORY_MODE}, {@link RECORD_FILE_MODE}); the source stays
32
+ * off argv for the same reason (ADR-0033).
22
33
  */
23
34
  export class RunStore {
24
35
  readonly #directory: string;
@@ -84,9 +95,15 @@ export class RunStore {
84
95
  const target = join(this.#directory, `${record.id}.json`);
85
96
  const temp = `${target}.${randomUUID()}.tmp`;
86
97
  try {
87
- await mkdir(this.#directory, { recursive: true });
88
- await writeFile(temp, JSON.stringify(record), "utf8");
98
+ await mkdir(this.#directory, { recursive: true, mode: RECORD_DIRECTORY_MODE });
99
+ await writeFile(temp, JSON.stringify(record), { encoding: "utf8", mode: RECORD_FILE_MODE });
89
100
  await rename(temp, target);
101
+ // A directory or a record file that an older yaag left readable is
102
+ // tightened here: the record can hold an Inline Program source. The two
103
+ // modes above already hold for what this write creates, because an
104
+ // explicit mode is not broadened by the umask.
105
+ await chmod(target, RECORD_FILE_MODE);
106
+ await chmod(this.#directory, RECORD_DIRECTORY_MODE);
90
107
  } catch {
91
108
  await unlink(temp).catch(() => {});
92
109
  }
@@ -95,7 +112,7 @@ export class RunStore {
95
112
  async #read(entry: string): Promise<RunRecord | null> {
96
113
  try {
97
114
  const value: unknown = JSON.parse(await readFile(join(this.#directory, entry), "utf8"));
98
- return isRunRecord(value) ? value : null;
115
+ return parseRunRecord(value);
99
116
  } catch {
100
117
  return null;
101
118
  }
@@ -0,0 +1,265 @@
1
+ import type {
2
+ AgentActivity,
3
+ AgentInfo,
4
+ AgentState,
5
+ NodeInfo,
6
+ NodeState,
7
+ RunOutcome,
8
+ RunSummary,
9
+ TokenBreakdown,
10
+ } from "@yaag/runtime";
11
+
12
+ /**
13
+ * Parses the persisted Summary of a Run record, or returns `null`.
14
+ *
15
+ * The Summary is the one field of a Run record whose whole shape is read again
16
+ * by the renderers, so it is reconstructed field by field instead of cast: a
17
+ * record truncated by a crash or written by an older yaag is dropped rather
18
+ * than repaired (ADR-0021). Every optional field of the fold is rebuilt only
19
+ * when the stored value has the type the fold gives it.
20
+ */
21
+ export function parseRunSummary(value: unknown): RunSummary | null {
22
+ const stored = asRecord(value);
23
+ if (stored === null) return null;
24
+ const base = parseBase(stored);
25
+ if (base === null) return null;
26
+ if (stored.runState === "running") {
27
+ if (stored.outcome !== null || stored.ok !== null) return null;
28
+ return { ...base, runState: "running", outcome: null, ok: null };
29
+ }
30
+ if (stored.runState !== "ended") return null;
31
+ const outcome = stored.outcome;
32
+ const lost = stored.checkpointLost;
33
+ if (!isOutcome(outcome) || typeof stored.ok !== "boolean") return null;
34
+ if (!isNullableNumber(stored.endedAt)) return null;
35
+ if (lost !== undefined && typeof lost !== "string") return null;
36
+ return {
37
+ ...base,
38
+ runState: "ended",
39
+ outcome,
40
+ ok: stored.ok,
41
+ endedAt: stored.endedAt,
42
+ ...(lost === undefined ? {} : { checkpointLost: lost }),
43
+ };
44
+ }
45
+
46
+ /** The accounting and identity facts every Summary state carries. */
47
+ interface SummaryBase {
48
+ readonly program: string;
49
+ readonly startedAt: number | null;
50
+ readonly artifact: string | null;
51
+ readonly agents: Readonly<Record<string, AgentInfo>>;
52
+ readonly asksStarted: number;
53
+ readonly asksSettled: number;
54
+ readonly cost: number;
55
+ readonly tokens: TokenBreakdown | null;
56
+ readonly incomplete: boolean;
57
+ readonly durationMs: number;
58
+ readonly worstFrameGapMs: number;
59
+ }
60
+
61
+ function parseBase(stored: Record<string, unknown>): SummaryBase | null {
62
+ const agents = parseAgents(stored.agents);
63
+ const tokens = parseTokens(stored.tokens);
64
+ if (
65
+ agents === null ||
66
+ tokens === undefined ||
67
+ typeof stored.program !== "string" ||
68
+ !isNullableNumber(stored.startedAt) ||
69
+ !isNullableString(stored.artifact) ||
70
+ typeof stored.asksStarted !== "number" ||
71
+ typeof stored.asksSettled !== "number" ||
72
+ typeof stored.cost !== "number" ||
73
+ typeof stored.incomplete !== "boolean" ||
74
+ typeof stored.durationMs !== "number" ||
75
+ typeof stored.worstFrameGapMs !== "number"
76
+ ) {
77
+ return null;
78
+ }
79
+ return {
80
+ program: stored.program,
81
+ startedAt: stored.startedAt,
82
+ artifact: stored.artifact,
83
+ agents,
84
+ asksStarted: stored.asksStarted,
85
+ asksSettled: stored.asksSettled,
86
+ cost: stored.cost,
87
+ tokens,
88
+ incomplete: stored.incomplete,
89
+ durationMs: stored.durationMs,
90
+ worstFrameGapMs: stored.worstFrameGapMs,
91
+ };
92
+ }
93
+
94
+ function parseAgents(value: unknown): Record<string, AgentInfo> | null {
95
+ const stored = asRecord(value);
96
+ if (stored === null) return null;
97
+ const agents: Record<string, AgentInfo> = {};
98
+ for (const [name, entry] of Object.entries(stored)) {
99
+ const agent = parseAgent(entry);
100
+ if (agent === null) return null;
101
+ agents[name] = agent;
102
+ }
103
+ return agents;
104
+ }
105
+
106
+ function parseAgent(value: unknown): AgentInfo | null {
107
+ const stored = asRecord(value);
108
+ if (stored === null) return null;
109
+ const state = stored.state;
110
+ const nodes = parseNodes(stored.nodes);
111
+ const tokens = parseTokens(stored.tokens);
112
+ const activity = parseActivity(stored.activity);
113
+ if (
114
+ nodes === null ||
115
+ tokens === undefined ||
116
+ activity === undefined ||
117
+ !isAgentState(state) ||
118
+ !isNullableString(stored.model) ||
119
+ !isNullableString(stored.cwd) ||
120
+ !isNullableString(stored.branch) ||
121
+ !isNullableString(stored.sessionFile) ||
122
+ !isNullableNumber(stored.cost) ||
123
+ typeof stored.incomplete !== "boolean" ||
124
+ !isNullableNumber(stored.stateChangedAt) ||
125
+ !isNullableNumber(stored.usageUpdatedAt) ||
126
+ !isNullableNumber(stored.askStartedAt) ||
127
+ typeof stored.finishedNodesPruned !== "number"
128
+ ) {
129
+ return null;
130
+ }
131
+ const base = {
132
+ model: stored.model,
133
+ cwd: stored.cwd,
134
+ branch: stored.branch,
135
+ sessionFile: stored.sessionFile,
136
+ activity,
137
+ tokens,
138
+ cost: stored.cost,
139
+ incomplete: stored.incomplete,
140
+ stateChangedAt: stored.stateChangedAt,
141
+ usageUpdatedAt: stored.usageUpdatedAt,
142
+ askStartedAt: stored.askStartedAt,
143
+ nodes,
144
+ finishedNodesPruned: stored.finishedNodesPruned,
145
+ };
146
+ if (state === "asking") {
147
+ if (typeof stored.askIndex !== "number") return null;
148
+ if (typeof stored.promptGist !== "string") return null;
149
+ if (typeof stored.replayed !== "boolean") return null;
150
+ return {
151
+ ...base,
152
+ state,
153
+ askIndex: stored.askIndex,
154
+ promptGist: stored.promptGist,
155
+ replayed: stored.replayed,
156
+ };
157
+ }
158
+ if (!isNullableNumber(stored.askIndex)) return null;
159
+ if (!isNullableString(stored.promptGist)) return null;
160
+ return { ...base, state, askIndex: stored.askIndex, promptGist: stored.promptGist };
161
+ }
162
+
163
+ function parseNodes(value: unknown): NodeInfo[] | null {
164
+ if (!Array.isArray(value)) return null;
165
+ const nodes: NodeInfo[] = [];
166
+ for (const entry of value) {
167
+ const stored = asRecord(entry);
168
+ if (stored === null) return null;
169
+ const tokens = parseTokens(stored.tokens);
170
+ if (
171
+ tokens === undefined ||
172
+ typeof stored.path !== "string" ||
173
+ !isNodeState(stored.state) ||
174
+ !isNullableString(stored.activityGist) ||
175
+ !isNullableNumber(stored.cost) ||
176
+ !isNullableNumber(stored.updatedAt)
177
+ ) {
178
+ return null;
179
+ }
180
+ nodes.push({
181
+ path: stored.path,
182
+ state: stored.state,
183
+ activityGist: stored.activityGist,
184
+ tokens,
185
+ cost: stored.cost,
186
+ updatedAt: stored.updatedAt,
187
+ });
188
+ }
189
+ return nodes;
190
+ }
191
+
192
+ /**
193
+ * `undefined` means the stored value is malformed or missing; a stored `null`
194
+ * is an Agent or Node whose usage was never reported.
195
+ */
196
+ function parseTokens(value: unknown): TokenBreakdown | null | undefined {
197
+ if (value === null) return null;
198
+ const stored = asRecord(value);
199
+ if (stored === null) return undefined;
200
+ const fields = ["input", "output", "cacheRead", "cacheWrite", "total"] as const;
201
+ if (fields.some((field) => typeof stored[field] !== "number")) return undefined;
202
+ return {
203
+ input: stored.input as number,
204
+ output: stored.output as number,
205
+ cacheRead: stored.cacheRead as number,
206
+ cacheWrite: stored.cacheWrite as number,
207
+ total: stored.total as number,
208
+ };
209
+ }
210
+
211
+ /**
212
+ * `undefined` means the stored value is malformed or missing; a stored `null`
213
+ * is an Agent with no current activity.
214
+ */
215
+ function parseActivity(value: unknown): AgentActivity | null | undefined {
216
+ if (value === null) return null;
217
+ const stored = asRecord(value);
218
+ if (stored === null) return undefined;
219
+ const type = stored.type;
220
+ if (type === "thinking" || type === "writing" || type === "compacting") return { type };
221
+ if (type === "tool") {
222
+ if (typeof stored.name !== "string" || typeof stored.argsGist !== "string") {
223
+ return undefined;
224
+ }
225
+ return { type, name: stored.name, argsGist: stored.argsGist };
226
+ }
227
+ if (type === "retrying") {
228
+ if (typeof stored.attempt !== "number" || typeof stored.max !== "number") {
229
+ return undefined;
230
+ }
231
+ return { type, attempt: stored.attempt, max: stored.max };
232
+ }
233
+ return undefined;
234
+ }
235
+
236
+ function asRecord(value: unknown): Record<string, unknown> | null {
237
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
238
+ return { ...value };
239
+ }
240
+
241
+ function isNullableString(value: unknown): value is string | null {
242
+ return value === null || typeof value === "string";
243
+ }
244
+
245
+ function isNullableNumber(value: unknown): value is number | null {
246
+ return value === null || typeof value === "number";
247
+ }
248
+
249
+ function isAgentState(value: unknown): value is AgentState {
250
+ return value === "idle" || value === "asking" || value === "exited";
251
+ }
252
+
253
+ function isNodeState(value: unknown): value is NodeState {
254
+ return value === "running" || value === "exited" || value === "failed";
255
+ }
256
+
257
+ function isOutcome(value: unknown): value is RunOutcome {
258
+ return (
259
+ value === "completed" ||
260
+ value === "failed" ||
261
+ value === "stopped" ||
262
+ value === "paused" ||
263
+ value === "interrupted"
264
+ );
265
+ }
@@ -1,16 +1,14 @@
1
1
  import { dirname, join } from "node:path";
2
2
  import type { AgentToolResult, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
- import { stripTerminalSequences } from "@earendil-works/pi-tui";
4
- import { initialSummary } from "@yaag/runtime";
5
3
  import type { BackgroundStatus } from "./background-status.ts";
6
- import { fakeTheme } from "./fake-theme.ts";
7
4
  import { resolveBun } from "./resolve-bun.ts";
8
5
  import { resolveCliEntry } from "./resolve-cli.ts";
6
+ import type { RunParams } from "./run-call-render.ts";
9
7
  import type { RunDetails } from "./run-details.ts";
10
8
  import { RunRegistry } from "./run-registry.ts";
11
9
  import { createRunTool, type SendMessage } from "./run-tool.ts";
12
10
  import type { RunTreeStore } from "./run-trees.ts";
13
- import type { RunHandle, StartRunOptions } from "./spawn-run.ts";
11
+ import type { RunHandle } from "./spawn-run.ts";
14
12
  import { createStopTool, type StopDetails } from "./stop-tool.ts";
15
13
  import { TestExtensionContext } from "./test-extension-context.ts";
16
14
 
@@ -21,63 +19,6 @@ const ctx = new TestExtensionContext(dirname(cli));
21
19
 
22
20
  export const fixture = (name: string): string => join(dirname(cli), "fixtures", `${name}.ts`);
23
21
 
24
- export interface RunParams {
25
- readonly file?: string;
26
- readonly script?: string;
27
- readonly args?: string;
28
- readonly background?: boolean;
29
- readonly record?: string;
30
- readonly resume?: string;
31
- }
32
-
33
- /** Renders the tool's call line, stripped of styling, as the transcript shows it. */
34
- export function renderCallLabel(params: RunParams): string {
35
- const tool = createRunTool({
36
- bun,
37
- cli,
38
- registry: new RunRegistry(),
39
- sendMessage: () => undefined,
40
- });
41
- const component = tool.renderCall?.(params, fakeTheme(), {
42
- args: params,
43
- toolCallId: "call-1",
44
- invalidate: () => {},
45
- lastComponent: undefined,
46
- state: undefined,
47
- cwd: process.cwd(),
48
- executionStarted: false,
49
- argsComplete: true,
50
- isPartial: false,
51
- expanded: false,
52
- showImages: false,
53
- isError: false,
54
- });
55
- return stripTerminalSequences(component?.render(80)[0] ?? "");
56
- }
57
-
58
- /** A start seam that never spawns: it captures its options and ends the Run at once. */
59
- export function fakeStart(stdout = "{}"): {
60
- readonly start: (options: StartRunOptions) => RunHandle;
61
- readonly seen: StartRunOptions[];
62
- } {
63
- const seen: StartRunOptions[] = [];
64
- return {
65
- seen,
66
- start: (options) => {
67
- seen.push(options);
68
- return {
69
- stop: () => {},
70
- outcome: Promise.resolve({
71
- code: 0,
72
- stdout,
73
- stderr: "",
74
- summary: initialSummary(),
75
- }),
76
- };
77
- },
78
- };
79
- }
80
-
81
22
  export interface Sent {
82
23
  readonly content: unknown;
83
24
  readonly details: unknown;