@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,29 @@
1
+ import { mkdtemp, readFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { agent, final, machine, operation, run, } from "../src/index.js";
5
+ import { acpAgent } from "../src/acp.js";
6
+ const workspace = await mkdtemp(join(tmpdir(), "machines-pi-"));
7
+ const helloPath = join(workspace, "hello.txt");
8
+ const example = machine({
9
+ initial: "write",
10
+ states: {
11
+ write: agent("Create hello.txt containing exactly: Hello from Machines.", { completed: "verify" }, { cwd: workspace }),
12
+ verify: operation(async () => {
13
+ const content = await readFile(helloPath, "utf8");
14
+ if (content.trim() !== "Hello from Machines.") {
15
+ throw new Error("hello.txt did not contain the expected text");
16
+ }
17
+ return { type: "passed" };
18
+ }, { passed: "done" }),
19
+ done: final(),
20
+ },
21
+ });
22
+ const result = await run(example, {
23
+ agents: {
24
+ default: acpAgent("npx", ["-y", "pi-acp"]),
25
+ },
26
+ });
27
+ console.log(`Pi workspace: ${workspace}`);
28
+ console.log(`hello.txt: ${(await readFile(helloPath, "utf8")).trim()}`);
29
+ console.log(`verified --> ${String(result.value)}`);
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { type CallToolResult } from "@modelcontextprotocol/sdk/types.js";
4
+ export type { RunSnapshot } from "../src/session.ts";
5
+ export declare class MachineSession {
6
+ #private;
7
+ list(input: unknown): Promise<CallToolResult>;
8
+ agents(input: unknown): Promise<CallToolResult>;
9
+ start(input: unknown): Promise<CallToolResult>;
10
+ status(input: unknown): CallToolResult;
11
+ respond(input: unknown): Promise<CallToolResult>;
12
+ close(): void;
13
+ }
14
+ export declare function createMachinesMcpServer(widgetHtml?: string): {
15
+ readonly server: Server;
16
+ readonly session: MachineSession;
17
+ };
@@ -0,0 +1,313 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { isAbsolute } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
8
+ import * as v from "valibot";
9
+ import { MachineSession as RunSession } from "../src/session.js";
10
+ import { listAgentPresets, listMachines } from "../src/launcher.js";
11
+ const widgetUri = "ui://machines/run-v1.html";
12
+ 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
+ const listInput = v.strictObject({ cwd });
14
+ const startInput = v.strictObject({
15
+ cwd,
16
+ machine: v.pipe(v.string(), v.nonEmpty("machine must not be empty")),
17
+ input: v.optional(v.string()),
18
+ 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
+ });
20
+ const statusInput = v.strictObject({
21
+ runId: v.optional(v.pipe(v.string(), v.nonEmpty("runId must not be empty"))),
22
+ });
23
+ const respondInput = v.strictObject({
24
+ requestId: v.pipe(v.string(), v.nonEmpty("requestId must not be empty")),
25
+ runId: v.pipe(v.string(), v.nonEmpty("runId must not be empty")),
26
+ response: v.string(),
27
+ });
28
+ const locationInputSchema = {
29
+ type: "object",
30
+ properties: {
31
+ cwd: {
32
+ type: "string",
33
+ description: "Absolute path to the current Codex project",
34
+ },
35
+ },
36
+ required: ["cwd"],
37
+ additionalProperties: false,
38
+ };
39
+ const tools = [
40
+ {
41
+ name: "machine_list",
42
+ title: "List Machines",
43
+ description: "List Machines available to the current project, why to use them, and any missing Agent bindings.",
44
+ inputSchema: locationInputSchema,
45
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
46
+ },
47
+ {
48
+ name: "machine_agents",
49
+ title: "List Machine Agents",
50
+ description: "List configured Agent presets before overriding a Machine's semantic Agent role.",
51
+ inputSchema: locationInputSchema,
52
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
53
+ },
54
+ {
55
+ name: "machine_start",
56
+ title: "Start Machine",
57
+ description: "Start one discovered Machine asynchronously. Preserve the user's requested scope and use only Agent presets returned by machine_agents.",
58
+ inputSchema: {
59
+ type: "object",
60
+ properties: {
61
+ cwd: {
62
+ type: "string",
63
+ description: "Absolute path to the current Codex project",
64
+ },
65
+ machine: {
66
+ type: "string",
67
+ description: "Exact Machine name returned by machine_list",
68
+ },
69
+ input: {
70
+ type: "string",
71
+ description: "Clear task or input for the Machine",
72
+ },
73
+ agents: {
74
+ type: "object",
75
+ description: "Optional Machine role to configured Agent preset name",
76
+ additionalProperties: { type: "string" },
77
+ },
78
+ },
79
+ required: ["cwd", "machine"],
80
+ additionalProperties: false,
81
+ },
82
+ _meta: {
83
+ ui: { resourceUri: widgetUri },
84
+ "openai/outputTemplate": widgetUri,
85
+ "openai/toolInvocation/invoking": "Starting Machine…",
86
+ "openai/toolInvocation/invoked": "Machine started.",
87
+ },
88
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
89
+ },
90
+ {
91
+ name: "machine_status",
92
+ title: "Machine Status",
93
+ description: "Read one exact Machine run, or all runs owned by this Codex session.",
94
+ inputSchema: {
95
+ type: "object",
96
+ properties: {
97
+ runId: {
98
+ type: "string",
99
+ description: "Exact run id returned by machine_start; omit to list this session's runs",
100
+ },
101
+ },
102
+ additionalProperties: false,
103
+ },
104
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
105
+ },
106
+ {
107
+ name: "machine_respond",
108
+ title: "Respond to Machine",
109
+ description: "Send Human input directly to one waiting Machine. Use an exact allowed choice when choices are present.",
110
+ inputSchema: {
111
+ type: "object",
112
+ properties: {
113
+ runId: {
114
+ type: "string",
115
+ description: "Exact run id returned by machine_start",
116
+ },
117
+ requestId: {
118
+ type: "string",
119
+ description: "Exact human.requestId from the current waiting run snapshot",
120
+ },
121
+ response: {
122
+ type: "string",
123
+ description: "Human response, or an exact allowed choice",
124
+ },
125
+ },
126
+ required: ["runId", "requestId", "response"],
127
+ additionalProperties: false,
128
+ },
129
+ _meta: {
130
+ "openai/toolInvocation/invoking": "Responding…",
131
+ "openai/toolInvocation/invoked": "Response sent.",
132
+ },
133
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
134
+ },
135
+ ];
136
+ export class MachineSession {
137
+ #session = new RunSession();
138
+ async list(input) {
139
+ const { cwd } = parse(listInput, input);
140
+ const machines = await listMachines({ cwd });
141
+ const text = machines.length === 0
142
+ ? "No Machines found."
143
+ : machines.map((machine) => {
144
+ const roles = Object.keys(machine.agentRoles);
145
+ const ready = machine.error !== undefined
146
+ ? `invalid: ${machine.error}`
147
+ : machine.missingAgents.length === 0
148
+ ? "ready"
149
+ : `missing: ${machine.missingAgents.join(", ")}`;
150
+ return `${machine.name} — ${machine.description}\n ${roles.length === 0 ? "no Agent roles" : `roles: ${roles.join(", ")}`} · ${ready}\n ${machine.path}`;
151
+ }).join("\n\n");
152
+ return result(text, { machines });
153
+ }
154
+ async agents(input) {
155
+ const { cwd } = parse(listInput, input);
156
+ const agents = await listAgentPresets({ cwd });
157
+ const text = agents.length === 0
158
+ ? "No Agent presets configured."
159
+ : agents.map((agent) => {
160
+ const identity = [agent.harness, agent.model, agent.thinking]
161
+ .filter((part) => part !== undefined)
162
+ .join(" · ");
163
+ return `${agent.name} — ${agent.description}${identity === "" ? "" : ` (${identity})`}\n ${agent.source}`;
164
+ }).join("\n\n");
165
+ return result(text, { agents });
166
+ }
167
+ async start(input) {
168
+ const run = await this.#session.start(parse(startInput, input));
169
+ return result(`Started ${run.machine}.\nRun id: ${run.id}\nStatus: ${run.status}`, { run });
170
+ }
171
+ status(input) {
172
+ const { runId } = parse(statusInput, input);
173
+ const runs = this.#session.status(runId);
174
+ return result(runs.length === 0 ? "No Machine runs in this Codex session." : runs.map(formatSnapshot).join("\n\n"), { runs });
175
+ }
176
+ async respond(input) {
177
+ const run = await this.#session.respond(parse(respondInput, input));
178
+ return result(`Sent response to ${run.machine}.\nRun id: ${run.id}\nStatus: ${run.status}`, { run });
179
+ }
180
+ close() {
181
+ this.#session.close();
182
+ }
183
+ }
184
+ export function createMachinesMcpServer(widgetHtml = readFileSync(new URL("../codex/machines/ui/machines.html", import.meta.url), "utf8")) {
185
+ const session = new MachineSession();
186
+ const server = new Server({ name: "machines", version: "0.1.0" }, {
187
+ capabilities: { tools: {}, resources: {} },
188
+ 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.",
194
+ ].join(" "),
195
+ });
196
+ server.onclose = () => session.close();
197
+ server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: [...tools] }));
198
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
199
+ try {
200
+ const input = request.params.arguments ?? {};
201
+ switch (request.params.name) {
202
+ case "machine_list": return await session.list(input);
203
+ case "machine_agents": return await session.agents(input);
204
+ case "machine_start": return await session.start(input);
205
+ case "machine_status": return session.status(input);
206
+ case "machine_respond": return await session.respond(input);
207
+ default: throw new Error(`Unknown tool "${request.params.name}"`);
208
+ }
209
+ }
210
+ catch (cause) {
211
+ return {
212
+ isError: true,
213
+ content: [{ type: "text", text: errorMessage(cause) }],
214
+ };
215
+ }
216
+ });
217
+ server.setRequestHandler(ListResourcesRequestSchema, () => ({
218
+ resources: [{
219
+ name: "Machine run status",
220
+ uri: widgetUri,
221
+ mimeType: "text/html;profile=mcp-app",
222
+ description: "Live status and direct Human input for one Machine run",
223
+ }],
224
+ }));
225
+ server.setRequestHandler(ReadResourceRequestSchema, (request) => {
226
+ if (request.params.uri !== widgetUri) {
227
+ throw new Error(`Unknown resource "${request.params.uri}"`);
228
+ }
229
+ return {
230
+ contents: [{
231
+ uri: widgetUri,
232
+ mimeType: "text/html;profile=mcp-app",
233
+ text: widgetHtml,
234
+ _meta: { ui: { prefersBorder: true, csp: {} } },
235
+ }],
236
+ };
237
+ });
238
+ return { server, session };
239
+ }
240
+ function parse(schema, input) {
241
+ const parsed = v.safeParse(schema, input);
242
+ if (parsed.success)
243
+ return parsed.output;
244
+ const details = parsed.issues.map((issue) => {
245
+ const path = issue.path?.map((item) => String(item.key)).join(".");
246
+ return path === undefined || path === "" ? issue.message : `${path}: ${issue.message}`;
247
+ });
248
+ throw new Error(`Invalid Machine tool input: ${details.join("; ")}`);
249
+ }
250
+ function result(text, structuredContent) {
251
+ return { content: [{ type: "text", text }], structuredContent };
252
+ }
253
+ function formatSnapshot(run) {
254
+ const lines = [
255
+ `${run.machine} (${run.status})`,
256
+ `Run id: ${run.id}`,
257
+ `State: ${formatState(run.state) ?? "starting"}`,
258
+ `Elapsed: ${run.elapsedSeconds}s`,
259
+ ];
260
+ if (run.agent !== undefined) {
261
+ lines.push(`Agent: ${[
262
+ run.agent.harness,
263
+ run.agent.model,
264
+ run.agent.thinking === undefined ? undefined : `thinking ${run.agent.thinking}`,
265
+ ].filter((part) => part !== undefined).join(" · ")}`);
266
+ }
267
+ if (run.human !== undefined) {
268
+ lines.push(`Human: ${run.human.prompt}`, `Request id: ${run.human.requestId}`);
269
+ if (run.human.choices !== undefined)
270
+ lines.push(`Choices: ${run.human.choices.join(", ")}`);
271
+ if (run.human.suggestions !== undefined) {
272
+ lines.push(`Suggestions: ${run.human.suggestions.join(", ")}`);
273
+ }
274
+ }
275
+ if (run.error !== undefined)
276
+ lines.push(`Error: ${run.error}`);
277
+ return lines.join("\n");
278
+ }
279
+ function formatState(state) {
280
+ if (state === undefined)
281
+ return undefined;
282
+ return typeof state === "string" ? state : JSON.stringify(state);
283
+ }
284
+ function errorMessage(cause) {
285
+ return cause instanceof Error ? cause.message : String(cause);
286
+ }
287
+ async function main() {
288
+ const { server, session } = createMachinesMcpServer();
289
+ let closed = false;
290
+ const close = () => {
291
+ if (closed)
292
+ return;
293
+ closed = true;
294
+ session.close();
295
+ };
296
+ process.once("exit", close);
297
+ process.once("SIGINT", () => {
298
+ close();
299
+ process.exit(0);
300
+ });
301
+ process.once("SIGTERM", () => {
302
+ close();
303
+ process.exit(0);
304
+ });
305
+ process.stdin.once("end", close);
306
+ await server.connect(new StdioServerTransport());
307
+ }
308
+ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
309
+ void main().catch((cause) => {
310
+ console.error(errorMessage(cause));
311
+ process.exit(1);
312
+ });
313
+ }
@@ -0,0 +1,52 @@
1
+ interface ToolResult<T = unknown> {
2
+ readonly content: readonly [{
3
+ readonly type: "text";
4
+ readonly text: string;
5
+ }];
6
+ readonly details: T;
7
+ }
8
+ interface UiLike {
9
+ select(title: string, options: string[], settings?: {
10
+ readonly signal?: AbortSignal;
11
+ }): Promise<string | undefined>;
12
+ input(title: string, placeholder?: string, settings?: {
13
+ readonly signal?: AbortSignal;
14
+ }): Promise<string | undefined>;
15
+ notify(message: string, type?: "info" | "warning" | "error"): void;
16
+ setWidget(key: string, content: string[] | ((tui: unknown, theme: ThemeLike) => ComponentLike) | undefined, options?: {
17
+ readonly placement?: "aboveEditor" | "belowEditor";
18
+ }): void;
19
+ }
20
+ interface ToolContextLike {
21
+ readonly cwd: string;
22
+ readonly ui: UiLike;
23
+ }
24
+ interface ThemeLike {
25
+ fg(color: string, text: string): string;
26
+ bold(text: string): string;
27
+ }
28
+ interface ComponentLike {
29
+ render(width: number): string[];
30
+ invalidate(): void;
31
+ }
32
+ interface RenderOptionsLike {
33
+ readonly expanded: boolean;
34
+ }
35
+ interface ToolDefinitionLike {
36
+ readonly name: string;
37
+ readonly label: string;
38
+ readonly description: string;
39
+ readonly promptSnippet?: string;
40
+ readonly promptGuidelines?: readonly string[];
41
+ readonly parameters: unknown;
42
+ readonly executionMode?: "sequential" | "parallel";
43
+ execute(toolCallId: string, params: Record<string, unknown>, signal: AbortSignal | undefined, onUpdate: unknown, context: ToolContextLike): Promise<ToolResult>;
44
+ renderCall?(params: Record<string, unknown>, theme: ThemeLike): ComponentLike;
45
+ renderResult?(result: ToolResult, options: RenderOptionsLike, theme: ThemeLike): ComponentLike;
46
+ }
47
+ interface PiLike {
48
+ registerTool(tool: ToolDefinitionLike): void;
49
+ on(event: "session_shutdown", handler: (event: unknown, context: ToolContextLike) => void | Promise<void>): void;
50
+ }
51
+ export default function machinesExtension(pi: PiLike): void;
52
+ export {};