@anhlp/alp-runtime 0.0.0-stage → 0.10.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 (89) hide show
  1. package/README.md +6 -2
  2. package/dist/acp-transport.d.ts +107 -0
  3. package/dist/acp-transport.js +464 -0
  4. package/dist/antigravity-transport.d.ts +80 -0
  5. package/dist/antigravity-transport.js +609 -0
  6. package/dist/board-domain.d.ts +56 -0
  7. package/dist/board-domain.js +156 -0
  8. package/dist/board.d.ts +21 -0
  9. package/dist/board.js +69 -0
  10. package/dist/catalog.d.ts +31 -0
  11. package/dist/catalog.js +38 -0
  12. package/dist/claude-transport.d.ts +157 -0
  13. package/dist/claude-transport.js +673 -0
  14. package/dist/contracts.d.ts +163 -0
  15. package/dist/contracts.js +5 -0
  16. package/dist/events.d.ts +1 -0
  17. package/dist/events.js +0 -0
  18. package/dist/github.d.ts +10 -0
  19. package/dist/github.js +39 -0
  20. package/dist/handoff.d.ts +36 -0
  21. package/dist/handoff.js +73 -0
  22. package/dist/index.d.ts +24 -0
  23. package/dist/index.js +63 -0
  24. package/dist/language.d.ts +41 -0
  25. package/dist/language.js +68 -0
  26. package/dist/live.d.ts +48 -0
  27. package/dist/live.js +44 -0
  28. package/dist/mailbox.d.ts +24 -0
  29. package/dist/mailbox.js +36 -0
  30. package/dist/mcp-tools.d.ts +115 -0
  31. package/dist/mcp-tools.js +71 -0
  32. package/dist/process-info.d.ts +14 -0
  33. package/dist/process-info.js +29 -0
  34. package/dist/providers.d.ts +28 -0
  35. package/dist/providers.js +71 -0
  36. package/dist/recall.d.ts +42 -0
  37. package/dist/recall.js +60 -0
  38. package/dist/resolve.d.ts +121 -0
  39. package/dist/resolve.js +238 -0
  40. package/dist/runtime-context.d.ts +43 -0
  41. package/dist/runtime-context.js +61 -0
  42. package/dist/runtime.d.ts +4 -0
  43. package/dist/runtime.js +2947 -0
  44. package/dist/session-config.d.ts +1973 -0
  45. package/dist/session-config.js +205 -0
  46. package/dist/session-state.d.ts +138 -0
  47. package/dist/session-state.js +0 -0
  48. package/dist/tool-results.d.ts +11 -0
  49. package/dist/tool-results.js +21 -0
  50. package/dist/tools.d.ts +674 -0
  51. package/dist/tools.js +375 -0
  52. package/dist/transport.d.ts +47 -0
  53. package/dist/transport.js +165 -0
  54. package/dist/usage.d.ts +15 -0
  55. package/dist/usage.js +35 -0
  56. package/dist/workspace.d.ts +101 -0
  57. package/dist/workspace.js +259 -0
  58. package/dist/worktree-domain.d.ts +88 -0
  59. package/dist/worktree-domain.js +196 -0
  60. package/package.json +247 -4
  61. package/src/acp-transport.ts +453 -0
  62. package/src/antigravity-transport.ts +658 -0
  63. package/src/board-domain.ts +184 -0
  64. package/src/board.ts +80 -0
  65. package/src/catalog.ts +32 -0
  66. package/src/claude-transport.ts +769 -0
  67. package/src/contracts.ts +166 -0
  68. package/src/events.ts +1 -0
  69. package/src/github.ts +37 -0
  70. package/src/handoff.ts +86 -0
  71. package/src/index.ts +26 -0
  72. package/src/language.ts +102 -0
  73. package/src/live.ts +85 -0
  74. package/src/mailbox.ts +46 -0
  75. package/src/mcp-tools.ts +73 -0
  76. package/src/process-info.ts +36 -0
  77. package/src/providers.ts +84 -0
  78. package/src/recall.ts +85 -0
  79. package/src/resolve.ts +230 -0
  80. package/src/runtime-context.ts +49 -0
  81. package/src/runtime.ts +3545 -0
  82. package/src/session-config.ts +301 -0
  83. package/src/session-state.ts +139 -0
  84. package/src/tool-results.ts +22 -0
  85. package/src/tools.ts +379 -0
  86. package/src/transport.ts +106 -0
  87. package/src/usage.ts +42 -0
  88. package/src/workspace.ts +318 -0
  89. package/src/worktree-domain.ts +216 -0
