@dna113p/machines 0.1.0 → 0.2.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 CHANGED
@@ -63,6 +63,7 @@ Run it with `node demo.ts`. No build is needed to develop against the source.
63
63
  | Agent escalation | `npm run example:agents` | Built-in fake runners |
64
64
  | Child host and Human response | `npm run example:host` | Node only |
65
65
  | Real Agent writes and verifies a file | `npm run example:pi` | Configured Pi through `pi-acp`; may use network and model credits |
66
+ | Real AGY writes and verifies a file | `npm run example:agy` | Configured Antigravity CLI (`agy`); may use network and model credits |
66
67
 
67
68
  ## Run your own workflows
68
69
 
@@ -140,7 +141,7 @@ npm pack
140
141
  Then install the generated `dna113p-machines-0.1.0.tgz` into a project:
141
142
 
142
143
  ```bash
143
- npm install /absolute/path/to/machines/dna113p-machines-0.1.0.tgz
144
+ npm install /absolute/path/to/machines/dna113p-machines-0.2.0.tgz
144
145
  npx machine list
145
146
  ```
146
147
 
@@ -173,6 +174,11 @@ workflow decisions in Human states. The launcher defaults unnamed Agents to
173
174
  `npx -y pi-acp`; [Agent presets](docs/authoring.md#agent-presets) let you select
174
175
  another ACP command or runner explicitly.
175
176
 
177
+ The built-in `agy` preset selects the Antigravity CLI with AGY permission checks
178
+ enabled. [AGY configuration](docs/authoring.md#antigravity-agy) explains runner
179
+ settings and explicit opt-in to automatic tool approval. `npm run example:agy`
180
+ opts in for its temporary-directory demonstration.
181
+
176
182
  ## Checks
177
183
 
178
184
  ```bash
@@ -183,4 +189,21 @@ npm run build:plugin
183
189
  npm run smoke:plugin # exercise a relocated standalone plugin
184
190
  ```
185
191
 
192
+ ## Releasing
193
+
194
+ The [publishing workflow](.github/workflows/publish.yml) uses a standard GitHub
195
+ runner and npm trusted publishing, without an npm token stored in GitHub.
196
+ Pushing a `vX.Y.Z` tag runs the checks above and publishes to npm if they pass.
197
+ The tag must match the stable version in `package.json`.
198
+
199
+ For example, to release the next minor version from an up-to-date, clean `main`:
200
+
201
+ ```bash
202
+ npm version minor
203
+ git push origin main --follow-tags
204
+ ```
205
+
206
+ Running **Publish to npm** manually in GitHub Actions performs the checks and a
207
+ publishing dry run; it does not publish a package.
208
+
186
209
  Licensed under the [MIT License](LICENSE).
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,30 @@
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 { agyAgent } from "../src/agy.js";
6
+ const workspace = await mkdtemp(join(tmpdir(), "machines-agy-"));
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
+ // This demo explicitly permits unattended tools in its temporary workspace.
25
+ default: agyAgent("agy", [], { dangerouslySkipPermissions: true }),
26
+ },
27
+ });
28
+ console.log(`agy workspace: ${workspace}`);
29
+ console.log(`hello.txt: ${(await readFile(helloPath, "utf8")).trim()}`);
30
+ console.log(`verified --> ${String(result.value)}`);
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) {
@@ -112,7 +113,7 @@ async function runSession(child, request, outputMode, report, harness) {
112
113
  if (result.stopReason !== "end_turn") {
113
114
  throw new Error(`ACP Agent stopped with "${result.stopReason}"`);
114
115
  }
115
- return returnedEvent(output, outputMode === "capture");
116
+ return readAgentEvent(output, { adapter: "ACP", includeMessage: outputMode === "capture" });
116
117
  });
117
118
  }
