@yaag/extension 0.2.1 → 0.3.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 CHANGED
@@ -35,12 +35,17 @@ For a throwaway session instead: `pi -e ./packages/extension`.
35
35
 
36
36
  | Parameter | Type | Meaning |
37
37
  |---|---|---|
38
- | `file` | `string` | Path to the Orchestration Program file |
38
+ | `file` | `string?` | Path to the Orchestration Program file |
39
+ | `script` | `string?` | Orchestration Program source text (Inline Program) |
39
40
  | `args` | `string?` | The program's arguments, as a JSON object string |
40
41
  | `background` | `boolean?` | Start the Run in the background and return its Run id |
41
42
  | `record` | `string?` | Write this Run's Cassette artifact to this path |
42
43
  | `resume` | `string?` | Replay a matching Cassette prefix, then continue live |
43
44
 
45
+ Give `file` or `script`, and not both; a violation is a parameter error raised
46
+ before any process starts. A `script` program can import `@yaag/runtime` and
47
+ `typebox` only (ADR-0033).
48
+
44
49
  Blocking by default: the call returns when the Run ends, and its content is the
45
50
  Run's return value. With `background: true` it returns at once with a short Run
46
51
  id (`r1`, `r2`, …) and the result arrives later as a follow-up message that
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yaag/extension",
3
- "version": "0.2.1",
3
+ "version": "0.3.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.2.1",
28
- "@yaag/runtime": "0.2.1",
29
- "@yaag/tui": "0.2.1",
27
+ "@yaag/cli": "0.3.0",
28
+ "@yaag/runtime": "0.3.0",
29
+ "@yaag/tui": "0.3.0",
30
30
  "nanoid": "^6.0.1"
31
31
  },
