@dna113p/machines 0.3.4 → 0.3.6
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 +7 -2
- package/claude/statusline.mjs +2 -0
- package/dist/mcp/server.js +2 -2
- package/dist/src/cli.js +58 -4
- package/dist/src/codex.d.ts +2 -0
- package/dist/src/codex.js +2 -0
- package/dist/src/host.d.ts +6 -0
- package/dist/src/host.js +7 -2
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/launcher.d.ts +3 -0
- package/dist/src/launcher.js +3 -0
- package/dist/src/run-graph.d.ts +39 -0
- package/dist/src/run-graph.js +91 -0
- package/dist/src/run-status.d.ts +16 -2
- package/dist/src/run-status.js +56 -3
- package/dist/src/run-ui-page.d.ts +6 -0
- package/dist/src/run-ui-page.js +690 -0
- package/dist/src/run-ui.d.ts +62 -0
- package/dist/src/run-ui.js +258 -0
- package/dist/src/run-viewer.d.ts +2 -0
- package/dist/src/run-viewer.js +1 -0
- package/docs/authoring.md +13 -0
- package/docs/integrations.md +62 -4
- package/package.json +88 -21
- package/skills/machine-builder/SKILL.md +31 -0
- package/skills/machine-delegation/SKILL.md +23 -0
- package/skills/machine-project-setup/SKILL.md +23 -5
- package/skills/machine-project-setup/references/project-workflows.md +4 -0
package/README.md
CHANGED
|
@@ -176,10 +176,10 @@ npm ci
|
|
|
176
176
|
npm pack
|
|
177
177
|
```
|
|
178
178
|
|
|
179
|
-
Then install the generated `dna113p-machines-0.3.
|
|
179
|
+
Then install the generated `dna113p-machines-0.3.6.tgz` into a project:
|
|
180
180
|
|
|
181
181
|
```bash
|
|
182
|
-
npm install /absolute/path/to/machines/dna113p-machines-0.3.
|
|
182
|
+
npm install /absolute/path/to/machines/dna113p-machines-0.3.6.tgz
|
|
183
183
|
npx machine list
|
|
184
184
|
```
|
|
185
185
|
|
|
@@ -205,6 +205,7 @@ machine runs --for [<session>] [--project <dir>] [--json] [--all] # the runs o
|
|
|
205
205
|
machine respond <run-id> <request-id> <response> # answer a waiting run
|
|
206
206
|
machine watch [<run-id-or-label-start> | --dir <path>] # show other runs on this session's status line
|
|
207
207
|
machine unwatch <run-id-or-label-start> | --dir <path> | --all
|
|
208
|
+
machine ui [--port <port>] [--project <dir>] # a live page of every run, with its Machine drawn as a graph
|
|
208
209
|
```
|
|
209
210
|
|
|
210
211
|
`machine runs` shows each waiting run's prompt, request id, and choices. A run
|
|
@@ -220,6 +221,10 @@ such as a ticket a daemon runs. See
|
|
|
220
221
|
runs one session shows, so any application can
|
|
221
222
|
[draw its own widgets](docs/integrations.md#drawing-your-own-widgets); `watch`
|
|
222
223
|
and `unwatch` take `--for <session>` first to manage that session's list.
|
|
224
|
+
`machine ui` serves a local page of every published run, drawing each run's
|
|
225
|
+
Machine with its current state and the path it took, answering waiting runs, and
|
|
226
|
+
showing a project's own panels; see
|
|
227
|
+
[watching runs in a browser](docs/integrations.md#watching-runs-in-a-browser).
|
|
223
228
|
|
|
224
229
|
## Execution model and limitations
|
|
225
230
|
|
package/claude/statusline.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import { pathToFileURL } from "node:url";
|
|
|
16
16
|
const terminalVisibleMilliseconds = 30_000;
|
|
17
17
|
const maximumRuns = 5;
|
|
18
18
|
const defaultColumns = 120;
|
|
19
|
+
// oxlint-disable-next-line no-control-regex -- the escape character is the point: this strips terminal styling.
|
|
19
20
|
const ansiSequence = /\x1b\[[0-9;]*m/uy;
|
|
20
21
|
// Combining marks, format characters such as joiners, and Hangul medial and
|
|
21
22
|
// final jamo are drawn on the character before them.
|
|
@@ -196,6 +197,7 @@ function formatElapsed(startedAt, endedAt) {
|
|
|
196
197
|
|
|
197
198
|
/** Recorded text must not be able to move the cursor or restyle the status line. */
|
|
198
199
|
function plain(text) {
|
|
200
|
+
// oxlint-disable-next-line no-control-regex -- control characters are what a status line must not carry.
|
|
199
201
|
return text.replace(/[\u0000-\u001f\u007f-\u009f]+/gu, " ").trim();
|
|
200
202
|
}
|
|
201
203
|
|
package/dist/mcp/server.js
CHANGED
|
@@ -10,7 +10,7 @@ import { isJsonValue } from "../src/json.js";
|
|
|
10
10
|
import { MachineSession as RunSession } from "../src/session.js";
|
|
11
11
|
import { listAgentPresets, listMachines } from "../src/launcher.js";
|
|
12
12
|
import { watchRunInbox } from "../src/run-inbox.js";
|
|
13
|
-
import { createRunStatusPublisher } from "../src/run-status.js";
|
|
13
|
+
import { createRunStatusPublisher, runOutputSummary } from "../src/run-status.js";
|
|
14
14
|
import { machineCoordinatorGuidelines, machineDiscoveryDescription, machineSupervisionGuidelines } from "../src/coordinator-guidance.js";
|
|
15
15
|
const widgetUri = "ui://machines/run-v1.html";
|
|
16
16
|
const cwd = v.pipe(v.string(), v.nonEmpty("cwd must be a non-empty absolute path"), v.check(isAbsolute, "cwd must be an absolute path"));
|
|
@@ -219,7 +219,7 @@ export class MachineSession {
|
|
|
219
219
|
});
|
|
220
220
|
return;
|
|
221
221
|
}
|
|
222
|
-
publisher.finish(run.status === "completed" ? { state: run.state } : { error: run.error ?? "Machine run failed" });
|
|
222
|
+
publisher.finish(run.status === "completed" ? { state: run.state, note: runOutputSummary(run.output) } : { error: run.error ?? "Machine run failed" });
|
|
223
223
|
this.#published.delete(run.id);
|
|
224
224
|
}
|
|
225
225
|
/** A published request can also be answered, or asked about, through the run's inbox by `machine respond`. */
|
package/dist/src/cli.js
CHANGED
|
@@ -10,15 +10,17 @@ import { assertJsonValue, isJsonValue } from "./json.js";
|
|
|
10
10
|
import { discoverMachines, machinesUserHome } from "./discovery.js";
|
|
11
11
|
import { listAgentPresets, listMachines, prepareMachineRun, } from "./launcher.js";
|
|
12
12
|
import { inboxHuman, respondToRun } from "./run-inbox.js";
|
|
13
|
-
import { createRunStatusPublisher, isProcessAlive, readRunStatuses, runSessionId, runStatusDirectory, runStatusObserver, } from "./run-status.js";
|
|
13
|
+
import { createRunStatusPublisher, isProcessAlive, readRunStatuses, runOutputSummary, runSessionId, runStatusDirectory, runStatusObserver, } from "./run-status.js";
|
|
14
14
|
import { readRunWatches, runWatch, runWatchMatches, runWatchPath, sameRunWatch, writeRunWatches, } from "./run-watch.js";
|
|
15
15
|
import { runsForViewer } from "./run-viewer.js";
|
|
16
|
+
import { startRunUi, uiProjectDir } from "./run-ui.js";
|
|
16
17
|
const [command, selector, ...argumentsAfterSelector] = process.argv.slice(2);
|
|
17
18
|
const runsOptions = selector === undefined ? [] : [selector, ...argumentsAfterSelector];
|
|
18
19
|
const watchUsage = "machine watch [<run-id-or-label-start> | --dir <path>]";
|
|
19
20
|
const unwatchUsage = "machine unwatch <run-id-or-label-start> | --dir <path> | --all";
|
|
20
21
|
const watchForUsage = "machine watch --for <session> [<run-id-or-label-start> | --dir <path>]";
|
|
21
22
|
const unwatchForUsage = "machine unwatch --for <session> <run-id-or-label-start> | --dir <path> | --all";
|
|
23
|
+
const uiUsage = "machine ui [--port <port>] [--project <dir>]";
|
|
22
24
|
try {
|
|
23
25
|
const listing = command === "runs" ? parseRunsOptions(runsOptions) : undefined;
|
|
24
26
|
if (command === "list" && selector === undefined) {
|
|
@@ -62,6 +64,9 @@ try {
|
|
|
62
64
|
else if (command === "unwatch" && selector !== undefined) {
|
|
63
65
|
unwatch([selector, ...argumentsAfterSelector]);
|
|
64
66
|
}
|
|
67
|
+
else if (command === "ui") {
|
|
68
|
+
await ui(selector === undefined ? [] : [selector, ...argumentsAfterSelector]);
|
|
69
|
+
}
|
|
65
70
|
else {
|
|
66
71
|
throw new Error([
|
|
67
72
|
"Usage:",
|
|
@@ -76,6 +81,7 @@ try {
|
|
|
76
81
|
` ${unwatchUsage}`,
|
|
77
82
|
` ${watchForUsage}`,
|
|
78
83
|
` ${unwatchForUsage}`,
|
|
84
|
+
` ${uiUsage}`,
|
|
79
85
|
].join("\n"));
|
|
80
86
|
}
|
|
81
87
|
}
|
|
@@ -105,7 +111,13 @@ async function runMachine(machinePath, args) {
|
|
|
105
111
|
});
|
|
106
112
|
const status = createRunStatus(machinePath);
|
|
107
113
|
const runId = randomUUID();
|
|
108
|
-
const published = createRunStatusPublisher({
|
|
114
|
+
const published = createRunStatusPublisher({
|
|
115
|
+
id: runId,
|
|
116
|
+
machine: prepared.name,
|
|
117
|
+
path: prepared.path,
|
|
118
|
+
cwd: process.cwd(),
|
|
119
|
+
...(prepared.graph === undefined ? {} : { graph: prepared.graph }),
|
|
120
|
+
});
|
|
109
121
|
const statusDirectory = published.directory;
|
|
110
122
|
// Machines started by this run's Operations and Agents are its sub-runs.
|
|
111
123
|
process.env.MACHINES_RUN_PARENT = runId;
|
|
@@ -140,7 +152,7 @@ async function runMachine(machinePath, args) {
|
|
|
140
152
|
status.onState(state);
|
|
141
153
|
},
|
|
142
154
|
});
|
|
143
|
-
published.finish({ state: result.value });
|
|
155
|
+
published.finish({ state: result.value, note: runOutputSummary(result.output) });
|
|
144
156
|
status.succeed();
|
|
145
157
|
console.log(`--> ${String(result.value)}`);
|
|
146
158
|
if (result.output !== undefined) {
|
|
@@ -320,8 +332,12 @@ function printWatchedRuns(watched, runs) {
|
|
|
320
332
|
const matched = runs.filter((record) => runWatchMatches(watched, record));
|
|
321
333
|
if (matched.length === 0)
|
|
322
334
|
console.log(" no published run matches yet");
|
|
323
|
-
for (const record of matched)
|
|
335
|
+
for (const record of matched) {
|
|
324
336
|
console.log(` ${runColumns(record).join("\t")}`);
|
|
337
|
+
const note = finishedRunNote(record);
|
|
338
|
+
if (note !== undefined)
|
|
339
|
+
console.log(` ${note}`);
|
|
340
|
+
}
|
|
325
341
|
}
|
|
326
342
|
/** The session whose watch list is read or changed, this process's own unless one is named, and where the list is kept. */
|
|
327
343
|
function requireWatchList(named) {
|
|
@@ -351,6 +367,35 @@ async function respond(args) {
|
|
|
351
367
|
});
|
|
352
368
|
console.log(`Machine run ${runId} took the ${question ? "question" : "response"}.`);
|
|
353
369
|
}
|
|
370
|
+
/** Serves the live page of published runs until interrupted. */
|
|
371
|
+
async function ui(args) {
|
|
372
|
+
let port;
|
|
373
|
+
let project;
|
|
374
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
375
|
+
const argument = args[index];
|
|
376
|
+
const value = args[index + 1];
|
|
377
|
+
if (argument === "--port" && value !== undefined && /^\d+$/u.test(value) && Number(value) <= 65_535)
|
|
378
|
+
port = Number(value);
|
|
379
|
+
else if (argument === "--project" && value !== undefined)
|
|
380
|
+
project = resolve(value);
|
|
381
|
+
else
|
|
382
|
+
throw new Error(`Usage: ${uiUsage}`);
|
|
383
|
+
index += 1;
|
|
384
|
+
}
|
|
385
|
+
const page = await startRunUi({
|
|
386
|
+
directory: requireStatusDirectory(),
|
|
387
|
+
projectDir: project ?? uiProjectDir(process.cwd()),
|
|
388
|
+
...(port === undefined ? {} : { port }),
|
|
389
|
+
});
|
|
390
|
+
console.log(`Machines UI: ${page.url}`);
|
|
391
|
+
console.log("Press Ctrl-C to stop.");
|
|
392
|
+
await new Promise((resolveStop) => {
|
|
393
|
+
const stop = () => resolveStop();
|
|
394
|
+
process.once("SIGINT", stop);
|
|
395
|
+
process.once("SIGTERM", stop);
|
|
396
|
+
});
|
|
397
|
+
await page.close();
|
|
398
|
+
}
|
|
354
399
|
function requireStatusDirectory() {
|
|
355
400
|
const directory = runStatusDirectory();
|
|
356
401
|
if (directory === undefined) {
|
|
@@ -465,6 +510,7 @@ function printAgentListing(preset, separate) {
|
|
|
465
510
|
}
|
|
466
511
|
/** Recorded text must not be able to move the cursor or restyle the terminal. */
|
|
467
512
|
function plain(text) {
|
|
513
|
+
// oxlint-disable-next-line no-control-regex -- control characters are what this removes.
|
|
468
514
|
return text.replace(/[\u0000-\u001f\u007f-\u009f]+/gu, " ").trim();
|
|
469
515
|
}
|
|
470
516
|
/** A run as tab-separated columns: its label follows the four that were always there. */
|
|
@@ -472,6 +518,11 @@ function runColumns(record) {
|
|
|
472
518
|
const label = record.label === undefined ? "" : plain(record.label);
|
|
473
519
|
return [record.id, record.machine, runStatus(record), record.state ?? "starting"].map(plain).concat(label === "" ? [] : [label]);
|
|
474
520
|
}
|
|
521
|
+
function finishedRunNote(record) {
|
|
522
|
+
if (record.status !== "completed" && record.status !== "failed")
|
|
523
|
+
return undefined;
|
|
524
|
+
return record.note?.split(/\r?\n/u).map(plain).find((line) => line !== "");
|
|
525
|
+
}
|
|
475
526
|
function printRunListing(record, separate, watched) {
|
|
476
527
|
const state = record.state ?? "starting";
|
|
477
528
|
const recorded = runStatus(record);
|
|
@@ -486,6 +537,9 @@ function printRunListing(record, separate, watched) {
|
|
|
486
537
|
...(human.suggestions === undefined ? [] : [`suggestions: ${human.suggestions.map(plain).join(", ")}`]),
|
|
487
538
|
...(human.discussion === true ? ["discussion: questions allowed"] : []),
|
|
488
539
|
];
|
|
540
|
+
const note = finishedRunNote(record);
|
|
541
|
+
if (note !== undefined)
|
|
542
|
+
details.push(note);
|
|
489
543
|
if (!process.stdout.isTTY) {
|
|
490
544
|
const columns = runColumns(record);
|
|
491
545
|
// The mark has its own column, after the label's, whether or not the run has a label.
|
package/dist/src/codex.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export interface CodexAgentOptions {
|
|
|
5
5
|
readonly model?: string;
|
|
6
6
|
/** A reasoning effort supported by the selected Codex model. */
|
|
7
7
|
readonly effort?: string;
|
|
8
|
+
/** Codex service tier, e.g. "fast" or "ultrafast"; omitted values inherit the CLI configuration. */
|
|
9
|
+
readonly serviceTier?: string;
|
|
8
10
|
/** Defaults to read-only; writes and unrestricted access require an explicit choice. */
|
|
9
11
|
readonly sandbox?: "read-only" | "workspace-write" | "danger-full-access";
|
|
10
12
|
readonly skipGitRepoCheck?: boolean;
|
package/dist/src/codex.js
CHANGED
|
@@ -17,6 +17,7 @@ export function codexAgent(command = "codex", args = [], options = {}) {
|
|
|
17
17
|
const env = { ...process.env, ...options.env };
|
|
18
18
|
const model = options.model ?? env.CODEX_MODEL ?? env.MACHINES_AGENT_MODEL;
|
|
19
19
|
const effort = options.effort ?? env.CODEX_EFFORT ?? env.MACHINES_AGENT_EFFORT;
|
|
20
|
+
const serviceTier = options.serviceTier ?? env.CODEX_SERVICE_TIER;
|
|
20
21
|
const outputMode = options.output ?? "stream";
|
|
21
22
|
report?.({
|
|
22
23
|
type: "identity",
|
|
@@ -37,6 +38,7 @@ export function codexAgent(command = "codex", args = [], options = {}) {
|
|
|
37
38
|
...(options.ephemeral === false ? [] : ["--ephemeral"]),
|
|
38
39
|
...(model === undefined ? [] : ["--model", model]),
|
|
39
40
|
...(effort === undefined ? [] : ["--config", `model_reasoning_effort=${JSON.stringify(effort)}`]),
|
|
41
|
+
...(serviceTier === undefined ? [] : ["--config", `service_tier=${JSON.stringify(serviceTier)}`]),
|
|
40
42
|
"--output-last-message", finalPath, "-",
|
|
41
43
|
], { cwd: request.cwd, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
42
44
|
const exited = new Promise((resolve, reject) => {
|
package/dist/src/host.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type JsonValue } from "./json.ts";
|
|
2
2
|
import type { StateValue } from "xstate";
|
|
3
3
|
import type { AgentUpdate, HumanRequest } from "./index.ts";
|
|
4
|
+
import type { RunGraph } from "./run-graph.ts";
|
|
4
5
|
import { type PrepareMachineRunOptions } from "./launcher.ts";
|
|
5
6
|
export interface HostedHumanRequest extends HumanRequest {
|
|
6
7
|
readonly requestId: string;
|
|
@@ -33,6 +34,11 @@ export interface MachineHostRun {
|
|
|
33
34
|
readonly description: string;
|
|
34
35
|
readonly path: string;
|
|
35
36
|
readonly agentRoles: Readonly<Record<string, string>>;
|
|
37
|
+
/**
|
|
38
|
+
* The Machine's states and transitions, as its published run status carries
|
|
39
|
+
* them, whether or not this run is published. Absent for a Machine too large to draw.
|
|
40
|
+
*/
|
|
41
|
+
readonly graph?: RunGraph;
|
|
36
42
|
readonly result: Promise<MachineHostResult>;
|
|
37
43
|
respond(response: string, requestId: string): Promise<void>;
|
|
38
44
|
ask(question: string, requestId: string): Promise<void>;
|
package/dist/src/host.js
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import * as v from "valibot";
|
|
6
6
|
import { assertJsonValue, isJsonValue } from "./json.js";
|
|
7
7
|
import { prepareMachineRun, } from "./launcher.js";
|
|
8
|
-
import { createRunStatusPublisher, runStatusDirectory, runStatusObserver, } from "./run-status.js";
|
|
8
|
+
import { createRunStatusPublisher, runOutputSummary, runStatusDirectory, runStatusObserver, } from "./run-status.js";
|
|
9
9
|
const childArgument = "--machines-child-host";
|
|
10
10
|
const humanRequestSchema = v.strictObject({
|
|
11
11
|
discussion: v.optional(v.boolean()),
|
|
@@ -74,6 +74,8 @@ const childMessageSchema = v.variant("type", [
|
|
|
74
74
|
description: v.string(),
|
|
75
75
|
path: v.string(),
|
|
76
76
|
agentRoles: v.record(v.string(), v.string()),
|
|
77
|
+
// Checked as the record is read back; here it only has to be an object.
|
|
78
|
+
graph: v.optional(v.custom((value) => value !== null && typeof value === "object")),
|
|
77
79
|
}),
|
|
78
80
|
v.strictObject({ type: v.literal("state"), state: stateValueSchema }),
|
|
79
81
|
v.strictObject({ type: v.literal("agentUpdate"), update: agentUpdateSchema }),
|
|
@@ -193,6 +195,7 @@ export function startMachineHost(options) {
|
|
|
193
195
|
machine: message.name,
|
|
194
196
|
path: message.path,
|
|
195
197
|
cwd: resolvePath(cwd),
|
|
198
|
+
...(message.graph === undefined ? {} : { graph: message.graph }),
|
|
196
199
|
...(options.label === undefined ? {} : { label: options.label }),
|
|
197
200
|
...(options.owner === undefined ? {} : { owner: options.owner }),
|
|
198
201
|
// Responses reach this run through `respond` and `ask` below, never through an inbox.
|
|
@@ -205,6 +208,7 @@ export function startMachineHost(options) {
|
|
|
205
208
|
description: message.description,
|
|
206
209
|
path: message.path,
|
|
207
210
|
agentRoles: message.agentRoles,
|
|
211
|
+
...(message.graph === undefined ? {} : { graph: message.graph }),
|
|
208
212
|
result,
|
|
209
213
|
async respond(response, requestId) {
|
|
210
214
|
if (phase !== "running" || pendingHuman === undefined) {
|
|
@@ -290,7 +294,7 @@ export function startMachineHost(options) {
|
|
|
290
294
|
}
|
|
291
295
|
phase = "completed";
|
|
292
296
|
pendingHuman = undefined;
|
|
293
|
-
published?.finish({ state: message.state });
|
|
297
|
+
published?.finish({ state: message.state, note: runOutputSummary(message.output) });
|
|
294
298
|
dispose();
|
|
295
299
|
resolveResult({
|
|
296
300
|
state: message.state,
|
|
@@ -401,6 +405,7 @@ function runChildHost() {
|
|
|
401
405
|
description: prepared.description,
|
|
402
406
|
path: prepared.path,
|
|
403
407
|
agentRoles: prepared.agentRoles,
|
|
408
|
+
...(prepared.graph === undefined ? {} : { graph: prepared.graph }),
|
|
404
409
|
});
|
|
405
410
|
// Machines started by this run's Operations and Agents are its sub-runs.
|
|
406
411
|
if (runId !== undefined)
|
package/dist/src/index.d.ts
CHANGED
|
@@ -79,6 +79,7 @@ export type AgentUpdate = {
|
|
|
79
79
|
export type AgentReporter = (update: AgentUpdate) => void;
|
|
80
80
|
export type { JsonValue } from "./json.ts";
|
|
81
81
|
export { AgentLimitError, agentLimitOf, type AgentLimit } from "./agent-limit.ts";
|
|
82
|
+
export { machineGraph, statePaths, type RunGraph, type RunGraphEdge, type RunGraphState, type RunTrailStep, } from "./run-graph.ts";
|
|
82
83
|
export type AgentRunner = (request: AgentRequest, report?: AgentReporter) => Event | Promise<Event>;
|
|
83
84
|
export interface RunOptions {
|
|
84
85
|
readonly agents?: Readonly<Record<string, AgentRunner>>;
|
package/dist/src/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { agentLimitOf } from "./agent-limit.js";
|
|
|
2
2
|
import { terminalHuman } from "./terminal-human.js";
|
|
3
3
|
import { createActor, fromPromise, raise, setup, } from "xstate";
|
|
4
4
|
export { AgentLimitError, agentLimitOf } from "./agent-limit.js";
|
|
5
|
+
export { machineGraph, statePaths, } from "./run-graph.js";
|
|
5
6
|
const machines = setup({
|
|
6
7
|
actions: {
|
|
7
8
|
humanInputFinished: () => { },
|
package/dist/src/launcher.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type JsonValue } from "./json.ts";
|
|
|
3
3
|
import { type LauncherLocation, type MachineSummary, type AgentPresetSummary } from "./catalog.ts";
|
|
4
4
|
export type { LauncherLocation, MachineSummary, AgentPresetSummary } from "./catalog.ts";
|
|
5
5
|
import { type RunOptions } from "./index.ts";
|
|
6
|
+
import { type RunGraph } from "./run-graph.ts";
|
|
6
7
|
export interface PrepareMachineRunOptions extends LauncherLocation {
|
|
7
8
|
readonly machine: string;
|
|
8
9
|
readonly input?: JsonValue;
|
|
@@ -22,6 +23,8 @@ export interface PreparedMachineRun {
|
|
|
22
23
|
readonly description: string;
|
|
23
24
|
readonly path: string;
|
|
24
25
|
readonly agentRoles: Readonly<Record<string, string>>;
|
|
26
|
+
/** The definition's states and transitions, for run status; undefined when it is too large to draw. */
|
|
27
|
+
readonly graph?: RunGraph;
|
|
25
28
|
start(options?: Omit<RunOptions, "agents">): Promise<SnapshotFrom<AnyStateMachine>>;
|
|
26
29
|
}
|
|
27
30
|
export declare function listMachines(location?: LauncherLocation): Promise<readonly MachineSummary[]>;
|
package/dist/src/launcher.js
CHANGED
|
@@ -7,6 +7,7 @@ import { queryCatalog } from "./discovery-worker.js";
|
|
|
7
7
|
import { loadMachineModule, readAgentRoles, requireMachineDescription } from "./machine-module.js";
|
|
8
8
|
import { discoverMachines, machinesUserHome } from "./discovery.js";
|
|
9
9
|
import { agent, final, human, machine, operation, requiredAgentNames, run, } from "./index.js";
|
|
10
|
+
import { machineGraph } from "./run-graph.js";
|
|
10
11
|
export async function listMachines(location = {}) {
|
|
11
12
|
return queryCatalog("machines", location);
|
|
12
13
|
}
|
|
@@ -34,11 +35,13 @@ export async function prepareMachineRun(options) {
|
|
|
34
35
|
preflightAgentRoles(path, usedAgentRoles, agentRoles, agents);
|
|
35
36
|
const limitAware = limitAwareAgents(requiredRoles, overrides, configured, limitPolicy(options.agentLimits));
|
|
36
37
|
let started = false;
|
|
38
|
+
const graph = machineGraph(definition);
|
|
37
39
|
return {
|
|
38
40
|
name: machineName(path),
|
|
39
41
|
description,
|
|
40
42
|
path,
|
|
41
43
|
agentRoles,
|
|
44
|
+
...(graph === undefined ? {} : { graph }),
|
|
42
45
|
start(runOptions = {}) {
|
|
43
46
|
if (started)
|
|
44
47
|
throw new Error(`Prepared Machine "${path}" has already started`);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { AnyStateMachine } from "xstate";
|
|
2
|
+
/**
|
|
3
|
+
* A Machine's states and transitions as data, for a surface that draws them.
|
|
4
|
+
* Ids are state paths joined by dots, with the root left out: `plan`, or
|
|
5
|
+
* `review.round` for a state nested in `review`.
|
|
6
|
+
*/
|
|
7
|
+
export interface RunGraph {
|
|
8
|
+
readonly initial?: string;
|
|
9
|
+
readonly states: readonly RunGraphState[];
|
|
10
|
+
readonly edges: readonly RunGraphEdge[];
|
|
11
|
+
}
|
|
12
|
+
export interface RunGraphState {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
/** The compound state this one is nested in. */
|
|
15
|
+
readonly parent?: string;
|
|
16
|
+
/** What runs in the state. `group` holds nested states; `state` runs nothing itself. */
|
|
17
|
+
readonly kind: "agent" | "human" | "operation" | "final" | "group" | "state";
|
|
18
|
+
/** The Agent role an Agent state runs. */
|
|
19
|
+
readonly role?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface RunGraphEdge {
|
|
22
|
+
readonly from: string;
|
|
23
|
+
readonly to: string;
|
|
24
|
+
/** The event the transition takes: an outcome, `done` when a group finishes, or `always`/`after`. */
|
|
25
|
+
readonly event: string;
|
|
26
|
+
}
|
|
27
|
+
/** One state the run entered, and when. */
|
|
28
|
+
export interface RunTrailStep {
|
|
29
|
+
readonly state: string;
|
|
30
|
+
readonly at: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Reads the graph out of a Machine definition, or returns undefined for one too
|
|
34
|
+
* large to draw or one whose shape this cannot read. Never throws: a picture of
|
|
35
|
+
* the workflow is never worth the run.
|
|
36
|
+
*/
|
|
37
|
+
export declare function machineGraph(definition: AnyStateMachine): RunGraph | undefined;
|
|
38
|
+
/** A state value as the dotted paths of its active leaves: `{"review":"round"}` is `review.round`. */
|
|
39
|
+
export declare function statePaths(value: unknown): string[];
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** Past this a Machine is not drawn: its record would grow past what a reader can take in at once. */
|
|
2
|
+
const maxStates = 300;
|
|
3
|
+
const maxEdges = 1_000;
|
|
4
|
+
/**
|
|
5
|
+
* Reads the graph out of a Machine definition, or returns undefined for one too
|
|
6
|
+
* large to draw or one whose shape this cannot read. Never throws: a picture of
|
|
7
|
+
* the workflow is never worth the run.
|
|
8
|
+
*/
|
|
9
|
+
export function machineGraph(definition) {
|
|
10
|
+
try {
|
|
11
|
+
const root = definition.root;
|
|
12
|
+
const states = [];
|
|
13
|
+
const edges = [];
|
|
14
|
+
const id = (node) => node.path.join(".");
|
|
15
|
+
const visit = (node, parent) => {
|
|
16
|
+
const using = node.meta?.["machines.agent"]?.using;
|
|
17
|
+
const src = node.invoke[0]?.src;
|
|
18
|
+
const kind = node.type === "final" ? "final"
|
|
19
|
+
: node.type === "compound" || node.type === "parallel" ? "group"
|
|
20
|
+
: typeof using === "string" ? "agent"
|
|
21
|
+
: src === "human" ? "human"
|
|
22
|
+
: src !== undefined ? "operation"
|
|
23
|
+
: "state";
|
|
24
|
+
states.push({
|
|
25
|
+
id: id(node),
|
|
26
|
+
...(parent === undefined ? {} : { parent }),
|
|
27
|
+
kind,
|
|
28
|
+
...(kind === "agent" ? { role: using } : {}),
|
|
29
|
+
});
|
|
30
|
+
const add = (event, targets) => {
|
|
31
|
+
for (const target of targets ?? []) {
|
|
32
|
+
// A target outside the drawn states is the root itself, which nothing draws.
|
|
33
|
+
if (target.path.length > 0)
|
|
34
|
+
edges.push({ from: id(node), to: id(target), event });
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
for (const [event, definitions] of node.transitions) {
|
|
38
|
+
// The runtime's own actor events only run actions; the outcome they raise is the transition.
|
|
39
|
+
// A plain XState state can target from them directly, through `onDone` and `onError`.
|
|
40
|
+
const label = event.startsWith("xstate.done.state.") || event.startsWith("xstate.done.actor.") ? "done"
|
|
41
|
+
: event.startsWith("xstate.error.actor.") ? "error"
|
|
42
|
+
: event.startsWith("xstate.after.") ? "after"
|
|
43
|
+
: event;
|
|
44
|
+
for (const transition of definitions)
|
|
45
|
+
add(label, transition.target);
|
|
46
|
+
}
|
|
47
|
+
for (const transition of node.always ?? [])
|
|
48
|
+
add("always", transition.target);
|
|
49
|
+
for (const child of Object.values(node.states))
|
|
50
|
+
visit(child, kind === "group" ? id(node) : parent);
|
|
51
|
+
};
|
|
52
|
+
for (const child of Object.values(root.states))
|
|
53
|
+
visit(child, undefined);
|
|
54
|
+
if (states.length === 0 || states.length > maxStates || edges.length > maxEdges)
|
|
55
|
+
return undefined;
|
|
56
|
+
const initial = root.initial?.target?.[0];
|
|
57
|
+
return { ...(initial === undefined ? {} : { initial: id(initial) }), states, edges: dedupe(edges) };
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** A state value as the dotted paths of its active leaves: `{"review":"round"}` is `review.round`. */
|
|
64
|
+
export function statePaths(value) {
|
|
65
|
+
if (typeof value === "string") {
|
|
66
|
+
if (!value.startsWith("{"))
|
|
67
|
+
return [value];
|
|
68
|
+
try {
|
|
69
|
+
return statePaths(JSON.parse(value));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return [value];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (value === null || typeof value !== "object")
|
|
76
|
+
return [];
|
|
77
|
+
return Object.entries(value).flatMap(([key, child]) => {
|
|
78
|
+
const below = statePaths(child);
|
|
79
|
+
return below.length === 0 ? [key] : below.map((path) => `${key}.${path}`);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function dedupe(edges) {
|
|
83
|
+
const seen = new Set();
|
|
84
|
+
return edges.filter((edge) => {
|
|
85
|
+
const key = `${edge.from}\0${edge.to}\0${edge.event}`;
|
|
86
|
+
if (seen.has(key))
|
|
87
|
+
return false;
|
|
88
|
+
seen.add(key);
|
|
89
|
+
return true;
|
|
90
|
+
});
|
|
91
|
+
}
|
package/dist/src/run-status.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { StateValue } from "xstate";
|
|
2
2
|
import type { AgentUpdate } from "./index.ts";
|
|
3
|
+
import { type RunGraph, type RunTrailStep } from "./run-graph.ts";
|
|
3
4
|
/** One published run, stored as `<id>.json` in the run-status directory. */
|
|
4
5
|
export interface RunStatusRecord {
|
|
5
6
|
/** Version 1 records carry only `human.prompt`. */
|
|
@@ -30,6 +31,12 @@ export interface RunStatusRecord {
|
|
|
30
31
|
/** Names the inbox responses the run took lately, so that each sender can tell; never a response. */
|
|
31
32
|
readonly deliveries?: readonly string[];
|
|
32
33
|
readonly error?: string;
|
|
34
|
+
/** The finished run's summary, at most 2000 characters including any ellipsis. */
|
|
35
|
+
readonly note?: string;
|
|
36
|
+
/** The Machine's states and transitions, when it is small enough to draw. */
|
|
37
|
+
readonly graph?: RunGraph;
|
|
38
|
+
/** The states the run entered, oldest first: the latest `trailLimit` of them. */
|
|
39
|
+
readonly trail?: readonly RunTrailStep[];
|
|
33
40
|
readonly startedAt: string;
|
|
34
41
|
readonly updatedAt: string;
|
|
35
42
|
}
|
|
@@ -63,6 +70,7 @@ export interface RunStatusRun {
|
|
|
63
70
|
/** Replaces `MACHINES_RUN_LABEL`, and is shortened as it is. */
|
|
64
71
|
readonly label?: string;
|
|
65
72
|
readonly answeredBy?: "host";
|
|
73
|
+
readonly graph?: RunGraph;
|
|
66
74
|
}
|
|
67
75
|
/** A present key replaces its field; a present key holding `undefined` clears it. */
|
|
68
76
|
export interface RunStatusChange {
|
|
@@ -77,6 +85,7 @@ export interface RunStatusChange {
|
|
|
77
85
|
}
|
|
78
86
|
export type RunStatusOutcome = {
|
|
79
87
|
readonly state?: StateValue;
|
|
88
|
+
readonly note?: string;
|
|
80
89
|
} | {
|
|
81
90
|
readonly error: string;
|
|
82
91
|
};
|
|
@@ -89,6 +98,8 @@ export interface RunStatusPublisher {
|
|
|
89
98
|
update(change: RunStatusChange): void;
|
|
90
99
|
finish(outcome: RunStatusOutcome): void;
|
|
91
100
|
}
|
|
101
|
+
/** How many entered states a record keeps: enough for a long run's repairs, small enough to rewrite at every step. */
|
|
102
|
+
export declare const trailLimit = 200;
|
|
92
103
|
/** How long a record goes on naming a response its run took: longer than a sender waits to hear of it. */
|
|
93
104
|
export declare const deliveryRetentionMilliseconds = 60000;
|
|
94
105
|
/** How long a session's watch list is kept after its last change: sessions end without saying so. */
|
|
@@ -115,8 +126,9 @@ export declare function runDeliveryPath(directory: string, id: string, delivery:
|
|
|
115
126
|
/**
|
|
116
127
|
* Publishes one run for hosts that can only read files, such as a status line,
|
|
117
128
|
* in `runStatusDirectory`. Publication is presentation: it never throws, and it
|
|
118
|
-
* records
|
|
119
|
-
* written, as in a read-only home, is not
|
|
129
|
+
* records no Machine input and only the supplied summary of its output.
|
|
130
|
+
* A run whose first record cannot be written, as in a read-only home, is not
|
|
131
|
+
* published at all.
|
|
120
132
|
*/
|
|
121
133
|
export declare function createRunStatusPublisher(run: RunStatusRun, environment?: NodeJS.ProcessEnv): RunStatusPublisher;
|
|
122
134
|
/**
|
|
@@ -139,3 +151,5 @@ export declare function readRunStatuses(directory: string): RunStatusRecord[];
|
|
|
139
151
|
/** The id becomes a file name, so it must not be able to leave the directory. */
|
|
140
152
|
export declare function isFileSafeId(id: string): boolean;
|
|
141
153
|
export declare function isProcessAlive(pid: number): boolean;
|
|
154
|
+
/** Only an object's non-empty summary is published from a Machine's output. */
|
|
155
|
+
export declare function runOutputSummary(output: unknown): string | undefined;
|