@dna113p/machines 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +120 -4
  2. package/claude/statusline.mjs +244 -0
  3. package/dist/examples/agy.d.ts +1 -0
  4. package/dist/examples/agy.js +30 -0
  5. package/dist/examples/chatgpt-web.d.ts +1 -0
  6. package/dist/examples/chatgpt-web.js +36 -0
  7. package/dist/examples/claude.d.ts +1 -0
  8. package/dist/examples/claude.js +30 -0
  9. package/dist/examples/codex.d.ts +1 -0
  10. package/dist/examples/codex.js +30 -0
  11. package/dist/examples/decision.d.ts +1 -0
  12. package/dist/examples/decision.js +66 -0
  13. package/dist/examples/deepseek.d.ts +1 -0
  14. package/dist/examples/deepseek.js +27 -0
  15. package/dist/mcp/server.d.ts +1 -0
  16. package/dist/mcp/server.js +72 -10
  17. package/dist/pi-extension/index.js +16 -9
  18. package/dist/src/acp.js +4 -41
  19. package/dist/src/agent-bindings.d.ts +6 -0
  20. package/dist/src/agent-bindings.js +35 -5
  21. package/dist/src/agent-limit.d.ts +25 -0
  22. package/dist/src/agent-limit.js +34 -0
  23. package/dist/src/agent-limits.d.ts +37 -0
  24. package/dist/src/agent-limits.js +212 -0
  25. package/dist/src/agent-protocol.d.ts +6 -0
  26. package/dist/src/agent-protocol.js +40 -0
  27. package/dist/src/agy.d.ts +11 -0
  28. package/dist/src/agy.js +168 -0
  29. package/dist/src/catalog.d.ts +3 -0
  30. package/dist/src/catalog.js +94 -12
  31. package/dist/src/chatgpt-web.d.ts +143 -0
  32. package/dist/src/chatgpt-web.js +946 -0
  33. package/dist/src/claude.d.ts +35 -0
  34. package/dist/src/claude.js +294 -0
  35. package/dist/src/cli.js +194 -16
  36. package/dist/src/codex.d.ts +16 -0
  37. package/dist/src/codex.js +176 -0
  38. package/dist/src/conversation.d.ts +74 -0
  39. package/dist/src/conversation.js +46 -0
  40. package/dist/src/coordinator-guidance.d.ts +3 -0
  41. package/dist/src/coordinator-guidance.js +17 -0
  42. package/dist/src/decision.d.ts +29 -0
  43. package/dist/src/decision.js +67 -0
  44. package/dist/src/deepseek.d.ts +12 -0
  45. package/dist/src/deepseek.js +16 -0
  46. package/dist/src/host.d.ts +3 -0
  47. package/dist/src/host.js +68 -7
  48. package/dist/src/index.d.ts +39 -2
  49. package/dist/src/index.js +62 -5
  50. package/dist/src/jev.d.ts +15 -0
  51. package/dist/src/jev.js +90 -0
  52. package/dist/src/json.d.ts +6 -0
  53. package/dist/src/json.js +28 -0
  54. package/dist/src/launcher.d.ts +11 -1
  55. package/dist/src/launcher.js +72 -3
  56. package/dist/src/laya.d.ts +15 -0
  57. package/dist/src/laya.js +13 -0
  58. package/dist/src/openrouter.d.ts +16 -0
  59. package/dist/src/openrouter.js +97 -0
  60. package/dist/src/run-inbox.d.ts +49 -0
  61. package/dist/src/run-inbox.js +229 -0
  62. package/dist/src/run-status.d.ts +97 -0
  63. package/dist/src/run-status.js +285 -0
  64. package/dist/src/session.d.ts +13 -0
  65. package/dist/src/session.js +41 -3
  66. package/dist/src/system-one-http.d.ts +25 -0
  67. package/dist/src/system-one-http.js +146 -0
  68. package/dist/src/terminal-human.d.ts +8 -1
  69. package/dist/src/terminal-human.js +36 -8
  70. package/dist/src/von.d.ts +15 -0
  71. package/dist/src/von.js +13 -0
  72. package/docs/architecture.md +69 -2
  73. package/docs/authoring.md +849 -2
  74. package/docs/integrations.md +243 -3
  75. package/docs/local-decisions.md +212 -0
  76. package/package.json +23 -4
  77. package/services/laya/server.py +230 -0
  78. package/skills/machine-builder/SKILL.md +56 -6
  79. package/skills/machine-delegation/SKILL.md +123 -0
  80. package/skills/machine-delegation/references/delegation-machine.md +90 -0
  81. package/skills/machine-project-setup/SKILL.md +181 -0
  82. package/skills/machine-project-setup/references/project-workflows.md +50 -0