package/README.md CHANGED
@@ -1,3 +1,7 @@
1
- # Temporary Holding Version
1
+ # @anhlp/alp-runtime
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Viewer-neutral orchestration and normalized native provider adapters.
4
+
5
+ Build: `npm run build --workspace=@anhlp/alp-runtime`. Test: `npm run test --workspace=@anhlp/alp-runtime`.
6
+
7
+ Public imports use the package root and its explicitly exported subpaths. See [architecture, provider extension, compatibility and release instructions](../../docs/modular-monorepo.md).
@@ -0,0 +1,107 @@
1
+ import { type PermissionProfile } from '@anhlp/alp-core/permissions';
2
+ /**
3
+ * ACP agents (ALPD §46): a generic transport for any agent that speaks the Agent Client
4
+ * Protocol v1 over stdio, such as Gemini CLI or opencode. It maps the runtime's
5
+ * normalized protocol (thread/start, turn/start, item and turn notifications) to ACP's
6
+ * initialize, session/new, session/prompt and session/update.
7
+ *
8
+ * What ACP does not have, ALP does without: instructions go in the first prompt, mail
9
+ * waits for the turn to end (no steer), and a session resumes only when the agent can
10
+ * load one. ALP's tools reach the agent as an MCP server ("alp") that the agent starts:
11
+ * a small bridge process connected back to this transport by a local socket.
12
+ */
13
+ export type AcpProvider = {
14
+ id: string;
15
+ label: string;
16
+ command: string;
17
+ args: string[];
18
+ env: Record<string, string>;
19
+ models?: Array<{
20
+ id: string;
21
+ label?: string;
22
+ description?: string;
23
+ }>;
24
+ additionalModels?: Array<{
25
+ id: string;
26
+ label?: string;
27
+ description?: string;
28
+ }>;
29
+ enabled?: boolean;
30
+ };
31
+ type Listener = (method: string, params: any) => void;
32
+ type RequestHandler = (method: string, params: any) => Promise<unknown>;
33
+ /** What ALP decides about an ACP agent's permission request. */
34
+ export type AcpPermission = {
35
+ kind: string;
36
+ title: string;
37
+ command?: string;
38
+ paths: string[];
39
+ alpTool: boolean;
40
+ };
41
+ /** What an ACP permission request asks for: its tool kind, the command it runs, and the paths it touches. */
42
+ export declare function acpPermission(toolCall: any, tools: string[]): AcpPermission;
43
+ /**
44
+ * Whether ALP lets an ACP agent use a tool: ALP's tools always; then the profile's
45
+ * Bash rules for a command; then the session's mode. Read-only allows reading;
46
+ * workspace-write also changing files inside the workspace and running commands, which
47
+ * only the agent's own sandbox, if it has one, contains; full access allows everything.
48
+ * 'mode' is beyond the mode: the profile decides whether ALP asks the user.
49
+ */
50
+ export declare function acpDecision(request: AcpPermission, mode: string, profile: PermissionProfile | null | undefined, workdir: string): 'allow' | 'deny' | 'rule' | 'mode';
51
+ /** The text ALP shows the user when an ACP agent asks for a permission. */
52
+ export declare function acpWhat(request: AcpPermission): string;
53
+ /** Maps an ACP agent over stdio to the normalized runtime protocol. */
54
+ export declare class AcpTransport {
55
+ private readonly provider;
56
+ private readonly cwd;
57
+ private readonly env;
58
+ private child?;
59
+ private exit;
60
+ private sequence;
61
+ private pending;
62
+ private listeners;
63
+ private failures;
64
+ private requestHandler?;
65
+ private closed;
66
+ private capabilities;
67
+ private sessionId;
68
+ private config;
69
+ /** Instructions go with the first prompt of a new session; a loaded one has them already. */
70
+ private instructionsSent;
71
+ private loading;
72
+ private activeTurn?;
73
+ /** The running prompt; a cancelled one still answers before the next starts. */
74
+ private prompting?;
75
+ /** Text of the running turn's current message; a tool call starts the next one. */
76
+ private message?;
77
+ private messages;
78
+ private toolCalls;
79
+ /** Permission questions of the running turn, answered cancelled when it is cancelled. */
80
+ private questions;
81
+ private bridge?;
82
+ private tools;
83
+ constructor(provider: AcpProvider, cwd: string, env: NodeJS.ProcessEnv);
84
+ initialize(): Promise<void>;
85
+ onNotification(listener: Listener): () => boolean;
86
+ onFailure(listener: (error: Error) => void): () => boolean;
87
+ onRequest(handler: RequestHandler): void;
88
+ request(method: string, params: any): Promise<any>;
89
+ /** Picks the session's model when ALP was given one and the agent offers a choice. */
90
+ private chooseModel;
91
+ private endTurn;
92
+ private closeMessage;
93
+ private update;
94
+ /** The agent asks before using a tool; ALP's runtime decides, and the answer picks one of the agent's options. */
95
+ private permission;
96
+ private receive;
97
+ private call;
98
+ private write;
99
+ /** Starts the socket ALP's MCP bridge connects to, and returns the server entry for the agent. */
100
+ private openBridge;
101
+ private serveBridge;
102
+ private toolCall;
103
+ private emit;
104
+ private fail;
105
+ close(): Promise<void>;
106
+ }
107
+ export {};
@@ -0,0 +1,464 @@
1
+ // packages/runtime/src/acp-transport.ts
2
+ import { spawn, execFile } from "node:child_process";
3
+ import { randomBytes, randomUUID } from "node:crypto";
4
+ import { rmSync } from "node:fs";
5
+ import net from "node:net";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+ import { createInterface } from "node:readline";
9
+ import { commandDecision, unwrapShell } from "@anhlp/alp-core/permissions";
10
+ var BRIDGE = `
11
+ const net = require('node:net');
12
+ const readline = require('node:readline');
13
+ const socket = net.connect(process.env.ALP_BRIDGE_SOCKET);
14
+ const waiting = new Map();
15
+ let sequence = 0;
16
+ let buffer = '';
17
+ socket.setEncoding('utf8');
18
+ socket.write(JSON.stringify({ token: process.env.ALP_BRIDGE_TOKEN }) + '\\n');
19
+ socket.on('data', data => {
20
+ buffer += data;
21
+ for (let index; (index = buffer.indexOf('\\n')) >= 0;) {
22
+ const line = buffer.slice(0, index); buffer = buffer.slice(index + 1);
23
+ try { const message = JSON.parse(line); const waiter = waiting.get(message.id); if (waiter) { waiting.delete(message.id); waiter(message); } } catch {}
24
+ }
25
+ });
26
+ socket.on('close', () => process.exit(0));
27
+ socket.on('error', () => process.exit(1));
28
+ const ask = message => new Promise(resolve => { const id = ++sequence; waiting.set(id, resolve); socket.write(JSON.stringify({ ...message, id }) + '\\n'); });
29
+ const send = value => process.stdout.write(JSON.stringify({ jsonrpc: '2.0', ...value }) + '\\n');
30
+ readline.createInterface({ input: process.stdin }).on('line', async line => {
31
+ let message; try { message = JSON.parse(line); } catch { return; }
32
+ if (message.id === undefined || message.id === null) return;
33
+ try {
34
+ if (message.method === 'initialize') return send({ id: message.id, result: { protocolVersion: (message.params && message.params.protocolVersion) || '2025-06-18', capabilities: { tools: {} }, serverInfo: { name: 'alp', version: '1' } } });
35
+ if (message.method === 'ping') return send({ id: message.id, result: {} });
36
+ if (message.method === 'tools/list') { const reply = await ask({ op: 'list' }); return send({ id: message.id, result: { tools: reply.tools } }); }
37
+ if (message.method === 'tools/call') { const reply = await ask({ op: 'call', name: message.params.name, arguments: message.params.arguments || {} }); return send({ id: message.id, result: reply.result }); }
38
+ send({ id: message.id, error: { code: -32601, message: 'Method not found' } });
39
+ } catch (error) { send({ id: message.id, error: { code: -32603, message: String((error && error.message) || error) } }); }
40
+ });
41
+ process.stdin.on('end', () => process.exit(0));
42
+ `;
43
+ var READERS = ["read", "search", "think", "fetch"];
44
+ var inside = (file, root) => {
45
+ const relative = path.relative(root, file);
46
+ return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
47
+ };
48
+ function acpPermission(toolCall, tools) {
49
+ const raw = toolCall?.rawInput;
50
+ const kind = typeof toolCall?.kind === "string" ? toolCall.kind : "other";
51
+ const title = typeof toolCall?.title === "string" ? toolCall.title : "";
52
+ const command = kind === "execute" ? typeof raw?.command === "string" ? raw.command : Array.isArray(raw?.command) ? raw.command.join(" ") : title || void 0 : void 0;
53
+ const paths = (Array.isArray(toolCall?.locations) ? toolCall.locations : []).map((location) => location?.path).filter((value) => typeof value === "string");
54
+ const named = `${title} ${typeof toolCall?.name === "string" ? toolCall.name : ""}`;
55
+ const alpTool = !["execute", "edit", "delete", "move"].includes(kind) && tools.some((tool) => new RegExp(`\\b${tool}\\b`).test(named));
56
+ return { kind, title, ...command ? { command } : {}, paths, alpTool };
57
+ }
58
+ function acpDecision(request, mode, profile, workdir) {
59
+ if (request.alpTool) return "allow";
60
+ const rule = request.command !== void 0 && profile ? commandDecision(profile, request.command) : void 0;
61
+ if (rule === "deny") return "deny";
62
+ if (rule === "allow") return "allow";
63
+ if (rule === "ask") return "rule";
64
+ if (mode === "full-access" || READERS.includes(request.kind)) return "allow";
65
+ if (mode === "workspace-write") {
66
+ const outside = request.paths.some((file) => !inside(path.resolve(workdir, file), workdir) && !inside(path.resolve(file), os.tmpdir()));
67
+ if (!outside) return "allow";
68
+ }
69
+ return "mode";
70
+ }
71
+ function acpWhat(request) {
72
+ if (request.command !== void 0) return `run \`${unwrapShell(request.command).slice(0, 400)}\``;
73
+ return `${request.kind === "other" ? "use" : request.kind} ${request.title ? `\`${request.title.slice(0, 300)}\`` : "a tool"}${request.paths.length ? ` (${request.paths.slice(0, 5).join(", ")})` : ""}`;
74
+ }
75
+ function acpServers(servers, http) {
76
+ return Object.entries(servers).map(([name, server]) => {
77
+ if (server.url) {
78
+ if (!http) throw new Error(`MCP server '${name}' is HTTP, which this ACP agent does not take; use a stdio server`);
79
+ return { type: "http", name, url: server.url, headers: Object.entries(server.http_headers ?? {}).map(([key, value]) => ({ name: key, value: String(value) })) };
80
+ }
81
+ return { name, command: server.command, args: server.args ?? [], env: Object.entries(server.env ?? {}).map(([key, value]) => ({ name: key, value: String(value) })) };
82
+ });
83
+ }
84
+ var AcpTransport = class {
85
+ constructor(provider, cwd, env) {
86
+ this.provider = provider;
87
+ this.cwd = cwd;
88
+ this.env = env;
89
+ }
90
+ child;
91
+ exit = Promise.resolve();
92
+ sequence = 0;
93
+ pending = /* @__PURE__ */ new Map();
94
+ listeners = /* @__PURE__ */ new Set();
95
+ failures = /* @__PURE__ */ new Set();
96
+ requestHandler;
97
+ closed = false;
98
+ capabilities = {};
99
+ sessionId = "";
100
+ config;
101
+ /** Instructions go with the first prompt of a new session; a loaded one has them already. */
102
+ instructionsSent = false;
103
+ loading = false;
104
+ activeTurn;
105
+ /** The running prompt; a cancelled one still answers before the next starts. */
106
+ prompting;
107
+ /** Text of the running turn's current message; a tool call starts the next one. */
108
+ message;
109
+ messages = 0;
110
+ toolCalls = /* @__PURE__ */ new Map();
111
+ /** Permission questions of the running turn, answered cancelled when it is cancelled. */
112
+ questions = /* @__PURE__ */ new Set();
113
+ bridge;
114
+ tools = [];
115
+ async initialize() {
116
+ if (/\.(cmd|bat|ps1)$/i.test(this.provider.command)) throw new Error(`${this.provider.label}: the command must be a native executable, not a shell launcher`);
117
+ const child = spawn(this.provider.command, this.provider.args, { cwd: this.cwd, env: { ...this.env, ...this.provider.env }, windowsHide: true, stdio: ["pipe", "pipe", "pipe"] });
118
+ this.child = child;
119
+ this.exit = new Promise((resolve) => {
120
+ child.once("close", () => resolve());
121
+ child.once("error", () => resolve());
122
+ });
123
+ child.stderr.on("data", () => {
124
+ });
125
+ child.stdin.on("error", (error) => this.fail(error));
126
+ child.on("error", (error) => this.fail(new Error(`${this.provider.label} did not start: ${error.message}`)));
127
+ child.on("close", (code) => {
128
+ if (!this.closed) this.fail(new Error(`${this.provider.label} exited (${code})`));
129
+ });
130
+ createInterface({ input: child.stdout }).on("line", (line) => this.receive(line));
131
+ const result = await this.call("initialize", {
132
+ protocolVersion: 1,
133
+ // The agent reads, writes and runs commands with its own tools; ALP answers its permission requests.
134
+ clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false },
135
+ clientInfo: { name: "alp", title: "ALP", version: "0.0.0" }
136
+ }, 3e4);
137
+ if (result?.protocolVersion !== 1) throw new Error(`${this.provider.label} speaks ACP version ${result?.protocolVersion}; ALP speaks version 1`);
138
+ this.capabilities = result.agentCapabilities ?? {};
139
+ }
140
+ onNotification(listener) {
141
+ this.listeners.add(listener);
142
+ return () => this.listeners.delete(listener);
143
+ }
144
+ onFailure(listener) {
145
+ this.failures.add(listener);
146
+ return () => this.failures.delete(listener);
147
+ }
148
+ onRequest(handler) {
149
+ this.requestHandler = handler;
150
+ }
151
+ async request(method, params) {
152
+ if (this.closed) throw new Error("Runtime is closed");
153
+ if (method === "thread/start" || method === "thread/resume") {
154
+ this.config = params;
155
+ this.tools = params.dynamicTools ?? [];
156
+ const mcpServers = [...acpServers(params.mcpServers ?? {}, Boolean(this.capabilities.mcpCapabilities?.http)), ...this.tools.length ? [await this.openBridge()] : []];
157
+ if (method === "thread/resume") {
158
+ if (!this.capabilities.loadSession) throw new Error(`${this.provider.label} cannot resume a session: it does not support session/load`);
159
+ this.loading = true;
160
+ try {
161
+ await this.call("session/load", { sessionId: params.threadId, cwd: params.cwd, mcpServers }, 12e4);
162
+ } finally {
163
+ this.loading = false;
164
+ }
165
+ this.sessionId = params.threadId;
166
+ this.instructionsSent = true;
167
+ } else {
168
+ const result = await this.call("session/new", { cwd: params.cwd, mcpServers }, 12e4);
169
+ this.sessionId = result.sessionId;
170
+ await this.chooseModel(result.models, result.configOptions);
171
+ }
172
+ return { thread: { id: this.sessionId, turns: [] }, cwd: params.cwd, model: params.model, reasoningEffort: params.thinking };
173
+ }
174
+ if (method === "turn/start") {
175
+ if (!this.sessionId) throw new Error(`${this.provider.label} session is not open`);
176
+ if (this.prompting) await Promise.race([this.prompting, new Promise((resolve) => setTimeout(resolve, 1e4).unref())]);
177
+ if (this.closed) throw new Error("Runtime is closed");
178
+ const turnId = randomUUID();
179
+ this.activeTurn = turnId;
180
+ this.message = void 0;
181
+ const prompt = (params.input ?? []).filter((part) => typeof part?.text === "string").map((part) => ({ type: "text", text: part.text }));
182
+ if (!this.instructionsSent && this.config?.developerInstructions) {
183
+ prompt.unshift({ type: "text", text: `Instructions from ALP, which runs you. Follow them for this whole session.
184
+
185
+ ${this.config.developerInstructions}` });
186
+ this.instructionsSent = true;
187
+ }
188
+ this.prompting = this.call("session/prompt", { sessionId: this.sessionId, prompt }).then(
189
+ (result) => this.endTurn(turnId, result?.stopReason),
190
+ (error) => this.endTurn(turnId, "error", error instanceof Error ? error.message : String(error))
191
+ ).finally(() => {
192
+ if (this.activeTurn === void 0 || this.activeTurn === turnId) this.prompting = void 0;
193
+ });
194
+ return { turn: { id: turnId } };
195
+ }
196
+ if (method === "turn/steer") throw new Error(`${this.provider.label} takes no messages during a turn; ALP delivers them when it ends`);
197
+ if (method === "turn/interrupt") {
198
+ if (this.sessionId) this.write({ jsonrpc: "2.0", method: "session/cancel", params: { sessionId: this.sessionId } });
199
+ for (const answer of this.questions) answer({ outcome: { outcome: "cancelled" } });
200
+ this.questions.clear();
201
+ return {};
202
+ }
203
+ if (method === "session/configure") return {};
204
+ if (method === "thread/delete") return {};
205
+ throw new Error(`${this.provider.label} does not support '${method}'`);
206
+ }
207
+ /** Picks the session's model when ALP was given one and the agent offers a choice. */
208
+ async chooseModel(models, configOptions) {
209
+ const wanted = this.config?.model?.includes("/") ? this.config.model.slice(this.config.model.indexOf("/") + 1) : void 0;
210
+ if (!wanted) return;
211
+ const config = configOptions?.find((option) => option.category === "model" && option.type === "select");
212
+ if (config) {
213
+ if (config.currentValue === wanted) return;
214
+ const options = (config.options ?? []).flatMap((option) => option.options ?? [option]);
215
+ if (!options.some((option) => option.value === wanted)) throw new Error(`${this.provider.label} has no model '${wanted}'`);
216
+ await this.call("session/set_config_option", { sessionId: this.sessionId, configId: config.id, value: wanted }, 3e4);
217
+ return;
218
+ }
219
+ if (models?.currentModelId === wanted) return;
220
+ if (!Array.isArray(models?.availableModels)) throw new Error(`${this.provider.label} does not let ALP choose its model; use acp:${this.provider.id} alone`);
221
+ if (!models.availableModels.some((model) => model?.modelId === wanted)) throw new Error(`${this.provider.label} has no model '${wanted}'`);
222
+ await this.call("session/set_model", { sessionId: this.sessionId, modelId: wanted }, 3e4);
223
+ }
224
+ endTurn(turnId, stopReason, error) {
225
+ if (this.closed || this.activeTurn !== turnId) return;
226
+ this.closeMessage();
227
+ this.activeTurn = void 0;
228
+ this.toolCalls.clear();
229
+ const status = stopReason === "end_turn" ? "completed" : stopReason === "cancelled" ? "interrupted" : "failed";
230
+ this.emit("turn/completed", {
231
+ threadId: this.sessionId,
232
+ turn: { id: turnId, status, ...status === "failed" ? { error: { message: error ?? `${this.provider.label} stopped: ${stopReason}` } } : {} }
233
+ });
234
+ }
235
+ closeMessage() {
236
+ if (this.message?.text) this.emit("item/completed", { threadId: this.sessionId, item: { type: "agentMessage", id: this.message.id, text: this.message.text } });
237
+ this.message = void 0;
238
+ }
239
+ update(update) {
240
+ if (this.loading || !this.activeTurn || !update) return;
241
+ const kind = update.sessionUpdate;
242
+ if (kind === "agent_message_chunk") {
243
+ const text = update.content?.type === "text" && typeof update.content.text === "string" ? update.content.text : "";
244
+ if (!text) return;
245
+ this.message ??= { id: `acp-${this.activeTurn}-${++this.messages}`, text: "" };
246
+ this.message.text += text;
247
+ this.emit("item/agentMessage/delta", { threadId: this.sessionId, itemId: this.message.id, delta: text });
248
+ return;
249
+ }
250
+ if (kind === "tool_call" || kind === "tool_call_update") {
251
+ const id = String(update.toolCallId ?? "");
252
+ if (!id) return;
253
+ let call = this.toolCalls.get(id);
254
+ if (!call) {
255
+ const request = acpPermission(update, this.tools.map((tool) => tool.name));
256
+ call = { command: request.command ?? `${request.kind}: ${request.title}`, alp: request.alpTool };
257
+ this.toolCalls.set(id, call);
258
+ this.closeMessage();
259
+ if (!call.alp) this.emit("item/started", { threadId: this.sessionId, item: { type: "commandExecution", id, command: call.command, cwd: this.cwd, status: "inProgress", aggregatedOutput: "" } });
260
+ }
261
+ if ((update.status === "completed" || update.status === "failed") && !call.alp) {
262
+ const output = (Array.isArray(update.content) ? update.content : []).map((part) => part?.type === "content" && part.content?.type === "text" ? part.content.text : part?.type === "diff" ? `diff ${part.path}` : "").filter(Boolean).join("\n");
263
+ this.emit("item/completed", { threadId: this.sessionId, item: { type: "commandExecution", id, command: call.command, cwd: this.cwd, status: update.status, exitCode: update.status === "failed" ? 1 : 0, aggregatedOutput: output } });
264
+ }
265
+ return;
266
+ }
267
+ if (kind === "usage_update" && typeof update.used === "number") {
268
+ const last = { totalTokens: update.used };
269
+ this.emit("thread/tokenUsage/updated", { threadId: this.sessionId, turnId: this.activeTurn, tokenUsage: { last, total: last, ...typeof update.size === "number" ? { modelContextWindow: update.size } : {} } });
270
+ }
271
+ }
272
+ /** The agent asks before using a tool; ALP's runtime decides, and the answer picks one of the agent's options. */
273
+ async permission(params) {
274
+ const options = Array.isArray(params?.options) ? params.options : [];
275
+ const pick = (...kinds) => kinds.map((kind) => options.find((option2) => option2?.kind === kind)).find(Boolean);
276
+ if (!this.activeTurn || !this.requestHandler) return { outcome: { outcome: "cancelled" } };
277
+ const request = acpPermission(params?.toolCall, this.tools.map((tool) => tool.name));
278
+ const question = new Promise((resolve) => this.questions.add(resolve));
279
+ const answer = await Promise.race([this.requestHandler("item/acp/permission", request), question]);
280
+ if (answer?.outcome) return answer;
281
+ const option = answer?.allow ? pick("allow_once", "allow_always") : pick("reject_once", "reject_always");
282
+ return option ? { outcome: { outcome: "selected", optionId: option.optionId } } : { outcome: { outcome: "cancelled" } };
283
+ }
284
+ receive(line) {
285
+ if (line.length > 16 * 1024 * 1024) {
286
+ this.fail(new Error(`${this.provider.label} sent a frame over the size limit`));
287
+ return;
288
+ }
289
+ let message;
290
+ try {
291
+ message = JSON.parse(line);
292
+ } catch {
293
+ return;
294
+ }
295
+ if (message.method && message.id !== void 0) {
296
+ const answer = message.method === "session/request_permission" ? this.permission(message.params) : Promise.reject(Object.assign(new Error(`ALP does not offer '${message.method}'`), { code: -32601 }));
297
+ answer.then(
298
+ (result) => {
299
+ if (!this.closed) this.write({ jsonrpc: "2.0", id: message.id, result });
300
+ },
301
+ (error) => {
302
+ if (!this.closed) this.write({ jsonrpc: "2.0", id: message.id, error: { code: error?.code ?? -32603, message: error instanceof Error ? error.message : String(error) } });
303
+ }
304
+ );
305
+ } else if (message.id !== void 0) {
306
+ const waiter = this.pending.get(message.id);
307
+ if (!waiter) return;
308
+ this.pending.delete(message.id);
309
+ clearTimeout(waiter.timer);
310
+ if (message.error) {
311
+ const auth = message.error.code === -32e3 || /auth/i.test(String(message.error.message ?? ""));
312
+ waiter.reject(new Error(auth ? `${this.provider.label} needs you to sign in first: run it once in a terminal and log in (${message.error.message ?? "authentication required"})` : `${this.provider.label}: ${message.error.message ?? "request failed"}`));
313
+ } else waiter.resolve(message.result);
314
+ } else if (message.method === "session/update" && message.params?.sessionId === this.sessionId) {
315
+ this.update(message.params.update);
316
+ }
317
+ }
318
+ call(method, params, timeout) {
319
+ if (this.closed) return Promise.reject(new Error("Runtime is closed"));
320
+ const id = ++this.sequence;
321
+ return new Promise((resolve, reject) => {
322
+ const timer = timeout ? setTimeout(() => {
323
+ this.pending.delete(id);
324
+ reject(new Error(`${this.provider.label} did not answer ${method} within ${timeout / 1e3} s`));
325
+ }, timeout) : void 0;
326
+ this.pending.set(id, { resolve, reject, timer });
327
+ try {
328
+ this.write({ jsonrpc: "2.0", id, method, params });
329
+ } catch (error) {
330
+ clearTimeout(timer);
331
+ this.pending.delete(id);
332
+ reject(error);
333
+ }
334
+ });
335
+ }
336
+ write(value) {
337
+ if (this.closed || !this.child) throw new Error("Runtime is closed");
338
+ this.child.stdin.write(`${JSON.stringify(value)}
339
+ `);
340
+ }
341
+ /** Starts the socket ALP's MCP bridge connects to, and returns the server entry for the agent. */
342
+ async openBridge() {
343
+ if (!this.bridge) {
344
+ const token = randomBytes(16).toString("hex");
345
+ const name = `alp-acp-${randomBytes(6).toString("hex")}`;
346
+ const socket = process.platform === "win32" ? `\\\\.\\pipe\\${name}` : path.join(os.tmpdir(), `${name}.sock`);
347
+ const connections = /* @__PURE__ */ new Set();
348
+ const server = net.createServer((connection) => {
349
+ connections.add(connection);
350
+ connection.once("close", () => connections.delete(connection));
351
+ this.serveBridge(connection, token);
352
+ });
353
+ await new Promise((resolve, reject) => {
354
+ server.once("error", reject);
355
+ server.listen(socket, () => resolve());
356
+ });
357
+ this.bridge = { server, socket, token, connections };
358
+ }
359
+ const electron = process.versions.electron ? [{ name: "ELECTRON_RUN_AS_NODE", value: "1" }] : [];
360
+ return { name: "alp", command: process.execPath, args: ["-e", BRIDGE], env: [{ name: "ALP_BRIDGE_SOCKET", value: this.bridge.socket }, { name: "ALP_BRIDGE_TOKEN", value: this.bridge.token }, ...electron] };
361
+ }
362
+ serveBridge(connection, token) {
363
+ let trusted = false;
364
+ connection.setEncoding("utf8");
365
+ connection.on("error", () => {
366
+ });
367
+ const reply = (value) => {
368
+ if (!connection.destroyed) connection.write(`${JSON.stringify(value)}
369
+ `);
370
+ };
371
+ createInterface({ input: connection }).on("line", async (line) => {
372
+ let message;
373
+ try {
374
+ message = JSON.parse(line);
375
+ } catch {
376
+ connection.destroy();
377
+ return;
378
+ }
379
+ if (!trusted) {
380
+ if (message.token !== token) {
381
+ connection.destroy();
382
+ return;
383
+ }
384
+ trusted = true;
385
+ return;
386
+ }
387
+ if (message.op === "list") {
388
+ reply({ id: message.id, tools: this.tools.map((tool) => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema ?? { type: "object" } })) });
389
+ return;
390
+ }
391
+ if (message.op === "call") reply({ id: message.id, result: await this.toolCall(String(message.name), message.arguments ?? {}) });
392
+ });
393
+ }
394
+ async toolCall(tool, args) {
395
+ if (!this.requestHandler || !this.activeTurn || !this.tools.some((candidate) => candidate.name === tool)) {
396
+ return { content: [{ type: "text", text: this.activeTurn ? `Unknown ALP tool '${tool}'` : "ALP tools are unavailable between turns" }], isError: true };
397
+ }
398
+ const callId = `acp-tool-${randomUUID()}`;
399
+ const item = { type: "dynamicToolCall", id: callId, callId, tool, arguments: args };
400
+ this.closeMessage();
401
+ this.emit("item/started", { threadId: this.sessionId, item: { ...item, status: "inProgress" } });
402
+ const result = await this.requestHandler("item/tool/call", { threadId: this.sessionId, turnId: this.activeTurn, callId, namespace: null, tool, arguments: args }).catch((error) => ({ success: false, contentItems: [{ type: "inputText", text: error instanceof Error ? error.message : String(error) }] }));
403
+ const ok = result?.success !== false;
404
+ this.emit("item/completed", { threadId: this.sessionId, item: { ...item, status: ok ? "completed" : "failed", success: ok, contentItems: result?.contentItems ?? [] } });
405
+ return { content: (result?.contentItems ?? []).map((part) => ({ type: "text", text: part.text ?? JSON.stringify(part) })), isError: !ok };
406
+ }
407
+ emit(method, params) {
408
+ for (const listener of this.listeners) listener(method, params);
409
+ }
410
+ fail(error) {
411
+ if (this.closed) return;
412
+ for (const waiter of this.pending.values()) {
413
+ clearTimeout(waiter.timer);
414
+ waiter.reject(error);
415
+ }
416
+ this.pending.clear();
417
+ for (const listener of this.failures) listener(error);
418
+ void this.close();
419
+ }
420
+ async close() {
421
+ if (this.closed) return this.exit;
422
+ this.closed = true;
423
+ for (const waiter of this.pending.values()) {
424
+ clearTimeout(waiter.timer);
425
+ waiter.reject(new Error("Runtime closed"));
426
+ }
427
+ this.pending.clear();
428
+ this.listeners.clear();
429
+ this.failures.clear();
430
+ for (const answer of this.questions) answer({ outcome: { outcome: "cancelled" } });
431
+ this.questions.clear();
432
+ if (this.bridge) {
433
+ this.bridge.server.close();
434
+ for (const connection of this.bridge.connections) connection.destroy();
435
+ if (process.platform !== "win32") try {
436
+ rmSync(this.bridge.socket, { force: true });
437
+ } catch {
438
+ }
439
+ }
440
+ const child = this.child;
441
+ if (!child) return;
442
+ child.stdin.end();
443
+ const timer = setTimeout(() => {
444
+ if (process.platform === "win32" && child.pid) execFile("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], { windowsHide: true }, () => {
445
+ });
446
+ else child.kill("SIGKILL");
447
+ }, 1500);
448
+ let deadline;
449
+ try {
450
+ await Promise.race([this.exit, new Promise((resolve) => {
451
+ deadline = setTimeout(resolve, 4e3);
452
+ })]);
453
+ } finally {
454
+ clearTimeout(timer);
455
+ clearTimeout(deadline);
456
+ }
457
+ }
458
+ };
459
+ export {
460
+ AcpTransport,
461
+ acpDecision,
462
+ acpPermission,
463
+ acpWhat
464
+ };
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Antigravity provider definition for ALP.
3
+ */
4
+ export type AntigravityProvider = {
5
+ id: string;
6
+ label: string;
7
+ command: string;
8
+ args?: string[];
9
+ env?: Record<string, string>;
10
+ models?: Array<{
11
+ id: string;
12
+ label?: string;
13
+ description?: string;
14
+ }>;
15
+ additionalModels?: Array<{
16
+ id: string;
17
+ label?: string;
18
+ description?: string;
19
+ }>;
20
+ enabled?: boolean;
21
+ /** Explicit opt-in: read-only is a prompt constraint, never a native sandbox. */
22
+ readOnlyMode?: 'instructions' | 'reject';
23
+ };
24
+ type Listener = (method: string, params: any) => void;
25
+ type RequestHandler = (method: string, params: any) => Promise<unknown>;
26
+ export declare function encodePrompt(text: string): string;
27
+ /**
28
+ * AntigravityTransport runs the Antigravity CLI (agy) over its stream-json protocol.
29
+ * Emits line-delimited JSON events and parses stdout line by line.
30
+ */
31
+ export declare class AntigravityTransport {
32
+ private readonly command;
33
+ private readonly cwd;
34
+ private readonly env;
35
+ private readonly provider?;
36
+ private child?;
37
+ private lines?;
38
+ private exit;
39
+ private listeners;
40
+ private failures;
41
+ private requestHandler?;
42
+ private closed;
43
+ private conversationId;
44
+ private config;
45
+ private activeTurn?;
46
+ private messageText;
47
+ private messages;
48
+ private instructionsSent;
49
+ private tools;
50
+ private bridge?;
51
+ private readyPromise?;
52
+ private resolveReady?;
53
+ private rejectReady?;
54
+ private external?;
55
+ private externalStarting?;
56
+ private readonly startupAbort;
57
+ constructor(command?: string, cwd?: string, env?: NodeJS.ProcessEnv, provider?: AntigravityProvider | undefined);
58
+ initialize(): Promise<void>;
59
+ onNotification(listener: Listener): () => void;
60
+ onFailure(listener: (error: Error) => void): () => void;
61
+ onRequest(handler: RequestHandler): void;
62
+ request(method: string, params: any): Promise<any>;
63
+ private startProcess;
64
+ private receiveLine;
65
+ private handleStepUpdate;
66
+ private handleResult;
67
+ private endTurn;
68
+ private openBridge;
69
+ /** One stable launcher; each child inherits its own socket/token from agy.
70
+ * No session endpoint or credential is written to the global configuration.
71
+ * Leave the launcher installed: removing it would break concurrent sessions.
72
+ */
73
+ private registerMcpServer;
74
+ private serveBridge;
75
+ private toolCall;
76
+ private emit;
77
+ private fail;
78
+ close(): Promise<void>;
79
+ }
80
+ export {};