@yaag/extension 0.6.2 → 0.8.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/README.md +23 -0
- package/docs/authoring.md +115 -0
- package/docs/cli.md +102 -0
- package/docs/examples/01-minimal.ts +14 -0
- package/docs/examples/02-args.ts +22 -0
- package/docs/examples/03-fan-out.ts +27 -0
- package/docs/examples/04-controlled-ask.ts +33 -0
- package/docs/examples/05-record-resume.ts +18 -0
- package/docs/examples.md +102 -0
- package/docs/getting-started.md +53 -0
- package/docs/troubleshooting.md +132 -0
- package/package.json +5 -4
- package/src/docs/doc-embed.ts +127 -0
- package/src/docs/docs-root.ts +32 -0
- package/src/docs/index.ts +13 -0
- package/src/process/run-argv.ts +6 -0
- package/src/process/spawn-run.ts +6 -0
- package/src/process/status.ts +4 -1
- package/src/record/index.ts +1 -1
- package/src/record/resume-source.ts +56 -11
- package/src/record/run-record.ts +6 -0
- package/src/record/run-registry.ts +11 -1
- package/src/record/run-summary-parse.ts +46 -0
- package/src/testing/run-call-render.ts +2 -0
- package/src/tool/describe-tool.ts +6 -0
- package/src/tool/run-tool.ts +38 -4
- package/src/tool/system-prompt-append.ts +4 -1
- package/src/view/restored-run-text.ts +5 -2
- package/src/view/run-details.ts +33 -3
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yaag/extension",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
7
|
"files": [
|
|
8
8
|
"src",
|
|
9
|
+
"docs",
|
|
9
10
|
"!src/**/*.test.ts"
|
|
10
11
|
],
|
|
11
12
|
"type": "module",
|
|
@@ -24,9 +25,9 @@
|
|
|
24
25
|
},
|
|
25
26
|
"dependencies": {
|
|
26
27
|
"@earendil-works/pi-tui": "^0.84.0",
|
|
27
|
-
"@yaag/cli": "0.
|
|
28
|
-
"@yaag/runtime": "0.
|
|
29
|
-
"@yaag/tui": "0.
|
|
28
|
+
"@yaag/cli": "0.8.0",
|
|
29
|
+
"@yaag/runtime": "0.8.0",
|
|
30
|
+
"@yaag/tui": "0.8.0",
|
|
30
31
|
"nanoid": "^6.0.1"
|
|
31
32
|
},
|
|
32
33
|
"peerDependencies": {
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The drift guard behind every doc page: a fenced block must be a verbatim
|
|
3
|
+
* region of a real source file, and every block must name that file with an
|
|
4
|
+
* `<!-- embed: <source> -->` marker.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { createRequire } from "node:module";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { docPath } from "./docs-root.ts";
|
|
11
|
+
|
|
12
|
+
/** One fenced block of a page, with the marker that precedes it. */
|
|
13
|
+
export interface DocBlock {
|
|
14
|
+
/** The page the block came from, relative to the docs root. */
|
|
15
|
+
readonly page: string;
|
|
16
|
+
/** The marker payload, e.g. `docs/examples/01-minimal.ts` or `@yaag/cli/src/argv.ts#USAGE`. */
|
|
17
|
+
readonly source: string | null;
|
|
18
|
+
/** The block body, without the fences and without a trailing newline. */
|
|
19
|
+
readonly body: string;
|
|
20
|
+
/** 1-based line of the opening fence, for a self-explaining failure. */
|
|
21
|
+
readonly line: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const MARKER = /^<!--\s*embed:\s*(.+?)\s*-->$/;
|
|
25
|
+
|
|
26
|
+
/** Reads every fenced block of a page, together with its preceding marker. */
|
|
27
|
+
export function readBlocks(page: string): DocBlock[] {
|
|
28
|
+
const lines = readFileSync(docPath(page), "utf8").split("\n");
|
|
29
|
+
const blocks: DocBlock[] = [];
|
|
30
|
+
let marker: string | null = null;
|
|
31
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
32
|
+
const line = lines[index] ?? "";
|
|
33
|
+
const found = MARKER.exec(line.trim());
|
|
34
|
+
if (found?.[1] !== undefined) {
|
|
35
|
+
marker = found[1];
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (!line.startsWith("```")) continue;
|
|
39
|
+
const body: string[] = [];
|
|
40
|
+
index += 1;
|
|
41
|
+
const open = index;
|
|
42
|
+
while (index < lines.length && !(lines[index] ?? "").startsWith("```")) {
|
|
43
|
+
body.push(lines[index] ?? "");
|
|
44
|
+
index += 1;
|
|
45
|
+
}
|
|
46
|
+
blocks.push({ page, source: marker, body: body.join("\n"), line: open });
|
|
47
|
+
marker = null;
|
|
48
|
+
}
|
|
49
|
+
return blocks;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Resolves a marker payload to the text it must match. */
|
|
53
|
+
export function resolveSource(spec: string): string {
|
|
54
|
+
const [path, constant] = spec.split("#");
|
|
55
|
+
const text = readFileSync(resolvePath(path ?? ""), "utf8");
|
|
56
|
+
return constant === undefined ? text : exportedString(text, constant, spec);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resolvePath(path: string): string {
|
|
60
|
+
if (path.startsWith("docs/")) return docPath(path.slice("docs/".length));
|
|
61
|
+
if (path.startsWith("src/")) return fileURLToPath(new URL(`../../${path}`, import.meta.url));
|
|
62
|
+
const require = createRequire(import.meta.url);
|
|
63
|
+
const packaged = /^(@yaag\/[a-z]+)\/(src\/.+)$/.exec(path);
|
|
64
|
+
if (packaged?.[1] === "@yaag/runtime" && packaged[2] !== undefined) {
|
|
65
|
+
// @yaag/runtime publishes one entry point, so reach its files through it.
|
|
66
|
+
const entry = require.resolve("@yaag/runtime");
|
|
67
|
+
return join(entry.slice(0, entry.lastIndexOf("/src/")), packaged[2]);
|
|
68
|
+
}
|
|
69
|
+
return require.resolve(path);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Reads the body of `export const NAME = \`…\`;` out of a source file. */
|
|
73
|
+
function exportedString(text: string, name: string, spec: string): string {
|
|
74
|
+
const start = text.indexOf(`export const ${name} = \``);
|
|
75
|
+
if (start === -1) throw new Error(`${spec}: no exported template string named ${name}`);
|
|
76
|
+
const from = text.indexOf("`", start) + 1;
|
|
77
|
+
const to = text.indexOf("`;", from);
|
|
78
|
+
if (to === -1) throw new Error(`${spec}: ${name} is not terminated`);
|
|
79
|
+
return text.slice(from, to);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** One quoted message of a page, with the source it is guarded against. */
|
|
83
|
+
export interface DocQuote {
|
|
84
|
+
readonly source: string;
|
|
85
|
+
readonly text: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const QUOTE_MARKER = /^<!--\s*quote:\s*(.+?)\s*-->$/;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Reads every `> quoted` line of a markdown text with the
|
|
92
|
+
* `<!-- quote: <source> -->` marker that precedes it.
|
|
93
|
+
*
|
|
94
|
+
* One marker guards one quote: the marker is consumed, so a quote can never be
|
|
95
|
+
* compared with the file of the entry above it. Throws when a quote line has no
|
|
96
|
+
* marker of its own.
|
|
97
|
+
*/
|
|
98
|
+
export function readQuotes(markdown: string): DocQuote[] {
|
|
99
|
+
const found: DocQuote[] = [];
|
|
100
|
+
let source: string | null = null;
|
|
101
|
+
for (const line of markdown.split("\n")) {
|
|
102
|
+
const marker = QUOTE_MARKER.exec(line.trim());
|
|
103
|
+
if (marker?.[1] !== undefined) {
|
|
104
|
+
source = marker[1];
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (!line.startsWith("> ")) continue;
|
|
108
|
+
if (source === null) throw new Error(`quoted message with no marker: ${line}`);
|
|
109
|
+
found.push({ source, text: line.slice(2).trim() });
|
|
110
|
+
source = null;
|
|
111
|
+
}
|
|
112
|
+
return found;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Fails unless the block body appears line for line inside its source. */
|
|
116
|
+
export function assertVerbatim(block: DocBlock): void {
|
|
117
|
+
if (block.source === null) throw new Error(describe(block, "carries no embed marker"));
|
|
118
|
+
const source = resolveSource(block.source);
|
|
119
|
+
const body = block.body.replace(/\s+$/, "");
|
|
120
|
+
if (source.replace(/\r/g, "").includes(body)) return;
|
|
121
|
+
throw new Error(describe(block, `is not verbatim in ${block.source}`));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function describe(block: DocBlock, what: string): string {
|
|
125
|
+
const first = block.body.split("\n")[0] ?? "";
|
|
126
|
+
return `${block.page}:${block.line} (\`${first}\`) ${what}`;
|
|
127
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shipped docs folder. The repo path is the tarball path, so one constant
|
|
3
|
+
* serves the system prompt, the error messages, and the drift guards.
|
|
4
|
+
*/
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
/** Absolute path of the shipped `docs/` folder, with no trailing separator. */
|
|
9
|
+
export const DOCS_ROOT = fileURLToPath(new URL("../../docs", import.meta.url));
|
|
10
|
+
|
|
11
|
+
/** The five shipped pages, in reading order. */
|
|
12
|
+
export const DOC_PAGES = [
|
|
13
|
+
"getting-started.md",
|
|
14
|
+
"authoring.md",
|
|
15
|
+
"examples.md",
|
|
16
|
+
"cli.md",
|
|
17
|
+
"troubleshooting.md",
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
/** The five shipped example programs, in reading order. */
|
|
21
|
+
export const EXAMPLE_FILES = [
|
|
22
|
+
"01-minimal.ts",
|
|
23
|
+
"02-args.ts",
|
|
24
|
+
"03-fan-out.ts",
|
|
25
|
+
"04-controlled-ask.ts",
|
|
26
|
+
"05-record-resume.ts",
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
29
|
+
/** Absolute path of one shipped doc file, named relative to the docs root. */
|
|
30
|
+
export function docPath(name: string): string {
|
|
31
|
+
return join(DOCS_ROOT, name);
|
|
32
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public surface of the `docs/` module: where the shipped docs are, and the
|
|
3
|
+
* drift guard that keeps them true.
|
|
4
|
+
*/
|
|
5
|
+
export {
|
|
6
|
+
assertVerbatim,
|
|
7
|
+
type DocBlock,
|
|
8
|
+
type DocQuote,
|
|
9
|
+
readBlocks,
|
|
10
|
+
readQuotes,
|
|
11
|
+
resolveSource,
|
|
12
|
+
} from "./doc-embed.ts";
|
|
13
|
+
export { DOC_PAGES, DOCS_ROOT, docPath, EXAMPLE_FILES } from "./docs-root.ts";
|
package/src/process/run-argv.ts
CHANGED
|
@@ -15,6 +15,10 @@ export interface RunArgvOptions {
|
|
|
15
15
|
readonly record?: string;
|
|
16
16
|
/** Cassette path to resume from, forwarded as `--resume` (ADR-0014). */
|
|
17
17
|
readonly resume?: string;
|
|
18
|
+
/** Run Config path, forwarded as `--config` (ADR-0040). */
|
|
19
|
+
readonly config?: string;
|
|
20
|
+
/** Suppresses the discovered config layers, forwarded as `--no-config` (ADR-0040). */
|
|
21
|
+
readonly noConfig?: boolean;
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
/**
|
|
@@ -35,5 +39,7 @@ export function runArgv(options: RunArgvOptions): readonly string[] {
|
|
|
35
39
|
...(options.args === undefined ? [] : ["--args", options.args]),
|
|
36
40
|
...(options.record === undefined ? [] : ["--record", options.record]),
|
|
37
41
|
...(options.resume === undefined ? [] : ["--resume", options.resume]),
|
|
42
|
+
...(options.config === undefined ? [] : ["--config", options.config]),
|
|
43
|
+
...(options.noConfig === true ? ["--no-config"] : []),
|
|
38
44
|
];
|
|
39
45
|
}
|
package/src/process/spawn-run.ts
CHANGED
|
@@ -26,6 +26,10 @@ export interface StartRunOptions {
|
|
|
26
26
|
readonly record?: string;
|
|
27
27
|
/** Cassette path to resume from, forwarded as `--resume` (ADR-0014). */
|
|
28
28
|
readonly resume?: string;
|
|
29
|
+
/** Run Config path, forwarded as `--config` (ADR-0040). */
|
|
30
|
+
readonly config?: string;
|
|
31
|
+
/** Suppresses the discovered config layers, forwarded as `--no-config` (ADR-0040). */
|
|
32
|
+
readonly noConfig?: boolean;
|
|
29
33
|
/**
|
|
30
34
|
* Called after each event is folded. `sequence` identifies this fd 3 stream
|
|
31
35
|
* occurrence, not an event value or timestamp; it makes renderer observation
|
|
@@ -60,6 +64,8 @@ export function startRun(options: StartRunOptions): RunHandle {
|
|
|
60
64
|
...(options.args === undefined ? {} : { args: options.args }),
|
|
61
65
|
...(options.record === undefined ? {} : { record: options.record }),
|
|
62
66
|
...(options.resume === undefined ? {} : { resume: options.resume }),
|
|
67
|
+
...(options.config === undefined ? {} : { config: options.config }),
|
|
68
|
+
...(options.noConfig === true ? { noConfig: true } : {}),
|
|
63
69
|
});
|
|
64
70
|
const child = startCliChild({
|
|
65
71
|
bun: options.bun,
|
package/src/process/status.ts
CHANGED
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
* The one message the extension has to say about itself: either the bridge to
|
|
3
3
|
* the Bun CLI is in place, or exactly what is missing and how to install it.
|
|
4
4
|
*/
|
|
5
|
+
import { docPath } from "../docs/index.ts";
|
|
6
|
+
|
|
5
7
|
export function statusReport(bun: string | null, cli: string): string {
|
|
6
8
|
if (bun === null) {
|
|
7
9
|
return (
|
|
8
10
|
"yaag needs Bun and could not find it on PATH or in ~/.bun/bin. " +
|
|
9
|
-
"Install it with: curl -fsSL https://bun.sh/install | bash"
|
|
11
|
+
"Install it with: curl -fsSL https://bun.sh/install | bash. " +
|
|
12
|
+
`See ${docPath("troubleshooting.md#bun-is-missing")}`
|
|
10
13
|
);
|
|
11
14
|
}
|
|
12
15
|
return `yaag ready — bun: ${bun}, cli: ${cli}`;
|
package/src/record/index.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Public surface of the `record/` module: the Run record registry, store, restore, and resume.
|
|
3
3
|
* Files inside this directory import each other directly.
|
|
4
4
|
*/
|
|
5
|
-
export { resolveProgramParams } from "./resume-source.ts";
|
|
5
|
+
export { resolveLaunchConfig, resolveProgramParams } from "./resume-source.ts";
|
|
6
6
|
export { mintRunId } from "./run-id.ts";
|
|
7
7
|
export {
|
|
8
8
|
confirmProgramTarget,
|
|
@@ -13,10 +13,12 @@ import type { RunRecord } from "./run-record.ts";
|
|
|
13
13
|
* source it sees is byte-identical to the recorded one.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
/** The stored Runs a resume
|
|
17
|
-
export interface
|
|
16
|
+
/** The stored Runs a resume reads back; a structural type, so a test needs no store. */
|
|
17
|
+
export interface ResumeRecordLookup {
|
|
18
18
|
/** The Inline Program source whose Run published `artifact`, or null. */
|
|
19
19
|
inlineSourceFor(artifact: string): Promise<string | null>;
|
|
20
|
+
/** The Run Config path of the Run that published `artifact`, or null. */
|
|
21
|
+
resumeConfigFor(artifact: string): Promise<string | null>;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
/**
|
|
@@ -32,18 +34,36 @@ export function pickInlineResumeSource(
|
|
|
32
34
|
): string | null {
|
|
33
35
|
let found: string | null = null;
|
|
34
36
|
for (const record of records) {
|
|
35
|
-
|
|
36
|
-
if (
|
|
37
|
-
const { script, record: recorded } = launch;
|
|
38
|
-
const published = record.summary.artifact;
|
|
39
|
-
const matches =
|
|
40
|
-
(published !== null && published !== undefined && resolve(published) === artifact) ||
|
|
41
|
-
(recorded !== undefined && resolve(recorded) === artifact);
|
|
42
|
-
if (matches) found = script;
|
|
37
|
+
if (record.launch.kind !== "inline") continue;
|
|
38
|
+
if (publishes(record, artifact)) found = record.launch.script;
|
|
43
39
|
}
|
|
44
40
|
return found;
|
|
45
41
|
}
|
|
46
42
|
|
|
43
|
+
/**
|
|
44
|
+
* The stored Run Config path of the Run that published `artifact`, or null.
|
|
45
|
+
*
|
|
46
|
+
* Pure, and blind to the launch kind: a file Run and an inline Run both carry
|
|
47
|
+
* their Run Config path. The newest match wins, as above.
|
|
48
|
+
*/
|
|
49
|
+
export function pickResumeConfig(records: readonly RunRecord[], artifact: string): string | null {
|
|
50
|
+
let found: string | null = null;
|
|
51
|
+
for (const record of records) {
|
|
52
|
+
if (publishes(record, artifact)) found = record.launch.config ?? null;
|
|
53
|
+
}
|
|
54
|
+
return found;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Whether the Run of this record published, or wrote, the given Checkpoint. */
|
|
58
|
+
function publishes(record: RunRecord, artifact: string): boolean {
|
|
59
|
+
const published = record.summary.artifact;
|
|
60
|
+
const recorded = record.launch.record;
|
|
61
|
+
return (
|
|
62
|
+
(published !== null && published !== undefined && resolve(published) === artifact) ||
|
|
63
|
+
(recorded !== undefined && resolve(recorded) === artifact)
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
47
67
|
/** What the tool says when no record holds the source of the given Checkpoint. */
|
|
48
68
|
export function noStoredSourceMessage(artifact: string): string {
|
|
49
69
|
return [
|
|
@@ -66,7 +86,7 @@ export function noStoredSourceMessage(artifact: string): string {
|
|
|
66
86
|
*/
|
|
67
87
|
export async function resolveProgramParams(
|
|
68
88
|
params: ProgramParams,
|
|
69
|
-
lookup:
|
|
89
|
+
lookup: ResumeRecordLookup,
|
|
70
90
|
): Promise<ProgramParams> {
|
|
71
91
|
if (params.file !== undefined || params.script !== undefined) return params;
|
|
72
92
|
if (params.resume === undefined) return params;
|
|
@@ -75,3 +95,28 @@ export async function resolveProgramParams(
|
|
|
75
95
|
if (source === null) throw new Error(noStoredSourceMessage(artifact));
|
|
76
96
|
return { ...params, script: source };
|
|
77
97
|
}
|
|
98
|
+
|
|
99
|
+
/** The launch parameters a resume needs, with the stored Run Config supplied. */
|
|
100
|
+
export interface LaunchParams {
|
|
101
|
+
readonly resume?: string;
|
|
102
|
+
readonly config?: string;
|
|
103
|
+
readonly noConfig?: boolean;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The Run Config path a resume inherits from its Run record.
|
|
108
|
+
*
|
|
109
|
+
* A call that gives `config`, or `noConfig: true`, states its own layers, so it
|
|
110
|
+
* passes through untouched. `noConfig: false` states nothing: it is the default,
|
|
111
|
+
* and it emits no `--no-config`, so it must not drop the stored path either. A call that gives `resume` alone repeats the layers of the
|
|
112
|
+
* Run it resumes: the path is stored, and every layer is read again now.
|
|
113
|
+
*/
|
|
114
|
+
export async function resolveLaunchConfig<T extends LaunchParams>(
|
|
115
|
+
params: T,
|
|
116
|
+
lookup: ResumeRecordLookup,
|
|
117
|
+
): Promise<T & LaunchParams> {
|
|
118
|
+
if (params.config !== undefined || params.noConfig === true) return params;
|
|
119
|
+
if (params.resume === undefined) return params;
|
|
120
|
+
const config = await lookup.resumeConfigFor(resolve(params.resume));
|
|
121
|
+
return config === null ? params : { ...params, config };
|
|
122
|
+
}
|
package/src/record/run-record.ts
CHANGED
|
@@ -22,6 +22,11 @@ export interface RunLaunchContext {
|
|
|
22
22
|
readonly args?: string;
|
|
23
23
|
readonly record?: string;
|
|
24
24
|
readonly resume?: string;
|
|
25
|
+
/**
|
|
26
|
+
* The Run Config path the launch gave, never its contents: a resume reads
|
|
27
|
+
* every layer again at resume time (ADR-0040).
|
|
28
|
+
*/
|
|
29
|
+
readonly config?: string;
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
/**
|
|
@@ -144,6 +149,7 @@ function launchContext(stored: Record<string, unknown>): RunLaunchContext {
|
|
|
144
149
|
...(typeof stored.args === "string" ? { args: stored.args } : {}),
|
|
145
150
|
...(typeof stored.record === "string" ? { record: stored.record } : {}),
|
|
146
151
|
...(typeof stored.resume === "string" ? { resume: stored.resume } : {}),
|
|
152
|
+
...(typeof stored.config === "string" ? { config: stored.config } : {}),
|
|
147
153
|
};
|
|
148
154
|
}
|
|
149
155
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { RunSummary, RunOutcome as SummaryOutcome } from "@yaag/runtime";
|
|
2
2
|
import type { ProcessIdentity, RunOutcome } from "../process/index.ts";
|
|
3
|
-
import { pickInlineResumeSource } from "./resume-source.ts";
|
|
3
|
+
import { pickInlineResumeSource, pickResumeConfig } from "./resume-source.ts";
|
|
4
4
|
import { type RunLaunch, type RunRecord, startedRecord } from "./run-record.ts";
|
|
5
5
|
import { restoreRecords } from "./run-restore.ts";
|
|
6
6
|
import { failedSummary, settledRecord } from "./run-settle-record.ts";
|
|
@@ -229,6 +229,16 @@ export class RunRegistry {
|
|
|
229
229
|
return pickInlineResumeSource(await this.#store.load(), artifact);
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
/**
|
|
233
|
+
* The Run Config path a Checkpoint resumes with, read back from the durable
|
|
234
|
+
* records (ADR-0040). Null when no record holds one. Same wait as above.
|
|
235
|
+
*/
|
|
236
|
+
async resumeConfigFor(artifact: string): Promise<string | null> {
|
|
237
|
+
if (this.#store === undefined) return null;
|
|
238
|
+
await this.#store.settled();
|
|
239
|
+
return pickResumeConfig(await this.#store.load(), artifact);
|
|
240
|
+
}
|
|
241
|
+
|
|
232
242
|
/** Reads a Run this session never started back from the store, by id. */
|
|
233
243
|
async recall(id: string): Promise<RunStatus> {
|
|
234
244
|
const known = this.lookup(id);
|
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
AgentActivity,
|
|
3
3
|
AgentInfo,
|
|
4
4
|
AgentState,
|
|
5
|
+
ModelFallbackInfo,
|
|
5
6
|
NodeInfo,
|
|
6
7
|
NodeState,
|
|
7
8
|
RunOutcome,
|
|
@@ -56,6 +57,7 @@ interface SummaryBase {
|
|
|
56
57
|
readonly incomplete: boolean;
|
|
57
58
|
readonly durationMs: number;
|
|
58
59
|
readonly worstFrameGapMs: number;
|
|
60
|
+
readonly modelFallbacks: number;
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
function parseBase(stored: Record<string, unknown>): SummaryBase | null {
|
|
@@ -88,6 +90,9 @@ function parseBase(stored: Record<string, unknown>): SummaryBase | null {
|
|
|
88
90
|
incomplete: stored.incomplete,
|
|
89
91
|
durationMs: stored.durationMs,
|
|
90
92
|
worstFrameGapMs: stored.worstFrameGapMs,
|
|
93
|
+
// Tolerant, unlike the strict fields above: a record written before the
|
|
94
|
+
// fallback counter existed must still load instead of disappearing.
|
|
95
|
+
modelFallbacks: typeof stored.modelFallbacks === "number" ? stored.modelFallbacks : 0,
|
|
91
96
|
};
|
|
92
97
|
}
|
|
93
98
|
|
|
@@ -110,7 +115,9 @@ function parseAgent(value: unknown): AgentInfo | null {
|
|
|
110
115
|
const nodes = parseNodes(stored.nodes);
|
|
111
116
|
const tokens = parseTokens(stored.tokens);
|
|
112
117
|
const activity = parseActivity(stored.activity);
|
|
118
|
+
const fallbacks = parseFallbacks(stored.modelFallbacks);
|
|
113
119
|
if (
|
|
120
|
+
fallbacks === null ||
|
|
114
121
|
nodes === null ||
|
|
115
122
|
tokens === undefined ||
|
|
116
123
|
activity === undefined ||
|
|
@@ -142,6 +149,10 @@ function parseAgent(value: unknown): AgentInfo | null {
|
|
|
142
149
|
askStartedAt: stored.askStartedAt,
|
|
143
150
|
nodes,
|
|
144
151
|
finishedNodesPruned: stored.finishedNodesPruned,
|
|
152
|
+
modelFallbacks: fallbacks,
|
|
153
|
+
// Tolerant for the same reason as the Run-level counter.
|
|
154
|
+
modelFallbacksPruned:
|
|
155
|
+
typeof stored.modelFallbacksPruned === "number" ? stored.modelFallbacksPruned : 0,
|
|
145
156
|
};
|
|
146
157
|
if (state === "asking") {
|
|
147
158
|
if (typeof stored.askIndex !== "number") return null;
|
|
@@ -189,6 +200,37 @@ function parseNodes(value: unknown): NodeInfo[] | null {
|
|
|
189
200
|
return nodes;
|
|
190
201
|
}
|
|
191
202
|
|
|
203
|
+
/**
|
|
204
|
+
* The per-Agent fallback table: `[]` when a record predates it, `null` when the
|
|
205
|
+
* stored value is malformed.
|
|
206
|
+
*/
|
|
207
|
+
function parseFallbacks(value: unknown): ModelFallbackInfo[] | null {
|
|
208
|
+
if (value === undefined) return [];
|
|
209
|
+
if (!Array.isArray(value)) return null;
|
|
210
|
+
const fallbacks: ModelFallbackInfo[] = [];
|
|
211
|
+
for (const entry of value) {
|
|
212
|
+
const stored = asRecord(entry);
|
|
213
|
+
if (
|
|
214
|
+
stored === null ||
|
|
215
|
+
typeof stored.failedModel !== "string" ||
|
|
216
|
+
typeof stored.resolvedModel !== "string" ||
|
|
217
|
+
typeof stored.attempt !== "number" ||
|
|
218
|
+
!isReason(stored.reason) ||
|
|
219
|
+
!isNullableNumber(stored.at)
|
|
220
|
+
) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
fallbacks.push({
|
|
224
|
+
failedModel: stored.failedModel,
|
|
225
|
+
resolvedModel: stored.resolvedModel,
|
|
226
|
+
attempt: stored.attempt,
|
|
227
|
+
reason: stored.reason,
|
|
228
|
+
at: stored.at,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
return fallbacks;
|
|
232
|
+
}
|
|
233
|
+
|
|
192
234
|
/**
|
|
193
235
|
* `undefined` means the stored value is malformed or missing; a stored `null`
|
|
194
236
|
* is an Agent or Node whose usage was never reported.
|
|
@@ -250,6 +292,10 @@ function isAgentState(value: unknown): value is AgentState {
|
|
|
250
292
|
return value === "idle" || value === "asking" || value === "exited";
|
|
251
293
|
}
|
|
252
294
|
|
|
295
|
+
function isReason(value: unknown): value is ModelFallbackInfo["reason"] {
|
|
296
|
+
return value === "not_found" || value === "auth" || value === "rate_limited";
|
|
297
|
+
}
|
|
298
|
+
|
|
253
299
|
function isNodeState(value: unknown): value is NodeState {
|
|
254
300
|
return value === "running" || value === "exited" || value === "failed";
|
|
255
301
|
}
|
|
@@ -16,6 +16,8 @@ export interface RunParams {
|
|
|
16
16
|
readonly background?: boolean;
|
|
17
17
|
readonly record?: string;
|
|
18
18
|
readonly resume?: string;
|
|
19
|
+
readonly config?: string;
|
|
20
|
+
readonly noConfig?: boolean;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
function renderCallComponent(params: RunParams, expanded: boolean) {
|
|
@@ -20,6 +20,12 @@ const DESCRIPTION = [
|
|
|
20
20
|
"handle.ask supports maxTurns, maxToolCalls, maxDurationMs, and wrapUpPrompt as recoverable",
|
|
21
21
|
"ASK_LIMIT controls. These differ from timeoutMs, the destructive Agent-killing fallback.",
|
|
22
22
|
"",
|
|
23
|
+
"model takes one pattern, an ordered list of patterns, or a function of the failures so far;",
|
|
24
|
+
"thinking takes a level or a function of the settled model. A pattern can end with a thinking",
|
|
25
|
+
'suffix ("opus-5:medium"), which wins over thinking. yaag falls back only on not_found, auth,',
|
|
26
|
+
"and rate_limited, at spawn and inside an Ask. Exhausted candidates fail with",
|
|
27
|
+
"MODEL_RESOLUTION_FAILED.",
|
|
28
|
+
"",
|
|
23
29
|
"Describing imports the program and executes its module top level. Programs should keep",
|
|
24
30
|
"module top level side-effect free; use this only for reviewed, user-authored files.",
|
|
25
31
|
].join("\n");
|
package/src/tool/run-tool.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
mintRunId,
|
|
36
36
|
observedSettlement,
|
|
37
37
|
programTarget,
|
|
38
|
+
resolveLaunchConfig,
|
|
38
39
|
resolveProgramParams,
|
|
39
40
|
} from "../record/index.ts";
|
|
40
41
|
|
|
@@ -65,6 +66,12 @@ const parameters = Type.Object({
|
|
|
65
66
|
"Resume from this Cassette: matching Asks replay free, then the Run continues live. For an inline Run, give resume with no file and no script, and yaag reuses the stored source",
|
|
66
67
|
}),
|
|
67
68
|
),
|
|
69
|
+
config: Type.Optional(Type.String({ description: "Path to one more config file for this Run" })),
|
|
70
|
+
noConfig: Type.Optional(
|
|
71
|
+
Type.Boolean({
|
|
72
|
+
description: "Ignore the global config and the project config for this Run",
|
|
73
|
+
}),
|
|
74
|
+
),
|
|
68
75
|
});
|
|
69
76
|
|
|
70
77
|
/** pi's own message channel, narrowed to what a background Run needs. */
|
|
@@ -121,6 +128,12 @@ const DESCRIPTION = [
|
|
|
121
128
|
"pass its artifact as `resume` on the retry: Asks that already succeeded",
|
|
122
129
|
"replay instantly and free, and the Run goes live where it diverges. Record",
|
|
123
130
|
"the retry too (to a different path) to keep every attempt resumable.",
|
|
131
|
+
"",
|
|
132
|
+
"`config` gives one more config file to this Run. yaag reads it after the",
|
|
133
|
+
"global config and after the project config. `noConfig: true` tells yaag to",
|
|
134
|
+
"ignore the global config and the project config. It does not ignore `config`,",
|
|
135
|
+
"so `config` with `noConfig: true` gives the Run one known config. A config",
|
|
136
|
+
"file that is not there stops the Run at the start.",
|
|
124
137
|
].join("\n");
|
|
125
138
|
|
|
126
139
|
/**
|
|
@@ -169,6 +182,9 @@ export function createRunTool(deps: RunToolOptions): ToolDefinition<typeof param
|
|
|
169
182
|
// A resume with no file and no script takes its source from the Run
|
|
170
183
|
// record, before the shape check, so the shape it produces is valid.
|
|
171
184
|
const target = programTarget(await resolveProgramParams(params, registry));
|
|
185
|
+
// A resume that states no layers of its own repeats the layers of the Run
|
|
186
|
+
// it resumes; every layer is read again now, at resume time (ADR-0040).
|
|
187
|
+
const launch = await resolveLaunchConfig(params, registry);
|
|
172
188
|
if (bun === null) throw new Error(statusReport(null, cli));
|
|
173
189
|
const program = await confirmProgramTarget(target);
|
|
174
190
|
|
|
@@ -183,7 +199,7 @@ export function createRunTool(deps: RunToolOptions): ToolDefinition<typeof param
|
|
|
183
199
|
cli,
|
|
184
200
|
program,
|
|
185
201
|
args: params.args,
|
|
186
|
-
...
|
|
202
|
+
...launchOptions(launch),
|
|
187
203
|
id,
|
|
188
204
|
registry,
|
|
189
205
|
start,
|
|
@@ -217,16 +233,26 @@ export function createRunTool(deps: RunToolOptions): ToolDefinition<typeof param
|
|
|
217
233
|
}
|
|
218
234
|
|
|
219
235
|
/**
|
|
220
|
-
* Cassette paths are `resolve`d here; the CLI owns every rule about
|
|
221
|
-
* combinations, validation, refusals — and its own message surfaces on
|
|
236
|
+
* Cassette and config paths are `resolve`d here; the CLI owns every rule about
|
|
237
|
+
* them — combinations, validation, refusals — and its own message surfaces on
|
|
238
|
+
* failure.
|
|
222
239
|
*/
|
|
223
|
-
function
|
|
240
|
+
function launchOptions(params: {
|
|
241
|
+
readonly record?: string;
|
|
242
|
+
readonly resume?: string;
|
|
243
|
+
readonly config?: string;
|
|
244
|
+
readonly noConfig?: boolean;
|
|
245
|
+
}): {
|
|
224
246
|
record?: string;
|
|
225
247
|
resume?: string;
|
|
248
|
+
config?: string;
|
|
249
|
+
noConfig?: boolean;
|
|
226
250
|
} {
|
|
227
251
|
return {
|
|
228
252
|
...(params.record === undefined ? {} : { record: resolve(params.record) }),
|
|
229
253
|
...(params.resume === undefined ? {} : { resume: resolve(params.resume) }),
|
|
254
|
+
...(params.config === undefined ? {} : { config: resolve(params.config) }),
|
|
255
|
+
...(params.noConfig === true ? { noConfig: true } : {}),
|
|
230
256
|
};
|
|
231
257
|
}
|
|
232
258
|
|
|
@@ -259,6 +285,8 @@ function registeredRun(options: {
|
|
|
259
285
|
readonly args?: string;
|
|
260
286
|
readonly record?: string;
|
|
261
287
|
readonly resume?: string;
|
|
288
|
+
readonly config?: string;
|
|
289
|
+
readonly noConfig?: boolean;
|
|
262
290
|
readonly id: string;
|
|
263
291
|
readonly registry: RunRegistry;
|
|
264
292
|
readonly start: (options: StartRunOptions) => RunHandle;
|
|
@@ -273,6 +301,8 @@ function registeredRun(options: {
|
|
|
273
301
|
args: options.args,
|
|
274
302
|
record: options.record,
|
|
275
303
|
resume: options.resume,
|
|
304
|
+
config: options.config,
|
|
305
|
+
noConfig: options.noConfig,
|
|
276
306
|
onProgress: (summary, event, sequence) => {
|
|
277
307
|
options.registry.progress(options.id, summary);
|
|
278
308
|
options.store.ingest(options.id, { summary, event, sequence, id: options.id });
|
|
@@ -289,6 +319,10 @@ function registeredRun(options: {
|
|
|
289
319
|
...(options.args === undefined ? {} : { args: options.args }),
|
|
290
320
|
...(options.record === undefined ? {} : { record: options.record }),
|
|
291
321
|
...(options.resume === undefined ? {} : { resume: options.resume }),
|
|
322
|
+
// The record stores the Run Config path, never its contents; a resume
|
|
323
|
+
// re-resolves every layer at resume time (ADR-0040). `noConfig` is not
|
|
324
|
+
// stored: it is a property of one launch, not of the Run's program.
|
|
325
|
+
...(options.config === undefined ? {} : { config: options.config }),
|
|
292
326
|
};
|
|
293
327
|
const launch: RunLaunch =
|
|
294
328
|
options.program.kind === "file"
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { DOCS_ROOT } from "../docs/index.ts";
|
|
2
|
+
|
|
1
3
|
/** Builds the host-visible yaag block appended to pi's system prompt. */
|
|
2
4
|
export function yaagPromptBlock(directories: readonly string[]): string {
|
|
3
5
|
const list = directories.join(", ");
|
|
@@ -24,8 +26,9 @@ export function yaagPromptBlock(directories: readonly string[]): string {
|
|
|
24
26
|
"});",
|
|
25
27
|
"```",
|
|
26
28
|
"",
|
|
27
|
-
"Full authoring surface (defineAgent, args schemas, ask limits, worktrees):",
|
|
29
|
+
"Full authoring surface (defineAgent, args schemas, ask limits, worktrees, model fallback):",
|
|
28
30
|
"read `<program dir>/.yaag/types/runtime/index.d.ts`.",
|
|
31
|
+
`Shipped docs (${DOCS_ROOT}): getting-started.md, authoring.md, examples.md, cli.md, troubleshooting.md.`,
|
|
29
32
|
].join("\n");
|
|
30
33
|
}
|
|
31
34
|
|