@theokit/acp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/LICENSE +201 -0
- package/README.md +71 -0
- package/bin/theokit-acp.mjs +113 -0
- package/dist/index.cjs +681 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +116 -0
- package/dist/index.d.ts +116 -0
- package/dist/index.js +655 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
- package/registry/README.md +24 -0
- package/registry/agent.json +12 -0
- package/registry/icon.svg +4 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var stream = require('stream');
|
|
4
|
+
var acp2 = require('@agentclientprotocol/sdk');
|
|
5
|
+
var crypto = require('crypto');
|
|
6
|
+
var fs = require('fs');
|
|
7
|
+
var path = require('path');
|
|
8
|
+
var sdk = require('@theokit/sdk');
|
|
9
|
+
|
|
10
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
|
+
|
|
12
|
+
function _interopNamespace(e) {
|
|
13
|
+
if (e && e.__esModule) return e;
|
|
14
|
+
var n = Object.create(null);
|
|
15
|
+
if (e) {
|
|
16
|
+
Object.keys(e).forEach(function (k) {
|
|
17
|
+
if (k !== 'default') {
|
|
18
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
19
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
20
|
+
enumerable: true,
|
|
21
|
+
get: function () { return e[k]; }
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
n.default = e;
|
|
27
|
+
return Object.freeze(n);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
var acp2__namespace = /*#__PURE__*/_interopNamespace(acp2);
|
|
31
|
+
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
32
|
+
|
|
33
|
+
// src/serve.ts
|
|
34
|
+
|
|
35
|
+
// src/agent-resolver.ts
|
|
36
|
+
function looksLikeSDKAgent(value) {
|
|
37
|
+
return typeof value === "object" && value !== null && typeof value.agentId === "string" && typeof value.send === "function";
|
|
38
|
+
}
|
|
39
|
+
var InvalidAgentError = class extends Error {
|
|
40
|
+
name = "InvalidAgentError";
|
|
41
|
+
};
|
|
42
|
+
function resolveAgentFactory(input, options = {}) {
|
|
43
|
+
const log = options.log ?? ((msg) => process.stderr.write(`${msg}
|
|
44
|
+
`));
|
|
45
|
+
if (typeof input === "function") {
|
|
46
|
+
return input;
|
|
47
|
+
}
|
|
48
|
+
if (looksLikeSDKAgent(input)) {
|
|
49
|
+
let warned = false;
|
|
50
|
+
return (_sessionId) => {
|
|
51
|
+
if (!warned) {
|
|
52
|
+
warned = true;
|
|
53
|
+
log(
|
|
54
|
+
"[acp] WARN: serveAcp received a single SDKAgent instance; sessions will share conversation state. Pass a factory (sessionId) => Agent.create(...) for per-session isolation (D351)."
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return input;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
throw new InvalidAgentError(
|
|
61
|
+
"serveAcp({ agent }) must receive an SDKAgent or a factory (sessionId) => SDKAgent | Promise<SDKAgent>"
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
function buildInitializeResponse(_params, deps) {
|
|
65
|
+
const userCaps = deps.capabilities ?? {};
|
|
66
|
+
return {
|
|
67
|
+
protocolVersion: acp2__namespace.PROTOCOL_VERSION,
|
|
68
|
+
agentCapabilities: {
|
|
69
|
+
loadSession: userCaps.loadSession ?? true,
|
|
70
|
+
promptCapabilities: {
|
|
71
|
+
image: userCaps.prompt?.image ?? true,
|
|
72
|
+
audio: userCaps.prompt?.audio ?? false,
|
|
73
|
+
embeddedContext: userCaps.prompt?.embeddedContext ?? true
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
authMethods: []
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
var ACP_ERR = {
|
|
80
|
+
INVALID_REQUEST: -32602,
|
|
81
|
+
INTERNAL_ERROR: -32603,
|
|
82
|
+
INVALID_SESSION: -32001,
|
|
83
|
+
AUTH_REQUIRED: -32002
|
|
84
|
+
};
|
|
85
|
+
function resolveCwdOrError(rawCwd) {
|
|
86
|
+
if (typeof rawCwd !== "string" || rawCwd.length === 0) {
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
error: { code: ACP_ERR.INVALID_REQUEST, message: "cwd is required" }
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const resolved = path__default.default.resolve(rawCwd);
|
|
93
|
+
if (!fs.existsSync(resolved)) {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
error: { code: ACP_ERR.INVALID_REQUEST, message: `cwd not found: ${resolved}` }
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return { ok: true, value: resolved };
|
|
100
|
+
}
|
|
101
|
+
async function handleNewSession(params, deps) {
|
|
102
|
+
const cwdResult = resolveCwdOrError(params.cwd);
|
|
103
|
+
if (!cwdResult.ok) return { error: cwdResult.error };
|
|
104
|
+
const sessionId = crypto.randomUUID();
|
|
105
|
+
let agent;
|
|
106
|
+
try {
|
|
107
|
+
agent = await deps.factory(sessionId);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
110
|
+
deps.log(`[acp] new_session factory threw: ${msg}`);
|
|
111
|
+
return {
|
|
112
|
+
error: {
|
|
113
|
+
code: ACP_ERR.INTERNAL_ERROR,
|
|
114
|
+
message: `agent factory threw: ${msg}`
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const session = {
|
|
119
|
+
sessionId,
|
|
120
|
+
agent,
|
|
121
|
+
createdAt: Date.now(),
|
|
122
|
+
lastUsedAt: Date.now(),
|
|
123
|
+
abortController: new AbortController(),
|
|
124
|
+
cwd: cwdResult.value
|
|
125
|
+
};
|
|
126
|
+
deps.store.create(session);
|
|
127
|
+
return { response: { sessionId } };
|
|
128
|
+
}
|
|
129
|
+
var STORAGE_HINT = " \u2014 if running on serverless/multi-host infra, pass conversationStorage to Agent.create (see docs/recipes/conversation-storage-postgres.md)";
|
|
130
|
+
async function handleLoadSession(params, deps) {
|
|
131
|
+
const cwdResult = resolveCwdOrError(params.cwd);
|
|
132
|
+
if (!cwdResult.ok) return { error: cwdResult.error };
|
|
133
|
+
if (deps.store.has(params.sessionId)) {
|
|
134
|
+
return {
|
|
135
|
+
error: {
|
|
136
|
+
code: ACP_ERR.INVALID_REQUEST,
|
|
137
|
+
message: `session ${params.sessionId} already loaded`
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
let agent;
|
|
142
|
+
try {
|
|
143
|
+
agent = await sdk.Agent.resume(params.sessionId, { local: { cwd: cwdResult.value } });
|
|
144
|
+
} catch (err) {
|
|
145
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
146
|
+
deps.log(`[acp] load_session Agent.resume threw for ${params.sessionId}: ${msg}`);
|
|
147
|
+
return {
|
|
148
|
+
error: {
|
|
149
|
+
code: ACP_ERR.INVALID_SESSION,
|
|
150
|
+
message: `session not found: ${params.sessionId}${STORAGE_HINT}`
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const session = {
|
|
155
|
+
sessionId: params.sessionId,
|
|
156
|
+
agent,
|
|
157
|
+
createdAt: Date.now(),
|
|
158
|
+
lastUsedAt: Date.now(),
|
|
159
|
+
abortController: new AbortController(),
|
|
160
|
+
cwd: cwdResult.value
|
|
161
|
+
};
|
|
162
|
+
deps.store.create(session);
|
|
163
|
+
return { response: {} };
|
|
164
|
+
}
|
|
165
|
+
async function handleForkSession(params, deps) {
|
|
166
|
+
const parent = deps.store.get(params.sessionId);
|
|
167
|
+
if (parent === void 0) {
|
|
168
|
+
return {
|
|
169
|
+
error: {
|
|
170
|
+
code: ACP_ERR.INVALID_SESSION,
|
|
171
|
+
message: `parent session not loaded: ${params.sessionId}`
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
parent.cwd;
|
|
176
|
+
if (typeof params.cwd === "string" && params.cwd.length > 0) {
|
|
177
|
+
const cwdResult = resolveCwdOrError(params.cwd);
|
|
178
|
+
if (!cwdResult.ok) return { error: cwdResult.error };
|
|
179
|
+
cwdResult.value;
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
error: {
|
|
183
|
+
code: ACP_ERR.INVALID_REQUEST,
|
|
184
|
+
message: "session/fork is deferred to @theokit/acp v0.2 \u2014 current SDK fork is a one-shot sub-run, not a session split"
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function handleListSessions(_params, store) {
|
|
189
|
+
const sessions = store.list().map((s) => ({
|
|
190
|
+
sessionId: s.sessionId,
|
|
191
|
+
cwd: s.cwd,
|
|
192
|
+
title: s.agent.agentId,
|
|
193
|
+
lastUpdatedAt: new Date(s.lastUsedAt).toISOString()
|
|
194
|
+
}));
|
|
195
|
+
return { sessions };
|
|
196
|
+
}
|
|
197
|
+
function handleCancel(params, deps) {
|
|
198
|
+
const session = deps.store.get(params.sessionId);
|
|
199
|
+
if (session === void 0) return;
|
|
200
|
+
session.lastUsedAt = Date.now();
|
|
201
|
+
if (!session.abortController.signal.aborted) {
|
|
202
|
+
session.abortController.abort("cancelled by ACP client");
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/translator.ts
|
|
207
|
+
var MAX_CONSECUTIVE_SEND_FAILURES = 10;
|
|
208
|
+
function toolKind(name) {
|
|
209
|
+
const n = name.toLowerCase();
|
|
210
|
+
if (n === "read_file" || n === "list_dir" || n === "git_diff" || n === "search_text")
|
|
211
|
+
return "read";
|
|
212
|
+
if (n === "write_file" || n === "apply_patch") return "edit";
|
|
213
|
+
if (n === "run_command" || n === "run_vitest") return "execute";
|
|
214
|
+
if (n === "web_search" || n === "fetch_url") return "fetch";
|
|
215
|
+
return "other";
|
|
216
|
+
}
|
|
217
|
+
function mapToolStatus(sdkStatus) {
|
|
218
|
+
if (sdkStatus === "completed") return "completed";
|
|
219
|
+
if (sdkStatus === "error") return "failed";
|
|
220
|
+
return "in_progress";
|
|
221
|
+
}
|
|
222
|
+
function textChunk(text) {
|
|
223
|
+
return { type: "text", text };
|
|
224
|
+
}
|
|
225
|
+
async function safeNotify(conn, sessionId, update, state, log) {
|
|
226
|
+
if (state.bailed) return;
|
|
227
|
+
try {
|
|
228
|
+
await conn.sessionUpdate({ sessionId, update });
|
|
229
|
+
state.consecutiveFailures = 0;
|
|
230
|
+
} catch (err) {
|
|
231
|
+
state.consecutiveFailures += 1;
|
|
232
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
233
|
+
log(
|
|
234
|
+
`[acp] sessionUpdate failed (${state.consecutiveFailures}/${MAX_CONSECUTIVE_SEND_FAILURES}): ${msg}`
|
|
235
|
+
);
|
|
236
|
+
if (state.consecutiveFailures >= MAX_CONSECUTIVE_SEND_FAILURES) {
|
|
237
|
+
state.bailed = true;
|
|
238
|
+
log("[acp] translator bailing \u2014 too many consecutive sessionUpdate failures");
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
async function handleAssistant(msg, args, state) {
|
|
243
|
+
for (const content of msg.message.content) {
|
|
244
|
+
if (content.type === "text") {
|
|
245
|
+
await safeNotify(
|
|
246
|
+
args.conn,
|
|
247
|
+
args.sessionId,
|
|
248
|
+
{ sessionUpdate: "agent_message_chunk", content: textChunk(content.text) },
|
|
249
|
+
state,
|
|
250
|
+
args.log
|
|
251
|
+
);
|
|
252
|
+
} else if (content.type === "tool_use") {
|
|
253
|
+
await safeNotify(
|
|
254
|
+
args.conn,
|
|
255
|
+
args.sessionId,
|
|
256
|
+
{
|
|
257
|
+
sessionUpdate: "tool_call",
|
|
258
|
+
toolCallId: content.id,
|
|
259
|
+
title: content.name,
|
|
260
|
+
kind: toolKind(content.name),
|
|
261
|
+
status: "in_progress"
|
|
262
|
+
},
|
|
263
|
+
state,
|
|
264
|
+
args.log
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async function handleToolCall(msg, args, state) {
|
|
270
|
+
const content = [];
|
|
271
|
+
if (msg.result !== void 0 && msg.result !== null) {
|
|
272
|
+
const text = typeof msg.result === "string" ? msg.result : JSON.stringify(msg.result);
|
|
273
|
+
content.push({ type: "content", content: textChunk(text) });
|
|
274
|
+
}
|
|
275
|
+
await safeNotify(
|
|
276
|
+
args.conn,
|
|
277
|
+
args.sessionId,
|
|
278
|
+
{
|
|
279
|
+
sessionUpdate: "tool_call_update",
|
|
280
|
+
toolCallId: msg.call_id,
|
|
281
|
+
status: mapToolStatus(msg.status),
|
|
282
|
+
...content.length > 0 ? { content } : {}
|
|
283
|
+
},
|
|
284
|
+
state,
|
|
285
|
+
args.log
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
async function handleThinking(msg, args, state) {
|
|
289
|
+
if (args.suppressThinking === true) return;
|
|
290
|
+
await safeNotify(
|
|
291
|
+
args.conn,
|
|
292
|
+
args.sessionId,
|
|
293
|
+
{ sessionUpdate: "agent_thought_chunk", content: textChunk(msg.text) },
|
|
294
|
+
state,
|
|
295
|
+
args.log
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
async function translateStream(args) {
|
|
299
|
+
const state = { consecutiveFailures: 0, bailed: false };
|
|
300
|
+
for await (const msg of args.messages) {
|
|
301
|
+
if (args.signal.aborted) break;
|
|
302
|
+
if (state.bailed) break;
|
|
303
|
+
await dispatchMessage(msg, args, state);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
async function dispatchMessage(msg, args, state) {
|
|
307
|
+
switch (msg.type) {
|
|
308
|
+
case "system":
|
|
309
|
+
return;
|
|
310
|
+
// no ACP equivalent
|
|
311
|
+
case "user":
|
|
312
|
+
return;
|
|
313
|
+
// echo, no ACP equivalent
|
|
314
|
+
case "assistant":
|
|
315
|
+
await handleAssistant(msg, args, state);
|
|
316
|
+
return;
|
|
317
|
+
case "thinking":
|
|
318
|
+
await handleThinking(msg, args, state);
|
|
319
|
+
return;
|
|
320
|
+
case "tool_call":
|
|
321
|
+
await handleToolCall(msg, args, state);
|
|
322
|
+
return;
|
|
323
|
+
case "status":
|
|
324
|
+
return;
|
|
325
|
+
// cloud-only
|
|
326
|
+
case "task":
|
|
327
|
+
return;
|
|
328
|
+
// milestones — skipped in v0.1
|
|
329
|
+
case "request":
|
|
330
|
+
return;
|
|
331
|
+
// permission requests routed via plugin
|
|
332
|
+
case "object_delta":
|
|
333
|
+
return;
|
|
334
|
+
// streamObject not used in plain prompt flow
|
|
335
|
+
default: {
|
|
336
|
+
const _exhaustive = msg;
|
|
337
|
+
throw new Error(`unhandled SDKMessage type: ${JSON.stringify(_exhaustive)}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// src/permission-plugin.ts
|
|
343
|
+
async function askWithTimeout(args, toolName) {
|
|
344
|
+
const toolCallId = `acp-perm-${crypto.randomUUID()}`;
|
|
345
|
+
const reqPromise = args.conn.requestPermission({
|
|
346
|
+
sessionId: args.sessionId,
|
|
347
|
+
toolCall: {
|
|
348
|
+
toolCallId,
|
|
349
|
+
title: toolName,
|
|
350
|
+
kind: toolKind(toolName),
|
|
351
|
+
status: "pending"
|
|
352
|
+
},
|
|
353
|
+
options: [
|
|
354
|
+
{ optionId: "allow", name: "Allow", kind: "allow_once" },
|
|
355
|
+
{ optionId: "deny", name: "Deny", kind: "reject_once" }
|
|
356
|
+
]
|
|
357
|
+
});
|
|
358
|
+
let timeoutHandle;
|
|
359
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
360
|
+
timeoutHandle = setTimeout(() => {
|
|
361
|
+
reject(new Error("permission_timeout"));
|
|
362
|
+
}, args.timeoutMs);
|
|
363
|
+
});
|
|
364
|
+
let response;
|
|
365
|
+
try {
|
|
366
|
+
response = await Promise.race([reqPromise, timeoutPromise]);
|
|
367
|
+
} catch (err) {
|
|
368
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
369
|
+
if (msg === "permission_timeout") {
|
|
370
|
+
return { block: true, message: `permission timed out after ${args.timeoutMs}ms` };
|
|
371
|
+
}
|
|
372
|
+
return { block: true, message: `permission request failed (client disconnected?): ${msg}` };
|
|
373
|
+
} finally {
|
|
374
|
+
if (timeoutHandle !== void 0) clearTimeout(timeoutHandle);
|
|
375
|
+
}
|
|
376
|
+
const outcome = response.outcome;
|
|
377
|
+
if (outcome.outcome === "cancelled") {
|
|
378
|
+
return { block: true, message: "permission cancelled by client" };
|
|
379
|
+
}
|
|
380
|
+
if (outcome.outcome === "selected" && outcome.optionId === "deny") {
|
|
381
|
+
return { block: true, message: "denied by user" };
|
|
382
|
+
}
|
|
383
|
+
return void 0;
|
|
384
|
+
}
|
|
385
|
+
function installPermissionPlugin(agent, args) {
|
|
386
|
+
const plugin = sdk.definePlugin({
|
|
387
|
+
name: `acp-permission-${args.sessionId}`,
|
|
388
|
+
version: "1.0.0",
|
|
389
|
+
kind: "general",
|
|
390
|
+
register(ctx) {
|
|
391
|
+
ctx.on("pre_tool_call", async (rawCtx) => {
|
|
392
|
+
const event = rawCtx;
|
|
393
|
+
if (args.mode === "deny") {
|
|
394
|
+
return { block: true, message: "denied (permissionDefault=deny)" };
|
|
395
|
+
}
|
|
396
|
+
if (args.trustedTools.has(event.name)) {
|
|
397
|
+
return void 0;
|
|
398
|
+
}
|
|
399
|
+
return askWithTimeout(args, event.name);
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
const agentLike = agent;
|
|
404
|
+
const mgr = agentLike.pluginManager?.();
|
|
405
|
+
if (mgr === void 0) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (typeof mgr.register === "function") {
|
|
409
|
+
mgr.register(plugin);
|
|
410
|
+
} else if (typeof mgr.initialize === "function") {
|
|
411
|
+
void mgr.initialize([plugin]);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/types.ts
|
|
416
|
+
var PromptTooLargeError = class extends Error {
|
|
417
|
+
constructor(size, limit) {
|
|
418
|
+
super(`prompt exceeds ${limit} bytes (got ${size})`);
|
|
419
|
+
this.size = size;
|
|
420
|
+
this.limit = limit;
|
|
421
|
+
}
|
|
422
|
+
size;
|
|
423
|
+
limit;
|
|
424
|
+
name = "PromptTooLargeError";
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
// src/prompt-extract.ts
|
|
428
|
+
function utf8Bytes(s) {
|
|
429
|
+
return Buffer.byteLength(s, "utf8");
|
|
430
|
+
}
|
|
431
|
+
function base64DecodedBytes(b64) {
|
|
432
|
+
return Math.floor(b64.length * 3 / 4);
|
|
433
|
+
}
|
|
434
|
+
function recordBytes(state, bytes) {
|
|
435
|
+
state.totalBytes += bytes;
|
|
436
|
+
if (state.totalBytes > state.maxBytes) {
|
|
437
|
+
throw new PromptTooLargeError(state.totalBytes, state.maxBytes);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
function processBlock(block, state) {
|
|
441
|
+
switch (block.type) {
|
|
442
|
+
case "text":
|
|
443
|
+
recordBytes(state, utf8Bytes(block.text));
|
|
444
|
+
state.textParts.push(block.text);
|
|
445
|
+
return;
|
|
446
|
+
case "image": {
|
|
447
|
+
const data = block.data ?? "";
|
|
448
|
+
const bytes = base64DecodedBytes(data);
|
|
449
|
+
recordBytes(state, bytes);
|
|
450
|
+
state.attachments.push({ type: "image", mimeType: block.mimeType, data, bytes });
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
case "audio": {
|
|
454
|
+
const data = block.data ?? "";
|
|
455
|
+
const bytes = base64DecodedBytes(data);
|
|
456
|
+
recordBytes(state, bytes);
|
|
457
|
+
state.attachments.push({ type: "audio", mimeType: block.mimeType, data, bytes });
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
case "resource_link": {
|
|
461
|
+
const uri = block.uri ?? "";
|
|
462
|
+
const bytes = utf8Bytes(uri);
|
|
463
|
+
recordBytes(state, bytes);
|
|
464
|
+
state.attachments.push({ type: "resource_link", uri, bytes });
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
case "resource": {
|
|
468
|
+
const bytes = utf8Bytes(JSON.stringify(block));
|
|
469
|
+
recordBytes(state, bytes);
|
|
470
|
+
state.attachments.push({ type: "embedded_resource", bytes });
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
default: {
|
|
474
|
+
const _exhaustive = block;
|
|
475
|
+
throw new Error(`unknown ContentBlock type: ${JSON.stringify(_exhaustive)}`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
function extractPrompt(blocks, maxBytes) {
|
|
480
|
+
const state = { textParts: [], attachments: [], totalBytes: 0, maxBytes };
|
|
481
|
+
for (const block of blocks) processBlock(block, state);
|
|
482
|
+
return { text: state.textParts.join("\n"), attachments: state.attachments };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// src/prompt-handler.ts
|
|
486
|
+
function mapStopReason(runStatus, errorCode) {
|
|
487
|
+
if (errorCode === "aborted" || runStatus === "cancelled") return "cancelled";
|
|
488
|
+
if (errorCode === "safety_blocked") return "refusal";
|
|
489
|
+
if (errorCode === "context_length_exceeded" || errorCode === "max_tokens") return "max_tokens";
|
|
490
|
+
if (errorCode === "max_iterations") return "max_turn_requests";
|
|
491
|
+
if (runStatus === "finished") return "end_turn";
|
|
492
|
+
return "end_turn";
|
|
493
|
+
}
|
|
494
|
+
function isAbortError(err) {
|
|
495
|
+
if (!(err instanceof Error)) return false;
|
|
496
|
+
return err.name === "AbortError" || err.message.includes("aborted");
|
|
497
|
+
}
|
|
498
|
+
function extractOrError(params, deps) {
|
|
499
|
+
try {
|
|
500
|
+
const result = extractPrompt(params.prompt, deps.maxPromptBytes);
|
|
501
|
+
if (result.text.length === 0) {
|
|
502
|
+
return { ok: false, error: { code: ACP_ERR.INVALID_REQUEST, message: "empty prompt" } };
|
|
503
|
+
}
|
|
504
|
+
return { ok: true, text: result.text };
|
|
505
|
+
} catch (err) {
|
|
506
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
507
|
+
deps.log(`[acp] prompt extract rejected: ${msg}`);
|
|
508
|
+
return { ok: false, error: { code: ACP_ERR.INVALID_REQUEST, message: msg } };
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
async function runPrompt(session, text, params, deps) {
|
|
512
|
+
const run = await session.agent.send(text, { signal: session.abortController.signal });
|
|
513
|
+
try {
|
|
514
|
+
await translateStream({
|
|
515
|
+
messages: run.stream(),
|
|
516
|
+
conn: deps.conn,
|
|
517
|
+
sessionId: params.sessionId,
|
|
518
|
+
signal: session.abortController.signal,
|
|
519
|
+
log: deps.log
|
|
520
|
+
});
|
|
521
|
+
} catch (err) {
|
|
522
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
523
|
+
deps.log(`[acp] translator threw mid-stream: ${msg}`);
|
|
524
|
+
}
|
|
525
|
+
const result = await run.wait();
|
|
526
|
+
const errorCode = result.status === "error" && result.error !== void 0 ? result.error.code ?? "" : "";
|
|
527
|
+
return mapStopReason(result.status, errorCode);
|
|
528
|
+
}
|
|
529
|
+
async function handlePrompt(params, deps) {
|
|
530
|
+
const session = deps.store.get(params.sessionId);
|
|
531
|
+
if (session === void 0) {
|
|
532
|
+
return {
|
|
533
|
+
error: { code: ACP_ERR.INVALID_SESSION, message: `unknown session: ${params.sessionId}` }
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
session.lastUsedAt = Date.now();
|
|
537
|
+
const extracted = extractOrError(params, deps);
|
|
538
|
+
if (!extracted.ok) return { error: extracted.error };
|
|
539
|
+
if (deps.permissionMode !== "auto") {
|
|
540
|
+
installPermissionPlugin(session.agent, {
|
|
541
|
+
conn: deps.conn,
|
|
542
|
+
sessionId: params.sessionId,
|
|
543
|
+
mode: deps.permissionMode,
|
|
544
|
+
trustedTools: deps.trustedTools,
|
|
545
|
+
timeoutMs: deps.permissionTimeoutMs
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
try {
|
|
549
|
+
const stopReason = await runPrompt(session, extracted.text, params, deps);
|
|
550
|
+
return { response: { stopReason } };
|
|
551
|
+
} catch (err) {
|
|
552
|
+
if (isAbortError(err)) return { response: { stopReason: "cancelled" } };
|
|
553
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
554
|
+
deps.log(`[acp] agent.send threw: ${msg}`);
|
|
555
|
+
return { error: { code: ACP_ERR.INTERNAL_ERROR, message: msg } };
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// src/session-store.ts
|
|
560
|
+
var SessionStore = class {
|
|
561
|
+
map = /* @__PURE__ */ new Map();
|
|
562
|
+
create(session) {
|
|
563
|
+
if (this.map.has(session.sessionId)) {
|
|
564
|
+
throw new Error(`duplicate sessionId: ${session.sessionId}`);
|
|
565
|
+
}
|
|
566
|
+
this.map.set(session.sessionId, session);
|
|
567
|
+
}
|
|
568
|
+
get(sessionId) {
|
|
569
|
+
return this.map.get(sessionId);
|
|
570
|
+
}
|
|
571
|
+
delete(sessionId) {
|
|
572
|
+
return this.map.delete(sessionId);
|
|
573
|
+
}
|
|
574
|
+
list() {
|
|
575
|
+
return [...this.map.values()];
|
|
576
|
+
}
|
|
577
|
+
size() {
|
|
578
|
+
return this.map.size;
|
|
579
|
+
}
|
|
580
|
+
has(sessionId) {
|
|
581
|
+
return this.map.has(sessionId);
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
// src/serve.ts
|
|
586
|
+
var DEFAULT_MAX_PROMPT_BYTES = 2 * 1024 * 1024;
|
|
587
|
+
var DEFAULT_PERMISSION_TIMEOUT_MS = 6e4;
|
|
588
|
+
var DEFAULT_PERMISSION_MODE = "ask";
|
|
589
|
+
async function serveAcp(options) {
|
|
590
|
+
const log = options.log ?? ((msg) => process.stderr.write(`${msg}
|
|
591
|
+
`));
|
|
592
|
+
const factory = resolveAgentFactory(options.agent, { log });
|
|
593
|
+
const store = new SessionStore();
|
|
594
|
+
const maxPromptBytes = options.maxPromptBytes ?? DEFAULT_MAX_PROMPT_BYTES;
|
|
595
|
+
const permissionMode = options.permissionDefault ?? DEFAULT_PERMISSION_MODE;
|
|
596
|
+
const permissionTimeoutMs = options.permissionTimeoutMs ?? DEFAULT_PERMISSION_TIMEOUT_MS;
|
|
597
|
+
const trustedTools = new Set(options.trustedTools ?? []);
|
|
598
|
+
const stdin = options.stdin ?? process.stdin;
|
|
599
|
+
const stdout = options.stdout ?? process.stdout;
|
|
600
|
+
const inputWeb = stream.Readable.toWeb(stdin);
|
|
601
|
+
const outputWeb = new WritableStream({
|
|
602
|
+
write(chunk) {
|
|
603
|
+
return new Promise((resolve, reject) => {
|
|
604
|
+
const ok = stdout.write(chunk, (err) => err ? reject(err) : void 0);
|
|
605
|
+
if (ok) resolve();
|
|
606
|
+
else stdout.once("drain", resolve);
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
const stream$1 = acp2__namespace.ndJsonStream(outputWeb, inputWeb);
|
|
611
|
+
const closedPromise = new Promise((resolve) => {
|
|
612
|
+
stdin.once("end", () => resolve());
|
|
613
|
+
stdin.once("close", () => resolve());
|
|
614
|
+
});
|
|
615
|
+
new acp2__namespace.AgentSideConnection((conn) => {
|
|
616
|
+
return {
|
|
617
|
+
initialize: async (params) => buildInitializeResponse(params, { capabilities: options.capabilities }),
|
|
618
|
+
authenticate: async (_params) => {
|
|
619
|
+
return {};
|
|
620
|
+
},
|
|
621
|
+
newSession: async (params) => {
|
|
622
|
+
const result = await handleNewSession(params, { factory, store, log });
|
|
623
|
+
if ("error" in result) {
|
|
624
|
+
throw new acp2__namespace.RequestError(result.error.code, result.error.message);
|
|
625
|
+
}
|
|
626
|
+
return result.response;
|
|
627
|
+
},
|
|
628
|
+
loadSession: async (params) => {
|
|
629
|
+
const result = await handleLoadSession(params, { store, log });
|
|
630
|
+
if ("error" in result) {
|
|
631
|
+
throw new acp2__namespace.RequestError(result.error.code, result.error.message);
|
|
632
|
+
}
|
|
633
|
+
return result.response;
|
|
634
|
+
},
|
|
635
|
+
unstable_forkSession: async (params) => {
|
|
636
|
+
const result = await handleForkSession(params, { store});
|
|
637
|
+
if ("error" in result) {
|
|
638
|
+
throw new acp2__namespace.RequestError(result.error.code, result.error.message);
|
|
639
|
+
}
|
|
640
|
+
return result.response;
|
|
641
|
+
},
|
|
642
|
+
listSessions: async (params) => handleListSessions(params, store),
|
|
643
|
+
cancel: async (params) => {
|
|
644
|
+
handleCancel(params, { store });
|
|
645
|
+
},
|
|
646
|
+
prompt: async (params) => {
|
|
647
|
+
const result = await handlePrompt(params, {
|
|
648
|
+
store,
|
|
649
|
+
conn,
|
|
650
|
+
maxPromptBytes,
|
|
651
|
+
permissionMode,
|
|
652
|
+
permissionTimeoutMs,
|
|
653
|
+
trustedTools,
|
|
654
|
+
log
|
|
655
|
+
});
|
|
656
|
+
if ("error" in result) {
|
|
657
|
+
throw new acp2__namespace.RequestError(result.error.code, result.error.message);
|
|
658
|
+
}
|
|
659
|
+
return result.response;
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
}, stream$1);
|
|
663
|
+
await closedPromise;
|
|
664
|
+
const sessions = store.list();
|
|
665
|
+
log(`[acp] stdin closed; disposing ${sessions.length} session(s)`);
|
|
666
|
+
await Promise.allSettled(
|
|
667
|
+
sessions.map(async (session) => {
|
|
668
|
+
try {
|
|
669
|
+
await session.agent.dispose();
|
|
670
|
+
} catch (err) {
|
|
671
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
672
|
+
log(`[acp] dispose of ${session.sessionId} threw: ${msg}`);
|
|
673
|
+
}
|
|
674
|
+
})
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
exports.PromptTooLargeError = PromptTooLargeError;
|
|
679
|
+
exports.serveAcp = serveAcp;
|
|
680
|
+
//# sourceMappingURL=index.cjs.map
|
|
681
|
+
//# sourceMappingURL=index.cjs.map
|