@@ -0,0 +1,35 @@
1
+ import type { AgentRunner } from "./index.ts";
2
+ export interface ClaudeAgentOptions {
3
+ readonly env?: Readonly<Record<string, string>>;
4
+ readonly harness?: string;
5
+ /** A Claude Code model alias (for example "opus", "sonnet", "haiku") or a full model name. */
6
+ readonly model?: string;
7
+ /** An effort level supported by the installed Claude Code and selected model. */
8
+ readonly effort?: string;
9
+ /**
10
+ * Defaults to "dontAsk": tools run only when Claude Code's own permission
11
+ * rules already allow them, and anything that would prompt is denied.
12
+ */
13
+ readonly permissionMode?: "acceptEdits" | "auto" | "bypassPermissions" | "dontAsk" | "manual" | "plan";
14
+ /** Restricts the built-in tool set, for example ["Read"]. */
15
+ readonly tools?: readonly string[];
16
+ /** Permission rules to allow in addition to Claude Code's settings, for example ["Bash(npm test)"]. */
17
+ readonly allowedTools?: readonly string[];
18
+ /** Tools or permission rules to deny regardless of Claude Code's settings. */
19
+ readonly disallowedTools?: readonly string[];
20
+ /** Defaults to false, so each invocation does not save a resumable Claude Code session. */
21
+ readonly sessionPersistence?: boolean;
22
+ /**
23
+ * Defaults to "limit": once the plan limit is used up and Claude Code starts
24
+ * spending paid extra usage, the turn stops with an `AgentLimitError`.
25
+ * "allow" lets the turn continue on extra usage.
26
+ */
27
+ readonly extraUsage?: "limit" | "allow";
28
+ readonly output?: "capture" | "stream";
29
+ }
30
+ /**
31
+ * Runs one fresh, non-interactive Claude Code turn using the installed CLI and
32
+ * its existing login. Authentication, settings, and permission rules stay in
33
+ * Claude Code; this runner never reads or forwards credentials.
34
+ */
35
+ export declare function claudeAgent(command?: string, args?: readonly string[], options?: ClaudeAgentOptions): AgentRunner;
@@ -0,0 +1,294 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createInterface } from "node:readline";
3
+ import * as v from "valibot";
4
+ import { AgentLimitError } from "./agent-limit.js";
5
+ import { agentPrompt, readAgentEvent } from "./agent-protocol.js";
6
+ /**
7
+ * Runs one fresh, non-interactive Claude Code turn using the installed CLI and
8
+ * its existing login. Authentication, settings, and permission rules stay in
9
+ * Claude Code; this runner never reads or forwards credentials.
10
+ */
11
+ export function claudeAgent(command = "claude", args = [], options = {}) {
12
+ return async (request, report) => {
13
+ if (request.cwd === undefined)
14
+ throw new Error("Claude Agent requires a working directory");
15
+ if (request.outcomes.length === 0)
16
+ throw new Error("Claude Agent requires at least one allowed outcome");
17
+ const harness = options.harness ?? command;
18
+ const env = { ...process.env, ...options.env };
19
+ // Claude Code exports its own CLAUDE_* variables (including CLAUDE_EFFORT) to
20
+ // child processes, so runner settings use names it does not own.
21
+ const model = options.model ?? env.MACHINES_CLAUDE_MODEL ?? env.MACHINES_AGENT_MODEL;
22
+ const effort = options.effort ?? env.MACHINES_CLAUDE_EFFORT ?? env.MACHINES_AGENT_EFFORT;
23
+ const outputMode = options.output ?? "stream";
24
+ report?.({
25
+ type: "identity",
26
+ harness,
27
+ ...(model === undefined ? {} : { model }),
28
+ ...(effort === undefined ? {} : { thinking: effort }),
29
+ });
30
+ let stderr = "";
31
+ try {
32
+ const child = spawn(command, [
33
+ ...args,
34
+ "--print", "--output-format", "stream-json", "--verbose",
35
+ "--permission-mode", options.permissionMode ?? "dontAsk",
36
+ ...(options.sessionPersistence === true ? [] : ["--no-session-persistence"]),
37
+ ...(model === undefined ? [] : ["--model", model]),
38
+ ...(effort === undefined ? [] : ["--effort", effort]),
39
+ ...(options.tools === undefined ? [] : ["--tools", options.tools.join(",")]),
40
+ ...(options.allowedTools === undefined ? [] : ["--allowed-tools", options.allowedTools.join(",")]),
41
+ ...(options.disallowedTools === undefined ? [] : ["--disallowed-tools", options.disallowedTools.join(",")]),
42
+ ], { cwd: request.cwd, env, stdio: ["pipe", "pipe", "pipe"] });
43
+ const exited = new Promise((resolve, reject) => {
44
+ child.once("error", reject);
45
+ child.stdin.once("error", reject);
46
+ child.once("close", (code, signal) => {
47
+ if (code === 0)
48
+ resolve();
49
+ else
50
+ reject(new Error(signal === null
51
+ ? `Process exited with code ${code}`
52
+ : `Process terminated by ${signal}`));
53
+ });
54
+ });
55
+ // Attach handlers before reading stdout: errors can arrive while it drains.
56
+ void exited.catch(() => { });
57
+ child.stderr.setEncoding("utf8");
58
+ child.stderr.on("data", (chunk) => {
59
+ stderr += chunk;
60
+ if (outputMode === "stream")
61
+ process.stderr.write(chunk);
62
+ });
63
+ const lines = createInterface({ input: child.stdout });
64
+ const toolTitles = new Map();
65
+ let result;
66
+ // The CLI reports a limit beside its messages: on the last top-level
67
+ // assistant record and in the latest rate-limit information.
68
+ let assistantError = {};
69
+ let rateLimit;
70
+ // The CLI streams one record per content block, and its final result
71
+ // joins the text blocks of the closing message.
72
+ let closingMessage;
73
+ let closingText = "";
74
+ const emitMessage = (text) => {
75
+ if (text === "")
76
+ return;
77
+ report?.({ type: "output", text: `${text}${text.endsWith("\n") ? "" : "\n"}` });
78
+ if (outputMode === "stream")
79
+ process.stdout.write(`${text}${text.endsWith("\n") ? "" : "\n"}`);
80
+ };
81
+ try {
82
+ child.stdin.end(agentPrompt(request));
83
+ for await (const line of lines) {
84
+ const record = decodeRecord(line);
85
+ if (record === undefined)
86
+ continue;
87
+ if (record.type === "malformed-result") {
88
+ // An unreadable final result must not leave an earlier one in effect.
89
+ result = { failed: true, text: "Claude returned a malformed final result" };
90
+ }
91
+ else if (record.type === "system") {
92
+ // The CLI resolves aliases and defaults; report what actually runs.
93
+ report?.({
94
+ type: "identity",
95
+ harness,
96
+ model: record.model,
97
+ ...(effort === undefined ? {} : { thinking: effort }),
98
+ });
99
+ }
100
+ else if (record.type === "rate_limit_event") {
101
+ const info = record.rate_limit_info;
102
+ if (info?.isUsingOverage === true && options.extraUsage !== "allow") {
103
+ // The plan limit is already used up; continuing would spend paid credits.
104
+ throw new AgentLimitError("Claude plan limit reached; extra usage is not used", agentLimit(harness, info));
105
+ }
106
+ // Information without a recognised status does not replace what is known.
107
+ if (info?.status !== undefined)
108
+ rateLimit = info;
109
+ }
110
+ else if (record.type === "result") {
111
+ result = {
112
+ failed: record.is_error === true || record.subtype !== "success",
113
+ text: record.result ?? record.errors?.join("\n") ?? record.subtype,
114
+ ...(record.is_error === true
115
+ ? { apiError: { name: record.api_error, rateLimited: record.api_error_status === 429 } }
116
+ : {}),
117
+ };
118
+ }
119
+ else if (record.parent_tool_use_id !== undefined && record.parent_tool_use_id !== null) {
120
+ // Subagent activity belongs to the parent's tool call, not the final answer.
121
+ }
122
+ else if (record.type === "assistant") {
123
+ assistantError = { name: record.api_error, rateLimited: record.error === "rate_limit" };
124
+ const info = record.api_error_params?.rate_limit_info;
125
+ if (info?.status !== undefined)
126
+ rateLimit = info;
127
+ const message = record.message.id ?? undefined;
128
+ if (message === undefined || message !== closingMessage)
129
+ closingText = "";
130
+ closingMessage = message;
131
+ for (const block of record.message.content) {
132
+ if (block.type === "text") {
133
+ closingText += block.text;
134
+ emitMessage(block.text);
135
+ }
136
+ else if (block.type === "tool_use") {
137
+ toolTitles.set(block.id, block.name);
138
+ report?.({ type: "tool", id: block.id, title: block.name, status: "in_progress" });
139
+ }
140
+ }
141
+ }
142
+ else if (typeof record.message.content !== "string") {
143
+ for (const block of record.message.content) {
144
+ if (block.type !== "tool_result")
145
+ continue;
146
+ const title = toolTitles.get(block.tool_use_id);
147
+ report?.({
148
+ type: "tool",
149
+ id: block.tool_use_id,
150
+ ...(title === undefined ? {} : { title }),
151
+ status: block.is_error === true ? "failed" : "completed",
152
+ });
153
+ }
154
+ }
155
+ }
156
+ // Only a final result that ended on an API error can be a limit. The latest
157
+ // rate-limit information names it and its reset, whatever its own status.
158
+ const limit = result?.apiError !== undefined
159
+ && reachedLimit(result.apiError, assistantError, rateLimit, result.text)
160
+ ? agentLimit(harness, rateLimit)
161
+ : undefined;
162
+ try {
163
+ await exited;
164
+ }
165
+ catch (cause) {
166
+ // An empty result adds nothing to the exit, but a limit it carries is still reported.
167
+ if (result === undefined || (result.text === "" && limit === undefined))
168
+ throw cause;
169
+ const message = cause instanceof Error ? cause.message : String(cause);
170
+ throw turnFailure(result.text === "" ? message : `${message}: ${result.text}`, limit, { cause });
171
+ }
172
+ if (result === undefined)
173
+ throw new Error("Claude exited without a final result");
174
+ if (result.failed)
175
+ throw turnFailure(result.text, limit);
176
+ // Earlier messages may contain progress or a preliminary event. Only the
177
+ // CLI's final result determines the outcome, never accumulated activity.
178
+ if (withoutSpace(result.text) !== withoutSpace(closingText))
179
+ emitMessage(result.text);
180
+ return readAgentEvent(result.text, { adapter: "Claude", includeMessage: outputMode === "capture" });
181
+ }
182
+ finally {
183
+ lines.close();
184
+ child.kill();
185
+ }
186
+ }
187
+ catch (cause) {
188
+ const message = cause instanceof Error ? cause.message : String(cause);
189
+ const detail = stderr.trim();
190
+ const text = `Claude Agent "${command}" failed in "${request.cwd}": ${message}${detail === "" ? "" : `\n${detail}`}`;
191
+ throw cause instanceof AgentLimitError
192
+ ? new AgentLimitError(text, cause.limit, { cause })
193
+ : new Error(text, { cause });
194
+ }
195
+ };
196
+ }
197
+ const planLimitErrors = new Set(["usage_limit_reached", "model_requires_usage_credits"]);
198
+ const limitText = /^(?:You've (?:hit|reached) your |You're out of (?:usage credits|extra usage))/u;
199
+ const maximumResetDelay = 8 * 24 * 60 * 60 * 1000;
200
+ // A 429 alone may be a temporary throttle; a plan limit also names itself, is
201
+ // rejected by the rate-limit information, or uses Claude Code's limit wording.
202
+ function reachedLimit(result, assistant, rateLimit, text) {
203
+ if ([result.name, assistant.name].some((name) => name !== undefined && planLimitErrors.has(name)))
204
+ return true;
205
+ if (result.rateLimited !== true && assistant.rateLimited !== true)
206
+ return false;
207
+ return (rateLimit?.status === "rejected" && rateLimit.isUsingOverage !== true) || limitText.test(text);
208
+ }
209
+ function agentLimit(harness, info) {
210
+ const now = Date.now();
211
+ const resetsAt = info?.resetsAt === undefined ? undefined : info.resetsAt * 1000;
212
+ return {
213
+ harness,
214
+ ...(info?.rateLimitType === undefined ? {} : { kind: info.rateLimitType }),
215
+ // A reset time that is past or implausibly distant is not reported.
216
+ ...(resetsAt !== undefined && Number.isFinite(resetsAt) && resetsAt > now && resetsAt <= now + maximumResetDelay
217
+ ? { resetsAt }
218
+ : {}),
219
+ };
220
+ }
221
+ function turnFailure(message, limit, options) {
222
+ return limit === undefined ? new Error(message, options) : new AgentLimitError(message, limit, options);
223
+ }
224
+ const parentSchema = v.nullish(v.string());
225
+ // Unlisted block types (thinking, images, future additions) are skipped, not rejected.
226
+ const otherBlockSchema = v.pipe(v.object({ type: v.string() }), v.transform(() => ({ type: "other" })));
227
+ const assistantBlockSchema = v.union([
228
+ v.object({ type: v.literal("text"), text: v.string() }),
229
+ v.object({ type: v.literal("tool_use"), id: v.string(), name: v.string() }),
230
+ otherBlockSchema,
231
+ ]);
232
+ const userBlockSchema = v.union([
233
+ v.object({ type: v.literal("tool_result"), tool_use_id: v.string(), is_error: v.nullish(v.boolean()) }),
234
+ otherBlockSchema,
235
+ ]);
236
+ // The limit fields were read from the Claude Code bundle, not observed at a real
237
+ // limit, so a malformed one is dropped without rejecting the record that carries it.
238
+ // A value outside the listed ones counts as malformed.
239
+ const tolerant = (schema) => v.fallback(v.optional(schema), undefined);
240
+ const rateLimitInfoSchema = v.object({
241
+ status: tolerant(v.picklist(["allowed", "allowed_warning", "rejected"])),
242
+ // Whole Unix seconds.
243
+ resetsAt: tolerant(v.pipe(v.number(), v.safeInteger())),
244
+ rateLimitType: tolerant(v.picklist([
245
+ "five_hour", "seven_day", "seven_day_opus", "seven_day_sonnet", "seven_day_overage_included", "overage",
246
+ ])),
247
+ isUsingOverage: tolerant(v.boolean()),
248
+ });
249
+ const recordSchema = v.variant("type", [
250
+ v.object({ type: v.literal("system"), subtype: v.literal("init"), model: v.string() }),
251
+ v.object({ type: v.literal("rate_limit_event"), rate_limit_info: tolerant(rateLimitInfoSchema) }),
252
+ v.object({
253
+ type: v.literal("assistant"),
254
+ parent_tool_use_id: parentSchema,
255
+ message: v.object({ id: v.nullish(v.string()), content: v.array(assistantBlockSchema) }),
256
+ error: tolerant(v.string()),
257
+ api_error: tolerant(v.string()),
258
+ api_error_params: tolerant(v.object({ rate_limit_info: tolerant(rateLimitInfoSchema) })),
259
+ }),
260
+ v.object({
261
+ type: v.literal("user"),
262
+ parent_tool_use_id: parentSchema,
263
+ message: v.object({ content: v.union([v.string(), v.array(userBlockSchema)]) }),
264
+ }),
265
+ v.object({
266
+ type: v.literal("result"),
267
+ subtype: v.string(),
268
+ is_error: v.nullish(v.boolean()),
269
+ result: v.nullish(v.string()),
270
+ errors: v.nullish(v.array(v.string())),
271
+ api_error: tolerant(v.string()),
272
+ api_error_status: tolerant(v.number()),
273
+ }),
274
+ ]);
275
+ // Ignore unsupported events and malformed records without exposing raw protocol data.
276
+ function decodeRecord(line) {
277
+ let value;
278
+ try {
279
+ value = JSON.parse(line);
280
+ }
281
+ catch {
282
+ return undefined;
283
+ }
284
+ const parsed = v.safeParse(recordSchema, value);
285
+ if (parsed.success)
286
+ return parsed.output;
287
+ return value !== null && typeof value === "object" && "type" in value && value.type === "result"
288
+ ? { type: "malformed-result" }
289
+ : undefined;
290
+ }
291
+ // Block boundaries differ between streamed messages and the joined result.
292
+ function withoutSpace(text) {
293
+ return text.replace(/\s+/gu, "");
294
+ }
package/dist/src/cli.js CHANGED
@@ -1,12 +1,16 @@
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, runStatusDirectory, } from "./run-status.js";
10
14
  const [command, selector, ...argumentsAfterSelector] = process.argv.slice(2);
