@dna113p/machines 0.2.0 → 0.3.1
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 +115 -11
- package/claude/statusline.mjs +277 -0
- package/dist/examples/chatgpt-web.d.ts +1 -0
- package/dist/examples/chatgpt-web.js +36 -0
- package/dist/examples/claude.d.ts +1 -0
- package/dist/examples/claude.js +30 -0
- package/dist/examples/codex.d.ts +1 -0
- package/dist/examples/codex.js +30 -0
- package/dist/examples/decision.d.ts +1 -0
- package/dist/examples/decision.js +66 -0
- package/dist/examples/deepseek.d.ts +1 -0
- package/dist/examples/deepseek.js +27 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +74 -11
- package/dist/pi-extension/index.js +16 -9
- package/dist/src/acp.js +2 -1
- package/dist/src/agent-bindings.d.ts +6 -0
- package/dist/src/agent-bindings.js +35 -5
- package/dist/src/agent-limit.d.ts +25 -0
- package/dist/src/agent-limit.js +34 -0
- package/dist/src/agent-limits.d.ts +37 -0
- package/dist/src/agent-limits.js +212 -0
- package/dist/src/agent-protocol.js +3 -1
- package/dist/src/catalog.d.ts +3 -0
- package/dist/src/catalog.js +84 -12
- package/dist/src/chatgpt-web.d.ts +143 -0
- package/dist/src/chatgpt-web.js +946 -0
- package/dist/src/claude.d.ts +35 -0
- package/dist/src/claude.js +294 -0
- package/dist/src/cli.js +282 -16
- package/dist/src/codex.d.ts +16 -0
- package/dist/src/codex.js +176 -0
- package/dist/src/conversation.d.ts +74 -0
- package/dist/src/conversation.js +46 -0
- package/dist/src/coordinator-guidance.d.ts +3 -0
- package/dist/src/coordinator-guidance.js +17 -0
- package/dist/src/decision.d.ts +29 -0
- package/dist/src/decision.js +67 -0
- package/dist/src/deepseek.d.ts +12 -0
- package/dist/src/deepseek.js +16 -0
- package/dist/src/host.d.ts +11 -0
- package/dist/src/host.js +153 -16
- package/dist/src/index.d.ts +39 -2
- package/dist/src/index.js +62 -5
- package/dist/src/jev.d.ts +15 -0
- package/dist/src/jev.js +90 -0
- package/dist/src/json.d.ts +6 -0
- package/dist/src/json.js +28 -0
- package/dist/src/launcher.d.ts +11 -1
- package/dist/src/launcher.js +72 -3
- package/dist/src/laya.d.ts +15 -0
- package/dist/src/laya.js +13 -0
- package/dist/src/openrouter.d.ts +16 -0
- package/dist/src/openrouter.js +97 -0
- package/dist/src/run-inbox.d.ts +49 -0
- package/dist/src/run-inbox.js +233 -0
- package/dist/src/run-status.d.ts +121 -0
- package/dist/src/run-status.js +336 -0
- package/dist/src/run-watch.d.ts +24 -0
- package/dist/src/run-watch.js +112 -0
- package/dist/src/session.d.ts +15 -2
- package/dist/src/session.js +47 -8
- package/dist/src/system-one-http.d.ts +25 -0
- package/dist/src/system-one-http.js +146 -0
- package/dist/src/terminal-human.d.ts +8 -1
- package/dist/src/terminal-human.js +36 -8
- package/dist/src/von.d.ts +15 -0
- package/dist/src/von.js +13 -0
- package/docs/architecture.md +74 -3
- package/docs/authoring.md +809 -1
- package/docs/integrations.md +322 -3
- package/docs/local-decisions.md +212 -0
- package/package.json +21 -4
- package/services/laya/server.py +230 -0
- package/skills/machine-builder/SKILL.md +56 -6
- package/skills/machine-delegation/SKILL.md +128 -0
- package/skills/machine-delegation/references/delegation-machine.md +90 -0
- package/skills/machine-project-setup/SKILL.md +181 -0
- package/skills/machine-project-setup/references/project-workflows.md +50 -0
package/dist/src/cli.js
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { readFile } from "node:fs/promises";
|
|
3
4
|
import { basename, dirname, isAbsolute, resolve } from "node:path";
|
|
4
5
|
import { emitKeypressEvents } from "node:readline";
|
|
5
|
-
import { stripVTControlCharacters } from "node:util";
|
|
6
|
+
import { inspect, stripVTControlCharacters } from "node:util";
|
|
6
7
|
import yoctoSpinner from "yocto-spinner";
|
|
7
8
|
import { bold, cyan, dim, green, magenta, red, yellow } from "yoctocolors";
|
|
9
|
+
import { assertJsonValue, isJsonValue } from "./json.js";
|
|
8
10
|
import { discoverMachines, machinesUserHome } from "./discovery.js";
|
|
9
11
|
import { listAgentPresets, listMachines, prepareMachineRun, } from "./launcher.js";
|
|
12
|
+
import { inboxHuman, respondToRun } from "./run-inbox.js";
|
|
13
|
+
import { createRunStatusPublisher, isProcessAlive, readRunStatuses, runSessionId, runStatusDirectory, runStatusObserver, } from "./run-status.js";
|
|
14
|
+
import { readRunWatches, runWatch, runWatchMatches, runWatchPath, sameRunWatch, writeRunWatches, } from "./run-watch.js";
|
|
10
15
|
const [command, selector, ...argumentsAfterSelector] = process.argv.slice(2);
|
|
16
|
+
const runsOptions = selector === undefined ? [] : [selector, ...argumentsAfterSelector];
|
|
17
|
+
const watchUsage = "machine watch [<run-id-or-label-start> | --dir <path>]";
|
|
18
|
+
const unwatchUsage = "machine unwatch <run-id-or-label-start> | --dir <path> | --all";
|
|
11
19
|
try {
|
|
12
20
|
if (command === "list" && selector === undefined) {
|
|
13
21
|
const machines = await listMachines({ cwd: process.cwd() });
|
|
@@ -35,13 +43,30 @@ try {
|
|
|
35
43
|
else if (command === "run" && selector !== undefined) {
|
|
36
44
|
await runMachine(await resolveMachine(selector), argumentsAfterSelector);
|
|
37
45
|
}
|
|
46
|
+
else if (command === "runs"
|
|
47
|
+
&& runsOptions.every((option, index) => (option === "--json" || option === "--all") && runsOptions.indexOf(option) === index)) {
|
|
48
|
+
listRuns(runsOptions.includes("--json"), runsOptions.includes("--all"));
|
|
49
|
+
}
|
|
50
|
+
else if (command === "respond" && selector !== undefined) {
|
|
51
|
+
await respond([selector, ...argumentsAfterSelector]);
|
|
52
|
+
}
|
|
53
|
+
else if (command === "watch") {
|
|
54
|
+
watch(selector === undefined ? [] : [selector, ...argumentsAfterSelector]);
|
|
55
|
+
}
|
|
56
|
+
else if (command === "unwatch" && selector !== undefined) {
|
|
57
|
+
unwatch([selector, ...argumentsAfterSelector]);
|
|
58
|
+
}
|
|
38
59
|
else {
|
|
39
60
|
throw new Error([
|
|
40
61
|
"Usage:",
|
|
41
62
|
" machine list",
|
|
42
63
|
" machine agents",
|
|
43
64
|
" machine show <name-or-file>",
|
|
44
|
-
" machine run <name-or-file> [--agent role=preset]... [--] [input]",
|
|
65
|
+
" machine run <name-or-file> [--agent role=preset]... [--input-file <file|->] [--] [input]",
|
|
66
|
+
" machine runs [--json] [--all]",
|
|
67
|
+
" machine respond [--question] <run-id> <request-id> [--] <response>",
|
|
68
|
+
` ${watchUsage}`,
|
|
69
|
+
` ${unwatchUsage}`,
|
|
45
70
|
].join("\n"));
|
|
46
71
|
}
|
|
47
72
|
}
|
|
@@ -61,7 +86,7 @@ async function resolveMachine(selector) {
|
|
|
61
86
|
return found.path;
|
|
62
87
|
}
|
|
63
88
|
async function runMachine(machinePath, args) {
|
|
64
|
-
const invocation = parseRunArguments(args);
|
|
89
|
+
const invocation = await parseRunArguments(args);
|
|
65
90
|
printMachineHeader(machinePath);
|
|
66
91
|
const prepared = await prepareMachineRun({
|
|
67
92
|
cwd: process.cwd(),
|
|
@@ -70,16 +95,51 @@ async function runMachine(machinePath, args) {
|
|
|
70
95
|
agents: Object.fromEntries(invocation.agentOverrides),
|
|
71
96
|
});
|
|
72
97
|
const status = createRunStatus(machinePath);
|
|
98
|
+
const runId = randomUUID();
|
|
99
|
+
const published = createRunStatusPublisher({ id: runId, machine: prepared.name, path: prepared.path, cwd: process.cwd() });
|
|
100
|
+
const statusDirectory = runStatusDirectory();
|
|
101
|
+
// Machines started by this run's Operations and Agents are its sub-runs.
|
|
102
|
+
process.env.MACHINES_RUN_PARENT = runId;
|
|
103
|
+
const observed = runStatusObserver((change) => published.update(change));
|
|
73
104
|
try {
|
|
74
105
|
const result = await prepared.start({
|
|
75
|
-
onAgentUpdate
|
|
76
|
-
|
|
77
|
-
|
|
106
|
+
onAgentUpdate(update) {
|
|
107
|
+
observed.onAgentUpdate(update);
|
|
108
|
+
status.onAgentUpdate(update);
|
|
109
|
+
},
|
|
110
|
+
// A published run can also be answered through its inbox by `machine respond`.
|
|
111
|
+
...(statusDirectory === undefined ? {} : {
|
|
112
|
+
human: inboxHuman({
|
|
113
|
+
directory: statusDirectory,
|
|
114
|
+
runId,
|
|
115
|
+
publish: (request) => published.update({ status: "waiting", human: request }),
|
|
116
|
+
acknowledge: (delivery) => published.update({ delivery }),
|
|
117
|
+
onTerminalRefused: (reason) => console.error(reason),
|
|
118
|
+
onTerminalClosed(requestId) {
|
|
119
|
+
// Finish the unanswered prompt line before saying where the answer can come from.
|
|
120
|
+
process.stdout.write("\n");
|
|
121
|
+
console.error(`No terminal input. Answer with: machine respond ${runId} ${requestId} <response>`);
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
124
|
+
}),
|
|
125
|
+
onHumanInput(active) {
|
|
126
|
+
published.update(active ? { status: "waiting" } : { status: "running", human: undefined });
|
|
127
|
+
status.onHumanInput(active);
|
|
128
|
+
},
|
|
129
|
+
onState(state) {
|
|
130
|
+
observed.onState(state);
|
|
131
|
+
status.onState(state);
|
|
132
|
+
},
|
|
78
133
|
});
|
|
134
|
+
published.finish({ state: result.value });
|
|
79
135
|
status.succeed();
|
|
80
136
|
console.log(`--> ${String(result.value)}`);
|
|
137
|
+
if (result.output !== undefined) {
|
|
138
|
+
console.log(isJsonValue(result.output) ? JSON.stringify(result.output, null, 2) : inspect(result.output));
|
|
139
|
+
}
|
|
81
140
|
}
|
|
82
141
|
catch (cause) {
|
|
142
|
+
published.finish({ error: cause instanceof Error ? cause.message : String(cause) });
|
|
83
143
|
status.fail();
|
|
84
144
|
throw cause;
|
|
85
145
|
}
|
|
@@ -87,7 +147,130 @@ async function runMachine(machinePath, args) {
|
|
|
87
147
|
status.close();
|
|
88
148
|
}
|
|
89
149
|
}
|
|
90
|
-
function
|
|
150
|
+
function listRuns(json, all) {
|
|
151
|
+
const directory = requireStatusDirectory();
|
|
152
|
+
const published = publishedRuns(directory);
|
|
153
|
+
// Without --all, only the runs that can still change or be answered.
|
|
154
|
+
const runs = all ? published : published.filter(isActiveRun);
|
|
155
|
+
if (json) {
|
|
156
|
+
console.log(JSON.stringify(runs, null, 2));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (runs.length === 0)
|
|
160
|
+
console.log(all ? "No Machine runs are published." : "No Machine runs are running or waiting.");
|
|
161
|
+
const session = runSessionId();
|
|
162
|
+
const watches = session === undefined ? [] : readRunWatches(directory, session);
|
|
163
|
+
runs.forEach((record, index) => {
|
|
164
|
+
printRunListing(record, index > 0, watches.some((watched) => runWatchMatches(watched, record)));
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/** Every record in the directory, including runs that finished or lost their process and are still recorded. */
|
|
168
|
+
function publishedRuns(directory) {
|
|
169
|
+
return readRunStatuses(directory).sort((left, right) => Date.parse(left.startedAt) - Date.parse(right.startedAt));
|
|
170
|
+
}
|
|
171
|
+
/** A run that stopped reporting without finishing has lost its process, as on the status line. */
|
|
172
|
+
function runStatus(record) {
|
|
173
|
+
return record.status !== "completed" && record.status !== "failed" && !isProcessAlive(record.pid) ? "lost" : record.status;
|
|
174
|
+
}
|
|
175
|
+
function isActiveRun(record) {
|
|
176
|
+
const status = runStatus(record);
|
|
177
|
+
return status === "running" || status === "waiting";
|
|
178
|
+
}
|
|
179
|
+
function watch(args) {
|
|
180
|
+
const { directory, session } = requireWatchList();
|
|
181
|
+
const watches = readRunWatches(directory, session);
|
|
182
|
+
const runs = publishedRuns(directory);
|
|
183
|
+
if (args.length === 0) {
|
|
184
|
+
if (watches.length === 0)
|
|
185
|
+
console.log("This session watches no other runs.");
|
|
186
|
+
for (const watched of watches) {
|
|
187
|
+
console.log(describeWatch(watched));
|
|
188
|
+
printWatchedRuns(watched, runs);
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const added = parseWatch(args, watchUsage);
|
|
193
|
+
if (!watches.some((watched) => sameRunWatch(watched, added)))
|
|
194
|
+
writeRunWatches(directory, session, [...watches, added]);
|
|
195
|
+
console.log(`Watching ${describeWatch(added)}`);
|
|
196
|
+
printWatchedRuns(added, runs);
|
|
197
|
+
}
|
|
198
|
+
function unwatch(args) {
|
|
199
|
+
const { directory, session } = requireWatchList();
|
|
200
|
+
if (args[0] === "--all") {
|
|
201
|
+
if (args.length !== 1)
|
|
202
|
+
throw new Error(`Usage: ${unwatchUsage}`);
|
|
203
|
+
writeRunWatches(directory, session, []);
|
|
204
|
+
console.log("This session watches no other runs.");
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const removed = parseWatch(args, unwatchUsage);
|
|
208
|
+
const watches = readRunWatches(directory, session);
|
|
209
|
+
const kept = watches.filter((watched) => !sameRunWatch(watched, removed));
|
|
210
|
+
if (kept.length === watches.length) {
|
|
211
|
+
console.log(`This session was not watching ${describeWatch(removed)}`);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
writeRunWatches(directory, session, kept);
|
|
215
|
+
console.log(`No longer watching ${describeWatch(removed)}`);
|
|
216
|
+
}
|
|
217
|
+
function parseWatch(args, usage) {
|
|
218
|
+
if (args[0] === "--dir") {
|
|
219
|
+
if (args.length !== 2)
|
|
220
|
+
throw new Error(`Usage: ${usage}`);
|
|
221
|
+
return runWatch({ dir: args[1] });
|
|
222
|
+
}
|
|
223
|
+
if (args[0] !== "--" && args[0]?.startsWith("--") === true)
|
|
224
|
+
throw new Error(`Usage: ${usage}`);
|
|
225
|
+
return runWatch({ text: (args[0] === "--" ? args.slice(1) : args).join(" ") });
|
|
226
|
+
}
|
|
227
|
+
function describeWatch(watched) {
|
|
228
|
+
return "dir" in watched ? `--dir ${watched.dir}` : watched.text;
|
|
229
|
+
}
|
|
230
|
+
function printWatchedRuns(watched, runs) {
|
|
231
|
+
const matched = runs.filter((record) => runWatchMatches(watched, record));
|
|
232
|
+
if (matched.length === 0)
|
|
233
|
+
console.log(" no published run matches yet");
|
|
234
|
+
for (const record of matched)
|
|
235
|
+
console.log(` ${runColumns(record).join("\t")}`);
|
|
236
|
+
}
|
|
237
|
+
/** The session whose watch list this process changes, and where the list is kept. */
|
|
238
|
+
function requireWatchList() {
|
|
239
|
+
const directory = requireStatusDirectory();
|
|
240
|
+
const session = runSessionId();
|
|
241
|
+
if (session === undefined) {
|
|
242
|
+
throw new Error("No session to watch for: set MACHINES_RUN_OWNER to the id of the session whose status line shows the runs.");
|
|
243
|
+
}
|
|
244
|
+
// Refuses a session id that cannot name a file.
|
|
245
|
+
runWatchPath(directory, session);
|
|
246
|
+
return { directory, session };
|
|
247
|
+
}
|
|
248
|
+
async function respond(args) {
|
|
249
|
+
const question = args[0] === "--question";
|
|
250
|
+
const [runId, requestId, ...words] = question ? args.slice(1) : args;
|
|
251
|
+
if (words[0] === "--")
|
|
252
|
+
words.shift();
|
|
253
|
+
if (runId === undefined || requestId === undefined || words.length === 0) {
|
|
254
|
+
throw new Error("Usage: machine respond [--question] <run-id> <request-id> [--] <response>");
|
|
255
|
+
}
|
|
256
|
+
const text = words.join(" ");
|
|
257
|
+
await respondToRun({
|
|
258
|
+
directory: requireStatusDirectory(),
|
|
259
|
+
runId,
|
|
260
|
+
requestId,
|
|
261
|
+
response: question ? { type: "question", text } : text,
|
|
262
|
+
});
|
|
263
|
+
console.log(`Machine run ${runId} took the ${question ? "question" : "response"}.`);
|
|
264
|
+
}
|
|
265
|
+
function requireStatusDirectory() {
|
|
266
|
+
const directory = runStatusDirectory();
|
|
267
|
+
if (directory === undefined) {
|
|
268
|
+
throw new Error("Run status publication is off: set MACHINES_RUN_STATUS_DIR to the directory the runs publish to.");
|
|
269
|
+
}
|
|
270
|
+
return directory;
|
|
271
|
+
}
|
|
272
|
+
async function parseRunArguments(args) {
|
|
273
|
+
let inputFile;
|
|
91
274
|
const agentOverrides = new Map();
|
|
92
275
|
let index = 0;
|
|
93
276
|
while (index < args.length) {
|
|
@@ -96,6 +279,13 @@ function parseRunArguments(args) {
|
|
|
96
279
|
index += 1;
|
|
97
280
|
break;
|
|
98
281
|
}
|
|
282
|
+
if (argument === "--input-file") {
|
|
283
|
+
if (inputFile !== undefined || args[index + 1] === undefined)
|
|
284
|
+
throw new Error("Expected one --input-file <file|->");
|
|
285
|
+
inputFile = args[index + 1];
|
|
286
|
+
index += 2;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
99
289
|
if (argument !== "--agent")
|
|
100
290
|
break;
|
|
101
291
|
const assignment = args[index + 1];
|
|
@@ -117,10 +307,24 @@ function parseRunArguments(args) {
|
|
|
117
307
|
agentOverrides.set(role, preset);
|
|
118
308
|
index += 2;
|
|
119
309
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
310
|
+
let input = args.slice(index).join(" ");
|
|
311
|
+
if (inputFile !== undefined) {
|
|
312
|
+
if (args.length > index)
|
|
313
|
+
throw new Error("Use positional input or --input-file, not both");
|
|
314
|
+
let text;
|
|
315
|
+
if (inputFile === "-") {
|
|
316
|
+
const chunks = [];
|
|
317
|
+
for await (const chunk of process.stdin)
|
|
318
|
+
chunks.push(Buffer.from(chunk));
|
|
319
|
+
text = Buffer.concat(chunks).toString("utf8");
|
|
320
|
+
}
|
|
321
|
+
else
|
|
322
|
+
text = await readFile(inputFile, "utf8");
|
|
323
|
+
const parsed = JSON.parse(text);
|
|
324
|
+
assertJsonValue(parsed, "Machine input");
|
|
325
|
+
input = parsed;
|
|
326
|
+
}
|
|
327
|
+
return { agentOverrides, input };
|
|
124
328
|
}
|
|
125
329
|
function printMachineListing(found, separate) {
|
|
126
330
|
if (!process.stdout.isTTY) {
|
|
@@ -153,6 +357,7 @@ function printAgentListing(preset, separate) {
|
|
|
153
357
|
preset.model ?? "",
|
|
154
358
|
preset.thinking ?? "",
|
|
155
359
|
preset.source,
|
|
360
|
+
preset.fallback?.join(", ") ?? "",
|
|
156
361
|
].join("\t"));
|
|
157
362
|
return;
|
|
158
363
|
}
|
|
@@ -162,8 +367,53 @@ function printAgentListing(preset, separate) {
|
|
|
162
367
|
console.log(` ${preset.description}`);
|
|
163
368
|
if (identity.length > 0)
|
|
164
369
|
console.log(` ${magenta(identity.join(" · "))}`);
|
|
370
|
+
if (preset.fallback !== undefined)
|
|
371
|
+
console.log(` fallback: ${preset.fallback.join(", ") || "none"}`);
|
|
165
372
|
console.log(` ${dim(preset.source)}`);
|
|
166
373
|
}
|
|
374
|
+
/** Recorded text must not be able to move the cursor or restyle the terminal. */
|
|
375
|
+
function plain(text) {
|
|
376
|
+
return text.replace(/[\u0000-\u001f\u007f-\u009f]+/gu, " ").trim();
|
|
377
|
+
}
|
|
378
|
+
/** A run as tab-separated columns: its label follows the four that were always there. */
|
|
379
|
+
function runColumns(record) {
|
|
380
|
+
const label = record.label === undefined ? "" : plain(record.label);
|
|
381
|
+
return [record.id, record.machine, runStatus(record), record.state ?? "starting"].map(plain).concat(label === "" ? [] : [label]);
|
|
382
|
+
}
|
|
383
|
+
function printRunListing(record, separate, watched) {
|
|
384
|
+
const state = record.state ?? "starting";
|
|
385
|
+
const recorded = runStatus(record);
|
|
386
|
+
const human = recorded === "waiting" ? record.human : undefined;
|
|
387
|
+
// A run that finished or lost its process is no longer waiting for anything.
|
|
388
|
+
const limit = recorded === "running" || recorded === "waiting" ? record.limit : undefined;
|
|
389
|
+
const details = human === undefined ? (limit === undefined ? [] : [plain(limit.text)]) : [
|
|
390
|
+
...human.prompt.split("\n").map(plain).filter((line) => line !== ""),
|
|
391
|
+
...(human.requestId === undefined ? [] : [`request: ${plain(human.requestId)}`]),
|
|
392
|
+
...(record.answeredBy === "host" ? ["answered through its host"] : []),
|
|
393
|
+
...(human.choices === undefined ? [] : [`choices: ${human.choices.map(plain).join(", ")}`]),
|
|
394
|
+
...(human.suggestions === undefined ? [] : [`suggestions: ${human.suggestions.map(plain).join(", ")}`]),
|
|
395
|
+
...(human.discussion === true ? ["discussion: questions allowed"] : []),
|
|
396
|
+
];
|
|
397
|
+
if (!process.stdout.isTTY) {
|
|
398
|
+
const columns = runColumns(record);
|
|
399
|
+
// The mark has its own column, after the label's, whether or not the run has a label.
|
|
400
|
+
console.log((watched ? [...columns, ...(columns.length === 4 ? [""] : []), "watched"] : columns).join("\t"));
|
|
401
|
+
for (const line of details)
|
|
402
|
+
console.log(` ${line}`);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (separate)
|
|
406
|
+
console.log();
|
|
407
|
+
const status = recorded === "waiting"
|
|
408
|
+
? yellow(recorded)
|
|
409
|
+
: recorded === "running" ? cyan(recorded) : recorded === "completed" ? green(recorded) : red(recorded);
|
|
410
|
+
console.log(`${bold(plain(record.id))} ${bold(cyan(plain(record.machine)))} › ${status} · ${plain(state)}${watched ? ` · ${dim("watched")}` : ""}`);
|
|
411
|
+
if (record.label !== undefined && plain(record.label) !== "")
|
|
412
|
+
console.log(` ${plain(record.label)}`);
|
|
413
|
+
for (const line of details) {
|
|
414
|
+
console.log(` ${line.startsWith("request: ") || line === "answered through its host" ? dim(line) : line}`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
167
417
|
function printMachineHeader(machinePath) {
|
|
168
418
|
if (!process.stdout.isTTY) {
|
|
169
419
|
console.log(`Machine: ${machinePath}`);
|
|
@@ -179,7 +429,10 @@ function createRunStatus(machinePath) {
|
|
|
179
429
|
onState(state) {
|
|
180
430
|
console.error(`● ${name} · ${formatState(state)}`);
|
|
181
431
|
},
|
|
182
|
-
onAgentUpdate() {
|
|
432
|
+
onAgentUpdate(update) {
|
|
433
|
+
if (update.type === "limit")
|
|
434
|
+
console.error(update.text.replace(/\s+/gu, " "));
|
|
435
|
+
},
|
|
183
436
|
onHumanInput() { },
|
|
184
437
|
succeed() { },
|
|
185
438
|
fail() { },
|
|
@@ -192,6 +445,8 @@ function createRunStatus(machinePath) {
|
|
|
192
445
|
let activityExpanded = false;
|
|
193
446
|
let agentOutput = "";
|
|
194
447
|
let agentIdentity = {};
|
|
448
|
+
let agentLimit = "";
|
|
449
|
+
let agentWaiting = false;
|
|
195
450
|
let refresh;
|
|
196
451
|
const tools = new Map();
|
|
197
452
|
const hotkeysAvailable = Boolean(process.stdin.isTTY && process.stdin.setRawMode);
|
|
@@ -211,12 +466,14 @@ function createRunStatus(machinePath) {
|
|
|
211
466
|
? undefined
|
|
212
467
|
: `thinking ${agentIdentity.thinking}`,
|
|
213
468
|
].filter(Boolean).join(" · ");
|
|
214
|
-
const
|
|
469
|
+
const width = Math.max(20, (process.stderr.columns ?? 100) - 4);
|
|
470
|
+
const identityLine = identity === "" ? "" : `\n ${dim(truncate(identity, width))}`;
|
|
471
|
+
const limitLine = agentLimit === ""
|
|
215
472
|
? ""
|
|
216
|
-
: `\n ${
|
|
473
|
+
: `\n ${yellow(truncate(stripVTControlCharacters(agentLimit).replace(/\s+/gu, " "), width))}`;
|
|
217
474
|
return activityExpanded
|
|
218
|
-
? `${status}${identityLine}\n${activityText()}`
|
|
219
|
-
: `${status}${identityLine}`;
|
|
475
|
+
? `${status}${identityLine}${limitLine}\n${activityText()}`
|
|
476
|
+
: `${status}${identityLine}${limitLine}`;
|
|
220
477
|
}
|
|
221
478
|
function completedText() {
|
|
222
479
|
return `${green(displayState(currentState))} · ${dim(formatElapsed(Date.now() - enteredAt))}`;
|
|
@@ -317,6 +574,13 @@ function createRunStatus(machinePath) {
|
|
|
317
574
|
else if (update.type === "output") {
|
|
318
575
|
agentOutput = (agentOutput + update.text).slice(-32_768);
|
|
319
576
|
}
|
|
577
|
+
else if (update.type === "limit") {
|
|
578
|
+
// A wait stays on screen while its presets are tried again, until one of them resumes.
|
|
579
|
+
if (update.status !== "swapped")
|
|
580
|
+
agentWaiting = update.status === "waiting";
|
|
581
|
+
if (update.status !== "swapped" || !agentWaiting)
|
|
582
|
+
agentLimit = update.text;
|
|
583
|
+
}
|
|
320
584
|
else {
|
|
321
585
|
const previous = tools.get(update.id);
|
|
322
586
|
tools.set(update.id, {
|
|
@@ -344,6 +608,8 @@ function createRunStatus(machinePath) {
|
|
|
344
608
|
enteredAt = Date.now();
|
|
345
609
|
agentOutput = "";
|
|
346
610
|
agentIdentity = {};
|
|
611
|
+
agentLimit = "";
|
|
612
|
+
agentWaiting = false;
|
|
347
613
|
tools.clear();
|
|
348
614
|
if (humanInputActive)
|
|
349
615
|
return;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AgentRunner } from "./index.ts";
|
|
2
|
+
export interface CodexAgentOptions {
|
|
3
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
4
|
+
readonly harness?: string;
|
|
5
|
+
readonly model?: string;
|
|
6
|
+
/** A reasoning effort supported by the selected Codex model. */
|
|
7
|
+
readonly effort?: string;
|
|
8
|
+
/** Defaults to read-only; writes and unrestricted access require an explicit choice. */
|
|
9
|
+
readonly sandbox?: "read-only" | "workspace-write" | "danger-full-access";
|
|
10
|
+
readonly skipGitRepoCheck?: boolean;
|
|
11
|
+
/** Defaults to true, so each invocation does not persist a Codex session. */
|
|
12
|
+
readonly ephemeral?: boolean;
|
|
13
|
+
readonly output?: "capture" | "stream";
|
|
14
|
+
}
|
|
15
|
+
/** Runs one fresh, non-interactive Codex turn using the installed CLI and its authentication. */
|
|
16
|
+
export declare function codexAgent(command?: string, args?: readonly string[], options?: CodexAgentOptions): AgentRunner;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { createInterface } from "node:readline";
|
|
6
|
+
import * as v from "valibot";
|
|
7
|
+
import { AgentLimitError } from "./agent-limit.js";
|
|
8
|
+
import { agentPrompt, readAgentEvent } from "./agent-protocol.js";
|
|
9
|
+
/** Runs one fresh, non-interactive Codex turn using the installed CLI and its authentication. */
|
|
10
|
+
export function codexAgent(command = "codex", args = [], options = {}) {
|
|
11
|
+
return async (request, report) => {
|
|
12
|
+
if (request.cwd === undefined)
|
|
13
|
+
throw new Error("Codex Agent requires a working directory");
|
|
14
|
+
if (request.outcomes.length === 0)
|
|
15
|
+
throw new Error("Codex Agent requires at least one allowed outcome");
|
|
16
|
+
const harness = options.harness ?? command;
|
|
17
|
+
const env = { ...process.env, ...options.env };
|
|
18
|
+
const model = options.model ?? env.CODEX_MODEL ?? env.MACHINES_AGENT_MODEL;
|
|
19
|
+
const effort = options.effort ?? env.CODEX_EFFORT ?? env.MACHINES_AGENT_EFFORT;
|
|
20
|
+
const outputMode = options.output ?? "stream";
|
|
21
|
+
report?.({
|
|
22
|
+
type: "identity",
|
|
23
|
+
harness,
|
|
24
|
+
...(model === undefined ? {} : { model }),
|
|
25
|
+
...(effort === undefined ? {} : { thinking: effort }),
|
|
26
|
+
});
|
|
27
|
+
const temporary = await mkdtemp(join(tmpdir(), "machines-codex-result-"));
|
|
28
|
+
const finalPath = join(temporary, "response.txt");
|
|
29
|
+
let stderr = "";
|
|
30
|
+
try {
|
|
31
|
+
const child = spawn(command, [
|
|
32
|
+
...args,
|
|
33
|
+
"exec", "--json", "--color", "never",
|
|
34
|
+
"--sandbox", options.sandbox ?? "read-only",
|
|
35
|
+
"--config", 'approval_policy="never"',
|
|
36
|
+
...(options.skipGitRepoCheck === true ? ["--skip-git-repo-check"] : []),
|
|
37
|
+
...(options.ephemeral === false ? [] : ["--ephemeral"]),
|
|
38
|
+
...(model === undefined ? [] : ["--model", model]),
|
|
39
|
+
...(effort === undefined ? [] : ["--config", `model_reasoning_effort=${JSON.stringify(effort)}`]),
|
|
40
|
+
"--output-last-message", finalPath, "-",
|
|
41
|
+
], { cwd: request.cwd, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
42
|
+
const exited = new Promise((resolve, reject) => {
|
|
43
|
+
child.once("error", reject);
|
|
44
|
+
child.stdin.once("error", reject);
|
|
45
|
+
child.once("close", (code, signal) => {
|
|
46
|
+
if (code === 0)
|
|
47
|
+
resolve();
|
|
48
|
+
else
|
|
49
|
+
reject(new Error(signal === null
|
|
50
|
+
? `Process exited with code ${code}`
|
|
51
|
+
: `Process terminated by ${signal}`));
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
// Attach handlers before reading stdout: errors can arrive while it drains.
|
|
55
|
+
void exited.catch(() => { });
|
|
56
|
+
child.stderr.setEncoding("utf8");
|
|
57
|
+
child.stderr.on("data", (chunk) => {
|
|
58
|
+
stderr += chunk;
|
|
59
|
+
if (outputMode === "stream")
|
|
60
|
+
process.stderr.write(chunk);
|
|
61
|
+
});
|
|
62
|
+
const lines = createInterface({ input: child.stdout });
|
|
63
|
+
let completed = false;
|
|
64
|
+
let failure;
|
|
65
|
+
let lastError;
|
|
66
|
+
let lastMessage;
|
|
67
|
+
const emitMessage = (text) => {
|
|
68
|
+
if (text === "")
|
|
69
|
+
return;
|
|
70
|
+
report?.({ type: "output", text: `${text}${text.endsWith("\n") ? "" : "\n"}` });
|
|
71
|
+
if (outputMode === "stream")
|
|
72
|
+
process.stdout.write(`${text}${text.endsWith("\n") ? "" : "\n"}`);
|
|
73
|
+
};
|
|
74
|
+
try {
|
|
75
|
+
child.stdin.end(agentPrompt(request));
|
|
76
|
+
for await (const line of lines) {
|
|
77
|
+
const record = decodeRecord(line);
|
|
78
|
+
if (record === undefined)
|
|
79
|
+
continue;
|
|
80
|
+
if (record.type === "turn.completed")
|
|
81
|
+
completed = true;
|
|
82
|
+
else if (record.type === "turn.failed")
|
|
83
|
+
failure = record.error.message;
|
|
84
|
+
else if (record.type === "error")
|
|
85
|
+
lastError = record.message;
|
|
86
|
+
else {
|
|
87
|
+
const item = record.item;
|
|
88
|
+
if (item.type === "agent_message") {
|
|
89
|
+
if (record.type === "item.completed") {
|
|
90
|
+
lastMessage = item.text;
|
|
91
|
+
emitMessage(item.text);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const title = item.type === "command_execution" ? item.command
|
|
96
|
+
: item.type === "mcp_tool_call" ? `${item.server}/${item.tool}`
|
|
97
|
+
: item.type === "web_search" ? item.query : "File changes";
|
|
98
|
+
const status = item.status === "failed" || item.status === "declined" ? "failed"
|
|
99
|
+
: record.type === "item.completed" ? "completed" : "in_progress";
|
|
100
|
+
report?.({ type: "tool", id: item.id, title, status });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Codex states a usage limit only in the text of a failed turn.
|
|
105
|
+
const detail = failure ?? lastError;
|
|
106
|
+
const turnFailure = (message, cause) => detail !== undefined && usageLimitText.test(detail)
|
|
107
|
+
? new AgentLimitError(message, { harness, kind: "usage" }, cause)
|
|
108
|
+
: new Error(message, cause);
|
|
109
|
+
try {
|
|
110
|
+
await exited;
|
|
111
|
+
}
|
|
112
|
+
catch (cause) {
|
|
113
|
+
if (detail === undefined)
|
|
114
|
+
throw cause;
|
|
115
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
116
|
+
throw turnFailure(`${message}: ${detail}`, { cause });
|
|
117
|
+
}
|
|
118
|
+
if (failure !== undefined)
|
|
119
|
+
throw turnFailure(failure);
|
|
120
|
+
if (!completed)
|
|
121
|
+
throw turnFailure(lastError ?? "Codex exited without completing its turn");
|
|
122
|
+
// A progress message may contain a preliminary event. Only the CLI's
|
|
123
|
+
// final-response file determines the outcome, never accumulated activity.
|
|
124
|
+
const response = await readFile(finalPath, "utf8");
|
|
125
|
+
if (response.trimEnd() !== lastMessage?.trimEnd())
|
|
126
|
+
emitMessage(response);
|
|
127
|
+
return readAgentEvent(response, { adapter: "Codex", includeMessage: outputMode === "capture" });
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
lines.close();
|
|
131
|
+
child.kill();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (cause) {
|
|
135
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
136
|
+
const detail = stderr.trim();
|
|
137
|
+
const text = `Codex Agent "${command}" failed in "${request.cwd}": ${message}${detail === "" ? "" : `\n${detail}`}`;
|
|
138
|
+
throw cause instanceof AgentLimitError
|
|
139
|
+
? new AgentLimitError(text, cause.limit, { cause })
|
|
140
|
+
: new Error(text, { cause });
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
await rm(temporary, { recursive: true, force: true });
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
// Codex 0.162.1 writes the first of these with a typographic apostrophe (U+2019).
|
|
148
|
+
const usageLimitText = /^(?:You['’]ve hit your usage limit|Your workspace is out of credits|You hit your spend cap)/u;
|
|
149
|
+
const statusSchema = v.optional(v.picklist(["in_progress", "completed", "failed", "declined"]));
|
|
150
|
+
const itemSchema = v.variant("type", [
|
|
151
|
+
v.object({ type: v.literal("agent_message"), id: v.string(), text: v.string() }),
|
|
152
|
+
v.object({ type: v.literal("command_execution"), id: v.string(), command: v.string(), status: statusSchema }),
|
|
153
|
+
v.object({ type: v.literal("file_change"), id: v.string(), status: statusSchema }),
|
|
154
|
+
v.object({ type: v.literal("mcp_tool_call"), id: v.string(), server: v.string(), tool: v.string(), status: statusSchema }),
|
|
155
|
+
v.object({ type: v.literal("web_search"), id: v.string(), query: v.string(), status: statusSchema }),
|
|
156
|
+
]);
|
|
157
|
+
const recordSchema = v.variant("type", [
|
|
158
|
+
v.object({ type: v.literal("item.started"), item: itemSchema }),
|
|
159
|
+
v.object({ type: v.literal("item.updated"), item: itemSchema }),
|
|
160
|
+
v.object({ type: v.literal("item.completed"), item: itemSchema }),
|
|
161
|
+
v.object({ type: v.literal("turn.completed") }),
|
|
162
|
+
v.object({ type: v.literal("turn.failed"), error: v.object({ message: v.string() }) }),
|
|
163
|
+
v.object({ type: v.literal("error"), message: v.string() }),
|
|
164
|
+
]);
|
|
165
|
+
// Ignore unsupported events and malformed records without exposing raw protocol data.
|
|
166
|
+
function decodeRecord(line) {
|
|
167
|
+
let value;
|
|
168
|
+
try {
|
|
169
|
+
value = JSON.parse(line);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
const result = v.safeParse(recordSchema, value);
|
|
175
|
+
return result.success ? result.output : undefined;
|
|
176
|
+
}
|