@tanstack/ai-acp 0.2.3 → 0.2.4

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.
@@ -1,313 +1,318 @@
1
- import { normalizeSystemPrompts, EventType } from "@tanstack/ai";
2
- import { toRunErrorRawEvent } from "@tanstack/ai/adapter-internals";
3
- import { BaseTextAdapter } from "@tanstack/ai/adapters";
4
- import { SandboxCapability, getSandbox, buildApprovalRequestedEvent, resolveHarnessCwd, createBridgeEventChannel, getToolBridgeProvisioner, nodeHttpBridgeProvisioner, getWorkspaceProjection, mergeChunkStreams } from "@tanstack/ai-sandbox";
5
1
  import { AsyncQueue } from "../stream/queue.js";
6
- import { startAcpSession } from "../session/acp-client.js";
7
2
  import { translateAcpStream } from "../stream/translate.js";
3
+ import { startAcpSession } from "../session/acp-client.js";
8
4
  import { resolveInteractivePermission, resolvePermission } from "../permissions.js";
9
5
  import { buildAcpPrompt } from "../messages/prompt.js";
10
6
  import { projectAcpWorkspace, workspaceMcpServers } from "./projection.js";
11
- const DEFAULT_WORKDIR = "/workspace";
7
+ import { EventType, normalizeSystemPrompts } from "@tanstack/ai";
8
+ import { DurableAttachNotSupportedError, SandboxCapability, buildApprovalRequestedEvent, createBridgeEventChannel, getSandbox, getSandboxDurability, getToolBridgeProvisioner, getWorkspaceProjection, mergeChunkStreams, nodeHttpBridgeProvisioner, resolveDurableRunId, resolveHarnessCwd } from "@tanstack/ai-sandbox";
9
+ import { toRunErrorRawEvent } from "@tanstack/ai/adapter-internals";
10
+ import { BaseTextAdapter } from "@tanstack/ai/adapters";
11
+ //#region src/adapters/compatible.ts
12
+ var DEFAULT_WORKDIR = "/workspace";
12
13
  function q(value) {
13
- return `'${value.replace(/'/g, `'\\''`)}'`;
14
+ return `'${value.replace(/'/g, `'\\''`)}'`;
14
15
  }
