@dna113p/machines 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -11
- package/claude/statusline.mjs +277 -0
- package/dist/examples/chatgpt-web.d.ts +1 -0
- package/dist/examples/chatgpt-web.js +36 -0
- package/dist/examples/claude.d.ts +1 -0
- package/dist/examples/claude.js +30 -0
- package/dist/examples/codex.d.ts +1 -0
- package/dist/examples/codex.js +30 -0
- package/dist/examples/decision.d.ts +1 -0
- package/dist/examples/decision.js +66 -0
- package/dist/examples/deepseek.d.ts +1 -0
- package/dist/examples/deepseek.js +27 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +74 -11
- package/dist/pi-extension/index.js +16 -9
- package/dist/src/acp.js +2 -1
- package/dist/src/agent-bindings.d.ts +6 -0
- package/dist/src/agent-bindings.js +35 -5
- package/dist/src/agent-limit.d.ts +25 -0
- package/dist/src/agent-limit.js +34 -0
- package/dist/src/agent-limits.d.ts +37 -0
- package/dist/src/agent-limits.js +212 -0
- package/dist/src/agent-protocol.js +3 -1
- package/dist/src/catalog.d.ts +3 -0
- package/dist/src/catalog.js +84 -12
- package/dist/src/chatgpt-web.d.ts +143 -0
- package/dist/src/chatgpt-web.js +946 -0
- package/dist/src/claude.d.ts +35 -0
- package/dist/src/claude.js +294 -0
- package/dist/src/cli.js +282 -16
- package/dist/src/codex.d.ts +16 -0
- package/dist/src/codex.js +176 -0
- package/dist/src/conversation.d.ts +74 -0
- package/dist/src/conversation.js +46 -0
- package/dist/src/coordinator-guidance.d.ts +3 -0
- package/dist/src/coordinator-guidance.js +17 -0
- package/dist/src/decision.d.ts +29 -0
- package/dist/src/decision.js +67 -0
- package/dist/src/deepseek.d.ts +12 -0
- package/dist/src/deepseek.js +16 -0
- package/dist/src/host.d.ts +11 -0
- package/dist/src/host.js +153 -16
- package/dist/src/index.d.ts +39 -2
- package/dist/src/index.js +62 -5
- package/dist/src/jev.d.ts +15 -0
- package/dist/src/jev.js +90 -0
- package/dist/src/json.d.ts +6 -0
- package/dist/src/json.js +28 -0
- package/dist/src/launcher.d.ts +11 -1
- package/dist/src/launcher.js +72 -3
- package/dist/src/laya.d.ts +15 -0
- package/dist/src/laya.js +13 -0
- package/dist/src/openrouter.d.ts +16 -0
- package/dist/src/openrouter.js +97 -0
- package/dist/src/run-inbox.d.ts +49 -0
- package/dist/src/run-inbox.js +233 -0
- package/dist/src/run-status.d.ts +121 -0
- package/dist/src/run-status.js +336 -0
- package/dist/src/run-watch.d.ts +24 -0
- package/dist/src/run-watch.js +112 -0
- package/dist/src/session.d.ts +15 -2
- package/dist/src/session.js +47 -8
- package/dist/src/system-one-http.d.ts +25 -0
- package/dist/src/system-one-http.js +146 -0
- package/dist/src/terminal-human.d.ts +8 -1
- package/dist/src/terminal-human.js +36 -8
- package/dist/src/von.d.ts +15 -0
- package/dist/src/von.js +13 -0
- package/docs/architecture.md +74 -3
- package/docs/authoring.md +809 -1
- package/docs/integrations.md +322 -3
- package/docs/local-decisions.md +212 -0
- package/package.json +21 -4
- package/services/laya/server.py +230 -0
- package/skills/machine-builder/SKILL.md +56 -6
- package/skills/machine-delegation/SKILL.md +128 -0
- package/skills/machine-delegation/references/delegation-machine.md +90 -0
- package/skills/machine-project-setup/SKILL.md +181 -0
- package/skills/machine-project-setup/references/project-workflows.md +50 -0
|
@@ -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
|
+
}
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -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>;
|
package/dist/mcp/server.js
CHANGED
|
@@ -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.
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
190
|
-
|
|
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:
|
|
109
|
-
promptSnippet: "Discover
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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
|
|
30
|
-
for (const [name, preset] of Object.entries(
|
|
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
|
-
|
|
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 {};
|