@agent-native/core 0.64.1 → 0.66.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 (40) hide show
  1. package/dist/agent/harness/acp-adapter.d.ts +145 -0
  2. package/dist/agent/harness/acp-adapter.d.ts.map +1 -0
  3. package/dist/agent/harness/acp-adapter.js +632 -0
  4. package/dist/agent/harness/acp-adapter.js.map +1 -0
  5. package/dist/agent/harness/acp-builtin.d.ts +25 -0
  6. package/dist/agent/harness/acp-builtin.d.ts.map +1 -0
  7. package/dist/agent/harness/acp-builtin.js +57 -0
  8. package/dist/agent/harness/acp-builtin.js.map +1 -0
  9. package/dist/agent/harness/builtin.d.ts.map +1 -1
  10. package/dist/agent/harness/builtin.js +15 -0
  11. package/dist/agent/harness/builtin.js.map +1 -1
  12. package/dist/agent/harness/index.d.ts +2 -0
  13. package/dist/agent/harness/index.d.ts.map +1 -1
  14. package/dist/agent/harness/index.js +2 -0
  15. package/dist/agent/harness/index.js.map +1 -1
  16. package/dist/embedding/agent.d.ts +32 -0
  17. package/dist/embedding/agent.d.ts.map +1 -0
  18. package/dist/embedding/agent.js +110 -0
  19. package/dist/embedding/agent.js.map +1 -0
  20. package/dist/embedding/bridge.d.ts +37 -0
  21. package/dist/embedding/bridge.d.ts.map +1 -0
  22. package/dist/embedding/bridge.js +148 -0
  23. package/dist/embedding/bridge.js.map +1 -0
  24. package/dist/embedding/index.d.ts +5 -0
  25. package/dist/embedding/index.d.ts.map +1 -0
  26. package/dist/embedding/index.js +5 -0
  27. package/dist/embedding/index.js.map +1 -0
  28. package/dist/embedding/protocol.d.ts +46 -0
  29. package/dist/embedding/protocol.d.ts.map +1 -0
  30. package/dist/embedding/protocol.js +122 -0
  31. package/dist/embedding/protocol.js.map +1 -0
  32. package/dist/embedding/react.d.ts +39 -0
  33. package/dist/embedding/react.d.ts.map +1 -0
  34. package/dist/embedding/react.js +147 -0
  35. package/dist/embedding/react.js.map +1 -0
  36. package/dist/templates/workspace-core/.agents/skills/harness-agents/SKILL.md +47 -1
  37. package/docs/content/agent-surfaces.md +4 -2
  38. package/docs/content/harness-agents.md +59 -0
  39. package/package.json +6 -1
  40. package/src/templates/workspace-core/.agents/skills/harness-agents/SKILL.md +47 -1