15
16
  async function disposeTransport(transport) {
16
- if (transport.kind === "stdio") {
17
- await transport.process.kill();
18
- return;
19
- }
20
- await transport.dispose();
21
- }
22
- class AcpCompatibleTextAdapter extends BaseTextAdapter {
23
- name;
24
- requires = [SandboxCapability];
25
- harness;
26
- constructor(config, model) {
27
- super({}, model);
28
- if (config.command === void 0 && config.openTransport === void 0) {
29
- throw new Error(
30
- `acpCompatible("${config.name}") needs either a "command" or an "openTransport".`
31
- );
32
- }
33
- this.harness = config;
34
- this.name = config.name;
35
- }
36
- sandboxFrom(options) {
37
- const ctx = options.capabilities;
38
- if (!ctx) {
39
- throw new Error(
40
- `Adapter "${this.name}" requires a sandbox. Add withSandbox(defineSandbox({ ... })) to chat() middleware.`
41
- );
42
- }
43
- return getSandbox(ctx);
44
- }
45
- buildPrompt(messages, sessionId) {
46
- return this.harness.buildPrompt ? this.harness.buildPrompt(messages, sessionId) : buildAcpPrompt(messages, sessionId, this.name);
47
- }
48
- applySystemPrompts(systemPrompts, prompt) {
49
- if (systemPrompts.length === 0) return prompt;
50
- return `${systemPrompts.join("\n\n")}
51
-
52
- ${prompt}`;
53
- }
54
- makePermissionHandler(input) {
55
- if (this.harness.onPermissionRequest)
56
- return this.harness.onPermissionRequest;
57
- if (this.harness.permissions === "interactive") {
58
- return (request) => {
59
- const result = resolveInteractivePermission(
60
- request,
61
- input.mode,
62
- input.bridgedToolNames,
63
- input.approvals,
64
- this.name
65
- );
66
- if (result.approvalId !== void 0) {
67
- input.approvalRequests.push(
68
- buildApprovalRequestedEvent({
69
- approvalId: result.approvalId,
70
- title: result.title ?? request.toolCall.title ?? request.toolCall.toolCallId,
71
- threadId: input.threadId,
72
- runId: input.runId,
73
- detail: { provider: this.name }
74
- })
75
- );
76
- }
77
- return result.outcome;
78
- };
79
- }
80
- return (request) => resolvePermission(request, input.mode, input.bridgedToolNames);
81
- }
82
- async *chatStream(options) {
83
- const { logger } = options;
84
- let handle;
85
- let bridge;
86
- let transport;
87
- const externalSignal = options.abortController?.signal ?? options.request?.signal ?? void 0;
88
- let onAbort;
89
- try {
90
- const sandbox = this.sandboxFrom(options);
91
- const modelOptions = options.modelOptions;
92
- const cwd = modelOptions?.cwd ?? this.harness.cwd ?? DEFAULT_WORKDIR;
93
- const harnessCwd = resolveHarnessCwd(sandbox, cwd);
94
- const runId = options.runId ?? this.generateId();
95
- const threadId = options.threadId ?? this.generateId();
96
- const channel = createBridgeEventChannel({
97
- model: this.model,
98
- threadId,
99
- runId
100
- });
101
- const sessionId = modelOptions?.sessionId;
102
- const { prompt: resumePrompt } = this.buildPrompt(
103
- options.messages,
104
- sessionId
105
- );
106
- const bridgedToolNames = new Set(
107
- (options.tools ?? []).map((tool) => tool.name)
108
- );
109
- if (options.tools && options.tools.length > 0) {
110
- const provisioner = (options.capabilities ? getToolBridgeProvisioner(options.capabilities, { optional: true }) : void 0) ?? nodeHttpBridgeProvisioner;
111
- bridge = await provisioner.provision(options.tools, {
112
- provider: sandbox.provider,
113
- context: options.context,
114
- emitCustomEvent: channel.emitCustomEvent,
115
- ...externalSignal ? { signal: externalSignal } : {}
116
- });
117
- }
118
- let workspaceServers = [];
119
- const projection = options.capabilities ? getWorkspaceProjection(options.capabilities, { optional: true }) : void 0;
120
- if (projection !== void 0) {
121
- await projectAcpWorkspace(sandbox, projection, {
122
- ...this.harness.skillsDir !== void 0 && {
123
- skillsDir: this.harness.skillsDir
124
- },
125
- harnessName: this.name
126
- });
127
- workspaceServers = workspaceMcpServers(projection);
128
- }
129
- const ctx = {
130
- sandbox,
131
- model: this.model,
132
- cwd,
133
- harnessCwd,
134
- env: this.harness.env,
135
- modelOptions,
136
- signal: externalSignal
137
- };
138
- transport = this.harness.openTransport ? await this.harness.openTransport(ctx) : await this.openStdioTransport(ctx);
139
- const mode = modelOptions?.permissionMode ?? this.harness.permissionMode ?? "bypassPermissions";
140
- const authMethodId = modelOptions?.authMethodId ?? this.harness.authMethodId;
141
- const approvalRequests = [];
142
- const permissionHandler = this.makePermissionHandler({
143
- mode,
144
- bridgedToolNames,
145
- approvals: options.approvals,
146
- approvalRequests,
147
- threadId,
148
- runId
149
- });
150
- const queue = new AsyncQueue();
151
- logger.request(
152
- `activity=chat provider=${this.name} model=${this.model} sandbox=${sandbox.provider} messages=${options.messages.length} resume=${sessionId ?? "none"}`,
153
- { provider: this.name, model: this.model }
154
- );
155
- const mcpServers = [
156
- ...bridge !== void 0 ? [
157
- {
158
- name: bridge.name,
159
- url: bridge.url,
160
- headers: [
161
- { name: "Authorization", value: `Bearer ${bridge.token}` }
162
- ]
163
- }
164
- ] : [],
165
- ...workspaceServers
166
- ];
167
- const onAcpUpdate = (update) => queue.push({ kind: "update", update });
168
- handle = await startAcpSession({
169
- transport,
170
- cwd: harnessCwd,
171
- ...authMethodId !== void 0 && { authMethodId },
172
- ...sessionId !== void 0 && { resumeSessionId: sessionId },
173
- ...mcpServers.length > 0 && { mcpServers },
174
- onUpdate: onAcpUpdate,
175
- ...this.harness.onExtNotification && {
176
- onExtNotification: this.harness.onExtNotification
177
- },
178
- onPermissionRequest: permissionHandler
179
- });
180
- const session = handle;
181
- if (externalSignal !== void 0) {
182
- onAbort = () => void session.cancel().catch(() => void 0);
183
- if (externalSignal.aborted) onAbort();
184
- else externalSignal.addEventListener("abort", onAbort, { once: true });
185
- }
186
- queue.push({ kind: "session", sessionId: session.sessionId });
187
- const systemPrompts = normalizeSystemPrompts(options.systemPrompts).map((p) => p.content).filter((c) => c.trim() !== "");
188
- const promptText = this.applySystemPrompts(
189
- systemPrompts,
190
- session.resumed || sessionId === void 0 ? resumePrompt : this.buildPrompt(options.messages, void 0).prompt
191
- );
192
- session.prompt(promptText).then(({ stopReason, usage }) => {
193
- queue.push({
194
- kind: "done",
195
- stopReason,
196
- ...usage !== void 0 && { usage }
197
- });
198
- queue.end();
199
- }).catch((error) => queue.fail(error));
200
- yield* mergeChunkStreams(
201
- translateAcpStream(queue, {
202
- model: this.model,
203
- runId,
204
- threadId,
205
- ...options.parentRunId !== void 0 && {
206
- parentRunId: options.parentRunId
207
- },
208
- genId: () => this.generateId(),
209
- bridgedToolNames,
210
- labels: {
211
- sessionIdEvent: `${this.name}.session-id`,
212
- // Surface non-text agent content (image/audio/resource) instead of
213
- // dropping it — emitted as a CUSTOM `<name>.message-content` event.
214
- contentEvent: `${this.name}.message-content`,
215
- ...this.harness.planEventName !== void 0 && {
216
- planEvent: this.harness.planEventName
217
- },
218
- ...this.harness.refusalMessage !== void 0 && {
219
- refusalMessage: this.harness.refusalMessage
220
- }
221
- },
222
- onAcpEvent: (event) => logger.provider(`provider=${this.name} kind=${event.kind}`, {
223
- chunk: event
224
- })
225
- }),
226
- channel.stream
227
- );
228
- for (const event of approvalRequests) yield event;
229
- if (this.harness.emitDiff) {
230
- yield* this.emitDiffChunks(sandbox, cwd, threadId, runId);
231
- }
232
- } catch (error) {
233
- const err = error;
234
- const rawEvent = toRunErrorRawEvent(error);
235
- logger.errors(`${this.name}.chatStream fatal`, {
236
- error,
237
- source: `${this.name}.chatStream`
238
- });
239
- yield {
240
- type: EventType.RUN_ERROR,
241
- model: options.model,
242
- timestamp: Date.now(),
243
- message: err.message || "Unknown error occurred",
244
- ...err.code !== void 0 && { code: err.code },
245
- ...rawEvent !== void 0 && { rawEvent },
246
- error: {
247
- message: err.message || "Unknown error occurred",
248
- ...err.code !== void 0 && { code: err.code }
249
- }
250
- };
251
- } finally {
252
- if (externalSignal !== void 0 && onAbort !== void 0) {
253
- externalSignal.removeEventListener("abort", onAbort);
254
- }
255
- if (handle !== void 0) await handle.dispose();
256
- else if (transport !== void 0) await disposeTransport(transport);
257
- await bridge?.close();
258
- }
259
- }
260
- async openStdioTransport(ctx) {
261
- const build = this.harness.command;
262
- if (build === void 0) {
263
- throw new Error(
264
- `acpCompatible("${this.name}") has no "command" to launch over stdio.`
265
- );
266
- }
267
- const command = build(ctx);
268
- const proc = await ctx.sandbox.process.spawn(command, {
269
- cwd: ctx.cwd,
270
- ...this.harness.env ? { env: this.harness.env } : {},
271
- ...ctx.signal ? { signal: ctx.signal } : {}
272
- });
273
- return { kind: "stdio", process: proc };
274
- }
275
- async *emitDiffChunks(sandbox, cwd, threadId, runId) {
276
- try {
277
- const diff = await sandbox.process.exec(`git -C ${q(cwd)} diff`, { cwd });
278
- if (diff.exitCode === 0 && diff.stdout.trim() !== "") {
279
- yield {
280
- type: EventType.CUSTOM,
281
- name: "file.changed",
282
- value: { path: ".", diff: diff.stdout },
283
- timestamp: Date.now(),
284
- threadId,
285
- runId
286
- };
287
- }
288
- } catch {
289
- }
290
- }
291
- structuredOutput(_options) {
292
- return Promise.reject(
293
- new Error(
294
- `Structured output is not supported by the in-sandbox "${this.name}" ACP harness adapter. Use a model adapter for structured output, or omit outputSchema.`
295
- )
296
- );
297
- }
17
+ if (transport.kind === "stdio") {
18
+ await transport.process.kill();
19
+ return;
20
+ }
21
+ await transport.dispose();
298
22
  }
