@yaag/extension 0.5.2 → 0.6.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 +4 -4
- package/src/index.ts +5 -2
- package/src/restored-run-text.ts +77 -0
- package/src/run-listing.ts +33 -0
- package/src/run-picker.ts +11 -4
- package/src/run-record.ts +9 -0
- package/src/run-registry.ts +70 -11
- package/src/status-tool.ts +53 -65
- package/src/stop-tool.ts +2 -2
- package/src/yaag-command.ts +18 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yaag/extension",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@earendil-works/pi-tui": "^0.84.0",
|
|
27
|
-
"@yaag/cli": "0.
|
|
28
|
-
"@yaag/runtime": "0.
|
|
29
|
-
"@yaag/tui": "0.
|
|
27
|
+
"@yaag/cli": "0.6.0",
|
|
28
|
+
"@yaag/runtime": "0.6.0",
|
|
29
|
+
"@yaag/tui": "0.6.0",
|
|
30
30
|
"nanoid": "^6.0.1"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -35,7 +35,6 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
35
35
|
// One registry per session, mirrored to a durable store so a restart of this
|
|
36
36
|
// Host Session keeps every Run id addressable (ticket 02).
|
|
37
37
|
const registry = new RunRegistry({ store: new RunStore() });
|
|
38
|
-
await registry.restore();
|
|
39
38
|
// The renderer projections and the background footer segment both live for
|
|
40
39
|
// the session, beside the registry, so a background Run keeps a tree.
|
|
41
40
|
const store = new RunTreeStore();
|
|
@@ -56,9 +55,13 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
56
55
|
const appendYaagPrompt = createSystemPromptAppender(() => findProgramDirectories(process.cwd()));
|
|
57
56
|
pi.on("before_agent_start", (event) => appendYaagPrompt(event));
|
|
58
57
|
|
|
59
|
-
pi.on("session_start", (_event, ctx) => {
|
|
58
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
60
59
|
if (bun === null) ctx.ui.notify(report, "error");
|
|
61
60
|
if (ctx.hasUI) status.bind(ctx.ui);
|
|
61
|
+
// The pi session id is knowable only here, and `/resume` rebinds this
|
|
62
|
+
// extension and fires the same id, which is what re-adopts this session's
|
|
63
|
+
// Runs. A fork arrives with a new id and adopts nothing (ADR-0036).
|
|
64
|
+
await registry.restore(ctx.sessionManager.getSessionId());
|
|
62
65
|
});
|
|
63
66
|
|
|
64
67
|
// Commands and tools have separate namespaces: this CLI-resolution command
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The words a Run read back from its record gets, shared by `yaag_status` and
|
|
3
|
+
* the `/yaag` snapshot view (ADR-0036). A resumed session owns such a Run again
|
|
4
|
+
* when the record names it, but no session is attached to it: there is no event
|
|
5
|
+
* stream and no renderer projection, only the persisted Summary. Pure text, no
|
|
6
|
+
* I/O.
|
|
7
|
+
*/
|
|
8
|
+
import type { RunViewResult } from "@yaag/tui";
|
|
9
|
+
import type { RunRecord } from "./run-record.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* States the persisted verdict of a Run that no process is attached to.
|
|
13
|
+
*
|
|
14
|
+
* The folded Summary alone cannot say that the Run stopped without an owner
|
|
15
|
+
* process, or that a Checkpoint of it can be resumed (ticket 02), so the state,
|
|
16
|
+
* the surviving process, and the resume call are stated here.
|
|
17
|
+
*/
|
|
18
|
+
export function restoredStateText(record: RunRecord): string {
|
|
19
|
+
const state = `Run ${record.id} is ${record.state} (started ${record.startedAt}).`;
|
|
20
|
+
const alive =
|
|
21
|
+
record.state === "orphaned" && record.process !== null
|
|
22
|
+
? ` Its process ${record.process.pid} is still running; stop it with yaag_stop.`
|
|
23
|
+
: "";
|
|
24
|
+
return `${state}${alive}${resumeHint(record)}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The frozen Result region of a restored Run, for the read-only `/yaag` view.
|
|
29
|
+
*
|
|
30
|
+
* Every restored Run states its persisted verdict first, whatever its outcome:
|
|
31
|
+
* the reader opened a snapshot of a record, which no live Run feeds any more,
|
|
32
|
+
* and the value or the error alone does not say so (ADR-0036).
|
|
33
|
+
*/
|
|
34
|
+
export function restoredViewResult(record: RunRecord): RunViewResult {
|
|
35
|
+
const state = restoredStateText(record);
|
|
36
|
+
const { outcome } = record;
|
|
37
|
+
if (outcome?.kind === "fulfilled")
|
|
38
|
+
return { kind: "fulfilled", result: joinText(state, outcome.result) };
|
|
39
|
+
if (outcome?.kind === "rejected")
|
|
40
|
+
return { kind: "failed", error: joinText(state, outcome.reason) };
|
|
41
|
+
return { kind: "failed", error: state };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function joinText(state: string, detail: string): string {
|
|
45
|
+
return detail.trim() === "" ? state : `${state}\n${detail}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Names the Checkpoint an unowned Run left behind (ADR-0031), and the call that
|
|
50
|
+
* resumes it. The Run republishes the Checkpoint at each Ask boundary, so it
|
|
51
|
+
* exists whenever the Run reached one, for a dead Run and for a still-running
|
|
52
|
+
* orphan alike.
|
|
53
|
+
*
|
|
54
|
+
* The hint names the Checkpoint path and never the stored program source: the
|
|
55
|
+
* path is enough, because yaag reads the source back from the Run record
|
|
56
|
+
* (ADR-0033). A whole source here would flood the transcript and repeat a
|
|
57
|
+
* private text the record already holds.
|
|
58
|
+
*/
|
|
59
|
+
function resumeHint(record: RunRecord): string {
|
|
60
|
+
const artifact = record.summary.artifact;
|
|
61
|
+
if (artifact === null || artifact === undefined) return "";
|
|
62
|
+
if (record.state !== "interrupted" && record.state !== "orphaned") return "";
|
|
63
|
+
// Each path becomes a JSON string literal: a path can hold a quote, a
|
|
64
|
+
// backslash, or a newline, and the hint must stay a call the model can copy.
|
|
65
|
+
const resume = JSON.stringify(artifact);
|
|
66
|
+
const { launch } = record;
|
|
67
|
+
switch (launch.kind) {
|
|
68
|
+
case "inline":
|
|
69
|
+
return ` Resume it with yaag_run({ resume: ${resume} }) and no file and no script; yaag reuses the program source it stored.`;
|
|
70
|
+
case "file":
|
|
71
|
+
return ` Resume it with yaag_run({ file: ${JSON.stringify(launch.file)}, resume: ${resume} }).`;
|
|
72
|
+
default: {
|
|
73
|
+
const never: never = launch;
|
|
74
|
+
return never;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one bound both Run listings share: the `/yaag` picker and the
|
|
3
|
+
* `yaag_status` overview must agree on which Runs exist (ADR-0036).
|
|
4
|
+
*/
|
|
5
|
+
import type { DatedRun, RegisteredRun } from "./run-registry.ts";
|
|
6
|
+
|
|
7
|
+
/** How many Runs the `/yaag` picker and the `yaag_status` overview list. */
|
|
8
|
+
export const RUN_LISTING_CAP = 5;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The newest `cap` Runs, newest first, plus every live Run whatever its age.
|
|
12
|
+
*
|
|
13
|
+
* A live Run is exempt from the cap: the session must always see and stop what
|
|
14
|
+
* it runs (ADR-0036). Runs that start at the same instant keep registration
|
|
15
|
+
* order.
|
|
16
|
+
*/
|
|
17
|
+
export function boundedRuns(
|
|
18
|
+
runs: readonly DatedRun[],
|
|
19
|
+
cap: number = RUN_LISTING_CAP,
|
|
20
|
+
): readonly RegisteredRun[] {
|
|
21
|
+
const newestFirst = [...runs.entries()].sort(compareNewestFirst).map(([, run]) => run);
|
|
22
|
+
return newestFirst
|
|
23
|
+
.filter((run, rank) => rank < cap || run.entry.state === "live")
|
|
24
|
+
.map((run) => run.entry);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function compareNewestFirst(
|
|
28
|
+
left: readonly [number, DatedRun],
|
|
29
|
+
right: readonly [number, DatedRun],
|
|
30
|
+
): number {
|
|
31
|
+
if (left[1].startedAt === right[1].startedAt) return left[0] - right[0];
|
|
32
|
+
return left[1].startedAt < right[1].startedAt ? 1 : -1;
|
|
33
|
+
}
|
package/src/run-picker.ts
CHANGED
|
@@ -3,21 +3,28 @@
|
|
|
3
3
|
* the list can be tested without a live UI.
|
|
4
4
|
*/
|
|
5
5
|
import type { RunSummary } from "@yaag/runtime";
|
|
6
|
-
import
|
|
6
|
+
import { boundedRuns } from "./run-listing.ts";
|
|
7
|
+
import type { DatedRun } from "./run-registry.ts";
|
|
7
8
|
|
|
8
9
|
/** One Run the picker can open: its id, latest Summary fold, and liveness. */
|
|
9
10
|
export interface PickableRun {
|
|
10
11
|
readonly id: string;
|
|
11
12
|
readonly summary: RunSummary;
|
|
12
13
|
readonly live: boolean;
|
|
14
|
+
/** A Run an earlier session started; the view of it is a frozen snapshot. */
|
|
15
|
+
readonly restored: boolean;
|
|
13
16
|
}
|
|
14
17
|
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
/**
|
|
19
|
+
* The Runs the picker lists: the bounded listing the `yaag_status` overview
|
|
20
|
+
* uses, so both surfaces agree on which Runs exist (ADR-0036).
|
|
21
|
+
*/
|
|
22
|
+
export function pickableRuns(runs: readonly DatedRun[]): readonly PickableRun[] {
|
|
23
|
+
return boundedRuns(runs).map((run) => ({
|
|
18
24
|
id: run.run.id,
|
|
19
25
|
summary: run.run.summary,
|
|
20
26
|
live: run.state === "live",
|
|
27
|
+
restored: run.state === "restored",
|
|
21
28
|
}));
|
|
22
29
|
}
|
|
23
30
|
|
package/src/run-record.ts
CHANGED
|
@@ -40,6 +40,12 @@ export type RunLaunch =
|
|
|
40
40
|
/** One Run as stored on disk, at `<runs dir>/<id>.json`. */
|
|
41
41
|
export interface RunRecord {
|
|
42
42
|
readonly id: string;
|
|
43
|
+
/**
|
|
44
|
+
* The pi session that started the Run. `null` for a record an older yaag
|
|
45
|
+
* wrote, or an unbound registry; such a record belongs to no session
|
|
46
|
+
* (ADR-0036).
|
|
47
|
+
*/
|
|
48
|
+
readonly sessionId: string | null;
|
|
43
49
|
readonly launch: RunLaunch;
|
|
44
50
|
readonly process: ProcessIdentity | null;
|
|
45
51
|
readonly startedAt: string;
|
|
@@ -81,6 +87,7 @@ export function parseRunRecord(value: unknown): RunRecord | null {
|
|
|
81
87
|
// field a consumer reads has the type this parser proved (rule 0001 §3).
|
|
82
88
|
return {
|
|
83
89
|
id: candidate["id"],
|
|
90
|
+
sessionId: typeof candidate["sessionId"] === "string" ? candidate["sessionId"] : null,
|
|
84
91
|
launch,
|
|
85
92
|
process: identity,
|
|
86
93
|
startedAt: candidate["startedAt"],
|
|
@@ -97,9 +104,11 @@ export function startedRecord(options: {
|
|
|
97
104
|
readonly launch: RunLaunch;
|
|
98
105
|
readonly process: ProcessIdentity | null;
|
|
99
106
|
readonly now: string;
|
|
107
|
+
readonly sessionId?: string | null;
|
|
100
108
|
}): RunRecord {
|
|
101
109
|
return {
|
|
102
110
|
id: options.id,
|
|
111
|
+
sessionId: options.sessionId ?? null,
|
|
103
112
|
launch: options.launch,
|
|
104
113
|
process: options.process,
|
|
105
114
|
startedAt: options.now,
|
package/src/run-registry.ts
CHANGED
|
@@ -44,6 +44,12 @@ export type RegisteredRun =
|
|
|
44
44
|
/** What the registry knows about an id. */
|
|
45
45
|
export type RunStatus = RegisteredRun | { readonly state: "unknown" };
|
|
46
46
|
|
|
47
|
+
/** A registered Run plus the start instant that orders a bounded listing. */
|
|
48
|
+
export interface DatedRun {
|
|
49
|
+
readonly startedAt: string;
|
|
50
|
+
readonly entry: RegisteredRun;
|
|
51
|
+
}
|
|
52
|
+
|
|
47
53
|
/** How often a Run's latest Summary is persisted while it runs. */
|
|
48
54
|
export const SUMMARY_WRITE_INTERVAL_MS = 2_000;
|
|
49
55
|
|
|
@@ -67,8 +73,11 @@ export class RunRegistry {
|
|
|
67
73
|
readonly #records = new Map<string, RunRecord>();
|
|
68
74
|
readonly #lastWrite = new Map<string, number>();
|
|
69
75
|
readonly #trailing = new Map<string, ReturnType<typeof setTimeout>>();
|
|
76
|
+
readonly #startedAt = new Map<string, string>();
|
|
70
77
|
readonly #store: RunStore | undefined;
|
|
71
78
|
readonly #now: () => Date;
|
|
79
|
+
#sessionId: string | null = null;
|
|
80
|
+
#restoredFor: string | null | undefined = undefined;
|
|
72
81
|
|
|
73
82
|
constructor(options: RunRegistryOptions = {}) {
|
|
74
83
|
this.#store = options.store;
|
|
@@ -90,25 +99,72 @@ export class RunRegistry {
|
|
|
90
99
|
return [...this.#runs.keys()];
|
|
91
100
|
}
|
|
92
101
|
|
|
102
|
+
/** Every addressable Run with its start instant, in registration order. */
|
|
103
|
+
get datedRuns(): readonly DatedRun[] {
|
|
104
|
+
return [...this.#runs.entries()].map(([id, entry]) => ({
|
|
105
|
+
startedAt: this.#startedAt.get(id) ?? "",
|
|
106
|
+
entry,
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
|
|
93
110
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
111
|
+
* Binds this Host Session's pi session id, settles the Runs an earlier
|
|
112
|
+
* session left `live`, and adopts the ones this session owns (ADR-0036).
|
|
113
|
+
*
|
|
114
|
+
* Idempotent per session id: a second `session_start` with the same id (a
|
|
115
|
+
* `/reload`) adopts nothing again. A fork carries a new id and therefore
|
|
116
|
+
* adopts nothing; the parent's Runs stay reachable through {@link recall}.
|
|
97
117
|
*/
|
|
98
|
-
async restore(): Promise<void> {
|
|
118
|
+
async restore(sessionId?: string): Promise<void> {
|
|
119
|
+
const bound = sessionId ?? null;
|
|
120
|
+
if (this.#restoredFor === bound) return;
|
|
121
|
+
this.#sessionId = bound;
|
|
122
|
+
this.#restoredFor = bound;
|
|
99
123
|
if (this.#store === undefined) return;
|
|
100
124
|
const records = await restoreRecords({ store: this.#store, now: this.#now });
|
|
101
125
|
for (const record of records) {
|
|
102
|
-
|
|
126
|
+
// A Run of this process is never shadowed by its own persisted record.
|
|
127
|
+
if (this.#runs.has(record.id)) continue;
|
|
128
|
+
if (!this.#adoptable(record)) continue;
|
|
103
129
|
this.#runs.set(record.id, { state: "restored", run: restored(record) });
|
|
130
|
+
this.#startedAt.set(record.id, record.startedAt);
|
|
104
131
|
}
|
|
105
132
|
}
|
|
106
133
|
|
|
134
|
+
/** Persisted Runs this overview does not list; addressable by id only. */
|
|
135
|
+
async unlistedRunCount(listed: readonly string[]): Promise<number> {
|
|
136
|
+
if (this.#store === undefined) return 0;
|
|
137
|
+
const shown = new Set(listed);
|
|
138
|
+
return (await this.#store.load()).filter((record) => !shown.has(record.id)).length;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Persisted finished Runs that another pi session owns. */
|
|
142
|
+
async finishedElsewhereCount(): Promise<number> {
|
|
143
|
+
if (this.#store === undefined) return 0;
|
|
144
|
+
return (await this.#store.load()).filter(
|
|
145
|
+
(record) =>
|
|
146
|
+
record.state === "finished" &&
|
|
147
|
+
(this.#sessionId === null || record.sessionId !== this.#sessionId),
|
|
148
|
+
).length;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* `restoreRecords` already settled every stale `live` record, so this check
|
|
153
|
+
* can never adopt a Run as live (ADR-0036).
|
|
154
|
+
*/
|
|
155
|
+
#adoptable(record: RunRecord): boolean {
|
|
156
|
+
if (record.state === "orphaned" || record.state === "interrupted") return true;
|
|
157
|
+
return this.#sessionId !== null && record.sessionId === this.#sessionId;
|
|
158
|
+
}
|
|
159
|
+
|
|
107
160
|
/** Registers a newly-started Run and synchronously observes its settlement. */
|
|
108
161
|
add(run: LiveRun, launch?: RunLaunch, identity?: ProcessIdentity | null): void {
|
|
162
|
+
const startedAt = this.#now().toISOString();
|
|
163
|
+
this.#startedAt.set(run.id, startedAt);
|
|
109
164
|
this.#live.set(run.id, run);
|
|
110
165
|
this.#runs.set(run.id, { state: "live", run });
|
|
111
|
-
if (launch !== undefined)
|
|
166
|
+
if (launch !== undefined)
|
|
167
|
+
this.#persistStart({ id: run.id, launch, process: identity ?? null, now: startedAt });
|
|
112
168
|
void run.outcome.then(
|
|
113
169
|
(outcome) => this.finish(run.id, outcome),
|
|
114
170
|
(reason: unknown) => this.reject(run.id, reason),
|
|
@@ -203,13 +259,16 @@ export class RunRegistry {
|
|
|
203
259
|
this.#persistEnd(id, summary, outcome);
|
|
204
260
|
}
|
|
205
261
|
|
|
206
|
-
#persistStart(
|
|
262
|
+
#persistStart(options: {
|
|
263
|
+
readonly id: string;
|
|
264
|
+
readonly launch: RunLaunch;
|
|
265
|
+
readonly process: ProcessIdentity | null;
|
|
266
|
+
readonly now: string;
|
|
267
|
+
}): void {
|
|
207
268
|
if (this.#store === undefined) return;
|
|
208
269
|
const store = this.#store;
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
startedRecord({ id, launch, process: identity, now: this.#now().toISOString() }),
|
|
212
|
-
);
|
|
270
|
+
const id = options.id;
|
|
271
|
+
this.#records.set(id, startedRecord({ ...options, sessionId: this.#sessionId }));
|
|
213
272
|
this.#persist(id);
|
|
214
273
|
void store.prune();
|
|
215
274
|
}
|
package/src/status-tool.ts
CHANGED
|
@@ -2,7 +2,8 @@ import type { AgentToolResult, ToolDefinition } from "@earendil-works/pi-coding-
|
|
|
2
2
|
import type { RunSummary } from "@yaag/runtime";
|
|
3
3
|
import { renderSnapshot, TreeState } from "@yaag/tui";
|
|
4
4
|
import { Type } from "typebox";
|
|
5
|
-
import
|
|
5
|
+
import { restoredStateText } from "./restored-run-text.ts";
|
|
6
|
+
import { boundedRuns, RUN_LISTING_CAP } from "./run-listing.ts";
|
|
6
7
|
import type { RegisteredRun, RestoredRun, RunRegistry } from "./run-registry.ts";
|
|
7
8
|
import { toUsage } from "./usage.ts";
|
|
8
9
|
|
|
@@ -33,18 +34,22 @@ const DESCRIPTION = [
|
|
|
33
34
|
"resolves: a Run whose process died is reported as interrupted, and a Run whose",
|
|
34
35
|
"process is still alive is reported as orphaned.",
|
|
35
36
|
"",
|
|
36
|
-
"Without an id, lists this session
|
|
37
|
-
"
|
|
37
|
+
"Without an id, this tool lists the Runs of this session, and the orphaned or",
|
|
38
|
+
"interrupted Runs another session left. The list is bounded: it holds every",
|
|
39
|
+
`live Run, and then the newest Runs up to a total of ${RUN_LISTING_CAP}. An older Run stays`,
|
|
40
|
+
"on disk, and the last line of the list counts the Runs on disk. Give an id to",
|
|
41
|
+
"see one of them.",
|
|
38
42
|
"",
|
|
39
43
|
"An interrupted inline Run resumes with `resume` alone: yaag reads the program",
|
|
40
44
|
"source from its own Run record.",
|
|
41
45
|
].join("\n");
|
|
42
46
|
|
|
43
47
|
/**
|
|
44
|
-
* The `yaag_status` tool renders
|
|
48
|
+
* The `yaag_status` tool renders Run snapshots for the model.
|
|
45
49
|
*
|
|
46
|
-
*
|
|
47
|
-
* queryable
|
|
50
|
+
* An id resolves through {@link RunRegistry.recall}, so a Run of any session is
|
|
51
|
+
* queryable while its record is on disk; only an id no record holds throws.
|
|
52
|
+
* The tool never restarts or controls a Run.
|
|
48
53
|
*/
|
|
49
54
|
export function createStatusTool(
|
|
50
55
|
registry: RunRegistry,
|
|
@@ -58,7 +63,7 @@ export function createStatusTool(
|
|
|
58
63
|
description: DESCRIPTION,
|
|
59
64
|
parameters,
|
|
60
65
|
async execute(_id, params) {
|
|
61
|
-
if (params.id === undefined) return overview(registry
|
|
66
|
+
if (params.id === undefined) return await overview(registry, now, width);
|
|
62
67
|
const found = await registry.recall(params.id);
|
|
63
68
|
if (found.state === "unknown") throw unknownId(params.id, registry.knownIds);
|
|
64
69
|
const details = detailsFor(found);
|
|
@@ -71,25 +76,40 @@ export function createStatusTool(
|
|
|
71
76
|
};
|
|
72
77
|
}
|
|
73
78
|
|
|
74
|
-
|
|
75
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Lists the Runs of this session, bounded by {@link RUN_LISTING_CAP}.
|
|
81
|
+
*
|
|
82
|
+
* The disk counts are read on each call: the store holds dozens of small files
|
|
83
|
+
* and a stale count would name Runs that no longer exist (ADR-0036).
|
|
84
|
+
*/
|
|
85
|
+
async function overview(
|
|
86
|
+
registry: RunRegistry,
|
|
76
87
|
now: () => number,
|
|
77
88
|
width: number,
|
|
78
|
-
): AgentToolResult<StatusDetails
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
): Promise<AgentToolResult<StatusDetails>> {
|
|
90
|
+
const shown = boundedRuns(registry.datedRuns);
|
|
91
|
+
if (shown.length === 0) return text(emptyOverview(await registry.finishedElsewhereCount()));
|
|
92
|
+
const lines = shown.map((run) => snapshot(detailsFor(run), now, width, true));
|
|
93
|
+
const unlisted = await registry.unlistedRunCount(shown.map((run) => run.run.id));
|
|
94
|
+
if (unlisted > 0) lines.push(overflowLine(shown.length, unlisted));
|
|
95
|
+
return text(lines.join("\n"));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function text(value: string): AgentToolResult<StatusDetails> {
|
|
99
|
+
return { content: [{ type: "text", text: value }], details: { summary: undefined } };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The confirmed empty-overview copy of the specification, with its count. */
|
|
103
|
+
function emptyOverview(elsewhere: number): string {
|
|
104
|
+
if (elsewhere === 0) return "No Runs in this session.";
|
|
105
|
+
const runs = elsewhere === 1 ? "Run" : "Runs";
|
|
106
|
+
const verb = elsewhere === 1 ? "is" : "are";
|
|
107
|
+
return `No Runs in this session. ${elsewhere} finished ${runs} from other sessions ${verb} on disk; query one with yaag_status({ id }).`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The confirmed overflow copy of the specification, with its two counts. */
|
|
111
|
+
function overflowLine(shown: number, unlisted: number): string {
|
|
112
|
+
return `${shown} shown; ${unlisted} more on disk \u2014 query one with yaag_status({ id })`;
|
|
93
113
|
}
|
|
94
114
|
|
|
95
115
|
function detailsFor(run: RegisteredRun): SnapshotDetails {
|
|
@@ -106,51 +126,19 @@ function detailsFor(run: RegisteredRun): SnapshotDetails {
|
|
|
106
126
|
}
|
|
107
127
|
|
|
108
128
|
/**
|
|
109
|
-
* A Run
|
|
110
|
-
*
|
|
111
|
-
* the
|
|
129
|
+
* A Run read back from its record: this session may own it again, but nothing
|
|
130
|
+
* is attached to it. Its persisted state is stated in the Result region,
|
|
131
|
+
* because the folded Summary alone cannot say that the snapshot is final and
|
|
132
|
+
* that no process feeds it (ticket 02, ADR-0036).
|
|
112
133
|
*/
|
|
113
134
|
function restoredDetails(run: RestoredRun): SnapshotDetails {
|
|
114
135
|
const { record } = run;
|
|
115
|
-
const state = `Run ${record.id} is ${record.state} (started ${record.startedAt}).`;
|
|
116
|
-
const alive =
|
|
117
|
-
record.state === "orphaned" && record.process !== null
|
|
118
|
-
? ` Its process ${record.process.pid} is still running; stop it with yaag_stop.`
|
|
119
|
-
: "";
|
|
120
|
-
const detail = `${alive}${resumeHint(record)}`;
|
|
121
136
|
const result = record.outcome?.kind === "fulfilled" ? `\n${record.outcome.result}` : "";
|
|
122
|
-
return {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
* resumes it. The Run republishes the Checkpoint at each Ask boundary, so it
|
|
128
|
-
* exists whenever the Run reached one, for a dead Run and for a still-running
|
|
129
|
-
* orphan alike.
|
|
130
|
-
*
|
|
131
|
-
* The hint names the Checkpoint path and never the stored program source: the
|
|
132
|
-
* path is enough, because yaag reads the source back from the Run record
|
|
133
|
-
* (ADR-0033). A whole source here would flood the transcript and repeat a
|
|
134
|
-
* private text the record already holds.
|
|
135
|
-
*/
|
|
136
|
-
function resumeHint(record: RunRecord): string {
|
|
137
|
-
const artifact = record.summary.artifact;
|
|
138
|
-
if (artifact === null || artifact === undefined) return "";
|
|
139
|
-
if (record.state !== "interrupted" && record.state !== "orphaned") return "";
|
|
140
|
-
// Each path becomes a JSON string literal: a path can hold a quote, a
|
|
141
|
-
// backslash, or a newline, and the hint must stay a call the model can copy.
|
|
142
|
-
const resume = JSON.stringify(artifact);
|
|
143
|
-
const { launch } = record;
|
|
144
|
-
switch (launch.kind) {
|
|
145
|
-
case "inline":
|
|
146
|
-
return ` Resume it with yaag_run({ resume: ${resume} }) and no file and no script; yaag reuses the program source it stored.`;
|
|
147
|
-
case "file":
|
|
148
|
-
return ` Resume it with yaag_run({ file: ${JSON.stringify(launch.file)}, resume: ${resume} }).`;
|
|
149
|
-
default: {
|
|
150
|
-
const never: never = launch;
|
|
151
|
-
return never;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
137
|
+
return {
|
|
138
|
+
id: record.id,
|
|
139
|
+
summary: run.summary,
|
|
140
|
+
result: `${restoredStateText(record)}${result}`,
|
|
141
|
+
};
|
|
154
142
|
}
|
|
155
143
|
|
|
156
144
|
function snapshot(
|
package/src/stop-tool.ts
CHANGED
|
@@ -67,8 +67,8 @@ function agentStateReport(agent: RunSummary["agents"][string]): string {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
|
-
* Ends a Run
|
|
71
|
-
* belong to a dead Host Session — so the process group is killed outright
|
|
70
|
+
* Ends a Run no process is attached to. There is no reap ladder to run — its
|
|
71
|
+
* pipes belong to a dead Host Session — so the process group is killed outright
|
|
72
72
|
* (ADR-0008). The pid is re-probed first, because a record can name a pid that
|
|
73
73
|
* a different process has since inherited (ticket 02).
|
|
74
74
|
*/
|
package/src/yaag-command.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
RegisteredCommand,
|
|
15
15
|
} from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import { createRunTreeView, type RunTreeViewHost, type RunViewResult, TreeState } from "@yaag/tui";
|
|
17
|
+
import { restoredViewResult } from "./restored-run-text.ts";
|
|
17
18
|
import { type PickableRun, pickableRuns, runFromLabel, runPickerLabel } from "./run-picker.ts";
|
|
18
19
|
import type { RunRegistry, RunSettlement } from "./run-registry.ts";
|
|
19
20
|
import { observedSettlement, viewResult } from "./run-settlement.ts";
|
|
@@ -53,9 +54,9 @@ export function createYaagCommand(options: YaagCommandOptions): CommandOptions {
|
|
|
53
54
|
* not the TUI, or when the reader cancels the picker.
|
|
54
55
|
*/
|
|
55
56
|
export async function runYaagCommand(ctx: YaagContext, options: YaagCommandOptions): Promise<void> {
|
|
56
|
-
const runs = pickableRuns(options.registry.
|
|
57
|
+
const runs = pickableRuns(options.registry.datedRuns);
|
|
57
58
|
if (runs.length === 0) {
|
|
58
|
-
ctx.ui.notify("yaag: no Runs
|
|
59
|
+
ctx.ui.notify("yaag: no Runs in this session.", "info");
|
|
59
60
|
return;
|
|
60
61
|
}
|
|
61
62
|
if (ctx.mode !== "tui") {
|
|
@@ -87,8 +88,13 @@ async function openRun(
|
|
|
87
88
|
run: PickableRun,
|
|
88
89
|
options: YaagCommandOptions,
|
|
89
90
|
): Promise<void> {
|
|
90
|
-
const state = options.store.get(run.id) ?? TreeState.fromSummary(run.summary);
|
|
91
91
|
const found = options.registry.lookup(run.id);
|
|
92
|
+
// A Run of an earlier session has no projection here, so its view is built
|
|
93
|
+
// from the persisted Summary alone (ADR-0036).
|
|
94
|
+
const state =
|
|
95
|
+
found.state === "restored"
|
|
96
|
+
? TreeState.fromSummary(run.summary)
|
|
97
|
+
: (options.store.get(run.id) ?? TreeState.fromSummary(run.summary));
|
|
92
98
|
let unsubscribe: (() => void) | undefined;
|
|
93
99
|
// The observer below outlives no more than the `ctx.ui.custom()` call: once
|
|
94
100
|
// the view exits or the UI call rejects, a later settlement must touch
|
|
@@ -98,6 +104,8 @@ async function openRun(
|
|
|
98
104
|
await ctx.ui.custom<void>((tui, _theme, keybindings, done) => {
|
|
99
105
|
const host: RunTreeViewHost = {
|
|
100
106
|
...createRunTreeHost({ ui: ctx.ui, tui, keybindings, state }),
|
|
107
|
+
// A restored orphan is never re-attached, so there is nothing to stop
|
|
108
|
+
// from here; `yaag_stop` ends it (ADR-0036).
|
|
101
109
|
stop: () => {
|
|
102
110
|
if (found.state === "live") found.run.stop();
|
|
103
111
|
},
|
|
@@ -105,8 +113,13 @@ async function openRun(
|
|
|
105
113
|
// hand over, because a Run of this session is already addressable.
|
|
106
114
|
done: () => done(),
|
|
107
115
|
};
|
|
108
|
-
const view = createRunTreeView({
|
|
109
|
-
|
|
116
|
+
const view = createRunTreeView({
|
|
117
|
+
state,
|
|
118
|
+
host,
|
|
119
|
+
label: found.state === "restored" ? `${run.id} (restored)` : run.id,
|
|
120
|
+
});
|
|
121
|
+
if (found.state === "restored") view.settle(restoredViewResult(found.run.record));
|
|
122
|
+
else if (found.state === "finished")
|
|
110
123
|
settle(state, view, found.run.outcome, options.registry, run.id);
|
|
111
124
|
else if (found.state === "live") {
|
|
112
125
|
unsubscribe = options.store.subscribe((id) => {
|