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