@yaag/extension 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/run-tool.ts CHANGED
@@ -12,9 +12,16 @@ import { RunTreeStore } from "./run-trees.ts";
12
12
  export type { RunDetails } from "./run-details.ts";
13
13
 
14
14
  import { type ProcessIdentity, readProcessStart } from "./process-liveness.ts";
15
+ import { resolveProgramParams } from "./resume-source.ts";
15
16
  import type { ProgramTarget } from "./run-argv.ts";
16
17
  import { mintRunId } from "./run-id.ts";
17
- import { programLabel, resolveProgramTarget } from "./run-program-param.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";
18
25
  import type { LiveRun, RunRegistry, RunSettlement } from "./run-registry.ts";
19
26
  import { errorText, failure, observedSettlement } from "./run-settlement.ts";
20
27
  import { type RunHandle, type RunOutcome, type StartRunOptions, startRun } from "./spawn-run.ts";
@@ -44,7 +51,7 @@ const parameters = Type.Object({
44
51
  resume: Type.Optional(
45
52
  Type.String({
46
53
  description:
47
- "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",
48
55
  }),
49
56
  ),
50
57
  });
@@ -84,14 +91,19 @@ const DESCRIPTION = [
84
91
  "background Runs may overlap and each is stopped with `yaag_stop({ id })`.",
85
92
  "",
86
93
  "`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.",
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.",
89
97
  "A `script` program has the same shape as a file program. It default-exports",
90
98
  "`defineRun(...)`.",
91
99
  'A `script` program can import "@yaag/runtime" and "typebox" only. Make a file',
92
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`.",
93
103
  "`args`, `background`, `record` and `resume` work the same way for `script`.",
94
- "Give the same script when you resume a recorded `script` Run.",
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.",
95
107
  "`yaag_describe` reads a file only.",
96
108
  "",
97
109
  "Set `record` to write the Run's Cassette artifact. If a recorded Run fails,",
@@ -104,9 +116,13 @@ const DESCRIPTION = [
104
116
  * The `yaag_run` tool: runs one Orchestration Program through the Bun CLI and
105
117
  * returns its value (ADR-0005).
106
118
  *
107
- * `execute` throws on any failure a missing Bun, a path that is not there, a
108
- * Run that failed which is how pi marks a tool result as an error. On failure
109
- * 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.
110
126
  */
111
127
  export function createRunTool(deps: RunToolOptions): ToolDefinition<typeof parameters, RunDetails> {
112
128
  const { bun, cli, registry, sendMessage } = deps;
@@ -121,10 +137,11 @@ export function createRunTool(deps: RunToolOptions): ToolDefinition<typeof param
121
137
  renderCall(params, theme, context) {
122
138
  const text =
123
139
  context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
124
- text.setText(
140
+ const head =
125
141
  theme.fg("toolTitle", theme.bold("yaag_run")) +
126
- theme.fg("muted", `(${programLabel(params)})`),
127
- );
142
+ theme.fg("muted", `(${programLabel(params)})`);
143
+ const body = context.expanded ? programExpansion(params) : "";
144
+ text.setText(body === "" ? head : `${head}\n${theme.fg("muted", body)}`);
128
145
  return text;
129
146
  },
130
147
  renderResult(result, _options, _theme, context) {
@@ -138,10 +155,15 @@ export function createRunTool(deps: RunToolOptions): ToolDefinition<typeof param
138
155
  return tree;
139
156
  },
140
157
  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);
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));
144
165
  if (bun === null) throw new Error(statusReport(null, cli));
166
+ const program = await confirmProgramTarget(target);
145
167
 
146
168
  const id = mintRunId();
147
169
  const background = params.background === true;
@@ -188,7 +210,7 @@ export function createRunTool(deps: RunToolOptions): ToolDefinition<typeof param
188
210
  }
189
211
 
190
212
  /**
191
- * 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 —
192
214
  * combinations, validation, refusals — and its own message surfaces on failure.
193
215
  */
194
216
  function cassetteOptions(params: { readonly record?: string; readonly resume?: string }): {
@@ -256,14 +278,15 @@ function registeredRun(options: {
256
278
  outcome: handle.outcome,
257
279
  summary: initialSummary(),
258
280
  };
259
- const launch = {
260
- ...(options.program.kind === "file"
261
- ? { file: options.program.file }
262
- : { script: options.program.source }),
281
+ const context = {
263
282
  ...(options.args === undefined ? {} : { args: options.args }),
264
283
  ...(options.record === undefined ? {} : { record: options.record }),
265
284
  ...(options.resume === undefined ? {} : { resume: options.resume }),
266
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 };
267
290
  options.registry.add(run, launch, processIdentity(handle.pid));
268
291
  return run;
269
292
  }
@@ -8,12 +8,12 @@
8
8
  import { readFile } from "node:fs/promises";
9
9
  import { copyToClipboard, type ExtensionUIContext } from "@earendil-works/pi-coding-agent";
10
10
  import type { TUI } from "@earendil-works/pi-tui";
11
- import type { AgentInfo } from "@yaag/runtime";
11
+ import { type AgentInfo, readSystemPromptSidecar } from "@yaag/runtime";
12
12
  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. */
@@ -30,11 +30,14 @@ export function createRunTreeHost(options: RunTreeHostOptions): ReadOnlyRunTreeH
30
30
  keybindings: options.keybindings,
31
31
  agentInfo: (agent) => state.summary.agents[agent],
32
32
  readSession: (agent) => readSession(state.summary.agents[agent]),
33
+ readSystemPromptSidecar: (agent) =>
34
+ readSystemPromptSidecar(state.summary.agents[agent]?.sessionFile),
33
35
  sessionPath: (agent) => state.summary.agents[agent]?.sessionFile ?? null,
36
+ // No capability here opens a pi dialog: `ui.editor`, `select`, `input`, and
37
+ // `confirm` all clear the editor container, which removes the non-overlay
38
+ // `ctx.ui.custom()` view and stops pi's input loop (ADR-0034). Every drill
39
+ // layer draws itself instead.
34
40
  copyPath: (text) => copyToClipboard(text),
35
- openEditor: async (title, body) => {
36
- await ui.editor(title, body);
37
- },
38
41
  notify: (message, level) => ui.notify(message, level),
39
42
  requestRender: () => tui.requestRender(),
40
43
  rows: () => Math.max(1, tui.terminal.rows - 1),
package/src/spawn-run.ts CHANGED
@@ -18,7 +18,7 @@ export interface RunOutcome {
18
18
  export interface StartRunOptions {
19
19
  readonly bun: string;
20
20
  readonly cli: string;
21
- /** A program file, or an Inline Program that travels as `--eval` (ADR-0033). */
21
+ /** A program file, or an Inline Program whose source travels on fd 4 as `--eval-fd` (ADR-0033). */
22
22
  readonly program: ProgramTarget;
23
23
  /** Opaque JSON, forwarded untouched: the program validates it (ADR-0010). */
24
24
  readonly args?: string;
@@ -48,7 +48,8 @@ export interface RunHandle {
48
48
 
49
49
  /**
50
50
  * Starts one Orchestration Program as a child `bun` process without waiting for
51
- * 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).
52
53
  *
53
54
  * Never throws for a failed Run — the exit code and error tail are part of its
54
55
  * outcome; callers choose how that failure reaches the Host Session.
@@ -60,7 +61,13 @@ export function startRun(options: StartRunOptions): RunHandle {
60
61
  ...(options.record === undefined ? {} : { record: options.record }),
61
62
  ...(options.resume === undefined ? {} : { resume: options.resume }),
62
63
  });
63
- const child = startCliChild({ bun: options.bun, cli: options.cli, argv, events: true });
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
+ });
64
71
  let summary: RunSummary = initialSummary();
65
72
  let sequence = 0;
66
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. */