@yaag/cli 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/assets/types/runtime/index.d.ts +1 -0
- package/assets/types/runtime/pi-state.d.ts +19 -0
- package/assets/types/runtime/system-prompt-recorder-extension.d.ts +43 -0
- package/assets/types/runtime/system-prompt-recorder.d.ts +17 -0
- package/assets/types/runtime/tool-probe.d.ts +39 -7
- package/package.json +3 -3
- package/src/argv.ts +40 -7
- package/src/cli-tree-host.ts +4 -26
- package/src/eval-fd.ts +35 -0
- package/src/inline-imports.ts +3 -3
- package/src/run-program.ts +15 -3
|
@@ -28,6 +28,7 @@ export type { RunContext } from "./run-context.ts";
|
|
|
28
28
|
export type { DiscoveredSkill, SkillProbeFactory } from "./skill-probe.ts";
|
|
29
29
|
export type { AgentInfo, AgentState, AskingAgentInfo, EndedRunSummary, ExitedAgentInfo, IdleAgentInfo, NodeInfo, RunningRunSummary, RunOutcome, RunState, RunSummary, } from "./summary.ts";
|
|
30
30
|
export { applyEvent, initialSummary } from "./summary.ts";
|
|
31
|
+
export { readSystemPromptSidecar } from "./system-prompt-recorder.ts";
|
|
31
32
|
export type { AgentStats, AgentTransport, AskMarker, AskMarkerContext, AskPlayback, Frame, TokenBreakdown, TransportFactory, TransportStartup, TransportStartupObserver, WorktreeResolution, } from "./transport.ts";
|
|
32
33
|
export type { AskOptions, Handle, ResolvedSpawnOptions, SpawnOptions, SpawnOverrides, StructuredAskOptions, ThinkingLevel, } from "./types.ts";
|
|
33
34
|
export { AGENT_NODE_TABLE_MAX, ASK_OUTPUT_FLUSH_INTERVAL_MS, ASK_OUTPUT_MAX_BYTES, ASK_OUTPUT_TRUNCATION_MARKER, NODE_GIST_MAX_CHARS, PROMPT_GIST_MAX_CHARS, TOOL_ARGS_GIST_MAX_CHARS, } from "./wire-constants.ts";
|
|
@@ -10,6 +10,25 @@ export interface AgentProgress {
|
|
|
10
10
|
}
|
|
11
11
|
/** The command line for one Agent. */
|
|
12
12
|
export declare function piCommand(options: OpenOptions, toolProbeExtensionPath?: string): string[];
|
|
13
|
+
/** The spawn options that decide pi's `--tools` allowlist. */
|
|
14
|
+
export interface ToolSelection {
|
|
15
|
+
readonly tools?: readonly string[] | undefined;
|
|
16
|
+
readonly inherit?: boolean | undefined;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The names for pi's `--tools` flag, or null when yaag emits no allowlist and
|
|
20
|
+
* pi composes the toolset itself.
|
|
21
|
+
*
|
|
22
|
+
* This is the single source of the allowlist rule: `piCommand` writes the flag
|
|
23
|
+
* from it, and `toolContract` in tool-probe.ts states its promise from it.
|
|
24
|
+
*
|
|
25
|
+
* `--no-tools` strips extension tools as well, and refuses a tool registered
|
|
26
|
+
* afterwards, so "no tools" becomes an allowlist of exactly yaag's own tool
|
|
27
|
+
* (e2e/report-result-spike.test.ts). The tool stays inactive until a
|
|
28
|
+
* schema-bearing Ask activates it, so the Agent's effective capability is
|
|
29
|
+
* unchanged.
|
|
30
|
+
*/
|
|
31
|
+
export declare function toolAllowlist(options: ToolSelection): readonly string[] | null;
|
|
13
32
|
/**
|
|
14
33
|
* `provider/id` from a `get_state` response — what the Agent actually resolved
|
|
15
34
|
* to, not the pattern that was requested. Null when the payload has no model.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* yaag's private system-prompt recorder, loaded into every Agent at spawn.
|
|
3
|
+
*
|
|
4
|
+
* pi assembles the system prompt per turn and never persists it in the session
|
|
5
|
+
* file, so a Peek has nothing to show (observation #1204). This extension
|
|
6
|
+
* writes the assembled prompt to a sidecar file beside the session file, and pi
|
|
7
|
+
* loads it last, so the prompt it records holds every other extension's
|
|
8
|
+
* appends. It records rather than reports, so it is no probe: it sends no frame
|
|
9
|
+
* and the Orchestrator never waits for it (ADR-0035).
|
|
10
|
+
*
|
|
11
|
+
* It is strictly best-effort and silent: an Agent without a session file, or a
|
|
12
|
+
* write that fails, records nothing. It never changes the prompt, and it emits
|
|
13
|
+
* no frame, so no Ask can settle differently because of it.
|
|
14
|
+
*
|
|
15
|
+
* Types are declared locally rather than imported from pi: this file is passed
|
|
16
|
+
* to `pi -e` as a path and must load without yaag's dependency graph. pi loads
|
|
17
|
+
* it through jiti, in Node, so it uses `node:fs/promises` and no Bun-only API.
|
|
18
|
+
*/
|
|
19
|
+
/** The pi event that carries the assembled prompt of one turn. */
|
|
20
|
+
interface BeforeAgentStart {
|
|
21
|
+
readonly systemPrompt?: unknown;
|
|
22
|
+
}
|
|
23
|
+
/** The pi context slice this extension reads. */
|
|
24
|
+
interface RecorderContext {
|
|
25
|
+
readonly sessionManager: {
|
|
26
|
+
getSessionFile(): string | undefined;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** The pi surface this extension registers on. */
|
|
30
|
+
interface RecorderAPI {
|
|
31
|
+
on(event: "before_agent_start", handler: (event: BeforeAgentStart, context: RecorderContext) => Promise<undefined>): void;
|
|
32
|
+
}
|
|
33
|
+
/** Extension of the sidecar that holds one Agent's latest system prompt. */
|
|
34
|
+
export declare const SYSTEM_PROMPT_SIDECAR_SUFFIX = ".system-prompt.md";
|
|
35
|
+
/**
|
|
36
|
+
* The sidecar path for one pi session file.
|
|
37
|
+
*
|
|
38
|
+
* The session file's `.jsonl` extension is dropped, so `<id>.jsonl` and its
|
|
39
|
+
* sidecar `<id>.system-prompt.md` sit next to each other in the session dir.
|
|
40
|
+
*/
|
|
41
|
+
export declare function systemPromptSidecarPath(sessionFile: string): string;
|
|
42
|
+
export default function (pi: RecorderAPI): void;
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { systemPromptSidecarPath } from "./system-prompt-recorder-extension.ts";
|
|
2
|
+
export { systemPromptSidecarPath };
|
|
3
|
+
/** Filesystem sibling path of the recorder; decodes percent-escapes (e.g. spaces). */
|
|
4
|
+
export declare function systemPromptRecorderExtensionPath(moduleUrl: string): string;
|
|
5
|
+
/** Loadable source path for the private extension that records the system prompt. */
|
|
6
|
+
export declare const SYSTEM_PROMPT_RECORDER_EXTENSION_PATH: string;
|
|
7
|
+
/**
|
|
8
|
+
* Reads the system-prompt sidecar of one Agent, or null when there is none.
|
|
9
|
+
*
|
|
10
|
+
* Null covers every case a Peek cannot show a real prompt for: an Agent that
|
|
11
|
+
* reported no session file, a session whose first turn has not started, and a
|
|
12
|
+
* session written before yaag recorded a sidecar. A Peek then falls back to the
|
|
13
|
+
* session file. An Agent of a replayed Run has the recording's session path, so
|
|
14
|
+
* a Peek on the recording machine reads the original Agent's sidecar, exactly
|
|
15
|
+
* as it reads the original Agent's transcript.
|
|
16
|
+
*/
|
|
17
|
+
export declare function readSystemPromptSidecar(sessionFile: string | null | undefined): Promise<string | null>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ToolSelection } from "./pi-state.ts";
|
|
1
2
|
import type { Frame } from "./transport.ts";
|
|
2
3
|
/** The private status key used only by yaag's internal pi tool probe extension. */
|
|
3
4
|
export declare const TOOL_PROBE_STATUS_KEY = "yaag.tool-probe.v1";
|
|
@@ -14,17 +15,48 @@ export interface ToolProbe {
|
|
|
14
15
|
/** Rejects startup when the spawned pi process ends before reporting. */
|
|
15
16
|
fail(error: Error): void;
|
|
16
17
|
}
|
|
18
|
+
/** What the spawn flags promised about the Agent's effective toolset. */
|
|
19
|
+
export interface ToolContract {
|
|
20
|
+
/** Names that must be active at startup. Never holds `report_result`. */
|
|
21
|
+
readonly required: readonly string[];
|
|
22
|
+
/** The only names that may be active; null when the effective set is unpredictable. */
|
|
23
|
+
readonly allowed: readonly string[] | null;
|
|
24
|
+
/** Names that must not be active. */
|
|
25
|
+
readonly denied: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
/** The spawn options that decide what the probe can verify. */
|
|
28
|
+
export interface ToolContractOptions extends ToolSelection {
|
|
29
|
+
readonly disallowedTools?: readonly string[] | undefined;
|
|
30
|
+
}
|
|
17
31
|
/**
|
|
18
|
-
* Returns promised
|
|
32
|
+
* Returns what the spawn flags promised, or null when they promised nothing.
|
|
33
|
+
*
|
|
34
|
+
* Two promises are verifiable. A spawn that pi gets a `--tools` allowlist for
|
|
35
|
+
* promises that the allowlist holds every active name. A spawn that only denies
|
|
36
|
+
* tools promises the absence of every denied name, and nothing about the rest.
|
|
37
|
+
* An inheriting spawn without `tools` and without `disallowedTools` promises
|
|
38
|
+
* nothing, so it needs no probe.
|
|
19
39
|
*
|
|
20
|
-
*
|
|
40
|
+
* `report_result` is allowed but never required: report-result-extension.ts
|
|
41
|
+
* registers the tool when a schema-bearing Ask activates it, long after startup
|
|
42
|
+
* (ADR-0032), so at startup the probe reports it as inactive.
|
|
21
43
|
*/
|
|
22
|
-
export declare function
|
|
23
|
-
/**
|
|
24
|
-
|
|
44
|
+
export declare function toolContract(options: ToolContractOptions): ToolContract | null;
|
|
45
|
+
/**
|
|
46
|
+
* Creates a startup collector for the probe's report.
|
|
47
|
+
*
|
|
48
|
+
* The caller creates one only when `toolContract` returned a contract to verify
|
|
49
|
+
* it against, and loads the probe extension in the same decision.
|
|
50
|
+
*/
|
|
51
|
+
export declare function createToolProbe(timeoutMs?: number): ToolProbe;
|
|
25
52
|
/** True exactly for a status frame emitted by the private probe extension. */
|
|
26
53
|
export declare function isToolProbeFrame(frame: Frame): boolean;
|
|
27
54
|
/** Parses the private status payload, rejecting every malformed boundary. */
|
|
28
55
|
export declare function parseToolProbeFrame(frame: Frame): readonly string[];
|
|
29
|
-
/**
|
|
30
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Throws a diagnostic when the effective toolset breaks the promise.
|
|
58
|
+
*
|
|
59
|
+
* A missing required tool, an extra tool outside the allowlist, and a denied
|
|
60
|
+
* tool that is still active all fail the spawn the same way.
|
|
61
|
+
*/
|
|
62
|
+
export declare function verifyEffectiveTools(contract: ToolContract, effective: readonly string[]): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yaag/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@earendil-works/pi-tui": "^0.84.0",
|
|
24
|
-
"@yaag/runtime": "0.
|
|
25
|
-
"@yaag/tui": "0.
|
|
24
|
+
"@yaag/runtime": "0.5.0",
|
|
25
|
+
"@yaag/tui": "0.5.0",
|
|
26
26
|
"typebox": "1.3.7"
|
|
27
27
|
}
|
|
28
28
|
}
|
package/src/argv.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { resolve } from "node:path";
|
|
|
7
7
|
export const USAGE = `usage:
|
|
8
8
|
yaag run <program.ts> [--quiet] [--args <json>] [--record <file>] [--replay <file>] [--resume <file>] [--events-fd <n>]
|
|
9
9
|
yaag run --eval <source> [--quiet] [--args <json>] [--record <file>] [--replay <file>] [--resume <file>] [--events-fd <n>]
|
|
10
|
+
yaag run --eval-fd <n> [--quiet] [--args <json>] [--record <file>] [--replay <file>] [--resume <file>] [--events-fd <n>]
|
|
10
11
|
yaag describe <program.ts>
|
|
11
12
|
yaag setup-workspace [dir]
|
|
12
13
|
|
|
@@ -15,8 +16,12 @@ program file or --eval, but do not give both. A program that --eval runs can
|
|
|
15
16
|
import "@yaag/runtime" and "typebox" only. A program that imports other modules
|
|
16
17
|
must be a file.
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
--eval-fd reads the program source from descriptor <n>, and needs the writer to
|
|
20
|
+
close that descriptor. It keeps the source out of the process argument list,
|
|
21
|
+
where every local user can read it. The yaag extension always uses it.
|
|
22
|
+
|
|
23
|
+
A --resume or --replay Run also needs the program: give the program file,
|
|
24
|
+
--eval <source>, or --eval-fd <n>. A Cassette holds the history of a Run, and never the program
|
|
20
25
|
to run.
|
|
21
26
|
|
|
22
27
|
Warning: describe imports the module and executes its top level. Keep program module top level side-effect free.`;
|
|
@@ -24,7 +29,8 @@ Warning: describe imports the module and executes its top level. Keep program mo
|
|
|
24
29
|
/** The program one `run` invocation names: a file, or source text (ADR-0033). */
|
|
25
30
|
export type ProgramSource =
|
|
26
31
|
| { readonly kind: "file"; readonly file: string }
|
|
27
|
-
| { readonly kind: "inline"; readonly source: string }
|
|
32
|
+
| { readonly kind: "inline"; readonly source: string }
|
|
33
|
+
| { readonly kind: "inline-fd"; readonly fd: number };
|
|
28
34
|
|
|
29
35
|
export type ParsedArgv =
|
|
30
36
|
| {
|
|
@@ -60,6 +66,7 @@ export function parseArgv(argv: readonly string[]): ParsedArgv {
|
|
|
60
66
|
function parseRun(tokens: readonly string[]): ParsedArgv {
|
|
61
67
|
let file: string | undefined;
|
|
62
68
|
let evalSource: string | undefined;
|
|
69
|
+
let evalFd: number | undefined;
|
|
63
70
|
let args: unknown = {};
|
|
64
71
|
let eventsFd: number | undefined;
|
|
65
72
|
let record: string | undefined;
|
|
@@ -74,6 +81,7 @@ function parseRun(tokens: readonly string[]): ParsedArgv {
|
|
|
74
81
|
} else if (
|
|
75
82
|
token === "--args" ||
|
|
76
83
|
token === "--eval" ||
|
|
84
|
+
token === "--eval-fd" ||
|
|
77
85
|
token === "--events-fd" ||
|
|
78
86
|
token === "--record" ||
|
|
79
87
|
token === "--replay" ||
|
|
@@ -88,6 +96,10 @@ function parseRun(tokens: readonly string[]): ParsedArgv {
|
|
|
88
96
|
args = parsed.value;
|
|
89
97
|
} else if (token === "--eval") {
|
|
90
98
|
evalSource = value;
|
|
99
|
+
} else if (token === "--eval-fd") {
|
|
100
|
+
const parsed = parseEvalDescriptor(value);
|
|
101
|
+
if (!parsed.ok) return parsed;
|
|
102
|
+
evalFd = parsed.value;
|
|
91
103
|
} else if (token === "--events-fd") {
|
|
92
104
|
const parsed = parseDescriptor(value);
|
|
93
105
|
if (!parsed.ok) return parsed;
|
|
@@ -108,9 +120,9 @@ function parseRun(tokens: readonly string[]): ParsedArgv {
|
|
|
108
120
|
}
|
|
109
121
|
}
|
|
110
122
|
|
|
111
|
-
const clash = conflict({ file, evalSource, record, replay, resume });
|
|
123
|
+
const clash = conflict({ file, evalSource, evalFd, eventsFd, record, replay, resume });
|
|
112
124
|
if (clash !== null) return failure(`${clash}\n${USAGE}`);
|
|
113
|
-
const program = programSource(file, evalSource);
|
|
125
|
+
const program = programSource(file, evalSource, evalFd);
|
|
114
126
|
if (program === undefined) return missingProgram(resume, replay);
|
|
115
127
|
return {
|
|
116
128
|
ok: true,
|
|
@@ -128,6 +140,8 @@ function parseRun(tokens: readonly string[]): ParsedArgv {
|
|
|
128
140
|
interface RunFlags {
|
|
129
141
|
readonly file?: string;
|
|
130
142
|
readonly evalSource?: string;
|
|
143
|
+
readonly evalFd?: number;
|
|
144
|
+
readonly eventsFd?: number;
|
|
131
145
|
readonly record?: string;
|
|
132
146
|
readonly replay?: string;
|
|
133
147
|
readonly resume?: string;
|
|
@@ -135,10 +149,19 @@ interface RunFlags {
|
|
|
135
149
|
|
|
136
150
|
/** Names the first pair of options that cannot appear in one invocation. */
|
|
137
151
|
function conflict(flags: RunFlags): string | null {
|
|
138
|
-
const { file, evalSource, record, replay, resume } = flags;
|
|
152
|
+
const { file, evalSource, evalFd, eventsFd, record, replay, resume } = flags;
|
|
139
153
|
if (file !== undefined && evalSource !== undefined) {
|
|
140
154
|
return "--eval and a program file cannot be used together";
|
|
141
155
|
}
|
|
156
|
+
if (evalSource !== undefined && evalFd !== undefined) {
|
|
157
|
+
return "--eval and --eval-fd cannot be used together";
|
|
158
|
+
}
|
|
159
|
+
if (file !== undefined && evalFd !== undefined) {
|
|
160
|
+
return "--eval-fd and a program file cannot be used together";
|
|
161
|
+
}
|
|
162
|
+
if (evalFd !== undefined && eventsFd !== undefined && evalFd === eventsFd) {
|
|
163
|
+
return "--eval-fd and --events-fd must use different descriptors";
|
|
164
|
+
}
|
|
142
165
|
if (record !== undefined && replay !== undefined) {
|
|
143
166
|
return "--record and --replay cannot be used together";
|
|
144
167
|
}
|
|
@@ -159,7 +182,7 @@ function missingProgram(
|
|
|
159
182
|
if (resume === undefined && replay === undefined) return failure(USAGE);
|
|
160
183
|
const flag = resume !== undefined ? "--resume" : "--replay";
|
|
161
184
|
return failure(
|
|
162
|
-
`${flag} needs the program too: give <program.ts
|
|
185
|
+
`${flag} needs the program too: give <program.ts>, --eval <source>, or --eval-fd <n>. ` +
|
|
163
186
|
`A cassette holds the history of a Run, and never the program to run.\n${USAGE}`,
|
|
164
187
|
);
|
|
165
188
|
}
|
|
@@ -167,8 +190,10 @@ function missingProgram(
|
|
|
167
190
|
function programSource(
|
|
168
191
|
file: string | undefined,
|
|
169
192
|
source: string | undefined,
|
|
193
|
+
fd: number | undefined,
|
|
170
194
|
): ProgramSource | undefined {
|
|
171
195
|
if (source !== undefined) return { kind: "inline", source };
|
|
196
|
+
if (fd !== undefined) return { kind: "inline-fd", fd };
|
|
172
197
|
return file === undefined ? undefined : { kind: "file", file };
|
|
173
198
|
}
|
|
174
199
|
|
|
@@ -202,6 +227,14 @@ function parseDescriptor(value: string): ParsedValue<number> {
|
|
|
202
227
|
: failure(`--events-fd must be a descriptor number\n${USAGE}`);
|
|
203
228
|
}
|
|
204
229
|
|
|
230
|
+
/** 0, 1 and 2 are stdin, stdout and stderr, so an Inline Program needs 3 or higher. */
|
|
231
|
+
function parseEvalDescriptor(value: string): ParsedValue<number> {
|
|
232
|
+
const fd = Number(value);
|
|
233
|
+
return Number.isInteger(fd) && fd >= 3
|
|
234
|
+
? { ok: true, value: fd }
|
|
235
|
+
: failure(`--eval-fd must be a descriptor number of 3 or higher\n${USAGE}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
205
238
|
function failure(error: string): { readonly ok: false; readonly error: string } {
|
|
206
239
|
return { ok: false, error };
|
|
207
240
|
}
|
package/src/cli-tree-host.ts
CHANGED
|
@@ -6,11 +6,9 @@
|
|
|
6
6
|
* caller adds `stop` and `done`: nothing in this module can prompt
|
|
7
7
|
* an Agent (ADR — a Peek observes, it never sends).
|
|
8
8
|
*/
|
|
9
|
-
import { readFile
|
|
10
|
-
import { tmpdir } from "node:os";
|
|
11
|
-
import { join } from "node:path";
|
|
9
|
+
import { readFile } from "node:fs/promises";
|
|
12
10
|
import type { Terminal } from "@earendil-works/pi-tui";
|
|
13
|
-
import type
|
|
11
|
+
import { type AgentInfo, readSystemPromptSidecar } from "@yaag/runtime";
|
|
14
12
|
import { cliKeybindings, type RunTreeViewHost, type TreeState } from "@yaag/tui";
|
|
15
13
|
|
|
16
14
|
/** The terminal services one open CLI view holds. */
|
|
@@ -21,8 +19,6 @@ export interface CliTreeHostOptions {
|
|
|
21
19
|
/** Shows a transient message; `tree-app.ts` routes it to the TUI. */
|
|
22
20
|
notify(message: string, level: "info" | "warning" | "error"): void;
|
|
23
21
|
requestRender(): void;
|
|
24
|
-
/** Where `openEditor` puts the body; defaults to the system temp directory. */
|
|
25
|
-
readonly scratchDir?: string;
|
|
26
22
|
}
|
|
27
23
|
|
|
28
24
|
/** The read-only capabilities the CLI Run view has. */
|
|
@@ -35,23 +31,14 @@ export function createCliTreeHost(options: CliTreeHostOptions): ReadOnlyCliHost
|
|
|
35
31
|
keybindings: cliKeybindings(),
|
|
36
32
|
agentInfo: (agent) => state.summary.agents[agent],
|
|
37
33
|
readSession: (agent) => readSession(state.summary.agents[agent]),
|
|
34
|
+
readSystemPromptSidecar: (agent) =>
|
|
35
|
+
readSystemPromptSidecar(state.summary.agents[agent]?.sessionFile),
|
|
38
36
|
sessionPath: (agent) => state.summary.agents[agent]?.sessionFile ?? null,
|
|
39
37
|
// OSC 52 is the only copy path that works over SSH without a child
|
|
40
38
|
// process, and it is what pi-tui itself uses for its own copy gesture.
|
|
41
39
|
copyPath: async (text) => {
|
|
42
40
|
terminal.write(`\u001b]52;c;${Buffer.from(text, "utf8").toString("base64")}\u0007`);
|
|
43
41
|
},
|
|
44
|
-
// Spawning $EDITOR inside a live alt-screen would fight the TUI for the
|
|
45
|
-
// terminal, so the body lands in a file and the path is reported instead.
|
|
46
|
-
openEditor: async (title, body) => {
|
|
47
|
-
const path = join(options.scratchDir ?? tmpdir(), scratchName(title));
|
|
48
|
-
try {
|
|
49
|
-
await writeFile(path, body, "utf8");
|
|
50
|
-
options.notify(`Wrote ${title} to ${path}`, "info");
|
|
51
|
-
} catch (error) {
|
|
52
|
-
options.notify(`Could not write ${title}: ${message(error)}`, "error");
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
42
|
notify: (text, level) => options.notify(text, level),
|
|
56
43
|
requestRender: () => options.requestRender(),
|
|
57
44
|
rows: () => Math.max(1, terminal.rows - 1),
|
|
@@ -70,12 +57,3 @@ async function readSession(info: AgentInfo | undefined): Promise<string | null>
|
|
|
70
57
|
return null;
|
|
71
58
|
}
|
|
72
59
|
}
|
|
73
|
-
|
|
74
|
-
function scratchName(title: string): string {
|
|
75
|
-
const slug = title.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
76
|
-
return `yaag-${slug === "" ? "note" : slug}.txt`;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function message(error: unknown): string {
|
|
80
|
-
return error instanceof Error ? error.message : String(error);
|
|
81
|
-
}
|
package/src/eval-fd.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads the source of an Inline Program from a file descriptor (ADR-0033).
|
|
3
|
+
*
|
|
4
|
+
* An argv element is readable by every local user through `ps` and
|
|
5
|
+
* `/proc/<pid>/cmdline`, so the source of an Inline Program travels on a
|
|
6
|
+
* descriptor instead. Descriptor 3 stays exclusive to Run Lifecycle Events
|
|
7
|
+
* (ADR-0016), so the extension writes the source to descriptor 4.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** The descriptor the extension writes an Inline Program to; fd 3 is events. */
|
|
11
|
+
export const DEFAULT_EVAL_FD = 4;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Reads the whole Inline Program source from `fd`, until end of file.
|
|
15
|
+
*
|
|
16
|
+
* The read stops when the writer closes the descriptor, so a caller that
|
|
17
|
+
* keeps the descriptor open parks the Run before the program loads.
|
|
18
|
+
*
|
|
19
|
+
* Throws an `Error` that names the descriptor when the read fails, and when
|
|
20
|
+
* the descriptor gives no source.
|
|
21
|
+
*/
|
|
22
|
+
export async function readEvalSource(fd: number): Promise<string> {
|
|
23
|
+
let source: string;
|
|
24
|
+
try {
|
|
25
|
+
source = await Bun.file(fd).text();
|
|
26
|
+
} catch (error) {
|
|
27
|
+
throw new Error(`--eval-fd ${fd} cannot be read: ${String(error)}`);
|
|
28
|
+
}
|
|
29
|
+
if (source === "") {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`--eval-fd ${fd} gave no source; write the Orchestration Program to the descriptor and close it`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return source;
|
|
35
|
+
}
|
package/src/inline-imports.ts
CHANGED
|
@@ -35,7 +35,7 @@ export function assertClosedImports(source: string): void {
|
|
|
35
35
|
throw new Error(
|
|
36
36
|
`inline program cannot import a computed specifier ` +
|
|
37
37
|
`(import call on line ${computed.line}): ` +
|
|
38
|
-
|
|
38
|
+
`an Inline Program allows ${allowedText()} only, each one as a string literal`,
|
|
39
39
|
);
|
|
40
40
|
}
|
|
41
41
|
const transpiler = new Bun.Transpiler({ loader: "ts" });
|
|
@@ -43,7 +43,7 @@ export function assertClosedImports(source: string): void {
|
|
|
43
43
|
if (ALLOWED_INLINE_IMPORTS.includes(scanned.path)) continue;
|
|
44
44
|
throw new Error(
|
|
45
45
|
`inline program cannot import ${JSON.stringify(scanned.path)}: ` +
|
|
46
|
-
|
|
46
|
+
`an Inline Program allows ${allowedText()} only`,
|
|
47
47
|
);
|
|
48
48
|
}
|
|
49
49
|
}
|
|
@@ -61,7 +61,7 @@ export function assertClosedImports(source: string): void {
|
|
|
61
61
|
* by exactly one.
|
|
62
62
|
*/
|
|
63
63
|
export function inlineRequirePrelude(): string {
|
|
64
|
-
const suffix = JSON.stringify(`:
|
|
64
|
+
const suffix = JSON.stringify(`: an Inline Program allows ${allowedText()} only`);
|
|
65
65
|
return [
|
|
66
66
|
"const require = ((real, allowed) => (id) => {",
|
|
67
67
|
" if (allowed.includes(id)) return real(id);",
|
package/src/run-program.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Composes the
|
|
3
|
-
*
|
|
2
|
+
* Composes the ways `yaag run` gets its Orchestration Program: a file, or the
|
|
3
|
+
* source of an Inline Program. That source arrives as text, or on a
|
|
4
|
+
* descriptor (ADR-0033); the loader below does not know which.
|
|
4
5
|
*
|
|
5
6
|
* The composition sits above both loaders, so neither loader knows the other
|
|
6
7
|
* and the imports stay a DAG.
|
|
@@ -8,6 +9,7 @@
|
|
|
8
9
|
import { resolve } from "node:path";
|
|
9
10
|
import type { OrchestrationProgram } from "@yaag/runtime";
|
|
10
11
|
import type { ProgramSource } from "./argv.ts";
|
|
12
|
+
import { readEvalSource } from "./eval-fd.ts";
|
|
11
13
|
import { loadInlineProgram } from "./inline-program.ts";
|
|
12
14
|
import { loadProgram } from "./program-loader.ts";
|
|
13
15
|
|
|
@@ -25,7 +27,9 @@ export interface LoadedProgram {
|
|
|
25
27
|
* A file path is resolved against the working directory, and its
|
|
26
28
|
* `programFile` is that absolute path. An Inline Program reports no
|
|
27
29
|
* `programFile`, because its temporary module is unlinked after the import; it
|
|
28
|
-
* reports its source text as its identity instead.
|
|
30
|
+
* reports its source text as its identity instead. A `--eval-fd` program is
|
|
31
|
+
* read to end of file first, so a writer that never closes the descriptor
|
|
32
|
+
* parks the Run (see `eval-fd.ts`).
|
|
29
33
|
* Each failure of `loadProgram` or `loadInlineProgram` — a missing file, a
|
|
30
34
|
* specifier outside the closed contract, a module that is not a program —
|
|
31
35
|
* passes through unchanged.
|
|
@@ -42,5 +46,13 @@ export async function loadRunProgram(source: ProgramSource): Promise<LoadedProgr
|
|
|
42
46
|
programFile: undefined,
|
|
43
47
|
programSource: source.source,
|
|
44
48
|
};
|
|
49
|
+
case "inline-fd": {
|
|
50
|
+
const text = await readEvalSource(source.fd);
|
|
51
|
+
return {
|
|
52
|
+
program: await loadInlineProgram(text),
|
|
53
|
+
programFile: undefined,
|
|
54
|
+
programSource: text,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
45
57
|
}
|
|
46
58
|
}
|