@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
@@ -4,6 +4,7 @@ import { type CallToolResult } from "@modelcontextprotocol/sdk/types.js";
4
4
  export type { RunSnapshot } from "../src/session.ts";
5
5
  export declare class MachineSession {
6
6
  #private;
7
+ constructor();
7
8
  list(input: unknown): Promise<CallToolResult>;
8
9
  agents(input: unknown): Promise<CallToolResult>;
9
10
  start(input: unknown): Promise<CallToolResult>;
@@ -6,15 +6,19 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
7
  import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
8
8
  import * as v from "valibot";
9
+ import { isJsonValue } from "../src/json.js";
9
10
  import { MachineSession as RunSession } from "../src/session.js";
10
11
  import { listAgentPresets, listMachines } from "../src/launcher.js";
12
+ import { watchRunInbox } from "../src/run-inbox.js";
13
+ import { createRunStatusPublisher, runStatusDirectory } from "../src/run-status.js";
14
+ import { machineCoordinatorGuidelines, machineDiscoveryDescription, machineSupervisionGuidelines } from "../src/coordinator-guidance.js";
11
15
  const widgetUri = "ui://machines/run-v1.html";
12
16
  const cwd = v.pipe(v.string(), v.nonEmpty("cwd must be a non-empty absolute path"), v.check(isAbsolute, "cwd must be an absolute path"));
13
17
  const listInput = v.strictObject({ cwd });
14
18
  const startInput = v.strictObject({
15
19
  cwd,
16
20
  machine: v.pipe(v.string(), v.nonEmpty("machine must not be empty")),
17
- input: v.optional(v.string()),
21
+ input: v.optional(v.custom(isJsonValue)),
18
22
  agents: v.optional(v.record(v.pipe(v.string(), v.nonEmpty("Agent role names must not be empty")), v.pipe(v.string(), v.nonEmpty("Agent preset names must not be empty")))),
19
23
  });
20
24
  const statusInput = v.strictObject({
@@ -40,7 +44,7 @@ const tools = [
40
44
  {
41
45
  name: "machine_list",
42
46
  title: "List Machines",
43
- description: "List Machines available to the current project, why to use them, and any missing Agent bindings.",
47
+ description: machineDiscoveryDescription,
44
48
  inputSchema: locationInputSchema,
45
49
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
46
50
  },
@@ -67,8 +71,7 @@ const tools = [
67
71
  description: "Exact Machine name returned by machine_list",
68
72
  },
69
73
  input: {
70
- type: "string",
71
- description: "Clear task or input for the Machine",
74
+ description: "JSON-serializable task or structured input for the Machine",
72
75
  },
73
76
  agents: {
74
77
  type: "object",
@@ -135,6 +138,13 @@ const tools = [
135
138
  ];
136
139
  export class MachineSession {
137
140
  #session = new RunSession();
141
+ #published = new Map();
142
+ #inboxes = new Map();
143
+ /** The inbox delivery being handed to the session, named in the update that leaves its request. */
144
+ #taking;
145
+ constructor() {
146
+ this.#session.subscribe(({ run }) => this.#publish(run));
147
+ }
138
148
  async list(input) {
139
149
  const { cwd } = parse(listInput, input);
140
150
  const machines = await listMachines({ cwd });
@@ -160,7 +170,8 @@ export class MachineSession {
160
170
  const identity = [agent.harness, agent.model, agent.thinking]
161
171
  .filter((part) => part !== undefined)
162
172
  .join(" · ");
163
- return `${agent.name} — ${agent.description}${identity === "" ? "" : ` (${identity})`}\n ${agent.source}`;
173
+ const fallback = agent.fallback === undefined ? "" : ` · fallback: ${agent.fallback.join(", ") || "none"}`;
174
+ return `${agent.name} — ${agent.description}${identity === "" ? "" : ` (${identity})`}\n ${agent.source}${fallback}`;
164
175
  }).join("\n\n");
165
176
  return result(text, { agents });
166
177
  }
@@ -179,6 +190,58 @@ export class MachineSession {
179
190
  }
180
191
  close() {
181
192
  this.#session.close();
193
+ // Closing terminates the session's runs without a final event.
194
+ for (const publisher of this.#published.values())
195
+ publisher.finish({ error: "Machine session closed" });
196
+ this.#published.clear();
197
+ for (const inbox of this.#inboxes.values())
198
+ inbox.stop();
199
+ this.#inboxes.clear();
200
+ }
201
+ #publish(run) {
202
+ let publisher = this.#published.get(run.id);
203
+ if (publisher === undefined) {
204
+ publisher = createRunStatusPublisher({
205
+ id: run.id,
206
+ machine: run.machine,
207
+ path: run.path,
208
+ cwd: run.cwd,
209
+ startedAt: run.startedAt,
210
+ });
211
+ this.#published.set(run.id, publisher);
212
+ }
213
+ const human = run.status === "waiting" ? run.human : undefined;
214
+ this.#watchInbox(run.id, human);
215
+ if (run.status === "running" || run.status === "waiting") {
216
+ publisher.update({
217
+ status: run.status, state: run.state, agent: run.agent, limit: run.limit, human, delivery: this.#taking,
218
+ });
219
+ return;
220
+ }
221
+ publisher.finish(run.status === "completed" ? { state: run.state } : { error: run.error ?? "Machine run failed" });
222
+ this.#published.delete(run.id);
223
+ }
224
+ /** A published request can also be answered, or asked about, through the run's inbox by `machine respond`. */
225
+ #watchInbox(runId, request) {
226
+ const watched = this.#inboxes.get(runId);
227
+ if (watched?.requestId === request?.requestId)
228
+ return;
229
+ watched?.stop();
230
+ this.#inboxes.delete(runId);
231
+ const directory = runStatusDirectory();
232
+ if (request === undefined || directory === undefined)
233
+ return;
234
+ const stop = watchRunInbox(directory, runId, request, (response, delivery) => {
235
+ // The session leaves the request before either call returns. A tool call
236
+ // may have answered first; the session then refuses this one unpublished.
237
+ this.#taking = delivery;
238
+ const sent = typeof response === "string"
239
+ ? this.#session.respond({ runId, requestId: request.requestId, response })
240
+ : this.#session.ask({ runId, requestId: request.requestId, question: response.text });
241
+ this.#taking = undefined;
242
+ sent.catch(() => { });
243
+ });
244
+ this.#inboxes.set(runId, { requestId: request.requestId, stop });
182
245
  }
183
246
  }
184
247
  export function createMachinesMcpServer(widgetHtml = readFileSync(new URL("../codex/machines/ui/machines.html", import.meta.url), "utf8")) {
@@ -186,11 +249,8 @@ export function createMachinesMcpServer(widgetHtml = readFileSync(new URL("../co
186
249
  const server = new Server({ name: "machines", version: "0.1.0" }, {
187
250
  capabilities: { tools: {}, resources: {} },
188
251
  instructions: [
189
- "Use machine_list when the user names a Machine or a reusable multi-step workflow may fit.",
190
- "Choose by description rather than guessing a Machine name.",
191
- "machine_start is asynchronous; continue the conversation while it runs.",
192
- "Use machine_status for authoritative progress and machine_respond only after the Human's answer is clear.",
193
- "If no Machine fits a one-off task, handle it normally. Ask before creating persistent Machine policy.",
252
+ ...machineCoordinatorGuidelines,
253
+ ...machineSupervisionGuidelines,
194
254
  ].join(" "),
195
255
  });
196
256
  server.onclose = () => session.close();
@@ -264,6 +324,8 @@ function formatSnapshot(run) {
264
324
  run.agent.thinking === undefined ? undefined : `thinking ${run.agent.thinking}`,
265
325
  ].filter((part) => part !== undefined).join(" · ")}`);
266
326
  }
327
+ if (run.limit !== undefined)
328
+ lines.push(`Limit: ${run.limit.text}`);
267
329
  if (run.human !== undefined) {
268
330
  lines.push(`Human: ${run.human.prompt}`, `Request id: ${run.human.requestId}`);
269
331
  if (run.human.choices !== undefined)
@@ -10,6 +10,8 @@ import { realpathSync } from "node:fs";
10
10
  import { dirname, resolve } from "node:path";
11
11
  import { fileURLToPath, pathToFileURL } from "node:url";
12
12
  import { Type } from "typebox";
13
+ import { assertJsonValue } from "../src/json.js";
14
+ import { machineCoordinatorGuidelines, machineDiscoveryDescription } from "../src/coordinator-guidance.js";
13
15
  const sourceFile = realpathSync(fileURLToPath(import.meta.url));
14
16
  const sourceRoot = resolve(dirname(sourceFile), "..");
15
17
  const moduleExtension = sourceFile.endsWith(".ts") ? "ts" : "js";
@@ -105,12 +107,9 @@ export default function machinesExtension(pi) {
105
107
  pi.registerTool({
106
108
  name: "machine_list",
107
109
  label: "List Machines",
108
- description: "List available Machines, their purpose, Agent roles, and missing bindings.",
109
- promptSnippet: "Discover reusable asynchronous Machines with machine_list before starting one.",
110
- promptGuidelines: [
111
- "Use machine_list when the user asks to run a Machine or a reusable multi-step workflow may fit; choose an existing Machine by its description rather than guessing its name.",
112
- "If no Machine fits, handle a one-off task normally. For a reusable workflow, propose a short state sequence and ask before creating persistent Machine policy.",
113
- ],
110
+ description: machineDiscoveryDescription,
111
+ promptSnippet: "Discover and use Machines before delegated or multi-step work.",
112
+ promptGuidelines: machineCoordinatorGuidelines,
114
113
  parameters: Type.Object({}, { additionalProperties: false }),
115
114
  executionMode: "parallel",
116
115
  async execute(_id, _params, _signal, _onUpdate, context) {
@@ -156,7 +155,8 @@ export default function machinesExtension(pi) {
156
155
  const identity = [agent.harness, agent.model, agent.thinking]
157
156
  .filter((part) => part !== undefined)
158
157
  .join(" · ");
159
- return `${agent.name} — ${agent.description}${identity === "" ? "" : ` (${identity})`}\n ${agent.source}`;
158
+ const fallback = agent.fallback === undefined ? "" : ` · fallback: ${agent.fallback.join(", ") || "none"}`;
159
+ return `${agent.name} — ${agent.description}${identity === "" ? "" : ` (${identity})`}\n ${agent.source}${fallback}`;
160
160
  }).join("\n");
161
161
  return result(text, {
162
162
  summary: `${agents.length} Agent preset${agents.length === 1 ? "" : "s"}`,
@@ -177,13 +177,15 @@ export default function machinesExtension(pi) {
177
177
  ],
178
178
  parameters: Type.Object({
179
179
  machine: Type.String({ description: "Exact Machine name from machine_list" }),
180
- input: Type.Optional(Type.String({ description: "The task or input for the Machine" })),
180
+ input: Type.Optional(Type.Unknown({ description: "JSON-serializable task or structured input for the Machine" })),
181
181
  agents: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Optional Machine role to configured Agent preset name" })),
182
182
  }, { additionalProperties: false }),
183
183
  async execute(_id, rawParams, _signal, _onUpdate, context) {
184
184
  useContext(context);
185
185
  const machine = requiredString(rawParams.machine, "machine");
186
- const input = optionalString(rawParams.input, "input");
186
+ const input = rawParams.input;
187
+ if (input !== undefined)
188
+ assertJsonValue(input, "Machine input");
187
189
  const agents = optionalStringRecord(rawParams.agents, "agents");
188
190
  const runs = await getSession();
189
191
  const run = await runs.start({
@@ -263,6 +265,8 @@ function formatSnapshot(run) {
263
265
  ];
264
266
  if (run.agent !== undefined)
265
267
  lines.push(`Agent: ${formatAgent(run.agent)}`);
268
+ if (run.limit !== undefined)
269
+ lines.push(`Limit: ${run.limit.text}`);
266
270
  if (run.human !== undefined) {
267
271
  lines.push(`Human: ${run.human.prompt}`, `Request id: ${run.human.requestId}`);
268
272
  if (run.human.choices !== undefined)
@@ -314,6 +318,9 @@ function widgetLines(runs, theme, width) {
314
318
  lines.push(`${edge("│")} ${theme.fg("warning", promptLine)}`);
315
319
  }
316
320
  }
321
+ else if (run.limit !== undefined) {
322
+ lines.push(`${edge("│")} ${theme.fg("warning", run.limit.text)}`);
323
+ }
317
324
  else if (run.agent !== undefined) {
318
325
  lines.push(`${edge("│")} ${theme.fg("muted", formatAgent(run.agent))}`);
319
326
  }
package/dist/src/acp.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { Readable, Writable } from "node:stream";
3
3
  import * as acp from "@agentclientprotocol/sdk";
4
+ import { agentPrompt, readAgentEvent } from "./agent-protocol.js";
4
5
  export function acpAgent(command, args = [], options = {}) {
5
6
  return async (request, report) => {
6
7
  if (request.cwd === undefined) {
@@ -20,7 +21,8 @@ export function acpAgent(command, args = [], options = {}) {
20
21
  child.stderr.setEncoding("utf8");
21
22
  child.stderr.on("data", (chunk) => {
22
23
  stderr += chunk;
23
- process.stderr.write(chunk);
24
+ if (options.output !== "capture")
25
+ process.stderr.write(chunk);
24
26
  });
25
27
  try {
26
28
  await waitForSpawn(child);
@@ -112,7 +114,7 @@ async function runSession(child, request, outputMode, report, harness) {
112
114
  if (result.stopReason !== "end_turn") {
113
115
  throw new Error(`ACP Agent stopped with "${result.stopReason}"`);
114
116
  }
115
- return returnedEvent(output, outputMode === "capture");
117
+ return readAgentEvent(output, { adapter: "ACP", includeMessage: outputMode === "capture" });
116
118
  });
117
119
  }
118
120
  function identityUpdate(options, harness) {
@@ -137,45 +139,6 @@ function identityUpdate(options, harness) {
137
139
  ...(thinking === undefined ? {} : { thinking }),
138
140
  };
139
141
  }
140
- function agentPrompt(request) {
141
- return [
142
- request.prompt,
143
- "",
144
- "Complete the work using the available tools.",
145
- `Allowed outcome types: ${request.outcomes.join(", ")}.`,
146
- "When finished, end your final response with exactly one line in this form:",
147
- 'MACHINES_EVENT {"type":"completed"}',
148
- "Replace completed with exactly one allowed outcome type.",
149
- "Include additional JSON fields only when the task asks for them.",
150
- ].join("\n");
151
- }
152
- function returnedEvent(output, includeMessage) {
153
- const matches = [...output.matchAll(/^MACHINES_EVENT (.+)$/gmu)];
154
- const encoded = matches.at(-1)?.[1];
155
- if (encoded === undefined) {
156
- throw new Error("ACP Agent finished without returning a Machines event");
157
- }
158
- let event;
159
- try {
160
- event = JSON.parse(encoded);
161
- }
162
- catch (cause) {
163
- throw new Error("ACP Agent returned invalid event JSON", { cause });
164
- }
165
- if (event === null
166
- || typeof event !== "object"
167
- || !("type" in event)
168
- || typeof event.type !== "string") {
169
- throw new Error("ACP Agent returned an invalid Machines event");
170
- }
171
- const result = event;
172
- const message = output
173
- .replace(/^MACHINES_EVENT .+$(?:\r?\n)?/gmu, "")
174
- .trim();
175
- return includeMessage && message !== ""
176
- ? { ...result, message }
177
- : result;
178
- }
179
142
  function waitForSpawn(child) {
180
143
  return new Promise((resolve, reject) => {
181
144
  child.once("spawn", resolve);
@@ -5,11 +5,17 @@ export interface AgentPreset {
5
5
  readonly harness?: string;
6
6
  readonly model?: string;
7
7
  readonly thinking?: string;
8
+ /** Presets that take over at a usage limit, in order, instead of the shared list. `[]` never swaps. */
9
+ readonly fallback?: readonly string[];
8
10
  }
9
11
  export interface LoadedAgentBindings {
10
12
  readonly agents: Readonly<Record<string, AgentRunner>>;
11
13
  readonly presets: Readonly<Record<string, AgentPreset>>;
12
14
  readonly sources: Readonly<Record<string, string>>;
15
+ /** The shared fallback list from the nearest file that exports one. */
16
+ readonly fallback?: readonly string[];
13
17
  }
14
18
  export type AgentAdapters = Readonly<Record<string, unknown>>;
15
19
  export declare function loadAgentBindings(cwd?: string, home?: string, adapters?: AgentAdapters): Promise<LoadedAgentBindings>;
20
+ /** The presets that take over from one preset at a usage limit: its own chain, else the shared list without itself. */
21
+ export declare function fallbackPresets(name: string, preset: Pick<AgentPreset, "fallback">, shared: readonly string[] | undefined): readonly string[] | undefined;
@@ -10,7 +10,7 @@ import { stat } from "node:fs/promises";
10
10
  import { join, resolve } from "node:path";
11
11
  import { pathToFileURL } from "node:url";
12
12
  import { machinesUserHome, nearestMachinesDirectory } from "./discovery.js";
13
- const presetFields = new Set(["description", "runner", "harness", "model", "thinking"]);
13
+ const presetFields = new Set(["description", "runner", "harness", "model", "thinking", "fallback"]);
14
14
  export async function loadAgentBindings(cwd = process.cwd(), home = machinesUserHome(), adapters = {}) {
15
15
  const globalPath = join(home, ".machines", "agents.ts");
16
16
  const projectDirectory = await nearestMachinesDirectory(cwd);
@@ -23,17 +23,24 @@ export async function loadAgentBindings(cwd = process.cwd(), home = machinesUser
23
23
  const agents = {};
24
24
  const presets = {};
25
25
  const sources = {};
26
+ let fallback;
26
27
  for (const path of paths) {
27
28
  if (!await isFile(path))
28
29
  continue;
29
- const bindings = await loadBindingFile(path, adapters);
30
- for (const [name, preset] of Object.entries(bindings)) {
30
+ const loaded = await loadBindingFile(path, adapters);
31
+ for (const [name, preset] of Object.entries(loaded.presets)) {
31
32
  agents[name] = preset.runner;
32
33
  presets[name] = preset;
33
34
  sources[name] = path;
34
35
  }
36
+ // The nearer file's list replaces the other; the lists are never merged.
37
+ fallback = loaded.fallback ?? fallback;
35
38
  }
36
- return { agents, presets, sources };
39
+ return { agents, presets, sources, ...(fallback === undefined ? {} : { fallback }) };
40
+ }
41
+ /** The presets that take over from one preset at a usage limit: its own chain, else the shared list without itself. */
42
+ export function fallbackPresets(name, preset, shared) {
43
+ return preset.fallback ?? shared?.filter((other) => other !== name);
37
44
  }
38
45
  async function loadBindingFile(path, adapters) {
39
46
  let loaded;
@@ -82,9 +89,32 @@ async function loadBindingFile(path, adapters) {
82
89
  ...optionalField(candidate.harness, "harness", name, path),
83
90
  ...optionalField(candidate.model, "model", name, path),
84
91
  ...optionalField(candidate.thinking, "thinking", name, path),
92
+ ...(candidate.fallback === undefined
93
+ ? {}
94
+ : { fallback: fallbackNames(candidate.fallback, `Agent preset "${name}" fallback in "${path}"`, name) }),
85
95
  };
86
96
  }
87
- return bindings;
97
+ const shared = loaded.fallback;
98
+ return {
99
+ presets: bindings,
100
+ ...(shared === undefined ? {} : { fallback: fallbackNames(shared, `Agent bindings "${path}" fallback export`) }),
101
+ };
102
+ }
103
+ function fallbackNames(value, subject, self) {
104
+ if (!Array.isArray(value))
105
+ throw new Error(`${subject} must be an array of preset names`);
106
+ const names = [];
107
+ for (const name of value) {
108
+ if (typeof name !== "string" || name.trim() === "" || name !== name.trim() || /\r|\n|\t/u.test(name)) {
109
+ throw new Error(`${subject} must contain only non-empty, trimmed, one-line preset names`);
110
+ }
111
+ if (names.includes(name))
112
+ throw new Error(`${subject} names "${name}" more than once`);
113
+ if (name === self)
114
+ throw new Error(`${subject} must not name the preset itself`);
115
+ names.push(name);
116
+ }
117
+ return names;
88
118
  }
89
119
  function optionalField(value, field, name, path) {
90
120
  return value === undefined
@@ -0,0 +1,25 @@
1
+ /** A provider usage limit that ended an Agent turn. */
2
+ export interface AgentLimit {
3
+ /** The runner's harness label, for example "claude" or "codex". */
4
+ readonly harness: string;
5
+ /** The preset name. Runners leave it unset; the caller that selected the preset fills it in. */
6
+ readonly agent?: string;
7
+ /** The provider's name for the limit when known, for example "five_hour", "seven_day_opus", or "usage". */
8
+ readonly kind?: string;
9
+ /** Epoch milliseconds, only when the provider states when the limit resets. */
10
+ readonly resetsAt?: number;
11
+ }
12
+ declare const brand: unique symbol;
13
+ /** Thrown by an Agent runner when the provider refuses a turn because a usage limit is reached. */
14
+ export declare class AgentLimitError extends Error {
15
+ readonly limit: AgentLimit;
16
+ /** Every limit behind the failure when several presets were tried, each naming its preset in `agent`. */
17
+ readonly limits?: readonly AgentLimit[];
18
+ readonly [brand] = true;
19
+ constructor(message: string, limit: AgentLimit, options?: ErrorOptions & {
20
+ readonly limits?: readonly AgentLimit[];
21
+ });
22
+ }
23
+ /** Returns the usage limit behind a failure, searching its `cause` chain. */
24
+ export declare function agentLimitOf(cause: unknown): AgentLimit | undefined;
25
+ export {};
@@ -0,0 +1,34 @@
1
+ // A registered symbol identifies the error across module copies (source, `dist`,
2
+ // another package's installed copy), where `instanceof` would not.
3
+ const brand = Symbol.for("machines.agent-limit");
4
+ const maximumCauseDepth = 8;
5
+ /** Thrown by an Agent runner when the provider refuses a turn because a usage limit is reached. */
6
+ export class AgentLimitError extends Error {
7
+ limit;
8
+ /** Every limit behind the failure when several presets were tried, each naming its preset in `agent`. */
9
+ limits;
10
+ [brand] = true;
11
+ constructor(message, limit, options) {
12
+ super(message, options);
13
+ this.name = "AgentLimitError";
14
+ this.limit = limit;
15
+ if (options?.limits !== undefined)
16
+ this.limits = options.limits;
17
+ }
18
+ }
19
+ /** Returns the usage limit behind a failure, searching its `cause` chain. */
20
+ export function agentLimitOf(cause) {
21
+ let current = cause;
22
+ for (let depth = 0; depth < maximumCauseDepth; depth += 1) {
23
+ if (current === null || typeof current !== "object")
24
+ return undefined;
25
+ if (brand in current && current[brand] === true && "limit" in current && isAgentLimit(current.limit)) {
26
+ return current.limit;
27
+ }
28
+ current = "cause" in current ? current.cause : undefined;
29
+ }
30
+ return undefined;
31
+ }
32
+ function isAgentLimit(value) {
33
+ return value !== null && typeof value === "object" && "harness" in value && typeof value.harness === "string";
34
+ }
@@ -0,0 +1,37 @@
1
+ import { type AgentLimit } from "./agent-limit.ts";
2
+ import type { AgentRunner } from "./index.ts";
3
+ export interface LimitPolicy {
4
+ /** Longest one invocation waits for a reset, from its first wait. `0` never waits. */
5
+ readonly maxWaitMs: number;
6
+ /** How long a limit with no stated reset is trusted before the request is tried again. */
7
+ readonly probeMs: number;
8
+ readonly now?: () => number;
9
+ /** A replacement must let `now` advance by about the time it was asked to sleep. */
10
+ readonly sleep?: (ms: number) => Promise<void>;
11
+ }
12
+ export interface LimitCandidate {
13
+ /** The preset name. */
14
+ readonly name: string;
15
+ readonly runner: AgentRunner;
16
+ readonly harness?: string;
17
+ }
18
+ interface RememberedLimit {
19
+ readonly limit: AgentLimit;
20
+ /** When the preset is worth trying again. */
21
+ readonly until: number;
22
+ /** Whether `until` is the provider's stated reset rather than the next probe. */
23
+ readonly known: boolean;
24
+ }
25
+ /** Presets known to be at a limit, by name. One per prepared run, shared by its roles, never stored. */
26
+ export type LimitMemory = Map<string, RememberedLimit>;
27
+ export declare const defaultProbeMs: number;
28
+ export declare function createLimitMemory(): LimitMemory;
29
+ /**
30
+ * Runs one role's request on the first candidate that is not at a usage limit.
31
+ * A limited candidate hands the same request to the next one; when all are
32
+ * limited the request waits for a reset within the policy, or fails.
33
+ * A request with `limits: "report"` is tried on the bound candidate alone, and
34
+ * its limit is thrown at once.
35
+ */
36
+ export declare function limitAwareRunner(role: string, candidates: readonly LimitCandidate[], memory: LimitMemory, policy: LimitPolicy): AgentRunner;
37
+ export {};