32
32
  "peerDependencies": {
@@ -0,0 +1,34 @@
1
+ /** What a Run runs: a program file, or Inline Program source text (ADR-0033). */
2
+ export type ProgramTarget =
3
+ | { readonly kind: "file"; readonly file: string }
4
+ | { readonly kind: "inline"; readonly source: string };
5
+
6
+ /** The parts of a Run the CLI argv carries. */
7
+ export interface RunArgvOptions {
8
+ readonly program: ProgramTarget;
9
+ /** Opaque JSON, forwarded untouched: the program validates it (ADR-0010). */
10
+ readonly args?: string;
11
+ /** Cassette path to write, forwarded as `--record` (ADR-0013). */
12
+ readonly record?: string;
13
+ /** Cassette path to resume from, forwarded as `--resume` (ADR-0014). */
14
+ readonly resume?: string;
15
+ /** The descriptor Run Lifecycle Events travel on; 3 by default (ADR-0016). */
16
+ readonly eventsFd?: number;
17
+ }
18
+
19
+ /**
20
+ * Builds the CLI argv for one Run.
21
+ *
22
+ * An Inline Program travels as one `--eval` argument holding the source text
23
+ * verbatim (ADR-0033); this function never quotes, escapes or validates it —
24
+ * the CLI owns the closed import contract.
25
+ */
26
+ export function runArgv(options: RunArgvOptions): readonly string[] {
27
+ const { program } = options;
28
+ const argv = program.kind === "file" ? ["run", program.file] : ["run", "--eval", program.source];
29
+ argv.push("--events-fd", String(options.eventsFd ?? 3));
30
+ if (options.args !== undefined) argv.push("--args", options.args);
31
+ if (options.record !== undefined) argv.push("--record", options.record);
32
+ if (options.resume !== undefined) argv.push("--resume", options.resume);
33
+ return argv;
34
+ }
@@ -0,0 +1,64 @@
1
+ import { access } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { sanitizeTerminalLine } from "@yaag/tui";
4
+ import type { ProgramTarget } from "./run-argv.ts";
5
+
6
+ /** The `yaag_run` parameters that name the program; exactly one is given. */
7
+ export interface ProgramParams {
8
+ readonly file?: string;
9
+ readonly script?: string;
10
+ }
11
+
12
+ const LABEL_LIMIT = 60;
13
+
14
+ /**
15
+ * Resolves the program the caller named to a {@link ProgramTarget}.
16
+ *
17
+ * `file` and `script` are exactly-one-of (ADR-0033): a violation throws a
18
+ * parameter error, so the refusal happens before any process starts. A `script`
19
+ * touches no filesystem; the CLI writes and owns its temp module.
20
+ */
21
+ export async function resolveProgramTarget(params: ProgramParams): Promise<ProgramTarget> {
22
+ const { file, script } = params;
23
+ if (file !== undefined && script !== undefined) {
24
+ throw new Error("yaag_run: give file or script, and not both");
25
+ }
26
+ if (script !== undefined) {
27
+ if (script.trim() === "") throw new Error("yaag_run: script is empty");
28
+ return { kind: "inline", source: script };
29
+ }
30
+ if (file === undefined) throw new Error("yaag_run: give file or script");
31
+ const resolved = resolve(file);
32
+ try {
33
+ await access(resolved);
34
+ } catch {
35
+ throw new Error(`yaag_run: no such Orchestration Program: ${resolved}`);
36
+ }
37
+ return { kind: "file", file: resolved };
38
+ }
39
+
40
+ /**
41
+ * A one-line label for the call render. Total by design: the render runs before
42
+ * `execute` validates, so an invalid parameter pair must still produce a label.
43
+ *
44
+ * An Inline Program shows its declared name, else its first line — never a temp
45
+ * path, which exists only inside the CLI.
46
+ */
47
+ export function programLabel(params: ProgramParams): string {
48
+ const { file, script } = params;
49
+ if (file !== undefined && script !== undefined) return "(invalid call)";
50
+ if (script !== undefined) return truncate(inlineLabel(script));
51
+ if (file !== undefined) return file;
52
+ return "(invalid call)";
53
+ }
54
+
55
+ function inlineLabel(source: string): string {
56
+ const declared = /name:\s*["'`]([^"'`]+)/.exec(source);
57
+ const line = declared?.[1] ?? source.split("\n").find((one) => one.trim() !== "") ?? "";
58
+ return sanitizeTerminalLine(line).replace(/\s+/g, " ").trim();
59
+ }
60
+
61
+ function truncate(label: string): string {
62
+ if (label === "") return "(empty script)";
63
+ return label.length <= LABEL_LIMIT ? label : `${label.slice(0, LABEL_LIMIT)}…`;
64
+ }
package/src/run-record.ts CHANGED
@@ -18,7 +18,14 @@ export type PersistedOutcome =
18
18
 
19
19
  /** What starting a Run said about it; enough to describe it in a later session. */
20
20
  export interface RunLaunch {
21
- readonly file: string;
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;
22
29
  readonly args?: string;
23
30
  readonly record?: string;
24
31
  readonly resume?: string;
@@ -82,7 +89,10 @@ function isState(value: unknown): value is PersistedRunState {
82
89
 
83
90
  function isLaunch(value: unknown): value is RunLaunch {
84
91
  return (
85
- typeof value === "object" && value !== null && "file" in value && typeof value.file === "string"
92
+ typeof value === "object" &&
93
+ value !== null &&
94
+ (("file" in value && typeof value.file === "string") ||
95
+ ("script" in value && typeof value.script === "string"))
86
96
  );
87
97
  }
88
98
 
@@ -1,13 +1,16 @@
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";
3
5
  import type { BackgroundStatus } from "./background-status.ts";
6
+ import { fakeTheme } from "./fake-theme.ts";
4
7
  import { resolveBun } from "./resolve-bun.ts";
5
8
  import { resolveCliEntry } from "./resolve-cli.ts";
6
9
  import type { RunDetails } from "./run-details.ts";
7
10
  import { RunRegistry } from "./run-registry.ts";
8
11
  import { createRunTool, type SendMessage } from "./run-tool.ts";
9
12
  import type { RunTreeStore } from "./run-trees.ts";
10
- import type { RunHandle } from "./spawn-run.ts";
13
+ import type { RunHandle, StartRunOptions } from "./spawn-run.ts";
11
14
  import { createStopTool, type StopDetails } from "./stop-tool.ts";
12
15
  import { TestExtensionContext } from "./test-extension-context.ts";
13
16
 
@@ -19,9 +22,60 @@ const ctx = new TestExtensionContext(dirname(cli));
19
22
  export const fixture = (name: string): string => join(dirname(cli), "fixtures", `${name}.ts`);
20
23
 
21
24
  export interface RunParams {
22
- readonly file: string;
25
+ readonly file?: string;
26
+ readonly script?: string;
23
27
  readonly args?: string;
24
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
+ };
25
79
  }
26
80
 
27
81
  export interface Sent {
package/src/run-tool.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { access } from "node:fs/promises";
2
1
  import { resolve } from "node:path";
3
2
  import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
4
3
  import { Text } from "@earendil-works/pi-tui";
@@ -13,14 +12,26 @@ import { RunTreeStore } from "./run-trees.ts";
13
12
  export type { RunDetails } from "./run-details.ts";
14
13
 
15
14
  import { type ProcessIdentity, readProcessStart } from "./process-liveness.ts";
15
+ import type { ProgramTarget } from "./run-argv.ts";
16
16
  import { mintRunId } from "./run-id.ts";
17
+ import { programLabel, resolveProgramTarget } from "./run-program-param.ts";
17
18
  import type { LiveRun, RunRegistry, RunSettlement } from "./run-registry.ts";
18
19
  import { errorText, failure, observedSettlement } from "./run-settlement.ts";
19
20
  import { type RunHandle, type RunOutcome, type StartRunOptions, startRun } from "./spawn-run.ts";
20
21
  import { statusReport } from "./status.ts";
21
22
 
22
23
  const parameters = Type.Object({
23
- file: Type.String({ description: "Path to the Orchestration Program file" }),
24
+ file: Type.Optional(
25
+ Type.String({
26
+ description: "Path to the Orchestration Program file; use script instead for inline source",
27
+ }),
28
+ ),
29
+ script: Type.Optional(
30
+ Type.String({
31
+ description:
32
+ 'The Orchestration Program source text; it can import "@yaag/runtime" and "typebox" only',
33
+ }),
34
+ ),
24
35
  args: Type.Optional(
25
36
  Type.String({ description: "The program's arguments, as a JSON object string" }),
26
37
  ),
@@ -72,6 +83,17 @@ const DESCRIPTION = [
72
83
  "later message. Every Run gets an id and can be inspected with `yaag_status`;",
73
84
  "background Runs may overlap and each is stopped with `yaag_stop({ id })`.",
74
85
  "",
86
+ "`script` runs a program that you give as source text. It needs no file.",
87
+ "Give `file` or `script`. Do not give both. A call with both, or with neither,",
88
+ "fails before yaag starts the Run.",
89
+ "A `script` program has the same shape as a file program. It default-exports",
90
+ "`defineRun(...)`.",
91
+ 'A `script` program can import "@yaag/runtime" and "typebox" only. Make a file',
92
+ "program if the program needs other modules.",
93
+ "`args`, `background`, `record` and `resume` work the same way for `script`.",
94
+ "Give the same script when you resume a recorded `script` Run.",
95
+ "`yaag_describe` reads a file only.",
96
+ "",
75
97
  "Set `record` to write the Run's Cassette artifact. If a recorded Run fails,",
76
98
  "pass its artifact as `resume` on the retry: Asks that already succeeded",
77
99
  "replay instantly and free, and the Run goes live where it diverges. Record",
@@ -100,7 +122,8 @@ export function createRunTool(deps: RunToolOptions): ToolDefinition<typeof param
100
122
  const text =
101
123
  context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
102
124
  text.setText(
103
- theme.fg("toolTitle", theme.bold("yaag_run")) + theme.fg("muted", `(${params.file})`),
125
+ theme.fg("toolTitle", theme.bold("yaag_run")) +
126
+ theme.fg("muted", `(${programLabel(params)})`),
104
127
  );
105
128
  return text;
106
129
  },
@@ -115,15 +138,11 @@ export function createRunTool(deps: RunToolOptions): ToolDefinition<typeof param
115
138
  return tree;
116
139
  },
117
140
  async execute(_id, params, signal, onUpdate, ctx) {
141
+ // Parameter validation precedes the Bun check, so an exactly-one-of
142
+ // violation is refused identically with or without Bun (ADR-0033).
143
+ const program = await resolveProgramTarget(params);
118
144
  if (bun === null) throw new Error(statusReport(null, cli));
119
145
 
120
- const file = resolve(params.file);
121
- try {
122
- await access(file);
123
- } catch {
124
- throw new Error(`yaag_run: no such Orchestration Program: ${file}`);
125
- }
126
-
127
146
  const id = mintRunId();
128
147
  const background = params.background === true;
129
148
  const inline = inlineAvailable(ctx, background);
@@ -133,7 +152,7 @@ export function createRunTool(deps: RunToolOptions): ToolDefinition<typeof param
133
152
  const run = registeredRun({
134
153
  bun,
135
154
  cli,
136
- file,
155
+ program,
137
156
  args: params.args,
138
157
  ...cassetteOptions(params),
139
158
  id,
@@ -207,7 +226,7 @@ async function announce(
207
226
  function registeredRun(options: {
208
227
  readonly bun: string;
209
228
  readonly cli: string;
210
- readonly file: string;
229
+ readonly program: ProgramTarget;
211
230
  readonly args?: string;
212
231
  readonly record?: string;
213
232
  readonly resume?: string;
@@ -221,7 +240,7 @@ function registeredRun(options: {
221
240
  const handle = options.start({
222
241
  bun: options.bun,
223
242
  cli: options.cli,
224
- file: options.file,
243
+ program: options.program,
225
244
  args: options.args,
226
245
  record: options.record,
227
246
  resume: options.resume,
@@ -238,7 +257,9 @@ function registeredRun(options: {
238
257
  summary: initialSummary(),
239
258
  };
240
259
  const launch = {
241
- file: options.file,
260
+ ...(options.program.kind === "file"
261
+ ? { file: options.program.file }
262
+ : { script: options.program.source }),
242
263
  ...(options.args === undefined ? {} : { args: options.args }),
243
264
  ...(options.record === undefined ? {} : { record: options.record }),
244
265
  ...(options.resume === undefined ? {} : { resume: options.resume }),
package/src/spawn-run.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import { applyEvent, initialSummary, type LifecycleEvent, type RunSummary } from "@yaag/runtime";
2
2
  import { startCliChild } from "./cli-child.ts";
3
3
  import { readEvents } from "./event-reader.ts";
4
+ import { type ProgramTarget, runArgv } from "./run-argv.ts";
5
+
6
+ export type { ProgramTarget } from "./run-argv.ts";
4
7
 
5
8
  /** Everything one Run of the CLI produced, once the child has exited. */
6
9
  export interface RunOutcome {
@@ -15,7 +18,8 @@ export interface RunOutcome {
15
18
  export interface StartRunOptions {
16
19
  readonly bun: string;
17
20
  readonly cli: string;
18
- readonly file: string;
21
+ /** A program file, or an Inline Program that travels as `--eval` (ADR-0033). */
22
+ readonly program: ProgramTarget;
19
23
  /** Opaque JSON, forwarded untouched: the program validates it (ADR-0010). */
20
24
  readonly args?: string;
21
25
  /** Cassette path to write, forwarded as `--record` (ADR-0013). */
@@ -50,10 +54,12 @@ export interface RunHandle {
50
54
  * outcome; callers choose how that failure reaches the Host Session.
51
55
  */
52
56
  export function startRun(options: StartRunOptions): RunHandle {
53
- const argv = ["run", options.file, "--events-fd", "3"];
54
- if (options.args !== undefined) argv.push("--args", options.args);
55
- if (options.record !== undefined) argv.push("--record", options.record);
56
- if (options.resume !== undefined) argv.push("--resume", options.resume);
57
+ const argv = runArgv({
58
+ program: options.program,
59
+ ...(options.args === undefined ? {} : { args: options.args }),
60
+ ...(options.record === undefined ? {} : { record: options.record }),
61
+ ...(options.resume === undefined ? {} : { resume: options.resume }),
62
+ });
57
63
  const child = startCliChild({ bun: options.bun, cli: options.cli, argv, events: true });
58
64
  let summary: RunSummary = initialSummary();
59
65
  let sequence = 0;