@dna113p/machines 0.1.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 (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +186 -0
  3. package/dist/codex/machines/ui/machines.html +286 -0
  4. package/dist/examples/agent.d.ts +1 -0
  5. package/dist/examples/agent.js +19 -0
  6. package/dist/examples/choices.d.ts +1 -0
  7. package/dist/examples/choices.js +17 -0
  8. package/dist/examples/host.d.ts +3 -0
  9. package/dist/examples/host.js +32 -0
  10. package/dist/examples/human.d.ts +1 -0
  11. package/dist/examples/human.js +17 -0
  12. package/dist/examples/named-agents.d.ts +1 -0
  13. package/dist/examples/named-agents.js +38 -0
  14. package/dist/examples/operation.d.ts +1 -0
  15. package/dist/examples/operation.js +10 -0
  16. package/dist/examples/pi.d.ts +1 -0
  17. package/dist/examples/pi.js +29 -0
  18. package/dist/mcp/server.d.ts +17 -0
  19. package/dist/mcp/server.js +313 -0
  20. package/dist/pi-extension/index.d.ts +52 -0
  21. package/dist/pi-extension/index.js +390 -0
  22. package/dist/src/acp.d.ts +7 -0
  23. package/dist/src/acp.js +184 -0
  24. package/dist/src/agent-bindings.d.ts +15 -0
  25. package/dist/src/agent-bindings.js +113 -0
  26. package/dist/src/catalog.d.ts +31 -0
  27. package/dist/src/catalog.js +68 -0
  28. package/dist/src/cli.d.ts +2 -0
  29. package/dist/src/cli.js +392 -0
  30. package/dist/src/discovery-worker.d.ts +3 -0
  31. package/dist/src/discovery-worker.js +52 -0
  32. package/dist/src/discovery.d.ts +8 -0
  33. package/dist/src/discovery.js +94 -0
  34. package/dist/src/host.d.ts +25 -0
  35. package/dist/src/host.js +357 -0
  36. package/dist/src/index.d.ts +210 -0
  37. package/dist/src/index.js +204 -0
  38. package/dist/src/launcher.d.ts +19 -0
  39. package/dist/src/launcher.js +98 -0
  40. package/dist/src/machine-module.d.ts +3 -0
  41. package/dist/src/machine-module.js +55 -0
  42. package/dist/src/session.d.ts +40 -0
  43. package/dist/src/session.js +165 -0
  44. package/dist/src/terminal-human.d.ts +2 -0
  45. package/dist/src/terminal-human.js +100 -0
  46. package/docs/architecture.md +111 -0
  47. package/docs/authoring.md +193 -0
  48. package/docs/integrations.md +142 -0
  49. package/package.json +58 -0
  50. package/skills/machine-builder/SKILL.md +34 -0
@@ -0,0 +1,390 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { realpathSync } from "node:fs";
10
+ import { dirname, resolve } from "node:path";
11
+ import { fileURLToPath, pathToFileURL } from "node:url";
12
+ import { Type } from "typebox";
13
+ const sourceFile = realpathSync(fileURLToPath(import.meta.url));
14
+ const sourceRoot = resolve(dirname(sourceFile), "..");
15
+ const moduleExtension = sourceFile.endsWith(".ts") ? "ts" : "js";
16
+ let launcherModule;
17
+ let sessionModule;
18
+ function loadLauncher() {
19
+ launcherModule ??= import(__rewriteRelativeImportExtension(pathToFileURL(resolve(sourceRoot, `src/launcher.${moduleExtension}`)).href));
20
+ return launcherModule;
21
+ }
22
+ async function loadSession() {
23
+ await loadLauncher();
24
+ sessionModule ??= import(__rewriteRelativeImportExtension(pathToFileURL(resolve(sourceRoot, `src/session.${moduleExtension}`)).href));
25
+ return sessionModule;
26
+ }
27
+ const widgetKey = "machines";
28
+ const terminalWidgetDurationMs = 6_000;
29
+ export default function machinesExtension(pi) {
30
+ let session;
31
+ let pendingSession;
32
+ let ui;
33
+ let elapsedTimer;
34
+ let shuttingDown = false;
35
+ const dialogAbort = new AbortController();
36
+ let dialogQueue = Promise.resolve();
37
+ const useContext = (context) => {
38
+ ui = context.ui;
39
+ };
40
+ const renderWidget = () => {
41
+ if (ui === undefined)
42
+ return;
43
+ const now = Date.now();
44
+ const visible = (session?.status() ?? []).filter((run) => isActive(run.status) || now - Date.parse(run.updatedAt) < terminalWidgetDurationMs);
45
+ if (visible.length === 0) {
46
+ ui.setWidget(widgetKey, undefined);
47
+ stopTimer();
48
+ return;
49
+ }
50
+ ui.setWidget(widgetKey, (_tui, theme) => ({
51
+ render: (width) => widgetLines(visible, theme, width),
52
+ invalidate: () => { },
53
+ }), { placement: "aboveEditor" });
54
+ startTimer();
55
+ };
56
+ const startTimer = () => {
57
+ if (elapsedTimer !== undefined)
58
+ return;
59
+ elapsedTimer = setInterval(renderWidget, 1_000);
60
+ elapsedTimer.unref();
61
+ };
62
+ const stopTimer = () => {
63
+ if (elapsedTimer === undefined)
64
+ return;
65
+ clearInterval(elapsedTimer);
66
+ elapsedTimer = undefined;
67
+ };
68
+ const getSession = () => {
69
+ pendingSession ??= loadSession().then(({ MachineSession }) => {
70
+ session = new MachineSession();
71
+ if (shuttingDown)
72
+ session.close();
73
+ session.subscribe(({ type, run }) => {
74
+ renderWidget();
75
+ if (shuttingDown)
76
+ return;
77
+ if (type === "human" && run.human !== undefined) {
78
+ ui?.notify(`${run.machine} needs input (${shortId(run.id)})`, "warning");
79
+ enqueueHumanDialog(run.id, run.human);
80
+ }
81
+ else if (type === "finished") {
82
+ ui?.notify(`${run.machine} ${run.status} (${shortId(run.id)})${run.error === undefined ? "" : `: ${run.error}`}`, run.status === "completed" ? "info" : "error");
83
+ }
84
+ });
85
+ return session;
86
+ });
87
+ return pendingSession;
88
+ };
89
+ const enqueueHumanDialog = (id, request) => {
90
+ dialogQueue = dialogQueue.then(async () => {
91
+ const activeUi = ui;
92
+ const waiting = () => !shuttingDown
93
+ && session?.status().some((run) => run.id === id && run.human?.requestId === request.requestId);
94
+ if (activeUi === undefined || !waiting())
95
+ return;
96
+ const response = await requestHumanResponse(activeUi, request, dialogAbort.signal);
97
+ if (response === undefined || !waiting())
98
+ return;
99
+ await session?.respond({ runId: id, requestId: request.requestId, response });
100
+ }).catch((cause) => {
101
+ if (!shuttingDown)
102
+ ui?.notify(`Could not open Machine input: ${errorMessage(cause)}`, "error");
103
+ });
104
+ };
105
+ pi.registerTool({
106
+ name: "machine_list",
107
+ 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
+ ],
114
+ parameters: Type.Object({}, { additionalProperties: false }),
115
+ executionMode: "parallel",
116
+ async execute(_id, _params, _signal, _onUpdate, context) {
117
+ useContext(context);
118
+ const { listMachines } = await loadLauncher();
119
+ const machines = await listMachines({ cwd: context.cwd });
120
+ const text = machines.length === 0
121
+ ? "No Machines found."
122
+ : machines.map((machine) => {
123
+ const roles = Object.keys(machine.agentRoles);
124
+ const roleText = roles.length === 0 ? "no Agent roles" : `roles: ${roles.join(", ")}`;
125
+ const missing = machine.error !== undefined
126
+ ? `; invalid: ${machine.error}`
127
+ : machine.missingAgents.length === 0
128
+ ? ""
129
+ : `; missing: ${machine.missingAgents.join(", ")}`;
130
+ return `${machine.name} — ${machine.description} (${roleText}${missing})\n ${machine.path}`;
131
+ }).join("\n");
132
+ return result(text, {
133
+ summary: `${machines.length} Machine${machines.length === 1 ? "" : "s"}`,
134
+ machines,
135
+ });
136
+ },
137
+ renderCall: (_params, theme) => line(theme.fg("toolTitle", theme.bold("machine_list"))),
138
+ renderResult: compactResult,
139
+ });
140
+ pi.registerTool({
141
+ name: "machine_agents",
142
+ label: "List Machine Agents",
143
+ description: "List configured Agent presets available to Machine role bindings.",
144
+ promptGuidelines: [
145
+ "Use machine_agents before overriding a Machine Agent role; do not invent preset names or inline harness configuration.",
146
+ ],
147
+ parameters: Type.Object({}, { additionalProperties: false }),
148
+ executionMode: "parallel",
149
+ async execute(_id, _params, _signal, _onUpdate, context) {
150
+ useContext(context);
151
+ const { listAgentPresets } = await loadLauncher();
152
+ const agents = await listAgentPresets({ cwd: context.cwd });
153
+ const text = agents.length === 0
154
+ ? "No Agent presets configured."
155
+ : agents.map((agent) => {
156
+ const identity = [agent.harness, agent.model, agent.thinking]
157
+ .filter((part) => part !== undefined)
158
+ .join(" · ");
159
+ return `${agent.name} — ${agent.description}${identity === "" ? "" : ` (${identity})`}\n ${agent.source}`;
160
+ }).join("\n");
161
+ return result(text, {
162
+ summary: `${agents.length} Agent preset${agents.length === 1 ? "" : "s"}`,
163
+ agents,
164
+ });
165
+ },
166
+ renderCall: (_params, theme) => line(theme.fg("toolTitle", theme.bold("machine_agents"))),
167
+ renderResult: compactResult,
168
+ });
169
+ pi.registerTool({
170
+ name: "machine_start",
171
+ label: "Start Machine",
172
+ description: "Start one Machine asynchronously. Returns a run id immediately after preflight.",
173
+ promptGuidelines: [
174
+ "machine_start is asynchronous: after it returns, continue the conversation and use machine_status only when status is relevant.",
175
+ "Turn the Human's goal into clear Machine input without adding requirements or changing its intended scope.",
176
+ "Pass Agent overrides only as Machine role-to-preset names previously discovered with machine_agents.",
177
+ ],
178
+ parameters: Type.Object({
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" })),
181
+ agents: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Optional Machine role to configured Agent preset name" })),
182
+ }, { additionalProperties: false }),
183
+ async execute(_id, rawParams, _signal, _onUpdate, context) {
184
+ useContext(context);
185
+ const machine = requiredString(rawParams.machine, "machine");
186
+ const input = optionalString(rawParams.input, "input");
187
+ const agents = optionalStringRecord(rawParams.agents, "agents");
188
+ const runs = await getSession();
189
+ const run = await runs.start({
190
+ machine,
191
+ cwd: context.cwd,
192
+ ...(input === undefined ? {} : { input }),
193
+ ...(agents === undefined ? {} : { agents }),
194
+ });
195
+ return result(`Started ${run.machine}.\nRun id: ${run.id}\nStatus: ${run.status}`, { summary: `started ${run.machine} · ${shortId(run.id)}`, run });
196
+ },
197
+ renderCall: (params, theme) => line(`${theme.fg("toolTitle", theme.bold("machine_start"))} ${theme.fg("muted", String(params.machine ?? ""))}`),
198
+ renderResult: compactResult,
199
+ });
200
+ pi.registerTool({
201
+ name: "machine_status",
202
+ label: "Machine Status",
203
+ description: "Inspect one exact Machine run id, or all active and recently finished runs.",
204
+ parameters: Type.Object({
205
+ runId: Type.Optional(Type.String({ description: "Exact run id returned by machine_start" })),
206
+ }, { additionalProperties: false }),
207
+ executionMode: "parallel",
208
+ async execute(_id, rawParams, _signal, _onUpdate, context) {
209
+ useContext(context);
210
+ const runId = optionalString(rawParams.runId, "runId");
211
+ const snapshots = (await getSession()).status(runId);
212
+ const text = snapshots.length === 0
213
+ ? "No Machine runs in this Pi session."
214
+ : snapshots.map(formatSnapshot).join("\n\n");
215
+ return result(text, {
216
+ summary: snapshots.length === 0
217
+ ? "no runs"
218
+ : `${snapshots.length} run${snapshots.length === 1 ? "" : "s"}`,
219
+ runs: snapshots,
220
+ });
221
+ },
222
+ renderCall: (params, theme) => line(`${theme.fg("toolTitle", theme.bold("machine_status"))}${params.runId === undefined ? "" : ` ${theme.fg("muted", String(params.runId))}`}`),
223
+ renderResult: compactResult,
224
+ });
225
+ pi.registerTool({
226
+ name: "machine_respond",
227
+ label: "Respond to Machine",
228
+ description: "Send a clear Human response to one waiting Machine run.",
229
+ promptGuidelines: [
230
+ "Call machine_respond only when the Human's answer is clear, using the exact run id, current human.requestId, and an allowed choice when choices exist.",
231
+ ],
232
+ parameters: Type.Object({
233
+ runId: Type.String({ description: "Exact run id returned by machine_start" }),
234
+ requestId: Type.String({ description: "Exact human.requestId from the current waiting run snapshot" }),
235
+ response: Type.String({ description: "Human response or exact allowed choice" }),
236
+ }, { additionalProperties: false }),
237
+ async execute(_id, rawParams, _signal, _onUpdate, context) {
238
+ useContext(context);
239
+ const runId = requiredString(rawParams.runId, "runId");
240
+ const response = requiredString(rawParams.response, "response");
241
+ const requestId = requiredString(rawParams.requestId, "requestId");
242
+ const run = await (await getSession()).respond({ runId, requestId, response });
243
+ return result(`Sent response to ${run.machine}.\nRun id: ${run.id}\nStatus: ${run.status}`, { summary: `responded to ${run.machine} · ${shortId(run.id)}`, run });
244
+ },
245
+ renderCall: (params, theme) => line(`${theme.fg("toolTitle", theme.bold("machine_respond"))} ${theme.fg("muted", String(params.runId ?? ""))}`),
246
+ renderResult: compactResult,
247
+ });
248
+ pi.on("session_shutdown", (_event, context) => {
249
+ shuttingDown = true;
250
+ dialogAbort.abort();
251
+ stopTimer();
252
+ session?.close();
253
+ context.ui.setWidget(widgetKey, undefined);
254
+ ui = undefined;
255
+ });
256
+ }
257
+ function formatSnapshot(run) {
258
+ const lines = [
259
+ `${run.machine} (${run.status})`,
260
+ `Run id: ${run.id}`,
261
+ `State: ${formatState(run.state) ?? "starting"}`,
262
+ `Elapsed: ${run.elapsedSeconds}s`,
263
+ ];
264
+ if (run.agent !== undefined)
265
+ lines.push(`Agent: ${formatAgent(run.agent)}`);
266
+ if (run.human !== undefined) {
267
+ lines.push(`Human: ${run.human.prompt}`, `Request id: ${run.human.requestId}`);
268
+ if (run.human.choices !== undefined)
269
+ lines.push(`Choices: ${run.human.choices.join(", ")}`);
270
+ if (run.human.suggestions !== undefined) {
271
+ lines.push(`Suggestions: ${run.human.suggestions.join(", ")}`);
272
+ }
273
+ }
274
+ if (run.error !== undefined)
275
+ lines.push(`Error: ${run.error}`);
276
+ return lines.join("\n");
277
+ }
278
+ function compactResult(toolResult, options, theme) {
279
+ if (options.expanded)
280
+ return line(toolResult.content[0].text);
281
+ const details = toolResult.details;
282
+ const summary = typeof details?.summary === "string" ? details.summary : "done";
283
+ return line(`${theme.fg("success", "✓")} ${summary} ${theme.fg("dim", "(Ctrl+O for details)")}`);
284
+ }
285
+ function widgetLines(runs, theme, width) {
286
+ const edge = (text) => theme.fg("borderMuted", text);
287
+ const title = " Machines ";
288
+ const topRule = "─".repeat(Math.max(1, width - title.length - 3));
289
+ const bottomRule = "─".repeat(Math.max(1, width - 2));
290
+ const lines = [
291
+ `${theme.fg("borderAccent", "┌─")}${theme.fg("accent", theme.bold(title))}${theme.fg("borderAccent", `${topRule}┐`)}`,
292
+ ];
293
+ for (const run of runs) {
294
+ const waiting = run.status === "waiting";
295
+ const completed = run.status === "completed";
296
+ const failed = run.status === "failed";
297
+ const color = waiting
298
+ ? "warning"
299
+ : completed
300
+ ? "success"
301
+ : failed
302
+ ? "error"
303
+ : "accent";
304
+ const symbol = waiting ? "◆" : completed ? "✓" : failed ? "✕" : "●";
305
+ const state = waiting
306
+ ? "input needed"
307
+ : failed
308
+ ? "failed"
309
+ : formatState(run.state) ?? "starting";
310
+ const end = completed || failed ? Date.parse(run.updatedAt) : undefined;
311
+ lines.push(`${edge("│")} ${theme.fg(color, symbol)} ${theme.fg("muted", shortId(run.id))} ${theme.bold(run.machine)} › ${theme.fg(color, state)} · ${theme.fg("dim", elapsed(Date.parse(run.startedAt), end))}`);
312
+ if (run.human !== undefined) {
313
+ for (const promptLine of run.human.prompt.split("\n")) {
314
+ lines.push(`${edge("│")} ${theme.fg("warning", promptLine)}`);
315
+ }
316
+ }
317
+ else if (run.agent !== undefined) {
318
+ lines.push(`${edge("│")} ${theme.fg("muted", formatAgent(run.agent))}`);
319
+ }
320
+ }
321
+ lines.push(edge(`└${bottomRule}┘`));
322
+ return lines;
323
+ }
324
+ async function requestHumanResponse(ui, request, signal) {
325
+ if (request.choices !== undefined) {
326
+ return ui.select(request.prompt, [...request.choices], { signal });
327
+ }
328
+ if (request.suggestions !== undefined) {
329
+ const selected = await ui.select(request.prompt, [...request.suggestions, "Other…"], { signal });
330
+ if (selected !== "Other…")
331
+ return selected;
332
+ }
333
+ return ui.input(request.prompt, "Type your response", { signal });
334
+ }
335
+ function result(text, details) {
336
+ return { content: [{ type: "text", text }], details };
337
+ }
338
+ function line(text) {
339
+ return {
340
+ render: () => text.split("\n"),
341
+ invalidate: () => { },
342
+ };
343
+ }
344
+ function requiredString(value, name) {
345
+ if (typeof value !== "string" || value === "")
346
+ throw new Error(`${name} must be a non-empty string`);
347
+ return value;
348
+ }
349
+ function optionalString(value, name) {
350
+ if (value === undefined)
351
+ return undefined;
352
+ return requiredString(value, name);
353
+ }
354
+ function optionalStringRecord(value, name) {
355
+ if (value === undefined)
356
+ return undefined;
357
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
358
+ throw new Error(`${name} must map Machine roles to Agent preset names`);
359
+ }
360
+ const entries = Object.entries(value);
361
+ if (entries.some(([key, item]) => key === "" || typeof item !== "string" || item === "")) {
362
+ throw new Error(`${name} must map non-empty Machine roles to non-empty Agent preset names`);
363
+ }
364
+ return Object.fromEntries(entries);
365
+ }
366
+ function shortId(id) {
367
+ return id.slice(0, 8);
368
+ }
369
+ function isActive(status) {
370
+ return status === "running" || status === "waiting";
371
+ }
372
+ function elapsed(startedAt, endedAt = Date.now()) {
373
+ const seconds = Math.floor((endedAt - startedAt) / 1_000);
374
+ if (seconds < 60)
375
+ return `${seconds}s`;
376
+ return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
377
+ }
378
+ function formatState(state) {
379
+ if (state === undefined)
380
+ return undefined;
381
+ return typeof state === "string" ? state : JSON.stringify(state);
382
+ }
383
+ function formatAgent(agent) {
384
+ return [agent.harness, agent.model, agent.thinking === undefined ? undefined : `thinking ${agent.thinking}`]
385
+ .filter((part) => part !== undefined)
386
+ .join(" · ");
387
+ }
388
+ function errorMessage(cause) {
389
+ return cause instanceof Error ? cause.message : String(cause);
390
+ }
@@ -0,0 +1,7 @@
1
+ import type { AgentRunner } from "./index.ts";
2
+ export interface AcpAgentOptions {
3
+ readonly env?: Readonly<Record<string, string>>;
4
+ readonly harness?: string;
5
+ readonly output?: "capture" | "stream";
6
+ }
7
+ export declare function acpAgent(command: string, args?: readonly string[], options?: AcpAgentOptions): AgentRunner;
@@ -0,0 +1,184 @@
1
+ import { spawn } from "node:child_process";
2
+ import { Readable, Writable } from "node:stream";
3
+ import * as acp from "@agentclientprotocol/sdk";
4
+ export function acpAgent(command, args = [], options = {}) {
5
+ return async (request, report) => {
6
+ if (request.cwd === undefined) {
7
+ throw new Error("ACP Agent requires a working directory");
8
+ }
9
+ if (request.outcomes.length === 0) {
10
+ throw new Error("ACP Agent requires at least one allowed outcome");
11
+ }
12
+ const harness = options.harness ?? command;
13
+ report?.({ type: "identity", harness });
14
+ const child = spawn(command, [...args], {
15
+ cwd: request.cwd,
16
+ env: { ...process.env, ...options.env },
17
+ stdio: ["pipe", "pipe", "pipe"],
18
+ });
19
+ let stderr = "";
20
+ child.stderr.setEncoding("utf8");
21
+ child.stderr.on("data", (chunk) => {
22
+ stderr += chunk;
23
+ process.stderr.write(chunk);
24
+ });
25
+ try {
26
+ await waitForSpawn(child);
27
+ return await runSession(child, { ...request, cwd: request.cwd }, options.output ?? "stream", report, harness);
28
+ }
29
+ catch (cause) {
30
+ const message = cause instanceof Error ? cause.message : String(cause);
31
+ const detail = stderr.trim();
32
+ throw new Error(`ACP Agent "${command}" failed in "${request.cwd}": ${message}${detail === "" ? "" : `\n${detail}`}`, { cause });
33
+ }
34
+ finally {
35
+ child.kill();
36
+ }
37
+ };
38
+ }
39
+ async function runSession(child, request, outputMode, report, harness) {
40
+ const stream = acp.ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
41
+ let output = "";
42
+ let permissionError;
43
+ return acp
44
+ .client({ name: "machines" })
45
+ .onNotification(acp.methods.client.session.update, ({ params }) => {
46
+ const update = params.update;
47
+ if (update.sessionUpdate === "agent_message_chunk"
48
+ && update.content.type === "text") {
49
+ output += update.content.text;
50
+ report?.({ type: "output", text: update.content.text });
51
+ if (outputMode === "stream")
52
+ process.stdout.write(update.content.text);
53
+ }
54
+ else if (update.sessionUpdate === "tool_call") {
55
+ report?.({
56
+ type: "tool",
57
+ id: update.toolCallId,
58
+ title: update.title,
59
+ ...(update.status === undefined || update.status === null
60
+ ? {}
61
+ : { status: update.status }),
62
+ });
63
+ }
64
+ else if (update.sessionUpdate === "tool_call_update") {
65
+ report?.({
66
+ type: "tool",
67
+ id: update.toolCallId,
68
+ ...(update.title === undefined || update.title === null
69
+ ? {}
70
+ : { title: update.title }),
71
+ ...(update.status === undefined || update.status === null
72
+ ? {}
73
+ : { status: update.status }),
74
+ });
75
+ }
76
+ else if (update.sessionUpdate === "config_option_update") {
77
+ const identity = identityUpdate(update.configOptions, harness);
78
+ if (identity !== undefined)
79
+ report?.(identity);
80
+ }
81
+ })
82
+ .onRequest(acp.methods.client.session.requestPermission, ({ params }) => {
83
+ const title = params.toolCall.title ?? params.toolCall.toolCallId;
84
+ permissionError = new Error(`ACP Agent requested permission for "${title}", but permission input is not implemented yet`);
85
+ const rejection = params.options.find((option) => option.kind === "reject_once")
86
+ ?? params.options.find((option) => option.kind === "reject_always");
87
+ return rejection === undefined
88
+ ? { outcome: { outcome: "cancelled" } }
89
+ : { outcome: { outcome: "selected", optionId: rejection.optionId } };
90
+ })
91
+ .connectWith(stream, async (connection) => {
92
+ await connection.request(acp.methods.agent.initialize, {
93
+ protocolVersion: acp.PROTOCOL_VERSION,
94
+ clientCapabilities: {},
95
+ });
96
+ const session = await connection.request(acp.methods.agent.session.new, {
97
+ cwd: request.cwd,
98
+ mcpServers: [],
99
+ });
100
+ const identity = identityUpdate(session.configOptions, harness);
101
+ if (identity !== undefined)
102
+ report?.(identity);
103
+ const result = await connection.request(acp.methods.agent.session.prompt, {
104
+ sessionId: session.sessionId,
105
+ prompt: [{ type: "text", text: agentPrompt(request) }],
106
+ });
107
+ if (outputMode === "stream" && output !== "" && !output.endsWith("\n")) {
108
+ process.stdout.write("\n");
109
+ }
110
+ if (permissionError !== undefined)
111
+ throw permissionError;
112
+ if (result.stopReason !== "end_turn") {
113
+ throw new Error(`ACP Agent stopped with "${result.stopReason}"`);
114
+ }
115
+ return returnedEvent(output, outputMode === "capture");
116
+ });
117
+ }
118
+ function identityUpdate(options, harness) {
119
+ let model;
120
+ let thinking;
121
+ for (const option of options ?? []) {
122
+ if (option.type !== "select")
123
+ continue;
124
+ if (option.category === "model" || option.id === "model") {
125
+ model = option.currentValue;
126
+ }
127
+ else if (option.category === "thought_level" || option.id === "thought_level") {
128
+ thinking = option.currentValue;
129
+ }
130
+ }
131
+ return model === undefined && thinking === undefined
132
+ ? undefined
133
+ : {
134
+ type: "identity",
135
+ harness,
136
+ ...(model === undefined ? {} : { model }),
137
+ ...(thinking === undefined ? {} : { thinking }),
138
+ };
139
+ }
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
+ function waitForSpawn(child) {
180
+ return new Promise((resolve, reject) => {
181
+ child.once("spawn", resolve);
182
+ child.once("error", reject);
183
+ });
184
+ }
@@ -0,0 +1,15 @@
1
+ import type { AgentRunner } from "./index.ts";
2
+ export interface AgentPreset {
3
+ readonly description: string;
4
+ readonly runner: AgentRunner;
5
+ readonly harness?: string;
6
+ readonly model?: string;
7
+ readonly thinking?: string;
8
+ }
9
+ export interface LoadedAgentBindings {
10
+ readonly agents: Readonly<Record<string, AgentRunner>>;
11
+ readonly presets: Readonly<Record<string, AgentPreset>>;
12
+ readonly sources: Readonly<Record<string, string>>;
13
+ }
14
+ export type AgentAdapters = Readonly<Record<string, unknown>>;
15
+ export declare function loadAgentBindings(cwd?: string, home?: string, adapters?: AgentAdapters): Promise<LoadedAgentBindings>;