@yaag/extension 0.2.1 → 0.4.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-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,33 @@ 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 { resolveProgramParams } from "./resume-source.ts";
16
+ import type { ProgramTarget } from "./run-argv.ts";
16
17
  import { mintRunId } from "./run-id.ts";
18
+ import {
19
+ confirmProgramTarget,
20
+ programExpansion,
21
+ programLabel,
22
+ programTarget,
23
+ } from "./run-program-param.ts";
24
+ import type { RunLaunch } from "./run-record.ts";
17
25
  import type { LiveRun, RunRegistry, RunSettlement } from "./run-registry.ts";
18
26
  import { errorText, failure, observedSettlement } from "./run-settlement.ts";
19
27
  import { type RunHandle, type RunOutcome, type StartRunOptions, startRun } from "./spawn-run.ts";
20
28
  import { statusReport } from "./status.ts";
21
29
 
22
30
  const parameters = Type.Object({
23
- file: Type.String({ description: "Path to the Orchestration Program file" }),
31
+ file: Type.Optional(
32
+ Type.String({
33
+ description: "Path to the Orchestration Program file; use script instead for inline source",
34
+ }),
35
+ ),
36
+ script: Type.Optional(
37
+ Type.String({
38
+ description:
39
+ 'The Orchestration Program source text; it can import "@yaag/runtime" and "typebox" only',
40
+ }),
41
+ ),
24
42
  args: Type.Optional(
25
43
  Type.String({ description: "The program's arguments, as a JSON object string" }),
26
44
  ),
@@ -33,7 +51,7 @@ const parameters = Type.Object({
33
51
  resume: Type.Optional(
34
52
  Type.String({
35
53
  description:
36
- "Resume from this Cassette: matching Asks replay free, then the Run continues live",
54
+ "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",
37
55
  }),
38
56
  ),
39
57
  });
@@ -72,6 +90,22 @@ const DESCRIPTION = [
72
90
  "later message. Every Run gets an id and can be inspected with `yaag_status`;",
73
91
  "background Runs may overlap and each is stopped with `yaag_stop({ id })`.",
74
92
  "",
93
+ "`script` runs a program that you give as source text. It needs no file.",
94
+ "Give `file` or `script` to start a new Run. Do not give both. A call with",
95
+ "both, or a new Run with neither, fails before yaag starts the Run. A call that",
96
+ "gives `resume` alone is the one exception: it repeats a stored inline Run.",
97
+ "A `script` program has the same shape as a file program. It default-exports",
98
+ "`defineRun(...)`.",
99
+ 'A `script` program can import "@yaag/runtime" and "typebox" only. Make a file',
100
+ "program if the program needs other modules.",
101
+ "A `script` program must be 65536 bytes (64 KiB) or smaller. Write a larger",
102
+ "program to a file and give `file`.",
103
+ "`args`, `background`, `record` and `resume` work the same way for `script`.",
104
+ "To resume an inline Run, give `resume` and give no `file` and no `script`.",
105
+ "yaag reads the program source from its own Run record.",
106
+ "Give `script` again only when you want to change the program.",
107
+ "`yaag_describe` reads a file only.",
108
+ "",
75
109
  "Set `record` to write the Run's Cassette artifact. If a recorded Run fails,",
76
110
  "pass its artifact as `resume` on the retry: Asks that already succeeded",
77
111
  "replay instantly and free, and the Run goes live where it diverges. Record",
@@ -82,9 +116,13 @@ const DESCRIPTION = [
82
116
  * The `yaag_run` tool: runs one Orchestration Program through the Bun CLI and
83
117
  * returns its value (ADR-0005).
84
118
  *
85
- * `execute` throws on any failure a missing Bun, a path that is not there, a
86
- * Run that failed which is how pi marks a tool result as an error. On failure
87
- * the message carries the tail of the CLI's own error output.
119
+ * `execute` throws on any failure, which is how pi marks a tool result as an
120
+ * error. It refuses the call before any child starts when the parameters are
121
+ * not one program: both `file` and `script`, a new Run with neither, a blank
122
+ * or oversize `script`, or a `resume` alone whose Run record holds no Inline
123
+ * Program source (ADR-0033). It then throws for a missing Bun, a program file
124
+ * that is not there, or a Run that failed. On a Run failure the message
125
+ * carries the tail of the CLI's own error output.
88
126
  */
89
127
  export function createRunTool(deps: RunToolOptions): ToolDefinition<typeof parameters, RunDetails> {
90
128
  const { bun, cli, registry, sendMessage } = deps;
@@ -99,9 +137,11 @@ export function createRunTool(deps: RunToolOptions): ToolDefinition<typeof param
99
137
  renderCall(params, theme, context) {
100
138
  const text =
101
139
  context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
102
- text.setText(
103
- theme.fg("toolTitle", theme.bold("yaag_run")) + theme.fg("muted", `(${params.file})`),
104
- );
140
+ const head =
141
+ theme.fg("toolTitle", theme.bold("yaag_run")) +
142
+ theme.fg("muted", `(${programLabel(params)})`);
143
+ const body = context.expanded ? programExpansion(params) : "";
144
+ text.setText(body === "" ? head : `${head}\n${theme.fg("muted", body)}`);
105
145
  return text;
106
146
  },
107
147
  renderResult(result, _options, _theme, context) {
@@ -115,14 +155,15 @@ export function createRunTool(deps: RunToolOptions): ToolDefinition<typeof param
115
155
  return tree;
116
156
  },
117
157
  async execute(_id, params, signal, onUpdate, ctx) {
158
+ // Shape only, before the Bun check: an exactly-one-of violation, an empty
159
+ // or oversize script is refused identically with or without Bun. The file
160
+ // probe comes after, so a Host Session with no Bun is told to install Bun
161
+ // instead of being told its path is wrong (ADR-0033).
162
+ // A resume with no file and no script takes its source from the Run
163
+ // record, before the shape check, so the shape it produces is valid.
164
+ const target = programTarget(await resolveProgramParams(params, registry));
118
165
  if (bun === null) throw new Error(statusReport(null, cli));
119
-
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
- }
166
+ const program = await confirmProgramTarget(target);
126
167
 
127
168
  const id = mintRunId();
128
169
  const background = params.background === true;
@@ -133,7 +174,7 @@ export function createRunTool(deps: RunToolOptions): ToolDefinition<typeof param
133
174
  const run = registeredRun({
134
175
  bun,
135
176
  cli,
136
- file,
177
+ program,
137
178
  args: params.args,
138
179
  ...cassetteOptions(params),
139
180
  id,
@@ -169,7 +210,7 @@ export function createRunTool(deps: RunToolOptions): ToolDefinition<typeof param
169
210
  }
170
211
 
171
212
  /**
172
- * Cassette paths resolved like `file`; the CLI owns every rule about them —
213
+ * Cassette paths are `resolve`d here; the CLI owns every rule about them —
173
214
  * combinations, validation, refusals — and its own message surfaces on failure.
174
215
  */
175
216
  function cassetteOptions(params: { readonly record?: string; readonly resume?: string }): {
@@ -207,7 +248,7 @@ async function announce(
207
248
  function registeredRun(options: {
208
249
  readonly bun: string;
209
250
  readonly cli: string;
210
- readonly file: string;
251
+ readonly program: ProgramTarget;
211
252
  readonly args?: string;
212
253
  readonly record?: string;
213
254
  readonly resume?: string;
@@ -221,7 +262,7 @@ function registeredRun(options: {
221
262
  const handle = options.start({
222
263
  bun: options.bun,
223
264
  cli: options.cli,
224
- file: options.file,
265
+ program: options.program,
225
266
  args: options.args,
226
267
  record: options.record,
227
268
  resume: options.resume,
@@ -237,12 +278,15 @@ function registeredRun(options: {
237
278
  outcome: handle.outcome,
238
279
  summary: initialSummary(),
239
280
  };
240
- const launch = {
241
- file: options.file,
281
+ const context = {
242
282
  ...(options.args === undefined ? {} : { args: options.args }),
243
283
  ...(options.record === undefined ? {} : { record: options.record }),
244
284
  ...(options.resume === undefined ? {} : { resume: options.resume }),
245
285
  };
286
+ const launch: RunLaunch =
287
+ options.program.kind === "file"
288
+ ? { kind: "file", file: options.program.file, ...context }
289
+ : { kind: "inline", script: options.program.source, ...context };
246
290
  options.registry.add(run, launch, processIdentity(handle.pid));
247
291
  return run;
248
292
  }
@@ -13,7 +13,7 @@ import type { NamedKeybindings, RunTreeViewHost, TreeState } from "@yaag/tui";
13
13
 
14
14
  /** The pi services one open view holds. */
15
15
  export interface RunTreeHostOptions {
16
- readonly ui: Pick<ExtensionUIContext, "notify" | "editor">;
16
+ readonly ui: Pick<ExtensionUIContext, "notify">;
17
17
  readonly tui: TUI;
18
18
  readonly keybindings: NamedKeybindings;
19
19
  /** The Run's projection; the host only reads it. */
@@ -31,10 +31,11 @@ export function createRunTreeHost(options: RunTreeHostOptions): ReadOnlyRunTreeH
31
31
  agentInfo: (agent) => state.summary.agents[agent],
32
32
  readSession: (agent) => readSession(state.summary.agents[agent]),
33
33
  sessionPath: (agent) => state.summary.agents[agent]?.sessionFile ?? null,
34
+ // No capability here opens a pi dialog: `ui.editor`, `select`, `input`, and
35
+ // `confirm` all clear the editor container, which removes the non-overlay
36
+ // `ctx.ui.custom()` view and stops pi's input loop (ADR-0034). Every drill
37
+ // layer draws itself instead.
34
38
  copyPath: (text) => copyToClipboard(text),
35
- openEditor: async (title, body) => {
36
- await ui.editor(title, body);
37
- },
38
39
  notify: (message, level) => ui.notify(message, level),
39
40
  requestRender: () => tui.requestRender(),
40
41
  rows: () => Math.max(1, tui.terminal.rows - 1),
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 whose source travels on fd 4 as `--eval-fd` (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). */
@@ -44,17 +48,26 @@ export interface RunHandle {
44
48
 
45
49
  /**
46
50
  * Starts one Orchestration Program as a child `bun` process without waiting for
47
- * it. Descriptor 3 remains exclusive to Run Lifecycle Events (ADR-0016).
51
+ * it. Descriptor 3 remains exclusive to Run Lifecycle Events out (ADR-0016),
52
+ * and descriptor 4 carries the Inline Program source in (ADR-0033).
48
53
  *
49
54
  * Never throws for a failed Run — the exit code and error tail are part of its
50
55
  * outcome; callers choose how that failure reaches the Host Session.
51
56
  */
52
57
  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 child = startCliChild({ bun: options.bun, cli: options.cli, argv, events: true });
58
+ const argv = runArgv({
59
+ program: options.program,
60
+ ...(options.args === undefined ? {} : { args: options.args }),
61
+ ...(options.record === undefined ? {} : { record: options.record }),
62
+ ...(options.resume === undefined ? {} : { resume: options.resume }),
63
+ });
64
+ const child = startCliChild({
65
+ bun: options.bun,
66
+ cli: options.cli,
67
+ argv,
68
+ events: true,
69
+ ...(options.program.kind === "inline" ? { evalSource: options.program.source } : {}),
70
+ });
58
71
  let summary: RunSummary = initialSummary();
59
72
  let sequence = 0;
60
73
  const outcome = Promise.all([
@@ -2,7 +2,7 @@ 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 type { PersistedRunState } from "./run-record.ts";
5
+ import type { RunRecord } from "./run-record.ts";
6
6
  import type { RegisteredRun, RestoredRun, RunRegistry } from "./run-registry.ts";
7
7
  import { toUsage } from "./usage.ts";
8
8
 
@@ -35,6 +35,9 @@ const DESCRIPTION = [
35
35
  "",
36
36
  "Without an id, lists this session's Runs plus every orphaned or interrupted",
37
37
  "Run left by an earlier session.",
38
+ "",
39
+ "An interrupted inline Run resumes with `resume` alone: yaag reads the program",
40
+ "source from its own Run record.",
38
41
  ].join("\n");
39
42
 
40
43
  /**
@@ -114,20 +117,40 @@ function restoredDetails(run: RestoredRun): SnapshotDetails {
114
117
  record.state === "orphaned" && record.process !== null
115
118
  ? ` Its process ${record.process.pid} is still running; stop it with yaag_stop.`
116
119
  : "";
117
- const detail = `${alive}${resumeHint(record.state, run.summary.artifact)}`;
120
+ const detail = `${alive}${resumeHint(record)}`;
118
121
  const result = record.outcome?.kind === "fulfilled" ? `\n${record.outcome.result}` : "";
119
122
  return { id: record.id, summary: run.summary, result: `${state}${detail}${result}` };
120
123
  }
121
124
 
122
125
  /**
123
- * Names the Checkpoint an unowned Run left behind (ADR-0031). The Run
124
- * republishes it at each Ask boundary, so it exists whenever the Run reached
125
- * one, for a dead Run and for a still-running orphan alike.
126
+ * Names the Checkpoint an unowned Run left behind (ADR-0031), and the call that
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.
126
135
  */
127
- function resumeHint(state: PersistedRunState, artifact: string | null): string {
128
- if (artifact === null) return "";
129
- if (state !== "interrupted" && state !== "orphaned") return "";
130
- return ` Resume it from its checkpoint: ${artifact}.`;
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
+ }
131
154
  }
132
155
 
133
156
  function snapshot(
@@ -7,12 +7,7 @@
7
7
  * covers the rpc/json path, where every `ui` access must throw.
8
8
  */
9
9
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
10
- import {
11
- FakeExtensionUi,
12
- type Notice,
13
- type OpenedComponent,
14
- type OpenedEditor,
15
- } from "./fake-extension-ui.ts";
10
+ import { FakeExtensionUi, type Notice, type OpenedComponent } from "./fake-extension-ui.ts";
16
11
 
17
12
  export type { OpenedComponent } from "./fake-extension-ui.ts";
18
13
 
@@ -22,9 +17,9 @@ export interface TestTuiContextOptions {
22
17
  }
23
18
 
24
19
  /**
25
- * A tui context whose UI is `FakeExtensionUi`: `custom`, `notify`, and
26
- * `editor` work, and every other capability throws on access, so an
27
- * accidental dependency fails loudly.
20
+ * A tui context whose UI is `FakeExtensionUi`: `custom` and `notify` work, and
21
+ * every other capability throws on access, so an accidental dependency fails
22
+ * loudly. `ui.editor` throws with the rest (ADR-0034).
28
23
  */
29
24
  export class TestTuiContext implements ExtensionContext {
30
25
  readonly mode: ExtensionContext["mode"] = "tui";
@@ -53,11 +48,6 @@ export class TestTuiContext implements ExtensionContext {
53
48
  return this.ui.notices;
54
49
  }
55
50
 
56
- /** Editors the view opened. */
57
- get edits(): readonly OpenedEditor[] {
58
- return this.ui.edits;
59
- }
60
-
61
51
  /** The latest inline widget frame per key. */
62
52
  get widgets(): ReadonlyMap<string, string[] | undefined> {
63
53
  return this.ui.widgets;
@@ -33,7 +33,7 @@ export interface YaagCommandOptions {
33
33
  /** Everything `/yaag` needs from its pi context, and nothing more. */
34
34
  export interface YaagContext {
35
35
  readonly mode: ExtensionContext["mode"];
36
- readonly ui: Pick<ExtensionUIContext, "notify" | "select" | "custom" | "editor">;
36
+ readonly ui: Pick<ExtensionUIContext, "notify" | "select" | "custom">;
37
37
  }
38
38
 
39
39
  /** Builds the `/yaag` command bound to this session's registry and projections. */