118
119
  function identityUpdate(options, harness) {
@@ -137,45 +138,6 @@ function identityUpdate(options, harness) {
137
138
  ...(thinking === undefined ? {} : { thinking }),
138
139
  };
139
140
  }
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
141
  function waitForSpawn(child) {
180
142
  return new Promise((resolve, reject) => {
181
143
  child.once("spawn", resolve);
@@ -0,0 +1,6 @@
1
+ import type { AgentRequest, Event } from "./index.ts";
2
+ export declare function agentPrompt(request: AgentRequest): string;
3
+ export declare function readAgentEvent(output: string, options: {
4
+ readonly adapter: string;
5
+ readonly includeMessage: boolean;
6
+ }): Event;
@@ -0,0 +1,38 @@
1
+ export function agentPrompt(request) {
2
+ return [
3
+ request.prompt,
4
+ "",
5
+ "Complete the work using the available tools.",
6
+ `Allowed outcome types: ${request.outcomes.join(", ")}.`,
7
+ "When finished, end your final response with exactly one line in this form:",
8
+ 'MACHINES_EVENT {"type":"completed"}',
9
+ "Replace completed with exactly one allowed outcome type.",
10
+ "Include additional JSON fields only when the task asks for them.",
11
+ ].join("\n");
12
+ }
13
+ export function readAgentEvent(output, options) {
14
+ const matches = [...output.matchAll(/^MACHINES_EVENT (.+)$/gmu)];
15
+ const encoded = matches.at(-1)?.[1];
16
+ if (encoded === undefined) {
17
+ throw new Error(`${options.adapter} Agent finished without returning a Machines event`);
18
+ }
19
+ let event;
20
+ try {
21
+ event = JSON.parse(encoded);
22
+ }
23
+ catch (cause) {
24
+ throw new Error(`${options.adapter} Agent returned invalid event JSON`, { cause });
25
+ }
26
+ if (!isEvent(event)) {
27
+ throw new Error(`${options.adapter} Agent returned an invalid Machines event`);
28
+ }
29
+ const message = output.replace(/^MACHINES_EVENT .+$(?:\r?\n)?/gmu, "").trim();
30
+ return options.includeMessage && message !== "" ? { ...event, message } : event;
31
+ }
32
+ function isEvent(value) {
33
+ return value !== null
34
+ && typeof value === "object"
35
+ && !Array.isArray(value)
36
+ && "type" in value
37
+ && typeof value.type === "string";
38
+ }
@@ -0,0 +1,11 @@
1
+ import type { AgentRunner } from "./index.ts";
2
+ export interface AgyAgentOptions {
3
+ readonly env?: Readonly<Record<string, string>>;
4
+ readonly harness?: string;
5
+ readonly model?: string;
6
+ readonly effort?: "low" | "medium" | "high";
7
+ /** Explicitly opt in to auto-approving all AGY tool permission requests. */
8
+ readonly dangerouslySkipPermissions?: boolean;
9
+ readonly output?: "capture" | "stream";
10
+ }
11
+ export declare function agyAgent(command?: string, args?: readonly string[], options?: AgyAgentOptions): AgentRunner;
@@ -0,0 +1,168 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createInterface } from "node:readline";
3
+ import * as v from "valibot";
4
+ import { agentPrompt, readAgentEvent } from "./agent-protocol.js";
5
+ export function agyAgent(command = "agy", args = [], options = {}) {
6
+ return async (request, report) => {
7
+ if (request.cwd === undefined) {
8
+ throw new Error("agy Agent requires a working directory");
9
+ }
10
+ if (request.outcomes.length === 0) {
11
+ throw new Error("agy Agent requires at least one allowed outcome");
12
+ }
13
+ const harness = options.harness ?? (command === "agy" ? "agy" : command);
14
+ const env = { ...process.env, ...options.env };
15
+ const model = options.model ?? env.AGY_MODEL ?? env.MACHINES_AGENT_MODEL;
16
+ const effort = options.effort ?? env.AGY_EFFORT ?? env.MACHINES_AGENT_EFFORT;
17
+ const outputMode = options.output ?? "stream";
18
+ report?.({
19
+ type: "identity",
20
+ harness,
21
+ ...(model === undefined ? {} : { model }),
22
+ ...(effort === undefined ? {} : { thinking: effort }),
23
+ });
24
+ const cliArgs = [
25
+ ...args,
26
+ "--print",
27
+ agentPrompt(request),
28
+ "--output-format",
29
+ "stream-json",
30
+ "--disable-slash-commands",
31
+ ...(options.dangerouslySkipPermissions === true ? ["--dangerously-skip-permissions"] : []),
32
+ ...(model === undefined ? [] : ["--model", model]),
33
+ ...(effort === undefined ? [] : ["--effort", effort]),
34
+ ];
35
+ const child = spawn(command, cliArgs, {
36
+ cwd: request.cwd,
37
+ env,
38
+ stdio: ["pipe", "pipe", "pipe"],
39
+ });
40
+ let stderr = "";
41
+ child.stderr.setEncoding("utf8");
42
+ child.stderr.on("data", (chunk) => {
43
+ stderr += chunk;
44
+ if (outputMode === "stream")
45
+ process.stderr.write(chunk);
46
+ });
47
+ try {
48
+ await waitForSpawn(child);
49
+ return await runSession(child, outputMode, report);
50
+ }
51
+ catch (cause) {
52
+ const message = cause instanceof Error ? cause.message : String(cause);
53
+ const detail = stderr.trim();
54
+ throw new Error(`agy Agent "${command}" failed in "${request.cwd}": ${message}${detail === "" ? "" : `\n${detail}`}`, { cause });
55
+ }
56
+ finally {
57
+ child.kill();
58
+ }
59
+ };
60
+ }
61
+ async function runSession(child, outputMode, report) {
62
+ const exited = new Promise((resolve, reject) => {
63
+ child.once("close", (code, signal) => {
64
+ if (code === 0)
65
+ resolve();
66
+ else
67
+ reject(new Error(signal === null
68
+ ? `Process exited with code ${code}`
69
+ : `Process terminated by ${signal}`));
70
+ });
71
+ child.once("error", reject);
72
+ });
73
+ // Observe failures immediately, while stdout may still be draining.
74
+ void exited.catch(() => { });
75
+ const lines = createInterface({ input: child.stdout });
76
+ let displayedOutput = "";
77
+ let finalResponse;
78
+ const emitOutput = (text) => {
79
+ if (text === "")
80
+ return;
81
+ displayedOutput += text;
82
+ report?.({ type: "output", text });
83
+ if (outputMode === "stream")
84
+ process.stdout.write(text);
85
+ };
86
+ try {
87
+ for await (const line of lines) {
88
+ const record = decodeRecord(line);
89
+ if (record === undefined)
90
+ continue;
91
+ if (record.event === "result") {
92
+ finalResponse = record.result.response;
93
+ }
94
+ else {
95
+ const step = record.step_update;
96
+ if (step.step_type === "agent_response") {
97
+ emitOutput(step.text_delta);
98
+ }
99
+ else {
100
+ const id = String(step.step_index ?? step.tool_name ?? "tool");
101
+ const title = step.tool_name ?? step.tool_info?.name ?? "tool";
102
+ const status = step.state === "ACTIVE"
103
+ ? "in_progress"
104
+ : (step.state === "DONE" ? "completed" : undefined);
105
+ report?.({ type: "tool", id, title, ...(status === undefined ? {} : { status }) });
106
+ }
107
+ }
108
+ }
109
+ await exited;
110
+ // The final response owns the outcome; earlier deltas may contain progress
111
+ // or a preliminary event. Fall back only when no final response was sent.
112
+ const response = finalResponse ?? displayedOutput;
113
+ if (finalResponse !== undefined && !displayedOutput.endsWith(finalResponse)) {
114
+ if (finalResponse.startsWith(displayedOutput)) {
115
+ emitOutput(finalResponse.slice(displayedOutput.length));
116
+ }
117
+ else {
118
+ const separator = displayedOutput !== "" && !displayedOutput.endsWith("\n") ? "\n" : "";
119
+ emitOutput(separator + finalResponse);
120
+ }
121
+ }
122
+ if (outputMode === "stream" && displayedOutput !== "" && !displayedOutput.endsWith("\n")) {
123
+ process.stdout.write("\n");
124
+ }
125
+ return readAgentEvent(response, { adapter: "agy", includeMessage: outputMode === "capture" });
126
+ }
127
+ finally {
128
+ lines.close();
129
+ }
130
+ }
131
+ // Unknown events and malformed records are ignored at the transport boundary.
132
+ // Only these validated fields can reach the harness-neutral AgentUpdate API.
133
+ const recordSchema = v.variant("event", [
134
+ v.object({
135
+ event: v.literal("step_update"),
136
+ step_update: v.variant("step_type", [
137
+ v.object({ step_type: v.literal("agent_response"), text_delta: v.string() }),
138
+ v.object({
139
+ step_type: v.literal("tool"),
140
+ step_index: v.nullish(v.union([v.string(), v.number()])),
141
+ tool_name: v.nullish(v.string()),
142
+ tool_info: v.nullish(v.object({ name: v.nullish(v.string()) })),
143
+ state: v.nullish(v.string()),
144
+ }),
145
+ ]),
146
+ }),
147
+ v.object({
148
+ event: v.literal("result"),
149
+ result: v.object({ response: v.string() }),
150
+ }),
151
+ ]);
152
+ function decodeRecord(line) {
153
+ let value;
154
+ try {
155
+ value = JSON.parse(line);
156
+ }
157
+ catch {
158
+ return undefined;
159
+ }
160
+ const result = v.safeParse(recordSchema, value);
161
+ return result.success ? result.output : undefined;
162
+ }
163
+ function waitForSpawn(child) {
164
+ return new Promise((resolve, reject) => {
165
+ child.once("spawn", resolve);
166
+ child.once("error", reject);
167
+ });
168
+ }
@@ -1,5 +1,6 @@
1
1
  import { loadAgentBindings } from "./agent-bindings.js";
2
2
  import { acpAgent } from "./acp.js";
3
+ import { agyAgent } from "./agy.js";
3
4
  import { discoverMachines, machinesUserHome } from "./discovery.js";
4
5
  import { loadMachineModule, readAgentRoles, requireMachineDescription } from "./machine-module.js";
5
6
  export async function inspectMachines(location = {}) {
@@ -45,22 +46,31 @@ export async function inspectAgentPresets(location = {}) {
45
46
  .sort((left, right) => left.name.localeCompare(right.name));
46
47
  }
47
48
  export async function configuredAgentPresets(cwd, home) {
48
- const configured = await loadAgentBindings(cwd, home, { acpAgent });
49
+ const configured = await loadAgentBindings(cwd, home, { acpAgent, agyAgent });
49
50
  const defaultRunner = acpAgent("npx", ["-y", "pi-acp"], {
50
51
  harness: "pi-acp",
51
52
  output: "capture",
52
53
  });
54
+ const agyRunner = agyAgent("agy", [], {
55
+ harness: "agy",
56
+ output: "capture",
57
+ });
53
58
  return {
54
- agents: { default: defaultRunner, ...configured.agents },
59
+ agents: { default: defaultRunner, agy: agyRunner, ...configured.agents },
55
60
  presets: {
56
61
  default: {
57
62
  description: "Runs the current Pi Agent through ACP",
58
63
  harness: "pi-acp",
59
64
  runner: defaultRunner,
60
65
  },
66
+ agy: {
67
+ description: "Runs the Antigravity CLI (agy)",
68
+ harness: "agy",
69
+ runner: agyRunner,
70
+ },
61
71
  ...configured.presets,
62
72
  },
63
- sources: { default: "built in", ...configured.sources },
73
+ sources: { default: "built in", agy: "built in", ...configured.sources },
64
74
  };
65
75
  }
66
76
  function errorMessage(cause) {
@@ -41,6 +41,8 @@ MCP server ────┘ ↑
41
41
  | `src/machine-module.ts` | Shared module contract and metadata validation |
42
42
  | `src/catalog.ts` | Definition and preset catalog reads |
43
43
  | `src/acp.ts` | ACP process/session protocol, bounded Agent result, normalized activity |
44
+ | `src/agy.ts` | Antigravity CLI (agy) process protocol, bounded Agent result, normalized activity |
45
+ | `src/agent-protocol.ts` | Shared Agent prompt instructions and Machines event decoding |
44
46
  | `src/discovery.ts` | Project/global definition locations and precedence |
45
47
  | `src/launcher.ts` | Definition metadata, Agent bindings, preflight, single-use launch preparation |
46
48
  | `src/discovery-worker.ts` | Fresh metadata imports in disposable workers |
package/docs/authoring.md CHANGED
@@ -140,7 +140,7 @@ export default ({ acpAgent }: { acpAgent: typeof import("@dna113p/machines/acp")
140
140
  Install/configure the selected harness and its model credentials first. `acpAgent`
141
141
  accepts any compatible ACP command and argument array; the Pi adapter uses ACP as
142
142
  its supported execution path. The former `machines/pi` export has been retired;
143
- use `acpAgent` from `machines/acp` for Pi execution.
143
+ use `acpAgent` from `@dna113p/machines/acp` for Pi execution.
144
144
 
145
145
  Global presets live in `~/.machines/agents.ts`; project presets replace matching
146
146
  global names. Each preset requires a description and runner. Optional `harness`,
@@ -161,6 +161,45 @@ reports missing bindings or invalid definitions.
161
161
  `ready` describes valid metadata and resolved roles; it does not verify that a
162
162
  real harness is installed, authenticated, or able to run the requested model.
163
163
 
164
+ ### Antigravity (AGY)
165
+
166
+ The built-in `agy` preset runs the installed Antigravity CLI. Select it for the
167
+ default Agent or a declared named role:
168
+
169
+ ```bash
170
+ machine run my-workflow --agent default=agy -- "Do the task"
171
+ machine run review-task --agent implementer=agy -- "Implement the task"
172
+ ```
173
+
174
+ The runner keeps AGY's permission checks enabled. It does not forward interactive
175
+ permission prompts through Machines. For an unattended workflow that requires
176
+ automatic approval, configure an explicit preset in `.machines/agents.ts`:
177
+
178
+ ```ts
179
+ export default ({ agyAgent }: { agyAgent: typeof import("@dna113p/machines/agy").agyAgent }) => ({
180
+ "agy-unattended": {
181
+ description: "Runs AGY with automatic tool approval for trusted workflows",
182
+ harness: "agy",
183
+ runner: agyAgent("agy", [], {
184
+ dangerouslySkipPermissions: true,
185
+ output: "capture",
186
+ }),
187
+ },
188
+ });
189
+ ```
190
+
191
+ Select that preset with `--agent default=agy-unattended`. The option
192
+ `dangerouslySkipPermissions: true` auto-approves all AGY tool permission requests;
193
+ omitting it or setting it to `false` keeps the runner from adding that flag.
194
+ The `example:agy` demonstration explicitly opts in and asks AGY to work in a
195
+ temporary directory.
196
+
197
+ Runner `model` and `effort` options take precedence over environment settings.
198
+ Otherwise it uses `AGY_MODEL` / `AGY_EFFORT`, falling back to
199
+ `MACHINES_AGENT_MODEL` / `MACHINES_AGENT_EFFORT`. Per-runner `env` values override
200
+ the same variables inherited from the parent process. Effort values are `low`,
201
+ `medium`, or `high`.
202
+
164
203
  ## Programmatic execution and verification
165
204
 
166
205
  Direct runtime users supply runners through `run(definition, { agents, human })`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dna113p/machines",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Reusable workflows coordinating agents, local operations, and human decisions.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -22,6 +22,7 @@
22
22
  "exports": {
23
23
  ".": { "types": "./dist/src/index.d.ts", "import": "./dist/src/index.js" },
24
24
  "./acp": { "types": "./dist/src/acp.d.ts", "import": "./dist/src/acp.js" },
25
+ "./agy": { "types": "./dist/src/agy.d.ts", "import": "./dist/src/agy.js" },
25
26
  "./launcher": { "types": "./dist/src/launcher.d.ts", "import": "./dist/src/launcher.js" },
26
27
  "./mcp": { "types": "./dist/mcp/server.d.ts", "import": "./dist/mcp/server.js" },
27
28
  "./pi-extension": { "types": "./dist/pi-extension/index.d.ts", "import": "./dist/pi-extension/index.js" }
@@ -33,6 +34,7 @@
33
34
  "check": "npm run typecheck && npm test",
34
35
  "example": "node examples/operation.ts",
35
36
  "example:agents": "node examples/named-agents.ts",
37
+ "example:agy": "node examples/agy.ts",
36
38
  "example:host": "node examples/host.ts",
37
39
  "example:pi": "node examples/pi.ts",
38
40
  "mcp": "node mcp/server.ts",