@tanstack/ai-acp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +428 -0
  2. package/dist/esm/adapters/compatible.d.ts +206 -0
  3. package/dist/esm/adapters/compatible.js +313 -0
  4. package/dist/esm/adapters/compatible.js.map +1 -0
  5. package/dist/esm/adapters/projection.d.ts +29 -0
  6. package/dist/esm/adapters/projection.js +83 -0
  7. package/dist/esm/adapters/projection.js.map +1 -0
  8. package/dist/esm/index.d.ts +21 -0
  9. package/dist/esm/index.js +35 -0
  10. package/dist/esm/index.js.map +1 -0
  11. package/dist/esm/messages/prompt.d.ts +19 -0
  12. package/dist/esm/messages/prompt.js +37 -0
  13. package/dist/esm/messages/prompt.js.map +1 -0
  14. package/dist/esm/permissions.d.ts +8 -0
  15. package/dist/esm/permissions.js +44 -0
  16. package/dist/esm/permissions.js.map +1 -0
  17. package/dist/esm/session/acp-client.d.ts +37 -0
  18. package/dist/esm/session/acp-client.js +140 -0
  19. package/dist/esm/session/acp-client.js.map +1 -0
  20. package/dist/esm/session/sandbox-server.d.ts +46 -0
  21. package/dist/esm/session/sandbox-server.js +114 -0
  22. package/dist/esm/session/sandbox-server.js.map +1 -0
  23. package/dist/esm/stream/queue.d.ts +15 -0
  24. package/dist/esm/stream/queue.js +54 -0
  25. package/dist/esm/stream/queue.js.map +1 -0
  26. package/dist/esm/stream/translate.d.ts +38 -0
  27. package/dist/esm/stream/translate.js +320 -0
  28. package/dist/esm/stream/translate.js.map +1 -0
  29. package/dist/esm/transport/resolve.d.ts +7 -0
  30. package/dist/esm/transport/resolve.js +34 -0
  31. package/dist/esm/transport/resolve.js.map +1 -0
  32. package/dist/esm/transport/stdio.d.ts +3 -0
  33. package/dist/esm/transport/stdio.js +51 -0
  34. package/dist/esm/transport/stdio.js.map +1 -0
  35. package/dist/esm/transport/types.d.ts +29 -0
  36. package/dist/esm/transport/websocket.d.ts +17 -0
  37. package/dist/esm/transport/websocket.js +135 -0
  38. package/dist/esm/transport/websocket.js.map +1 -0
  39. package/dist/esm/types/acp-types.d.ts +75 -0
  40. package/package.json +61 -0
  41. package/src/adapters/compatible.ts +675 -0
  42. package/src/adapters/projection.ts +164 -0
  43. package/src/index.ts +85 -0
  44. package/src/messages/prompt.ts +71 -0
  45. package/src/permissions.ts +71 -0
  46. package/src/session/acp-client.ts +238 -0
  47. package/src/session/sandbox-server.ts +195 -0
  48. package/src/stream/queue.ts +61 -0
  49. package/src/stream/translate.ts +412 -0
  50. package/src/transport/resolve.ts +46 -0
  51. package/src/transport/stdio.ts +63 -0
  52. package/src/transport/types.ts +33 -0
  53. package/src/transport/websocket.ts +192 -0
  54. package/src/types/acp-types.ts +79 -0
