@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
@@ -0,0 +1,27 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { deepseekAgent } from "../src/deepseek.js";
6
+ import { agent, final, machine, operation, run } from "../src/index.js";
7
+ // Requires an installed, configured dsh ACP profile. May use model credits.
8
+ // No automatic permission approval; all work is requested in this temporary cwd.
9
+ const cwd = await mkdtemp(join(tmpdir(), "machines-deepseek-example-"));
10
+ try {
11
+ const workflow = machine({
12
+ initial: "write",
13
+ states: {
14
+ write: agent('Create hello.txt in your working directory containing exactly "Hello from DeepSeek.\\n". Do not modify any other files.', { completed: "verify" }, { cwd }),
15
+ verify: operation(async () => {
16
+ assert.equal(await readFile(join(cwd, "hello.txt"), "utf8"), "Hello from DeepSeek.\n");
17
+ return { type: "verified" };
18
+ }, { verified: "done" }),
19
+ done: final(),
20
+ },
21
+ });
22
+ const result = await run(workflow, { agents: { default: deepseekAgent() } });
23
+ console.log(`write --completed--> verify --verified--> ${String(result.value)}`);
24
+ }
25
+ finally {
26
+ await rm(cwd, { recursive: true, force: true });
27
+ }
@@ -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,12 +170,14 @@ 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
  }
167
178
  async start(input) {
168
- const run = await this.#session.start(parse(startInput, input));
179
+ // This server publishes its runs itself, so that they also take responses through their inbox.
180
+ const run = await this.#session.start({ ...parse(startInput, input), publishRunStatus: false });
169
181
  return result(`Started ${run.machine}.\nRun id: ${run.id}\nStatus: ${run.status}`, { run });
170
182
  }
171
183
  status(input) {
@@ -179,6 +191,58 @@ export class MachineSession {
179
191
  }
180
192
  close() {
181
193
  this.#session.close();
194
+ // Closing terminates the session's runs without a final event.
195
+ for (const publisher of this.#published.values())
196
+ publisher.finish({ error: "Machine session closed" });
197
+ this.#published.clear();
198
+ for (const inbox of this.#inboxes.values())
199
+ inbox.stop();
200
+ this.#inboxes.clear();
201
+ }
202
+ #publish(run) {
203
+ let publisher = this.#published.get(run.id);
204
+ if (publisher === undefined) {
205
+ publisher = createRunStatusPublisher({
206
+ id: run.id,
207
+ machine: run.machine,
208
+ path: run.path,
209
+ cwd: run.cwd,
210
+ startedAt: run.startedAt,
211
+ });
212
+ this.#published.set(run.id, publisher);
213
+ }
214
+ const human = run.status === "waiting" ? run.human : undefined;
215
+ this.#watchInbox(run.id, human);
216
+ if (run.status === "running" || run.status === "waiting") {
217
+ publisher.update({
218
+ status: run.status, state: run.state, agent: run.agent, limit: run.limit, human, delivery: this.#taking,
219
+ });
220
+ return;
221
+ }
222
+ publisher.finish(run.status === "completed" ? { state: run.state } : { error: run.error ?? "Machine run failed" });
223
+ this.#published.delete(run.id);
224
+ }
225
+ /** A published request can also be answered, or asked about, through the run's inbox by `machine respond`. */
226
+ #watchInbox(runId, request) {
227
+ const watched = this.#inboxes.get(runId);
228
+ if (watched?.requestId === request?.requestId)
229
+ return;
230
+ watched?.stop();
231
+ this.#inboxes.delete(runId);
232
+ const directory = runStatusDirectory();
233
+ if (request === undefined || directory === undefined)
234
+ return;
235
+ const stop = watchRunInbox(directory, runId, request, (response, delivery) => {
236
+ // The session leaves the request before either call returns. A tool call
237
+ // may have answered first; the session then refuses this one unpublished.
238
+ this.#taking = delivery;
239
+ const sent = typeof response === "string"
240
+ ? this.#session.respond({ runId, requestId: request.requestId, response })
241
+ : this.#session.ask({ runId, requestId: request.requestId, question: response.text });
242
+ this.#taking = undefined;
243
+ sent.catch(() => { });
244
+ });
245
+ this.#inboxes.set(runId, { requestId: request.requestId, stop });
182
246
  }
183
247
  }
184
248
  export function createMachinesMcpServer(widgetHtml = readFileSync(new URL("../codex/machines/ui/machines.html", import.meta.url), "utf8")) {
@@ -186,11 +250,8 @@ export function createMachinesMcpServer(widgetHtml = readFileSync(new URL("../co
186
250
  const server = new Server({ name: "machines", version: "0.1.0" }, {
187
251
  capabilities: { tools: {}, resources: {} },
188
252
  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.",
253
+ ...machineCoordinatorGuidelines,
254
+ ...machineSupervisionGuidelines,
194
255
  ].join(" "),
195
256
  });
196
257
  server.onclose = () => session.close();
@@ -264,6 +325,8 @@ function formatSnapshot(run) {
264
325
  run.agent.thinking === undefined ? undefined : `thinking ${run.agent.thinking}`,
265
326
  ].filter((part) => part !== undefined).join(" · ")}`);
266
327
  }
328
+ if (run.limit !== undefined)
329
+ lines.push(`Limit: ${run.limit.text}`);
267
330
  if (run.human !== undefined) {
268
331
  lines.push(`Human: ${run.human.prompt}`, `Request id: ${run.human.requestId}`);
269
332
  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
@@ -21,7 +21,8 @@ export function acpAgent(command, args = [], options = {}) {
21
21
  child.stderr.setEncoding("utf8");
22
22
  child.stderr.on("data", (chunk) => {
23
23
  stderr += chunk;
24
- process.stderr.write(chunk);
24
+ if (options.output !== "capture")
25
+ process.stderr.write(chunk);
25
26
  });
26
27
  try {
27
28
  await waitForSpawn(child);
@@ -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 {};