@@ -0,0 +1,632 @@
1
+ /**
2
+ * ACP (Agent Client Protocol) harness adapter.
3
+ *
4
+ * Lets Agent-Native act as an ACP *client* and drive a local coding agent —
5
+ * Gemini CLI, Claude Code, or any other ACP-compliant agent — through the
6
+ * existing {@link AgentHarnessAdapter} substrate. The agent runs as a local
7
+ * subprocess that owns its own loop, tools, and workspace filesystem access,
8
+ * which is exactly the shape ACP was designed for. See:
9
+ * https://agentclientprotocol.com
10
+ *
11
+ * Scope: this adapter targets *local* coding. The agent is spawned as a child
12
+ * process and speaks newline-delimited JSON-RPC over stdio. It reuses whatever
13
+ * local CLI login the agent already has (e.g. `gemini`/`claude` auth in the
14
+ * user's home dir) by inheriting the parent environment. It is not a hosted or
15
+ * sandboxed transport, and it is not a chat/A2A transport.
16
+ *
17
+ * The protocol transport and framing are handled by the official
18
+ * `@zed-industries/agent-client-protocol` package, loaded lazily as an optional
19
+ * dependency so apps that never use ACP do not pay for it. Everything in this
20
+ * file beyond the thin spawn/connection glue is pure mapping logic between ACP
21
+ * `session/update` notifications and {@link AgentHarnessEvent}s.
22
+ */
23
+ import fs from "node:fs/promises";
24
+ import path from "node:path";
25
+ import { spawn } from "node:child_process";
26
+ import { Readable, Writable } from "node:stream";
27
+ /** Grace period between SIGTERM and SIGKILL when tearing a session down. */
28
+ const SIGKILL_GRACE_MS = 2_000;
29
+ /** Keep a bounded tail of child stderr for diagnostics. */
30
+ const STDERR_TAIL_LIMIT = 8_000;
31
+ /**
32
+ * The optional package that carries the ACP protocol transport. Loaded lazily;
33
+ * `resolveAgentHarness` surfaces a clear install error when it is missing.
34
+ */
35
+ export const ACP_PACKAGE = "@zed-industries/agent-client-protocol";
36
+ const DEFAULT_CAPABILITIES = {
37
+ // The agent runs locally with its own workspace access; Agent-Native does not
38
+ // provide it an isolated sandbox.
39
+ sandbox: false,
40
+ // Best-effort: resumable when the agent advertises the `loadSession`
41
+ // capability. Degrades to a fresh session per turn otherwise.
42
+ resumable: true,
43
+ approvals: true,
44
+ // ACP host tools would flow through MCP servers; not wired in this adapter.
45
+ hostTools: false,
46
+ fileEvents: true,
47
+ };
48
+ /**
49
+ * Indirect dynamic import so bundlers/TS do not try to resolve the optional ACP
50
+ * package at build time (mirrors the AI SDK harness adapter).
51
+ */
52
+ const dynamicImport = new Function("specifier", "return import(specifier)");
53
+ export function createAcpHarnessAdapter(options) {
54
+ const name = options.name ?? "acp";
55
+ return {
56
+ name,
57
+ label: options.label ?? "ACP Agent",
58
+ description: options.description ??
59
+ "Drives a local ACP-compliant coding agent over stdio.",
60
+ installPackage: options.installPackage ?? ACP_PACKAGE,
61
+ capabilities: DEFAULT_CAPABILITIES,
62
+ async createSession(sessionOptions) {
63
+ const command = options.command?.trim();
64
+ if (!command) {
65
+ throw new Error(`[acp-harness] Harness "${name}" requires a command. Pass { command, args } when resolving the harness (e.g. resolveAgentHarness("acp", { command: "gemini", args: ["--experimental-acp"] })).`);
66
+ }
67
+ const acp = await dynamicImport(ACP_PACKAGE);
68
+ const cwd = path.resolve(sessionOptions.cwd ?? options.cwd ?? process.cwd());
69
+ const env = {
70
+ ...process.env,
71
+ ...(options.env ?? {}),
72
+ };
73
+ const child = spawn(command, options.args ?? [], {
74
+ cwd,
75
+ env,
76
+ stdio: ["pipe", "pipe", "pipe"],
77
+ });
78
+ const session = new AcpHarnessSession({
79
+ acp,
80
+ child,
81
+ command,
82
+ cwd,
83
+ permissionMode: sessionOptions.permissionMode ?? "allow-reads",
84
+ });
85
+ try {
86
+ await session.initialize(sessionOptions);
87
+ }
88
+ catch (error) {
89
+ await session.destroy();
90
+ throw error;
91
+ }
92
+ return session;
93
+ },
94
+ };
95
+ }
96
+ class AcpHarnessSession {
97
+ id;
98
+ acp;
99
+ child;
100
+ command;
101
+ cwd;
102
+ permissionMode;
103
+ connection;
104
+ acpSessionId = "";
105
+ supportsLoad = false;
106
+ queue = null;
107
+ pendingPermissions = new Map();
108
+ toolTitles = new Map();
109
+ approvalCounter = 0;
110
+ stderrTail = "";
111
+ childExited = false;
112
+ shuttingDown = false;
113
+ constructor(deps) {
114
+ this.acp = deps.acp;
115
+ this.child = deps.child;
116
+ this.command = deps.command;
117
+ this.cwd = deps.cwd;
118
+ this.permissionMode = deps.permissionMode;
119
+ // Placeholder until newSession/loadSession assigns the real id.
120
+ this.id = `acp-${Math.random().toString(36).slice(2)}`;
121
+ this.child.stderr?.on("data", (chunk) => {
122
+ this.stderrTail = (this.stderrTail + chunk.toString()).slice(-STDERR_TAIL_LIMIT);
123
+ });
124
+ this.child.on("exit", () => {
125
+ this.childExited = true;
126
+ if (!this.shuttingDown && this.queue) {
127
+ this.queue.push({
128
+ type: "error",
129
+ error: this.childExitMessage(),
130
+ });
131
+ this.queue.close();
132
+ }
133
+ this.rejectAllPending();
134
+ });
135
+ this.child.on("error", (error) => {
136
+ this.childExited = true;
137
+ if (this.queue) {
138
+ this.queue.push({
139
+ type: "error",
140
+ error: `[acp-harness] ${this.command}: ${error.message}`,
141
+ });
142
+ this.queue.close();
143
+ }
144
+ this.rejectAllPending();
145
+ });
146
+ }
147
+ async initialize(opts) {
148
+ const stream = this.acp.ndJsonStream(Writable.toWeb(this.child.stdin), Readable.toWeb(this.child.stdout));
149
+ this.connection = new this.acp.ClientSideConnection(() => this.createClient(), stream);
150
+ const initResponse = await this.connection.initialize({
151
+ protocolVersion: this.acp.PROTOCOL_VERSION ?? 1,
152
+ clientCapabilities: {
153
+ fs: { readTextFile: true, writeTextFile: true },
154
+ terminal: false,
155
+ },
156
+ });
157
+ this.supportsLoad = Boolean(initResponse?.agentCapabilities?.loadSession);
158
+ const resume = opts.resumeState;
159
+ if (resume?.sessionId && this.supportsLoad) {
160
+ try {
161
+ await this.connection.loadSession({
162
+ sessionId: resume.sessionId,
163
+ cwd: this.cwd,
164
+ mcpServers: [],
165
+ });
166
+ this.acpSessionId = resume.sessionId;
167
+ return;
168
+ }
169
+ catch {
170
+ // Fall through to a fresh session if the agent could not load it.
171
+ }
172
+ }
173
+ const created = await this.connection.newSession({
174
+ cwd: this.cwd,
175
+ mcpServers: [],
176
+ });
177
+ this.acpSessionId =
178
+ typeof created?.sessionId === "string" ? created.sessionId : this.id;
179
+ }
180
+ async *streamTurn(input) {
181
+ if (this.childExited) {
182
+ yield { type: "error", error: this.childExitMessage() };
183
+ return;
184
+ }
185
+ const queue = new AsyncEventQueue();
186
+ this.queue = queue;
187
+ const abort = input.abortSignal;
188
+ const onAbort = () => {
189
+ this.connection?.cancel({ sessionId: this.acpSessionId }).catch(() => { });
190
+ };
191
+ if (abort) {
192
+ if (abort.aborted)
193
+ onAbort();
194
+ else
195
+ abort.addEventListener("abort", onAbort, { once: true });
196
+ }
197
+ this.connection
198
+ .prompt({
199
+ sessionId: this.acpSessionId,
200
+ prompt: buildAcpPromptBlocks(input),
201
+ })
202
+ .then((response) => {
203
+ queue.push({ type: "done", reason: response?.stopReason });
204
+ })
205
+ .catch((error) => {
206
+ queue.push({ type: "error", error: acpErrorMessage(error) });
207
+ })
208
+ .finally(() => {
209
+ if (abort)
210
+ abort.removeEventListener("abort", onAbort);
211
+ this.rejectAllPending();
212
+ queue.close();
213
+ this.queue = null;
214
+ });
215
+ yield* queue;
216
+ }
217
+ async approve(approval) {
218
+ const pending = this.pendingPermissions.get(approval.id);
219
+ if (!pending)
220
+ return;
221
+ this.pendingPermissions.delete(approval.id);
222
+ pending.resolve(buildAcpPermissionResponse(pending.options, approval.approved));
223
+ }
224
+ async detach() {
225
+ const state = { sessionId: this.acpSessionId, cwd: this.cwd };
226
+ await this.destroy();
227
+ return state;
228
+ }
229
+ async stop() {
230
+ return this.detach();
231
+ }
232
+ async destroy() {
233
+ if (this.shuttingDown)
234
+ return;
235
+ this.shuttingDown = true;
236
+ this.rejectAllPending();
237
+ try {
238
+ this.child.stdin?.end();
239
+ }
240
+ catch { }
241
+ if (!this.childExited && this.child.exitCode === null) {
242
+ try {
243
+ this.child.kill("SIGTERM");
244
+ }
245
+ catch { }
246
+ const killTimer = setTimeout(() => {
247
+ try {
248
+ if (this.child.exitCode === null)
249
+ this.child.kill("SIGKILL");
250
+ }
251
+ catch { }
252
+ }, SIGKILL_GRACE_MS);
253
+ killTimer.unref?.();
254
+ }
255
+ }
256
+ // --- ACP Client implementation (agent -> client) ---
257
+ createClient() {
258
+ return {
259
+ sessionUpdate: async (params) => {
260
+ this.handleSessionUpdate(params.update);
261
+ },
262
+ requestPermission: async (params) => this.handlePermission(params),
263
+ readTextFile: async (params) => this.handleReadTextFile(params),
264
+ writeTextFile: async (params) => this.handleWriteTextFile(params),
265
+ };
266
+ }
267
+ handleSessionUpdate(update) {
268
+ if (update?.sessionUpdate === "tool_call" && update.title) {
269
+ this.toolTitles.set(update.toolCallId, update.title);
270
+ }
271
+ // Updates that arrive without an active turn are history replay from
272
+ // loadSession; the transcript already contains them, so drop them.
273
+ if (!this.queue)
274
+ return;
275
+ for (const event of acpUpdateToHarnessEvents(update, (id) => this.toolTitles.get(id))) {
276
+ this.queue.push(event);
277
+ }
278
+ }
279
+ async handlePermission(params) {
280
+ const toolCall = params.toolCall ?? {};
281
+ const decision = acpAutoPermissionDecision(toolCall.kind ?? undefined, this.permissionMode);
282
+ if (decision === "allow") {
283
+ const optionId = selectAcpPermissionOption(params.options ?? [], true);
284
+ if (optionId)
285
+ return { outcome: { outcome: "selected", optionId } };
286
+ }
287
+ if (!this.queue) {
288
+ // No surface to prompt on: decline rather than hang the agent's turn.
289
+ return buildAcpPermissionResponse(params.options ?? [], false);
290
+ }
291
+ const id = `acp-approval-${++this.approvalCounter}`;
292
+ const response = new Promise((resolve) => {
293
+ this.pendingPermissions.set(id, {
294
+ resolve,
295
+ options: params.options ?? [],
296
+ });
297
+ });
298
+ this.queue.push({
299
+ type: "approval-request",
300
+ id,
301
+ tool: toolCall.toolCallId ?? toolCall.title,
302
+ message: toolCall.title
303
+ ? `Approve: ${toolCall.title}`
304
+ : "Agent is requesting permission",
305
+ input: toolCall.rawInput,
306
+ });
307
+ return response;
308
+ }
309
+ async handleReadTextFile(params) {
310
+ const abs = resolveAcpWorkspacePath(this.cwd, params.path);
311
+ const raw = await fs.readFile(abs, "utf8");
312
+ return { content: sliceTextFile(raw, params.line, params.limit) };
313
+ }
314
+ async handleWriteTextFile(params) {
315
+ const abs = resolveAcpWorkspacePath(this.cwd, params.path);
316
+ const existed = await fileExists(abs);
317
+ await fs.mkdir(path.dirname(abs), { recursive: true });
318
+ await fs.writeFile(abs, params.content, "utf8");
319
+ this.queue?.push({
320
+ type: "file-change",
321
+ path: params.path,
322
+ operation: existed ? "update" : "create",
323
+ });
324
+ return {};
325
+ }
326
+ rejectAllPending() {
327
+ if (this.pendingPermissions.size === 0)
328
+ return;
329
+ for (const [, pending] of this.pendingPermissions) {
330
+ pending.resolve({ outcome: { outcome: "cancelled" } });
331
+ }
332
+ this.pendingPermissions.clear();
333
+ }
334
+ childExitMessage() {
335
+ const tail = this.stderrTail.trim();
336
+ const base = `[acp-harness] ${this.command} exited before the turn completed.`;
337
+ return tail ? `${base}\n${tail.slice(-1_000)}` : base;
338
+ }
339
+ }
340
+ // --- Pure helpers (exported for testing) ---
341
+ /** Build the ACP prompt content blocks for a turn. */
342
+ export function buildAcpPromptBlocks(input) {
343
+ if (input.prompt && input.prompt.trim()) {
344
+ return [{ type: "text", text: input.prompt }];
345
+ }
346
+ const messages = input.messages ?? [];
347
+ for (let i = messages.length - 1; i >= 0; i--) {
348
+ if (messages[i].role === "user") {
349
+ const text = messageToText(messages[i].content);
350
+ if (text.trim())
351
+ return [{ type: "text", text }];
352
+ }
353
+ }
354
+ const joined = messages
355
+ .map((message) => messageToText(message.content))
356
+ .filter(Boolean)
357
+ .join("\n\n");
358
+ return [{ type: "text", text: joined }];
359
+ }
360
+ function messageToText(content) {
361
+ if (typeof content === "string")
362
+ return content;
363
+ if (!Array.isArray(content))
364
+ return "";
365
+ return content
366
+ .map((part) => {
367
+ if (typeof part === "string")
368
+ return part;
369
+ if (part && typeof part === "object" && "text" in part) {
370
+ const text = part.text;
371
+ return typeof text === "string" ? text : "";
372
+ }
373
+ return "";
374
+ })
375
+ .filter(Boolean)
376
+ .join("");
377
+ }
378
+ /**
379
+ * Translate a single ACP `session/update` payload into harness events. Pure and
380
+ * stateless; the caller supplies a resolver for tool titles seen on earlier
381
+ * `tool_call` updates so completion events can be labelled.
382
+ */
383
+ export function acpUpdateToHarnessEvents(update, titleFor) {
384
+ switch (update.sessionUpdate) {
385
+ case "agent_message_chunk": {
386
+ const text = acpContentBlockToText(update.content);
387
+ return text ? [{ type: "text-delta", text }] : [];
388
+ }
389
+ case "agent_thought_chunk": {
390
+ const text = acpContentBlockToText(update.content);
391
+ return text ? [{ type: "thinking-delta", text }] : [];
392
+ }
393
+ case "user_message_chunk":
394
+ // The user's own message; already in the transcript.
395
+ return [];
396
+ case "tool_call": {
397
+ const events = [
398
+ {
399
+ type: "tool-start",
400
+ id: update.toolCallId,
401
+ name: update.title || update.kind || "tool",
402
+ input: update.rawInput ?? {},
403
+ },
404
+ ];
405
+ events.push(...acpFileChangeEventsFromToolContent(update.content));
406
+ if (isTerminalToolStatus(update.status)) {
407
+ events.push({
408
+ type: "tool-done",
409
+ id: update.toolCallId,
410
+ name: update.title || titleFor?.(update.toolCallId) || "tool",
411
+ result: update.rawOutput ?? acpToolContentText(update.content),
412
+ });
413
+ }
414
+ return events;
415
+ }
416
+ case "tool_call_update": {
417
+ const content = update.content ?? undefined;
418
+ const events = acpFileChangeEventsFromToolContent(content);
419
+ if (isTerminalToolStatus(update.status)) {
420
+ events.push({
421
+ type: "tool-done",
422
+ id: update.toolCallId,
423
+ name: update.title || titleFor?.(update.toolCallId) || "tool",
424
+ result: update.rawOutput ?? acpToolContentText(content),
425
+ });
426
+ }
427
+ return events;
428
+ }
429
+ case "plan":
430
+ return [
431
+ {
432
+ type: "activity",
433
+ label: acpPlanLabel(update.entries),
434
+ tool: "acp:plan",
435
+ },
436
+ ];
437
+ case "available_commands_update":
438
+ case "current_mode_update":
439
+ return [];
440
+ default:
441
+ return [];
442
+ }
443
+ }
444
+ function isTerminalToolStatus(status) {
445
+ return status === "completed" || status === "failed";
446
+ }
447
+ /** Extract displayable text from an ACP content block. */
448
+ export function acpContentBlockToText(block) {
449
+ if (!block || typeof block !== "object")
450
+ return "";
451
+ if (block.type === "text")
452
+ return typeof block.text === "string" ? block.text : "";
453
+ if (block.type === "resource_link") {
454
+ const label = block.name || block.uri || "";
455
+ return block.uri ? `[${label}](${block.uri})` : label;
456
+ }
457
+ return "";
458
+ }
459
+ function acpToolContentText(content) {
460
+ if (!Array.isArray(content))
461
+ return "";
462
+ return content
463
+ .map((entry) => {
464
+ if (entry?.type === "content")
465
+ return acpContentBlockToText(entry.content);
466
+ if (entry?.type === "diff" && entry.path)
467
+ return `diff: ${entry.path}`;
468
+ return "";
469
+ })
470
+ .filter(Boolean)
471
+ .join("\n");
472
+ }
473
+ /** Derive file-change events from a tool call's `diff` content blocks. */
474
+ export function acpFileChangeEventsFromToolContent(content) {
475
+ if (!Array.isArray(content))
476
+ return [];
477
+ const events = [];
478
+ for (const entry of content) {
479
+ if (entry?.type === "diff" && typeof entry.path === "string") {
480
+ events.push({
481
+ type: "file-change",
482
+ path: entry.path,
483
+ operation: entry.oldText === null || entry.oldText === undefined
484
+ ? "create"
485
+ : "update",
486
+ });
487
+ }
488
+ }
489
+ return events;
490
+ }
491
+ function acpPlanLabel(entries) {
492
+ const list = Array.isArray(entries) ? entries : [];
493
+ const total = list.length;
494
+ const done = list.filter((entry) => entry?.status === "completed").length;
495
+ const active = list.find((entry) => entry?.status === "in_progress");
496
+ const suffix = active?.content ? ` — ${active.content}` : "";
497
+ return `Updated plan (${done}/${total})${suffix}`;
498
+ }
499
+ /**
500
+ * Map an Agent-Native permission mode onto a decision for an ACP permission
501
+ * request, using the tool-call kind the agent reports. Reads always run; edits
502
+ * run under `allow-edits`; everything risky prompts unless `allow-all`.
503
+ */
504
+ export function acpAutoPermissionDecision(kind, mode) {
505
+ if (mode === "allow-all")
506
+ return "allow";
507
+ const resolved = kind ?? "other";
508
+ const readish = resolved === "read" ||
509
+ resolved === "search" ||
510
+ resolved === "fetch" ||
511
+ resolved === "think";
512
+ if (readish)
513
+ return "allow";
514
+ if (mode === "allow-edits") {
515
+ return resolved === "edit" || resolved === "move" ? "allow" : "prompt";
516
+ }
517
+ return "prompt";
518
+ }
519
+ /**
520
+ * Pick the option id to return for an ACP permission request. Prefers the
521
+ * "once" variant so approvals do not silently become "always".
522
+ */
523
+ export function selectAcpPermissionOption(options, approved) {
524
+ const order = approved
525
+ ? ["allow_once", "allow_always"]
526
+ : ["reject_once", "reject_always"];
527
+ for (const kind of order) {
528
+ const match = options.find((option) => option?.kind === kind);
529
+ if (match)
530
+ return match.optionId;
531
+ }
532
+ return undefined;
533
+ }
534
+ function buildAcpPermissionResponse(options, approved) {
535
+ const optionId = selectAcpPermissionOption(options, approved);
536
+ if (optionId)
537
+ return { outcome: { outcome: "selected", optionId } };
538
+ return { outcome: { outcome: "cancelled" } };
539
+ }
540
+ /**
541
+ * Resolve a path requested by the agent against the session workspace, refusing
542
+ * anything that escapes it. The agent already has its own filesystem tools;
543
+ * this `fs/*` surface is a scoped convenience, not an arbitrary read/write hole.
544
+ */
545
+ export function resolveAcpWorkspacePath(cwd, requestedPath) {
546
+ if (typeof requestedPath !== "string" || requestedPath.length === 0) {
547
+ throw new Error("[acp-harness] File path must be a non-empty string.");
548
+ }
549
+ const normalizedCwd = path.resolve(cwd);
550
+ const abs = path.isAbsolute(requestedPath)
551
+ ? path.resolve(requestedPath)
552
+ : path.resolve(normalizedCwd, requestedPath);
553
+ const rel = path.relative(normalizedCwd, abs);
554
+ if (rel === "")
555
+ return abs;
556
+ if (rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) {
557
+ throw new Error(`[acp-harness] Refusing file access outside the session workspace: ${requestedPath}`);
558
+ }
559
+ return abs;
560
+ }
561
+ function sliceTextFile(content, line, limit) {
562
+ if (line == null && limit == null)
563
+ return content;
564
+ const lines = content.split("\n");
565
+ const start = line && line > 0 ? line - 1 : 0;
566
+ const end = limit && limit > 0 ? start + limit : lines.length;
567
+ return lines.slice(start, end).join("\n");
568
+ }
569
+ async function fileExists(filePath) {
570
+ try {
571
+ await fs.access(filePath);
572
+ return true;
573
+ }
574
+ catch {
575
+ return false;
576
+ }
577
+ }
578
+ function acpErrorMessage(error) {
579
+ if (error && typeof error === "object") {
580
+ const record = error;
581
+ if (typeof record.message === "string" && record.message) {
582
+ return record.message;
583
+ }
584
+ if (record.code !== undefined) {
585
+ return `ACP request failed (code ${String(record.code)})`;
586
+ }
587
+ }
588
+ return typeof error === "string" ? error : "ACP request failed";
589
+ }
590
+ /** Minimal async queue bridging ACP's callback updates to an async iterable. */
591
+ class AsyncEventQueue {
592
+ values = [];
593
+ resolvers = [];
594
+ closed = false;
595
+ push(value) {
596
+ if (this.closed)
597
+ return;
598
+ const resolver = this.resolvers.shift();
599
+ if (resolver)
600
+ resolver({ value, done: false });
601
+ else
602
+ this.values.push(value);
603
+ }
604
+ close() {
605
+ if (this.closed)
606
+ return;
607
+ this.closed = true;
608
+ let resolver;
609
+ while ((resolver = this.resolvers.shift())) {
610
+ resolver({ value: undefined, done: true });
611
+ }
612
+ }
613
+ [Symbol.asyncIterator]() {
614
+ return {
615
+ next: () => {
616
+ if (this.values.length > 0) {
617
+ return Promise.resolve({
618
+ value: this.values.shift(),
619
+ done: false,
620
+ });
621
+ }
622
+ if (this.closed) {
623
+ return Promise.resolve({ value: undefined, done: true });
624
+ }
625
+ return new Promise((resolve) => {
626
+ this.resolvers.push(resolve);
627
+ });
628
+ },
629
+ };
630
+ }
631
+ }
632
+ //# sourceMappingURL=acp-adapter.js.map