@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.
- package/README.md +120 -4
- package/claude/statusline.mjs +244 -0
- package/dist/examples/agy.d.ts +1 -0
- package/dist/examples/agy.js +30 -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 +72 -10
- package/dist/pi-extension/index.js +16 -9
- package/dist/src/acp.js +4 -41
- 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.d.ts +6 -0
- package/dist/src/agent-protocol.js +40 -0
- package/dist/src/agy.d.ts +11 -0
- package/dist/src/agy.js +168 -0
- package/dist/src/catalog.d.ts +3 -0
- package/dist/src/catalog.js +94 -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 +194 -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 +3 -0
- package/dist/src/host.js +68 -7
- 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 +229 -0
- package/dist/src/run-status.d.ts +97 -0
- package/dist/src/run-status.js +285 -0
- package/dist/src/session.d.ts +13 -0
- package/dist/src/session.js +41 -3
- 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 +69 -2
- package/docs/authoring.md +849 -2
- package/docs/integrations.md +243 -3
- package/docs/local-decisions.md +212 -0
- package/package.json +23 -4
- package/services/laya/server.py +230 -0
- package/skills/machine-builder/SKILL.md +56 -6
- package/skills/machine-delegation/SKILL.md +123 -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
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,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
|
-
|
|
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
|
-
|
|
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.",
|
|
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:
|
|
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
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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 {};
|