23
+ /**
24
+ * A generic ACP harness adapter built from {@link AcpCompatibleConfig}. Runs the
25
+ * configured coding-agent CLI inside the sandbox provided by `withSandbox(...)`
26
+ * and translates its ACP session into AG-UI `StreamChunk`s.
27
+ */
28
+ var AcpCompatibleTextAdapter = class extends BaseTextAdapter {
29
+ name;
30
+ requires = [SandboxCapability];
31
+ harness;
32
+ constructor(config, model) {
33
+ super({}, model);
34
+ if (config.command === void 0 && config.openTransport === void 0) throw new Error(`acpCompatible("${config.name}") needs either a "command" or an "openTransport".`);
35
+ this.harness = config;
36
+ this.name = config.name;
37
+ }
38
+ sandboxFrom(options) {
39
+ const ctx = options.capabilities;
40
+ if (!ctx) throw new Error(`Adapter "${this.name}" requires a sandbox. Add withSandbox(defineSandbox({ ... })) to chat() middleware.`);
41
+ return getSandbox(ctx);
42
+ }
43
+ buildPrompt(messages, sessionId) {
44
+ return this.harness.buildPrompt ? this.harness.buildPrompt(messages, sessionId) : buildAcpPrompt(messages, sessionId, this.name);
45
+ }
46
+ applySystemPrompts(systemPrompts, prompt) {
47
+ if (systemPrompts.length === 0) return prompt;
48
+ return `${systemPrompts.join("\n\n")}\n\n${prompt}`;
49
+ }
50
+ makePermissionHandler(input) {
51
+ if (this.harness.onPermissionRequest) return this.harness.onPermissionRequest;
52
+ if (this.harness.permissions === "interactive") return (request) => {
53
+ const result = resolveInteractivePermission(request, input.mode, input.bridgedToolNames, input.approvals, this.name);
54
+ if (result.approvalId !== void 0) input.approvalRequests.push(buildApprovalRequestedEvent({
55
+ approvalId: result.approvalId,
56
+ title: result.title ?? request.toolCall.title ?? request.toolCall.toolCallId,
57
+ threadId: input.threadId,
58
+ runId: input.runId,
59
+ detail: { provider: this.name }
60
+ }));
61
+ return result.outcome;
62
+ };
63
+ return (request) => resolvePermission(request, input.mode, input.bridgedToolNames);
64
+ }
65
+ async *chatStream(options) {
66
+ const { logger } = options;
67
+ let handle;
68
+ let bridge;
69
+ let transport;
70
+ const externalSignal = options.abortController?.signal ?? options.request?.signal ?? void 0;
71
+ let onAbort;
72
+ try {
73
+ const sandbox = this.sandboxFrom(options);
74
+ const modelOptions = options.modelOptions;
75
+ const cwd = modelOptions?.cwd ?? this.harness.cwd ?? DEFAULT_WORKDIR;
76
+ const harnessCwd = resolveHarnessCwd(sandbox, cwd);
77
+ const runId = resolveDurableRunId(options.runId, {
78
+ durable: false,
79
+ adapter: "acp",
80
+ fallback: () => this.generateId()
81
+ });
82
+ const threadId = options.threadId ?? this.generateId();
83
+ const durability = options.capabilities ? getSandboxDurability(options.capabilities, { optional: true }) : void 0;
84
+ if (durability !== void 0) {
85
+ if (durability.attach) throw new DurableAttachNotSupportedError("acp", "this adapter drives the harness over a bidirectional ACP connection and does not journal");
86
+ logger.warn("acp: sandbox durability is wired but this adapter never journals — this run will not be recoverable on reconnect. Use a journaling harness adapter for runs that must survive a host restart, or drop durability if these runs are not meant to.", {
87
+ runId,
88
+ adapter: "acp"
89
+ });
90
+ }
91
+ const channel = createBridgeEventChannel({
92
+ model: this.model,
93
+ threadId,
94
+ runId
95
+ });
96
+ const sessionId = modelOptions?.sessionId;
97
+ const { prompt: resumePrompt } = this.buildPrompt(options.messages, sessionId);
98
+ const bridgedToolNames = new Set((options.tools ?? []).map((tool) => tool.name));
99
+ if (options.tools && options.tools.length > 0) bridge = await ((options.capabilities ? getToolBridgeProvisioner(options.capabilities, { optional: true }) : void 0) ?? nodeHttpBridgeProvisioner).provision(options.tools, {
100
+ provider: sandbox.provider,
101
+ context: options.context,
102
+ emitCustomEvent: channel.emitCustomEvent,
103
+ ...externalSignal ? { signal: externalSignal } : {}
104
+ });
105
+ let workspaceServers = [];
106
+ const projection = options.capabilities ? getWorkspaceProjection(options.capabilities, { optional: true }) : void 0;
107
+ if (projection !== void 0) {
108
+ await projectAcpWorkspace(sandbox, projection, {
109
+ ...this.harness.skillsDir !== void 0 && { skillsDir: this.harness.skillsDir },
110
+ harnessName: this.name
111
+ });
112
+ workspaceServers = workspaceMcpServers(projection);
113
+ }
114
+ const ctx = {
115
+ sandbox,
116
+ model: this.model,
117
+ cwd,
118
+ harnessCwd,
119
+ env: this.harness.env,
120
+ modelOptions,
121
+ signal: externalSignal
122
+ };
123
+ transport = this.harness.openTransport ? await this.harness.openTransport(ctx) : await this.openStdioTransport(ctx);
124
+ const mode = modelOptions?.permissionMode ?? this.harness.permissionMode ?? "bypassPermissions";
125
+ const authMethodId = modelOptions?.authMethodId ?? this.harness.authMethodId;
126
+ const approvalRequests = [];
127
+ const permissionHandler = this.makePermissionHandler({
128
+ mode,
129
+ bridgedToolNames,
130
+ approvals: options.approvals,
131
+ approvalRequests,
132
+ threadId,
133
+ runId
134
+ });
135
+ const queue = new AsyncQueue();
136
+ logger.request(`activity=chat provider=${this.name} model=${this.model} sandbox=${sandbox.provider} messages=${options.messages.length} resume=${sessionId ?? "none"}`, {
137
+ provider: this.name,
138
+ model: this.model
139
+ });
140
+ const mcpServers = [...bridge !== void 0 ? [{
141
+ name: bridge.name,
142
+ url: bridge.url,
143
+ headers: [{
144
+ name: "Authorization",
145
+ value: `Bearer ${bridge.token}`
146
+ }]
147
+ }] : [], ...workspaceServers];
148
+ const onAcpUpdate = (update) => queue.push({
149
+ kind: "update",
150
+ update
151
+ });
152
+ handle = await startAcpSession({
153
+ transport,
154
+ cwd: harnessCwd,
155
+ ...authMethodId !== void 0 && { authMethodId },
156
+ ...sessionId !== void 0 && { resumeSessionId: sessionId },
157
+ ...mcpServers.length > 0 && { mcpServers },
158
+ onUpdate: onAcpUpdate,
159
+ ...this.harness.onExtNotification && { onExtNotification: this.harness.onExtNotification },
160
+ onPermissionRequest: permissionHandler
161
+ });
162
+ const session = handle;
163
+ if (externalSignal !== void 0) {
164
+ onAbort = () => void session.cancel().catch(() => void 0);
165
+ if (externalSignal.aborted) onAbort();
166
+ else externalSignal.addEventListener("abort", onAbort, { once: true });
167
+ }
168
+ queue.push({
169
+ kind: "session",
170
+ sessionId: session.sessionId
171
+ });
172
+ const systemPrompts = normalizeSystemPrompts(options.systemPrompts).map((p) => p.content).filter((c) => c.trim() !== "");
173
+ const promptText = this.applySystemPrompts(systemPrompts, session.resumed || sessionId === void 0 ? resumePrompt : this.buildPrompt(options.messages, void 0).prompt);
174
+ session.prompt(promptText).then(({ stopReason, usage }) => {
175
+ queue.push({
176
+ kind: "done",
177
+ stopReason,
178
+ ...usage !== void 0 && { usage }
179
+ });
180
+ queue.end();
181
+ }).catch((error) => queue.fail(error));
182
+ yield* mergeChunkStreams(translateAcpStream(queue, {
183
+ model: this.model,
184
+ runId,
185
+ threadId,
186
+ ...options.parentRunId !== void 0 && { parentRunId: options.parentRunId },
187
+ genId: () => this.generateId(),
188
+ bridgedToolNames,
189
+ labels: {
190
+ sessionIdEvent: `${this.name}.session-id`,
191
+ contentEvent: `${this.name}.message-content`,
192
+ ...this.harness.planEventName !== void 0 && { planEvent: this.harness.planEventName },
193
+ ...this.harness.refusalMessage !== void 0 && { refusalMessage: this.harness.refusalMessage }
194
+ },
195
+ onAcpEvent: (event) => logger.provider(`provider=${this.name} kind=${event.kind}`, { chunk: event })
196
+ }), channel.stream);
197
+ for (const event of approvalRequests) yield event;
198
+ if (this.harness.emitDiff) yield* this.emitDiffChunks(sandbox, cwd, threadId, runId);
199
+ } catch (error) {
200
+ const err = error;
201
+ const rawEvent = toRunErrorRawEvent(error);
202
+ logger.errors(`${this.name}.chatStream fatal`, {
203
+ error,
204
+ source: `${this.name}.chatStream`
205
+ });
206
+ yield {
207
+ type: EventType.RUN_ERROR,
208
+ model: options.model,
209
+ timestamp: Date.now(),
210
+ message: err.message || "Unknown error occurred",
211
+ ...err.code !== void 0 && { code: err.code },
212
+ ...rawEvent !== void 0 && { rawEvent },
213
+ error: {
214
+ message: err.message || "Unknown error occurred",
215
+ ...err.code !== void 0 && { code: err.code }
216
+ }
217
+ };
218
+ } finally {
219
+ if (externalSignal !== void 0 && onAbort !== void 0) externalSignal.removeEventListener("abort", onAbort);
220
+ if (handle !== void 0) await handle.dispose();
221
+ else if (transport !== void 0) await disposeTransport(transport);
222
+ await bridge?.close();
223
+ }
224
+ }
225
+ async openStdioTransport(ctx) {
226
+ const build = this.harness.command;
227
+ if (build === void 0) throw new Error(`acpCompatible("${this.name}") has no "command" to launch over stdio.`);
228
+ const command = build(ctx);
229
+ return {
230
+ kind: "stdio",
231
+ process: await ctx.sandbox.process.spawn(command, {
232
+ cwd: ctx.cwd,
233
+ ...this.harness.env ? { env: this.harness.env } : {},
234
+ ...ctx.signal ? { signal: ctx.signal } : {}
235
+ })
236
+ };
237
+ }
238
+ async *emitDiffChunks(sandbox, cwd, threadId, runId) {
239
+ try {
240
+ const diff = await sandbox.process.exec(`git -C ${q(cwd)} diff`, { cwd });
241
+ if (diff.exitCode === 0 && diff.stdout.trim() !== "") yield {
242
+ type: EventType.CUSTOM,
243
+ name: "file.changed",
244
+ value: {
245
+ path: ".",
246
+ diff: diff.stdout
247
+ },
248
+ timestamp: Date.now(),
249
+ threadId,
250
+ runId
251
+ };
252
+ } catch {}
253
+ }
254
+ structuredOutput(_options) {
255
+ return Promise.reject(/* @__PURE__ */ new Error(`Structured output is not supported by the in-sandbox "${this.name}" ACP harness adapter. Use a model adapter for structured output, or omit outputSchema.`));
256
+ }
257
+ };
258
+ /**
259
+ * Configure an ACP-compatible harness once, then select a model per call.
260
+ *
261
+ * Mirrors `openaiCompatible`: it lets you plug ANY Agent Client Protocol agent
262
+ * into a TanStack AI sandbox without a dedicated adapter package.
263
+ *
264
+ * @example
265
+ * ```ts
266
+ * import { acpCompatible } from '@tanstack/ai-acp'
267
+ * import { chat } from '@tanstack/ai'
268
+ * import { defineSandbox, withSandbox } from '@tanstack/ai-sandbox'
269
+ *
270
+ * const pi = acpCompatible({
271
+ * name: 'pi',
272
+ * // declaring `models` makes pi('…') type-safe; omit to accept any string
273
+ * models: ['pi-fast', 'pi-pro'],
274
+ * // declare per-call options; merged with the base ACP options and exposed
275
+ * // on ctx.modelOptions inside `command` / `openTransport`
276
+ * modelOptions: {} as { reasoningEffort?: 'low' | 'high' },
277
+ * command: ({ model, harnessCwd, modelOptions }) =>
278
+ * `pi --acp -m ${model} --cwd ${harnessCwd}` +
279
+ * (modelOptions?.reasoningEffort ? ` --effort ${modelOptions.reasoningEffort}` : ''),
280
+ * authMethodId: 'pi-api-key',
281
+ * })
282
+ *
283
+ * chat({
284
+ * adapter: pi('pi-pro'),
285
+ * modelOptions: { reasoningEffort: 'high' }, // typed
286
+ * messages,
287
+ * middleware: [withSandbox(defineSandbox({ /* provider, install pi *\/ }))],
288
+ * })
289
+ * ```
290
+ */
299
291
  function acpCompatible(config) {
300
- return (model, overrides) => new AcpCompatibleTextAdapter(
301
- overrides ? { ...config, ...overrides } : config,
302
- model
303
- );
292
+ return (model, overrides) => new AcpCompatibleTextAdapter(overrides ? {
293
+ ...config,
294
+ ...overrides
295
+ } : config, model);
304
296
  }
297
+ /**
298
+ * One-shot helper: build a single-model ACP-compatible harness adapter inline.
299
+ *
300
+ * @example
301
+ * ```ts
302
+ * chat({
303
+ * adapter: acpCompatibleText('pi-fast', {
304
+ * name: 'pi',
305
+ * command: ({ model }) => `pi --acp -m ${model}`,
306
+ * }),
307
+ * messages,
308
+ * middleware: [withSandbox(defineSandbox({ ... }))],
309
+ * })
310
+ * ```
311
+ */
305
312
  function acpCompatibleText(model, config) {
306
- return new AcpCompatibleTextAdapter(config, model);
313
+ return new AcpCompatibleTextAdapter(config, model);
307
314
  }
308
- export {
309
- AcpCompatibleTextAdapter,
310
- acpCompatible,
311
- acpCompatibleText
312
- };
313
- //# sourceMappingURL=compatible.js.map
315
+ //#endregion
316
+ export { AcpCompatibleTextAdapter, acpCompatible, acpCompatibleText };
317
+
318
+ //# sourceMappingURL=compatible.js.map