Repository navigation
docs: type-check the package READMEs and fix their broken examples #1981
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -182,65 +182,77 @@ pattern every ACP harness adapter follows: | |
|
|
||
| ```typescript | ||
| import { chat } from '@tanstack/ai' | ||
| import type { TextOptions } from '@tanstack/ai' | ||
| import { | ||
| AsyncQueue, | ||
| resolveInteractivePermission, | ||
| spawnHandleToAcpTransport, | ||
| startAcpSession, | ||
| translateAcpStream, | ||
| } from '@tanstack/ai-acp' | ||
| import type { | ||
| AcpCompatibleProviderOptions, | ||
| AcpStreamEvent, | ||
| } from '@tanstack/ai-acp' | ||
| import { withSandbox } from '@tanstack/ai-sandbox' | ||
| import type { HostToolBridge, SandboxHandle } from '@tanstack/ai-sandbox' | ||
|
|
||
| // Inside your adapter's chatStream(): | ||
| const proc = await sandbox.process.spawn('my-cli --acp -m auto', { | ||
| cwd: '/workspace', | ||
| }) | ||
| declare const sandbox: SandboxHandle | ||
| declare const bridge: HostToolBridge | undefined | ||
| declare const bridgedToolNames: ReadonlySet<string> | ||
| declare const userText: string | ||
|
|
||
| const queue = new AsyncQueue() | ||
| const session = await startAcpSession({ | ||
| transport: { kind: 'stdio', process: proc }, | ||
| cwd: '/workspace', | ||
| authMethodId: 'gemini-api-key', // when the harness advertises it | ||
| resumeSessionId: options.modelOptions?.sessionId, | ||
| mcpServers: bridge | ||
| ? [ | ||
| { | ||
| name: bridge.name, | ||
| url: bridge.url, | ||
| headers: [{ name: 'Authorization', value: `Bearer ${bridge.token}` }], | ||
| }, | ||
| ] | ||
| : undefined, | ||
| onUpdate: (update) => queue.push({ kind: 'update', update }), | ||
| onPermissionRequest: (request) => | ||
| resolveInteractivePermission( | ||
| request, | ||
| 'acceptEdits', | ||
| bridgedToolNames, | ||
| options.approvals, | ||
| 'my-harness', | ||
| ).outcome, | ||
| }) | ||
| async function* chatStream(options: TextOptions<AcpCompatibleProviderOptions>) { | ||
| const proc = await sandbox.process.spawn('my-cli --acp -m auto', { | ||
| cwd: '/workspace', | ||
| }) | ||
|
|
||
| queue.push({ kind: 'session', sessionId: session.sessionId }) | ||
| const queue = new AsyncQueue<AcpStreamEvent>() | ||
| const session = await startAcpSession({ | ||
| transport: { kind: 'stdio', process: proc }, | ||
| cwd: '/workspace', | ||
| authMethodId: 'gemini-api-key', // when the harness advertises it | ||
| resumeSessionId: options.modelOptions?.sessionId, | ||
| mcpServers: bridge | ||
| ? [ | ||
| { | ||
| name: bridge.name, | ||
| url: bridge.url, | ||
| headers: [ | ||
| { name: 'Authorization', value: `Bearer ${bridge.token}` }, | ||
| ], | ||
| }, | ||
| ] | ||
| : undefined, | ||
| onUpdate: (update) => queue.push({ kind: 'update', update }), | ||
| onPermissionRequest: (request) => | ||
| resolveInteractivePermission( | ||
| request, | ||
| 'acceptEdits', | ||
| bridgedToolNames, | ||
| options.approvals, | ||
| 'my-harness', | ||
| ).outcome, | ||
| }) | ||
|
|
||
| session | ||
| .prompt(userText) | ||
| .then(({ stopReason, usage }) => { | ||
| queue.push({ | ||
| kind: 'done', | ||
| stopReason, | ||
| ...(usage !== undefined && { usage }), | ||
| queue.push({ kind: 'session', sessionId: session.sessionId }) | ||
|
|
||
| session | ||
| .prompt(userText) | ||
| .then(({ stopReason, usage }) => { | ||
| queue.push({ | ||
| kind: 'done', | ||
| stopReason, | ||
| ...(usage !== undefined && { usage }), | ||
| }) | ||
| queue.end() | ||
| }) | ||
| queue.end() | ||
| }) | ||
| .catch((error) => queue.fail(error)) | ||
| .catch((error) => queue.fail(error)) | ||
|
|
||
| yield * | ||
| translateAcpStream(queue, { | ||
| yield* translateAcpStream(queue, { | ||
| model: 'auto', | ||
| runId, | ||
| threadId, | ||
| runId: options.runId ?? crypto.randomUUID(), | ||
| threadId: options.threadId, | ||
| genId: () => crypto.randomUUID(), | ||
| labels: { | ||
| sessionIdEvent: 'my-harness.session-id', | ||
|
|
@@ -249,6 +261,7 @@ yield * | |
| }, | ||
| bridgedToolNames, | ||
| }) | ||
| } | ||
| ``` | ||
|
|
||
| Wire the adapter into `chat()` with `withSandbox(...)` like any other harness package. | ||
|
|
@@ -262,11 +275,20 @@ ACP can run over **stdio** (newline-delimited JSON-RPC on the child process pipe | |
| Use when the sandbox can write to process stdin (`capabilities.writableStdin === true`): | ||
|
|
||
| ```typescript | ||
| import { spawnHandleToAcpTransport } from '@tanstack/ai-acp' | ||
| import { spawnHandleToAcpTransport, startAcpSession } from '@tanstack/ai-acp' | ||
| import type { StartAcpSessionOptions } from '@tanstack/ai-acp' | ||
| import type { SandboxHandle } from '@tanstack/ai-sandbox' | ||
|
|
||
| declare const sandbox: SandboxHandle | ||
| declare const cwd: string | ||
| declare const sessionOptions: Omit<StartAcpSessionOptions, 'transport'> | ||
|
|
||
| const proc = await sandbox.process.spawn('grok agent --acp -m auto', { cwd }) | ||
| // spawnHandleToAcpTransport adapts SpawnHandle stdout/stdin for ndJsonStream | ||
| await startAcpSession({ transport: { kind: 'stdio', process: proc }, ... }) | ||
| await startAcpSession({ | ||
| transport: { kind: 'stdio', process: proc }, | ||
| ...sessionOptions, | ||
| }) | ||
| ``` | ||
|
|
||
| On providers without writable stdin (e.g. Cloudflare Containers), adapters feed the prompt via a shell redirect instead of a host stdin write — that workaround lives in each harness adapter, not in `@tanstack/ai-acp`. | ||
|
|
@@ -280,23 +302,39 @@ import { | |
| buildGrokServeWebSocketUrl, | ||
| resolveAcpTransportMode, | ||
| startAcpServerInSandbox, | ||
| startAcpSession, | ||
| } from '@tanstack/ai-acp' | ||
| import type { StartAcpSessionOptions } from '@tanstack/ai-acp' | ||
| import type { SandboxHandle } from '@tanstack/ai-sandbox' | ||
|
|
||
| declare const sandbox: SandboxHandle | ||
| declare const secret: string | ||
| declare const signal: AbortSignal | ||
| declare const sessionOptions: Omit<StartAcpSessionOptions, 'transport'> | ||
|
|
||
| const mode = resolveAcpTransportMode(sandbox, 'auto') // 'stdio' | 'websocket' | ||
|
|
||
| const server = await startAcpServerInSandbox(sandbox, { | ||
| port: 2419, | ||
| cwd: '/workspace', | ||
| command: 'grok agent -m composer-2.5 --always-approve serve --bind 0.0.0.0:2419 --secret …', | ||
| command: | ||
| 'grok agent -m composer-2.5 --always-approve serve --bind 0.0.0.0:2419 --secret …', | ||
| buildWsUrl: ({ channel }) => buildGrokServeWebSocketUrl(channel.url, secret), | ||
| readyMarker: 'WebSocket URL:', | ||
| framing: 'frame', | ||
| }) | ||
|
|
||
| const { stream, close } = await server.connect(signal) | ||
| await startAcpSession({ | ||
| transport: { kind: 'stream', stream, dispose: async () => { close(); await server.dispose() } }, | ||
| ... | ||
| transport: { | ||
| kind: 'stream', | ||
| stream, | ||
| dispose: async () => { | ||
| close() | ||
| await server.dispose() | ||
| }, | ||
| }, | ||
|
Comment on lines
328
to
+336
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| ...sessionOptions, | ||
| }) | ||
| ``` | ||
|
|
||
|
|
@@ -327,7 +365,13 @@ The returned `AcpSessionHandle` exposes `{ sessionId, resumed, prompt, cancel, d | |
| On the first run, `translateAcpStream` emits a CUSTOM chunk: | ||
|
|
||
| ```typescript | ||
| { type: 'CUSTOM', name: 'my-harness.session-id', value: { sessionId: '…' } } | ||
| import type { CustomEvent } from '@tanstack/ai' | ||
|
|
||
| const chunk: CustomEvent = { | ||
| type: 'CUSTOM', | ||
| name: 'my-harness.session-id', | ||
| value: { sessionId: '…' }, | ||
| } | ||
| ``` | ||
|
|
||
| Thread that `sessionId` through `modelOptions.sessionId` on the next `chat()` call so `startAcpSession` can `loadSession` and only send the trailing user message. | ||
|
|
@@ -362,13 +406,24 @@ ACP → AG-UI mapping (high level): | |
| Harnesses can pause mid-turn and ask the client to approve a tool call. Wire `onPermissionRequest` on `startAcpSession`: | ||
|
|
||
| ```typescript | ||
| import type { TextOptions } from '@tanstack/ai' | ||
| import { | ||
| resolvePermission, | ||
| resolveInteractivePermission, | ||
| } from '@tanstack/ai-acp' | ||
| import type { | ||
| AcpPermissionMode, | ||
| AcpPermissionRequest, | ||
| PermissionHandler, | ||
| } from '@tanstack/ai-acp' | ||
|
|
||
| declare const permissionMode: AcpPermissionMode | ||
| declare const bridgedToolNames: ReadonlySet<string> | ||
| declare const request: AcpPermissionRequest | ||
| declare const options: TextOptions | ||
|
|
||
| // Headless / sandboxed: auto-approve bridged tools + edits, reject everything else | ||
| onPermissionRequest: (request) => | ||
| const onPermissionRequest: PermissionHandler = (request) => | ||
| resolvePermission(request, permissionMode, bridgedToolNames) | ||
|
|
||
| // Interactive: same policy, but emit approval-requested events for 'ask' actions | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script executed:
Repository: TanStack/ai
Length of output: 863
Script executed:
Repository: TanStack/ai
Length of output: 11986
Script executed:
Repository: TanStack/ai
Length of output: 9538
Script executed:
Repository: TanStack/ai
Length of output: 3208
Script executed:
Repository: TanStack/ai
Length of output: 1419
Add session cleanup to the
chatStreamexample.The example starts a session with
startAcpSessionbut never callssession.dispose(). Ending the translation does not dispose the session, so a consumer that stops early can leave the spawned process running. Wrap theyield* translateAcpStream(...)block intry, and callawait session.dispose()infinally, as the adapter does.