@tanstack/ai-acp 0.2.5 → 0.3.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/dist/esm/adapters/compatible.d.ts +17 -2
- package/dist/esm/adapters/compatible.js +65 -6
- package/dist/esm/adapters/compatible.js.map +1 -1
- package/dist/esm/adapters/projection.js +15 -14
- package/dist/esm/adapters/projection.js.map +1 -1
- package/dist/esm/stream/translate.js.map +1 -1
- package/dist/esm/transport/websocket.js +3 -2
- package/dist/esm/transport/websocket.js.map +1 -1
- package/package.json +8 -8
- package/src/adapters/compatible.ts +119 -9
- package/src/adapters/projection.ts +23 -15
|
@@ -79,10 +79,16 @@ export interface AcpCompatibleConfig<TModels extends ReadonlyArray<string> = Rea
|
|
|
79
79
|
skillsDir?: string;
|
|
80
80
|
/** Extra environment variables for the harness process. */
|
|
81
81
|
env?: Record<string, string>;
|
|
82
|
+
/**
|
|
83
|
+
* `'api-key'` (default) uses {@link authMethodId} (or `modelOptions.authMethodId`).
|
|
84
|
+
* `'host'` skips ACP authenticate (use the CLI login on the machine).
|
|
85
|
+
* Not inferred from the sandbox.
|
|
86
|
+
*/
|
|
87
|
+
authMode?: 'host' | 'api-key';
|
|
82
88
|
/**
|
|
83
89
|
* ACP auth method to select before the session starts, when the harness
|
|
84
90
|
* advertises one (e.g. `'pi-api-key'`). Overridable per call via
|
|
85
|
-
* `modelOptions.authMethodId`.
|
|
91
|
+
* `modelOptions.authMethodId`. Ignored when {@link authMode} is `'host'`.
|
|
86
92
|
*/
|
|
87
93
|
authMethodId?: string;
|
|
88
94
|
/** ACP permission policy. Defaults to `'bypassPermissions'`. */
|
|
@@ -127,7 +133,13 @@ export interface AcpCompatibleProviderOptions {
|
|
|
127
133
|
sessionId?: string;
|
|
128
134
|
/** Per-call override of the harness working directory. */
|
|
129
135
|
cwd?: string;
|
|
130
|
-
/**
|
|
136
|
+
/**
|
|
137
|
+
* `'api-key'` (default) uses {@link authMethodId}.
|
|
138
|
+
* `'host'` skips ACP authenticate.
|
|
139
|
+
* Not inferred from the sandbox.
|
|
140
|
+
*/
|
|
141
|
+
authMode?: 'host' | 'api-key';
|
|
142
|
+
/** Per-call override of the ACP auth method. Ignored when authMode is `'host'`. */
|
|
131
143
|
authMethodId?: string;
|
|
132
144
|
/** Per-call override of the ACP permission policy. */
|
|
133
145
|
permissionMode?: AcpPermissionMode;
|
|
@@ -144,6 +156,8 @@ export declare class AcpCompatibleTextAdapter<TModel extends string, TModelOptio
|
|
|
144
156
|
readonly requires: readonly [import('@tanstack/ai').Capability<SandboxHandle, "sandbox">];
|
|
145
157
|
private readonly harness;
|
|
146
158
|
constructor(config: AcpCompatibleConfig<ReadonlyArray<string>, TModelOptions>, model: TModel);
|
|
159
|
+
supportsCombinedToolsAndSchema(): boolean;
|
|
160
|
+
combinedStructuredOutputSource(): 'event';
|
|
147
161
|
private sandboxFrom;
|
|
148
162
|
private buildPrompt;
|
|
149
163
|
private applySystemPrompts;
|
|
@@ -151,6 +165,7 @@ export declare class AcpCompatibleTextAdapter<TModel extends string, TModelOptio
|
|
|
151
165
|
chatStream(options: TextOptions<ResolvedOptions<TModelOptions>>): AsyncIterable<StreamChunk>;
|
|
152
166
|
private openStdioTransport;
|
|
153
167
|
private emitDiffChunks;
|
|
168
|
+
private emitParsedStructuredOutput;
|
|
154
169
|
structuredOutput(_options: StructuredOutputOptions<ResolvedOptions<TModelOptions>>): Promise<StructuredOutputResult<unknown>>;
|
|
155
170
|
}
|
|
156
171
|
/**
|
|
@@ -6,7 +6,7 @@ import { buildAcpPrompt } from "../messages/prompt.js";
|
|
|
6
6
|
import { projectAcpWorkspace, workspaceMcpServers } from "./projection.js";
|
|
7
7
|
import { EventType, normalizeSystemPrompts } from "@tanstack/ai";
|
|
8
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";
|
|
9
|
+
import { appendOutputSchemaInstruction, parseJsonFromAssistantText, structuredOutputCompleteChunk, structuredOutputStartChunk, toRunErrorRawEvent } from "@tanstack/ai/adapter-internals";
|
|
10
10
|
import { BaseTextAdapter } from "@tanstack/ai/adapters";
|
|
11
11
|
//#region src/adapters/compatible.ts
|
|
12
12
|
var DEFAULT_WORKDIR = "/workspace";
|
|
@@ -35,6 +35,12 @@ var AcpCompatibleTextAdapter = class extends BaseTextAdapter {
|
|
|
35
35
|
this.harness = config;
|
|
36
36
|
this.name = config.name;
|
|
37
37
|
}
|
|
38
|
+
supportsCombinedToolsAndSchema() {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
combinedStructuredOutputSource() {
|
|
42
|
+
return "event";
|
|
43
|
+
}
|
|
38
44
|
sandboxFrom(options) {
|
|
39
45
|
const ctx = options.capabilities;
|
|
40
46
|
if (!ctx) throw new Error(`Adapter "${this.name}" requires a sandbox. Add withSandbox(defineSandbox({ ... })) to chat() middleware.`);
|
|
@@ -122,7 +128,7 @@ var AcpCompatibleTextAdapter = class extends BaseTextAdapter {
|
|
|
122
128
|
};
|
|
123
129
|
transport = this.harness.openTransport ? await this.harness.openTransport(ctx) : await this.openStdioTransport(ctx);
|
|
124
130
|
const mode = modelOptions?.permissionMode ?? this.harness.permissionMode ?? "bypassPermissions";
|
|
125
|
-
const authMethodId = modelOptions?.authMethodId ?? this.harness.authMethodId;
|
|
131
|
+
const authMethodId = (modelOptions?.authMode ?? this.harness.authMode ?? "api-key") === "host" ? void 0 : modelOptions?.authMethodId ?? this.harness.authMethodId;
|
|
126
132
|
const approvalRequests = [];
|
|
127
133
|
const permissionHandler = this.makePermissionHandler({
|
|
128
134
|
mode,
|
|
@@ -170,7 +176,8 @@ var AcpCompatibleTextAdapter = class extends BaseTextAdapter {
|
|
|
170
176
|
sessionId: session.sessionId
|
|
171
177
|
});
|
|
172
178
|
const systemPrompts = normalizeSystemPrompts(options.systemPrompts).map((p) => p.content).filter((c) => c.trim() !== "");
|
|
173
|
-
|
|
179
|
+
let promptText = this.applySystemPrompts(systemPrompts, session.resumed || sessionId === void 0 ? resumePrompt : this.buildPrompt(options.messages, void 0).prompt);
|
|
180
|
+
if (options.outputSchema) promptText = appendOutputSchemaInstruction(promptText, options.outputSchema);
|
|
174
181
|
session.prompt(promptText).then(({ stopReason, usage }) => {
|
|
175
182
|
queue.push({
|
|
176
183
|
kind: "done",
|
|
@@ -179,7 +186,11 @@ var AcpCompatibleTextAdapter = class extends BaseTextAdapter {
|
|
|
179
186
|
});
|
|
180
187
|
queue.end();
|
|
181
188
|
}).catch((error) => queue.fail(error));
|
|
182
|
-
|
|
189
|
+
const wantsStructured = options.outputSchema !== void 0;
|
|
190
|
+
let lastAssistantText = "";
|
|
191
|
+
let lastTextMessageId;
|
|
192
|
+
let heldFinished;
|
|
193
|
+
for await (const chunk of mergeChunkStreams(translateAcpStream(queue, {
|
|
183
194
|
model: this.model,
|
|
184
195
|
runId,
|
|
185
196
|
threadId,
|
|
@@ -193,7 +204,21 @@ var AcpCompatibleTextAdapter = class extends BaseTextAdapter {
|
|
|
193
204
|
...this.harness.refusalMessage !== void 0 && { refusalMessage: this.harness.refusalMessage }
|
|
194
205
|
},
|
|
195
206
|
onAcpEvent: (event) => logger.provider(`provider=${this.name} kind=${event.kind}`, { chunk: event })
|
|
196
|
-
}), channel.stream)
|
|
207
|
+
}), channel.stream)) {
|
|
208
|
+
if (wantsStructured && chunk.type === EventType.RUN_FINISHED) {
|
|
209
|
+
heldFinished = chunk;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (wantsStructured) {
|
|
213
|
+
if (chunk.type === EventType.TEXT_MESSAGE_START) {
|
|
214
|
+
lastAssistantText = "";
|
|
215
|
+
if (typeof chunk.messageId === "string" && chunk.messageId !== "") lastTextMessageId = chunk.messageId;
|
|
216
|
+
} else if (chunk.type === EventType.TEXT_MESSAGE_CONTENT && typeof chunk.delta === "string") lastAssistantText += chunk.delta;
|
|
217
|
+
}
|
|
218
|
+
yield chunk;
|
|
219
|
+
}
|
|
220
|
+
if (options.outputSchema) yield* this.emitParsedStructuredOutput(lastAssistantText, threadId, runId, lastTextMessageId);
|
|
221
|
+
if (heldFinished) yield heldFinished;
|
|
197
222
|
for (const event of approvalRequests) yield event;
|
|
198
223
|
if (this.harness.emitDiff) yield* this.emitDiffChunks(sandbox, cwd, threadId, runId);
|
|
199
224
|
} catch (error) {
|
|
@@ -251,8 +276,42 @@ var AcpCompatibleTextAdapter = class extends BaseTextAdapter {
|
|
|
251
276
|
};
|
|
252
277
|
} catch {}
|
|
253
278
|
}
|
|
279
|
+
*emitParsedStructuredOutput(raw, threadId, runId, messageId = this.generateId()) {
|
|
280
|
+
try {
|
|
281
|
+
const object = parseJsonFromAssistantText(raw);
|
|
282
|
+
yield structuredOutputStartChunk({
|
|
283
|
+
messageId,
|
|
284
|
+
model: this.model,
|
|
285
|
+
threadId,
|
|
286
|
+
runId
|
|
287
|
+
});
|
|
288
|
+
yield structuredOutputCompleteChunk({
|
|
289
|
+
messageId,
|
|
290
|
+
model: this.model,
|
|
291
|
+
threadId,
|
|
292
|
+
runId,
|
|
293
|
+
object,
|
|
294
|
+
raw
|
|
295
|
+
});
|
|
296
|
+
} catch (error) {
|
|
297
|
+
const parserMessage = error instanceof Error ? error.message : "Failed to parse structured output";
|
|
298
|
+
const preview = raw.trim().slice(0, 200);
|
|
299
|
+
const message = preview === "" ? parserMessage : `${parserMessage} Content: ${preview}`;
|
|
300
|
+
yield {
|
|
301
|
+
type: EventType.RUN_ERROR,
|
|
302
|
+
model: this.model,
|
|
303
|
+
timestamp: Date.now(),
|
|
304
|
+
message,
|
|
305
|
+
code: "structured-output-parse-failed",
|
|
306
|
+
error: {
|
|
307
|
+
message,
|
|
308
|
+
code: "structured-output-parse-failed"
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
254
313
|
structuredOutput(_options) {
|
|
255
|
-
return Promise.reject(/* @__PURE__ */ new Error(
|
|
314
|
+
return Promise.reject(/* @__PURE__ */ new Error("This harness honors outputSchema on chat() in the same turn. Pass outputSchema to chat(), or use a model adapter for a one-shot extract."));
|
|
256
315
|
}
|
|
257
316
|
};
|
|
258
317
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compatible.js","names":[],"sources":["../../../src/adapters/compatible.ts"],"sourcesContent":["import { EventType, normalizeSystemPrompts } from '@tanstack/ai'\nimport { toRunErrorRawEvent } from '@tanstack/ai/adapter-internals'\nimport { BaseTextAdapter } from '@tanstack/ai/adapters'\nimport {\n DurableAttachNotSupportedError,\n SandboxCapability,\n buildApprovalRequestedEvent,\n createBridgeEventChannel,\n getSandbox,\n getSandboxDurability,\n getToolBridgeProvisioner,\n getWorkspaceProjection,\n mergeChunkStreams,\n nodeHttpBridgeProvisioner,\n resolveDurableRunId,\n resolveHarnessCwd,\n} from '@tanstack/ai-sandbox'\nimport { AsyncQueue } from '../stream/queue'\nimport { startAcpSession } from '../session/acp-client'\nimport { translateAcpStream } from '../stream/translate'\nimport { resolveInteractivePermission, resolvePermission } from '../permissions'\nimport { buildAcpPrompt } from '../messages/prompt'\nimport { projectAcpWorkspace, workspaceMcpServers } from './projection'\nimport type { AcpMcpServer } from './projection'\nimport type { HostToolBridge, SandboxHandle } from '@tanstack/ai-sandbox'\nimport type {\n StructuredOutputOptions,\n StructuredOutputResult,\n} from '@tanstack/ai/adapters'\nimport type {\n DefaultMessageMetadataByModality,\n Modality,\n ModelMessage,\n StreamChunk,\n TextOptions,\n} from '@tanstack/ai'\nimport type { AcpSessionHandle } from '../session/acp-client'\nimport type { AcpStreamEvent } from '../stream/translate'\nimport type { AcpSessionTransport } from '../transport/types'\nimport type {\n AcpPermissionMode,\n AcpSessionUpdate,\n PermissionHandler,\n} from '../types/acp-types'\nimport type { BuiltAcpPrompt } from '../messages/prompt'\n\nconst DEFAULT_WORKDIR = '/workspace'\n\n/**\n * Everything a harness needs to know to launch its ACP server inside the\n * sandbox. Passed to {@link AcpCompatibleConfig.command} /\n * {@link AcpCompatibleConfig.openTransport}.\n */\nexport interface AcpHarnessContext<\n TModelOptions extends Record<string, any> = AcpCompatibleProviderOptions,\n> {\n /** The sandbox the harness runs in (from `withSandbox(...)` middleware). */\n sandbox: SandboxHandle\n /** The selected model id. */\n model: string\n /** Virtual cwd for `sandbox.process.spawn` (the provider maps `/workspace`). */\n cwd: string\n /** Literal cwd for the harness's own `--cwd` flag / ACP `newSession`. */\n harnessCwd: string\n /** Extra env vars configured for the harness process. */\n env: Record<string, string> | undefined\n /**\n * Per-call options from `chat({ modelOptions })` — the base ACP options plus\n * whatever you declared via {@link AcpCompatibleConfig.modelOptions}. Read\n * these to turn options into CLI flags / transport choices.\n */\n modelOptions: TModelOptions | undefined\n /** Abort signal for the run, when one was provided. */\n signal: AbortSignal | undefined\n}\n\n/** Union of selectable model names from a `models` tuple (any string if omitted). */\nexport type AcpModelNameOf<TModels extends ReadonlyArray<string>> =\n TModels[number]\n\nexport interface AcpCompatibleConfig<\n TModels extends ReadonlyArray<string> = ReadonlyArray<string>,\n TModelOptions extends Record<string, any> = AcpCompatibleProviderOptions,\n> {\n /**\n * Harness name. Used as the provider label, the log prefix, and the CUSTOM\n * session-id event name (`<name>.session-id`).\n */\n name: string\n /**\n * The models this harness accepts. Declaring them makes the returned factory\n * type-safe — `harness('known-model')` is checked, unknown ids are rejected.\n * Omit to accept any string.\n */\n models?: TModels\n /**\n * Type-only brand for the per-call options accepted via `chat({ modelOptions })`.\n * Declare your harness's options here with `{} as { ... }` (the value is unused\n * at runtime); they are merged with the base {@link AcpCompatibleProviderOptions}\n * and exposed on {@link AcpHarnessContext.modelOptions} so `command` /\n * `openTransport` can turn them into CLI flags.\n *\n * @example modelOptions: {} as { reasoningEffort?: 'low' | 'high' }\n */\n modelOptions?: TModelOptions\n /**\n * Build the shell command that launches the harness's ACP server over\n * **stdio** inside the sandbox (e.g. `` `pi --acp -m ${model}` ``). Required\n * unless {@link openTransport} is provided.\n */\n command?: (\n ctx: AcpHarnessContext<AcpCompatibleProviderOptions & TModelOptions>,\n ) => string\n /**\n * Full transport escape hatch — open any {@link AcpSessionTransport} yourself\n * (e.g. boot a `serve` process and connect over WebSocket, as Grok Build\n * does). Overrides {@link command}. Put ALL teardown in the returned\n * transport's `dispose` (stream) / process (stdio); it is disposed when the\n * session ends.\n */\n openTransport?: (\n ctx: AcpHarnessContext<AcpCompatibleProviderOptions & TModelOptions>,\n ) => Promise<AcpSessionTransport> | AcpSessionTransport\n /** Working directory inside the sandbox. Defaults to `/workspace`. */\n cwd?: string\n /**\n * The harness's skills directory, relative to the workspace root (e.g.\n * `'.pi/skills'`) — its native convention for where it auto-discovers skills,\n * the way Claude Code uses `.claude/skills`. When set, `withSandbox` workspace\n * `gitSkill`s are linked here. MCP skills don't need this: they're passed to\n * the agent over ACP natively. Omit and `gitSkill`s are left unlinked (warned).\n */\n skillsDir?: string\n /** Extra environment variables for the harness process. */\n env?: Record<string, string>\n /**\n * ACP auth method to select before the session starts, when the harness\n * advertises one (e.g. `'pi-api-key'`). Overridable per call via\n * `modelOptions.authMethodId`.\n */\n authMethodId?: string\n /** ACP permission policy. Defaults to `'bypassPermissions'`. */\n permissionMode?: AcpPermissionMode\n /**\n * Permission strategy:\n * - `'headless'` (default) — auto-resolve via {@link permissionMode}; the\n * sandbox is the boundary, so the agent runs without prompting.\n * - `'interactive'` — same policy, but `ask`-style prompts emit an\n * approval-requested event so a client can approve and re-run.\n */\n permissions?: 'headless' | 'interactive'\n /** Custom permission handler; overrides {@link permissions}/{@link permissionMode}. */\n onPermissionRequest?: PermissionHandler\n /** Message used for `RUN_ERROR` when the harness refuses a request. */\n refusalMessage?: string\n /** Emit ACP `plan` updates as a CUSTOM event under this name (off by default). */\n planEventName?: string\n /**\n * After the run, emit the `git diff` of the working dir as a `file.changed`\n * CUSTOM event. Requires a git repo at `cwd`. Off by default.\n */\n emitDiff?: boolean\n /**\n * Harness-specific JSON-RPC notifications (vendor `_x/...` extensions). Must\n * return without throwing — unknown extensions must not tear down the session.\n */\n onExtNotification?: (method: string, params: Record<string, unknown>) => void\n /**\n * Convert chat history into the harness prompt + resume inputs. Defaults to\n * {@link buildAcpPrompt} (trailing user message + flattened transcript).\n */\n buildPrompt?: (\n messages: Array<ModelMessage>,\n sessionId: string | undefined,\n ) => BuiltAcpPrompt\n}\n\n/** Per-call provider options, passed via `modelOptions` on `chat()`. */\nexport interface AcpCompatibleProviderOptions {\n /**\n * Resume an existing harness session. The adapter emits the session id of\n * every run via a CUSTOM `<name>.session-id` event; thread it back here to\n * continue (only the trailing user message is sent).\n */\n sessionId?: string\n /** Per-call override of the harness working directory. */\n cwd?: string\n /** Per-call override of the ACP auth method. */\n authMethodId?: string\n /** Per-call override of the ACP permission policy. */\n permissionMode?: AcpPermissionMode\n}\n\n/** Per-call options the adapter sees: the base ACP options + the harness's own. */\ntype ResolvedOptions<TModelOptions extends Record<string, any>> =\n AcpCompatibleProviderOptions & TModelOptions\n\nfunction q(value: string): string {\n return `'${value.replace(/'/g, `'\\\\''`)}'`\n}\n\nasync function disposeTransport(transport: AcpSessionTransport): Promise<void> {\n if (transport.kind === 'stdio') {\n await transport.process.kill()\n return\n }\n await transport.dispose()\n}\n\n/**\n * A generic ACP harness adapter built from {@link AcpCompatibleConfig}. Runs the\n * configured coding-agent CLI inside the sandbox provided by `withSandbox(...)`\n * and translates its ACP session into AG-UI `StreamChunk`s.\n */\nexport class AcpCompatibleTextAdapter<\n TModel extends string,\n TModelOptions extends Record<string, any> = AcpCompatibleProviderOptions,\n> extends BaseTextAdapter<\n TModel,\n ResolvedOptions<TModelOptions>,\n ReadonlyArray<Modality> & readonly ['text'],\n DefaultMessageMetadataByModality,\n ReadonlyArray<string>,\n unknown,\n never\n> {\n override readonly name: string\n\n override readonly requires = [SandboxCapability] as const\n\n private readonly harness: AcpCompatibleConfig<\n ReadonlyArray<string>,\n TModelOptions\n >\n\n constructor(\n config: AcpCompatibleConfig<ReadonlyArray<string>, TModelOptions>,\n model: TModel,\n ) {\n super({}, model)\n if (config.command === undefined && config.openTransport === undefined) {\n throw new Error(\n `acpCompatible(\"${config.name}\") needs either a \"command\" or an \"openTransport\".`,\n )\n }\n this.harness = config\n this.name = config.name\n }\n\n private sandboxFrom(\n options: TextOptions<ResolvedOptions<TModelOptions>>,\n ): SandboxHandle {\n const ctx = options.capabilities\n if (!ctx) {\n throw new Error(\n `Adapter \"${this.name}\" requires a sandbox. Add withSandbox(defineSandbox({ ... })) to chat() middleware.`,\n )\n }\n return getSandbox(ctx)\n }\n\n private buildPrompt(\n messages: Array<ModelMessage>,\n sessionId: string | undefined,\n ): BuiltAcpPrompt {\n return this.harness.buildPrompt\n ? this.harness.buildPrompt(messages, sessionId)\n : buildAcpPrompt(messages, sessionId, this.name)\n }\n\n private applySystemPrompts(\n systemPrompts: Array<string>,\n prompt: string,\n ): string {\n if (systemPrompts.length === 0) return prompt\n return `${systemPrompts.join('\\n\\n')}\\n\\n${prompt}`\n }\n\n private makePermissionHandler(input: {\n mode: AcpPermissionMode\n bridgedToolNames: ReadonlySet<string>\n approvals: ReadonlyMap<string, boolean> | undefined\n approvalRequests: Array<StreamChunk>\n threadId: string\n runId: string\n }): PermissionHandler {\n if (this.harness.onPermissionRequest)\n return this.harness.onPermissionRequest\n\n if (this.harness.permissions === 'interactive') {\n return (request) => {\n const result = resolveInteractivePermission(\n request,\n input.mode,\n input.bridgedToolNames,\n input.approvals,\n this.name,\n )\n if (result.approvalId !== undefined) {\n input.approvalRequests.push(\n buildApprovalRequestedEvent({\n approvalId: result.approvalId,\n title:\n result.title ??\n request.toolCall.title ??\n request.toolCall.toolCallId,\n threadId: input.threadId,\n runId: input.runId,\n detail: { provider: this.name },\n }),\n )\n }\n return result.outcome\n }\n }\n\n return (request) =>\n resolvePermission(request, input.mode, input.bridgedToolNames)\n }\n\n async *chatStream(\n options: TextOptions<ResolvedOptions<TModelOptions>>,\n ): AsyncIterable<StreamChunk> {\n const { logger } = options\n let handle: AcpSessionHandle | undefined\n let bridge: HostToolBridge | undefined\n let transport: AcpSessionTransport | undefined\n const externalSignal =\n options.abortController?.signal ?? options.request?.signal ?? undefined\n let onAbort: (() => void) | undefined\n\n try {\n const sandbox = this.sandboxFrom(options)\n const modelOptions = options.modelOptions\n const cwd = modelOptions?.cwd ?? this.harness.cwd ?? DEFAULT_WORKDIR\n const harnessCwd = resolveHarnessCwd(sandbox, cwd)\n // This adapter does not journal yet, so a generated id is still fine.\n // Routed through the helper anyway so that whenever it gains journaling\n // it inherits the caller-supplied-runId requirement instead of\n // re-deriving it (see `packages/ai-sandbox/src/durability.ts`).\n const runId = resolveDurableRunId(options.runId, {\n durable: false,\n adapter: 'acp',\n fallback: () => this.generateId(),\n })\n const threadId = options.threadId ?? this.generateId()\n\n // Durability wired onto a path that cannot deliver it. Two outcomes, split\n // by whether a first attempt has already run.\n //\n // ATTACH is fatal. `sandboxRunDriver`'s `drive()` sets `attach: true` only\n // when a previous host was already streaming this run, so continuing past\n // here reaches `startAcpSession` + `session.prompt(...)` and re-runs the\n // agent from scratch against the workspace that attempt already mutated,\n // appending its whole output to a log that still holds the first\n // attempt's. This adapter has no journal to tail, no\n // `awaitAttachableJournal` to refuse the attach up front, and no\n // `alignedIfAttaching` to suppress the already-delivered prefix — so there\n // is nothing between here and that corruption except this throw.\n //\n // A FRESH durable run only fails to be recoverable LATER, which an app may\n // knowingly accept (it can wire `withSandbox({ runs, durability })` once at\n // the middleware level and still route some runs through this adapter). So\n // that is a warn, not a throw: audible, not fatal. Once per run, not per\n // chunk — a per-chunk warning would be worse than none. Mirrors\n // `ai-grok-build`'s `chatStreamAcp`.\n const durability = options.capabilities\n ? getSandboxDurability(options.capabilities, { optional: true })\n : undefined\n if (durability !== undefined) {\n if (durability.attach) {\n throw new DurableAttachNotSupportedError(\n 'acp',\n 'this adapter drives the harness over a bidirectional ACP ' +\n 'connection and does not journal',\n )\n }\n logger.warn(\n 'acp: sandbox durability is wired but this adapter never journals — ' +\n 'this run will not be recoverable on reconnect. Use a journaling ' +\n 'harness adapter for runs that must survive a host restart, or drop ' +\n 'durability if these runs are not meant to.',\n { runId, adapter: 'acp' },\n )\n }\n\n const channel = createBridgeEventChannel({\n model: this.model,\n threadId,\n runId,\n })\n\n const sessionId = modelOptions?.sessionId\n const { prompt: resumePrompt } = this.buildPrompt(\n options.messages,\n sessionId,\n )\n\n // Bridge chat()-provided tools into the agent over MCP (ACP http server).\n const bridgedToolNames = new Set(\n (options.tools ?? []).map((tool) => tool.name),\n )\n if (options.tools && options.tools.length > 0) {\n const provisioner =\n (options.capabilities\n ? getToolBridgeProvisioner(options.capabilities, { optional: true })\n : undefined) ?? nodeHttpBridgeProvisioner\n bridge = await provisioner.provision(options.tools, {\n provider: sandbox.provider,\n context: options.context,\n emitCustomEvent: channel.emitCustomEvent,\n ...(externalSignal ? { signal: externalSignal } : {}),\n })\n }\n\n // Project workspace skills declared via withSandbox. MCP skills ride ACP's\n // native `mcpServers` (below); gitSkills are linked into `skillsDir`.\n let workspaceServers: Array<AcpMcpServer> = []\n const projection = options.capabilities\n ? getWorkspaceProjection(options.capabilities, { optional: true })\n : undefined\n if (projection !== undefined) {\n await projectAcpWorkspace(sandbox, projection, {\n ...(this.harness.skillsDir !== undefined && {\n skillsDir: this.harness.skillsDir,\n }),\n harnessName: this.name,\n })\n workspaceServers = workspaceMcpServers(projection)\n }\n\n const ctx: AcpHarnessContext<ResolvedOptions<TModelOptions>> = {\n sandbox,\n model: this.model,\n cwd,\n harnessCwd,\n env: this.harness.env,\n modelOptions,\n signal: externalSignal,\n }\n transport = this.harness.openTransport\n ? await this.harness.openTransport(ctx)\n : await this.openStdioTransport(ctx)\n\n const mode =\n modelOptions?.permissionMode ??\n this.harness.permissionMode ??\n 'bypassPermissions'\n const authMethodId =\n modelOptions?.authMethodId ?? this.harness.authMethodId\n\n const approvalRequests: Array<StreamChunk> = []\n const permissionHandler = this.makePermissionHandler({\n mode,\n bridgedToolNames,\n approvals: options.approvals,\n approvalRequests,\n threadId,\n runId,\n })\n\n const queue = new AsyncQueue<AcpStreamEvent>()\n\n logger.request(\n `activity=chat provider=${this.name} model=${this.model} sandbox=${sandbox.provider} messages=${options.messages.length} resume=${sessionId ?? 'none'}`,\n { provider: this.name, model: this.model },\n )\n\n // The host tool-bridge (chat() tools) + workspace MCP skills, both over\n // ACP's native MCP channel.\n const mcpServers: Array<AcpMcpServer> = [\n ...(bridge !== undefined\n ? [\n {\n name: bridge.name,\n url: bridge.url,\n headers: [\n { name: 'Authorization', value: `Bearer ${bridge.token}` },\n ],\n },\n ]\n : []),\n ...workspaceServers,\n ]\n\n const onAcpUpdate = (update: AcpSessionUpdate) =>\n queue.push({ kind: 'update', update })\n handle = await startAcpSession({\n transport,\n cwd: harnessCwd,\n ...(authMethodId !== undefined && { authMethodId }),\n ...(sessionId !== undefined && { resumeSessionId: sessionId }),\n ...(mcpServers.length > 0 && { mcpServers }),\n onUpdate: onAcpUpdate,\n ...(this.harness.onExtNotification && {\n onExtNotification: this.harness.onExtNotification,\n }),\n onPermissionRequest: permissionHandler,\n })\n const session = handle\n\n if (externalSignal !== undefined) {\n onAbort = () => void session.cancel().catch(() => undefined)\n if (externalSignal.aborted) onAbort()\n else externalSignal.addEventListener('abort', onAbort, { once: true })\n }\n\n queue.push({ kind: 'session', sessionId: session.sessionId })\n\n const systemPrompts = normalizeSystemPrompts(options.systemPrompts)\n .map((p) => p.content)\n .filter((c) => c.trim() !== '')\n const promptText = this.applySystemPrompts(\n systemPrompts,\n session.resumed || sessionId === undefined\n ? resumePrompt\n : this.buildPrompt(options.messages, undefined).prompt,\n )\n\n session\n .prompt(promptText)\n .then(({ stopReason, usage }) => {\n queue.push({\n kind: 'done',\n stopReason,\n ...(usage !== undefined && { usage }),\n })\n queue.end()\n })\n .catch((error: unknown) => queue.fail(error))\n\n yield* mergeChunkStreams(\n translateAcpStream(queue, {\n model: this.model,\n runId,\n threadId,\n ...(options.parentRunId !== undefined && {\n parentRunId: options.parentRunId,\n }),\n genId: () => this.generateId(),\n bridgedToolNames,\n labels: {\n sessionIdEvent: `${this.name}.session-id`,\n // Surface non-text agent content (image/audio/resource) instead of\n // dropping it — emitted as a CUSTOM `<name>.message-content` event.\n contentEvent: `${this.name}.message-content`,\n ...(this.harness.planEventName !== undefined && {\n planEvent: this.harness.planEventName,\n }),\n ...(this.harness.refusalMessage !== undefined && {\n refusalMessage: this.harness.refusalMessage,\n }),\n },\n onAcpEvent: (event) =>\n logger.provider(`provider=${this.name} kind=${event.kind}`, {\n chunk: event,\n }),\n }),\n channel.stream,\n )\n\n // Surface any pending approval requests (interactive ask-policy actions\n // awaiting a client decision); the client approves and re-runs to continue.\n for (const event of approvalRequests) yield event\n\n if (this.harness.emitDiff) {\n yield* this.emitDiffChunks(sandbox, cwd, threadId, runId)\n }\n } catch (error: unknown) {\n const err = error as Error & { code?: string }\n const rawEvent = toRunErrorRawEvent(error)\n logger.errors(`${this.name}.chatStream fatal`, {\n error,\n source: `${this.name}.chatStream`,\n })\n yield {\n type: EventType.RUN_ERROR,\n model: options.model,\n timestamp: Date.now(),\n message: err.message || 'Unknown error occurred',\n ...(err.code !== undefined && { code: err.code }),\n ...(rawEvent !== undefined && { rawEvent }),\n error: {\n message: err.message || 'Unknown error occurred',\n ...(err.code !== undefined && { code: err.code }),\n },\n }\n } finally {\n if (externalSignal !== undefined && onAbort !== undefined) {\n externalSignal.removeEventListener('abort', onAbort)\n }\n // startAcpSession owns transport teardown once a handle exists (and tears\n // it down itself on a failed init). Only dispose here if we opened a\n // transport but never reached a session.\n if (handle !== undefined) await handle.dispose()\n else if (transport !== undefined) await disposeTransport(transport)\n await bridge?.close()\n }\n }\n\n private async openStdioTransport(\n ctx: AcpHarnessContext<ResolvedOptions<TModelOptions>>,\n ): Promise<AcpSessionTransport> {\n const build = this.harness.command\n if (build === undefined) {\n // Unreachable — the constructor requires `command` or `openTransport`,\n // and this path only runs when `openTransport` is absent.\n throw new Error(\n `acpCompatible(\"${this.name}\") has no \"command\" to launch over stdio.`,\n )\n }\n const command = build(ctx)\n const proc = await ctx.sandbox.process.spawn(command, {\n cwd: ctx.cwd,\n ...(this.harness.env ? { env: this.harness.env } : {}),\n ...(ctx.signal ? { signal: ctx.signal } : {}),\n })\n return { kind: 'stdio', process: proc }\n }\n\n private async *emitDiffChunks(\n sandbox: SandboxHandle,\n cwd: string,\n threadId: string,\n runId: string,\n ): AsyncIterable<StreamChunk> {\n try {\n const diff = await sandbox.process.exec(`git -C ${q(cwd)} diff`, { cwd })\n if (diff.exitCode === 0 && diff.stdout.trim() !== '') {\n yield {\n type: EventType.CUSTOM,\n name: 'file.changed',\n value: { path: '.', diff: diff.stdout },\n timestamp: Date.now(),\n threadId,\n runId,\n }\n }\n } catch {\n // ignore — diff is best-effort\n }\n }\n\n structuredOutput(\n _options: StructuredOutputOptions<ResolvedOptions<TModelOptions>>,\n ): Promise<StructuredOutputResult<unknown>> {\n return Promise.reject(\n new Error(\n `Structured output is not supported by the in-sandbox \"${this.name}\" ACP harness adapter. ` +\n 'Use a model adapter for structured output, or omit outputSchema.',\n ),\n )\n }\n}\n\n/**\n * Configure an ACP-compatible harness once, then select a model per call.\n *\n * Mirrors `openaiCompatible`: it lets you plug ANY Agent Client Protocol agent\n * into a TanStack AI sandbox without a dedicated adapter package.\n *\n * @example\n * ```ts\n * import { acpCompatible } from '@tanstack/ai-acp'\n * import { chat } from '@tanstack/ai'\n * import { defineSandbox, withSandbox } from '@tanstack/ai-sandbox'\n *\n * const pi = acpCompatible({\n * name: 'pi',\n * // declaring `models` makes pi('…') type-safe; omit to accept any string\n * models: ['pi-fast', 'pi-pro'],\n * // declare per-call options; merged with the base ACP options and exposed\n * // on ctx.modelOptions inside `command` / `openTransport`\n * modelOptions: {} as { reasoningEffort?: 'low' | 'high' },\n * command: ({ model, harnessCwd, modelOptions }) =>\n * `pi --acp -m ${model} --cwd ${harnessCwd}` +\n * (modelOptions?.reasoningEffort ? ` --effort ${modelOptions.reasoningEffort}` : ''),\n * authMethodId: 'pi-api-key',\n * })\n *\n * chat({\n * adapter: pi('pi-pro'),\n * modelOptions: { reasoningEffort: 'high' }, // typed\n * messages,\n * middleware: [withSandbox(defineSandbox({ /* provider, install pi *\\/ }))],\n * })\n * ```\n */\nexport function acpCompatible<\n const TModels extends ReadonlyArray<string> = ReadonlyArray<string>,\n TModelOptions extends Record<string, any> = AcpCompatibleProviderOptions,\n>(config: AcpCompatibleConfig<TModels, TModelOptions>) {\n return <TModel extends AcpModelNameOf<TModels>>(\n model: TModel,\n overrides?: Partial<AcpCompatibleConfig<TModels, TModelOptions>>,\n ): AcpCompatibleTextAdapter<TModel, TModelOptions> =>\n new AcpCompatibleTextAdapter<TModel, TModelOptions>(\n overrides ? { ...config, ...overrides } : config,\n model,\n )\n}\n\n/**\n * One-shot helper: build a single-model ACP-compatible harness adapter inline.\n *\n * @example\n * ```ts\n * chat({\n * adapter: acpCompatibleText('pi-fast', {\n * name: 'pi',\n * command: ({ model }) => `pi --acp -m ${model}`,\n * }),\n * messages,\n * middleware: [withSandbox(defineSandbox({ ... }))],\n * })\n * ```\n */\nexport function acpCompatibleText<\n TModel extends string,\n TModelOptions extends Record<string, any> = AcpCompatibleProviderOptions,\n>(\n model: TModel,\n config: AcpCompatibleConfig<ReadonlyArray<string>, TModelOptions>,\n): AcpCompatibleTextAdapter<TModel, TModelOptions> {\n return new AcpCompatibleTextAdapter<TModel, TModelOptions>(config, model)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"compatible.js","names":[],"sources":["../../../src/adapters/compatible.ts"],"sourcesContent":["import { EventType, normalizeSystemPrompts } from '@tanstack/ai'\nimport {\n appendOutputSchemaInstruction,\n parseJsonFromAssistantText,\n structuredOutputCompleteChunk,\n structuredOutputStartChunk,\n toRunErrorRawEvent,\n} from '@tanstack/ai/adapter-internals'\nimport { BaseTextAdapter } from '@tanstack/ai/adapters'\nimport {\n DurableAttachNotSupportedError,\n SandboxCapability,\n buildApprovalRequestedEvent,\n createBridgeEventChannel,\n getSandbox,\n getSandboxDurability,\n getToolBridgeProvisioner,\n getWorkspaceProjection,\n mergeChunkStreams,\n nodeHttpBridgeProvisioner,\n resolveDurableRunId,\n resolveHarnessCwd,\n} from '@tanstack/ai-sandbox'\nimport { AsyncQueue } from '../stream/queue'\nimport { startAcpSession } from '../session/acp-client'\nimport { translateAcpStream } from '../stream/translate'\nimport { resolveInteractivePermission, resolvePermission } from '../permissions'\nimport { buildAcpPrompt } from '../messages/prompt'\nimport { projectAcpWorkspace, workspaceMcpServers } from './projection'\nimport type { AcpMcpServer } from './projection'\nimport type { HostToolBridge, SandboxHandle } from '@tanstack/ai-sandbox'\nimport type {\n StructuredOutputOptions,\n StructuredOutputResult,\n} from '@tanstack/ai/adapters'\nimport type {\n DefaultMessageMetadataByModality,\n Modality,\n ModelMessage,\n StreamChunk,\n TextOptions,\n} from '@tanstack/ai'\nimport type { AcpSessionHandle } from '../session/acp-client'\nimport type { AcpStreamEvent } from '../stream/translate'\nimport type { AcpSessionTransport } from '../transport/types'\nimport type {\n AcpPermissionMode,\n AcpSessionUpdate,\n PermissionHandler,\n} from '../types/acp-types'\nimport type { BuiltAcpPrompt } from '../messages/prompt'\n\nconst DEFAULT_WORKDIR = '/workspace'\n\n/**\n * Everything a harness needs to know to launch its ACP server inside the\n * sandbox. Passed to {@link AcpCompatibleConfig.command} /\n * {@link AcpCompatibleConfig.openTransport}.\n */\nexport interface AcpHarnessContext<\n TModelOptions extends Record<string, any> = AcpCompatibleProviderOptions,\n> {\n /** The sandbox the harness runs in (from `withSandbox(...)` middleware). */\n sandbox: SandboxHandle\n /** The selected model id. */\n model: string\n /** Virtual cwd for `sandbox.process.spawn` (the provider maps `/workspace`). */\n cwd: string\n /** Literal cwd for the harness's own `--cwd` flag / ACP `newSession`. */\n harnessCwd: string\n /** Extra env vars configured for the harness process. */\n env: Record<string, string> | undefined\n /**\n * Per-call options from `chat({ modelOptions })` — the base ACP options plus\n * whatever you declared via {@link AcpCompatibleConfig.modelOptions}. Read\n * these to turn options into CLI flags / transport choices.\n */\n modelOptions: TModelOptions | undefined\n /** Abort signal for the run, when one was provided. */\n signal: AbortSignal | undefined\n}\n\n/** Union of selectable model names from a `models` tuple (any string if omitted). */\nexport type AcpModelNameOf<TModels extends ReadonlyArray<string>> =\n TModels[number]\n\nexport interface AcpCompatibleConfig<\n TModels extends ReadonlyArray<string> = ReadonlyArray<string>,\n TModelOptions extends Record<string, any> = AcpCompatibleProviderOptions,\n> {\n /**\n * Harness name. Used as the provider label, the log prefix, and the CUSTOM\n * session-id event name (`<name>.session-id`).\n */\n name: string\n /**\n * The models this harness accepts. Declaring them makes the returned factory\n * type-safe — `harness('known-model')` is checked, unknown ids are rejected.\n * Omit to accept any string.\n */\n models?: TModels\n /**\n * Type-only brand for the per-call options accepted via `chat({ modelOptions })`.\n * Declare your harness's options here with `{} as { ... }` (the value is unused\n * at runtime); they are merged with the base {@link AcpCompatibleProviderOptions}\n * and exposed on {@link AcpHarnessContext.modelOptions} so `command` /\n * `openTransport` can turn them into CLI flags.\n *\n * @example modelOptions: {} as { reasoningEffort?: 'low' | 'high' }\n */\n modelOptions?: TModelOptions\n /**\n * Build the shell command that launches the harness's ACP server over\n * **stdio** inside the sandbox (e.g. `` `pi --acp -m ${model}` ``). Required\n * unless {@link openTransport} is provided.\n */\n command?: (\n ctx: AcpHarnessContext<AcpCompatibleProviderOptions & TModelOptions>,\n ) => string\n /**\n * Full transport escape hatch — open any {@link AcpSessionTransport} yourself\n * (e.g. boot a `serve` process and connect over WebSocket, as Grok Build\n * does). Overrides {@link command}. Put ALL teardown in the returned\n * transport's `dispose` (stream) / process (stdio); it is disposed when the\n * session ends.\n */\n openTransport?: (\n ctx: AcpHarnessContext<AcpCompatibleProviderOptions & TModelOptions>,\n ) => Promise<AcpSessionTransport> | AcpSessionTransport\n /** Working directory inside the sandbox. Defaults to `/workspace`. */\n cwd?: string\n /**\n * The harness's skills directory, relative to the workspace root (e.g.\n * `'.pi/skills'`) — its native convention for where it auto-discovers skills,\n * the way Claude Code uses `.claude/skills`. When set, `withSandbox` workspace\n * `gitSkill`s are linked here. MCP skills don't need this: they're passed to\n * the agent over ACP natively. Omit and `gitSkill`s are left unlinked (warned).\n */\n skillsDir?: string\n /** Extra environment variables for the harness process. */\n env?: Record<string, string>\n /**\n * `'api-key'` (default) uses {@link authMethodId} (or `modelOptions.authMethodId`).\n * `'host'` skips ACP authenticate (use the CLI login on the machine).\n * Not inferred from the sandbox.\n */\n authMode?: 'host' | 'api-key'\n /**\n * ACP auth method to select before the session starts, when the harness\n * advertises one (e.g. `'pi-api-key'`). Overridable per call via\n * `modelOptions.authMethodId`. Ignored when {@link authMode} is `'host'`.\n */\n authMethodId?: string\n /** ACP permission policy. Defaults to `'bypassPermissions'`. */\n permissionMode?: AcpPermissionMode\n /**\n * Permission strategy:\n * - `'headless'` (default) — auto-resolve via {@link permissionMode}; the\n * sandbox is the boundary, so the agent runs without prompting.\n * - `'interactive'` — same policy, but `ask`-style prompts emit an\n * approval-requested event so a client can approve and re-run.\n */\n permissions?: 'headless' | 'interactive'\n /** Custom permission handler; overrides {@link permissions}/{@link permissionMode}. */\n onPermissionRequest?: PermissionHandler\n /** Message used for `RUN_ERROR` when the harness refuses a request. */\n refusalMessage?: string\n /** Emit ACP `plan` updates as a CUSTOM event under this name (off by default). */\n planEventName?: string\n /**\n * After the run, emit the `git diff` of the working dir as a `file.changed`\n * CUSTOM event. Requires a git repo at `cwd`. Off by default.\n */\n emitDiff?: boolean\n /**\n * Harness-specific JSON-RPC notifications (vendor `_x/...` extensions). Must\n * return without throwing — unknown extensions must not tear down the session.\n */\n onExtNotification?: (method: string, params: Record<string, unknown>) => void\n /**\n * Convert chat history into the harness prompt + resume inputs. Defaults to\n * {@link buildAcpPrompt} (trailing user message + flattened transcript).\n */\n buildPrompt?: (\n messages: Array<ModelMessage>,\n sessionId: string | undefined,\n ) => BuiltAcpPrompt\n}\n\n/** Per-call provider options, passed via `modelOptions` on `chat()`. */\nexport interface AcpCompatibleProviderOptions {\n /**\n * Resume an existing harness session. The adapter emits the session id of\n * every run via a CUSTOM `<name>.session-id` event; thread it back here to\n * continue (only the trailing user message is sent).\n */\n sessionId?: string\n /** Per-call override of the harness working directory. */\n cwd?: string\n /**\n * `'api-key'` (default) uses {@link authMethodId}.\n * `'host'` skips ACP authenticate.\n * Not inferred from the sandbox.\n */\n authMode?: 'host' | 'api-key'\n /** Per-call override of the ACP auth method. Ignored when authMode is `'host'`. */\n authMethodId?: string\n /** Per-call override of the ACP permission policy. */\n permissionMode?: AcpPermissionMode\n}\n\n/** Per-call options the adapter sees: the base ACP options + the harness's own. */\ntype ResolvedOptions<TModelOptions extends Record<string, any>> =\n AcpCompatibleProviderOptions & TModelOptions\n\nfunction q(value: string): string {\n return `'${value.replace(/'/g, `'\\\\''`)}'`\n}\n\nasync function disposeTransport(transport: AcpSessionTransport): Promise<void> {\n if (transport.kind === 'stdio') {\n await transport.process.kill()\n return\n }\n await transport.dispose()\n}\n\n/**\n * A generic ACP harness adapter built from {@link AcpCompatibleConfig}. Runs the\n * configured coding-agent CLI inside the sandbox provided by `withSandbox(...)`\n * and translates its ACP session into AG-UI `StreamChunk`s.\n */\nexport class AcpCompatibleTextAdapter<\n TModel extends string,\n TModelOptions extends Record<string, any> = AcpCompatibleProviderOptions,\n> extends BaseTextAdapter<\n TModel,\n ResolvedOptions<TModelOptions>,\n ReadonlyArray<Modality> & readonly ['text'],\n DefaultMessageMetadataByModality,\n ReadonlyArray<string>,\n unknown,\n never\n> {\n override readonly name: string\n\n override readonly requires = [SandboxCapability] as const\n\n private readonly harness: AcpCompatibleConfig<\n ReadonlyArray<string>,\n TModelOptions\n >\n\n constructor(\n config: AcpCompatibleConfig<ReadonlyArray<string>, TModelOptions>,\n model: TModel,\n ) {\n super({}, model)\n if (config.command === undefined && config.openTransport === undefined) {\n throw new Error(\n `acpCompatible(\"${config.name}\") needs either a \"command\" or an \"openTransport\".`,\n )\n }\n this.harness = config\n this.name = config.name\n }\n\n supportsCombinedToolsAndSchema(): boolean {\n return true\n }\n\n combinedStructuredOutputSource(): 'event' {\n return 'event'\n }\n\n private sandboxFrom(\n options: TextOptions<ResolvedOptions<TModelOptions>>,\n ): SandboxHandle {\n const ctx = options.capabilities\n if (!ctx) {\n throw new Error(\n `Adapter \"${this.name}\" requires a sandbox. Add withSandbox(defineSandbox({ ... })) to chat() middleware.`,\n )\n }\n return getSandbox(ctx)\n }\n\n private buildPrompt(\n messages: Array<ModelMessage>,\n sessionId: string | undefined,\n ): BuiltAcpPrompt {\n return this.harness.buildPrompt\n ? this.harness.buildPrompt(messages, sessionId)\n : buildAcpPrompt(messages, sessionId, this.name)\n }\n\n private applySystemPrompts(\n systemPrompts: Array<string>,\n prompt: string,\n ): string {\n if (systemPrompts.length === 0) return prompt\n return `${systemPrompts.join('\\n\\n')}\\n\\n${prompt}`\n }\n\n private makePermissionHandler(input: {\n mode: AcpPermissionMode\n bridgedToolNames: ReadonlySet<string>\n approvals: ReadonlyMap<string, boolean> | undefined\n approvalRequests: Array<StreamChunk>\n threadId: string\n runId: string\n }): PermissionHandler {\n if (this.harness.onPermissionRequest)\n return this.harness.onPermissionRequest\n\n if (this.harness.permissions === 'interactive') {\n return (request) => {\n const result = resolveInteractivePermission(\n request,\n input.mode,\n input.bridgedToolNames,\n input.approvals,\n this.name,\n )\n if (result.approvalId !== undefined) {\n input.approvalRequests.push(\n buildApprovalRequestedEvent({\n approvalId: result.approvalId,\n title:\n result.title ??\n request.toolCall.title ??\n request.toolCall.toolCallId,\n threadId: input.threadId,\n runId: input.runId,\n detail: { provider: this.name },\n }),\n )\n }\n return result.outcome\n }\n }\n\n return (request) =>\n resolvePermission(request, input.mode, input.bridgedToolNames)\n }\n\n async *chatStream(\n options: TextOptions<ResolvedOptions<TModelOptions>>,\n ): AsyncIterable<StreamChunk> {\n const { logger } = options\n let handle: AcpSessionHandle | undefined\n let bridge: HostToolBridge | undefined\n let transport: AcpSessionTransport | undefined\n const externalSignal =\n options.abortController?.signal ?? options.request?.signal ?? undefined\n let onAbort: (() => void) | undefined\n\n try {\n const sandbox = this.sandboxFrom(options)\n const modelOptions = options.modelOptions\n const cwd = modelOptions?.cwd ?? this.harness.cwd ?? DEFAULT_WORKDIR\n const harnessCwd = resolveHarnessCwd(sandbox, cwd)\n // This adapter does not journal yet, so a generated id is still fine.\n // Routed through the helper anyway so that whenever it gains journaling\n // it inherits the caller-supplied-runId requirement instead of\n // re-deriving it (see `packages/ai-sandbox/src/durability.ts`).\n const runId = resolveDurableRunId(options.runId, {\n durable: false,\n adapter: 'acp',\n fallback: () => this.generateId(),\n })\n const threadId = options.threadId ?? this.generateId()\n\n // Durability wired onto a path that cannot deliver it. Two outcomes, split\n // by whether a first attempt has already run.\n //\n // ATTACH is fatal. `sandboxRunDriver`'s `drive()` sets `attach: true` only\n // when a previous host was already streaming this run, so continuing past\n // here reaches `startAcpSession` + `session.prompt(...)` and re-runs the\n // agent from scratch against the workspace that attempt already mutated,\n // appending its whole output to a log that still holds the first\n // attempt's. This adapter has no journal to tail, no\n // `awaitAttachableJournal` to refuse the attach up front, and no\n // `alignedIfAttaching` to suppress the already-delivered prefix — so there\n // is nothing between here and that corruption except this throw.\n //\n // A FRESH durable run only fails to be recoverable LATER, which an app may\n // knowingly accept (it can wire `withSandbox({ runs, durability })` once at\n // the middleware level and still route some runs through this adapter). So\n // that is a warn, not a throw: audible, not fatal. Once per run, not per\n // chunk — a per-chunk warning would be worse than none. Mirrors\n // `ai-grok-build`'s `chatStreamAcp`.\n const durability = options.capabilities\n ? getSandboxDurability(options.capabilities, { optional: true })\n : undefined\n if (durability !== undefined) {\n if (durability.attach) {\n throw new DurableAttachNotSupportedError(\n 'acp',\n 'this adapter drives the harness over a bidirectional ACP ' +\n 'connection and does not journal',\n )\n }\n logger.warn(\n 'acp: sandbox durability is wired but this adapter never journals — ' +\n 'this run will not be recoverable on reconnect. Use a journaling ' +\n 'harness adapter for runs that must survive a host restart, or drop ' +\n 'durability if these runs are not meant to.',\n { runId, adapter: 'acp' },\n )\n }\n\n const channel = createBridgeEventChannel({\n model: this.model,\n threadId,\n runId,\n })\n\n const sessionId = modelOptions?.sessionId\n const { prompt: resumePrompt } = this.buildPrompt(\n options.messages,\n sessionId,\n )\n\n // Bridge chat()-provided tools into the agent over MCP (ACP http server).\n const bridgedToolNames = new Set(\n (options.tools ?? []).map((tool) => tool.name),\n )\n if (options.tools && options.tools.length > 0) {\n const provisioner =\n (options.capabilities\n ? getToolBridgeProvisioner(options.capabilities, { optional: true })\n : undefined) ?? nodeHttpBridgeProvisioner\n bridge = await provisioner.provision(options.tools, {\n provider: sandbox.provider,\n context: options.context,\n emitCustomEvent: channel.emitCustomEvent,\n ...(externalSignal ? { signal: externalSignal } : {}),\n })\n }\n\n // Project workspace skills declared via withSandbox. MCP skills ride ACP's\n // native `mcpServers` (below); gitSkills are linked into `skillsDir`.\n let workspaceServers: Array<AcpMcpServer> = []\n const projection = options.capabilities\n ? getWorkspaceProjection(options.capabilities, { optional: true })\n : undefined\n if (projection !== undefined) {\n await projectAcpWorkspace(sandbox, projection, {\n ...(this.harness.skillsDir !== undefined && {\n skillsDir: this.harness.skillsDir,\n }),\n harnessName: this.name,\n })\n workspaceServers = workspaceMcpServers(projection)\n }\n\n const ctx: AcpHarnessContext<ResolvedOptions<TModelOptions>> = {\n sandbox,\n model: this.model,\n cwd,\n harnessCwd,\n env: this.harness.env,\n modelOptions,\n signal: externalSignal,\n }\n transport = this.harness.openTransport\n ? await this.harness.openTransport(ctx)\n : await this.openStdioTransport(ctx)\n\n const mode =\n modelOptions?.permissionMode ??\n this.harness.permissionMode ??\n 'bypassPermissions'\n const authMode =\n modelOptions?.authMode ?? this.harness.authMode ?? 'api-key'\n const authMethodId =\n authMode === 'host'\n ? undefined\n : (modelOptions?.authMethodId ?? this.harness.authMethodId)\n\n const approvalRequests: Array<StreamChunk> = []\n const permissionHandler = this.makePermissionHandler({\n mode,\n bridgedToolNames,\n approvals: options.approvals,\n approvalRequests,\n threadId,\n runId,\n })\n\n const queue = new AsyncQueue<AcpStreamEvent>()\n\n logger.request(\n `activity=chat provider=${this.name} model=${this.model} sandbox=${sandbox.provider} messages=${options.messages.length} resume=${sessionId ?? 'none'}`,\n { provider: this.name, model: this.model },\n )\n\n // The host tool-bridge (chat() tools) + workspace MCP skills, both over\n // ACP's native MCP channel.\n const mcpServers: Array<AcpMcpServer> = [\n ...(bridge !== undefined\n ? [\n {\n name: bridge.name,\n url: bridge.url,\n headers: [\n { name: 'Authorization', value: `Bearer ${bridge.token}` },\n ],\n },\n ]\n : []),\n ...workspaceServers,\n ]\n\n const onAcpUpdate = (update: AcpSessionUpdate) =>\n queue.push({ kind: 'update', update })\n handle = await startAcpSession({\n transport,\n cwd: harnessCwd,\n ...(authMethodId !== undefined && { authMethodId }),\n ...(sessionId !== undefined && { resumeSessionId: sessionId }),\n ...(mcpServers.length > 0 && { mcpServers }),\n onUpdate: onAcpUpdate,\n ...(this.harness.onExtNotification && {\n onExtNotification: this.harness.onExtNotification,\n }),\n onPermissionRequest: permissionHandler,\n })\n const session = handle\n\n if (externalSignal !== undefined) {\n onAbort = () => void session.cancel().catch(() => undefined)\n if (externalSignal.aborted) onAbort()\n else externalSignal.addEventListener('abort', onAbort, { once: true })\n }\n\n queue.push({ kind: 'session', sessionId: session.sessionId })\n\n const systemPrompts = normalizeSystemPrompts(options.systemPrompts)\n .map((p) => p.content)\n .filter((c) => c.trim() !== '')\n let promptText = this.applySystemPrompts(\n systemPrompts,\n session.resumed || sessionId === undefined\n ? resumePrompt\n : this.buildPrompt(options.messages, undefined).prompt,\n )\n if (options.outputSchema) {\n promptText = appendOutputSchemaInstruction(\n promptText,\n options.outputSchema,\n )\n }\n\n session\n .prompt(promptText)\n .then(({ stopReason, usage }) => {\n queue.push({\n kind: 'done',\n stopReason,\n ...(usage !== undefined && { usage }),\n })\n queue.end()\n })\n .catch((error: unknown) => queue.fail(error))\n\n const wantsStructured = options.outputSchema !== undefined\n let lastAssistantText = ''\n let lastTextMessageId: string | undefined\n let heldFinished: StreamChunk | undefined\n for await (const chunk of mergeChunkStreams(\n translateAcpStream(queue, {\n model: this.model,\n runId,\n threadId,\n ...(options.parentRunId !== undefined && {\n parentRunId: options.parentRunId,\n }),\n genId: () => this.generateId(),\n bridgedToolNames,\n labels: {\n sessionIdEvent: `${this.name}.session-id`,\n // Surface non-text agent content (image/audio/resource) instead of\n // dropping it — emitted as a CUSTOM `<name>.message-content` event.\n contentEvent: `${this.name}.message-content`,\n ...(this.harness.planEventName !== undefined && {\n planEvent: this.harness.planEventName,\n }),\n ...(this.harness.refusalMessage !== undefined && {\n refusalMessage: this.harness.refusalMessage,\n }),\n },\n onAcpEvent: (event) =>\n logger.provider(`provider=${this.name} kind=${event.kind}`, {\n chunk: event,\n }),\n }),\n channel.stream,\n )) {\n if (wantsStructured && chunk.type === EventType.RUN_FINISHED) {\n heldFinished = chunk\n continue\n }\n if (wantsStructured) {\n if (chunk.type === EventType.TEXT_MESSAGE_START) {\n lastAssistantText = ''\n if (typeof chunk.messageId === 'string' && chunk.messageId !== '') {\n lastTextMessageId = chunk.messageId\n }\n } else if (\n chunk.type === EventType.TEXT_MESSAGE_CONTENT &&\n typeof chunk.delta === 'string'\n ) {\n lastAssistantText += chunk.delta\n }\n }\n yield chunk\n }\n\n if (options.outputSchema) {\n yield* this.emitParsedStructuredOutput(\n lastAssistantText,\n threadId,\n runId,\n lastTextMessageId,\n )\n }\n if (heldFinished) yield heldFinished\n\n // Surface any pending approval requests (interactive ask-policy actions\n // awaiting a client decision); the client approves and re-runs to continue.\n for (const event of approvalRequests) yield event\n\n if (this.harness.emitDiff) {\n yield* this.emitDiffChunks(sandbox, cwd, threadId, runId)\n }\n } catch (error: unknown) {\n const err = error as Error & { code?: string }\n const rawEvent = toRunErrorRawEvent(error)\n logger.errors(`${this.name}.chatStream fatal`, {\n error,\n source: `${this.name}.chatStream`,\n })\n yield {\n type: EventType.RUN_ERROR,\n model: options.model,\n timestamp: Date.now(),\n message: err.message || 'Unknown error occurred',\n ...(err.code !== undefined && { code: err.code }),\n ...(rawEvent !== undefined && { rawEvent }),\n error: {\n message: err.message || 'Unknown error occurred',\n ...(err.code !== undefined && { code: err.code }),\n },\n }\n } finally {\n if (externalSignal !== undefined && onAbort !== undefined) {\n externalSignal.removeEventListener('abort', onAbort)\n }\n // startAcpSession owns transport teardown once a handle exists (and tears\n // it down itself on a failed init). Only dispose here if we opened a\n // transport but never reached a session.\n if (handle !== undefined) await handle.dispose()\n else if (transport !== undefined) await disposeTransport(transport)\n await bridge?.close()\n }\n }\n\n private async openStdioTransport(\n ctx: AcpHarnessContext<ResolvedOptions<TModelOptions>>,\n ): Promise<AcpSessionTransport> {\n const build = this.harness.command\n if (build === undefined) {\n // Unreachable — the constructor requires `command` or `openTransport`,\n // and this path only runs when `openTransport` is absent.\n throw new Error(\n `acpCompatible(\"${this.name}\") has no \"command\" to launch over stdio.`,\n )\n }\n const command = build(ctx)\n const proc = await ctx.sandbox.process.spawn(command, {\n cwd: ctx.cwd,\n ...(this.harness.env ? { env: this.harness.env } : {}),\n ...(ctx.signal ? { signal: ctx.signal } : {}),\n })\n return { kind: 'stdio', process: proc }\n }\n\n private async *emitDiffChunks(\n sandbox: SandboxHandle,\n cwd: string,\n threadId: string,\n runId: string,\n ): AsyncIterable<StreamChunk> {\n try {\n const diff = await sandbox.process.exec(`git -C ${q(cwd)} diff`, { cwd })\n if (diff.exitCode === 0 && diff.stdout.trim() !== '') {\n yield {\n type: EventType.CUSTOM,\n name: 'file.changed',\n value: { path: '.', diff: diff.stdout },\n timestamp: Date.now(),\n threadId,\n runId,\n }\n }\n } catch {\n // ignore — diff is best-effort\n }\n }\n\n private *emitParsedStructuredOutput(\n raw: string,\n threadId: string,\n runId: string,\n messageId = this.generateId(),\n ): Generator<StreamChunk> {\n try {\n const object = parseJsonFromAssistantText(raw)\n yield structuredOutputStartChunk({\n messageId,\n model: this.model,\n threadId,\n runId,\n })\n yield structuredOutputCompleteChunk({\n messageId,\n model: this.model,\n threadId,\n runId,\n object,\n raw,\n })\n } catch (error: unknown) {\n const parserMessage =\n error instanceof Error\n ? error.message\n : 'Failed to parse structured output'\n const preview = raw.trim().slice(0, 200)\n const message =\n preview === '' ? parserMessage : `${parserMessage} Content: ${preview}`\n yield {\n type: EventType.RUN_ERROR,\n model: this.model,\n timestamp: Date.now(),\n message,\n code: 'structured-output-parse-failed',\n error: { message, code: 'structured-output-parse-failed' },\n }\n }\n }\n\n structuredOutput(\n _options: StructuredOutputOptions<ResolvedOptions<TModelOptions>>,\n ): Promise<StructuredOutputResult<unknown>> {\n return Promise.reject(\n new Error(\n 'This harness honors outputSchema on chat() in the same turn. ' +\n 'Pass outputSchema to chat(), or use a model adapter for a one-shot extract.',\n ),\n )\n }\n}\n\n/**\n * Configure an ACP-compatible harness once, then select a model per call.\n *\n * Mirrors `openaiCompatible`: it lets you plug ANY Agent Client Protocol agent\n * into a TanStack AI sandbox without a dedicated adapter package.\n *\n * @example\n * ```ts\n * import { acpCompatible } from '@tanstack/ai-acp'\n * import { chat } from '@tanstack/ai'\n * import { defineSandbox, withSandbox } from '@tanstack/ai-sandbox'\n *\n * const pi = acpCompatible({\n * name: 'pi',\n * // declaring `models` makes pi('…') type-safe; omit to accept any string\n * models: ['pi-fast', 'pi-pro'],\n * // declare per-call options; merged with the base ACP options and exposed\n * // on ctx.modelOptions inside `command` / `openTransport`\n * modelOptions: {} as { reasoningEffort?: 'low' | 'high' },\n * command: ({ model, harnessCwd, modelOptions }) =>\n * `pi --acp -m ${model} --cwd ${harnessCwd}` +\n * (modelOptions?.reasoningEffort ? ` --effort ${modelOptions.reasoningEffort}` : ''),\n * authMethodId: 'pi-api-key',\n * })\n *\n * chat({\n * adapter: pi('pi-pro'),\n * modelOptions: { reasoningEffort: 'high' }, // typed\n * messages,\n * middleware: [withSandbox(defineSandbox({ /* provider, install pi *\\/ }))],\n * })\n * ```\n */\nexport function acpCompatible<\n const TModels extends ReadonlyArray<string> = ReadonlyArray<string>,\n TModelOptions extends Record<string, any> = AcpCompatibleProviderOptions,\n>(config: AcpCompatibleConfig<TModels, TModelOptions>) {\n return <TModel extends AcpModelNameOf<TModels>>(\n model: TModel,\n overrides?: Partial<AcpCompatibleConfig<TModels, TModelOptions>>,\n ): AcpCompatibleTextAdapter<TModel, TModelOptions> =>\n new AcpCompatibleTextAdapter<TModel, TModelOptions>(\n overrides ? { ...config, ...overrides } : config,\n model,\n )\n}\n\n/**\n * One-shot helper: build a single-model ACP-compatible harness adapter inline.\n *\n * @example\n * ```ts\n * chat({\n * adapter: acpCompatibleText('pi-fast', {\n * name: 'pi',\n * command: ({ model }) => `pi --acp -m ${model}`,\n * }),\n * messages,\n * middleware: [withSandbox(defineSandbox({ ... }))],\n * })\n * ```\n */\nexport function acpCompatibleText<\n TModel extends string,\n TModelOptions extends Record<string, any> = AcpCompatibleProviderOptions,\n>(\n model: TModel,\n config: AcpCompatibleConfig<ReadonlyArray<string>, TModelOptions>,\n): AcpCompatibleTextAdapter<TModel, TModelOptions> {\n return new AcpCompatibleTextAdapter<TModel, TModelOptions>(config, model)\n}\n"],"mappings":";;;;;;;;;;;AAoDA,IAAM,kBAAkB;AAmKxB,SAAS,EAAE,OAAuB;CAChC,OAAO,IAAI,MAAM,QAAQ,MAAM,OAAO,EAAE;AAC1C;AAEA,eAAe,iBAAiB,WAA+C;CAC7E,IAAI,UAAU,SAAS,SAAS;EAC9B,MAAM,UAAU,QAAQ,KAAK;EAC7B;CACF;CACA,MAAM,UAAU,QAAQ;AAC1B;;;;;;AAOA,IAAa,2BAAb,cAGU,gBAQR;CACA;CAEA,WAA6B,CAAC,iBAAiB;CAE/C;CAKA,YACE,QACA,OACA;EACA,MAAM,CAAC,GAAG,KAAK;EACf,IAAI,OAAO,YAAY,KAAA,KAAa,OAAO,kBAAkB,KAAA,GAC3D,MAAM,IAAI,MACR,kBAAkB,OAAO,KAAK,mDAChC;EAEF,KAAK,UAAU;EACf,KAAK,OAAO,OAAO;CACrB;CAEA,iCAA0C;EACxC,OAAO;CACT;CAEA,iCAA0C;EACxC,OAAO;CACT;CAEA,YACE,SACe;EACf,MAAM,MAAM,QAAQ;EACpB,IAAI,CAAC,KACH,MAAM,IAAI,MACR,YAAY,KAAK,KAAK,oFACxB;EAEF,OAAO,WAAW,GAAG;CACvB;CAEA,YACE,UACA,WACgB;EAChB,OAAO,KAAK,QAAQ,cAChB,KAAK,QAAQ,YAAY,UAAU,SAAS,IAC5C,eAAe,UAAU,WAAW,KAAK,IAAI;CACnD;CAEA,mBACE,eACA,QACQ;EACR,IAAI,cAAc,WAAW,GAAG,OAAO;EACvC,OAAO,GAAG,cAAc,KAAK,MAAM,EAAE,MAAM;CAC7C;CAEA,sBAA8B,OAOR;EACpB,IAAI,KAAK,QAAQ,qBACf,OAAO,KAAK,QAAQ;EAEtB,IAAI,KAAK,QAAQ,gBAAgB,eAC/B,QAAQ,YAAY;GAClB,MAAM,SAAS,6BACb,SACA,MAAM,MACN,MAAM,kBACN,MAAM,WACN,KAAK,IACP;GACA,IAAI,OAAO,eAAe,KAAA,GACxB,MAAM,iBAAiB,KACrB,4BAA4B;IAC1B,YAAY,OAAO;IACnB,OACE,OAAO,SACP,QAAQ,SAAS,SACjB,QAAQ,SAAS;IACnB,UAAU,MAAM;IAChB,OAAO,MAAM;IACb,QAAQ,EAAE,UAAU,KAAK,KAAK;GAChC,CAAC,CACH;GAEF,OAAO,OAAO;EAChB;EAGF,QAAQ,YACN,kBAAkB,SAAS,MAAM,MAAM,MAAM,gBAAgB;CACjE;CAEA,OAAO,WACL,SAC4B;EAC5B,MAAM,EAAE,WAAW;EACnB,IAAI;EACJ,IAAI;EACJ,IAAI;EACJ,MAAM,iBACJ,QAAQ,iBAAiB,UAAU,QAAQ,SAAS,UAAU,KAAA;EAChE,IAAI;EAEJ,IAAI;GACF,MAAM,UAAU,KAAK,YAAY,OAAO;GACxC,MAAM,eAAe,QAAQ;GAC7B,MAAM,MAAM,cAAc,OAAO,KAAK,QAAQ,OAAO;GACrD,MAAM,aAAa,kBAAkB,SAAS,GAAG;GAKjD,MAAM,QAAQ,oBAAoB,QAAQ,OAAO;IAC/C,SAAS;IACT,SAAS;IACT,gBAAgB,KAAK,WAAW;GAClC,CAAC;GACD,MAAM,WAAW,QAAQ,YAAY,KAAK,WAAW;GAqBrD,MAAM,aAAa,QAAQ,eACvB,qBAAqB,QAAQ,cAAc,EAAE,UAAU,KAAK,CAAC,IAC7D,KAAA;GACJ,IAAI,eAAe,KAAA,GAAW;IAC5B,IAAI,WAAW,QACb,MAAM,IAAI,+BACR,OACA,0FAEF;IAEF,OAAO,KACL,oPAIA;KAAE;KAAO,SAAS;IAAM,CAC1B;GACF;GAEA,MAAM,UAAU,yBAAyB;IACvC,OAAO,KAAK;IACZ;IACA;GACF,CAAC;GAED,MAAM,YAAY,cAAc;GAChC,MAAM,EAAE,QAAQ,iBAAiB,KAAK,YACpC,QAAQ,UACR,SACF;GAGA,MAAM,mBAAmB,IAAI,KAC1B,QAAQ,SAAS,CAAC,EAAA,CAAG,KAAK,SAAS,KAAK,IAAI,CAC/C;GACA,IAAI,QAAQ,SAAS,QAAQ,MAAM,SAAS,GAK1C,SAAS,QAHN,QAAQ,eACL,yBAAyB,QAAQ,cAAc,EAAE,UAAU,KAAK,CAAC,IACjE,KAAA,MAAc,0BAAA,CACO,UAAU,QAAQ,OAAO;IAClD,UAAU,QAAQ;IAClB,SAAS,QAAQ;IACjB,iBAAiB,QAAQ;IACzB,GAAI,iBAAiB,EAAE,QAAQ,eAAe,IAAI,CAAC;GACrD,CAAC;GAKH,IAAI,mBAAwC,CAAC;GAC7C,MAAM,aAAa,QAAQ,eACvB,uBAAuB,QAAQ,cAAc,EAAE,UAAU,KAAK,CAAC,IAC/D,KAAA;GACJ,IAAI,eAAe,KAAA,GAAW;IAC5B,MAAM,oBAAoB,SAAS,YAAY;KAC7C,GAAI,KAAK,QAAQ,cAAc,KAAA,KAAa,EAC1C,WAAW,KAAK,QAAQ,UAC1B;KACA,aAAa,KAAK;IACpB,CAAC;IACD,mBAAmB,oBAAoB,UAAU;GACnD;GAEA,MAAM,MAAyD;IAC7D;IACA,OAAO,KAAK;IACZ;IACA;IACA,KAAK,KAAK,QAAQ;IAClB;IACA,QAAQ;GACV;GACA,YAAY,KAAK,QAAQ,gBACrB,MAAM,KAAK,QAAQ,cAAc,GAAG,IACpC,MAAM,KAAK,mBAAmB,GAAG;GAErC,MAAM,OACJ,cAAc,kBACd,KAAK,QAAQ,kBACb;GAGF,MAAM,gBADJ,cAAc,YAAY,KAAK,QAAQ,YAAY,eAEtC,SACT,KAAA,IACC,cAAc,gBAAgB,KAAK,QAAQ;GAElD,MAAM,mBAAuC,CAAC;GAC9C,MAAM,oBAAoB,KAAK,sBAAsB;IACnD;IACA;IACA,WAAW,QAAQ;IACnB;IACA;IACA;GACF,CAAC;GAED,MAAM,QAAQ,IAAI,WAA2B;GAE7C,OAAO,QACL,0BAA0B,KAAK,KAAK,SAAS,KAAK,MAAM,WAAW,QAAQ,SAAS,YAAY,QAAQ,SAAS,OAAO,UAAU,aAAa,UAC/I;IAAE,UAAU,KAAK;IAAM,OAAO,KAAK;GAAM,CAC3C;GAIA,MAAM,aAAkC,CACtC,GAAI,WAAW,KAAA,IACX,CACE;IACE,MAAM,OAAO;IACb,KAAK,OAAO;IACZ,SAAS,CACP;KAAE,MAAM;KAAiB,OAAO,UAAU,OAAO;IAAQ,CAC3D;GACF,CACF,IACA,CAAC,GACL,GAAG,gBACL;GAEA,MAAM,eAAe,WACnB,MAAM,KAAK;IAAE,MAAM;IAAU;GAAO,CAAC;GACvC,SAAS,MAAM,gBAAgB;IAC7B;IACA,KAAK;IACL,GAAI,iBAAiB,KAAA,KAAa,EAAE,aAAa;IACjD,GAAI,cAAc,KAAA,KAAa,EAAE,iBAAiB,UAAU;IAC5D,GAAI,WAAW,SAAS,KAAK,EAAE,WAAW;IAC1C,UAAU;IACV,GAAI,KAAK,QAAQ,qBAAqB,EACpC,mBAAmB,KAAK,QAAQ,kBAClC;IACA,qBAAqB;GACvB,CAAC;GACD,MAAM,UAAU;GAEhB,IAAI,mBAAmB,KAAA,GAAW;IAChC,gBAAgB,KAAK,QAAQ,OAAO,CAAC,CAAC,YAAY,KAAA,CAAS;IAC3D,IAAI,eAAe,SAAS,QAAQ;SAC/B,eAAe,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;GACvE;GAEA,MAAM,KAAK;IAAE,MAAM;IAAW,WAAW,QAAQ;GAAU,CAAC;GAE5D,MAAM,gBAAgB,uBAAuB,QAAQ,aAAa,CAAC,CAChE,KAAK,MAAM,EAAE,OAAO,CAAC,CACrB,QAAQ,MAAM,EAAE,KAAK,MAAM,EAAE;GAChC,IAAI,aAAa,KAAK,mBACpB,eACA,QAAQ,WAAW,cAAc,KAAA,IAC7B,eACA,KAAK,YAAY,QAAQ,UAAU,KAAA,CAAS,CAAC,CAAC,MACpD;GACA,IAAI,QAAQ,cACV,aAAa,8BACX,YACA,QAAQ,YACV;GAGF,QACG,OAAO,UAAU,CAAC,CAClB,MAAM,EAAE,YAAY,YAAY;IAC/B,MAAM,KAAK;KACT,MAAM;KACN;KACA,GAAI,UAAU,KAAA,KAAa,EAAE,MAAM;IACrC,CAAC;IACD,MAAM,IAAI;GACZ,CAAC,CAAC,CACD,OAAO,UAAmB,MAAM,KAAK,KAAK,CAAC;GAE9C,MAAM,kBAAkB,QAAQ,iBAAiB,KAAA;GACjD,IAAI,oBAAoB;GACxB,IAAI;GACJ,IAAI;GACJ,WAAW,MAAM,SAAS,kBACxB,mBAAmB,OAAO;IACxB,OAAO,KAAK;IACZ;IACA;IACA,GAAI,QAAQ,gBAAgB,KAAA,KAAa,EACvC,aAAa,QAAQ,YACvB;IACA,aAAa,KAAK,WAAW;IAC7B;IACA,QAAQ;KACN,gBAAgB,GAAG,KAAK,KAAK;KAG7B,cAAc,GAAG,KAAK,KAAK;KAC3B,GAAI,KAAK,QAAQ,kBAAkB,KAAA,KAAa,EAC9C,WAAW,KAAK,QAAQ,cAC1B;KACA,GAAI,KAAK,QAAQ,mBAAmB,KAAA,KAAa,EAC/C,gBAAgB,KAAK,QAAQ,eAC/B;IACF;IACA,aAAa,UACX,OAAO,SAAS,YAAY,KAAK,KAAK,QAAQ,MAAM,QAAQ,EAC1D,OAAO,MACT,CAAC;GACL,CAAC,GACD,QAAQ,MACV,GAAG;IACD,IAAI,mBAAmB,MAAM,SAAS,UAAU,cAAc;KAC5D,eAAe;KACf;IACF;IACA,IAAI,iBAAiB;KACnB,IAAI,MAAM,SAAS,UAAU,oBAAoB;MAC/C,oBAAoB;MACpB,IAAI,OAAO,MAAM,cAAc,YAAY,MAAM,cAAc,IAC7D,oBAAoB,MAAM;KAE9B,OAAO,IACL,MAAM,SAAS,UAAU,wBACzB,OAAO,MAAM,UAAU,UAEvB,qBAAqB,MAAM;IAE/B;IACA,MAAM;GACR;GAEA,IAAI,QAAQ,cACV,OAAO,KAAK,2BACV,mBACA,UACA,OACA,iBACF;GAEF,IAAI,cAAc,MAAM;GAIxB,KAAK,MAAM,SAAS,kBAAkB,MAAM;GAE5C,IAAI,KAAK,QAAQ,UACf,OAAO,KAAK,eAAe,SAAS,KAAK,UAAU,KAAK;EAE5D,SAAS,OAAgB;GACvB,MAAM,MAAM;GACZ,MAAM,WAAW,mBAAmB,KAAK;GACzC,OAAO,OAAO,GAAG,KAAK,KAAK,oBAAoB;IAC7C;IACA,QAAQ,GAAG,KAAK,KAAK;GACvB,CAAC;GACD,MAAM;IACJ,MAAM,UAAU;IAChB,OAAO,QAAQ;IACf,WAAW,KAAK,IAAI;IACpB,SAAS,IAAI,WAAW;IACxB,GAAI,IAAI,SAAS,KAAA,KAAa,EAAE,MAAM,IAAI,KAAK;IAC/C,GAAI,aAAa,KAAA,KAAa,EAAE,SAAS;IACzC,OAAO;KACL,SAAS,IAAI,WAAW;KACxB,GAAI,IAAI,SAAS,KAAA,KAAa,EAAE,MAAM,IAAI,KAAK;IACjD;GACF;EACF,UAAU;GACR,IAAI,mBAAmB,KAAA,KAAa,YAAY,KAAA,GAC9C,eAAe,oBAAoB,SAAS,OAAO;GAKrD,IAAI,WAAW,KAAA,GAAW,MAAM,OAAO,QAAQ;QAC1C,IAAI,cAAc,KAAA,GAAW,MAAM,iBAAiB,SAAS;GAClE,MAAM,QAAQ,MAAM;EACtB;CACF;CAEA,MAAc,mBACZ,KAC8B;EAC9B,MAAM,QAAQ,KAAK,QAAQ;EAC3B,IAAI,UAAU,KAAA,GAGZ,MAAM,IAAI,MACR,kBAAkB,KAAK,KAAK,0CAC9B;EAEF,MAAM,UAAU,MAAM,GAAG;EAMzB,OAAO;GAAE,MAAM;GAAS,SAAS,MALd,IAAI,QAAQ,QAAQ,MAAM,SAAS;IACpD,KAAK,IAAI;IACT,GAAI,KAAK,QAAQ,MAAM,EAAE,KAAK,KAAK,QAAQ,IAAI,IAAI,CAAC;IACpD,GAAI,IAAI,SAAS,EAAE,QAAQ,IAAI,OAAO,IAAI,CAAC;GAC7C,CAAC;EACqC;CACxC;CAEA,OAAe,eACb,SACA,KACA,UACA,OAC4B;EAC5B,IAAI;GACF,MAAM,OAAO,MAAM,QAAQ,QAAQ,KAAK,UAAU,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,CAAC;GACxE,IAAI,KAAK,aAAa,KAAK,KAAK,OAAO,KAAK,MAAM,IAChD,MAAM;IACJ,MAAM,UAAU;IAChB,MAAM;IACN,OAAO;KAAE,MAAM;KAAK,MAAM,KAAK;IAAO;IACtC,WAAW,KAAK,IAAI;IACpB;IACA;GACF;EAEJ,QAAQ,CAER;CACF;CAEA,CAAS,2BACP,KACA,UACA,OACA,YAAY,KAAK,WAAW,GACJ;EACxB,IAAI;GACF,MAAM,SAAS,2BAA2B,GAAG;GAC7C,MAAM,2BAA2B;IAC/B;IACA,OAAO,KAAK;IACZ;IACA;GACF,CAAC;GACD,MAAM,8BAA8B;IAClC;IACA,OAAO,KAAK;IACZ;IACA;IACA;IACA;GACF,CAAC;EACH,SAAS,OAAgB;GACvB,MAAM,gBACJ,iBAAiB,QACb,MAAM,UACN;GACN,MAAM,UAAU,IAAI,KAAK,CAAC,CAAC,MAAM,GAAG,GAAG;GACvC,MAAM,UACJ,YAAY,KAAK,gBAAgB,GAAG,cAAc,YAAY;GAChE,MAAM;IACJ,MAAM,UAAU;IAChB,OAAO,KAAK;IACZ,WAAW,KAAK,IAAI;IACpB;IACA,MAAM;IACN,OAAO;KAAE;KAAS,MAAM;IAAiC;GAC3D;EACF;CACF;CAEA,iBACE,UAC0C;EAC1C,OAAO,QAAQ,uBACb,IAAI,MACF,0IAEF,CACF;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,SAAgB,cAGd,QAAqD;CACrD,QACE,OACA,cAEA,IAAI,yBACF,YAAY;EAAE,GAAG;EAAQ,GAAG;CAAU,IAAI,QAC1C,KACF;AACJ;;;;;;;;;;;;;;;;AAiBA,SAAgB,kBAId,OACA,QACiD;CACjD,OAAO,IAAI,yBAAgD,QAAQ,KAAK;AAC1E"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isSecretRef, resolveGitSkillDir } from "@tanstack/ai-sandbox";
|
|
1
|
+
import { discoverSkillDirs, isSecretRef, resolveGitSkillDir, resolveHarnessCwd } from "@tanstack/ai-sandbox";
|
|
2
2
|
//#region src/adapters/projection.ts
|
|
3
3
|
/**
|
|
4
4
|
* Generic workspace projector for ACP harnesses.
|
|
@@ -22,10 +22,6 @@ import { isSecretRef, resolveGitSkillDir } from "@tanstack/ai-sandbox";
|
|
|
22
22
|
function shellQuote(value) {
|
|
23
23
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
24
24
|
}
|
|
25
|
-
function basenameOf(path) {
|
|
26
|
-
const segments = path.split("/").filter((segment) => segment !== "");
|
|
27
|
-
return segments[segments.length - 1] ?? path;
|
|
28
|
-
}
|
|
29
25
|
/**
|
|
30
26
|
* Make a sandbox path relative to the workspace root, so shell commands work on
|
|
31
27
|
* every provider. Only `fs.*` remaps the virtual `/workspace`; a raw `/workspace`
|
|
@@ -80,15 +76,20 @@ async function projectAcpWorkspace(handle, projection, options) {
|
|
|
80
76
|
if (await handle.fs.exists(projection.markerPath)) return;
|
|
81
77
|
const { skillsDir, harnessName } = options;
|
|
82
78
|
const gitSkills = projection.skills.filter((skill) => skill.kind === "git");
|
|
83
|
-
if (gitSkills.length > 0)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
79
|
+
if (gitSkills.length > 0) {
|
|
80
|
+
if (skillsDir === void 0) for (const skill of gitSkills) console.warn(`[${harnessName}] gitSkill "${skill.repo}" cannot be projected: this harness declares no \`skillsDir\`. The clone is still available under the workspace, but the harness will not auto-discover it. Skipping link.`);
|
|
81
|
+
else {
|
|
82
|
+
await handle.fs.mkdir(`${projection.root}/${skillsDir}`);
|
|
83
|
+
for (const skill of gitSkills) {
|
|
84
|
+
const source = skill.into ?? resolveGitSkillDir(projection.root, skill);
|
|
85
|
+
const discovered = await discoverSkillDirs(handle, source);
|
|
86
|
+
for (const { name, dir } of discovered) {
|
|
87
|
+
const relSource = relativeToRoot(resolveHarnessCwd(handle, projection.root), resolveHarnessCwd(handle, dir));
|
|
88
|
+
const relTarget = `${skillsDir}/${name}`;
|
|
89
|
+
const cp = await handle.process.exec(`cp -r ${shellQuote(relSource)} ${shellQuote(relTarget)}`, { cwd: projection.root });
|
|
90
|
+
if (cp.exitCode !== 0) console.warn(`[${harnessName}] failed to copy gitSkill "${skill.repo}" into ${relTarget}: ${cp.stderr.trim()}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
95
|
for (const skill of projection.skills) if (skill.kind === "agent-skill") console.warn(`[${harnessName}] agentSkill "${skill.name}" cannot be projected: there is no generic ACP primitive to install a skill by bare name. Provide it as a gitSkill instead. Skipping.`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"projection.js","names":[],"sources":["../../../src/adapters/projection.ts"],"sourcesContent":["/**\n * Generic workspace projector for ACP harnesses.\n *\n * `withSandbox` surfaces a portable {@link WorkspaceProjection} (skills, plugins,\n * a secret resolver, a one-time marker path) via a capability. Most of it maps\n * onto ACP natively:\n *\n * - **MCP skills** → passed straight through ACP's `newSession` `mcpServers`\n * (see {@link workspaceMcpServers}); no config file is written, because an\n * ACP agent receives MCP servers over the protocol. This is the key\n * difference from file-based harnesses (Claude Code, Codex) that read MCP\n * from disk.\n * - **gitSkill repos** → linked into the harness's skills directory (when the\n * harness declares one via `skillsDir`, e.g. `.pi/skills`).\n * - **agentSkill / plugins** → no generic ACP primitive, so we warn-and-skip.\n *\n * `fileSkill` and `instructions` are already written by the provider-agnostic\n * bootstrap (into the workspace root + `AGENTS.md`), so they need no projection.\n */\nimport {
|
|
1
|
+
{"version":3,"file":"projection.js","names":[],"sources":["../../../src/adapters/projection.ts"],"sourcesContent":["/**\n * Generic workspace projector for ACP harnesses.\n *\n * `withSandbox` surfaces a portable {@link WorkspaceProjection} (skills, plugins,\n * a secret resolver, a one-time marker path) via a capability. Most of it maps\n * onto ACP natively:\n *\n * - **MCP skills** → passed straight through ACP's `newSession` `mcpServers`\n * (see {@link workspaceMcpServers}); no config file is written, because an\n * ACP agent receives MCP servers over the protocol. This is the key\n * difference from file-based harnesses (Claude Code, Codex) that read MCP\n * from disk.\n * - **gitSkill repos** → linked into the harness's skills directory (when the\n * harness declares one via `skillsDir`, e.g. `.pi/skills`).\n * - **agentSkill / plugins** → no generic ACP primitive, so we warn-and-skip.\n *\n * `fileSkill` and `instructions` are already written by the provider-agnostic\n * bootstrap (into the workspace root + `AGENTS.md`), so they need no projection.\n */\nimport {\n discoverSkillDirs,\n isSecretRef,\n resolveGitSkillDir,\n resolveHarnessCwd,\n} from '@tanstack/ai-sandbox'\nimport type {\n BearerRef,\n SandboxHandle,\n SecretRef,\n WorkspaceProjection,\n} from '@tanstack/ai-sandbox'\n\n/** ACP `newSession` MCP server descriptor (HTTP transport). */\nexport interface AcpMcpServer {\n name: string\n url: string\n headers: Array<{ name: string; value: string }>\n}\n\nfunction shellQuote(value: string): string {\n return `'${value.replace(/'/g, `'\\\\''`)}'`\n}\n\n/**\n * Make a sandbox path relative to the workspace root, so shell commands work on\n * every provider. Only `fs.*` remaps the virtual `/workspace`; a raw `/workspace`\n * in a shell command is the real path in a container but a non-existent absolute\n * path on local-process. Running relative to the root (the exec cwd) is correct\n * everywhere.\n */\nfunction relativeToRoot(root: string, p: string): string {\n return p.startsWith(`${root}/`) ? p.slice(root.length + 1) : p\n}\n\nfunction isBearerMarker(value: unknown): value is BearerRef {\n return (\n typeof value === 'object' &&\n value !== null &&\n isSecretRef((value as { __bearerRef?: unknown }).__bearerRef)\n )\n}\n\nfunction resolveHeaderValue(\n value: string | SecretRef | BearerRef,\n resolveSecret: (ref: SecretRef) => string,\n): string {\n if (isSecretRef(value)) return resolveSecret(value)\n if (isBearerMarker(value)) return `Bearer ${resolveSecret(value.__bearerRef)}`\n return value\n}\n\n/**\n * Build ACP `mcpServers` entries from the workspace's `mcp` skills, resolving\n * every header value (SecretRef / bearer / plain string). Returns `[]` when\n * there are no MCP skills. Pass the result to `startAcpSession({ mcpServers })`.\n */\nexport function workspaceMcpServers(\n projection: WorkspaceProjection,\n): Array<AcpMcpServer> {\n const servers: Array<AcpMcpServer> = []\n for (const skill of projection.skills) {\n if (skill.kind !== 'mcp') continue\n const rawUrl = skill.config['url']\n const url = typeof rawUrl === 'string' ? rawUrl : ''\n const headers = Object.entries(skill.config.headers ?? {}).map(\n ([name, value]) => ({\n name,\n value: resolveHeaderValue(value, projection.resolveSecret),\n }),\n )\n servers.push({ name: skill.name, url, headers })\n }\n return servers\n}\n\n/**\n * Project the non-MCP parts of a workspace into an ACP harness. Links each\n * cloned `gitSkill` repo into the harness's skills directory when one is\n * declared; warns and skips `agentSkill`s, `plugin`s, and (when no `skillsDir`\n * is configured) `gitSkill`s. Idempotent — gated by the projection marker.\n *\n * MCP skills are NOT handled here; pass {@link workspaceMcpServers} to the ACP\n * session instead.\n */\nexport async function projectAcpWorkspace(\n handle: SandboxHandle,\n projection: WorkspaceProjection,\n options: { skillsDir?: string; harnessName: string },\n): Promise<void> {\n // Idempotent, non-secret operations only — gate on the one-time marker.\n if (await handle.fs.exists(projection.markerPath)) return\n\n const { skillsDir, harnessName } = options\n const gitSkills = projection.skills.filter((skill) => skill.kind === 'git')\n\n if (gitSkills.length > 0) {\n if (skillsDir === undefined) {\n for (const skill of gitSkills) {\n console.warn(\n `[${harnessName}] gitSkill \"${skill.repo}\" cannot be projected: this ` +\n 'harness declares no `skillsDir`. The clone is still available under ' +\n 'the workspace, but the harness will not auto-discover it. Skipping link.',\n )\n }\n } else {\n // Create the skills dir via fs (which remaps the virtual root), then copy\n // each clone in with paths relative to the exec cwd (the workspace root)\n // so the shell command resolves on every provider.\n await handle.fs.mkdir(`${projection.root}/${skillsDir}`)\n for (const skill of gitSkills) {\n // Keep virtual `/workspace` paths for fs discovery. handle.fs remaps\n // them. Remap only when building shell-relative paths so Daytona and\n // local-process both resolve the same clone.\n const source = skill.into ?? resolveGitSkillDir(projection.root, skill)\n const discovered = await discoverSkillDirs(handle, source)\n for (const { name, dir } of discovered) {\n const realRoot = resolveHarnessCwd(handle, projection.root)\n const realDir = resolveHarnessCwd(handle, dir)\n const relSource = relativeToRoot(realRoot, realDir)\n const relTarget = `${skillsDir}/${name}`\n const cp = await handle.process.exec(\n `cp -r ${shellQuote(relSource)} ${shellQuote(relTarget)}`,\n { cwd: projection.root },\n )\n if (cp.exitCode !== 0) {\n console.warn(\n `[${harnessName}] failed to copy gitSkill \"${skill.repo}\" into ${relTarget}: ${cp.stderr.trim()}`,\n )\n }\n }\n }\n }\n }\n\n for (const skill of projection.skills) {\n if (skill.kind === 'agent-skill') {\n console.warn(\n `[${harnessName}] agentSkill \"${skill.name}\" cannot be projected: there is ` +\n 'no generic ACP primitive to install a skill by bare name. Provide it as ' +\n 'a gitSkill instead. Skipping.',\n )\n }\n }\n for (const name of projection.plugins) {\n console.warn(\n `[${harnessName}] plugin \"${name}\" cannot be projected: ACP has no generic ` +\n 'plugin concept. Provide its functionality as a gitSkill or MCP server. Skipping.',\n )\n }\n\n await handle.fs.write(projection.markerPath, '')\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAuCA,SAAS,WAAW,OAAuB;CACzC,OAAO,IAAI,MAAM,QAAQ,MAAM,OAAO,EAAE;AAC1C;;;;;;;;AASA,SAAS,eAAe,MAAc,GAAmB;CACvD,OAAO,EAAE,WAAW,GAAG,KAAK,EAAE,IAAI,EAAE,MAAM,KAAK,SAAS,CAAC,IAAI;AAC/D;AAEA,SAAS,eAAe,OAAoC;CAC1D,OACE,OAAO,UAAU,YACjB,UAAU,QACV,YAAa,MAAoC,WAAW;AAEhE;AAEA,SAAS,mBACP,OACA,eACQ;CACR,IAAI,YAAY,KAAK,GAAG,OAAO,cAAc,KAAK;CAClD,IAAI,eAAe,KAAK,GAAG,OAAO,UAAU,cAAc,MAAM,WAAW;CAC3E,OAAO;AACT;;;;;;AAOA,SAAgB,oBACd,YACqB;CACrB,MAAM,UAA+B,CAAC;CACtC,KAAK,MAAM,SAAS,WAAW,QAAQ;EACrC,IAAI,MAAM,SAAS,OAAO;EAC1B,MAAM,SAAS,MAAM,OAAO;EAC5B,MAAM,MAAM,OAAO,WAAW,WAAW,SAAS;EAClD,MAAM,UAAU,OAAO,QAAQ,MAAM,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,KACxD,CAAC,MAAM,YAAY;GAClB;GACA,OAAO,mBAAmB,OAAO,WAAW,aAAa;EAC3D,EACF;EACA,QAAQ,KAAK;GAAE,MAAM,MAAM;GAAM;GAAK;EAAQ,CAAC;CACjD;CACA,OAAO;AACT;;;;;;;;;;AAWA,eAAsB,oBACpB,QACA,YACA,SACe;CAEf,IAAI,MAAM,OAAO,GAAG,OAAO,WAAW,UAAU,GAAG;CAEnD,MAAM,EAAE,WAAW,gBAAgB;CACnC,MAAM,YAAY,WAAW,OAAO,QAAQ,UAAU,MAAM,SAAS,KAAK;CAE1E,IAAI,UAAU,SAAS,GAAG;EACxB,IAAI,cAAc,KAAA,GAChB,KAAK,MAAM,SAAS,WAClB,QAAQ,KACN,IAAI,YAAY,cAAc,MAAM,KAAK,2KAG3C;OAEG;GAIL,MAAM,OAAO,GAAG,MAAM,GAAG,WAAW,KAAK,GAAG,WAAW;GACvD,KAAK,MAAM,SAAS,WAAW;IAI7B,MAAM,SAAS,MAAM,QAAQ,mBAAmB,WAAW,MAAM,KAAK;IACtE,MAAM,aAAa,MAAM,kBAAkB,QAAQ,MAAM;IACzD,KAAK,MAAM,EAAE,MAAM,SAAS,YAAY;KAGtC,MAAM,YAAY,eAFD,kBAAkB,QAAQ,WAAW,IAErB,GADjB,kBAAkB,QAAQ,GACC,CAAO;KAClD,MAAM,YAAY,GAAG,UAAU,GAAG;KAClC,MAAM,KAAK,MAAM,OAAO,QAAQ,KAC9B,SAAS,WAAW,SAAS,EAAE,GAAG,WAAW,SAAS,KACtD,EAAE,KAAK,WAAW,KAAK,CACzB;KACA,IAAI,GAAG,aAAa,GAClB,QAAQ,KACN,IAAI,YAAY,6BAA6B,MAAM,KAAK,SAAS,UAAU,IAAI,GAAG,OAAO,KAAK,GAChG;IAEJ;GACF;EACF;CACF;CAEA,KAAK,MAAM,SAAS,WAAW,QAC7B,IAAI,MAAM,SAAS,eACjB,QAAQ,KACN,IAAI,YAAY,gBAAgB,MAAM,KAAK,sIAG7C;CAGJ,KAAK,MAAM,QAAQ,WAAW,SAC5B,QAAQ,KACN,IAAI,YAAY,YAAY,KAAK,2HAEnC;CAGF,MAAM,OAAO,GAAG,MAAM,WAAW,YAAY,EAAE;AACjD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translate.js","names":[],"sources":["../../../src/stream/translate.ts"],"sourcesContent":["import { EventType, buildBaseUsage } from '@tanstack/ai'\nimport type { StreamChunk, TokenUsage } from '@tanstack/ai'\nimport type {\n AcpSessionUpdate,\n AcpStopReason,\n AcpToolCallUpdate,\n AcpUsage,\n} from '../types/acp-types'\n\nexport const BRIDGED_MCP_SERVER_NAME = 'tanstack'\n\nexport type AcpStreamEvent =\n | { kind: 'session'; sessionId: string }\n | { kind: 'update'; update: AcpSessionUpdate }\n | { kind: 'done'; stopReason: AcpStopReason; usage?: AcpUsage }\n\nexport interface AcpTranslateLabels {\n sessionIdEvent: string\n planEvent?: string\n refusalMessage?: string\n /**\n * When set, non-text agent message content (image / audio / resource /\n * resource_link blocks) is surfaced as a CUSTOM event under this name instead\n * of being dropped. Each event's `value` is `{ content: <ACP content block> }`.\n * Omit to keep the text-only behavior.\n */\n contentEvent?: string\n}\n\nexport interface TranslateContext {\n model: string\n runId: string\n threadId: string\n parentRunId?: string\n genId: () => string\n labels: AcpTranslateLabels\n bridgedToolNames?: ReadonlySet<string>\n onAcpEvent?: (event: AcpStreamEvent) => void\n}\n\nexport function matchBridgedToolName(\n title: string | null | undefined,\n bridgedToolNames: ReadonlySet<string> | undefined,\n): string | undefined {\n if (!title || !bridgedToolNames) return undefined\n if (bridgedToolNames.has(title)) return title\n for (const name of bridgedToolNames) {\n if (title.startsWith(`${name} (`)) return name\n }\n return undefined\n}\n\nfunction resolveToolName(\n update: AcpToolCallUpdate,\n bridgedToolNames: ReadonlySet<string> | undefined,\n): string {\n return (\n matchBridgedToolName(update.title, bridgedToolNames) ??\n update.kind ??\n 'tool'\n )\n}\n\nfunction stringifyToolOutput(update: AcpToolCallUpdate): string {\n if (update.rawOutput !== undefined) {\n return typeof update.rawOutput === 'string'\n ? update.rawOutput\n : JSON.stringify(update.rawOutput)\n }\n const blocks = update.content ?? []\n const text = blocks\n .map((block) =>\n block.content && typeof block.content.text === 'string'\n ? block.content.text\n : '',\n )\n .join('')\n // Preserve non-text tool content (diff / terminal / image / resource blocks)\n // by serializing the structured array, rather than collapsing to a stub.\n const hasNonText = blocks.some(\n (block) =>\n block.type !== 'content' ||\n (block.content !== undefined && block.content.type !== 'text'),\n )\n if (hasNonText) return JSON.stringify(blocks)\n if (text !== '') return text\n return JSON.stringify({ status: update.status ?? 'completed' })\n}\n\nfunction buildUsage(usage: AcpUsage | undefined): TokenUsage | undefined {\n if (!usage) return undefined\n const promptTokens = usage.inputTokens ?? 0\n const completionTokens = usage.outputTokens ?? 0\n const result = buildBaseUsage({\n promptTokens,\n completionTokens,\n totalTokens: usage.totalTokens ?? promptTokens + completionTokens,\n })\n if (usage.cachedReadTokens) {\n result.promptTokensDetails = { cachedTokens: usage.cachedReadTokens }\n }\n if (usage.thoughtTokens) {\n result.completionTokensDetails = { reasoningTokens: usage.thoughtTokens }\n }\n return result\n}\n\nexport async function* translateAcpStream(\n events: AsyncIterable<AcpStreamEvent>,\n ctx: TranslateContext,\n): AsyncIterable<StreamChunk> {\n const { model, runId, threadId, genId, labels } = ctx\n const now = () => Date.now()\n\n let runStarted = false\n const unresolvedToolCalls = new Set<string>()\n const knownToolCalls = new Set<string>()\n\n let textMessageId: string | null = null\n let textContent = ''\n let reasoningId: string | null = null\n\n function* startRun(): Generator<StreamChunk> {\n if (runStarted) return\n runStarted = true\n yield {\n type: EventType.RUN_STARTED,\n runId,\n threadId,\n model,\n timestamp: now(),\n ...(ctx.parentRunId !== undefined && { parentRunId: ctx.parentRunId }),\n }\n }\n\n function* closeText(): Generator<StreamChunk> {\n if (textMessageId !== null) {\n yield {\n type: EventType.TEXT_MESSAGE_END,\n messageId: textMessageId,\n model,\n timestamp: now(),\n }\n }\n textMessageId = null\n textContent = ''\n }\n\n function* closeReasoning(): Generator<StreamChunk> {\n if (reasoningId !== null) {\n yield {\n type: EventType.REASONING_MESSAGE_END,\n messageId: reasoningId,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_END,\n messageId: reasoningId,\n model,\n timestamp: now(),\n }\n }\n reasoningId = null\n }\n\n function* synthesizeUnresolvedResults(): Generator<StreamChunk> {\n for (const toolCallId of unresolvedToolCalls) {\n yield {\n type: EventType.TOOL_CALL_RESULT,\n toolCallId,\n messageId: genId(),\n model,\n timestamp: now(),\n content: JSON.stringify({ status: 'interrupted' }),\n }\n }\n unresolvedToolCalls.clear()\n }\n\n function* openToolCall(update: AcpToolCallUpdate): Generator<StreamChunk> {\n if (knownToolCalls.has(update.toolCallId)) return\n knownToolCalls.add(update.toolCallId)\n const toolCallName = resolveToolName(update, ctx.bridgedToolNames)\n const input = {\n ...(update.title != null && { title: update.title }),\n ...(update.rawInput !== undefined && update.rawInput !== null\n ? typeof update.rawInput === 'object'\n ? (update.rawInput as Record<string, unknown>)\n : { input: update.rawInput }\n : {}),\n }\n const args = JSON.stringify(input)\n yield {\n type: EventType.TOOL_CALL_START,\n toolCallId: update.toolCallId,\n toolCallName,\n toolName: toolCallName,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.TOOL_CALL_ARGS,\n toolCallId: update.toolCallId,\n model,\n timestamp: now(),\n delta: args,\n args,\n }\n yield {\n type: EventType.TOOL_CALL_END,\n toolCallId: update.toolCallId,\n toolCallName,\n toolName: toolCallName,\n model,\n timestamp: now(),\n input,\n }\n unresolvedToolCalls.add(update.toolCallId)\n }\n\n function* resolveToolCall(update: AcpToolCallUpdate): Generator<StreamChunk> {\n yield* openToolCall(update)\n unresolvedToolCalls.delete(update.toolCallId)\n yield {\n type: EventType.TOOL_CALL_RESULT,\n toolCallId: update.toolCallId,\n messageId: genId(),\n model,\n timestamp: now(),\n content: stringifyToolOutput(update),\n ...(update.status === 'failed' && { state: 'output-error' as const }),\n }\n }\n\n function* handleUpdate(update: AcpSessionUpdate): Generator<StreamChunk> {\n if (update.sessionUpdate === 'agent_message_chunk') {\n yield* closeReasoning()\n // Non-text content (image / audio / resource / resource_link): surface it\n // as a CUSTOM event when the harness opted in, instead of dropping it.\n if (update.content.type !== 'text') {\n if (labels.contentEvent !== undefined) {\n yield* closeText()\n yield {\n type: EventType.CUSTOM,\n model,\n timestamp: now(),\n name: labels.contentEvent,\n value: { content: update.content },\n }\n }\n return\n }\n const text =\n typeof update.content.text === 'string' ? update.content.text : ''\n if (text === '') return\n if (textMessageId === null) {\n textMessageId = genId()\n yield {\n type: EventType.TEXT_MESSAGE_START,\n messageId: textMessageId,\n model,\n timestamp: now(),\n role: 'assistant',\n }\n }\n textContent += text\n yield {\n type: EventType.TEXT_MESSAGE_CONTENT,\n messageId: textMessageId,\n model,\n timestamp: now(),\n delta: text,\n content: textContent,\n }\n } else if (update.sessionUpdate === 'agent_thought_chunk') {\n yield* closeText()\n const thought =\n typeof update.content.text === 'string' ? update.content.text : ''\n if (thought === '') return\n if (reasoningId === null) {\n reasoningId = genId()\n yield {\n type: EventType.REASONING_START,\n messageId: reasoningId,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_MESSAGE_START,\n messageId: reasoningId,\n role: 'reasoning' as const,\n model,\n timestamp: now(),\n }\n }\n yield {\n type: EventType.REASONING_MESSAGE_CONTENT,\n messageId: reasoningId,\n delta: thought,\n model,\n timestamp: now(),\n }\n } else if (update.sessionUpdate === 'tool_call') {\n yield* closeText()\n yield* closeReasoning()\n yield* openToolCall(update)\n if (update.status === 'completed' || update.status === 'failed') {\n yield* resolveToolCall(update)\n }\n } else if (update.sessionUpdate === 'tool_call_update') {\n if (update.status === 'completed' || update.status === 'failed') {\n yield* resolveToolCall(update)\n } else if (\n update.status === 'in_progress' &&\n update.rawInput !== undefined\n ) {\n yield* closeText()\n yield* closeReasoning()\n if (!knownToolCalls.has(update.toolCallId)) {\n yield* openToolCall(update)\n } else {\n const input = {\n ...(update.title != null && { title: update.title }),\n ...(typeof update.rawInput === 'object' && update.rawInput !== null\n ? (update.rawInput as Record<string, unknown>)\n : { input: update.rawInput }),\n }\n const args = JSON.stringify(input)\n yield {\n type: EventType.TOOL_CALL_ARGS,\n toolCallId: update.toolCallId,\n model,\n timestamp: now(),\n delta: args,\n args,\n }\n }\n }\n } else if (\n update.sessionUpdate === 'plan' &&\n labels.planEvent !== undefined\n ) {\n yield {\n type: EventType.CUSTOM,\n model,\n timestamp: now(),\n name: labels.planEvent,\n value: { entries: update.entries },\n }\n }\n }\n\n try {\n for await (const event of events) {\n ctx.onAcpEvent?.(event)\n\n if (event.kind === 'session') {\n yield* startRun()\n yield {\n type: EventType.CUSTOM,\n model,\n timestamp: now(),\n name: labels.sessionIdEvent,\n value: { sessionId: event.sessionId },\n }\n } else if (event.kind === 'update') {\n yield* startRun()\n yield* handleUpdate(event.update)\n } else {\n yield* startRun()\n yield* closeText()\n yield* closeReasoning()\n yield* synthesizeUnresolvedResults()\n\n if (event.stopReason === 'refusal') {\n const message =\n labels.refusalMessage ?? 'The harness refused the request.'\n yield {\n type: EventType.RUN_ERROR,\n model,\n timestamp: now(),\n message,\n code: 'refusal',\n error: { message, code: 'refusal' },\n }\n } else {\n const usage = buildUsage(event.usage)\n const finishReason =\n event.stopReason === 'max_tokens' ||\n event.stopReason === 'max_turn_requests'\n ? ('length' as const)\n : ('stop' as const)\n yield {\n type: EventType.RUN_FINISHED,\n runId,\n threadId,\n model,\n timestamp: now(),\n finishReason,\n ...(usage !== undefined && { usage }),\n }\n }\n }\n }\n } catch (error) {\n yield* closeText()\n yield* closeReasoning()\n yield* synthesizeUnresolvedResults()\n throw error\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"translate.js","names":[],"sources":["../../../src/stream/translate.ts"],"sourcesContent":["import { EventType, buildBaseUsage } from '@tanstack/ai'\nimport type { StreamChunk, TokenUsage } from '@tanstack/ai'\nimport type {\n AcpSessionUpdate,\n AcpStopReason,\n AcpToolCallUpdate,\n AcpUsage,\n} from '../types/acp-types'\n\nexport const BRIDGED_MCP_SERVER_NAME = 'tanstack'\n\nexport type AcpStreamEvent =\n | { kind: 'session'; sessionId: string }\n | { kind: 'update'; update: AcpSessionUpdate }\n | { kind: 'done'; stopReason: AcpStopReason; usage?: AcpUsage }\n\nexport interface AcpTranslateLabels {\n sessionIdEvent: string\n planEvent?: string\n refusalMessage?: string\n /**\n * When set, non-text agent message content (image / audio / resource /\n * resource_link blocks) is surfaced as a CUSTOM event under this name instead\n * of being dropped. Each event's `value` is `{ content: <ACP content block> }`.\n * Omit to keep the text-only behavior.\n */\n contentEvent?: string\n}\n\nexport interface TranslateContext {\n model: string\n runId: string\n threadId: string\n parentRunId?: string\n genId: () => string\n labels: AcpTranslateLabels\n bridgedToolNames?: ReadonlySet<string>\n onAcpEvent?: (event: AcpStreamEvent) => void\n}\n\nexport function matchBridgedToolName(\n title: string | null | undefined,\n bridgedToolNames: ReadonlySet<string> | undefined,\n): string | undefined {\n if (!title || !bridgedToolNames) return undefined\n if (bridgedToolNames.has(title)) return title\n for (const name of bridgedToolNames) {\n if (title.startsWith(`${name} (`)) return name\n }\n return undefined\n}\n\nfunction resolveToolName(\n update: AcpToolCallUpdate,\n bridgedToolNames: ReadonlySet<string> | undefined,\n): string {\n return (\n matchBridgedToolName(update.title, bridgedToolNames) ??\n update.kind ??\n 'tool'\n )\n}\n\nfunction stringifyToolOutput(update: AcpToolCallUpdate): string {\n if (update.rawOutput !== undefined) {\n return typeof update.rawOutput === 'string'\n ? update.rawOutput\n : JSON.stringify(update.rawOutput)\n }\n const blocks = update.content ?? []\n const text = blocks\n .map((block) =>\n block.content && typeof block.content.text === 'string'\n ? block.content.text\n : '',\n )\n .join('')\n // Preserve non-text tool content (diff / terminal / image / resource blocks)\n // by serializing the structured array, rather than collapsing to a stub.\n const hasNonText = blocks.some(\n (block) =>\n block.type !== 'content' ||\n (block.content !== undefined && block.content.type !== 'text'),\n )\n if (hasNonText) return JSON.stringify(blocks)\n if (text !== '') return text\n return JSON.stringify({ status: update.status ?? 'completed' })\n}\n\nfunction buildUsage(usage: AcpUsage | undefined): TokenUsage | undefined {\n if (!usage) return undefined\n const promptTokens = usage.inputTokens ?? 0\n const completionTokens = usage.outputTokens ?? 0\n const result = buildBaseUsage({\n promptTokens,\n completionTokens,\n totalTokens: usage.totalTokens ?? promptTokens + completionTokens,\n })\n if (usage.cachedReadTokens) {\n result.promptTokensDetails = { cachedTokens: usage.cachedReadTokens }\n }\n if (usage.thoughtTokens) {\n result.completionTokensDetails = { reasoningTokens: usage.thoughtTokens }\n }\n return result\n}\n\nexport async function* translateAcpStream(\n events: AsyncIterable<AcpStreamEvent>,\n ctx: TranslateContext,\n): AsyncIterable<StreamChunk> {\n const { model, runId, threadId, genId, labels } = ctx\n const now = () => Date.now()\n\n let runStarted = false\n const unresolvedToolCalls = new Set<string>()\n const knownToolCalls = new Set<string>()\n\n let textMessageId: string | null = null\n let textContent = ''\n let reasoningId: string | null = null\n\n function* startRun(): Generator<StreamChunk> {\n if (runStarted) return\n runStarted = true\n yield {\n type: EventType.RUN_STARTED,\n runId,\n threadId,\n model,\n timestamp: now(),\n ...(ctx.parentRunId !== undefined && { parentRunId: ctx.parentRunId }),\n }\n }\n\n function* closeText(): Generator<StreamChunk> {\n if (textMessageId !== null) {\n yield {\n type: EventType.TEXT_MESSAGE_END,\n messageId: textMessageId,\n model,\n timestamp: now(),\n }\n }\n textMessageId = null\n textContent = ''\n }\n\n function* closeReasoning(): Generator<StreamChunk> {\n if (reasoningId !== null) {\n yield {\n type: EventType.REASONING_MESSAGE_END,\n messageId: reasoningId,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_END,\n messageId: reasoningId,\n model,\n timestamp: now(),\n }\n }\n reasoningId = null\n }\n\n function* synthesizeUnresolvedResults(): Generator<StreamChunk> {\n for (const toolCallId of unresolvedToolCalls) {\n yield {\n type: EventType.TOOL_CALL_RESULT,\n toolCallId,\n messageId: genId(),\n model,\n timestamp: now(),\n content: JSON.stringify({ status: 'interrupted' }),\n }\n }\n unresolvedToolCalls.clear()\n }\n\n function* openToolCall(update: AcpToolCallUpdate): Generator<StreamChunk> {\n if (knownToolCalls.has(update.toolCallId)) return\n knownToolCalls.add(update.toolCallId)\n const toolCallName = resolveToolName(update, ctx.bridgedToolNames)\n const input = {\n ...(update.title != null && { title: update.title }),\n ...(update.rawInput !== undefined && update.rawInput !== null\n ? typeof update.rawInput === 'object'\n ? (update.rawInput as Record<string, unknown>)\n : { input: update.rawInput }\n : {}),\n }\n const args = JSON.stringify(input)\n yield {\n type: EventType.TOOL_CALL_START,\n toolCallId: update.toolCallId,\n toolCallName,\n toolName: toolCallName,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.TOOL_CALL_ARGS,\n toolCallId: update.toolCallId,\n model,\n timestamp: now(),\n delta: args,\n args,\n }\n yield {\n type: EventType.TOOL_CALL_END,\n toolCallId: update.toolCallId,\n toolCallName,\n toolName: toolCallName,\n model,\n timestamp: now(),\n input,\n }\n unresolvedToolCalls.add(update.toolCallId)\n }\n\n function* resolveToolCall(update: AcpToolCallUpdate): Generator<StreamChunk> {\n yield* openToolCall(update)\n unresolvedToolCalls.delete(update.toolCallId)\n yield {\n type: EventType.TOOL_CALL_RESULT,\n toolCallId: update.toolCallId,\n messageId: genId(),\n model,\n timestamp: now(),\n content: stringifyToolOutput(update),\n ...(update.status === 'failed' && { state: 'output-error' as const }),\n }\n }\n\n function* handleUpdate(update: AcpSessionUpdate): Generator<StreamChunk> {\n if (update.sessionUpdate === 'agent_message_chunk') {\n yield* closeReasoning()\n // Non-text content (image / audio / resource / resource_link): surface it\n // as a CUSTOM event when the harness opted in, instead of dropping it.\n if (update.content.type !== 'text') {\n if (labels.contentEvent !== undefined) {\n yield* closeText()\n yield {\n type: EventType.CUSTOM,\n model,\n timestamp: now(),\n name: labels.contentEvent,\n value: { content: update.content },\n }\n }\n return\n }\n const text =\n typeof update.content.text === 'string' ? update.content.text : ''\n if (text === '') return\n if (textMessageId === null) {\n textMessageId = genId()\n yield {\n type: EventType.TEXT_MESSAGE_START,\n messageId: textMessageId,\n model,\n timestamp: now(),\n role: 'assistant',\n }\n }\n textContent += text\n yield {\n type: EventType.TEXT_MESSAGE_CONTENT,\n messageId: textMessageId,\n model,\n timestamp: now(),\n delta: text,\n content: textContent,\n }\n } else if (update.sessionUpdate === 'agent_thought_chunk') {\n yield* closeText()\n const thought =\n typeof update.content.text === 'string' ? update.content.text : ''\n if (thought === '') return\n if (reasoningId === null) {\n reasoningId = genId()\n yield {\n type: EventType.REASONING_START,\n messageId: reasoningId,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_MESSAGE_START,\n messageId: reasoningId,\n role: 'reasoning' as const,\n model,\n timestamp: now(),\n }\n }\n yield {\n type: EventType.REASONING_MESSAGE_CONTENT,\n messageId: reasoningId,\n delta: thought,\n model,\n timestamp: now(),\n }\n } else if (update.sessionUpdate === 'tool_call') {\n yield* closeText()\n yield* closeReasoning()\n yield* openToolCall(update)\n if (update.status === 'completed' || update.status === 'failed') {\n yield* resolveToolCall(update)\n }\n } else if (update.sessionUpdate === 'tool_call_update') {\n if (update.status === 'completed' || update.status === 'failed') {\n yield* resolveToolCall(update)\n } else if (\n update.status === 'in_progress' &&\n update.rawInput !== undefined\n ) {\n yield* closeText()\n yield* closeReasoning()\n if (!knownToolCalls.has(update.toolCallId)) {\n yield* openToolCall(update)\n } else {\n const input = {\n ...(update.title != null && { title: update.title }),\n ...(typeof update.rawInput === 'object' && update.rawInput !== null\n ? (update.rawInput as Record<string, unknown>)\n : { input: update.rawInput }),\n }\n const args = JSON.stringify(input)\n yield {\n type: EventType.TOOL_CALL_ARGS,\n toolCallId: update.toolCallId,\n model,\n timestamp: now(),\n delta: args,\n args,\n }\n }\n }\n } else if (\n update.sessionUpdate === 'plan' &&\n labels.planEvent !== undefined\n ) {\n yield {\n type: EventType.CUSTOM,\n model,\n timestamp: now(),\n name: labels.planEvent,\n value: { entries: update.entries },\n }\n }\n }\n\n try {\n for await (const event of events) {\n ctx.onAcpEvent?.(event)\n\n if (event.kind === 'session') {\n yield* startRun()\n yield {\n type: EventType.CUSTOM,\n model,\n timestamp: now(),\n name: labels.sessionIdEvent,\n value: { sessionId: event.sessionId },\n }\n } else if (event.kind === 'update') {\n yield* startRun()\n yield* handleUpdate(event.update)\n } else {\n yield* startRun()\n yield* closeText()\n yield* closeReasoning()\n yield* synthesizeUnresolvedResults()\n\n if (event.stopReason === 'refusal') {\n const message =\n labels.refusalMessage ?? 'The harness refused the request.'\n yield {\n type: EventType.RUN_ERROR,\n model,\n timestamp: now(),\n message,\n code: 'refusal',\n error: { message, code: 'refusal' },\n }\n } else {\n const usage = buildUsage(event.usage)\n const finishReason =\n event.stopReason === 'max_tokens' ||\n event.stopReason === 'max_turn_requests'\n ? ('length' as const)\n : ('stop' as const)\n yield {\n type: EventType.RUN_FINISHED,\n runId,\n threadId,\n model,\n timestamp: now(),\n finishReason,\n ...(usage !== undefined && { usage }),\n }\n }\n }\n }\n } catch (error) {\n yield* closeText()\n yield* closeReasoning()\n yield* synthesizeUnresolvedResults()\n throw error\n }\n}\n"],"mappings":"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"}
|
|
@@ -101,14 +101,15 @@ function webSocketNdjsonToAcpStream(ws) {
|
|
|
101
101
|
ws.close();
|
|
102
102
|
}
|
|
103
103
|
});
|
|
104
|
-
|
|
104
|
+
const writable = new WritableStream({
|
|
105
105
|
write(chunk) {
|
|
106
106
|
ws.send(chunk);
|
|
107
107
|
},
|
|
108
108
|
close() {
|
|
109
109
|
ws.close();
|
|
110
110
|
}
|
|
111
|
-
})
|
|
111
|
+
});
|
|
112
|
+
return ndJsonStream(writable, readable);
|
|
112
113
|
}
|
|
113
114
|
/**
|
|
114
115
|
* Open a WebSocket to an in-sandbox ACP server and adapt it for
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"websocket.js","names":[],"sources":["../../../src/transport/websocket.ts"],"sourcesContent":["import { ndJsonStream } from '@agentclientprotocol/sdk'\nimport type { AcpJsonRpcStream, AcpMessageFraming } from './types'\n\nfunction waitForWebSocketOpen(\n ws: WebSocket,\n signal?: AbortSignal,\n): Promise<void> {\n if (ws.readyState === WebSocket.OPEN) return Promise.resolve()\n return new Promise((resolve, reject) => {\n const onOpen = (): void => {\n cleanup()\n resolve()\n }\n const onError = (): void => {\n cleanup()\n reject(new Error('WebSocket connection failed'))\n }\n const onAbort = (): void => {\n cleanup()\n ws.close()\n reject(signal?.reason ?? new Error('WebSocket connection aborted'))\n }\n const cleanup = (): void => {\n ws.removeEventListener('open', onOpen)\n ws.removeEventListener('error', onError)\n signal?.removeEventListener('abort', onAbort)\n }\n ws.addEventListener('open', onOpen)\n ws.addEventListener('error', onError)\n signal?.addEventListener('abort', onAbort, { once: true })\n })\n}\n\n/**\n * Wrap a stream controller so its terminal calls are idempotent. A WebSocket can\n * fire `close` after `error` (or after the reader cancels), and closing/erroring\n * an already-settled controller throws `ERR_INVALID_STATE` out of the listener.\n */\nfunction idempotentController<T>(\n controller: ReadableStreamDefaultController<T>,\n): {\n enqueue: (chunk: T) => void\n close: () => void\n error: (reason: unknown) => void\n} {\n let settled = false\n return {\n enqueue: (chunk) => {\n if (!settled) controller.enqueue(chunk)\n },\n close: () => {\n if (settled) return\n settled = true\n controller.close()\n },\n error: (reason) => {\n if (settled) return\n settled = true\n controller.error(reason)\n },\n }\n}\n\n/**\n * One JSON-RPC object per WebSocket text frame (e.g. `grok agent serve`).\n */\nexport function webSocketFrameToAcpStream(ws: WebSocket): AcpJsonRpcStream {\n const decoder = new TextDecoder()\n\n const readable = new ReadableStream<unknown>({\n start(rawController) {\n const controller = idempotentController(rawController)\n ws.addEventListener('message', (event) => {\n const text =\n typeof event.data === 'string'\n ? event.data\n : decoder.decode(event.data as ArrayBuffer)\n const trimmed = text.trim()\n if (trimmed === '') return\n try {\n controller.enqueue(JSON.parse(trimmed))\n } catch (error) {\n controller.error(\n error instanceof Error ? error : new Error(String(error)),\n )\n }\n })\n ws.addEventListener('close', () => controller.close())\n ws.addEventListener('error', () =>\n controller.error(new Error('WebSocket connection error')),\n )\n },\n cancel() {\n ws.close()\n },\n })\n\n const writable = new WritableStream({\n write(message) {\n ws.send(JSON.stringify(message))\n },\n close() {\n ws.close()\n },\n })\n\n return { readable, writable } as AcpJsonRpcStream\n}\n\nfunction webSocketNdjsonToAcpStream(ws: WebSocket): AcpJsonRpcStream {\n const encoder = new TextEncoder()\n const decoder = new TextDecoder()\n\n const readable = new ReadableStream<Uint8Array>({\n start(rawController) {\n const controller = idempotentController(rawController)\n ws.addEventListener('message', (event) => {\n const text =\n typeof event.data === 'string'\n ? event.data\n : decoder.decode(event.data as ArrayBuffer)\n controller.enqueue(encoder.encode(text))\n })\n ws.addEventListener('close', () => controller.close())\n ws.addEventListener('error', () =>\n controller.error(new Error('WebSocket connection error')),\n )\n },\n cancel() {\n ws.close()\n },\n })\n\n const writable = new WritableStream<Uint8Array>({\n write(chunk) {\n // TS 5.7+ types Uint8Array as generic over its buffer (ArrayBufferLike),\n // which no longer structurally matches the DOM `BufferSource` param.\n // Runtime accepts any typed array; narrow to the lib's expected type.\n ws.send(chunk as BufferSource)\n },\n close() {\n ws.close()\n },\n })\n\n return ndJsonStream(writable, readable)\n}\n\nexport interface ConnectAcpWebSocketOptions {\n headers?: Record<string, string>\n signal?: AbortSignal\n framing?: AcpMessageFraming\n}\n\nexport interface AcpWebSocketConnection {\n stream: AcpJsonRpcStream\n close: () => void\n}\n\n/**\n * Open a WebSocket to an in-sandbox ACP server and adapt it for\n * {@link ClientSideConnection}.\n */\nfunction openWebSocket(\n url: string,\n headers?: Record<string, string>,\n): WebSocket {\n if (headers === undefined) return new WebSocket(url)\n // Node/ws accepts `{ headers }`; DOM lib constructor types omit this overload.\n return Reflect.construct(WebSocket, [url, { headers }]) as WebSocket\n}\n\nexport async function connectAcpWebSocket(\n url: string,\n options: ConnectAcpWebSocketOptions = {},\n): Promise<AcpWebSocketConnection> {\n const ws = openWebSocket(url, options.headers)\n await waitForWebSocketOpen(ws, options.signal)\n\n const framing = options.framing ?? 'frame'\n const stream =\n framing === 'ndjson'\n ? webSocketNdjsonToAcpStream(ws)\n : webSocketFrameToAcpStream(ws)\n\n return {\n stream,\n close: () => ws.close(),\n }\n}\n\n/** Convert an HTTP sandbox channel URL to a WebSocket base URL. */\nexport function httpChannelUrlToWsBase(channelUrl: string): string {\n return channelUrl.replace(/^http/i, 'ws').replace(/\\/$/, '')\n}\n"],"mappings":";;AAGA,SAAS,qBACP,IACA,QACe;CACf,IAAI,GAAG,eAAe,UAAU,MAAM,OAAO,QAAQ,QAAQ;CAC7D,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,eAAqB;GACzB,QAAQ;GACR,QAAQ;EACV;EACA,MAAM,gBAAsB;GAC1B,QAAQ;GACR,uBAAO,IAAI,MAAM,6BAA6B,CAAC;EACjD;EACA,MAAM,gBAAsB;GAC1B,QAAQ;GACR,GAAG,MAAM;GACT,OAAO,QAAQ,0BAAU,IAAI,MAAM,8BAA8B,CAAC;EACpE;EACA,MAAM,gBAAsB;GAC1B,GAAG,oBAAoB,QAAQ,MAAM;GACrC,GAAG,oBAAoB,SAAS,OAAO;GACvC,QAAQ,oBAAoB,SAAS,OAAO;EAC9C;EACA,GAAG,iBAAiB,QAAQ,MAAM;EAClC,GAAG,iBAAiB,SAAS,OAAO;EACpC,QAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;CAC3D,CAAC;AACH;;;;;;AAOA,SAAS,qBACP,YAKA;CACA,IAAI,UAAU;CACd,OAAO;EACL,UAAU,UAAU;GAClB,IAAI,CAAC,SAAS,WAAW,QAAQ,KAAK;EACxC;EACA,aAAa;GACX,IAAI,SAAS;GACb,UAAU;GACV,WAAW,MAAM;EACnB;EACA,QAAQ,WAAW;GACjB,IAAI,SAAS;GACb,UAAU;GACV,WAAW,MAAM,MAAM;EACzB;CACF;AACF;;;;AAKA,SAAgB,0BAA0B,IAAiC;CACzE,MAAM,UAAU,IAAI,YAAY;CAuChC,OAAO;EAAE,UAAA,IArCY,eAAwB;GAC3C,MAAM,eAAe;IACnB,MAAM,aAAa,qBAAqB,aAAa;IACrD,GAAG,iBAAiB,YAAY,UAAU;KAKxC,MAAM,WAHJ,OAAO,MAAM,SAAS,WAClB,MAAM,OACN,QAAQ,OAAO,MAAM,IAAmB,EAAA,CACzB,KAAK;KAC1B,IAAI,YAAY,IAAI;KACpB,IAAI;MACF,WAAW,QAAQ,KAAK,MAAM,OAAO,CAAC;KACxC,SAAS,OAAO;MACd,WAAW,MACT,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC,CAC1D;KACF;IACF,CAAC;IACD,GAAG,iBAAiB,eAAe,WAAW,MAAM,CAAC;IACrD,GAAG,iBAAiB,eAClB,WAAW,sBAAM,IAAI,MAAM,4BAA4B,CAAC,CAC1D;GACF;GACA,SAAS;IACP,GAAG,MAAM;GACX;EACF,CAWS;EAAU,UAAA,IATE,eAAe;GAClC,MAAM,SAAS;IACb,GAAG,KAAK,KAAK,UAAU,OAAO,CAAC;GACjC;GACA,QAAQ;IACN,GAAG,MAAM;GACX;EACF,CAEmB;CAAS;AAC9B;AAEA,SAAS,2BAA2B,IAAiC;CACnE,MAAM,UAAU,IAAI,YAAY;CAChC,MAAM,UAAU,IAAI,YAAY;CAEhC,MAAM,WAAW,IAAI,eAA2B;EAC9C,MAAM,eAAe;GACnB,MAAM,aAAa,qBAAqB,aAAa;GACrD,GAAG,iBAAiB,YAAY,UAAU;IACxC,MAAM,OACJ,OAAO,MAAM,SAAS,WAClB,MAAM,OACN,QAAQ,OAAO,MAAM,IAAmB;IAC9C,WAAW,QAAQ,QAAQ,OAAO,IAAI,CAAC;GACzC,CAAC;GACD,GAAG,iBAAiB,eAAe,WAAW,MAAM,CAAC;GACrD,GAAG,iBAAiB,eAClB,WAAW,sBAAM,IAAI,MAAM,4BAA4B,CAAC,CAC1D;EACF;EACA,SAAS;GACP,GAAG,MAAM;EACX;CACF,CAAC;
|
|
1
|
+
{"version":3,"file":"websocket.js","names":[],"sources":["../../../src/transport/websocket.ts"],"sourcesContent":["import { ndJsonStream } from '@agentclientprotocol/sdk'\nimport type { AcpJsonRpcStream, AcpMessageFraming } from './types'\n\nfunction waitForWebSocketOpen(\n ws: WebSocket,\n signal?: AbortSignal,\n): Promise<void> {\n if (ws.readyState === WebSocket.OPEN) return Promise.resolve()\n return new Promise((resolve, reject) => {\n const onOpen = (): void => {\n cleanup()\n resolve()\n }\n const onError = (): void => {\n cleanup()\n reject(new Error('WebSocket connection failed'))\n }\n const onAbort = (): void => {\n cleanup()\n ws.close()\n reject(signal?.reason ?? new Error('WebSocket connection aborted'))\n }\n const cleanup = (): void => {\n ws.removeEventListener('open', onOpen)\n ws.removeEventListener('error', onError)\n signal?.removeEventListener('abort', onAbort)\n }\n ws.addEventListener('open', onOpen)\n ws.addEventListener('error', onError)\n signal?.addEventListener('abort', onAbort, { once: true })\n })\n}\n\n/**\n * Wrap a stream controller so its terminal calls are idempotent. A WebSocket can\n * fire `close` after `error` (or after the reader cancels), and closing/erroring\n * an already-settled controller throws `ERR_INVALID_STATE` out of the listener.\n */\nfunction idempotentController<T>(\n controller: ReadableStreamDefaultController<T>,\n): {\n enqueue: (chunk: T) => void\n close: () => void\n error: (reason: unknown) => void\n} {\n let settled = false\n return {\n enqueue: (chunk) => {\n if (!settled) controller.enqueue(chunk)\n },\n close: () => {\n if (settled) return\n settled = true\n controller.close()\n },\n error: (reason) => {\n if (settled) return\n settled = true\n controller.error(reason)\n },\n }\n}\n\n/**\n * One JSON-RPC object per WebSocket text frame (e.g. `grok agent serve`).\n */\nexport function webSocketFrameToAcpStream(ws: WebSocket): AcpJsonRpcStream {\n const decoder = new TextDecoder()\n\n const readable = new ReadableStream<unknown>({\n start(rawController) {\n const controller = idempotentController(rawController)\n ws.addEventListener('message', (event) => {\n const text =\n typeof event.data === 'string'\n ? event.data\n : decoder.decode(event.data as ArrayBuffer)\n const trimmed = text.trim()\n if (trimmed === '') return\n try {\n controller.enqueue(JSON.parse(trimmed))\n } catch (error) {\n controller.error(\n error instanceof Error ? error : new Error(String(error)),\n )\n }\n })\n ws.addEventListener('close', () => controller.close())\n ws.addEventListener('error', () =>\n controller.error(new Error('WebSocket connection error')),\n )\n },\n cancel() {\n ws.close()\n },\n })\n\n const writable = new WritableStream({\n write(message) {\n ws.send(JSON.stringify(message))\n },\n close() {\n ws.close()\n },\n })\n\n return { readable, writable } as AcpJsonRpcStream\n}\n\nfunction webSocketNdjsonToAcpStream(ws: WebSocket): AcpJsonRpcStream {\n const encoder = new TextEncoder()\n const decoder = new TextDecoder()\n\n const readable = new ReadableStream<Uint8Array>({\n start(rawController) {\n const controller = idempotentController(rawController)\n ws.addEventListener('message', (event) => {\n const text =\n typeof event.data === 'string'\n ? event.data\n : decoder.decode(event.data as ArrayBuffer)\n controller.enqueue(encoder.encode(text))\n })\n ws.addEventListener('close', () => controller.close())\n ws.addEventListener('error', () =>\n controller.error(new Error('WebSocket connection error')),\n )\n },\n cancel() {\n ws.close()\n },\n })\n\n const writable = new WritableStream<Uint8Array>({\n write(chunk) {\n // TS 5.7+ types Uint8Array as generic over its buffer (ArrayBufferLike),\n // which no longer structurally matches the DOM `BufferSource` param.\n // Runtime accepts any typed array; narrow to the lib's expected type.\n ws.send(chunk as BufferSource)\n },\n close() {\n ws.close()\n },\n })\n\n return ndJsonStream(writable, readable)\n}\n\nexport interface ConnectAcpWebSocketOptions {\n headers?: Record<string, string>\n signal?: AbortSignal\n framing?: AcpMessageFraming\n}\n\nexport interface AcpWebSocketConnection {\n stream: AcpJsonRpcStream\n close: () => void\n}\n\n/**\n * Open a WebSocket to an in-sandbox ACP server and adapt it for\n * {@link ClientSideConnection}.\n */\nfunction openWebSocket(\n url: string,\n headers?: Record<string, string>,\n): WebSocket {\n if (headers === undefined) return new WebSocket(url)\n // Node/ws accepts `{ headers }`; DOM lib constructor types omit this overload.\n return Reflect.construct(WebSocket, [url, { headers }]) as WebSocket\n}\n\nexport async function connectAcpWebSocket(\n url: string,\n options: ConnectAcpWebSocketOptions = {},\n): Promise<AcpWebSocketConnection> {\n const ws = openWebSocket(url, options.headers)\n await waitForWebSocketOpen(ws, options.signal)\n\n const framing = options.framing ?? 'frame'\n const stream =\n framing === 'ndjson'\n ? webSocketNdjsonToAcpStream(ws)\n : webSocketFrameToAcpStream(ws)\n\n return {\n stream,\n close: () => ws.close(),\n }\n}\n\n/** Convert an HTTP sandbox channel URL to a WebSocket base URL. */\nexport function httpChannelUrlToWsBase(channelUrl: string): string {\n return channelUrl.replace(/^http/i, 'ws').replace(/\\/$/, '')\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-acp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Shared Agent Client Protocol (ACP) transport, session, and AG-UI translation for TanStack AI harness adapters.",
|
|
5
5
|
"author": "",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,13 +36,13 @@
|
|
|
36
36
|
"@agentclientprotocol/sdk": "^0.25.0"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
|
-
"@tanstack/ai": "^0.
|
|
40
|
-
"@tanstack/ai-sandbox": "^0.3.
|
|
39
|
+
"@tanstack/ai": "^0.45.0",
|
|
40
|
+
"@tanstack/ai-sandbox": "^0.3.3"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@vitest/coverage-v8": "4.
|
|
44
|
-
"@tanstack/ai": "0.
|
|
45
|
-
"@tanstack/ai-sandbox": "0.3.
|
|
43
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
44
|
+
"@tanstack/ai": "0.45.0",
|
|
45
|
+
"@tanstack/ai-sandbox": "0.3.3",
|
|
46
46
|
"@tanstack/ai-sandbox-local-process": "0.2.1"
|
|
47
47
|
},
|
|
48
48
|
"scripts": {
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
"lint:fix": "oxlint src --type-aware --fix",
|
|
52
52
|
"test:build": "publint --strict",
|
|
53
53
|
"test:oxlint": "oxlint src --type-aware",
|
|
54
|
-
"test:lib": "vitest",
|
|
55
|
-
"test:lib:dev": "
|
|
54
|
+
"test:lib": "vitest run --testTimeout=30000",
|
|
55
|
+
"test:lib:dev": "vitest --watch",
|
|
56
56
|
"test:types": "tsc"
|
|
57
57
|
}
|
|
58
58
|
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { EventType, normalizeSystemPrompts } from '@tanstack/ai'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
appendOutputSchemaInstruction,
|
|
4
|
+
parseJsonFromAssistantText,
|
|
5
|
+
structuredOutputCompleteChunk,
|
|
6
|
+
structuredOutputStartChunk,
|
|
7
|
+
toRunErrorRawEvent,
|
|
8
|
+
} from '@tanstack/ai/adapter-internals'
|
|
3
9
|
import { BaseTextAdapter } from '@tanstack/ai/adapters'
|
|
4
10
|
import {
|
|
5
11
|
DurableAttachNotSupportedError,
|
|
@@ -133,10 +139,16 @@ export interface AcpCompatibleConfig<
|
|
|
133
139
|
skillsDir?: string
|
|
134
140
|
/** Extra environment variables for the harness process. */
|
|
135
141
|
env?: Record<string, string>
|
|
142
|
+
/**
|
|
143
|
+
* `'api-key'` (default) uses {@link authMethodId} (or `modelOptions.authMethodId`).
|
|
144
|
+
* `'host'` skips ACP authenticate (use the CLI login on the machine).
|
|
145
|
+
* Not inferred from the sandbox.
|
|
146
|
+
*/
|
|
147
|
+
authMode?: 'host' | 'api-key'
|
|
136
148
|
/**
|
|
137
149
|
* ACP auth method to select before the session starts, when the harness
|
|
138
150
|
* advertises one (e.g. `'pi-api-key'`). Overridable per call via
|
|
139
|
-
* `modelOptions.authMethodId`.
|
|
151
|
+
* `modelOptions.authMethodId`. Ignored when {@link authMode} is `'host'`.
|
|
140
152
|
*/
|
|
141
153
|
authMethodId?: string
|
|
142
154
|
/** ACP permission policy. Defaults to `'bypassPermissions'`. */
|
|
@@ -185,7 +197,13 @@ export interface AcpCompatibleProviderOptions {
|
|
|
185
197
|
sessionId?: string
|
|
186
198
|
/** Per-call override of the harness working directory. */
|
|
187
199
|
cwd?: string
|
|
188
|
-
/**
|
|
200
|
+
/**
|
|
201
|
+
* `'api-key'` (default) uses {@link authMethodId}.
|
|
202
|
+
* `'host'` skips ACP authenticate.
|
|
203
|
+
* Not inferred from the sandbox.
|
|
204
|
+
*/
|
|
205
|
+
authMode?: 'host' | 'api-key'
|
|
206
|
+
/** Per-call override of the ACP auth method. Ignored when authMode is `'host'`. */
|
|
189
207
|
authMethodId?: string
|
|
190
208
|
/** Per-call override of the ACP permission policy. */
|
|
191
209
|
permissionMode?: AcpPermissionMode
|
|
@@ -247,6 +265,14 @@ export class AcpCompatibleTextAdapter<
|
|
|
247
265
|
this.name = config.name
|
|
248
266
|
}
|
|
249
267
|
|
|
268
|
+
supportsCombinedToolsAndSchema(): boolean {
|
|
269
|
+
return true
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
combinedStructuredOutputSource(): 'event' {
|
|
273
|
+
return 'event'
|
|
274
|
+
}
|
|
275
|
+
|
|
250
276
|
private sandboxFrom(
|
|
251
277
|
options: TextOptions<ResolvedOptions<TModelOptions>>,
|
|
252
278
|
): SandboxHandle {
|
|
@@ -446,8 +472,12 @@ export class AcpCompatibleTextAdapter<
|
|
|
446
472
|
modelOptions?.permissionMode ??
|
|
447
473
|
this.harness.permissionMode ??
|
|
448
474
|
'bypassPermissions'
|
|
475
|
+
const authMode =
|
|
476
|
+
modelOptions?.authMode ?? this.harness.authMode ?? 'api-key'
|
|
449
477
|
const authMethodId =
|
|
450
|
-
|
|
478
|
+
authMode === 'host'
|
|
479
|
+
? undefined
|
|
480
|
+
: (modelOptions?.authMethodId ?? this.harness.authMethodId)
|
|
451
481
|
|
|
452
482
|
const approvalRequests: Array<StreamChunk> = []
|
|
453
483
|
const permissionHandler = this.makePermissionHandler({
|
|
@@ -510,12 +540,18 @@ export class AcpCompatibleTextAdapter<
|
|
|
510
540
|
const systemPrompts = normalizeSystemPrompts(options.systemPrompts)
|
|
511
541
|
.map((p) => p.content)
|
|
512
542
|
.filter((c) => c.trim() !== '')
|
|
513
|
-
|
|
543
|
+
let promptText = this.applySystemPrompts(
|
|
514
544
|
systemPrompts,
|
|
515
545
|
session.resumed || sessionId === undefined
|
|
516
546
|
? resumePrompt
|
|
517
547
|
: this.buildPrompt(options.messages, undefined).prompt,
|
|
518
548
|
)
|
|
549
|
+
if (options.outputSchema) {
|
|
550
|
+
promptText = appendOutputSchemaInstruction(
|
|
551
|
+
promptText,
|
|
552
|
+
options.outputSchema,
|
|
553
|
+
)
|
|
554
|
+
}
|
|
519
555
|
|
|
520
556
|
session
|
|
521
557
|
.prompt(promptText)
|
|
@@ -529,7 +565,11 @@ export class AcpCompatibleTextAdapter<
|
|
|
529
565
|
})
|
|
530
566
|
.catch((error: unknown) => queue.fail(error))
|
|
531
567
|
|
|
532
|
-
|
|
568
|
+
const wantsStructured = options.outputSchema !== undefined
|
|
569
|
+
let lastAssistantText = ''
|
|
570
|
+
let lastTextMessageId: string | undefined
|
|
571
|
+
let heldFinished: StreamChunk | undefined
|
|
572
|
+
for await (const chunk of mergeChunkStreams(
|
|
533
573
|
translateAcpStream(queue, {
|
|
534
574
|
model: this.model,
|
|
535
575
|
runId,
|
|
@@ -557,7 +597,36 @@ export class AcpCompatibleTextAdapter<
|
|
|
557
597
|
}),
|
|
558
598
|
}),
|
|
559
599
|
channel.stream,
|
|
560
|
-
)
|
|
600
|
+
)) {
|
|
601
|
+
if (wantsStructured && chunk.type === EventType.RUN_FINISHED) {
|
|
602
|
+
heldFinished = chunk
|
|
603
|
+
continue
|
|
604
|
+
}
|
|
605
|
+
if (wantsStructured) {
|
|
606
|
+
if (chunk.type === EventType.TEXT_MESSAGE_START) {
|
|
607
|
+
lastAssistantText = ''
|
|
608
|
+
if (typeof chunk.messageId === 'string' && chunk.messageId !== '') {
|
|
609
|
+
lastTextMessageId = chunk.messageId
|
|
610
|
+
}
|
|
611
|
+
} else if (
|
|
612
|
+
chunk.type === EventType.TEXT_MESSAGE_CONTENT &&
|
|
613
|
+
typeof chunk.delta === 'string'
|
|
614
|
+
) {
|
|
615
|
+
lastAssistantText += chunk.delta
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
yield chunk
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (options.outputSchema) {
|
|
622
|
+
yield* this.emitParsedStructuredOutput(
|
|
623
|
+
lastAssistantText,
|
|
624
|
+
threadId,
|
|
625
|
+
runId,
|
|
626
|
+
lastTextMessageId,
|
|
627
|
+
)
|
|
628
|
+
}
|
|
629
|
+
if (heldFinished) yield heldFinished
|
|
561
630
|
|
|
562
631
|
// Surface any pending approval requests (interactive ask-policy actions
|
|
563
632
|
// awaiting a client decision); the client approves and re-runs to continue.
|
|
@@ -641,13 +710,54 @@ export class AcpCompatibleTextAdapter<
|
|
|
641
710
|
}
|
|
642
711
|
}
|
|
643
712
|
|
|
713
|
+
private *emitParsedStructuredOutput(
|
|
714
|
+
raw: string,
|
|
715
|
+
threadId: string,
|
|
716
|
+
runId: string,
|
|
717
|
+
messageId = this.generateId(),
|
|
718
|
+
): Generator<StreamChunk> {
|
|
719
|
+
try {
|
|
720
|
+
const object = parseJsonFromAssistantText(raw)
|
|
721
|
+
yield structuredOutputStartChunk({
|
|
722
|
+
messageId,
|
|
723
|
+
model: this.model,
|
|
724
|
+
threadId,
|
|
725
|
+
runId,
|
|
726
|
+
})
|
|
727
|
+
yield structuredOutputCompleteChunk({
|
|
728
|
+
messageId,
|
|
729
|
+
model: this.model,
|
|
730
|
+
threadId,
|
|
731
|
+
runId,
|
|
732
|
+
object,
|
|
733
|
+
raw,
|
|
734
|
+
})
|
|
735
|
+
} catch (error: unknown) {
|
|
736
|
+
const parserMessage =
|
|
737
|
+
error instanceof Error
|
|
738
|
+
? error.message
|
|
739
|
+
: 'Failed to parse structured output'
|
|
740
|
+
const preview = raw.trim().slice(0, 200)
|
|
741
|
+
const message =
|
|
742
|
+
preview === '' ? parserMessage : `${parserMessage} Content: ${preview}`
|
|
743
|
+
yield {
|
|
744
|
+
type: EventType.RUN_ERROR,
|
|
745
|
+
model: this.model,
|
|
746
|
+
timestamp: Date.now(),
|
|
747
|
+
message,
|
|
748
|
+
code: 'structured-output-parse-failed',
|
|
749
|
+
error: { message, code: 'structured-output-parse-failed' },
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
644
754
|
structuredOutput(
|
|
645
755
|
_options: StructuredOutputOptions<ResolvedOptions<TModelOptions>>,
|
|
646
756
|
): Promise<StructuredOutputResult<unknown>> {
|
|
647
757
|
return Promise.reject(
|
|
648
758
|
new Error(
|
|
649
|
-
|
|
650
|
-
'
|
|
759
|
+
'This harness honors outputSchema on chat() in the same turn. ' +
|
|
760
|
+
'Pass outputSchema to chat(), or use a model adapter for a one-shot extract.',
|
|
651
761
|
),
|
|
652
762
|
)
|
|
653
763
|
}
|
|
@@ -17,7 +17,12 @@
|
|
|
17
17
|
* `fileSkill` and `instructions` are already written by the provider-agnostic
|
|
18
18
|
* bootstrap (into the workspace root + `AGENTS.md`), so they need no projection.
|
|
19
19
|
*/
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
discoverSkillDirs,
|
|
22
|
+
isSecretRef,
|
|
23
|
+
resolveGitSkillDir,
|
|
24
|
+
resolveHarnessCwd,
|
|
25
|
+
} from '@tanstack/ai-sandbox'
|
|
21
26
|
import type {
|
|
22
27
|
BearerRef,
|
|
23
28
|
SandboxHandle,
|
|
@@ -36,11 +41,6 @@ function shellQuote(value: string): string {
|
|
|
36
41
|
return `'${value.replace(/'/g, `'\\''`)}'`
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
function basenameOf(path: string): string {
|
|
40
|
-
const segments = path.split('/').filter((segment) => segment !== '')
|
|
41
|
-
return segments[segments.length - 1] ?? path
|
|
42
|
-
}
|
|
43
|
-
|
|
44
44
|
/**
|
|
45
45
|
* Make a sandbox path relative to the workspace root, so shell commands work on
|
|
46
46
|
* every provider. Only `fs.*` remaps the virtual `/workspace`; a raw `/workspace`
|
|
@@ -128,17 +128,25 @@ export async function projectAcpWorkspace(
|
|
|
128
128
|
// so the shell command resolves on every provider.
|
|
129
129
|
await handle.fs.mkdir(`${projection.root}/${skillsDir}`)
|
|
130
130
|
for (const skill of gitSkills) {
|
|
131
|
+
// Keep virtual `/workspace` paths for fs discovery. handle.fs remaps
|
|
132
|
+
// them. Remap only when building shell-relative paths so Daytona and
|
|
133
|
+
// local-process both resolve the same clone.
|
|
131
134
|
const source = skill.into ?? resolveGitSkillDir(projection.root, skill)
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
135
|
+
const discovered = await discoverSkillDirs(handle, source)
|
|
136
|
+
for (const { name, dir } of discovered) {
|
|
137
|
+
const realRoot = resolveHarnessCwd(handle, projection.root)
|
|
138
|
+
const realDir = resolveHarnessCwd(handle, dir)
|
|
139
|
+
const relSource = relativeToRoot(realRoot, realDir)
|
|
140
|
+
const relTarget = `${skillsDir}/${name}`
|
|
141
|
+
const cp = await handle.process.exec(
|
|
142
|
+
`cp -r ${shellQuote(relSource)} ${shellQuote(relTarget)}`,
|
|
143
|
+
{ cwd: projection.root },
|
|
141
144
|
)
|
|
145
|
+
if (cp.exitCode !== 0) {
|
|
146
|
+
console.warn(
|
|
147
|
+
`[${harnessName}] failed to copy gitSkill "${skill.repo}" into ${relTarget}: ${cp.stderr.trim()}`,
|
|
148
|
+
)
|
|
149
|
+
}
|
|
142
150
|
}
|
|
143
151
|
}
|
|
144
152
|
}
|