@@ -0,0 +1,8 @@
1
+ import { AcpPermissionMode, AcpPermissionOutcome, AcpPermissionRequest, PermissionHandler } from './types/acp-types.js';
2
+ export type { AcpPermissionMode, PermissionHandler };
3
+ export declare function resolvePermission(request: AcpPermissionRequest, mode: AcpPermissionMode, bridgedToolNames: ReadonlySet<string> | undefined): AcpPermissionOutcome;
4
+ export declare function resolveInteractivePermission(request: AcpPermissionRequest, mode: AcpPermissionMode, bridgedToolNames: ReadonlySet<string> | undefined, approvals: ReadonlyMap<string, boolean> | undefined, provider: string): {
5
+ outcome: AcpPermissionOutcome;
6
+ approvalId?: string;
7
+ title?: string;
8
+ };
@@ -0,0 +1,44 @@
1
+ import { approvalId } from "@tanstack/ai-sandbox";
2
+ import { matchBridgedToolName } from "./stream/translate.js";
3
+ const EDIT_KINDS = /* @__PURE__ */ new Set(["edit", "move", "delete"]);
4
+ function pickOption(request, kinds) {
5
+ for (const kind of kinds) {
6
+ const option = request.options.find((candidate) => candidate.kind === kind);
7
+ if (option) return { outcome: "selected", optionId: option.optionId };
8
+ }
9
+ return { outcome: "cancelled" };
10
+ }
11
+ function resolvePermission(request, mode, bridgedToolNames) {
12
+ const allow = () => pickOption(request, ["allow_once", "allow_always"]);
13
+ const reject = () => pickOption(request, ["reject_once", "reject_always"]);
14
+ if (matchBridgedToolName(request.toolCall.title, bridgedToolNames) !== void 0) {
15
+ return allow();
16
+ }
17
+ if (mode === "bypassPermissions") return allow();
18
+ if (mode === "acceptEdits" && EDIT_KINDS.has(request.toolCall.kind ?? "")) {
19
+ return allow();
20
+ }
21
+ return reject();
22
+ }
23
+ function resolveInteractivePermission(request, mode, bridgedToolNames, approvals, provider) {
24
+ const allow = () => pickOption(request, ["allow_once", "allow_always"]);
25
+ const reject = () => pickOption(request, ["reject_once", "reject_always"]);
26
+ const title = request.toolCall.title ?? request.toolCall.toolCallId;
27
+ if (matchBridgedToolName(title, bridgedToolNames) !== void 0) {
28
+ return { outcome: allow() };
29
+ }
30
+ if (mode === "bypassPermissions") return { outcome: allow() };
31
+ if (mode === "acceptEdits" && EDIT_KINDS.has(request.toolCall.kind ?? "")) {
32
+ return { outcome: allow() };
33
+ }
34
+ const id = approvalId({ provider, kind: "tool", target: title });
35
+ const granted = approvals?.get(id);
36
+ if (granted === true) return { outcome: allow() };
37
+ if (granted === false) return { outcome: reject() };
38
+ return { outcome: reject(), approvalId: id, title };
39
+ }
40
+ export {
41
+ resolveInteractivePermission,
42
+ resolvePermission
43
+ };
44
+ //# sourceMappingURL=permissions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permissions.js","sources":["../../src/permissions.ts"],"sourcesContent":["import { approvalId } from '@tanstack/ai-sandbox'\nimport { matchBridgedToolName } from './stream/translate'\nimport type {\n AcpPermissionMode,\n AcpPermissionOutcome,\n AcpPermissionRequest,\n PermissionHandler,\n} from './types/acp-types'\n\nexport type { AcpPermissionMode, PermissionHandler }\n\nconst EDIT_KINDS = new Set(['edit', 'move', 'delete'])\n\nfunction pickOption(\n request: AcpPermissionRequest,\n kinds: Array<string>,\n): AcpPermissionOutcome {\n for (const kind of kinds) {\n const option = request.options.find((candidate) => candidate.kind === kind)\n if (option) return { outcome: 'selected', optionId: option.optionId }\n }\n return { outcome: 'cancelled' }\n}\n\nexport function resolvePermission(\n request: AcpPermissionRequest,\n mode: AcpPermissionMode,\n bridgedToolNames: ReadonlySet<string> | undefined,\n): AcpPermissionOutcome {\n const allow = () => pickOption(request, ['allow_once', 'allow_always'])\n const reject = () => pickOption(request, ['reject_once', 'reject_always'])\n\n if (\n matchBridgedToolName(request.toolCall.title, bridgedToolNames) !== undefined\n ) {\n return allow()\n }\n if (mode === 'bypassPermissions') return allow()\n if (mode === 'acceptEdits' && EDIT_KINDS.has(request.toolCall.kind ?? '')) {\n return allow()\n }\n return reject()\n}\n\nexport function resolveInteractivePermission(\n request: AcpPermissionRequest,\n mode: AcpPermissionMode,\n bridgedToolNames: ReadonlySet<string> | undefined,\n approvals: ReadonlyMap<string, boolean> | undefined,\n provider: string,\n): { outcome: AcpPermissionOutcome; approvalId?: string; title?: string } {\n const allow = (): AcpPermissionOutcome =>\n pickOption(request, ['allow_once', 'allow_always'])\n const reject = (): AcpPermissionOutcome =>\n pickOption(request, ['reject_once', 'reject_always'])\n const title = request.toolCall.title ?? request.toolCall.toolCallId\n\n if (matchBridgedToolName(title, bridgedToolNames) !== undefined) {\n return { outcome: allow() }\n }\n if (mode === 'bypassPermissions') return { outcome: allow() }\n if (mode === 'acceptEdits' && EDIT_KINDS.has(request.toolCall.kind ?? '')) {\n return { outcome: allow() }\n }\n\n const id = approvalId({ provider, kind: 'tool', target: title })\n const granted = approvals?.get(id)\n if (granted === true) return { outcome: allow() }\n if (granted === false) return { outcome: reject() }\n return { outcome: reject(), approvalId: id, title }\n}\n"],"names":[],"mappings":";;AAWA,MAAM,aAAa,oBAAI,IAAI,CAAC,QAAQ,QAAQ,QAAQ,CAAC;AAErD,SAAS,WACP,SACA,OACsB;AACtB,aAAW,QAAQ,OAAO;AACxB,UAAM,SAAS,QAAQ,QAAQ,KAAK,CAAC,cAAc,UAAU,SAAS,IAAI;AAC1E,QAAI,OAAQ,QAAO,EAAE,SAAS,YAAY,UAAU,OAAO,SAAA;AAAA,EAC7D;AACA,SAAO,EAAE,SAAS,YAAA;AACpB;AAEO,SAAS,kBACd,SACA,MACA,kBACsB;AACtB,QAAM,QAAQ,MAAM,WAAW,SAAS,CAAC,cAAc,cAAc,CAAC;AACtE,QAAM,SAAS,MAAM,WAAW,SAAS,CAAC,eAAe,eAAe,CAAC;AAEzE,MACE,qBAAqB,QAAQ,SAAS,OAAO,gBAAgB,MAAM,QACnE;AACA,WAAO,MAAA;AAAA,EACT;AACA,MAAI,SAAS,oBAAqB,QAAO,MAAA;AACzC,MAAI,SAAS,iBAAiB,WAAW,IAAI,QAAQ,SAAS,QAAQ,EAAE,GAAG;AACzE,WAAO,MAAA;AAAA,EACT;AACA,SAAO,OAAA;AACT;AAEO,SAAS,6BACd,SACA,MACA,kBACA,WACA,UACwE;AACxE,QAAM,QAAQ,MACZ,WAAW,SAAS,CAAC,cAAc,cAAc,CAAC;AACpD,QAAM,SAAS,MACb,WAAW,SAAS,CAAC,eAAe,eAAe,CAAC;AACtD,QAAM,QAAQ,QAAQ,SAAS,SAAS,QAAQ,SAAS;AAEzD,MAAI,qBAAqB,OAAO,gBAAgB,MAAM,QAAW;AAC/D,WAAO,EAAE,SAAS,QAAM;AAAA,EAC1B;AACA,MAAI,SAAS,oBAAqB,QAAO,EAAE,SAAS,QAAM;AAC1D,MAAI,SAAS,iBAAiB,WAAW,IAAI,QAAQ,SAAS,QAAQ,EAAE,GAAG;AACzE,WAAO,EAAE,SAAS,QAAM;AAAA,EAC1B;AAEA,QAAM,KAAK,WAAW,EAAE,UAAU,MAAM,QAAQ,QAAQ,OAAO;AAC/D,QAAM,UAAU,WAAW,IAAI,EAAE;AACjC,MAAI,YAAY,KAAM,QAAO,EAAE,SAAS,QAAM;AAC9C,MAAI,YAAY,MAAO,QAAO,EAAE,SAAS,SAAO;AAChD,SAAO,EAAE,SAAS,OAAA,GAAU,YAAY,IAAI,MAAA;AAC9C;"}
@@ -0,0 +1,37 @@
1
+ import { AcpPermissionOutcome, AcpPermissionRequest, AcpSessionUpdate, AcpStopReason, AcpUsage } from '../types/acp-types.js';
2
+ import { AcpSessionTransport } from '../transport/types.js';
3
+ export interface AcpSessionHandle {
4
+ sessionId: string;
5
+ resumed: boolean;
6
+ prompt: (text: string) => Promise<{
7
+ stopReason: AcpStopReason;
8
+ usage?: AcpUsage;
9
+ }>;
10
+ cancel: () => Promise<void>;
11
+ dispose: () => Promise<void>;
12
+ }
13
+ export interface StartAcpSessionOptions {
14
+ transport: AcpSessionTransport;
15
+ cwd: string;
16
+ authMethodId?: string;
17
+ mcpServers?: Array<{
18
+ name: string;
19
+ url: string;
20
+ headers?: Array<{
21
+ name: string;
22
+ value: string;
23
+ }>;
24
+ }>;
25
+ resumeSessionId?: string;
26
+ onUpdate: (update: AcpSessionUpdate) => void;
27
+ onPermissionRequest: (request: AcpPermissionRequest) => Promise<AcpPermissionOutcome> | AcpPermissionOutcome;
28
+ /**
29
+ * Harness-specific JSON-RPC notifications (e.g. Grok `_x.ai/session_notification`).
30
+ * Return without throwing — unknown vendor extensions must not tear down the session.
31
+ */
32
+ onExtNotification?: (method: string, params: Record<string, unknown>) => void;
33
+ }
34
+ /**
35
+ * Drive an ACP harness over stdio or a pre-connected JSON-RPC stream.
36
+ */
37
+ export declare function startAcpSession(options: StartAcpSessionOptions): Promise<AcpSessionHandle>;
@@ -0,0 +1,140 @@
1
+ import { ClientSideConnection, PROTOCOL_VERSION, ndJsonStream } from "@agentclientprotocol/sdk";
2
+ import { spawnHandleToAcpTransport } from "../transport/stdio.js";
3
+ const CLIENT_INFO = {
4
+ name: "@tanstack/ai-acp",
5
+ title: "TanStack AI",
6
+ version: "0.1.0"
7
+ };
8
+ function streamFromTransport(transport) {
9
+ if (transport.kind === "stdio") {
10
+ const byteTransport = spawnHandleToAcpTransport(transport.process);
11
+ return {
12
+ stream: ndJsonStream(byteTransport.writable, byteTransport.readable),
13
+ teardown: () => byteTransport.kill(),
14
+ exited: byteTransport.exited,
15
+ stderrTail: byteTransport.stderrTail
16
+ };
17
+ }
18
+ return {
19
+ stream: transport.stream,
20
+ teardown: transport.dispose,
21
+ exited: void 0,
22
+ stderrTail: transport.stderrTail ?? (() => "")
23
+ };
24
+ }
25
+ async function startAcpSession(options) {
26
+ const { stream, teardown, exited, stderrTail } = streamFromTransport(
27
+ options.transport
28
+ );
29
+ let replaying = false;
30
+ const client = {
31
+ requestPermission: async (params) => {
32
+ const outcome = await options.onPermissionRequest(params);
33
+ return { outcome };
34
+ },
35
+ sessionUpdate: (params) => {
36
+ if (!replaying) {
37
+ options.onUpdate(params.update);
38
+ }
39
+ return Promise.resolve();
40
+ },
41
+ extNotification: (method, params) => {
42
+ options.onExtNotification?.(method, params);
43
+ return Promise.resolve();
44
+ }
45
+ };
46
+ const race = (work) => exited !== void 0 ? Promise.race([work, exited]) : work;
47
+ try {
48
+ const connection = new ClientSideConnection(() => client, stream);
49
+ const initResult = await race(
50
+ connection.initialize({
51
+ protocolVersion: PROTOCOL_VERSION,
52
+ clientInfo: CLIENT_INFO,
53
+ clientCapabilities: {
54
+ // The agent runs inside the sandbox with direct filesystem + shell
55
+ // access, so it never needs to delegate file/terminal I/O back to the
56
+ // client. We advertise these as unsupported; per the ACP spec the
57
+ // agent MUST then treat them as unavailable and not call them.
58
+ fs: { readTextFile: false, writeTextFile: false }
59
+ }
60
+ })
61
+ );
62
+ if (typeof initResult.protocolVersion === "number" && initResult.protocolVersion > PROTOCOL_VERSION) {
63
+ throw new Error(
64
+ `ACP agent negotiated protocol version ${initResult.protocolVersion}, but this client supports up to ${PROTOCOL_VERSION}. Update @tanstack/ai-acp.`
65
+ );
66
+ }
67
+ if (options.authMethodId !== void 0) {
68
+ const available = initResult.authMethods ?? [];
69
+ if (!available.some((method) => method.id === options.authMethodId)) {
70
+ throw new Error(
71
+ `Harness does not advertise the ACP auth method '${options.authMethodId}'. Available: ${available.map((method) => method.id).join(", ") || "(none)"}.`
72
+ );
73
+ }
74
+ await race(connection.authenticate({ methodId: options.authMethodId }));
75
+ }
76
+ const mcpServers = (options.mcpServers ?? []).map(
77
+ (server) => ({
78
+ type: "http",
79
+ name: server.name,
80
+ url: server.url,
81
+ headers: server.headers ?? []
82
+ })
83
+ );
84
+ let sessionId;
85
+ let resumed = false;
86
+ if (options.resumeSessionId !== void 0 && initResult.agentCapabilities?.loadSession === true) {
87
+ replaying = true;
88
+ try {
89
+ await race(
90
+ connection.loadSession({
91
+ sessionId: options.resumeSessionId,
92
+ cwd: options.cwd,
93
+ mcpServers
94
+ })
95
+ );
96
+ sessionId = options.resumeSessionId;
97
+ resumed = true;
98
+ } catch {
99
+ } finally {
100
+ replaying = false;
101
+ }
102
+ }
103
+ if (sessionId === void 0) {
104
+ const session = await race(
105
+ connection.newSession({ cwd: options.cwd, mcpServers })
106
+ );
107
+ sessionId = session.sessionId;
108
+ }
109
+ return {
110
+ sessionId,
111
+ resumed,
112
+ prompt: async (text) => {
113
+ const response = await race(
114
+ connection.prompt({
115
+ sessionId,
116
+ prompt: [{ type: "text", text }]
117
+ })
118
+ );
119
+ return {
120
+ stopReason: response.stopReason,
121
+ ...response.usage != null && { usage: response.usage }
122
+ };
123
+ },
124
+ cancel: () => connection.cancel({ sessionId }),
125
+ dispose: teardown
126
+ };
127
+ } catch (error) {
128
+ await teardown();
129
+ const tail = stderrTail().trim();
130
+ if (error instanceof Error && tail !== "" && !error.message.includes(tail)) {
131
+ throw new Error(`${error.message}
132
+ stderr: ${tail}`, { cause: error });
133
+ }
134
+ throw error;
135
+ }
136
+ }
137
+ export {
138
+ startAcpSession
139
+ };
140
+ //# sourceMappingURL=acp-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"acp-client.js","sources":["../../../src/session/acp-client.ts"],"sourcesContent":["import {\n ClientSideConnection,\n PROTOCOL_VERSION,\n ndJsonStream,\n} from '@agentclientprotocol/sdk'\nimport { spawnHandleToAcpTransport } from '../transport/stdio'\nimport type {\n Client,\n McpServer,\n RequestPermissionRequest,\n RequestPermissionResponse,\n SessionNotification,\n} from '@agentclientprotocol/sdk'\nimport type {\n AcpPermissionOutcome,\n AcpPermissionRequest,\n AcpSessionUpdate,\n AcpStopReason,\n AcpUsage,\n} from '../types/acp-types'\nimport type { AcpJsonRpcStream, AcpSessionTransport } from '../transport/types'\n\n/**\n * Identifies this client to the agent in the ACP `initialize` handshake. The\n * `version` is informational (sent in `clientInfo`); keep it loosely in step\n * with the package version. `clientInfo` is currently optional in the protocol\n * but will become required, so we always send it.\n * ponytail: hardcoded — clientInfo is diagnostic; a stale patch version is harmless.\n */\nconst CLIENT_INFO = {\n name: '@tanstack/ai-acp',\n title: 'TanStack AI',\n version: '0.1.0',\n} as const\n\nexport interface AcpSessionHandle {\n sessionId: string\n resumed: boolean\n prompt: (\n text: string,\n ) => Promise<{ stopReason: AcpStopReason; usage?: AcpUsage }>\n cancel: () => Promise<void>\n dispose: () => Promise<void>\n}\n\nexport interface StartAcpSessionOptions {\n transport: AcpSessionTransport\n cwd: string\n authMethodId?: string\n mcpServers?: Array<{\n name: string\n url: string\n headers?: Array<{ name: string; value: string }>\n }>\n resumeSessionId?: string\n onUpdate: (update: AcpSessionUpdate) => void\n onPermissionRequest: (\n request: AcpPermissionRequest,\n ) => Promise<AcpPermissionOutcome> | AcpPermissionOutcome\n /**\n * Harness-specific JSON-RPC notifications (e.g. Grok `_x.ai/session_notification`).\n * Return without throwing — unknown vendor extensions must not tear down the session.\n */\n onExtNotification?: (method: string, params: Record<string, unknown>) => void\n}\n\nfunction streamFromTransport(transport: AcpSessionTransport): {\n stream: AcpJsonRpcStream\n teardown: () => Promise<void>\n exited: Promise<never> | undefined\n stderrTail: () => string\n} {\n if (transport.kind === 'stdio') {\n const byteTransport = spawnHandleToAcpTransport(transport.process)\n return {\n stream: ndJsonStream(byteTransport.writable, byteTransport.readable),\n teardown: () => byteTransport.kill(),\n exited: byteTransport.exited,\n stderrTail: byteTransport.stderrTail,\n }\n }\n\n return {\n stream: transport.stream,\n teardown: transport.dispose,\n exited: undefined,\n stderrTail: transport.stderrTail ?? (() => ''),\n }\n}\n\n/**\n * Drive an ACP harness over stdio or a pre-connected JSON-RPC stream.\n */\nexport async function startAcpSession(\n options: StartAcpSessionOptions,\n): Promise<AcpSessionHandle> {\n const { stream, teardown, exited, stderrTail } = streamFromTransport(\n options.transport,\n )\n\n let replaying = false\n\n const client: Client = {\n requestPermission: async (\n params: RequestPermissionRequest,\n ): Promise<RequestPermissionResponse> => {\n const outcome = await options.onPermissionRequest(params)\n return { outcome }\n },\n sessionUpdate: (params: SessionNotification): Promise<void> => {\n if (!replaying) {\n options.onUpdate(params.update as AcpSessionUpdate)\n }\n return Promise.resolve()\n },\n extNotification: (method, params) => {\n options.onExtNotification?.(method, params)\n return Promise.resolve()\n },\n }\n\n const race = <T>(work: Promise<T>): Promise<T> =>\n exited !== undefined ? Promise.race([work, exited]) : work\n\n try {\n const connection = new ClientSideConnection(() => client, stream)\n\n const initResult = await race(\n connection.initialize({\n protocolVersion: PROTOCOL_VERSION,\n clientInfo: CLIENT_INFO,\n clientCapabilities: {\n // The agent runs inside the sandbox with direct filesystem + shell\n // access, so it never needs to delegate file/terminal I/O back to the\n // client. We advertise these as unsupported; per the ACP spec the\n // agent MUST then treat them as unavailable and not call them.\n fs: { readTextFile: false, writeTextFile: false },\n },\n }),\n )\n\n // Protocol-version negotiation: the agent echoes the version it will speak.\n // The spec says a client SHOULD close the connection if that version is one\n // it doesn't support. We only implement the current PROTOCOL_VERSION, so a\n // higher number means the agent needs a newer client than this one.\n if (\n typeof initResult.protocolVersion === 'number' &&\n initResult.protocolVersion > PROTOCOL_VERSION\n ) {\n throw new Error(\n `ACP agent negotiated protocol version ${initResult.protocolVersion}, ` +\n `but this client supports up to ${PROTOCOL_VERSION}. Update @tanstack/ai-acp.`,\n )\n }\n\n if (options.authMethodId !== undefined) {\n const available = initResult.authMethods ?? []\n if (!available.some((method) => method.id === options.authMethodId)) {\n throw new Error(\n `Harness does not advertise the ACP auth method '${options.authMethodId}'. Available: ${\n available.map((method) => method.id).join(', ') || '(none)'\n }.`,\n )\n }\n await race(connection.authenticate({ methodId: options.authMethodId }))\n }\n\n const mcpServers: Array<McpServer> = (options.mcpServers ?? []).map(\n (server) => ({\n type: 'http' as const,\n name: server.name,\n url: server.url,\n headers: server.headers ?? [],\n }),\n )\n\n let sessionId: string | undefined\n let resumed = false\n if (\n options.resumeSessionId !== undefined &&\n initResult.agentCapabilities?.loadSession === true\n ) {\n replaying = true\n try {\n await race(\n connection.loadSession({\n sessionId: options.resumeSessionId,\n cwd: options.cwd,\n mcpServers,\n }),\n )\n sessionId = options.resumeSessionId\n resumed = true\n } catch {\n // Session unknown — fall through to a fresh one.\n } finally {\n replaying = false\n }\n }\n\n if (sessionId === undefined) {\n const session = await race(\n connection.newSession({ cwd: options.cwd, mcpServers }),\n )\n sessionId = session.sessionId\n }\n\n return {\n sessionId,\n resumed,\n prompt: async (text: string) => {\n const response = await race(\n connection.prompt({\n sessionId,\n prompt: [{ type: 'text', text }],\n }),\n )\n return {\n stopReason: response.stopReason,\n ...(response.usage != null && { usage: response.usage }),\n }\n },\n cancel: () => connection.cancel({ sessionId }),\n dispose: teardown,\n }\n } catch (error) {\n await teardown()\n const tail = stderrTail().trim()\n if (\n error instanceof Error &&\n tail !== '' &&\n !error.message.includes(tail)\n ) {\n throw new Error(`${error.message}\\nstderr: ${tail}`, { cause: error })\n }\n throw error\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ import { SandboxChannel, SandboxHandle } from '@tanstack/ai-sandbox';
2
+ import { AcpMessageFraming } from '../transport/types.js';
3
+ import { AcpWebSocketConnection } from '../transport/websocket.js';
4
+ export interface AcpSandboxServer {
5
+ /** WebSocket URL the orchestrator uses to reach the in-sandbox ACP server. */
6
+ wsUrl: string;
7
+ /** Sandbox channel used to build {@link wsUrl} (auth headers, when issued). */
8
+ channel: SandboxChannel;
9
+ /** Open the ACP JSON-RPC stream over WebSocket. */
10
+ connect: (signal?: AbortSignal) => Promise<AcpWebSocketConnection>;
11
+ /** Stop the in-sandbox server process. */
12
+ dispose: () => Promise<void>;
13
+ }
14
+ export interface StartAcpServerOptions {
15
+ port: number;
16
+ cwd: string;
17
+ /** Full shell command that starts the harness ACP server inside the sandbox. */
18
+ command: string;
19
+ /** Build the WebSocket URL once the server is ready and the port is exposed. */
20
+ buildWsUrl: (input: {
21
+ channel: SandboxChannel;
22
+ port: number;
23
+ stdout: string;
24
+ }) => string;
25
+ /**
26
+ * Return true once {@link buildWsUrl} can be called. Receives accumulated
27
+ * stdout from the server process.
28
+ */
29
+ isReady?: (stdout: string) => boolean;
30
+ /** Substring/regex marker used when {@link isReady} is omitted. */
31
+ readyMarker?: string;
32
+ env?: Record<string, string>;
33
+ timeoutMs?: number;
34
+ signal?: AbortSignal;
35
+ framing?: AcpMessageFraming;
36
+ hostname?: string;
37
+ }
38
+ /**
39
+ * Boot a harness ACP WebSocket server inside a sandbox and expose its port via
40
+ * {@link SandboxHandle.ports.connect}. Mirrors the `opencode serve` pattern.
41
+ */
42
+ export declare function startAcpServerInSandbox(sandbox: SandboxHandle, options: StartAcpServerOptions): Promise<AcpSandboxServer>;
43
+ /** Parse a `WebSocket URL: ws://…` line printed by `grok agent serve`. */
44
+ export declare function parseWebSocketUrlFromServeOutput(output: string): string | undefined;
45
+ /** Build a Grok `agent serve` WebSocket URL from a sandbox channel + secret. */
46
+ export declare function buildGrokServeWebSocketUrl(channelUrl: string, secret: string): string;
@@ -0,0 +1,114 @@
1
+ import { connectAcpWebSocket } from "../transport/websocket.js";
2
+ const DEFAULT_READY_MARKER = "WebSocket URL:";
3
+ function waitForReady(proc, options) {
4
+ const readyMarker = options.readyMarker ?? DEFAULT_READY_MARKER;
5
+ const isReady = options.isReady ?? ((output) => output.includes(readyMarker));
6
+ return new Promise((resolve, reject) => {
7
+ let stdout = "";
8
+ let stderr = "";
9
+ const state = { settled: false };
10
+ const settle = (fn) => {
11
+ if (state.settled) return;
12
+ state.settled = true;
13
+ clearTimeout(timer);
14
+ fn();
15
+ };
16
+ const diagnostics = () => [stdout, stderr].map((s) => s.trim()).filter(Boolean).join("\n").slice(-500);
17
+ const combined = () => stdout + stderr;
18
+ let stdoutDone = false;
19
+ let stderrDone = false;
20
+ const tryFinish = () => {
21
+ if (state.settled) return;
22
+ if (isReady(combined())) {
23
+ settle(() => resolve({ stdout, stderr }));
24
+ return;
25
+ }
26
+ if (stdoutDone && stderrDone) {
27
+ settle(
28
+ () => reject(
29
+ new Error(
30
+ `ACP server exited before becoming ready${diagnostics() ? `: ${diagnostics()}` : " (no output)"}`
31
+ )
32
+ )
33
+ );
34
+ }
35
+ };
36
+ const timer = setTimeout(
37
+ () => settle(
38
+ () => reject(
39
+ new Error(
40
+ `ACP server did not become ready within ${options.timeoutMs ?? 3e4}ms${diagnostics() ? `: ${diagnostics()}` : ""}`
41
+ )
42
+ )
43
+ ),
44
+ options.timeoutMs ?? 3e4
45
+ );
46
+ void (async () => {
47
+ try {
48
+ for await (const chunk of proc.stderr) {
49
+ stderr += chunk;
50
+ tryFinish();
51
+ if (state.settled) return;
52
+ }
53
+ } catch {
54
+ } finally {
55
+ stderrDone = true;
56
+ tryFinish();
57
+ }
58
+ })();
59
+ void (async () => {
60
+ try {
61
+ for await (const chunk of proc.stdout) {
62
+ stdout += chunk;
63
+ tryFinish();
64
+ if (state.settled) return;
65
+ }
66
+ } catch (error) {
67
+ settle(() => reject(error));
68
+ return;
69
+ } finally {
70
+ stdoutDone = true;
71
+ tryFinish();
72
+ }
73
+ })();
74
+ });
75
+ }
76
+ async function startAcpServerInSandbox(sandbox, options) {
77
+ const proc = await sandbox.process.spawn(options.command, {
78
+ cwd: options.cwd,
79
+ ...options.env ? { env: options.env } : {},
80
+ ...options.signal ? { signal: options.signal } : {}
81
+ });
82
+ const { stdout } = await waitForReady(proc, options);
83
+ const channel = await sandbox.ports.connect(options.port);
84
+ const wsUrl = options.buildWsUrl({
85
+ channel,
86
+ port: options.port,
87
+ stdout
88
+ });
89
+ const headers = channel.headers ?? (channel.token ? { Authorization: `Bearer ${channel.token}` } : void 0);
90
+ return {
91
+ wsUrl,
92
+ channel,
93
+ connect: (signal) => connectAcpWebSocket(wsUrl, {
94
+ ...headers ? { headers } : {},
95
+ ...signal ? { signal } : {},
96
+ framing: options.framing ?? "frame"
97
+ }),
98
+ dispose: () => proc.kill()
99
+ };
100
+ }
101
+ function parseWebSocketUrlFromServeOutput(output) {
102
+ const match = output.match(/WebSocket URL:\s*(ws\S+)/i);
103
+ return match?.[1];
104
+ }
105
+ function buildGrokServeWebSocketUrl(channelUrl, secret) {
106
+ const wsBase = channelUrl.replace(/^http/i, "ws").replace(/\/$/, "");
107
+ return `${wsBase}/ws?server-key=${encodeURIComponent(secret)}`;
108
+ }
109
+ export {
110
+ buildGrokServeWebSocketUrl,
111
+ parseWebSocketUrlFromServeOutput,
112
+ startAcpServerInSandbox
113
+ };
114
+ //# sourceMappingURL=sandbox-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox-server.js","sources":["../../../src/session/sandbox-server.ts"],"sourcesContent":["import { connectAcpWebSocket } from '../transport/websocket'\nimport type {\n SandboxChannel,\n SandboxHandle,\n SpawnHandle,\n} from '@tanstack/ai-sandbox'\nimport type { AcpMessageFraming } from '../transport/types'\nimport type { AcpWebSocketConnection } from '../transport/websocket'\n\nexport interface AcpSandboxServer {\n /** WebSocket URL the orchestrator uses to reach the in-sandbox ACP server. */\n wsUrl: string\n /** Sandbox channel used to build {@link wsUrl} (auth headers, when issued). */\n channel: SandboxChannel\n /** Open the ACP JSON-RPC stream over WebSocket. */\n connect: (signal?: AbortSignal) => Promise<AcpWebSocketConnection>\n /** Stop the in-sandbox server process. */\n dispose: () => Promise<void>\n}\n\nexport interface StartAcpServerOptions {\n port: number\n cwd: string\n /** Full shell command that starts the harness ACP server inside the sandbox. */\n command: string\n /** Build the WebSocket URL once the server is ready and the port is exposed. */\n buildWsUrl: (input: {\n channel: SandboxChannel\n port: number\n stdout: string\n }) => string\n /**\n * Return true once {@link buildWsUrl} can be called. Receives accumulated\n * stdout from the server process.\n */\n isReady?: (stdout: string) => boolean\n /** Substring/regex marker used when {@link isReady} is omitted. */\n readyMarker?: string\n env?: Record<string, string>\n timeoutMs?: number\n signal?: AbortSignal\n framing?: AcpMessageFraming\n hostname?: string\n}\n\nconst DEFAULT_READY_MARKER = 'WebSocket URL:'\n\nfunction waitForReady(\n proc: SpawnHandle,\n options: Pick<StartAcpServerOptions, 'isReady' | 'readyMarker' | 'timeoutMs'>,\n): Promise<{ stdout: string; stderr: string }> {\n const readyMarker = options.readyMarker ?? DEFAULT_READY_MARKER\n const isReady =\n options.isReady ?? ((output: string) => output.includes(readyMarker))\n\n return new Promise((resolve, reject) => {\n let stdout = ''\n let stderr = ''\n const state = { settled: false }\n const settle = (fn: () => void): void => {\n if (state.settled) return\n state.settled = true\n clearTimeout(timer)\n fn()\n }\n const diagnostics = (): string =>\n [stdout, stderr]\n .map((s) => s.trim())\n .filter(Boolean)\n .join('\\n')\n .slice(-500)\n const combined = (): string => stdout + stderr\n let stdoutDone = false\n let stderrDone = false\n const tryFinish = (): void => {\n if (state.settled) return\n if (isReady(combined())) {\n settle(() => resolve({ stdout, stderr }))\n return\n }\n if (stdoutDone && stderrDone) {\n settle(() =>\n reject(\n new Error(\n `ACP server exited before becoming ready${\n diagnostics() ? `: ${diagnostics()}` : ' (no output)'\n }`,\n ),\n ),\n )\n }\n }\n\n const timer = setTimeout(\n () =>\n settle(() =>\n reject(\n new Error(\n `ACP server did not become ready within ${options.timeoutMs ?? 30_000}ms${\n diagnostics() ? `: ${diagnostics()}` : ''\n }`,\n ),\n ),\n ),\n options.timeoutMs ?? 30_000,\n )\n\n void (async () => {\n try {\n for await (const chunk of proc.stderr) {\n stderr += chunk\n tryFinish()\n if (state.settled) return\n }\n } catch {\n // non-fatal\n } finally {\n stderrDone = true\n tryFinish()\n }\n })()\n\n void (async () => {\n try {\n for await (const chunk of proc.stdout) {\n stdout += chunk\n tryFinish()\n if (state.settled) return\n }\n } catch (error) {\n settle(() => reject(error))\n return\n } finally {\n stdoutDone = true\n tryFinish()\n }\n })()\n })\n}\n\n/**\n * Boot a harness ACP WebSocket server inside a sandbox and expose its port via\n * {@link SandboxHandle.ports.connect}. Mirrors the `opencode serve` pattern.\n */\nexport async function startAcpServerInSandbox(\n sandbox: SandboxHandle,\n options: StartAcpServerOptions,\n): Promise<AcpSandboxServer> {\n const proc = await sandbox.process.spawn(options.command, {\n cwd: options.cwd,\n ...(options.env ? { env: options.env } : {}),\n ...(options.signal ? { signal: options.signal } : {}),\n })\n\n const { stdout } = await waitForReady(proc, options)\n const channel = await sandbox.ports.connect(options.port)\n const wsUrl = options.buildWsUrl({\n channel,\n port: options.port,\n stdout,\n })\n\n const headers =\n channel.headers ??\n (channel.token ? { Authorization: `Bearer ${channel.token}` } : undefined)\n\n return {\n wsUrl,\n channel,\n connect: (signal?: AbortSignal) =>\n connectAcpWebSocket(wsUrl, {\n ...(headers ? { headers } : {}),\n ...(signal ? { signal } : {}),\n framing: options.framing ?? 'frame',\n }),\n dispose: () => proc.kill(),\n }\n}\n\n/** Parse a `WebSocket URL: ws://…` line printed by `grok agent serve`. */\nexport function parseWebSocketUrlFromServeOutput(\n output: string,\n): string | undefined {\n const match = output.match(/WebSocket URL:\\s*(ws\\S+)/i)\n return match?.[1]\n}\n\n/** Build a Grok `agent serve` WebSocket URL from a sandbox channel + secret. */\nexport function buildGrokServeWebSocketUrl(\n channelUrl: string,\n secret: string,\n): string {\n const wsBase = channelUrl.replace(/^http/i, 'ws').replace(/\\/$/, '')\n return `${wsBase}/ws?server-key=${encodeURIComponent(secret)}`\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Minimal promise-based async queue bridging callback-style ACP notifications
3
+ * into the async-iterable world the stream translator consumes.
4
+ */
5
+ export declare class AsyncQueue<T> implements AsyncIterable<T> {
6
+ private readonly values;
7
+ private readonly waiters;
8
+ private ended;
9
+ private error;
10
+ private failed;
11
+ push(value: T): void;
12
+ end(): void;
13
+ fail(error: unknown): void;
14
+ [Symbol.asyncIterator](): AsyncIterator<T>;
15
+ }
@@ -0,0 +1,54 @@
1
+ class AsyncQueue {
2
+ values = [];
3
+ waiters = [];
4
+ ended = false;
5
+ error = void 0;
6
+ failed = false;
7
+ push(value) {
8
+ if (this.ended || this.failed) return;
9
+ const waiter = this.waiters.shift();
10
+ if (waiter) {
11
+ waiter.resolve({ value, done: false });
12
+ } else {
13
+ this.values.push(value);
14
+ }
15
+ }
16
+ end() {
17
+ if (this.ended || this.failed) return;
18
+ this.ended = true;
19
+ for (const waiter of this.waiters.splice(0)) {
20
+ waiter.resolve({ value: void 0, done: true });
21
+ }
22
+ }
23
+ fail(error) {
24
+ if (this.ended || this.failed) return;
25
+ this.failed = true;
26
+ this.error = error;
27
+ for (const waiter of this.waiters.splice(0)) {
28
+ waiter.reject(error);
29
+ }
30
+ }
31
+ [Symbol.asyncIterator]() {
32
+ return {
33
+ next: () => {
34
+ if (this.values.length > 0) {
35
+ return Promise.resolve({
36
+ value: this.values.shift(),
37
+ done: false
38
+ });
39
+ }
40
+ if (this.failed) return Promise.reject(this.error);
41
+ if (this.ended) {
42
+ return Promise.resolve({ value: void 0, done: true });
43
+ }
44
+ return new Promise((resolve, reject) => {
45
+ this.waiters.push({ resolve, reject });
46
+ });
47
+ }
48
+ };
49
+ }
50
+ }
51
+ export {
52
+ AsyncQueue
53
+ };
54
+ //# sourceMappingURL=queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue.js","sources":["../../../src/stream/queue.ts"],"sourcesContent":["/**\n * Minimal promise-based async queue bridging callback-style ACP notifications\n * into the async-iterable world the stream translator consumes.\n */\nexport class AsyncQueue<T> implements AsyncIterable<T> {\n private readonly values: Array<T> = []\n private readonly waiters: Array<{\n resolve: (result: IteratorResult<T>) => void\n reject: (error: unknown) => void\n }> = []\n private ended = false\n private error: unknown = undefined\n private failed = false\n\n push(value: T): void {\n if (this.ended || this.failed) return\n const waiter = this.waiters.shift()\n if (waiter) {\n waiter.resolve({ value, done: false })\n } else {\n this.values.push(value)\n }\n }\n\n end(): void {\n if (this.ended || this.failed) return\n this.ended = true\n for (const waiter of this.waiters.splice(0)) {\n waiter.resolve({ value: undefined, done: true })\n }\n }\n\n fail(error: unknown): void {\n if (this.ended || this.failed) return\n this.failed = true\n this.error = error\n for (const waiter of this.waiters.splice(0)) {\n waiter.reject(error)\n }\n }\n\n [Symbol.asyncIterator](): AsyncIterator<T> {\n return {\n next: (): Promise<IteratorResult<T>> => {\n if (this.values.length > 0) {\n return Promise.resolve({\n value: this.values.shift() as T,\n done: false,\n })\n }\n if (this.failed) return Promise.reject(this.error)\n if (this.ended) {\n return Promise.resolve({ value: undefined, done: true })\n }\n return new Promise((resolve, reject) => {\n this.waiters.push({ resolve, reject })\n })\n },\n }\n }\n}\n"],"names":[],"mappings":"AAIO,MAAM,WAA0C;AAAA,EACpC,SAAmB,CAAA;AAAA,EACnB,UAGZ,CAAA;AAAA,EACG,QAAQ;AAAA,EACR,QAAiB;AAAA,EACjB,SAAS;AAAA,EAEjB,KAAK,OAAgB;AACnB,QAAI,KAAK,SAAS,KAAK,OAAQ;AAC/B,UAAM,SAAS,KAAK,QAAQ,MAAA;AAC5B,QAAI,QAAQ;AACV,aAAO,QAAQ,EAAE,OAAO,MAAM,OAAO;AAAA,IACvC,OAAO;AACL,WAAK,OAAO,KAAK,KAAK;AAAA,IACxB;AAAA,EACF;AAAA,EAEA,MAAY;AACV,QAAI,KAAK,SAAS,KAAK,OAAQ;AAC/B,SAAK,QAAQ;AACb,eAAW,UAAU,KAAK,QAAQ,OAAO,CAAC,GAAG;AAC3C,aAAO,QAAQ,EAAE,OAAO,QAAW,MAAM,MAAM;AAAA,IACjD;AAAA,EACF;AAAA,EAEA,KAAK,OAAsB;AACzB,QAAI,KAAK,SAAS,KAAK,OAAQ;AAC/B,SAAK,SAAS;AACd,SAAK,QAAQ;AACb,eAAW,UAAU,KAAK,QAAQ,OAAO,CAAC,GAAG;AAC3C,aAAO,OAAO,KAAK;AAAA,IACrB;AAAA,EACF;AAAA,EAEA,CAAC,OAAO,aAAa,IAAsB;AACzC,WAAO;AAAA,MACL,MAAM,MAAkC;AACtC,YAAI,KAAK,OAAO,SAAS,GAAG;AAC1B,iBAAO,QAAQ,QAAQ;AAAA,YACrB,OAAO,KAAK,OAAO,MAAA;AAAA,YACnB,MAAM;AAAA,UAAA,CACP;AAAA,QACH;AACA,YAAI,KAAK,OAAQ,QAAO,QAAQ,OAAO,KAAK,KAAK;AACjD,YAAI,KAAK,OAAO;AACd,iBAAO,QAAQ,QAAQ,EAAE,OAAO,QAAW,MAAM,MAAM;AAAA,QACzD;AACA,eAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,eAAK,QAAQ,KAAK,EAAE,SAAS,QAAQ;AAAA,QACvC,CAAC;AAAA,MACH;AAAA,IAAA;AAAA,EAEJ;AACF;"}
@@ -0,0 +1,38 @@
1
+ import { StreamChunk } from '@tanstack/ai';
2
+ import { AcpSessionUpdate, AcpStopReason, AcpUsage } from '../types/acp-types.js';
3
+ export declare const BRIDGED_MCP_SERVER_NAME = "tanstack";
4
+ export type AcpStreamEvent = {
5
+ kind: 'session';
6
+ sessionId: string;
7
+ } | {
8
+ kind: 'update';
9
+ update: AcpSessionUpdate;
10
+ } | {
11
+ kind: 'done';
12
+ stopReason: AcpStopReason;
13
+ usage?: AcpUsage;
14
+ };
15
+ export interface AcpTranslateLabels {
16
+ sessionIdEvent: string;
17
+ planEvent?: string;
18
+ refusalMessage?: string;
19
+ /**
20
+ * When set, non-text agent message content (image / audio / resource /
21
+ * resource_link blocks) is surfaced as a CUSTOM event under this name instead
22
+ * of being dropped. Each event's `value` is `{ content: <ACP content block> }`.
23
+ * Omit to keep the text-only behavior.
24
+ */
25
+ contentEvent?: string;
26
+ }
27
+ export interface TranslateContext {
28
+ model: string;
29
+ runId: string;
30
+ threadId: string;
31
+ parentRunId?: string;
32
+ genId: () => string;
33
+ labels: AcpTranslateLabels;
34
+ bridgedToolNames?: ReadonlySet<string>;
35
+ onAcpEvent?: (event: AcpStreamEvent) => void;
36
+ }
37
+ export declare function matchBridgedToolName(title: string | null | undefined, bridgedToolNames: ReadonlySet<string> | undefined): string | undefined;
38
+ export declare function translateAcpStream(events: AsyncIterable<AcpStreamEvent>, ctx: TranslateContext): AsyncIterable<StreamChunk>;