@tanstack/ai-acp 0.2.3 → 0.2.5
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/dist/esm/adapters/compatible.js +305 -300
- package/dist/esm/adapters/compatible.js.map +1 -1
- package/dist/esm/adapters/projection.js +87 -69
- package/dist/esm/adapters/projection.js.map +1 -1
- package/dist/esm/index.js +2 -25
- package/dist/esm/messages/prompt.js +30 -33
- package/dist/esm/messages/prompt.js.map +1 -1
- package/dist/esm/permissions.js +44 -37
- package/dist/esm/permissions.js.map +1 -1
- package/dist/esm/session/acp-client.js +118 -134
- package/dist/esm/session/acp-client.js.map +1 -1
- package/dist/esm/session/sandbox-server.js +93 -105
- package/dist/esm/session/sandbox-server.js.map +1 -1
- package/dist/esm/stream/queue.js +57 -53
- package/dist/esm/stream/queue.js.map +1 -1
- package/dist/esm/stream/translate.js +287 -309
- package/dist/esm/stream/translate.js.map +1 -1
- package/dist/esm/transport/resolve.js +21 -32
- package/dist/esm/transport/resolve.js.map +1 -1
- package/dist/esm/transport/stdio.js +40 -49
- package/dist/esm/transport/stdio.js.map +1 -1
- package/dist/esm/transport/websocket.js +121 -120
- package/dist/esm/transport/websocket.js.map +1 -1
- package/package.json +8 -8
- package/src/adapters/compatible.ts +52 -1
- package/src/transport/websocket.ts +4 -1
- package/dist/esm/index.js.map +0 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
14
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
14
15
|
}
|
|
15
16
|
async function disposeTransport(transport) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
313
|
+
return new AcpCompatibleTextAdapter(config, model);
|
|
307
314
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
};
|
|
313
|
-
//# sourceMappingURL=compatible.js.map
|
|
315
|
+
//#endregion
|
|
316
|
+
export { AcpCompatibleTextAdapter, acpCompatible, acpCompatibleText };
|
|
317
|
+
|
|
318
|
+
//# sourceMappingURL=compatible.js.map
|