@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.
Files changed (79) hide show
  1. package/README.md +115 -11
  2. package/claude/statusline.mjs +277 -0
  3. package/dist/examples/chatgpt-web.d.ts +1 -0
  4. package/dist/examples/chatgpt-web.js +36 -0
  5. package/dist/examples/claude.d.ts +1 -0
  6. package/dist/examples/claude.js +30 -0
  7. package/dist/examples/codex.d.ts +1 -0
  8. package/dist/examples/codex.js +30 -0
  9. package/dist/examples/decision.d.ts +1 -0
  10. package/dist/examples/decision.js +66 -0
  11. package/dist/examples/deepseek.d.ts +1 -0
  12. package/dist/examples/deepseek.js +27 -0
  13. package/dist/mcp/server.d.ts +1 -0
  14. package/dist/mcp/server.js +74 -11
  15. package/dist/pi-extension/index.js +16 -9
  16. package/dist/src/acp.js +2 -1
  17. package/dist/src/agent-bindings.d.ts +6 -0
  18. package/dist/src/agent-bindings.js +35 -5
  19. package/dist/src/agent-limit.d.ts +25 -0
  20. package/dist/src/agent-limit.js +34 -0
  21. package/dist/src/agent-limits.d.ts +37 -0
  22. package/dist/src/agent-limits.js +212 -0
  23. package/dist/src/agent-protocol.js +3 -1
  24. package/dist/src/catalog.d.ts +3 -0
  25. package/dist/src/catalog.js +84 -12
  26. package/dist/src/chatgpt-web.d.ts +143 -0
  27. package/dist/src/chatgpt-web.js +946 -0
  28. package/dist/src/claude.d.ts +35 -0
  29. package/dist/src/claude.js +294 -0
  30. package/dist/src/cli.js +282 -16
  31. package/dist/src/codex.d.ts +16 -0
  32. package/dist/src/codex.js +176 -0
  33. package/dist/src/conversation.d.ts +74 -0
  34. package/dist/src/conversation.js +46 -0
  35. package/dist/src/coordinator-guidance.d.ts +3 -0
  36. package/dist/src/coordinator-guidance.js +17 -0
  37. package/dist/src/decision.d.ts +29 -0
  38. package/dist/src/decision.js +67 -0
  39. package/dist/src/deepseek.d.ts +12 -0
  40. package/dist/src/deepseek.js +16 -0
  41. package/dist/src/host.d.ts +11 -0
  42. package/dist/src/host.js +153 -16
  43. package/dist/src/index.d.ts +39 -2
  44. package/dist/src/index.js +62 -5
  45. package/dist/src/jev.d.ts +15 -0
  46. package/dist/src/jev.js +90 -0
  47. package/dist/src/json.d.ts +6 -0
  48. package/dist/src/json.js +28 -0
  49. package/dist/src/launcher.d.ts +11 -1
  50. package/dist/src/launcher.js +72 -3
  51. package/dist/src/laya.d.ts +15 -0
  52. package/dist/src/laya.js +13 -0
  53. package/dist/src/openrouter.d.ts +16 -0
  54. package/dist/src/openrouter.js +97 -0
  55. package/dist/src/run-inbox.d.ts +49 -0
  56. package/dist/src/run-inbox.js +233 -0
  57. package/dist/src/run-status.d.ts +121 -0
  58. package/dist/src/run-status.js +336 -0
  59. package/dist/src/run-watch.d.ts +24 -0
  60. package/dist/src/run-watch.js +112 -0
  61. package/dist/src/session.d.ts +15 -2
  62. package/dist/src/session.js +47 -8
  63. package/dist/src/system-one-http.d.ts +25 -0
  64. package/dist/src/system-one-http.js +146 -0
  65. package/dist/src/terminal-human.d.ts +8 -1
  66. package/dist/src/terminal-human.js +36 -8
  67. package/dist/src/von.d.ts +15 -0
  68. package/dist/src/von.js +13 -0
  69. package/docs/architecture.md +74 -3
  70. package/docs/authoring.md +809 -1
  71. package/docs/integrations.md +322 -3
  72. package/docs/local-decisions.md +212 -0
  73. package/package.json +21 -4
  74. package/services/laya/server.py +230 -0
  75. package/skills/machine-builder/SKILL.md +56 -6
  76. package/skills/machine-delegation/SKILL.md +128 -0
  77. package/skills/machine-delegation/references/delegation-machine.md +90 -0
  78. package/skills/machine-project-setup/SKILL.md +181 -0
  79. 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: status.onAgentUpdate,
76
- onHumanInput: status.onHumanInput,
77
- onState: status.onState,
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 parseRunArguments(args) {
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
- return {
121
- agentOverrides,
122
- input: args.slice(index).join(" "),
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 identityLine = identity === ""
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 ${dim(truncate(identity, Math.max(20, (process.stderr.columns ?? 100) - 4)))}`;
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
+ }