11
15
  try {
12
16
  if (command === "list" && selector === undefined) {
@@ -35,13 +39,21 @@ try {
35
39
  else if (command === "run" && selector !== undefined) {
36
40
  await runMachine(await resolveMachine(selector), argumentsAfterSelector);
37
41
  }
42
+ else if (command === "runs" && (selector === undefined || selector === "--json") && argumentsAfterSelector.length === 0) {
43
+ listRuns(selector === "--json");
44
+ }
45
+ else if (command === "respond" && selector !== undefined) {
46
+ await respond([selector, ...argumentsAfterSelector]);
47
+ }
38
48
  else {
39
49
  throw new Error([
40
50
  "Usage:",
41
51
  " machine list",
42
52
  " machine agents",
43
53
  " machine show <name-or-file>",
44
- " machine run <name-or-file> [--agent role=preset]... [--] [input]",
54
+ " machine run <name-or-file> [--agent role=preset]... [--input-file <file|->] [--] [input]",
55
+ " machine runs [--json]",
56
+ " machine respond [--question] <run-id> <request-id> [--] <response>",
45
57
  ].join("\n"));
46
58
  }
47
59
  }
@@ -61,7 +73,7 @@ async function resolveMachine(selector) {
61
73
  return found.path;
62
74
  }
63
75
  async function runMachine(machinePath, args) {
64
- const invocation = parseRunArguments(args);
76
+ const invocation = await parseRunArguments(args);
65
77
  printMachineHeader(machinePath);
66
78
  const prepared = await prepareMachineRun({
67
79
  cwd: process.cwd(),
@@ -70,16 +82,78 @@ async function runMachine(machinePath, args) {
70
82
  agents: Object.fromEntries(invocation.agentOverrides),
71
83
  });
72
84
  const status = createRunStatus(machinePath);
85
+ const runId = randomUUID();
86
+ const published = createRunStatusPublisher({ id: runId, machine: prepared.name, path: prepared.path, cwd: process.cwd() });
87
+ const statusDirectory = runStatusDirectory();
88
+ // Machines started by this run's Operations and Agents are its sub-runs.
89
+ process.env.MACHINES_RUN_PARENT = runId;
90
+ let publishedState;
91
+ // A runner reports its identity as it starts. For a state entered by a
92
+ // transition that is before the state is announced, so an identity is held
93
+ // for the rest of the tick and published with the state it belongs to.
94
+ let arriving;
73
95
  try {
74
96
  const result = await prepared.start({
75
- onAgentUpdate: status.onAgentUpdate,
76
- onHumanInput: status.onHumanInput,
77
- onState: status.onState,
97
+ onAgentUpdate(update) {
98
+ if (update.type === "identity") {
99
+ arriving = update;
100
+ queueMicrotask(() => {
101
+ if (arriving !== update)
102
+ return;
103
+ arriving = undefined;
104
+ published.update({ agent: update });
105
+ });
106
+ }
107
+ else if (update.type === "limit" && update.status !== "swapped") {
108
+ // A swap while every preset is being tried again leaves the wait as it is published.
109
+ published.update({
110
+ limit: update.status === "resumed"
111
+ ? undefined
112
+ : { text: update.text, ...(update.until === undefined ? {} : { until: update.until }) },
113
+ });
114
+ }
115
+ status.onAgentUpdate(update);
116
+ },
117
+ // A published run can also be answered through its inbox by `machine respond`.
118
+ ...(statusDirectory === undefined ? {} : {
119
+ human: inboxHuman({
120
+ directory: statusDirectory,
121
+ runId,
122
+ publish: (request) => published.update({ status: "waiting", human: request }),
123
+ acknowledge: (delivery) => published.update({ delivery }),
124
+ onTerminalRefused: (reason) => console.error(reason),
125
+ onTerminalClosed(requestId) {
126
+ // Finish the unanswered prompt line before saying where the answer can come from.
127
+ process.stdout.write("\n");
128
+ console.error(`No terminal input. Answer with: machine respond ${runId} ${requestId} <response>`);
129
+ },
130
+ }),
131
+ }),
132
+ onHumanInput(active) {
133
+ published.update(active ? { status: "waiting" } : { status: "running", human: undefined });
134
+ status.onHumanInput(active);
135
+ },
136
+ onState(state) {
137
+ const entered = formatState(state);
138
+ if (entered === publishedState)
139
+ published.update({ state });
140
+ else {
141
+ published.update({ state, agent: arriving });
142
+ arriving = undefined;
143
+ }
144
+ publishedState = entered;
145
+ status.onState(state);
146
+ },
78
147
  });
148
+ published.finish({ state: result.value });
79
149
  status.succeed();
80
150
  console.log(`--> ${String(result.value)}`);
151
+ if (result.output !== undefined) {
152
+ console.log(isJsonValue(result.output) ? JSON.stringify(result.output, null, 2) : inspect(result.output));
153
+ }
81
154
  }
82
155
  catch (cause) {
156
+ published.finish({ error: cause instanceof Error ? cause.message : String(cause) });
83
157
  status.fail();
84
158
  throw cause;
85
159
  }
@@ -87,7 +161,46 @@ async function runMachine(machinePath, args) {
87
161
  status.close();
88
162
  }
89
163
  }
90
- function parseRunArguments(args) {
164
+ function listRuns(json) {
165
+ const runs = readRunStatuses(requireStatusDirectory())
166
+ .filter((record) => (record.status === "running" || record.status === "waiting") && isProcessAlive(record.pid))
167
+ .sort((left, right) => Date.parse(left.startedAt) - Date.parse(right.startedAt));
168
+ if (json) {
169
+ console.log(JSON.stringify(runs, null, 2));
170
+ return;
171
+ }
172
+ if (runs.length === 0)
173
+ console.log("No Machine runs are running or waiting.");
174
+ runs.forEach((record, index) => {
175
+ printRunListing(record, index > 0);
176
+ });
177
+ }
178
+ async function respond(args) {
179
+ const question = args[0] === "--question";
180
+ const [runId, requestId, ...words] = question ? args.slice(1) : args;
181
+ if (words[0] === "--")
182
+ words.shift();
183
+ if (runId === undefined || requestId === undefined || words.length === 0) {
184
+ throw new Error("Usage: machine respond [--question] <run-id> <request-id> [--] <response>");
185
+ }
186
+ const text = words.join(" ");
187
+ await respondToRun({
188
+ directory: requireStatusDirectory(),
189
+ runId,
190
+ requestId,
191
+ response: question ? { type: "question", text } : text,
192
+ });
193
+ console.log(`Machine run ${runId} took the ${question ? "question" : "response"}.`);
194
+ }
195
+ function requireStatusDirectory() {
196
+ const directory = runStatusDirectory();
197
+ if (directory === undefined) {
198
+ throw new Error("Run status publication is off: set MACHINES_RUN_STATUS_DIR to the directory the runs publish to.");
199
+ }
200
+ return directory;
201
+ }
202
+ async function parseRunArguments(args) {
203
+ let inputFile;
91
204
  const agentOverrides = new Map();
92
205
  let index = 0;
93
206
  while (index < args.length) {
@@ -96,6 +209,13 @@ function parseRunArguments(args) {
96
209
  index += 1;
97
210
  break;
98
211
  }
212
+ if (argument === "--input-file") {
213
+ if (inputFile !== undefined || args[index + 1] === undefined)
214
+ throw new Error("Expected one --input-file <file|->");
215
+ inputFile = args[index + 1];
216
+ index += 2;
217
+ continue;
218
+ }
99
219
  if (argument !== "--agent")
100
220
  break;
101
221
  const assignment = args[index + 1];
@@ -117,10 +237,24 @@ function parseRunArguments(args) {
117
237
  agentOverrides.set(role, preset);
118
238
  index += 2;
119
239
  }
120
- return {
121
- agentOverrides,
122
- input: args.slice(index).join(" "),
123
- };
240
+ let input = args.slice(index).join(" ");
241
+ if (inputFile !== undefined) {
242
+ if (args.length > index)
243
+ throw new Error("Use positional input or --input-file, not both");
244
+ let text;
245
+ if (inputFile === "-") {
246
+ const chunks = [];
247
+ for await (const chunk of process.stdin)
248
+ chunks.push(Buffer.from(chunk));
249
+ text = Buffer.concat(chunks).toString("utf8");
250
+ }
251
+ else
252
+ text = await readFile(inputFile, "utf8");
253
+ const parsed = JSON.parse(text);
254
+ assertJsonValue(parsed, "Machine input");
255
+ input = parsed;
256
+ }
257
+ return { agentOverrides, input };
124
258
  }
125
259
  function printMachineListing(found, separate) {
126
260
  if (!process.stdout.isTTY) {
@@ -153,6 +287,7 @@ function printAgentListing(preset, separate) {
153
287
  preset.model ?? "",
154
288
  preset.thinking ?? "",
155
289
  preset.source,
290
+ preset.fallback?.join(", ") ?? "",
156
291
  ].join("\t"));
157
292
  return;
158
293
  }
@@ -162,8 +297,35 @@ function printAgentListing(preset, separate) {
162
297
  console.log(` ${preset.description}`);
163
298
  if (identity.length > 0)
164
299
  console.log(` ${magenta(identity.join(" · "))}`);
300
+ if (preset.fallback !== undefined)
301
+ console.log(` fallback: ${preset.fallback.join(", ") || "none"}`);
165
302
  console.log(` ${dim(preset.source)}`);
166
303
  }
304
+ function printRunListing(record, separate) {
305
+ const state = record.state ?? "starting";
306
+ const human = record.status === "waiting" ? record.human : undefined;
307
+ // Recorded text must not be able to move the cursor or restyle the terminal.
308
+ const plain = (text) => text.replace(/[\u0000-\u001f\u007f-\u009f]+/gu, " ").trim();
309
+ const details = human === undefined ? (record.limit === undefined ? [] : [plain(record.limit.text)]) : [
310
+ ...human.prompt.split("\n").map(plain).filter((line) => line !== ""),
311
+ ...(human.requestId === undefined ? [] : [`request: ${plain(human.requestId)}`]),
312
+ ...(human.choices === undefined ? [] : [`choices: ${human.choices.map(plain).join(", ")}`]),
313
+ ...(human.suggestions === undefined ? [] : [`suggestions: ${human.suggestions.map(plain).join(", ")}`]),
314
+ ...(human.discussion === true ? ["discussion: questions allowed"] : []),
315
+ ];
316
+ if (!process.stdout.isTTY) {
317
+ console.log([record.id, record.machine, record.status, state].map(plain).join("\t"));
318
+ for (const line of details)
319
+ console.log(` ${line}`);
320
+ return;
321
+ }
322
+ if (separate)
323
+ console.log();
324
+ const status = record.status === "waiting" ? yellow("waiting") : cyan("running");
325
+ console.log(`${bold(plain(record.id))} ${bold(cyan(plain(record.machine)))} › ${status} · ${plain(state)}`);
326
+ for (const line of details)
327
+ console.log(` ${line.startsWith("request: ") ? dim(line) : line}`);
328
+ }
167
329
  function printMachineHeader(machinePath) {
168
330
  if (!process.stdout.isTTY) {
169
331
  console.log(`Machine: ${machinePath}`);
@@ -179,7 +341,10 @@ function createRunStatus(machinePath) {
179
341
  onState(state) {
180
342
  console.error(`● ${name} · ${formatState(state)}`);
181
343
  },
182
- onAgentUpdate() { },
344
+ onAgentUpdate(update) {
345
+ if (update.type === "limit")
346
+ console.error(update.text.replace(/\s+/gu, " "));
347
+ },
183
348
  onHumanInput() { },
184
349
  succeed() { },
185
350
  fail() { },
@@ -192,6 +357,8 @@ function createRunStatus(machinePath) {
192
357
  let activityExpanded = false;
193
358
  let agentOutput = "";
194
359
  let agentIdentity = {};
360
+ let agentLimit = "";
361
+ let agentWaiting = false;
195
362
  let refresh;
196
363
  const tools = new Map();
197
364
  const hotkeysAvailable = Boolean(process.stdin.isTTY && process.stdin.setRawMode);
@@ -211,12 +378,14 @@ function createRunStatus(machinePath) {
211
378
  ? undefined
212
379
  : `thinking ${agentIdentity.thinking}`,
213
380
  ].filter(Boolean).join(" · ");
214
- const identityLine = identity === ""
381
+ const width = Math.max(20, (process.stderr.columns ?? 100) - 4);
382
+ const identityLine = identity === "" ? "" : `\n ${dim(truncate(identity, width))}`;
383
+ const limitLine = agentLimit === ""
215
384
  ? ""
216
- : `\n ${dim(truncate(identity, Math.max(20, (process.stderr.columns ?? 100) - 4)))}`;
385
+ : `\n ${yellow(truncate(stripVTControlCharacters(agentLimit).replace(/\s+/gu, " "), width))}`;
217
386
  return activityExpanded
218
- ? `${status}${identityLine}\n${activityText()}`
219
- : `${status}${identityLine}`;
387
+ ? `${status}${identityLine}${limitLine}\n${activityText()}`
388
+ : `${status}${identityLine}${limitLine}`;
220
389
  }
221
390
  function completedText() {
222
391
  return `${green(displayState(currentState))} · ${dim(formatElapsed(Date.now() - enteredAt))}`;
@@ -317,6 +486,13 @@ function createRunStatus(machinePath) {
317
486
  else if (update.type === "output") {
318
487
  agentOutput = (agentOutput + update.text).slice(-32_768);
319
488
  }
489
+ else if (update.type === "limit") {
490
+ // A wait stays on screen while its presets are tried again, until one of them resumes.
491
+ if (update.status !== "swapped")
492
+ agentWaiting = update.status === "waiting";
493
+ if (update.status !== "swapped" || !agentWaiting)
494
+ agentLimit = update.text;
495
+ }
320
496
  else {
321
497
  const previous = tools.get(update.id);
322
498
  tools.set(update.id, {
@@ -344,6 +520,8 @@ function createRunStatus(machinePath) {
344
520
  enteredAt = Date.now();
345
521
  agentOutput = "";
346
522
  agentIdentity = {};
523
+ agentLimit = "";
524
+ agentWaiting = false;
347
525
  tools.clear();
348
526
  if (humanInputActive)
349
527
  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;