diff --git a/.changeset/deepagents-harness.md b/.changeset/deepagents-harness.md new file mode 100644 index 000000000000..0c81807e9472 --- /dev/null +++ b/.changeset/deepagents-harness.md @@ -0,0 +1,5 @@ +--- +'@ai-sdk/harness-deepagents': major +--- + +feat(harness-deepagents): implement harness adapter \ No newline at end of file diff --git a/.github/konsistent.json b/.github/konsistent.json index 1b34bb69c7e7..c92caa1f5897 100644 --- a/.github/konsistent.json +++ b/.github/konsistent.json @@ -226,6 +226,7 @@ "kebabToPascalMap": { "assemblyai": "AssemblyAI", "bytedance": "ByteDance", + "deepagents": "DeepAgents", "deepinfra": "DeepInfra", "deepseek": "DeepSeek", "elevenlabs": "ElevenLabs", @@ -239,6 +240,7 @@ "togetherai": "TogetherAI" }, "kebabToCamelMap": { + "deepagents": "deepAgents", "lmnt": "lmnt" } } diff --git a/content/providers/02-ai-sdk-harnesses/05-deepagents.mdx b/content/providers/02-ai-sdk-harnesses/05-deepagents.mdx new file mode 100644 index 000000000000..bce9b58a58fa --- /dev/null +++ b/content/providers/02-ai-sdk-harnesses/05-deepagents.mdx @@ -0,0 +1,195 @@ +--- +title: DeepAgents +description: Learn how to use the DeepAgents harness adapter. +--- + +# DeepAgents Harness + +The DeepAgents harness adapter connects `HarnessAgent` to +[DeepAgents](https://github.com/deep-agents/deepagents), a LangGraph-based agent +runtime. The adapter runs a Node bridge inside the sandbox that drives the +`deepagents` package (`createDeepAgent`) and streams its `streamEvents` output +back to the host over a sandbox-exposed WebSocket. + + + Harness packages are **experimental**. Expect breaking changes between + releases as this early API gets further refined. + + +## Setup + + + + + + + + + + + + + + + + +The adapter bootstraps the bridge's Node dependencies (the `deepagents` package +and LangChain) inside the sandbox via `pnpm` when the first session starts. + +## Import + +```ts +import { deepAgents, createDeepAgents } from '@ai-sdk/harness-deepagents'; +``` + +`deepAgents` is equivalent to `createDeepAgents()` with its default +configuration. + +## Basic Usage + +```ts +import { HarnessAgent } from '@ai-sdk/harness/agent'; +import { deepAgents } from '@ai-sdk/harness-deepagents'; +import { createVercelSandbox } from '@ai-sdk/sandbox-vercel'; + +const agent = new HarnessAgent({ + harness: deepAgents, + sandbox: createVercelSandbox({ + runtime: 'node24', + ports: [4000], + }), +}); + +const session = await agent.createSession(); + +let exitCode = 0; +try { + const result = await agent.stream({ + session, + prompt: 'Analyze this codebase and suggest improvements.', + }); + + for await (const part of result.stream) { + if (part.type === 'text-delta') { + process.stdout.write(part.text); + } + } +} catch (err) { + exitCode = 1; + console.error(err); +} finally { + await session.destroy(); + process.exit(exitCode); +} +``` + +To use this agent, ensure environment variables include `VERCEL_OIDC_TOKEN` for +Vercel Sandbox, and one of the variables listed under +[authentication](#authentication) for the model provider. + +## Adapter Settings + +Use `createDeepAgents()` to configure the runtime: + +```ts +const harness = createDeepAgents({ + model: 'claude-sonnet-4', +}); +``` + +Settings: + +- `auth`: Anthropic, OpenAI, or AI Gateway authentication settings. +- `model`: model id passed to the DeepAgents (LangChain) runtime. The bridge + converts it to LangChain's `provider:model` form internally. +- `port`: bridge port override. +- `startupTimeoutMs`: maximum time to wait for the bridge to start. + +## Authentication + +The provider is resolved from the model id (`anthropic/…` or `openai/…`, +defaulting to Anthropic). Authentication is resolved from the host environment +and forwarded to the sandbox bridge: explicit provider auth first, then AI +Gateway credentials, then ambient provider credentials. + +Supported environment variables: + +- `AI_GATEWAY_API_KEY` +- `VERCEL_OIDC_TOKEN` +- `AI_GATEWAY_BASE_URL` +- `ANTHROPIC_API_KEY` +- `ANTHROPIC_AUTH_TOKEN` +- `ANTHROPIC_BASE_URL` +- `OPENAI_API_KEY` +- `OPENAI_BASE_URL` +- `OPENAI_ORGANIZATION` +- `OPENAI_PROJECT` + +You can also pass explicit auth settings (`anthropic`, `openai`, or `gateway`): + +```ts +const harness = createDeepAgents({ + model: 'openai/gpt-5', + auth: { + openai: { + apiKey: process.env.OPENAI_API_KEY, + }, + }, +}); +``` + +## Sandbox + +DeepAgents requires a network sandbox with at least one exposed port, +e.g. `@ai-sdk/sandbox-vercel`: + +```ts +const sandbox = createVercelSandbox({ + runtime: 'node24', + ports: [4000], +}); +``` + +## Skills + +Skills passed to the session are materialized as native DeepAgents skill folders +(`/SKILL.md` plus any attached files) under `.deepagents/skills/` in the +sandbox, and loaded via DeepAgents' `skills` option — so the agent loads them on +demand and skill file references resolve. + +## Built-in Tools + +The adapter exposes these common DeepAgents built-ins through `agent.tools`: + +- `read` (native `read_file`) +- `write` (native `write_file`) +- `bash` (native `shell`) +- `grep` (native `search`) + +## Known Limitations + +- **Built-in tool approvals** are not supported yet. Use + `permissionMode: 'allow-all'`. Host-executed AI SDK tool approvals still work. +- **Cross-process resume, turn continuation, and suspend/detach** are not + supported yet — DeepAgents holds conversation state in memory (LangGraph + `MemorySaver`), which does not survive a bridge restart. These methods throw + `HarnessCapabilityUnsupportedError`. +- **Manual compaction** is not supported. + +## Related + +- [HarnessAgent](/docs/ai-sdk-harnesses/harness-agent) +- [Harness tools](/docs/ai-sdk-harnesses/tools) +- [Harness adapters](/docs/ai-sdk-harnesses/harness-adapters) diff --git a/examples/ai-functions/package.json b/examples/ai-functions/package.json index 97f2a111323e..f96fb39c6e16 100644 --- a/examples/ai-functions/package.json +++ b/examples/ai-functions/package.json @@ -30,6 +30,7 @@ "@ai-sdk/harness": "workspace:*", "@ai-sdk/harness-claude-code": "workspace:*", "@ai-sdk/harness-codex": "workspace:*", + "@ai-sdk/harness-deepagents": "workspace:*", "@ai-sdk/harness-pi": "workspace:*", "@ai-sdk/huggingface": "workspace:*", "@ai-sdk/hume": "workspace:*", diff --git a/examples/ai-functions/src/harness-agent/deepagents/attach.ts b/examples/ai-functions/src/harness-agent/deepagents/attach.ts new file mode 100644 index 000000000000..d88165c9a456 --- /dev/null +++ b/examples/ai-functions/src/harness-agent/deepagents/attach.ts @@ -0,0 +1,55 @@ +import { + HarnessAgent, + type HarnessAgentResumeSessionState, +} from '@ai-sdk/harness/agent'; +import { deepAgents } from '@ai-sdk/harness-deepagents'; +import { createVercelSandbox } from '@ai-sdk/sandbox-vercel'; +import { printFullStream } from '../../lib/print-full-stream'; +import { run } from '../../lib/run'; + +// Cross-process ATTACH: detach parks the live bridge + sandbox and returns +// coordinates; a fresh HarnessAgent reattaches and continues mid-conversation +// (the in-memory conversation survives because the bridge stays alive). +run(async () => { + const sandbox = createVercelSandbox({ + runtime: 'node24', + ports: [4000], + timeout: 10 * 60 * 1000, + }); + + let sessionId: string; + let resumeState: HarnessAgentResumeSessionState; + { + const agent = new HarnessAgent({ harness: deepAgents, sandbox }); + const session = await agent.createSession(); + sessionId = session.sessionId; + console.log('--- turn 1 ---'); + const result = await agent.stream({ + session, + prompt: 'My name is Ada. Remember it.', + }); + await printFullStream({ result }); + resumeState = await session.detach(); + console.log('[handle] live coords:', JSON.stringify(resumeState)); + } + + { + const agent = new HarnessAgent({ harness: deepAgents, sandbox }); + const session = await agent.createSession({ + sessionId, + resumeFrom: resumeState, + }); + console.log('--- turn 2 ---'); + if (!session.isResume) { + throw new Error('expected resumed session'); + } + const result = await agent.stream({ + session, + prompt: 'What is my name? Answer in one word.', + }); + await printFullStream({ result }); + await session.destroy(); + } + + process.exit(0); +}); diff --git a/examples/ai-functions/src/harness-agent/deepagents/bash-shell.ts b/examples/ai-functions/src/harness-agent/deepagents/bash-shell.ts new file mode 100644 index 000000000000..27ac609a2af7 --- /dev/null +++ b/examples/ai-functions/src/harness-agent/deepagents/bash-shell.ts @@ -0,0 +1,30 @@ +import { HarnessAgent } from '@ai-sdk/harness/agent'; +import { deepAgents } from '@ai-sdk/harness-deepagents'; +import { createVercelSandbox } from '@ai-sdk/sandbox-vercel'; +import { printFullStream } from '../../lib/print-full-stream'; +import { run } from '../../lib/run'; + +run(async () => { + const sandbox = createVercelSandbox({ + runtime: 'node24', + ports: [4000], + timeout: 10 * 60 * 1000, + }); + const agent = new HarnessAgent({ harness: deepAgents, sandbox }); + + let exitCode = 0; + const session = await agent.createSession(); + try { + const result = await agent.stream({ + session, + prompt: 'Run `uname -a` and tell me what kernel this sandbox is running.', + }); + await printFullStream({ result }); + } catch (err) { + exitCode = 1; + console.error('[example] failed:', err); + } finally { + await session.destroy(); + process.exit(exitCode); + } +}); diff --git a/examples/ai-functions/src/harness-agent/deepagents/builtin-tool-approval.ts b/examples/ai-functions/src/harness-agent/deepagents/builtin-tool-approval.ts new file mode 100644 index 000000000000..f0769def0f7c --- /dev/null +++ b/examples/ai-functions/src/harness-agent/deepagents/builtin-tool-approval.ts @@ -0,0 +1,52 @@ +import { HarnessAgent } from '@ai-sdk/harness/agent'; +import { deepAgents } from '@ai-sdk/harness-deepagents'; +import { printFullStream } from '../../lib/print-full-stream'; +import { run } from '../../lib/run'; +import { + createToolApprovalResponseMessages, + printFullStreamAndCaptureToolApproval, +} from '../../lib/harness-tool-approval'; +import { createVercelSandbox } from '@ai-sdk/sandbox-vercel'; + +run(async () => { + const sandbox = createVercelSandbox({ + runtime: 'node24', + ports: [4000], + timeout: 10 * 60 * 1000, + }); + const agent = new HarnessAgent({ + harness: deepAgents, + sandbox, + permissionMode: 'allow-edits', + }); + + let exitCode = 0; + const session = await agent.createSession(); + try { + const first = await agent.stream({ + session, + prompt: 'Run `pwd` with the bash tool and tell me the working directory.', + }); + const approval = await printFullStreamAndCaptureToolApproval({ + result: first, + }); + if (approval == null) { + throw new Error('Expected a built-in bash tool approval request.'); + } + + const second = await agent.stream({ + session, + messages: createToolApprovalResponseMessages({ + approval, + approved: true, + }), + }); + await printFullStream({ result: second }); + } catch (err) { + exitCode = 1; + console.error('[example] failed:', err); + } finally { + await session.destroy(); + process.exit(exitCode); + } +}); diff --git a/examples/ai-functions/src/harness-agent/deepagents/custom-tool-approval.ts b/examples/ai-functions/src/harness-agent/deepagents/custom-tool-approval.ts new file mode 100644 index 000000000000..622fed4a0541 --- /dev/null +++ b/examples/ai-functions/src/harness-agent/deepagents/custom-tool-approval.ts @@ -0,0 +1,69 @@ +import { HarnessAgent } from '@ai-sdk/harness/agent'; +import { deepAgents } from '@ai-sdk/harness-deepagents'; +import { createVercelSandbox } from '@ai-sdk/sandbox-vercel'; +import { tool } from 'ai'; +import { z } from 'zod'; +import { + createToolApprovalResponseMessages, + printFullStreamAndCaptureToolApproval, +} from '../../lib/harness-tool-approval'; +import { printFullStream } from '../../lib/print-full-stream'; +import { run } from '../../lib/run'; + +run(async () => { + const sandbox = createVercelSandbox({ + runtime: 'node24', + ports: [4000], + timeout: 10 * 60 * 1000, + }); + const weather = tool({ + description: 'Get the current temperature for a city.', + inputSchema: z.object({ city: z.string() }), + execute: async ({ city }: { city: string }) => { + const temps: Record = { + Paris: 12, + Tokyo: 18, + Reykjavik: 3, + }; + return { city, celsius: temps[city] ?? 20 }; + }, + }); + + const agent = new HarnessAgent({ + harness: deepAgents, + sandbox, + tools: { weather }, + toolApproval: { weather: 'user-approval' }, + }); + + let exitCode = 0; + const session = await agent.createSession(); + try { + const first = await agent.stream({ + session, + prompt: + 'What is the weather in Paris? Use the `weather` tool, then summarize in one sentence.', + }); + const approval = await printFullStreamAndCaptureToolApproval({ + result: first, + }); + if (approval == null) { + throw new Error('Expected a weather tool approval request.'); + } + + const second = await agent.stream({ + session, + messages: createToolApprovalResponseMessages({ + approval, + approved: true, + }), + }); + await printFullStream({ result: second }); + } catch (err) { + exitCode = 1; + console.error('[example] failed:', err); + } finally { + await session.destroy(); + process.exit(exitCode); + } +}); diff --git a/examples/ai-functions/src/harness-agent/deepagents/file-edit.ts b/examples/ai-functions/src/harness-agent/deepagents/file-edit.ts new file mode 100644 index 000000000000..c9b73ca72e98 --- /dev/null +++ b/examples/ai-functions/src/harness-agent/deepagents/file-edit.ts @@ -0,0 +1,45 @@ +import { HarnessAgent } from '@ai-sdk/harness/agent'; +import { deepAgents } from '@ai-sdk/harness-deepagents'; +import { createVercelSandbox } from '@ai-sdk/sandbox-vercel'; +import { printFullStream } from '../../lib/print-full-stream'; +import { run } from '../../lib/run'; + +run(async () => { + const sandbox = createVercelSandbox({ + runtime: 'node24', + ports: [4000], + timeout: 10 * 60 * 1000, + }); + const agent = new HarnessAgent({ harness: deepAgents, sandbox }); + + let exitCode = 0; + const session = await agent.createSession(); + try { + console.log('--- turn 1: create ---'); + const first = await agent.stream({ + session, + prompt: 'Create a file at `notes.md` containing the text "hello world".', + }); + await printFullStream({ result: first }); + + console.log('--- turn 2: edit ---'); + const second = await agent.stream({ + session, + prompt: 'Edit `notes.md` to replace "hello" with "Hello" (capitalized).', + }); + await printFullStream({ result: second }); + + console.log('--- turn 3: read ---'); + const third = await agent.stream({ + session, + prompt: 'Read `notes.md` and print its contents in your reply.', + }); + await printFullStream({ result: third }); + } catch (err) { + exitCode = 1; + console.error('[example] failed:', err); + } finally { + await session.destroy(); + process.exit(exitCode); + } +}); diff --git a/examples/ai-functions/src/harness-agent/deepagents/generate-text.ts b/examples/ai-functions/src/harness-agent/deepagents/generate-text.ts new file mode 100644 index 000000000000..affb4db01525 --- /dev/null +++ b/examples/ai-functions/src/harness-agent/deepagents/generate-text.ts @@ -0,0 +1,34 @@ +import { HarnessAgent } from '@ai-sdk/harness/agent'; +import { deepAgents } from '@ai-sdk/harness-deepagents'; +import { createVercelSandbox } from '@ai-sdk/sandbox-vercel'; +import { run } from '../../lib/run'; + +run(async () => { + const sandbox = createVercelSandbox({ + runtime: 'node24', + ports: [4000], + timeout: 10 * 60 * 1000, + }); + const agent = new HarnessAgent({ + harness: deepAgents, + sandbox, + }); + + let exitCode = 0; + const session = await agent.createSession(); + try { + const result = await agent.generate({ + session, + prompt: 'In one sentence, what is the capital of France?', + }); + console.log('text:', result.text); + console.log('finishReason:', result.finishReason); + console.log('usage:', result.usage); + } catch (err) { + exitCode = 1; + console.error('[example] failed:', err); + } finally { + await session.destroy(); + process.exit(exitCode); + } +}); diff --git a/examples/ai-functions/src/harness-agent/deepagents/multi-turn.ts b/examples/ai-functions/src/harness-agent/deepagents/multi-turn.ts new file mode 100644 index 000000000000..614c6b70f119 --- /dev/null +++ b/examples/ai-functions/src/harness-agent/deepagents/multi-turn.ts @@ -0,0 +1,41 @@ +import { HarnessAgent } from '@ai-sdk/harness/agent'; +import { deepAgents } from '@ai-sdk/harness-deepagents'; +import { createVercelSandbox } from '@ai-sdk/sandbox-vercel'; +import { printFullStream } from '../../lib/print-full-stream'; +import { run } from '../../lib/run'; + +run(async () => { + const sandbox = createVercelSandbox({ + runtime: 'node24', + ports: [4000], + timeout: 10 * 60 * 1000, + }); + const agent = new HarnessAgent({ + harness: deepAgents, + sandbox, + }); + + let exitCode = 0; + const session = await agent.createSession(); + try { + console.log('--- turn 1 ---'); + const first = await agent.stream({ + session, + prompt: 'My name is Ada. Remember it.', + }); + await printFullStream({ result: first }); + + console.log('\n--- turn 2 ---'); + const second = await agent.stream({ + session, + prompt: 'What is my name? Answer in one word.', + }); + await printFullStream({ result: second }); + } catch (err) { + exitCode = 1; + console.error('[example] failed:', err); + } finally { + await session.destroy(); + process.exit(exitCode); + } +}); diff --git a/examples/ai-functions/src/harness-agent/deepagents/stream-text.ts b/examples/ai-functions/src/harness-agent/deepagents/stream-text.ts new file mode 100644 index 000000000000..638d5cea8a1a --- /dev/null +++ b/examples/ai-functions/src/harness-agent/deepagents/stream-text.ts @@ -0,0 +1,37 @@ +import { HarnessAgent } from '@ai-sdk/harness/agent'; +import { deepAgents } from '@ai-sdk/harness-deepagents'; +import { createVercelSandbox } from '@ai-sdk/sandbox-vercel'; +import { printFullStream } from '../../lib/print-full-stream'; +import { run } from '../../lib/run'; + +run(async () => { + const sandbox = createVercelSandbox({ + runtime: 'node24', + ports: [4000], + timeout: 10 * 60 * 1000, + }); + const agent = new HarnessAgent({ + harness: deepAgents, + sandbox, + }); + + let exitCode = 0; + const session = await agent.createSession(); + try { + const result = await agent.stream({ + session, + prompt: 'Recite the first sentence of "A Tale of Two Cities".', + }); + + await printFullStream({ result }); + + console.log('finishReason:', await result.finishReason); + console.log('usage:', await result.usage); + } catch (err) { + exitCode = 1; + console.error('[example] failed:', err); + } finally { + await session.destroy(); + process.exit(exitCode); + } +}); diff --git a/examples/ai-functions/src/harness-agent/deepagents/typed-builtin-tools.ts b/examples/ai-functions/src/harness-agent/deepagents/typed-builtin-tools.ts new file mode 100644 index 000000000000..832756e9aebc --- /dev/null +++ b/examples/ai-functions/src/harness-agent/deepagents/typed-builtin-tools.ts @@ -0,0 +1,46 @@ +import { HarnessAgent } from '@ai-sdk/harness/agent'; +import { deepAgents } from '@ai-sdk/harness-deepagents'; +import { createVercelSandbox } from '@ai-sdk/sandbox-vercel'; +import { tool } from 'ai'; +import { z } from 'zod'; +import { printFullStream } from '../../lib/print-full-stream'; +import { run } from '../../lib/run'; + +// DeepAgents' builtin tool set (read/write/edit/bash/grep/glob/ls/task/write_todos) +// merges with user tools; TypeScript narrows `toolName`/`input` per tool across both surfaces. +run(async () => { + const sandbox = createVercelSandbox({ + runtime: 'node24', + ports: [4000], + timeout: 10 * 60 * 1000, + }); + + const echo = tool({ + description: 'Return the given message back to the model.', + inputSchema: z.object({ message: z.string() }), + execute: async ({ message }: { message: string }) => ({ message }), + }); + + const agent = new HarnessAgent({ + harness: deepAgents, + sandbox, + tools: { echo }, + }); + + let exitCode = 0; + const session = await agent.createSession(); + try { + const result = await agent.stream({ + session, + prompt: + 'Call the `echo` tool with the message "hello", then run `uname -a` and tell me the kernel.', + }); + await printFullStream({ result }); + } catch (err) { + exitCode = 1; + console.error('[example] failed:', err); + } finally { + await session.destroy(); + process.exit(exitCode); + } +}); diff --git a/examples/ai-functions/src/harness-agent/deepagents/with-skills.ts b/examples/ai-functions/src/harness-agent/deepagents/with-skills.ts new file mode 100644 index 000000000000..15320ace7ffe --- /dev/null +++ b/examples/ai-functions/src/harness-agent/deepagents/with-skills.ts @@ -0,0 +1,68 @@ +import { HarnessAgent } from '@ai-sdk/harness/agent'; +import { deepAgents } from '@ai-sdk/harness-deepagents'; +import { createVercelSandbox } from '@ai-sdk/sandbox-vercel'; +import { printFullStream } from '../../lib/print-full-stream'; +import { run } from '../../lib/run'; + +// Skills are loaded on demand by name/description; the harness writes them to `.skills.md`. +run(async () => { + const sandbox = createVercelSandbox({ + runtime: 'node24', + ports: [4000], + timeout: 10 * 60 * 1000, + }); + + const agent = new HarnessAgent({ + harness: deepAgents, + sandbox, + skills: [ + { + name: 'release-notes-format', + description: + 'Use when the user asks to write, draft, or update release notes. Provides our team-specific format that you will not know otherwise.', + content: `# Release notes format + +Before drafting release notes, read \`release-notes-format.md\`. It is the source of truth for the section order, tone, PR reference style, and version-tag rule.`, + files: [ + { + path: 'release-notes-format.md', + content: `# Release notes format reference + +Structure release notes as exactly three top-level sections in this order: + +## Highlights +User-facing new features. One short paragraph per item, present tense. +Reference PRs inline as bare \`#1234\` (no link). + +## Fixes +Bug fixes only. One bullet per fix, imperative mood ("Fix X" not "Fixed X"). + +## Breaking changes +Schema changes, removed APIs, behaviour changes that require migration. +Each item: a one-line summary followed by a "**Migration:**" sub-bullet. +Omit this section entirely if there are no breaking changes. + +End the document with the version tag on a line by itself, prefixed with \`v\`.`, + }, + ], + }, + ], + }); + + let exitCode = 0; + const session = await agent.createSession(); + try { + const result = await agent.stream({ + session, + prompt: + 'Draft release notes for our next release, v2.4.0. We added a dark mode toggle in #892, fixed an autofocus bug in the search bar in #901, and renamed the `--legacy` CLI flag to `--compat` (old flag removed, no alias).', + }); + await printFullStream({ result }); + } catch (err) { + exitCode = 1; + console.error('[example] failed:', err); + } finally { + await session.destroy(); + process.exit(exitCode); + } +}); diff --git a/examples/ai-functions/src/harness-agent/deepagents/with-tools.ts b/examples/ai-functions/src/harness-agent/deepagents/with-tools.ts new file mode 100644 index 000000000000..0b4bb8a0d0b4 --- /dev/null +++ b/examples/ai-functions/src/harness-agent/deepagents/with-tools.ts @@ -0,0 +1,53 @@ +import { HarnessAgent } from '@ai-sdk/harness/agent'; +import { deepAgents } from '@ai-sdk/harness-deepagents'; +import { createVercelSandbox } from '@ai-sdk/sandbox-vercel'; +import { tool } from 'ai'; +import { z } from 'zod'; +import { printFullStream } from '../../lib/print-full-stream'; +import { run } from '../../lib/run'; + +run(async () => { + const sandbox = createVercelSandbox({ + runtime: 'node24', + ports: [4000], + timeout: 10 * 60 * 1000, + }); + const weather = tool({ + description: 'Get the current temperature for a city.', + inputSchema: z.object({ city: z.string() }), + execute: async ({ city }: { city: string }) => { + const temps: Record = { + Paris: 12, + Tokyo: 18, + Reykjavik: 3, + }; + return { city, celsius: temps[city] ?? 20 }; + }, + }); + + const agent = new HarnessAgent({ + harness: deepAgents, + sandbox, + tools: { weather }, + }); + + let exitCode = 0; + const session = await agent.createSession(); + try { + const result = await agent.stream({ + session, + prompt: + 'What is the weather in Paris and Reykjavik? Use the `weather` tool, then summarize in one sentence.', + }); + + await printFullStream({ result }); + + console.log('steps:', (await result.steps).length); + } catch (err) { + exitCode = 1; + console.error('[example] failed:', err); + } finally { + await session.destroy(); + process.exit(exitCode); + } +}); diff --git a/examples/harness-e2e-next/agent/harness/deepagents/ai-sdk-coding-agent.ts b/examples/harness-e2e-next/agent/harness/deepagents/ai-sdk-coding-agent.ts new file mode 100644 index 000000000000..cfda0adf63c7 --- /dev/null +++ b/examples/harness-e2e-next/agent/harness/deepagents/ai-sdk-coding-agent.ts @@ -0,0 +1,52 @@ +import { HarnessAgent } from '@ai-sdk/harness/agent'; +import { deepAgents } from '@ai-sdk/harness-deepagents'; +import { createVercelSandbox } from '@ai-sdk/sandbox-vercel'; +import type { InferUITools, UIMessage } from 'ai'; + +// Default sandbox resources won't allow a full parallel build of all packages; +// guide the harness to use a lower turbo concurrency instead. +const instructions = ` +Building all packages at once (e.g. running \`pnpm build\` or \`pnpm build:packages\`) +will exceed sandbox memory. When asked to do this, use the corresponding +\`pnpm exec turbo\` call directly with a lower \`--concurrency=4\` flag. +`; + +export const aiSdkCodingDeepAgentsHarnessAgent = new HarnessAgent({ + harness: deepAgents, + instructions, + sandbox: createVercelSandbox({ + runtime: 'node24', + ports: [4000], + }), + onSandboxSession: async ({ session, sessionWorkDir, abortSignal }) => { + const result = await session.run({ + command: + 'test -d .git || git clone --depth 1 https://github.com/vercel/ai.git .', + workingDirectory: sessionWorkDir, + abortSignal, + }); + if (result.exitCode !== 0) { + throw new Error( + `Failed to clone vercel/ai (exit ${result.exitCode}): ${result.stderr}`, + ); + } + + const installResult = await session.run({ + command: 'test -d node_modules || pnpm install', + workingDirectory: sessionWorkDir, + abortSignal, + }); + if (installResult.exitCode !== 0) { + throw new Error( + `Failed to install dependencies (exit ${installResult.exitCode}): ${installResult.stderr}`, + ); + } + }, +}); + +// See basic-agent.ts for why the UIMessage type derives from agent.tools. +export type AiSdkCodingDeepAgentsHarnessAgentMessage = UIMessage< + unknown, + never, + InferUITools +>; diff --git a/examples/harness-e2e-next/agent/harness/deepagents/basic-agent.ts b/examples/harness-e2e-next/agent/harness/deepagents/basic-agent.ts new file mode 100644 index 000000000000..813f386266bb --- /dev/null +++ b/examples/harness-e2e-next/agent/harness/deepagents/basic-agent.ts @@ -0,0 +1,30 @@ +import { + createFileReporter, + createTraceTreeReporter, + HarnessAgent, +} from '@ai-sdk/harness/agent'; +import { deepAgents } from '@ai-sdk/harness-deepagents'; +import { createVercelSandbox } from '@ai-sdk/sandbox-vercel'; +import type { InferUITools, UIMessage } from 'ai'; + +export const deepAgentsHarnessAgent = new HarnessAgent({ + harness: deepAgents, + sandbox: createVercelSandbox({ + runtime: 'node24', + ports: [4000], + }), + debug: { enabled: true }, + telemetry: { + integrations: [ + createTraceTreeReporter(), + createFileReporter({ dir: '.harness-observability/deepagents/basic' }), + ], + }, +}); + +// Derived from `agent.tools` (not InferAgentUIMessage) — see Codex/OpenCode basic agents. +export type DeepAgentsHarnessAgentMessage = UIMessage< + unknown, + never, + InferUITools +>; diff --git a/examples/harness-e2e-next/agent/harness/deepagents/weather-agent.ts b/examples/harness-e2e-next/agent/harness/deepagents/weather-agent.ts new file mode 100644 index 000000000000..05783f95d2f1 --- /dev/null +++ b/examples/harness-e2e-next/agent/harness/deepagents/weather-agent.ts @@ -0,0 +1,47 @@ +import { weatherTool } from '@/lib/tools/weather-tool'; +import { + WEATHER_CODES_REFERENCE, + weatherCodesSkill, + weatherForecastSkill, + weatherInstructions, +} from '@/lib/weather-utils'; +import { + createFileReporter, + createTraceTreeReporter, + HarnessAgent, +} from '@ai-sdk/harness/agent'; +import { deepAgents } from '@ai-sdk/harness-deepagents'; +import { createVercelSandbox } from '@ai-sdk/sandbox-vercel'; +import type { InferUITools, UIMessage } from 'ai'; + +export const weatherDeepAgentsHarnessAgent = new HarnessAgent({ + harness: deepAgents, + instructions: weatherInstructions, + skills: [weatherForecastSkill, weatherCodesSkill], + tools: { get_weather: weatherTool }, + sandbox: createVercelSandbox({ + runtime: 'node24', + ports: [4000], + }), + onSandboxSession: async ({ session, sessionWorkDir, abortSignal }) => { + await session.writeTextFile({ + path: `${sessionWorkDir}/weather-codes.md`, + content: WEATHER_CODES_REFERENCE, + abortSignal, + }); + }, + debug: { enabled: true }, + telemetry: { + integrations: [ + createTraceTreeReporter(), + createFileReporter({ dir: '.harness-observability/deepagents/weather' }), + ], + }, +}); + +// See basic-agent.ts for why the UIMessage type derives from agent.tools. +export type WeatherDeepAgentsHarnessAgentMessage = UIMessage< + unknown, + never, + InferUITools +>; diff --git a/examples/harness-e2e-next/agent/harness/deepagents/weather-approval-agent.ts b/examples/harness-e2e-next/agent/harness/deepagents/weather-approval-agent.ts new file mode 100644 index 000000000000..53f5e80f97b4 --- /dev/null +++ b/examples/harness-e2e-next/agent/harness/deepagents/weather-approval-agent.ts @@ -0,0 +1,53 @@ +import { weatherTool } from '@/lib/tools/weather-tool'; +import { + WEATHER_CODES_REFERENCE, + weatherCodesSkill, + weatherForecastSkill, + weatherInstructions, +} from '@/lib/weather-utils'; +import { + createFileReporter, + createTraceTreeReporter, + HarnessAgent, +} from '@ai-sdk/harness/agent'; +import { deepAgents } from '@ai-sdk/harness-deepagents'; +import { createVercelSandbox } from '@ai-sdk/sandbox-vercel'; +import type { InferUITools, UIMessage } from 'ai'; + +export const weatherApprovalDeepAgentsHarnessAgent = new HarnessAgent({ + harness: deepAgents, + instructions: weatherInstructions, + skills: [weatherForecastSkill, weatherCodesSkill], + tools: { get_weather: weatherTool }, + // Host-tool approval is handled by HarnessAgent, independent of the adapter's + // built-in tool approval support. + toolApproval: { + get_weather: 'user-approval', + }, + sandbox: createVercelSandbox({ + runtime: 'node24', + ports: [4000], + }), + onSandboxSession: async ({ session, sessionWorkDir, abortSignal }) => { + await session.writeTextFile({ + path: `${sessionWorkDir}/weather-codes.md`, + content: WEATHER_CODES_REFERENCE, + abortSignal, + }); + }, + debug: { enabled: true }, + telemetry: { + integrations: [ + createTraceTreeReporter(), + createFileReporter({ + dir: '.harness-observability/deepagents/weather-approval', + }), + ], + }, +}); + +export type WeatherApprovalDeepAgentsHarnessAgentMessage = UIMessage< + unknown, + never, + InferUITools +>; diff --git a/examples/harness-e2e-next/app/api/harness/deepagents/ai-sdk-coding/route.ts b/examples/harness-e2e-next/app/api/harness/deepagents/ai-sdk-coding/route.ts new file mode 100644 index 000000000000..517bf6bac027 --- /dev/null +++ b/examples/harness-e2e-next/app/api/harness/deepagents/ai-sdk-coding/route.ts @@ -0,0 +1,41 @@ +import { aiSdkCodingDeepAgentsHarnessAgent } from '@/agent/harness/deepagents/ai-sdk-coding-agent'; +import { + detachAndPersist, + resumeOrCreateSession, +} from '@/util/harness-resume-store'; +import { + convertToModelMessages, + createUIMessageStreamResponse, + toUIMessageStream, + type UIMessage, +} from 'ai'; + +export async function POST(request: Request) { + const body: { + id?: string; + messages: UIMessage[]; + } = await request.json(); + + if (!body.id) { + return new Response('Missing chat id', { status: 400 }); + } + const chatId = body.id; + const messages = await convertToModelMessages(body.messages); + + const session = await resumeOrCreateSession( + aiSdkCodingDeepAgentsHarnessAgent, + chatId, + ); + + const result = await aiSdkCodingDeepAgentsHarnessAgent.stream({ + session, + messages, + }); + + return createUIMessageStreamResponse({ + stream: toUIMessageStream({ + stream: result.stream, + onFinish: () => detachAndPersist(chatId, session), + }), + }); +} diff --git a/examples/harness-e2e-next/app/api/harness/deepagents/basic-with-stop/route.ts b/examples/harness-e2e-next/app/api/harness/deepagents/basic-with-stop/route.ts new file mode 100644 index 000000000000..cd6dea7cff1e --- /dev/null +++ b/examples/harness-e2e-next/app/api/harness/deepagents/basic-with-stop/route.ts @@ -0,0 +1,35 @@ +import { deepAgentsHarnessAgent } from '@/agent/harness/deepagents/basic-agent'; +import { + resumeOrCreateSession, + stopAndPersist, +} from '@/util/harness-resume-store'; +import { + convertToModelMessages, + createUIMessageStreamResponse, + toUIMessageStream, + type UIMessage, +} from 'ai'; + +export async function POST(request: Request) { + const body: { + id?: string; + messages: UIMessage[]; + } = await request.json(); + + if (!body.id) { + return new Response('Missing chat id', { status: 400 }); + } + const chatId = body.id; + const messages = await convertToModelMessages(body.messages); + + const session = await resumeOrCreateSession(deepAgentsHarnessAgent, chatId); + + const result = await deepAgentsHarnessAgent.stream({ session, messages }); + + return createUIMessageStreamResponse({ + stream: toUIMessageStream({ + stream: result.stream, + onFinish: () => stopAndPersist(chatId, session), + }), + }); +} diff --git a/examples/harness-e2e-next/app/api/harness/deepagents/basic/route.ts b/examples/harness-e2e-next/app/api/harness/deepagents/basic/route.ts new file mode 100644 index 000000000000..124b2ab965af --- /dev/null +++ b/examples/harness-e2e-next/app/api/harness/deepagents/basic/route.ts @@ -0,0 +1,35 @@ +import { deepAgentsHarnessAgent } from '@/agent/harness/deepagents/basic-agent'; +import { + detachAndPersist, + resumeOrCreateSession, +} from '@/util/harness-resume-store'; +import { + convertToModelMessages, + createUIMessageStreamResponse, + toUIMessageStream, + type UIMessage, +} from 'ai'; + +export async function POST(request: Request) { + const body: { + id?: string; + messages: UIMessage[]; + } = await request.json(); + + if (!body.id) { + return new Response('Missing chat id', { status: 400 }); + } + const chatId = body.id; + const messages = await convertToModelMessages(body.messages); + + const session = await resumeOrCreateSession(deepAgentsHarnessAgent, chatId); + + const result = await deepAgentsHarnessAgent.stream({ session, messages }); + + return createUIMessageStreamResponse({ + stream: toUIMessageStream({ + stream: result.stream, + onFinish: () => detachAndPersist(chatId, session), + }), + }); +} diff --git a/examples/harness-e2e-next/app/api/harness/deepagents/weather-approval/route.ts b/examples/harness-e2e-next/app/api/harness/deepagents/weather-approval/route.ts new file mode 100644 index 000000000000..b078b37769a1 --- /dev/null +++ b/examples/harness-e2e-next/app/api/harness/deepagents/weather-approval/route.ts @@ -0,0 +1,42 @@ +import { weatherApprovalDeepAgentsHarnessAgent } from '@/agent/harness/deepagents/weather-approval-agent'; +import { + detachAndPersist, + resumeOrCreateSession, +} from '@/util/harness-resume-store'; +import { + convertToModelMessages, + createUIMessageStreamResponse, + toUIMessageStream, + type UIMessage, +} from 'ai'; + +export async function POST(request: Request) { + const body: { + id?: string; + messages: UIMessage[]; + } = await request.json(); + + if (!body.id) { + return new Response('Missing chat id', { status: 400 }); + } + const chatId = body.id; + const messages = await convertToModelMessages(body.messages); + + const session = await resumeOrCreateSession( + weatherApprovalDeepAgentsHarnessAgent, + chatId, + ); + + const result = await weatherApprovalDeepAgentsHarnessAgent.stream({ + session, + messages, + }); + + return createUIMessageStreamResponse({ + stream: toUIMessageStream({ + stream: result.stream, + originalMessages: body.messages, + onFinish: () => detachAndPersist(chatId, session), + }), + }); +} diff --git a/examples/harness-e2e-next/app/api/harness/deepagents/weather/route.ts b/examples/harness-e2e-next/app/api/harness/deepagents/weather/route.ts new file mode 100644 index 000000000000..a5cb858cb3d7 --- /dev/null +++ b/examples/harness-e2e-next/app/api/harness/deepagents/weather/route.ts @@ -0,0 +1,41 @@ +import { weatherDeepAgentsHarnessAgent } from '@/agent/harness/deepagents/weather-agent'; +import { + detachAndPersist, + resumeOrCreateSession, +} from '@/util/harness-resume-store'; +import { + convertToModelMessages, + createUIMessageStreamResponse, + toUIMessageStream, + type UIMessage, +} from 'ai'; + +export async function POST(request: Request) { + const body: { + id?: string; + messages: UIMessage[]; + } = await request.json(); + + if (!body.id) { + return new Response('Missing chat id', { status: 400 }); + } + const chatId = body.id; + const messages = await convertToModelMessages(body.messages); + + const session = await resumeOrCreateSession( + weatherDeepAgentsHarnessAgent, + chatId, + ); + + const result = await weatherDeepAgentsHarnessAgent.stream({ + session, + messages, + }); + + return createUIMessageStreamResponse({ + stream: toUIMessageStream({ + stream: result.stream, + onFinish: () => detachAndPersist(chatId, session), + }), + }); +} diff --git a/examples/harness-e2e-next/app/harness/deepagents/ai-sdk-coding/page.tsx b/examples/harness-e2e-next/app/harness/deepagents/ai-sdk-coding/page.tsx new file mode 100644 index 000000000000..fb607997ac54 --- /dev/null +++ b/examples/harness-e2e-next/app/harness/deepagents/ai-sdk-coding/page.tsx @@ -0,0 +1,19 @@ +import ChatIdProvider from '@/components/chat-id-provider'; +import DeepAgentsHarnessChat from '@/components/deepagents-harness-chat'; + +export const metadata = { + title: 'DeepAgents — AI SDK Coding', +}; + +const STORAGE_KEY = 'harness-deepagents-ai-sdk-coding-chat-id'; + +export default function HarnessDeepAgentsAiSdkCodingPage() { + return ( + + + + ); +} diff --git a/examples/harness-e2e-next/app/harness/deepagents/basic-with-stop/page.tsx b/examples/harness-e2e-next/app/harness/deepagents/basic-with-stop/page.tsx new file mode 100644 index 000000000000..061ca9383553 --- /dev/null +++ b/examples/harness-e2e-next/app/harness/deepagents/basic-with-stop/page.tsx @@ -0,0 +1,19 @@ +import ChatIdProvider from '@/components/chat-id-provider'; +import DeepAgentsHarnessChat from '@/components/deepagents-harness-chat'; + +export const metadata = { + title: 'DeepAgents — Basic (with stop)', +}; + +const STORAGE_KEY = 'harness-deepagents-basic-with-stop-chat-id'; + +export default function HarnessDeepAgentsBasicWithStopPage() { + return ( + + + + ); +} diff --git a/examples/harness-e2e-next/app/harness/deepagents/basic/page.tsx b/examples/harness-e2e-next/app/harness/deepagents/basic/page.tsx new file mode 100644 index 000000000000..91ad46da119f --- /dev/null +++ b/examples/harness-e2e-next/app/harness/deepagents/basic/page.tsx @@ -0,0 +1,19 @@ +import ChatIdProvider from '@/components/chat-id-provider'; +import DeepAgentsHarnessChat from '@/components/deepagents-harness-chat'; + +export const metadata = { + title: 'DeepAgents — Basic', +}; + +const STORAGE_KEY = 'harness-deepagents-basic-chat-id'; + +export default function HarnessDeepAgentsPage() { + return ( + + + + ); +} diff --git a/examples/harness-e2e-next/app/harness/deepagents/weather-approval/page.tsx b/examples/harness-e2e-next/app/harness/deepagents/weather-approval/page.tsx new file mode 100644 index 000000000000..78567a0ca0f7 --- /dev/null +++ b/examples/harness-e2e-next/app/harness/deepagents/weather-approval/page.tsx @@ -0,0 +1,19 @@ +import ChatIdProvider from '@/components/chat-id-provider'; +import WeatherDeepAgentsHarnessChat from '@/components/weather-deepagents-harness-chat'; + +export const metadata = { + title: 'DeepAgents — Weather Approval', +}; + +const STORAGE_KEY = 'harness-deepagents-weather-approval-chat-id'; + +export default function HarnessDeepAgentsWeatherApprovalPage() { + return ( + + + + ); +} diff --git a/examples/harness-e2e-next/app/harness/deepagents/weather/page.tsx b/examples/harness-e2e-next/app/harness/deepagents/weather/page.tsx new file mode 100644 index 000000000000..13cef11b3928 --- /dev/null +++ b/examples/harness-e2e-next/app/harness/deepagents/weather/page.tsx @@ -0,0 +1,19 @@ +import ChatIdProvider from '@/components/chat-id-provider'; +import WeatherDeepAgentsHarnessChat from '@/components/weather-deepagents-harness-chat'; + +export const metadata = { + title: 'DeepAgents — Weather', +}; + +const STORAGE_KEY = 'harness-deepagents-weather-chat-id'; + +export default function HarnessDeepAgentsWeatherPage() { + return ( + + + + ); +} diff --git a/examples/harness-e2e-next/app/page.tsx b/examples/harness-e2e-next/app/page.tsx index fd7324c9058f..7c66b7a9f439 100644 --- a/examples/harness-e2e-next/app/page.tsx +++ b/examples/harness-e2e-next/app/page.tsx @@ -31,6 +31,17 @@ const HARNESSES = [ 'weather-approval', ], }, + { + slug: 'deepagents', + label: 'DeepAgents', + variants: [ + 'basic', + 'basic-with-stop', + 'ai-sdk-coding', + 'weather', + 'weather-approval', + ], + }, { slug: 'pi', label: 'Pi', diff --git a/examples/harness-e2e-next/components/deepagents-harness-chat.tsx b/examples/harness-e2e-next/components/deepagents-harness-chat.tsx new file mode 100644 index 000000000000..5d60d00943a4 --- /dev/null +++ b/examples/harness-e2e-next/components/deepagents-harness-chat.tsx @@ -0,0 +1,125 @@ +'use client'; + +import type { DeepAgentsHarnessAgentMessage } from '@/agent/harness/deepagents/basic-agent'; +import { Response } from '@/components/ai-elements/response'; +import { useChatId } from '@/components/chat-id-provider'; +import ChatInput from '@/components/chat-input'; +import DynamicToolView from '@/components/tool/dynamic-tool-view'; +import HarnessBashToolView from '@/components/tool/harness-bash-tool-view'; +import HarnessFileToolView from '@/components/tool/harness-file-tool-view'; +import HarnessToolView from '@/components/tool/harness-tool-view'; +import { useChat } from '@ai-sdk/react'; +import { DefaultChatTransport } from 'ai'; + +export default function DeepAgentsHarnessChat({ + apiRoute, + exampleLabel, +}: { + apiRoute: string; + exampleLabel: string; +}) { + const { chatId, resetChatId } = useChatId(); + const { error, status, sendMessage, messages, regenerate } = + useChat({ + id: chatId, + transport: new DefaultChatTransport({ api: apiRoute }), + }); + + return ( +
+

DeepAgents — {exampleLabel}

+

+ chat id: {chatId} + +

+ + {messages.map(message => ( +
+ {message.role === 'user' ? 'You: ' : 'AI: '} + {message.parts.map((part, index) => { + switch (part.type) { + case 'text': { + return ( + + {part.text} + + ); + } + case 'reasoning': { + return ( + + {part.text} + + ); + } + case 'tool-bash': { + return ; + } + case 'tool-read': + case 'tool-write': { + return ; + } + case 'tool-grep': { + return ( + + ); + } + case 'dynamic-tool': { + return ; + } + } + })} +
+ ))} + + {status === 'submitted' && ( +
+ )} + + {error && ( +
+
+ {error.message || String(error)} +
+ +
+ )} + +
+ + sendMessage({ text })} + /> +
+ ); +} diff --git a/examples/harness-e2e-next/components/weather-deepagents-harness-chat.tsx b/examples/harness-e2e-next/components/weather-deepagents-harness-chat.tsx new file mode 100644 index 000000000000..0a6d8a4b2e5d --- /dev/null +++ b/examples/harness-e2e-next/components/weather-deepagents-harness-chat.tsx @@ -0,0 +1,128 @@ +'use client'; + +import type { WeatherDeepAgentsHarnessAgentMessage } from '@/agent/harness/deepagents/weather-agent'; +import { Response } from '@/components/ai-elements/response'; +import { useChatId } from '@/components/chat-id-provider'; +import ChatInput from '@/components/chat-input'; +import DynamicToolView from '@/components/tool/dynamic-tool-view'; +import HarnessBashToolView from '@/components/tool/harness-bash-tool-view'; +import HarnessFileToolView from '@/components/tool/harness-file-tool-view'; +import WeatherView from '@/components/tool/weather-tool-view'; +import { useChat } from '@ai-sdk/react'; +import { + DefaultChatTransport, + lastAssistantMessageIsCompleteWithApprovalResponses, +} from 'ai'; + +export default function WeatherDeepAgentsHarnessChat({ + apiRoute, + exampleLabel, +}: { + apiRoute: string; + exampleLabel: string; +}) { + const { chatId, resetChatId } = useChatId(); + const { + error, + status, + sendMessage, + messages, + regenerate, + addToolApprovalResponse, + } = useChat({ + id: chatId, + transport: new DefaultChatTransport({ api: apiRoute }), + sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses, + }); + + return ( +
+

DeepAgents — {exampleLabel}

+

+ chat id: {chatId} + +

+ + {messages.map(message => ( +
+ {message.role === 'user' ? 'You: ' : 'AI: '} + {message.parts.map((part, index) => { + switch (part.type) { + case 'text': { + return ( + + {part.text} + + ); + } + case 'reasoning': { + return ( + + {part.text} + + ); + } + case 'tool-get_weather': { + return ( + + ); + } + case 'tool-bash': { + return ; + } + case 'tool-read': + case 'tool-write': { + return ; + } + case 'dynamic-tool': { + return ; + } + } + })} +
+ ))} + + {status === 'submitted' && ( +
+ )} + + {error && ( +
+
+ {error.message || String(error)} +
+ +
+ )} + +
+ + sendMessage({ text })} + /> +
+ ); +} diff --git a/examples/harness-e2e-next/package.json b/examples/harness-e2e-next/package.json index cd7c44e3ef8f..b8e88d7d34a5 100644 --- a/examples/harness-e2e-next/package.json +++ b/examples/harness-e2e-next/package.json @@ -12,6 +12,7 @@ "@ai-sdk/harness": "workspace:*", "@ai-sdk/harness-claude-code": "workspace:*", "@ai-sdk/harness-codex": "workspace:*", + "@ai-sdk/harness-deepagents": "workspace:*", "@ai-sdk/harness-pi": "workspace:*", "@ai-sdk/provider-utils": "workspace:*", "@ai-sdk/react": "workspace:*", diff --git a/examples/harness-e2e-tui/agents/deepagents/ai-sdk-coding-agent.ts b/examples/harness-e2e-tui/agents/deepagents/ai-sdk-coding-agent.ts new file mode 100644 index 000000000000..cfda0adf63c7 --- /dev/null +++ b/examples/harness-e2e-tui/agents/deepagents/ai-sdk-coding-agent.ts @@ -0,0 +1,52 @@ +import { HarnessAgent } from '@ai-sdk/harness/agent'; +import { deepAgents } from '@ai-sdk/harness-deepagents'; +import { createVercelSandbox } from '@ai-sdk/sandbox-vercel'; +import type { InferUITools, UIMessage } from 'ai'; + +// Default sandbox resources won't allow a full parallel build of all packages; +// guide the harness to use a lower turbo concurrency instead. +const instructions = ` +Building all packages at once (e.g. running \`pnpm build\` or \`pnpm build:packages\`) +will exceed sandbox memory. When asked to do this, use the corresponding +\`pnpm exec turbo\` call directly with a lower \`--concurrency=4\` flag. +`; + +export const aiSdkCodingDeepAgentsHarnessAgent = new HarnessAgent({ + harness: deepAgents, + instructions, + sandbox: createVercelSandbox({ + runtime: 'node24', + ports: [4000], + }), + onSandboxSession: async ({ session, sessionWorkDir, abortSignal }) => { + const result = await session.run({ + command: + 'test -d .git || git clone --depth 1 https://github.com/vercel/ai.git .', + workingDirectory: sessionWorkDir, + abortSignal, + }); + if (result.exitCode !== 0) { + throw new Error( + `Failed to clone vercel/ai (exit ${result.exitCode}): ${result.stderr}`, + ); + } + + const installResult = await session.run({ + command: 'test -d node_modules || pnpm install', + workingDirectory: sessionWorkDir, + abortSignal, + }); + if (installResult.exitCode !== 0) { + throw new Error( + `Failed to install dependencies (exit ${installResult.exitCode}): ${installResult.stderr}`, + ); + } + }, +}); + +// See basic-agent.ts for why the UIMessage type derives from agent.tools. +export type AiSdkCodingDeepAgentsHarnessAgentMessage = UIMessage< + unknown, + never, + InferUITools +>; diff --git a/examples/harness-e2e-tui/agents/deepagents/basic-agent.ts b/examples/harness-e2e-tui/agents/deepagents/basic-agent.ts new file mode 100644 index 000000000000..996396d7b404 --- /dev/null +++ b/examples/harness-e2e-tui/agents/deepagents/basic-agent.ts @@ -0,0 +1,32 @@ +import { + createFileReporter, + createTraceTreeReporter, + HarnessAgent, +} from '@ai-sdk/harness/agent'; +import { deepAgents } from '@ai-sdk/harness-deepagents'; +import { createVercelSandbox } from '@ai-sdk/sandbox-vercel'; +import type { InferUITools, UIMessage } from 'ai'; + +export const deepAgentsHarnessAgent = new HarnessAgent({ + harness: deepAgents, + sandbox: createVercelSandbox({ + runtime: 'node24', + ports: [4000], + }), + // Observability wired in code (dev/testing app). Trace tree + diagnostics + // print to the terminal; the file reporter writes a per-agent `events.jsonl`. + debug: { enabled: true }, + telemetry: { + integrations: [ + createTraceTreeReporter(), + createFileReporter({ dir: '.harness-observability/deepagents/basic' }), + ], + }, +}); + +// Derived from `agent.tools` (not InferAgentUIMessage) — see Codex/OpenCode basic agents. +export type DeepAgentsHarnessAgentMessage = UIMessage< + unknown, + never, + InferUITools +>; diff --git a/examples/harness-e2e-tui/agents/deepagents/weather-agent.ts b/examples/harness-e2e-tui/agents/deepagents/weather-agent.ts new file mode 100644 index 000000000000..05783f95d2f1 --- /dev/null +++ b/examples/harness-e2e-tui/agents/deepagents/weather-agent.ts @@ -0,0 +1,47 @@ +import { weatherTool } from '@/lib/tools/weather-tool'; +import { + WEATHER_CODES_REFERENCE, + weatherCodesSkill, + weatherForecastSkill, + weatherInstructions, +} from '@/lib/weather-utils'; +import { + createFileReporter, + createTraceTreeReporter, + HarnessAgent, +} from '@ai-sdk/harness/agent'; +import { deepAgents } from '@ai-sdk/harness-deepagents'; +import { createVercelSandbox } from '@ai-sdk/sandbox-vercel'; +import type { InferUITools, UIMessage } from 'ai'; + +export const weatherDeepAgentsHarnessAgent = new HarnessAgent({ + harness: deepAgents, + instructions: weatherInstructions, + skills: [weatherForecastSkill, weatherCodesSkill], + tools: { get_weather: weatherTool }, + sandbox: createVercelSandbox({ + runtime: 'node24', + ports: [4000], + }), + onSandboxSession: async ({ session, sessionWorkDir, abortSignal }) => { + await session.writeTextFile({ + path: `${sessionWorkDir}/weather-codes.md`, + content: WEATHER_CODES_REFERENCE, + abortSignal, + }); + }, + debug: { enabled: true }, + telemetry: { + integrations: [ + createTraceTreeReporter(), + createFileReporter({ dir: '.harness-observability/deepagents/weather' }), + ], + }, +}); + +// See basic-agent.ts for why the UIMessage type derives from agent.tools. +export type WeatherDeepAgentsHarnessAgentMessage = UIMessage< + unknown, + never, + InferUITools +>; diff --git a/examples/harness-e2e-tui/agents/deepagents/weather-approval-agent.ts b/examples/harness-e2e-tui/agents/deepagents/weather-approval-agent.ts new file mode 100644 index 000000000000..53f5e80f97b4 --- /dev/null +++ b/examples/harness-e2e-tui/agents/deepagents/weather-approval-agent.ts @@ -0,0 +1,53 @@ +import { weatherTool } from '@/lib/tools/weather-tool'; +import { + WEATHER_CODES_REFERENCE, + weatherCodesSkill, + weatherForecastSkill, + weatherInstructions, +} from '@/lib/weather-utils'; +import { + createFileReporter, + createTraceTreeReporter, + HarnessAgent, +} from '@ai-sdk/harness/agent'; +import { deepAgents } from '@ai-sdk/harness-deepagents'; +import { createVercelSandbox } from '@ai-sdk/sandbox-vercel'; +import type { InferUITools, UIMessage } from 'ai'; + +export const weatherApprovalDeepAgentsHarnessAgent = new HarnessAgent({ + harness: deepAgents, + instructions: weatherInstructions, + skills: [weatherForecastSkill, weatherCodesSkill], + tools: { get_weather: weatherTool }, + // Host-tool approval is handled by HarnessAgent, independent of the adapter's + // built-in tool approval support. + toolApproval: { + get_weather: 'user-approval', + }, + sandbox: createVercelSandbox({ + runtime: 'node24', + ports: [4000], + }), + onSandboxSession: async ({ session, sessionWorkDir, abortSignal }) => { + await session.writeTextFile({ + path: `${sessionWorkDir}/weather-codes.md`, + content: WEATHER_CODES_REFERENCE, + abortSignal, + }); + }, + debug: { enabled: true }, + telemetry: { + integrations: [ + createTraceTreeReporter(), + createFileReporter({ + dir: '.harness-observability/deepagents/weather-approval', + }), + ], + }, +}); + +export type WeatherApprovalDeepAgentsHarnessAgentMessage = UIMessage< + unknown, + never, + InferUITools +>; diff --git a/examples/harness-e2e-tui/harness/deepagents/ai-sdk-coding.ts b/examples/harness-e2e-tui/harness/deepagents/ai-sdk-coding.ts new file mode 100644 index 000000000000..59a8d1793040 --- /dev/null +++ b/examples/harness-e2e-tui/harness/deepagents/ai-sdk-coding.ts @@ -0,0 +1,8 @@ +import { aiSdkCodingDeepAgentsHarnessAgent } from '../../agents/deepagents/ai-sdk-coding-agent'; +import { runTUI } from '../../lib/run-tui'; + +await runTUI({ + agent: aiSdkCodingDeepAgentsHarnessAgent, + entrypointUrl: import.meta.url, + title: 'DeepAgents — AI SDK Coding', +}); diff --git a/examples/harness-e2e-tui/harness/deepagents/basic.ts b/examples/harness-e2e-tui/harness/deepagents/basic.ts new file mode 100644 index 000000000000..366a6a720bc9 --- /dev/null +++ b/examples/harness-e2e-tui/harness/deepagents/basic.ts @@ -0,0 +1,8 @@ +import { deepAgentsHarnessAgent } from '../../agents/deepagents/basic-agent'; +import { runTUI } from '../../lib/run-tui'; + +await runTUI({ + agent: deepAgentsHarnessAgent, + entrypointUrl: import.meta.url, + title: 'DeepAgents — Basic', +}); diff --git a/examples/harness-e2e-tui/harness/deepagents/weather-approval.ts b/examples/harness-e2e-tui/harness/deepagents/weather-approval.ts new file mode 100644 index 000000000000..02016f3d5cf6 --- /dev/null +++ b/examples/harness-e2e-tui/harness/deepagents/weather-approval.ts @@ -0,0 +1,8 @@ +import { weatherApprovalDeepAgentsHarnessAgent } from '../../agents/deepagents/weather-approval-agent'; +import { runTUI } from '../../lib/run-tui'; + +await runTUI({ + agent: weatherApprovalDeepAgentsHarnessAgent, + entrypointUrl: import.meta.url, + title: 'DeepAgents — Weather Approval', +}); diff --git a/examples/harness-e2e-tui/harness/deepagents/weather.ts b/examples/harness-e2e-tui/harness/deepagents/weather.ts new file mode 100644 index 000000000000..ffe5c2dd9fb0 --- /dev/null +++ b/examples/harness-e2e-tui/harness/deepagents/weather.ts @@ -0,0 +1,8 @@ +import { weatherDeepAgentsHarnessAgent } from '../../agents/deepagents/weather-agent'; +import { runTUI } from '../../lib/run-tui'; + +await runTUI({ + agent: weatherDeepAgentsHarnessAgent, + entrypointUrl: import.meta.url, + title: 'DeepAgents — Weather', +}); diff --git a/examples/harness-e2e-tui/package.json b/examples/harness-e2e-tui/package.json index c7ca7695be8d..32763cadfcff 100644 --- a/examples/harness-e2e-tui/package.json +++ b/examples/harness-e2e-tui/package.json @@ -10,6 +10,7 @@ "@ai-sdk/harness": "workspace:*", "@ai-sdk/harness-claude-code": "workspace:*", "@ai-sdk/harness-codex": "workspace:*", + "@ai-sdk/harness-deepagents": "workspace:*", "@ai-sdk/harness-pi": "workspace:*", "@ai-sdk/sandbox-vercel": "workspace:*", "@ai-sdk/tui": "workspace:*", diff --git a/packages/harness-deepagents/CHANGELOG.md b/packages/harness-deepagents/CHANGELOG.md new file mode 100644 index 000000000000..ab63fb2ed893 --- /dev/null +++ b/packages/harness-deepagents/CHANGELOG.md @@ -0,0 +1 @@ +# @ai-sdk/harness-deepagents diff --git a/packages/harness-deepagents/README.md b/packages/harness-deepagents/README.md new file mode 100644 index 000000000000..2fd93554b744 --- /dev/null +++ b/packages/harness-deepagents/README.md @@ -0,0 +1,68 @@ +# @ai-sdk/harness-deepagents + +A [HarnessV1](../harness) adapter that runs [DeepAgents](https://github.com/langchain-ai/deepagentsjs) +(LangChain's LangGraph-based agent harness) as a coding-agent runtime inside an +AI SDK sandbox. + +This is a **bridge-backed** harness: the DeepAgents runtime runs inside the +sandbox via a Node bridge (`node bridge.mjs`) built on the shared +`@ai-sdk/harness/bridge` runtime, while the host adapter drives turns over a +WebSocket. + +> **Status: happy-path validated.** The host adapter (`doStart` + session: +> `doPromptTurn`/`doStop`/`doDestroy`) and the Node bridge (driving the +> `deepagents` npm package via `createDeepAgent` + `streamEvents`) are validated +> end-to-end against a live Vercel Sandbox: text generation, streaming, +> multi-turn memory, and host-executed tools all work. Turn continuation, +> suspend/detach, cross-process resume, and built-in tool approvals throw +> `HarnessCapabilityUnsupportedError` and are follow-ups. + +## Setup + +```bash +pnpm add @ai-sdk/harness-deepagents @ai-sdk/harness +``` + +The harness installs the +bridge's Node dependencies (the `deepagents` package and LangChain) into its +bootstrap directory via `pnpm` at startup. + +## Usage + +```ts +import { HarnessAgent } from '@ai-sdk/harness/agent'; +import { deepAgents } from '@ai-sdk/harness-deepagents'; + +const agent = new HarnessAgent({ + harness: deepAgents, + // ...sandbox provider configuration +}); +``` + +Configure the model and auth via `createDeepAgents({ model, auth })`. + +## Auth + +Auth is optional and provider-aware: the provider is resolved from the model id +(`anthropic/…` or `openai/…`, defaulting to Anthropic). With none configured the +adapter falls back to ambient AI Gateway credentials (`AI_GATEWAY_API_KEY`, then +`VERCEL_OIDC_TOKEN`), then ambient provider creds (`ANTHROPIC_*` / `OPENAI_*`). +Pin explicitly with `auth.anthropic`, `auth.openai`, or `auth.gateway`: + +```ts +createDeepAgents({ + model: 'openai/gpt-5', + auth: { openai: { apiKey: process.env.OPENAI_API_KEY } }, +}); +``` + +## Built-in tools + +| Common name | Native (LangGraph) tool | +| ----------- | ----------------------- | +| `read` | `read_file` | +| `write` | `write_file` | +| `bash` | `shell` | +| `grep` | `search` | + +See the [harness docs](https://ai-sdk.dev/docs) for broader concepts. diff --git a/packages/harness-deepagents/package.json b/packages/harness-deepagents/package.json new file mode 100644 index 000000000000..2bc19f1d1184 --- /dev/null +++ b/packages/harness-deepagents/package.json @@ -0,0 +1,76 @@ +{ + "name": "@ai-sdk/harness-deepagents", + "version": "0.0.0", + "type": "module", + "license": "Apache-2.0", + "sideEffects": false, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "source": "./src/index.ts", + "files": [ + "dist/**/*", + "src", + "!src/**/*.test.ts", + "!src/**/*.test-d.ts", + "!src/**/__snapshots__", + "!src/**/__fixtures__", + "CHANGELOG.md", + "README.md" + ], + "scripts": { + "build": "pnpm clean && tsup --tsconfig tsconfig.build.json && pnpm copy-bridge-assets", + "build:watch": "pnpm clean && tsup --watch", + "clean": "del-cli dist *.tsbuildinfo", + "copy-bridge-assets": "node -e \"import('node:fs/promises').then(async fs => { await fs.copyFile('src/bridge/package.json', 'dist/bridge/package.json'); await fs.copyFile('src/bridge/pnpm-lock.yaml', 'dist/bridge/pnpm-lock.yaml'); })\"", + "type-check": "tsc --build", + "test": "pnpm test:node", + "test:watch": "vitest --config vitest.node.config.js", + "test:node": "vitest --config vitest.node.config.js --run" + }, + "exports": { + "./package.json": "./package.json", + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "dependencies": { + "@ai-sdk/harness": "workspace:*", + "@ai-sdk/provider-utils": "workspace:*", + "ws": "8.21.0", + "zod": "3.25.76" + }, + "devDependencies": { + "@langchain/core": "^1.1.44", + "@langchain/langgraph": "^1.3.0", + "@types/node": "22.19.19", + "@types/ws": "^8.5.13", + "@vercel/ai-tsconfig": "workspace:*", + "deepagents": "1.10.2", + "tsup": "^8.5.1", + "typescript": "5.8.3" + }, + "engines": { + "node": ">=22" + }, + "publishConfig": { + "access": "public", + "provenance": true + }, + "homepage": "https://ai-sdk.dev/docs", + "repository": { + "type": "git", + "url": "https://github.com/vercel/ai", + "directory": "packages/harness-deepagents" + }, + "bugs": { + "url": "https://github.com/vercel/ai/issues" + }, + "keywords": [ + "ai", + "harness", + "deepagents", + "langgraph" + ] +} diff --git a/packages/harness-deepagents/src/bridge/approvals.test.ts b/packages/harness-deepagents/src/bridge/approvals.test.ts new file mode 100644 index 000000000000..59218a67c82e --- /dev/null +++ b/packages/harness-deepagents/src/bridge/approvals.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, it } from 'vitest'; +import { + buildInterruptOn, + builtinToolRequiresApproval, + collectActionRequests, +} from './approvals'; + +describe('builtinToolRequiresApproval', () => { + it('never requires approval under allow-all', () => { + expect(builtinToolRequiresApproval('readonly', 'allow-all')).toBe(false); + expect(builtinToolRequiresApproval('edit', 'allow-all')).toBe(false); + expect(builtinToolRequiresApproval('bash', 'allow-all')).toBe(false); + }); + + it('only gates bash under allow-edits', () => { + expect(builtinToolRequiresApproval('readonly', 'allow-edits')).toBe(false); + expect(builtinToolRequiresApproval('edit', 'allow-edits')).toBe(false); + expect(builtinToolRequiresApproval('bash', 'allow-edits')).toBe(true); + }); + + it('gates edit and bash under allow-reads', () => { + expect(builtinToolRequiresApproval('readonly', 'allow-reads')).toBe(false); + expect(builtinToolRequiresApproval('edit', 'allow-reads')).toBe(true); + expect(builtinToolRequiresApproval('bash', 'allow-reads')).toBe(true); + }); +}); + +describe('buildInterruptOn', () => { + it('returns undefined when no gating is needed', () => { + expect(buildInterruptOn(undefined)).toBeUndefined(); + expect(buildInterruptOn('allow-all')).toBeUndefined(); + }); + + it('gates only execute under allow-edits', () => { + expect(buildInterruptOn('allow-edits')).toEqual({ + execute: { allowedDecisions: ['approve', 'reject'] }, + }); + }); + + it('gates write, edit, and execute under allow-reads', () => { + expect(buildInterruptOn('allow-reads')).toEqual({ + write_file: { allowedDecisions: ['approve', 'reject'] }, + edit_file: { allowedDecisions: ['approve', 'reject'] }, + execute: { allowedDecisions: ['approve', 'reject'] }, + }); + }); +}); + +describe('collectActionRequests', () => { + it('flattens action requests across interrupts and defaults missing args', () => { + expect( + collectActionRequests([ + { + value: { + actionRequests: [ + { name: 'execute', args: { command: 'rm -rf /' } }, + { name: 'write_file' }, + ], + }, + }, + { value: { actionRequests: [{ name: 'edit_file', args: { a: 1 } }] } }, + ]), + ).toEqual([ + { name: 'execute', args: { command: 'rm -rf /' } }, + { name: 'write_file', args: {} }, + { name: 'edit_file', args: { a: 1 } }, + ]); + }); + + it('ignores interrupts without action requests', () => { + expect( + collectActionRequests([{ value: undefined }, { value: {} }]), + ).toEqual([]); + }); +}); diff --git a/packages/harness-deepagents/src/bridge/approvals.ts b/packages/harness-deepagents/src/bridge/approvals.ts new file mode 100644 index 000000000000..391390b5de37 --- /dev/null +++ b/packages/harness-deepagents/src/bridge/approvals.ts @@ -0,0 +1,61 @@ +import type { StartMessage } from '../deepagents-bridge-protocol'; + +export type PermissionMode = NonNullable; + +// Native built-in tool -> approval kind (mirrors the host adapter's toolUseKind). +export const NATIVE_TOOL_KIND: Readonly< + Record +> = { + read_file: 'readonly', + write_file: 'edit', + edit_file: 'edit', + execute: 'bash', + grep: 'readonly', + glob: 'readonly', + ls: 'readonly', +}; + +export function builtinToolRequiresApproval( + kind: 'readonly' | 'edit' | 'bash', + permissionMode: PermissionMode, +): boolean { + if (permissionMode === 'allow-all') return false; + if (permissionMode === 'allow-edits') return kind === 'bash'; + return kind === 'edit' || kind === 'bash'; +} + +// Per-tool HITL config for createDeepAgent; only built-ins are gated (host tools approve at the agent layer). +export function buildInterruptOn( + permissionMode: PermissionMode | undefined, +): + | Record }> + | undefined { + if (!permissionMode || permissionMode === 'allow-all') return undefined; + const config: Record< + string, + { allowedDecisions: Array<'approve' | 'reject'> } + > = {}; + for (const [nativeName, kind] of Object.entries(NATIVE_TOOL_KIND)) { + if (builtinToolRequiresApproval(kind, permissionMode)) { + config[nativeName] = { allowedDecisions: ['approve', 'reject'] }; + } + } + return Object.keys(config).length > 0 ? config : undefined; +} + +type ActionRequest = { name: string; args?: Record }; +type HITLInterruptValue = { actionRequests?: ActionRequest[] }; + +// Flatten LangChain HITL interrupt payloads into the gated tool calls awaiting a decision. +export function collectActionRequests( + interrupts: Array<{ value?: unknown }>, +): { name: string; args: Record }[] { + const out: { name: string; args: Record }[] = []; + for (const interrupt of interrupts) { + const value = interrupt.value as HITLInterruptValue | undefined; + for (const action of value?.actionRequests ?? []) { + out.push({ name: action.name, args: action.args ?? {} }); + } + } + return out; +} diff --git a/packages/harness-deepagents/src/bridge/index.ts b/packages/harness-deepagents/src/bridge/index.ts new file mode 100644 index 000000000000..2dd86381a045 --- /dev/null +++ b/packages/harness-deepagents/src/bridge/index.ts @@ -0,0 +1,386 @@ +// In-sandbox turn driver on `@ai-sdk/harness/bridge`; third-party imports stay external (tsup) and install in-sandbox from src/bridge/package.json — keep import/externals/deps in sync. + +import { randomUUID } from 'node:crypto'; +import { argv } from 'node:process'; +import { + runBridge, + type BridgeEvent, + type BridgeTurn, +} from '@ai-sdk/harness/bridge'; +import { tool } from '@langchain/core/tools'; +import { Command, MemorySaver } from '@langchain/langgraph'; +import { createDeepAgent, LocalShellBackend } from 'deepagents'; +import type { StartMessage } from '../deepagents-bridge-protocol'; +import { buildInterruptOn, collectActionRequests } from './approvals'; +import { jsonSchemaToZodObject } from './json-schema-to-zod'; + +// Native DeepAgents tool name -> harness-v1 common name (renames only; grep/glob/ls/task/write_todos forward unchanged). +const NATIVE_TO_COMMON: Readonly> = { + read_file: 'read', + write_file: 'write', + edit_file: 'edit', + execute: 'bash', +}; + +function toCommonName(nativeName: string): string { + return NATIVE_TO_COMMON[nativeName] ?? nativeName; +} + +function parseArgs(rawArgs: string[]): Record { + const out: Record = {}; + for (let i = 0; i < rawArgs.length; i++) { + const arg = rawArgs[i]; + if (arg.startsWith('--')) { + const key = arg + .slice(2) + .replace(/-([a-z])/g, (_, c: string) => c.toUpperCase()); + out[key] = rawArgs[i + 1]; + i++; + } + } + return out; +} + +// LangChain wants `provider:model`; the host sends `provider/model`. +function parseModelName(raw: string): string { + return raw.includes('/') ? raw.replace('/', ':') : raw; +} + +// LangChain reports some built-in tool args wrapped as `{ input: "" }`; unwrap to the inner JSON so AI SDK validates the real shape. +function toToolCallInput(raw: unknown): string { + if ( + raw && + typeof raw === 'object' && + !Array.isArray(raw) && + Object.keys(raw).length === 1 && + typeof (raw as { input?: unknown }).input === 'string' + ) { + const inner = (raw as { input: string }).input; + if (/^\s*[[{]/.test(inner)) return inner; + } + return JSON.stringify(raw ?? {}); +} + +const args = parseArgs(argv.slice(2)); +const workdir = args.workdir; +const bridgeStateDir = args.bridgeStateDir; +if (!workdir || !bridgeStateDir) { + // eslint-disable-next-line no-console + console.error('deepagents bridge: missing --workdir / --bridge-state-dir'); + process.exit(1); +} + +// One agent per bridge process, reused across turns; host tools read the live turn via `currentTurn`. +let agent: ReturnType | undefined; +let currentTurn: BridgeTurn | undefined; + +// Host tools become LangChain tools that emit a `tool-call` and block on the host's `tool-result`. +function buildHostTools(toolSchemas: StartMessage['tools']) { + return (toolSchemas ?? []).map(schema => + tool( + async (input: Record) => { + const turn = currentTurn; + if (!turn) throw new Error('no active turn'); + const toolCallId = `${schema.name}-${randomUUID()}`; + turn.emit({ + type: 'tool-call', + toolCallId, + toolName: schema.name, + input: JSON.stringify(input), + providerExecuted: false, + } as BridgeEvent); + const { output } = await turn.requestToolResult(toolCallId); + return typeof output === 'string' ? output : JSON.stringify(output); + }, + { + name: schema.name, + description: schema.description ?? '', + schema: jsonSchemaToZodObject(schema.inputSchema), + }, + ), + ); +} + +async function runTurn(start: StartMessage, turn: BridgeTurn): Promise { + currentTurn = turn; + const emit = (event: Record) => + turn.emit(event as BridgeEvent); + + const interruptOn = buildInterruptOn(start.permissionMode); + if (!agent) { + agent = createDeepAgent({ + // Defer to DeepAgents' own default when the host configured no model. + ...(start.model ? { model: parseModelName(start.model) } : {}), + tools: buildHostTools(start.tools), + backend: new LocalShellBackend({ rootDir: workdir }), + systemPrompt: start.instructions || undefined, + // Native skills loaded from the host-materialized source dir (on-demand, with working file refs). + ...(start.skillsPath ? { skills: [start.skillsPath] } : {}), + // Gate built-in tools behind HITL approval when the permission mode requires it. + ...(interruptOn ? { interruptOn } : {}), + // Real instance (LangGraph rejects `true` for root graphs); gives multi-turn memory. + checkpointer: new MemorySaver(), + }); + } + + emit({ + type: 'stream-start', + ...(start.model ? { modelId: start.model } : {}), + }); + + const hostToolNames = new Set((start.tools ?? []).map(t => t.name)); + let textBlockId: string | undefined; + let reasoningBlockId: string | undefined; + let inputTokens = 0; + let outputTokens = 0; + // Per-call streamed-usage fallback (max over chunks), used only when model-end carries no usage. + let streamedStepInput = 0; + let streamedStepOutput = 0; + const activeToolRunIds = new Set(); + // Approval-gated tools are announced before execution; these tie the later run back to the approval id and dedup the call. + const approvedToolQueue = new Map(); + const approvedRunIds = new Map(); + + const ensureTextBlock = (): string => { + if (!textBlockId) { + textBlockId = `text-${randomUUID()}`; + emit({ type: 'text-start', id: textBlockId }); + } + return textBlockId; + }; + const endTextBlock = () => { + if (textBlockId) { + emit({ type: 'text-end', id: textBlockId }); + textBlockId = undefined; + } + }; + const endReasoningBlock = () => { + if (reasoningBlockId) { + emit({ type: 'reasoning-end', id: reasoningBlockId }); + reasoningBlockId = undefined; + } + }; + // Text and reasoning are mutually exclusive open blocks: starting one closes the other. + const emitText = (delta: string) => { + endReasoningBlock(); + emit({ type: 'text-delta', id: ensureTextBlock(), delta }); + }; + const emitReasoning = (delta: string) => { + endTextBlock(); + if (!reasoningBlockId) { + reasoningBlockId = `reasoning-${randomUUID()}`; + emit({ type: 'reasoning-start', id: reasoningBlockId }); + } + emit({ type: 'reasoning-delta', id: reasoningBlockId, delta }); + }; + + const config = { + version: 'v2' as const, + configurable: { thread_id: 'bridge-session' }, + recursionLimit: 50, + signal: turn.abortSignal, + }; + + // After a stream segment ends, return the tool calls paused by HITL interrupts (empty when the turn is truly done). + const readPendingApprovals = async () => { + try { + const state = (await agent!.getState({ + configurable: { thread_id: 'bridge-session' }, + })) as { tasks?: Array<{ interrupts?: Array<{ value?: unknown }> }> }; + return collectActionRequests( + (state.tasks ?? []).flatMap(t => t.interrupts ?? []), + ); + } catch { + return []; + } + }; + + let resumeInput: unknown = { + messages: [{ role: 'user', content: start.prompt }], + }; + + while (true) { + const stream = await agent.streamEvents(resumeInput as never, config); + + for await (const event of stream) { + const kind = event.event; + const data = (event.data ?? {}) as Record; + + if (kind === 'on_chat_model_stream') { + const parentIds = (event as { parent_ids?: string[] }).parent_ids ?? []; + if (parentIds.some(id => activeToolRunIds.has(id))) continue; + const chunk = data.chunk as + | { + content?: unknown; + usage_metadata?: { + input_tokens?: number; + output_tokens?: number; + }; + } + | undefined; + if (!chunk) continue; + const content = chunk.content; + if (typeof content === 'string' && content) { + emitText(content); + } else if (Array.isArray(content)) { + for (const block of content) { + if (block && typeof block === 'object') { + const b = block as { + type?: string; + text?: string; + thinking?: string; + }; + if (b.type === 'text' && b.text) emitText(b.text); + else if (b.type === 'thinking' && b.thinking) + emitReasoning(b.thinking); + } + } + } + const usage = chunk.usage_metadata; + if (usage) { + streamedStepInput = Math.max( + streamedStepInput, + usage.input_tokens ?? 0, + ); + streamedStepOutput = Math.max( + streamedStepOutput, + usage.output_tokens ?? 0, + ); + } + } else if (kind === 'on_chat_model_end') { + // Final usage lands on model-end, not the chunks; each model call is one step. + const output = data.output as + | { + usage_metadata?: { + input_tokens?: number; + output_tokens?: number; + }; + } + | undefined; + const usage = output?.usage_metadata; + // One model call = one step; count its usage exactly once (model-end usage, else the streamed max). + const stepInput = usage?.input_tokens ?? streamedStepInput; + const stepOutput = usage?.output_tokens ?? streamedStepOutput; + inputTokens += stepInput; + outputTokens += stepOutput; + streamedStepInput = 0; + streamedStepOutput = 0; + endTextBlock(); + endReasoningBlock(); + turn.emit({ + type: 'finish-step', + finishReason: { unified: 'stop' }, + usage: { + inputTokens: { total: stepInput }, + outputTokens: { total: stepOutput }, + }, + }); + } else if (kind === 'on_tool_start') { + const toolName = (event.name as string) ?? 'unknown'; + const runId = (event.run_id as string) ?? ''; + if (runId) activeToolRunIds.add(runId); + // Host tools emit their own tool-call; only surface builtin (providerExecuted) tools here. + if (!hostToolNames.has(toolName)) { + const queued = approvedToolQueue.get(toolName); + if (queued && queued.length > 0) { + // Already announced at approval time; tie this run to that id and don't re-emit the call. + const approvalId = queued.shift()!; + if (runId) approvedRunIds.set(runId, approvalId); + } else { + endTextBlock(); + endReasoningBlock(); + emit({ + type: 'tool-call', + toolCallId: runId, + toolName: toCommonName(toolName), + input: toToolCallInput(data.input), + providerExecuted: true, + nativeName: toolName, + }); + } + } + } else if (kind === 'on_tool_end') { + const toolName = (event.name as string) ?? 'unknown'; + const runId = (event.run_id as string) ?? ''; + if (!hostToolNames.has(toolName)) { + let output: unknown = data.output ?? ''; + if (output && typeof output === 'object' && 'content' in output) { + output = (output as { content: unknown }).content; + } + emit({ + type: 'tool-result', + toolCallId: approvedRunIds.get(runId) ?? runId, + toolName: toCommonName(toolName), + result: output ?? null, + }); + approvedRunIds.delete(runId); + } + if (runId) activeToolRunIds.delete(runId); + } + } + + const actionRequests = await readPendingApprovals(); + if (actionRequests.length === 0) break; + + // HITL paused the run: announce each gated call, collect host decisions, then resume. + const decisions: Array< + { type: 'approve' } | { type: 'reject'; message?: string } + > = []; + for (const action of actionRequests) { + const approvalId = `approval-${randomUUID()}`; + endTextBlock(); + endReasoningBlock(); + emit({ + type: 'tool-call', + toolCallId: approvalId, + toolName: toCommonName(action.name), + input: JSON.stringify(action.args ?? {}), + providerExecuted: true, + nativeName: action.name, + }); + emit({ + type: 'tool-approval-request', + approvalId, + toolCallId: approvalId, + }); + const decision = await turn.requestToolApproval(approvalId); + if (decision.approved) { + const queue = approvedToolQueue.get(action.name) ?? []; + queue.push(approvalId); + approvedToolQueue.set(action.name, queue); + decisions.push({ type: 'approve' }); + } else { + // Rejected tools never execute, so surface the outcome as the result now. + emit({ + type: 'tool-result', + toolCallId: approvalId, + toolName: toCommonName(action.name), + result: decision.reason ?? 'Rejected by user.', + }); + decisions.push({ + type: 'reject', + ...(decision.reason ? { message: decision.reason } : {}), + }); + } + } + + resumeInput = new Command({ resume: { decisions } }); + } + + endTextBlock(); + endReasoningBlock(); + emit({ + type: 'finish', + finishReason: { unified: 'stop' }, + totalUsage: { + inputTokens: { total: inputTokens }, + outputTokens: { total: outputTokens }, + }, + }); +} + +await runBridge({ + bridgeType: 'deepagents', + bridgeStateDir: bridgeStateDir!, + onStart: runTurn, +}); diff --git a/packages/harness-deepagents/src/bridge/json-schema-to-zod.test.ts b/packages/harness-deepagents/src/bridge/json-schema-to-zod.test.ts new file mode 100644 index 000000000000..3289fe6def2c --- /dev/null +++ b/packages/harness-deepagents/src/bridge/json-schema-to-zod.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it } from 'vitest'; +import { jsonSchemaToZodObject } from './json-schema-to-zod'; + +describe('jsonSchemaToZodObject', () => { + it('handles flat scalar properties with required/optional', () => { + const schema = jsonSchemaToZodObject({ + type: 'object', + properties: { city: { type: 'string' }, days: { type: 'integer' } }, + required: ['city'], + }); + expect(schema.safeParse({ city: 'Paris' }).success).toBe(true); + expect(schema.safeParse({ city: 'Paris', days: 3 }).success).toBe(true); + expect(schema.safeParse({ days: 3 }).success).toBe(false); + expect(schema.safeParse({ city: 'Paris', days: 1.5 }).success).toBe(false); + }); + + it('preserves nested object structure (the flat converter dropped this)', () => { + const schema = jsonSchemaToZodObject({ + type: 'object', + properties: { + filter: { + type: 'object', + properties: { + status: { type: 'string' }, + limit: { type: 'number' }, + }, + required: ['status'], + }, + }, + required: ['filter'], + }); + expect( + schema.safeParse({ filter: { status: 'open', limit: 10 } }).success, + ).toBe(true); + // nested required field enforced — impossible with the old z.record(unknown) + expect(schema.safeParse({ filter: { limit: 10 } }).success).toBe(false); + expect(schema.safeParse({ filter: { status: 5 } }).success).toBe(false); + }); + + it('preserves array item types', () => { + const schema = jsonSchemaToZodObject({ + type: 'object', + properties: { tags: { type: 'array', items: { type: 'string' } } }, + required: ['tags'], + }); + expect(schema.safeParse({ tags: ['a', 'b'] }).success).toBe(true); + expect(schema.safeParse({ tags: [1, 2] }).success).toBe(false); + }); + + it('supports arrays of objects', () => { + const schema = jsonSchemaToZodObject({ + type: 'object', + properties: { + items: { + type: 'array', + items: { + type: 'object', + properties: { id: { type: 'integer' } }, + required: ['id'], + }, + }, + }, + }); + expect(schema.safeParse({ items: [{ id: 1 }] }).success).toBe(true); + expect(schema.safeParse({ items: [{ id: 'x' }] }).success).toBe(false); + }); + + it('honors nullable', () => { + const schema = jsonSchemaToZodObject({ + type: 'object', + properties: { note: { type: 'string', nullable: true } }, + required: ['note'], + }); + expect(schema.safeParse({ note: null }).success).toBe(true); + expect(schema.safeParse({ note: 'hi' }).success).toBe(true); + }); + + it('returns an empty object schema for a missing/!object input', () => { + expect(jsonSchemaToZodObject(undefined).safeParse({}).success).toBe(true); + }); +}); diff --git a/packages/harness-deepagents/src/bridge/json-schema-to-zod.ts b/packages/harness-deepagents/src/bridge/json-schema-to-zod.ts new file mode 100644 index 000000000000..7e7258b1b779 --- /dev/null +++ b/packages/harness-deepagents/src/bridge/json-schema-to-zod.ts @@ -0,0 +1,64 @@ +import { z } from 'zod/v4'; + +export type JsonSchemaObject = { + type?: string | string[]; + description?: string; + properties?: Record; + required?: string[]; + items?: JsonSchemaObject; + nullable?: boolean; +}; + +// Convert a host tool's JSON Schema to a zod object for LangChain's `tool()`. +export function jsonSchemaToZodObject(input: unknown) { + const schema = + input && typeof input === 'object' ? (input as JsonSchemaObject) : {}; + return z.object(toZodShape(schema)); +} + +function toZodShape(schema: JsonSchemaObject): Record { + if (!schema.properties) return {}; + const required = new Set(schema.required ?? []); + const shape: Record = {}; + for (const [key, propSchema] of Object.entries(schema.properties)) { + const propType = toZodType(propSchema); + shape[key] = required.has(key) ? propType : propType.optional(); + } + return shape; +} + +function toZodType(schema: JsonSchemaObject | undefined): z.ZodTypeAny { + if (!schema) return z.any(); + const types = Array.isArray(schema.type) + ? schema.type.filter(t => t !== 'null') + : ([schema.type].filter(Boolean) as string[]); + let zType: z.ZodTypeAny; + switch (types[0]) { + case 'string': + zType = z.string(); + break; + case 'number': + zType = z.number(); + break; + case 'integer': + zType = z.number().int(); + break; + case 'boolean': + zType = z.boolean(); + break; + case 'array': + zType = z.array(toZodType(schema.items)); + break; + case 'object': + zType = z.object(toZodShape(schema)); + break; + case 'null': + zType = z.null(); + break; + default: + zType = z.any(); + } + if (schema.description) zType = zType.describe(schema.description); + if (schema.nullable) zType = zType.nullable(); + return zType; +} diff --git a/packages/harness-deepagents/src/bridge/package.json b/packages/harness-deepagents/src/bridge/package.json new file mode 100644 index 000000000000..464723d373e5 --- /dev/null +++ b/packages/harness-deepagents/src/bridge/package.json @@ -0,0 +1,15 @@ +{ + "name": "harness-deepagents-bridge", + "version": "0.0.0", + "private": true, + "type": "module", + "dependencies": { + "@langchain/anthropic": "^1.0.0", + "@langchain/core": "^1.1.44", + "@langchain/langgraph": "^1.3.0", + "@langchain/openai": "^1.0.0", + "deepagents": "1.10.2", + "ws": "8.21.0", + "zod": "^4.3.6" + } +} diff --git a/packages/harness-deepagents/src/bridge/pnpm-lock.yaml b/packages/harness-deepagents/src/bridge/pnpm-lock.yaml new file mode 100644 index 000000000000..9777468ff24f --- /dev/null +++ b/packages/harness-deepagents/src/bridge/pnpm-lock.yaml @@ -0,0 +1,553 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@langchain/anthropic': + specifier: ^1.0.0 + version: 1.4.1(@langchain/core@1.1.49(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0)) + '@langchain/core': + specifier: ^1.1.44 + version: 1.1.49(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) + '@langchain/langgraph': + specifier: ^1.3.0 + version: 1.4.2(@langchain/core@1.1.49(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(zod@4.4.3) + '@langchain/openai': + specifier: ^1.0.0 + version: 1.4.7(@langchain/core@1.1.49(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(ws@8.21.0) + deepagents: + specifier: 1.10.2 + version: 1.10.2(langsmith@0.7.10(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) + ws: + specifier: 8.21.0 + version: 8.21.0 + zod: + specifier: ^4.3.6 + version: 4.4.3 + +packages: + + '@anthropic-ai/sdk@0.103.0': + resolution: {integrity: sha512-1uG7RNgoHTUxzOXqSCODKt0UTVlxWiHk/2Tt2/uQJiPW7XzBeKVuJyd3Aw6T3LPyvZV/jDTnPLX7SaM70WLLjA==} + hasBin: true + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@cfworker/json-schema@4.1.1': + resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==} + + '@langchain/anthropic@1.4.1': + resolution: {integrity: sha512-h3b6hxThcfh0WdmpuWr+qBi74MN+0BpNI/4H681vwXxbD3hLr2qMYN6ghqcPQhCxGJjg8ufs85qu2/ldSWonYQ==} + engines: {node: '>=20'} + peerDependencies: + '@langchain/core': ^1.1.49 + + '@langchain/core@1.1.49': + resolution: {integrity: sha512-7wkN3Qv/qZqsY0p3h48CNu6E6y5GMYatYxj+JrX4uVNBiqIVQm1Z528QrmayJWVW9SQTQicqRNoyTCzl+K9F8Q==} + engines: {node: '>=20'} + + '@langchain/langgraph-checkpoint@1.1.1': + resolution: {integrity: sha512-gHqhO6e2dyZ7TTfyaFy25yjcRsavURc9XMGT4q+LUBTc0hT4JxKe3qvrMX2OFTzW8W/0kjV59haHmSRFZIGkvg==} + engines: {node: '>=18'} + peerDependencies: + '@langchain/core': ^1.1.48 + + '@langchain/langgraph-sdk@1.9.22': + resolution: {integrity: sha512-DBKs9R2SGivlGqK/ZRTOUu39Q7Z+yRrG4PoTYLIWn7pqrLNhyZ4yZI/tEEEi/J0inpCuKfg/eydSwnRmPV/q3w==} + peerDependencies: + '@langchain/core': ^1.1.48 + react: ^18 || ^19 + react-dom: ^18 || ^19 + svelte: ^4.0.0 || ^5.0.0 + vue: ^3.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + svelte: + optional: true + vue: + optional: true + + '@langchain/langgraph@1.4.2': + resolution: {integrity: sha512-ivhYwbEKW4i/x2JfHcrTrToEE9EXZnwr4dPj7GC5974xEYeLgHYzii3GAYo1kgU5A0ZAd7rIxTpMOfcbycxliQ==} + engines: {node: '>=18'} + peerDependencies: + '@langchain/core': ^1.1.48 + zod: ^3.25.32 || ^4.2.0 + zod-to-json-schema: ^3.x + peerDependenciesMeta: + zod-to-json-schema: + optional: true + + '@langchain/openai@1.4.7': + resolution: {integrity: sha512-i1YLV4pWbGC6W8m0ZNpLObJuf1nyU4o8aWyX4AF9fHn7eM67HfIJWQ5n5XzcCpuSa41otrxA9jvH5XRKwI1qDA==} + engines: {node: '>=20'} + peerDependencies: + '@langchain/core': ^1.1.48 + + '@langchain/protocol@0.0.16': + resolution: {integrity: sha512-ws+J7MaHyhO5dG7f0vdyHQiUn9hoCnki0f3crJPa4MCTGzcRC39jYSCghyrGtBPYQnZbUQiGyRVpW3z3M8IpJg==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@stablelib/base64@1.0.1': + resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + deepagents@1.10.2: + resolution: {integrity: sha512-Ptp+t/FgIvMhDbVK0ml3IHcNx3gog3Cbqx+s88H4Hz8ieHG7svuR+/4Mawc/g14FY7mCls7Y8gCcrGb0i3Mi4w==} + peerDependencies: + langsmith: '>=0.6.0 <1.0.0' + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-network-error@1.3.2: + resolution: {integrity: sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==} + engines: {node: '>=16'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + js-tiktoken@1.0.21: + resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==} + + json-schema-to-ts@3.1.1: + resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} + engines: {node: '>=16'} + + langchain@1.4.5: + resolution: {integrity: sha512-P625jmIg91XwZoll6H3tyOLux1wQPjSptdGdiDdSrZVyUmeWKwzJu0+mmJjluNRCQVgzqCZzy1RWkz9p+vb+3A==} + engines: {node: '>=20'} + peerDependencies: + '@langchain/core': ^1.1.49 + + langsmith@0.7.10: + resolution: {integrity: sha512-3EjJx9zGMzqF60eT9JADHF+Hn/T5ayTgEVp4d3M5yvJIJi3q6seX0p5jT8ecBCWBi1kIvvssWrcDxfwgSier7Q==} + peerDependencies: + '@opentelemetry/api': '*' + '@opentelemetry/exporter-trace-otlp-proto': '*' + '@opentelemetry/sdk-trace-base': '*' + openai: '*' + ws: '>=7' + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@opentelemetry/exporter-trace-otlp-proto': + optional: true + '@opentelemetry/sdk-trace-base': + optional: true + openai: + optional: true + ws: + optional: true + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mustache@4.2.0: + resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} + hasBin: true + + openai@6.43.0: + resolution: {integrity: sha512-wVjioGjbnAZycj5mmkFVxbBxLEp+NkKpdMscCYP9LTbq+nbf1WTMVp+ovmD35jgyco4tldWZJkcqdmlh3O9yHQ==} + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + + p-queue@6.6.2: + resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} + engines: {node: '>=8'} + + p-queue@9.3.0: + resolution: {integrity: sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang==} + engines: {node: '>=20'} + + p-retry@7.1.1: + resolution: {integrity: sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==} + engines: {node: '>=20'} + + p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + + p-timeout@7.0.1: + resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==} + engines: {node: '>=20'} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + standardwebhooks@1.0.0: + resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ts-algebra@2.0.0: + resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + +snapshots: + + '@anthropic-ai/sdk@0.103.0(zod@4.4.3)': + dependencies: + json-schema-to-ts: 3.1.1 + standardwebhooks: 1.0.0 + optionalDependencies: + zod: 4.4.3 + + '@babel/runtime@7.29.7': {} + + '@cfworker/json-schema@4.1.1': {} + + '@langchain/anthropic@1.4.1(@langchain/core@1.1.49(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))': + dependencies: + '@anthropic-ai/sdk': 0.103.0(zod@4.4.3) + '@langchain/core': 1.1.49(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) + zod: 4.4.3 + + '@langchain/core@1.1.49(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0)': + dependencies: + '@cfworker/json-schema': 4.1.1 + '@standard-schema/spec': 1.1.0 + js-tiktoken: 1.0.21 + langsmith: 0.7.10(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) + mustache: 4.2.0 + p-queue: 6.6.2 + zod: 4.4.3 + transitivePeerDependencies: + - '@opentelemetry/api' + - '@opentelemetry/exporter-trace-otlp-proto' + - '@opentelemetry/sdk-trace-base' + - openai + - ws + + '@langchain/langgraph-checkpoint@1.1.1(@langchain/core@1.1.49(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))': + dependencies: + '@langchain/core': 1.1.49(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) + + '@langchain/langgraph-sdk@1.9.22(@langchain/core@1.1.49(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))': + dependencies: + '@langchain/core': 1.1.49(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) + '@langchain/protocol': 0.0.16 + '@types/json-schema': 7.0.15 + p-queue: 9.3.0 + p-retry: 7.1.1 + + '@langchain/langgraph@1.4.2(@langchain/core@1.1.49(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(zod@4.4.3)': + dependencies: + '@langchain/core': 1.1.49(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) + '@langchain/langgraph-checkpoint': 1.1.1(@langchain/core@1.1.49(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0)) + '@langchain/langgraph-sdk': 1.9.22(@langchain/core@1.1.49(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0)) + '@langchain/protocol': 0.0.16 + '@standard-schema/spec': 1.1.0 + zod: 4.4.3 + transitivePeerDependencies: + - react + - react-dom + - svelte + - vue + + '@langchain/openai@1.4.7(@langchain/core@1.1.49(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(ws@8.21.0)': + dependencies: + '@langchain/core': 1.1.49(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) + js-tiktoken: 1.0.21 + openai: 6.43.0(ws@8.21.0)(zod@4.4.3) + zod: 4.4.3 + transitivePeerDependencies: + - ws + + '@langchain/protocol@0.0.16': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@stablelib/base64@1.0.1': {} + + '@standard-schema/spec@1.1.0': {} + + '@types/json-schema@7.0.15': {} + + base64-js@1.5.1: {} + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + deepagents@1.10.2(langsmith@0.7.10(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0): + dependencies: + '@langchain/core': 1.1.49(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) + '@langchain/langgraph': 1.4.2(@langchain/core@1.1.49(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(zod@4.4.3) + '@langchain/langgraph-sdk': 1.9.22(@langchain/core@1.1.49(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0)) + fast-glob: 3.3.3 + langchain: 1.4.5(@langchain/core@1.1.49(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) + langsmith: 0.7.10(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) + micromatch: 4.0.8 + yaml: 2.9.0 + zod: 4.4.3 + transitivePeerDependencies: + - '@opentelemetry/api' + - '@opentelemetry/exporter-trace-otlp-proto' + - '@opentelemetry/sdk-trace-base' + - openai + - react + - react-dom + - svelte + - vue + - ws + - zod-to-json-schema + + eventemitter3@4.0.7: {} + + eventemitter3@5.0.4: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-sha256@1.3.0: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-network-error@1.3.2: {} + + is-number@7.0.0: {} + + js-tiktoken@1.0.21: + dependencies: + base64-js: 1.5.1 + + json-schema-to-ts@3.1.1: + dependencies: + '@babel/runtime': 7.29.7 + ts-algebra: 2.0.0 + + langchain@1.4.5(@langchain/core@1.1.49(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0): + dependencies: + '@langchain/core': 1.1.49(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) + '@langchain/langgraph': 1.4.2(@langchain/core@1.1.49(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(zod@4.4.3) + '@langchain/langgraph-checkpoint': 1.1.1(@langchain/core@1.1.49(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0)) + langsmith: 0.7.10(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) + zod: 4.4.3 + transitivePeerDependencies: + - '@opentelemetry/api' + - '@opentelemetry/exporter-trace-otlp-proto' + - '@opentelemetry/sdk-trace-base' + - openai + - react + - react-dom + - svelte + - vue + - ws + - zod-to-json-schema + + langsmith@0.7.10(openai@6.43.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0): + dependencies: + p-queue: 6.6.2 + optionalDependencies: + openai: 6.43.0(ws@8.21.0)(zod@4.4.3) + ws: 8.21.0 + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mustache@4.2.0: {} + + openai@6.43.0(ws@8.21.0)(zod@4.4.3): + optionalDependencies: + ws: 8.21.0 + zod: 4.4.3 + + p-finally@1.0.0: {} + + p-queue@6.6.2: + dependencies: + eventemitter3: 4.0.7 + p-timeout: 3.2.0 + + p-queue@9.3.0: + dependencies: + eventemitter3: 5.0.4 + p-timeout: 7.0.1 + + p-retry@7.1.1: + dependencies: + is-network-error: 1.3.2 + + p-timeout@3.2.0: + dependencies: + p-finally: 1.0.0 + + p-timeout@7.0.1: {} + + picomatch@2.3.2: {} + + queue-microtask@1.2.3: {} + + reusify@1.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + standardwebhooks@1.0.0: + dependencies: + '@stablelib/base64': 1.0.1 + fast-sha256: 1.3.0 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + ts-algebra@2.0.0: {} + + ws@8.21.0: {} + + yaml@2.9.0: {} + + zod@4.4.3: {} diff --git a/packages/harness-deepagents/src/deepagents-auth.test.ts b/packages/harness-deepagents/src/deepagents-auth.test.ts new file mode 100644 index 000000000000..b068d560a285 --- /dev/null +++ b/packages/harness-deepagents/src/deepagents-auth.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, it } from 'vitest'; +import { + resolveDeepAgentsEnv, + resolveDeepAgentsProvider, +} from './deepagents-auth'; + +describe('resolveDeepAgentsProvider', () => { + it('reads the provider from a slash/colon model string', () => { + expect(resolveDeepAgentsProvider({ model: 'openai/gpt-5' })).toBe('openai'); + expect(resolveDeepAgentsProvider({ model: 'anthropic:claude-x' })).toBe( + 'anthropic', + ); + }); + + it('defaults to anthropic', () => { + expect(resolveDeepAgentsProvider({ model: 'claude-sonnet-4' })).toBe( + 'anthropic', + ); + expect(resolveDeepAgentsProvider({})).toBe('anthropic'); + }); + + it('infers openai when only openai auth is configured', () => { + expect( + resolveDeepAgentsProvider({ auth: { openai: { apiKey: 'k' } } }), + ).toBe('openai'); + }); +}); + +describe('resolveDeepAgentsEnv', () => { + it('pins explicit anthropic auth', () => { + const env = resolveDeepAgentsEnv({ + auth: { + anthropic: { apiKey: 'sk-ant', baseUrl: 'https://example.test' }, + }, + processEnv: {}, + }); + expect(env).toEqual({ + ANTHROPIC_API_KEY: 'sk-ant', + ANTHROPIC_BASE_URL: 'https://example.test', + }); + }); + + it('pins explicit openai auth for an openai model', () => { + const env = resolveDeepAgentsEnv({ + model: 'openai/gpt-5', + auth: { openai: { apiKey: 'sk-oai', organization: 'org_1' } }, + processEnv: {}, + }); + expect(env.OPENAI_API_KEY).toBe('sk-oai'); + expect(env.OPENAI_ORGANIZATION).toBe('org_1'); + expect(env.ANTHROPIC_API_KEY).toBeUndefined(); + }); + + it('routes an anthropic model through the gateway (no /v1 suffix)', () => { + const env = resolveDeepAgentsEnv({ + auth: { gateway: { apiKey: 'gw-key' } }, + processEnv: {}, + }); + expect(env.AI_GATEWAY_API_KEY).toBe('gw-key'); + expect(env.ANTHROPIC_API_KEY).toBe('gw-key'); + expect(env.ANTHROPIC_BASE_URL).toBe('https://ai-gateway.vercel.sh'); + expect(env.OPENAI_BASE_URL).toBeUndefined(); + }); + + it('routes an openai model through the gateway (with /v1 suffix)', () => { + const env = resolveDeepAgentsEnv({ + model: 'openai/gpt-5', + auth: { gateway: { apiKey: 'gw-key' } }, + processEnv: {}, + }); + expect(env.OPENAI_API_KEY).toBe('gw-key'); + expect(env.OPENAI_BASE_URL).toBe('https://ai-gateway.vercel.sh/v1'); + expect(env.ANTHROPIC_BASE_URL).toBeUndefined(); + }); + + it('falls back to ambient gateway env before ambient provider creds', () => { + const env = resolveDeepAgentsEnv({ + processEnv: { + AI_GATEWAY_API_KEY: 'ambient-gw', + ANTHROPIC_API_KEY: 'ambient-ant', + }, + }); + expect(env.AI_GATEWAY_API_KEY).toBe('ambient-gw'); + expect(env.ANTHROPIC_API_KEY).toBe('ambient-gw'); + }); + + it('falls back to ambient OIDC token as the gateway key', () => { + const env = resolveDeepAgentsEnv({ + processEnv: { VERCEL_OIDC_TOKEN: 'oidc-token' }, + }); + expect(env.AI_GATEWAY_API_KEY).toBe('oidc-token'); + }); + + it('falls back to ambient anthropic when no gateway creds exist', () => { + const env = resolveDeepAgentsEnv({ + processEnv: { ANTHROPIC_API_KEY: 'ambient-ant' }, + }); + expect(env).toEqual({ ANTHROPIC_API_KEY: 'ambient-ant' }); + }); + + it('falls back to ambient openai creds for an openai model', () => { + const env = resolveDeepAgentsEnv({ + model: 'openai/gpt-5', + processEnv: { OPENAI_API_KEY: 'ambient-oai' }, + }); + expect(env).toEqual({ OPENAI_API_KEY: 'ambient-oai' }); + }); +}); diff --git a/packages/harness-deepagents/src/deepagents-auth.ts b/packages/harness-deepagents/src/deepagents-auth.ts new file mode 100644 index 000000000000..000ace2558ff --- /dev/null +++ b/packages/harness-deepagents/src/deepagents-auth.ts @@ -0,0 +1,149 @@ +import { getAiGatewayAuthFromEnv } from '@ai-sdk/harness/utils'; + +export type DeepAgentsAuthOptions = { + readonly anthropic?: { + readonly apiKey?: string; + readonly authToken?: string; + readonly baseUrl?: string; + }; + readonly openai?: { + readonly apiKey?: string; + readonly baseUrl?: string; + readonly organization?: string; + readonly project?: string; + }; + readonly gateway?: { + readonly apiKey?: string; + readonly baseUrl?: string; + }; +}; + +type Provider = 'anthropic' | 'openai'; + +// Pick the provider LangChain will resolve from the model string (or explicit auth); default anthropic. +export function resolveDeepAgentsProvider({ + model, + auth, +}: { + model?: string; + auth?: DeepAgentsAuthOptions; +}): Provider { + if (model) { + const head = model.includes('/') + ? model.split('/')[0] + : model.includes(':') + ? model.split(':')[0] + : ''; + if (head === 'openai') return 'openai'; + if (head === 'anthropic') return 'anthropic'; + } + if (auth?.openai && !auth?.anthropic) return 'openai'; + return 'anthropic'; +} + +// Resolve the bridge env vars for the model's provider: explicit provider auth, else gateway, else ambient. +export function resolveDeepAgentsEnv({ + auth, + model, + processEnv = process.env, +}: { + auth?: DeepAgentsAuthOptions; + model?: string; + processEnv?: Record; +}): Record { + const provider = resolveDeepAgentsProvider({ model, auth }); + + if (provider === 'openai' && auth?.openai) { + return pickOpenAI({ explicit: auth.openai, processEnv }); + } + if (provider === 'anthropic' && auth?.anthropic) { + return pickAnthropic({ explicit: auth.anthropic, processEnv }); + } + + const gatewayAuthFromEnv = getAiGatewayAuthFromEnv({ env: processEnv }); + if (auth?.gateway) { + return pickGateway({ + provider, + explicit: auth.gateway, + gatewayAuthFromEnv, + }); + } + if (gatewayAuthFromEnv.apiKey) { + return pickGateway({ provider, explicit: {}, gatewayAuthFromEnv }); + } + + return provider === 'openai' + ? pickOpenAI({ processEnv }) + : pickAnthropic({ processEnv }); +} + +function pickAnthropic({ + explicit, + processEnv, +}: { + explicit?: NonNullable; + processEnv: Record; +}): Record { + const env: Record = {}; + const apiKey = explicit?.apiKey ?? processEnv.ANTHROPIC_API_KEY; + if (apiKey) env.ANTHROPIC_API_KEY = apiKey; + const authToken = explicit?.authToken ?? processEnv.ANTHROPIC_AUTH_TOKEN; + if (authToken) env.ANTHROPIC_AUTH_TOKEN = authToken; + const baseUrl = explicit?.baseUrl ?? processEnv.ANTHROPIC_BASE_URL; + if (baseUrl) env.ANTHROPIC_BASE_URL = baseUrl; + return env; +} + +function pickOpenAI({ + explicit, + processEnv, +}: { + explicit?: NonNullable; + processEnv: Record; +}): Record { + const env: Record = {}; + const apiKey = explicit?.apiKey ?? processEnv.OPENAI_API_KEY; + if (apiKey) env.OPENAI_API_KEY = apiKey; + const baseUrl = explicit?.baseUrl ?? processEnv.OPENAI_BASE_URL; + if (baseUrl) env.OPENAI_BASE_URL = baseUrl; + const organization = explicit?.organization ?? processEnv.OPENAI_ORGANIZATION; + if (organization) env.OPENAI_ORGANIZATION = organization; + const project = explicit?.project ?? processEnv.OPENAI_PROJECT; + if (project) env.OPENAI_PROJECT = project; + return env; +} + +// The Anthropic SDK appends `/v1/messages` to its base URL; the OpenAI SDK appends `/chat/completions` to a `/v1` base. +function gatewayBaseUrl(baseUrl: string, provider: Provider): string { + const trimmed = baseUrl.replace(/\/+$/, ''); + if (provider === 'openai') { + return trimmed.endsWith('/v1') ? trimmed : `${trimmed}/v1`; + } + return trimmed; +} + +function pickGateway({ + provider, + explicit, + gatewayAuthFromEnv, +}: { + provider: Provider; + explicit: NonNullable; + gatewayAuthFromEnv: ReturnType; +}): Record { + const apiKey = explicit.apiKey ?? gatewayAuthFromEnv.apiKey; + const baseUrl = gatewayBaseUrl( + explicit.baseUrl ?? gatewayAuthFromEnv.baseUrl, + provider, + ); + const env: Record = {}; + if (apiKey) env.AI_GATEWAY_API_KEY = apiKey; + if (provider === 'openai') { + if (apiKey) env.OPENAI_API_KEY = apiKey; + env.OPENAI_BASE_URL = baseUrl; + } else { + if (apiKey) env.ANTHROPIC_API_KEY = apiKey; + env.ANTHROPIC_BASE_URL = baseUrl; + } + return env; +} diff --git a/packages/harness-deepagents/src/deepagents-bridge-protocol.test.ts b/packages/harness-deepagents/src/deepagents-bridge-protocol.test.ts new file mode 100644 index 000000000000..69fd055e0228 --- /dev/null +++ b/packages/harness-deepagents/src/deepagents-bridge-protocol.test.ts @@ -0,0 +1,99 @@ +import { describe, expect, it } from 'vitest'; +import { + inboundMessageSchema, + outboundMessageSchema, + startMessageSchema, +} from './deepagents-bridge-protocol'; + +describe('deepagents bridge protocol', () => { + it('parses a start message with deepagents extensions', () => { + const parsed = startMessageSchema.parse({ + type: 'start', + prompt: 'hello', + instructions: 'be terse', + model: 'anthropic/claude-sonnet-4', + tools: [{ name: 'lookup', description: 'd', inputSchema: {} }], + }); + expect(parsed.instructions).toBe('be terse'); + }); + + it('accepts the shared inbound commands', () => { + for (const msg of [ + { type: 'tool-result', toolCallId: 't1', output: { ok: true } }, + { type: 'user-message', text: 'more' }, + { type: 'abort' }, + { type: 'shutdown' }, + { type: 'resume', lastSeenEventId: 3 }, + { type: 'detach' }, + ]) { + expect(() => inboundMessageSchema.parse(msg)).not.toThrow(); + } + }); + + // These frames mirror exactly what the Node bridge emits. If the harness-v1 + // wire shapes change, this fails — signalling the bridge (src/bridge/index.ts) + // needs the matching update. + describe('outbound stream-part shapes emitted by the bridge', () => { + const cases: Array<[string, unknown]> = [ + ['stream-start', { type: 'stream-start', modelId: 'claude-sonnet-4' }], + ['text-start', { type: 'text-start', id: 'text-1' }], + ['text-delta', { type: 'text-delta', id: 'text-1', delta: 'hi' }], + ['text-end', { type: 'text-end', id: 'text-1' }], + ['reasoning-delta', { type: 'reasoning-delta', id: 'r-1', delta: '...' }], + [ + 'tool-call', + { + type: 'tool-call', + toolCallId: 'c1', + toolName: 'bash', + input: '{"command":"ls"}', + providerExecuted: true, + nativeName: 'shell', + }, + ], + [ + 'tool-result', + { + type: 'tool-result', + toolCallId: 'c1', + toolName: 'bash', + result: { stdout: 'ok' }, + isError: false, + }, + ], + [ + 'finish-step', + { + type: 'finish-step', + finishReason: { unified: 'stop' }, + usage: { inputTokens: { total: 1 }, outputTokens: { total: 2 } }, + }, + ], + [ + 'finish', + { + type: 'finish', + finishReason: { unified: 'stop' }, + totalUsage: { inputTokens: { total: 1 }, outputTokens: { total: 2 } }, + }, + ], + ['error', { type: 'error', error: { message: 'boom' } }], + ]; + + for (const [name, frame] of cases) { + it(`validates ${name}`, () => { + expect(() => outboundMessageSchema.parse(frame)).not.toThrow(); + }); + } + + it('tolerates an extra seq field (stripped by validation)', () => { + const parsed = outboundMessageSchema.parse({ + seq: 7, + type: 'text-delta', + id: 'text-1', + delta: 'hi', + }); + expect(parsed).not.toHaveProperty('seq'); + }); + }); +}); diff --git a/packages/harness-deepagents/src/deepagents-bridge-protocol.ts b/packages/harness-deepagents/src/deepagents-bridge-protocol.ts new file mode 100644 index 000000000000..f500b2f3aabb --- /dev/null +++ b/packages/harness-deepagents/src/deepagents-bridge-protocol.ts @@ -0,0 +1,29 @@ +import { + harnessV1BridgeInboundCommandSchemas, + harnessV1BridgeOutboundMessageSchema, + harnessV1BridgeReadySchema, + harnessV1BridgeStartBaseSchema, +} from '@ai-sdk/harness'; +import { z } from 'zod/v4'; + +// DeepAgents bridge wire protocol; only the `start` payload is adapter-specific. +export const outboundMessageSchema = harnessV1BridgeOutboundMessageSchema; +export type OutboundMessage = z.infer; + +export const startMessageSchema = harnessV1BridgeStartBaseSchema.extend({ + // Prepended to the first user message (createDeepAgent takes no instructions param). + instructions: z.string().optional(), + // In-backend path to the deepagents skills source dir, passed to createDeepAgent({ skills }). + skillsPath: z.string().optional(), +}); + +export type StartMessage = z.infer; + +export const inboundMessageSchema = z.discriminatedUnion('type', [ + startMessageSchema, + ...harnessV1BridgeInboundCommandSchemas, +]); +export type InboundMessage = z.infer; + +export const bridgeReadySchema = harnessV1BridgeReadySchema; +export type BridgeReady = z.infer; diff --git a/packages/harness-deepagents/src/deepagents-harness.test.ts b/packages/harness-deepagents/src/deepagents-harness.test.ts new file mode 100644 index 000000000000..fe5759122bae --- /dev/null +++ b/packages/harness-deepagents/src/deepagents-harness.test.ts @@ -0,0 +1,88 @@ +import type * as NodeFsPromises from 'node:fs/promises'; +import { describe, expect, it, vi } from 'vitest'; +import { + createDeepAgents, + DEEPAGENTS_BUILTIN_TOOLS, + DEEPAGENTS_DEFAULT_CONTEXT_WINDOW, +} from './deepagents-harness'; + +vi.mock('node:fs/promises', async importOriginal => { + const actual = await importOriginal(); + return { + ...actual, + readFile: vi.fn(async (input: unknown, ...rest: unknown[]) => { + const path = typeof input === 'string' ? input : String(input); + if (path.endsWith('/bridge/index.mjs')) return '// mock bridge\n'; + if (path.endsWith('/bridge/package.json')) return '{"name":"mock"}'; + if (path.endsWith('/bridge/pnpm-lock.yaml')) + return 'lockfileVersion: "9.0"\n'; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (actual.readFile as any)(input, ...rest); + }), + }; +}); + +describe('createDeepAgents', () => { + it('reports the harness-v1 metadata', () => { + const harness = createDeepAgents(); + expect(harness.specificationVersion).toBe('harness-v1'); + expect(harness.harnessId).toBe('deepagents'); + expect(harness.supportsBuiltinToolApprovals).toBe(true); + }); + + it('lists every model-callable DeepAgents built-in tool', () => { + expect(Object.keys(DEEPAGENTS_BUILTIN_TOOLS).sort()).toEqual([ + 'bash', + 'edit', + 'glob', + 'grep', + 'ls', + 'read', + 'task', + 'write', + 'write_todos', + ]); + }); + + it('maps common tools to their DeepAgents native names', () => { + expect(DEEPAGENTS_BUILTIN_TOOLS.read.nativeName).toBe('read_file'); + expect(DEEPAGENTS_BUILTIN_TOOLS.write.nativeName).toBe('write_file'); + expect(DEEPAGENTS_BUILTIN_TOOLS.edit.nativeName).toBe('edit_file'); + expect(DEEPAGENTS_BUILTIN_TOOLS.bash.nativeName).toBe('execute'); + expect(DEEPAGENTS_BUILTIN_TOOLS.grep.nativeName).toBe('grep'); + expect(DEEPAGENTS_BUILTIN_TOOLS.glob.nativeName).toBe('glob'); + }); + + it('has a default context window', () => { + expect(DEEPAGENTS_DEFAULT_CONTEXT_WINDOW).toBe(200_000); + }); + + it('ships the node bridge files and a pnpm install command in its bootstrap', async () => { + const harness = createDeepAgents(); + const bootstrap = await harness.getBootstrap!(); + expect(bootstrap.harnessId).toBe('deepagents'); + const paths = bootstrap.files.map(f => f.path); + expect(paths).toEqual( + expect.arrayContaining([ + expect.stringContaining('bridge.mjs'), + expect.stringContaining('package.json'), + expect.stringContaining('pnpm-lock.yaml'), + ]), + ); + const commands = bootstrap.commands.map(c => c.command).join('\n'); + expect(commands).toContain('pnpm'); + expect(commands).toContain('install'); + }); + + it('caches the bootstrap across calls', async () => { + const harness = createDeepAgents(); + const a = await harness.getBootstrap!(); + const b = await harness.getBootstrap!(); + expect(a).toBe(b); + }); + + it('exposes a lifecycle state schema for resume payloads', () => { + const harness = createDeepAgents(); + expect(harness.lifecycleStateSchema).toBeDefined(); + }); +}); diff --git a/packages/harness-deepagents/src/deepagents-harness.ts b/packages/harness-deepagents/src/deepagents-harness.ts new file mode 100644 index 000000000000..2d67b37903b5 --- /dev/null +++ b/packages/harness-deepagents/src/deepagents-harness.ts @@ -0,0 +1,755 @@ +import { randomBytes } from 'node:crypto'; +import { readFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import { + commonTool, + HarnessCapabilityUnsupportedError, + harnessV1DiagnosticFromBridgeFrame, + type HarnessV1, + type HarnessV1Bootstrap, + type HarnessV1BuiltinTool, + type HarnessV1ContinueTurnState, + type HarnessV1NetworkSandboxSession, + type HarnessV1PermissionMode, + type HarnessV1Prompt, + type HarnessV1PromptControl, + type HarnessV1ResumeSessionState, + type HarnessV1Session, + type HarnessV1Skill, + type HarnessV1StreamPart, +} from '@ai-sdk/harness'; +import { + markBridgeStarting, + SandboxChannel, + waitForBridgeReady, +} from '@ai-sdk/harness/utils'; +import { tool, type Experimental_SandboxProcess } from '@ai-sdk/provider-utils'; +import { WebSocket } from 'ws'; +import { z } from 'zod'; +import { + resolveDeepAgentsEnv, + type DeepAgentsAuthOptions, +} from './deepagents-auth'; +import { + outboundMessageSchema, + type InboundMessage, + type OutboundMessage, +} from './deepagents-bridge-protocol'; + +type DeepAgentsChannel = SandboxChannel; + +// Pure derived state in /tmp; reinstalled per sandbox, persistence is the provider snapshot. +const BOOTSTRAP_DIR = '/tmp/harness/deepagents'; + +// In-backend skills source path (resolved under the backend root = workDir). Dot-namespaced so it doesn't collide with a checked-out repo; matches deepagents' own `.deepagents/skills` project convention. +const SKILLS_SOURCE_PATH = '/.deepagents/skills'; + +const DEEPAGENTS_DEFAULT_CONTEXT_WINDOW = 200_000; + +export type DeepAgentsHarnessSettings = { + readonly auth?: DeepAgentsAuthOptions; + /** Model id for the DeepAgents runtime, e.g. `claude-sonnet-4` (converted to `provider:model`). */ + readonly model?: string; + /** Bridge port override; defaults to the sandbox's first declared port. */ + readonly port?: number; + /** Maximum milliseconds to wait for the bridge to advertise its port. Defaults to 120000. */ + readonly startupTimeoutMs?: number; +}; + +// Live bridge coordinates returned by doDetach/doSuspendTurn so a later process can reattach. +const deepAgentsBridgeCoordsSchema = z.object({ + port: z.number(), + token: z.string(), + lastSeenEventId: z.number(), + sandboxId: z.string().optional(), +}); +const deepAgentsResumeStateSchema = z.object({ + bridge: deepAgentsBridgeCoordsSchema.optional(), +}); +type DeepAgentsBridgeCoords = z.infer; + +// Every model-callable DeepAgents built-in, keyed by what the bridge emits (commonName ?? nativeName); all must be listed or AI SDK throws AI_NoSuchToolError. +const DEEPAGENTS_BUILTIN_TOOLS = { + read: commonTool('read', { + nativeName: 'read_file', + toolUseKind: 'readonly', + description: 'Read file contents', + inputSchema: z.object({ file_path: z.string() }), + }), + write: commonTool('write', { + nativeName: 'write_file', + toolUseKind: 'edit', + description: 'Create a file', + inputSchema: z.object({ file_path: z.string(), content: z.string() }), + }), + edit: commonTool('edit', { + nativeName: 'edit_file', + toolUseKind: 'edit', + description: 'Perform exact string replacements in a file', + inputSchema: z.object({ + file_path: z.string(), + old_string: z.string(), + new_string: z.string(), + }), + }), + bash: commonTool('bash', { + nativeName: 'execute', + toolUseKind: 'bash', + description: 'Run a shell command', + inputSchema: z.object({ command: z.string() }), + }), + grep: commonTool('grep', { + nativeName: 'grep', + toolUseKind: 'readonly', + description: 'Search file contents', + inputSchema: z.object({ pattern: z.string() }), + }), + glob: commonTool('glob', { + nativeName: 'glob', + toolUseKind: 'readonly', + description: 'Find files matching a glob pattern', + inputSchema: z.object({ pattern: z.string() }), + }), + // No common-name equivalent — keyed by native name. + ls: tool({ + description: 'List files in a directory', + inputSchema: z.object({ path: z.string().optional() }), + }), + task: tool({ + description: 'Spawn a subagent to handle a delegated task', + inputSchema: z.object({ + description: z.string().optional(), + subagent_type: z.string().optional(), + }), + }), + write_todos: tool({ + description: 'Manage a structured todo list', + inputSchema: z.object({ todos: z.array(z.unknown()).optional() }), + }), +} as const satisfies Record>; + +export function createDeepAgents( + settings: DeepAgentsHarnessSettings = {}, +): HarnessV1 { + let cachedBootstrap: HarnessV1Bootstrap | undefined; + + return { + specificationVersion: 'harness-v1', + harnessId: 'deepagents', + builtinTools: DEEPAGENTS_BUILTIN_TOOLS, + // Built-in tool approvals are gated in-bridge via DeepAgents' interruptOn (HITL) middleware. + supportsBuiltinToolApprovals: true, + lifecycleStateSchema: deepAgentsResumeStateSchema, + getBootstrap: async () => { + if (cachedBootstrap != null) return cachedBootstrap; + const [bridge, pkg, lock] = await Promise.all([ + readBridgeAsset('index.mjs'), + readBridgeAsset('package.json'), + readBridgeAsset('pnpm-lock.yaml'), + ]); + cachedBootstrap = { + harnessId: 'deepagents', + bootstrapDir: BOOTSTRAP_DIR, + files: [ + { path: `${BOOTSTRAP_DIR}/bridge.mjs`, content: bridge }, + { path: `${BOOTSTRAP_DIR}/package.json`, content: pkg }, + { path: `${BOOTSTRAP_DIR}/pnpm-lock.yaml`, content: lock }, + ], + commands: [ + { command: `mkdir -p ${BOOTSTRAP_DIR}` }, + { + command: `pnpm --dir ${BOOTSTRAP_DIR} install --frozen-lockfile --store-dir ${BOOTSTRAP_DIR}/.pnpm-store`, + }, + ], + }; + return cachedBootstrap; + }, + doStart: async startOpts => { + const permissionMode = startOpts.permissionMode; + const sandboxSession = startOpts.sandboxSession; + const session = sandboxSession.restricted(); + const sandboxId = sandboxSession.id; + + const lifecycleState = startOpts.continueFrom ?? startOpts.resumeFrom; + const isResume = lifecycleState != null; + const isContinue = startOpts.continueFrom != null; + const coords = + isResume && typeof lifecycleState?.data === 'object' + ? (lifecycleState.data as { bridge?: DeepAgentsBridgeCoords }).bridge + : undefined; + + const workDir = startOpts.sessionWorkDir; + const sessionDataDir = `${sandboxSession.defaultWorkingDirectory}/.agent-runs/${startOpts.sessionId}`; + const bridgeStateDir = `${sessionDataDir}/bridge`; + const timeoutMs = settings.startupTimeoutMs ?? 120_000; + + const report = startOpts.observability?.report; + const onDiagnostic = report + ? (frame: Parameters[0]) => + report( + harnessV1DiagnosticFromBridgeFrame(frame, { + sessionId: startOpts.sessionId, + timestamp: Date.now(), + }), + ) + : undefined; + + // Attach to the still-running bridge (continueFrom replays past the cursor); on failure fall through to a fresh spawn. + if (coords) { + try { + const attachUrl = + (await sandboxSession.getPortUrl({ + port: coords.port, + protocol: 'ws', + })) + `?agent_bridge_token=${encodeURIComponent(coords.token)}`; + const attachChannel: DeepAgentsChannel = new SandboxChannel({ + connect: () => openWebSocket(attachUrl), + outboundSchema: outboundMessageSchema, + initialLastSeenEventId: coords.lastSeenEventId, + onDiagnostic, + }); + await attachChannel.open(isContinue ? { resume: true } : undefined); + return createSession({ + sessionId: startOpts.sessionId, + channel: attachChannel, + proc: undefined, + model: settings.model, + bridgePort: coords.port, + bridgeToken: coords.token, + sandboxId, + isResume: true, + attached: true, + permissionMode, + }); + } catch { + // Bridge no longer reachable — recover by respawning below. + } + } + + const port = resolveBridgePort(sandboxSession, settings.port); + const token = randomBytes(32).toString('hex'); + + // Materialize skills as native deepagents skill folders the bridge passes to `createDeepAgent`. + const hasSkills = (startOpts.skills?.length ?? 0) > 0; + if (hasSkills) { + await writeSkills({ + sandbox: session, + workDir, + skills: startOpts.skills ?? [], + abortSignal: startOpts.abortSignal, + }); + } + // Absolute path: LocalShellBackend (non-virtual) treats a leading-slash path as a real fs path, so a workDir-relative skills dir must be fully qualified. + const skillsPath = hasSkills + ? `${workDir}${SKILLS_SOURCE_PATH}` + : undefined; + + const env = { + ...resolveDeepAgentsEnv({ auth: settings.auth, model: settings.model }), + BRIDGE_CHANNEL_TOKEN: token, + BRIDGE_WS_PORT: String(port), + }; + + await session.run({ + command: `mkdir -p ${shellQuote(workDir)} ${shellQuote(bridgeStateDir)}`, + abortSignal: startOpts.abortSignal, + }); + + await markBridgeStarting({ + sandbox: session, + bridgeStateDir, + bridgeType: 'deepagents', + abortSignal: startOpts.abortSignal, + }); + + const proc = await session.spawn({ + command: `node ${BOOTSTRAP_DIR}/bridge.mjs --workdir ${shellQuote(workDir)} --bridge-state-dir ${shellQuote(bridgeStateDir)} --bootstrap-dir ${shellQuote(BOOTSTRAP_DIR)}`, + env, + abortSignal: startOpts.abortSignal, + }); + + const { port: boundPort } = await waitForBridgeReady({ + proc, + sandbox: session, + bridgeStateDir, + bridgeType: 'deepagents', + timeoutMs, + abortSignal: startOpts.abortSignal, + createTimeoutError: () => + new Error('deepagents bridge did not become ready in time.'), + createExitError: () => + new Error('deepagents bridge exited before becoming ready.'), + }); + void forwardBridgeStderr(proc.stderr); + + const wsUrl = + (await sandboxSession.getPortUrl({ + port: boundPort, + protocol: 'ws', + })) + `?agent_bridge_token=${encodeURIComponent(token)}`; + + const channel: DeepAgentsChannel = new SandboxChannel({ + connect: () => openWebSocket(wsUrl), + outboundSchema: outboundMessageSchema, + onDiagnostic, + }); + await channel.open(); + + return createSession({ + sessionId: startOpts.sessionId, + channel, + proc, + model: settings.model, + bridgePort: boundPort, + bridgeToken: token, + sandboxId, + isResume, + // Freshly spawned bridge — it must receive the instructions on the first prompt. + attached: false, + skillsPath, + permissionMode, + }); + }, + }; +} + +function resolveBridgePort( + sandboxSession: HarnessV1NetworkSandboxSession, + override: number | undefined, +): number { + if (override !== undefined) return override; + if (sandboxSession.ports.length > 0) return sandboxSession.ports[0]; + throw new HarnessCapabilityUnsupportedError({ + harnessId: 'deepagents', + message: + 'The deepagents harness needs a TCP port exposed by the sandbox. ' + + 'Create the sandbox with `ports: []` or pass `createDeepAgents({ port })`.', + }); +} + +async function readBridgeAsset(name: string): Promise { + const candidates = [ + new URL(`./bridge/${name}`, import.meta.url), + new URL(`../bridge/${name}`, import.meta.url), + ]; + let lastErr: unknown; + for (const url of candidates) { + try { + return await readFile(fileURLToPath(url), 'utf8'); + } catch (err) { + const code = (err as NodeJS.ErrnoException).code; + if (code !== 'ENOENT') throw err; + lastErr = err; + } + } + throw lastErr ?? new Error(`bridge asset not found: ${name}`); +} + +// Materialize each skill as a native deepagents `/SKILL.md` folder (+ attached files) under the skills source path, so skills load on demand and file references resolve. +async function writeSkills({ + sandbox, + workDir, + skills, + abortSignal, +}: { + sandbox: ReturnType; + workDir: string; + skills: ReadonlyArray; + abortSignal?: AbortSignal; +}): Promise { + const root = `${workDir}${SKILLS_SOURCE_PATH}`; + for (const skill of skills) { + const name = safeSkillName(skill.name); + const skillDir = `${root}/${name}`; + // SKILL.md `name` must match the parent directory name (deepagents requirement). + const content = `---\nname: ${name}\ndescription: ${skill.description}\n---\n\n${skill.content}`; + await sandbox.writeTextFile({ + path: `${skillDir}/SKILL.md`, + content, + abortSignal, + }); + for (const file of skill.files ?? []) { + await sandbox.writeTextFile({ + path: `${skillDir}/${safeSkillFilePath(name, file.path)}`, + content: file.content, + abortSignal, + }); + } + } +} + +function safeSkillName(name: string): string { + if (!/^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$/.test(name)) { + throw new Error( + `Invalid deepagents skill name '${name}': must be lowercase alphanumeric with hyphens, 1-64 chars.`, + ); + } + return name; +} + +function safeSkillFilePath(skillName: string, filePath: string): string { + const normalized = filePath.replace(/^\/+/, ''); + if ( + normalized === '' || + normalized.startsWith('../') || + normalized.includes('/../') || + normalized.endsWith('/..') + ) { + throw new Error(`Invalid skill file path for '${skillName}': ${filePath}`); + } + return normalized; +} + +function shellQuote(value: string): string { + return `'${value.replace(/'/g, `'\\''`)}'`; +} + +function openWebSocket(url: string): Promise { + return new Promise((resolve, reject) => { + const ws = new WebSocket(url); + const onOpen = () => { + ws.off('error', onError); + resolve(ws); + }; + const onError = (err: Error) => { + ws.off('open', onOpen); + reject(err); + }; + ws.once('open', onOpen); + ws.once('error', onError); + }); +} + +async function forwardBridgeStderr( + stream: ReadableStream, +): Promise { + try { + const reader = stream.pipeThrough(new TextDecoderStream()).getReader(); + while (true) { + const { value, done } = await reader.read(); + if (done) return; + if (value) { + const trimmed = value.endsWith('\n') ? value.slice(0, -1) : value; + if (trimmed.length > 0) { + // eslint-disable-next-line no-console + console.log(`[bridge stderr] ${trimmed}`); + } + } + } + } catch { + // Reader errors are non-fatal — best-effort diagnostic only. + } +} + +function createSession({ + sessionId, + channel, + proc, + model, + bridgePort, + bridgeToken, + sandboxId, + isResume, + attached, + skillsPath, + permissionMode, +}: { + sessionId: string; + channel: DeepAgentsChannel; + // Undefined on attach — the live bridge was spawned by another process. + proc: Experimental_SandboxProcess | undefined; + model: string | undefined; + bridgePort: number; + bridgeToken: string; + sandboxId: string; + isResume: boolean; + // True only when attaching to a live bridge that already built the agent with + // its instructions. A fresh spawn (incl. a respawn on attach failure or a + // stop-resume) starts a new bridge that must receive the instructions again. + attached: boolean; + skillsPath?: string; + permissionMode?: HarnessV1PermissionMode; +}): HarnessV1Session { + let stopped = false; + let instructionsApplied = attached; + + const wireTurn = (turnOpts: { + emit: (event: HarnessV1StreamPart) => void; + abortSignal?: AbortSignal; + }): HarnessV1PromptControl => { + let pendingResolve: (() => void) | undefined; + let pendingReject: ((err: unknown) => void) | undefined; + const done = new Promise((resolve, reject) => { + pendingResolve = resolve; + pendingReject = reject; + }); + + const unsubs: Array<() => void> = []; + const forward = (event: HarnessV1StreamPart) => { + try { + turnOpts.emit(event); + } catch {} + }; + + const eventTypes = [ + 'stream-start', + 'text-start', + 'text-delta', + 'text-end', + 'reasoning-start', + 'reasoning-delta', + 'reasoning-end', + 'tool-call', + 'tool-approval-request', + 'tool-result', + 'file-change', + 'finish-step', + 'raw', + ] as const; + let isSettled = false; + const settleSuccess = () => { + if (isSettled) return; + isSettled = true; + for (const u of unsubs) u(); + pendingResolve!(); + }; + const settleError = (err: unknown) => { + if (isSettled) return; + isSettled = true; + for (const u of unsubs) u(); + pendingReject!(err); + }; + + for (const type of eventTypes) { + unsubs.push(channel.on(type, msg => forward(msg))); + } + unsubs.push( + channel.on('finish', msg => { + forward(msg); + settleSuccess(); + }), + ); + unsubs.push( + channel.on('error', msg => { + forward(msg); + settleError(msg.error); + }), + ); + + const onClose = () => { + if (isSettled) return; + settleError( + new Error('deepagents bridge closed before the turn finished.'), + ); + }; + channel.onClose(onClose); + + const onAbort = () => { + if (isSettled) return; + try { + channel.send({ type: 'abort' }); + } catch {} + settleError( + turnOpts.abortSignal?.reason ?? + new DOMException('Aborted', 'AbortError'), + ); + }; + if (turnOpts.abortSignal) { + if (turnOpts.abortSignal.aborted) { + onAbort(); + } else { + turnOpts.abortSignal.addEventListener('abort', onAbort, { once: true }); + } + } + + return { + submitToolResult: async input => { + channel.send({ + type: 'tool-result', + toolCallId: input.toolCallId, + output: input.output, + isError: input.isError, + }); + }, + submitUserMessage: async text => { + channel.send({ type: 'user-message', text }); + }, + submitToolApproval: async input => { + channel.send({ + type: 'tool-approval-response', + approvalId: input.approvalId, + approved: input.approved, + ...(input.reason != null ? { reason: input.reason } : {}), + }); + }, + done, + }; + }; + + const unsupported = (capability: string): never => { + throw new HarnessCapabilityUnsupportedError({ + harnessId: 'deepagents', + message: `Harness 'deepagents' does not support ${capability} yet.`, + }); + }; + + return { + sessionId, + isResume, + modelId: model, + doPromptTurn: async promptOpts => { + const control = wireTurn({ + emit: promptOpts.emit, + abortSignal: promptOpts.abortSignal, + }); + + const applyInstructions = + !instructionsApplied && !!promptOpts.instructions; + instructionsApplied = true; + + channel.send({ + type: 'start', + prompt: extractUserText(promptOpts.prompt), + ...(applyInstructions ? { instructions: promptOpts.instructions } : {}), + tools: (promptOpts.tools ?? []).map(t => ({ + name: t.name, + description: t.description, + inputSchema: t.inputSchema, + })), + ...(model ? { model } : {}), + ...(skillsPath ? { skillsPath } : {}), + ...(permissionMode ? { permissionMode } : {}), + }); + + return control; + }, + doContinueTurn: async continueOpts => { + // Attach/replay: doStart opened with `{ resume: true }` so the bridge replays past the cursor; no `start` is sent (that would clear the replay log). + return wireTurn({ + emit: continueOpts.emit, + abortSignal: continueOpts.abortSignal, + }); + }, + doSuspendTurn: async () => { + if (stopped) { + throw new Error( + `deepagents session ${sessionId} is stopped; cannot suspend.`, + ); + } + stopped = true; + // Freeze the active turn at the cursor, leaving the bridge running so the next slice replays the tail. + const lastSeenEventId = await channel.suspend(); + const payload: HarnessV1ContinueTurnState = { + type: 'continue-turn', + harnessId: 'deepagents', + specificationVersion: 'harness-v1', + data: { + bridge: { + port: bridgePort, + token: bridgeToken, + lastSeenEventId, + sandboxId, + }, + }, + }; + return payload; + }, + doDetach: async () => { + if (stopped) { + throw new Error( + `deepagents session ${sessionId} is already stopped; cannot detach.`, + ); + } + stopped = true; + // Park between turns: close the host socket but leave the bridge running for a later reattach via these coords. + const lastSeenEventId = await channel.suspend(); + const payload: HarnessV1ResumeSessionState = { + type: 'resume-session', + harnessId: 'deepagents', + specificationVersion: 'harness-v1', + data: { + bridge: { + port: bridgePort, + token: bridgeToken, + lastSeenEventId, + sandboxId, + }, + }, + }; + return payload; + }, + doCompact: async () => unsupported('manual compaction'), + doStop: async () => { + if (stopped) { + throw new Error( + `deepagents session ${sessionId} is already stopped; cannot stop.`, + ); + } + stopped = true; + await teardown(channel, proc); + // In-memory conversation is lost on teardown; the sandbox snapshot preserves the workspace files, not the conversation. + const payload: HarnessV1ResumeSessionState = { + type: 'resume-session', + harnessId: 'deepagents', + specificationVersion: 'harness-v1', + data: {}, + }; + return payload; + }, + doDestroy: async () => { + if (stopped) return; + stopped = true; + await teardown(channel, proc); + }, + }; +} + +async function teardown( + channel: DeepAgentsChannel, + proc: Experimental_SandboxProcess | undefined, +): Promise { + channel.beginClose(); + try { + if (!channel.isClosed()) { + channel.send({ type: 'shutdown' }); + } + } catch {} + let stopTimer: ReturnType | undefined; + try { + if (proc) { + await Promise.race([ + proc.wait(), + new Promise(resolve => { + stopTimer = setTimeout(resolve, 5000); + stopTimer.unref?.(); + }), + ]); + } + } finally { + if (stopTimer) clearTimeout(stopTimer); + try { + await proc?.kill(); + } catch {} + channel.close(); + } +} + +// Reduce the prompt to plain user text; non-text parts are unsupported. +function extractUserText(prompt: HarnessV1Prompt): string { + if (typeof prompt === 'string') return prompt; + const { content } = prompt; + if (typeof content === 'string') return content; + const parts: string[] = []; + for (const part of content) { + if (part.type !== 'text') { + throw new HarnessCapabilityUnsupportedError({ + harnessId: 'deepagents', + message: `The deepagents harness does not yet support user message parts of type '${part.type}'. Pass a string or a user message whose content contains only text parts.`, + }); + } + parts.push(part.text); + } + return parts.join('\n\n'); +} + +export { DEEPAGENTS_BUILTIN_TOOLS, DEEPAGENTS_DEFAULT_CONTEXT_WINDOW }; diff --git a/packages/harness-deepagents/src/index.ts b/packages/harness-deepagents/src/index.ts new file mode 100644 index 000000000000..5d0a1b3aba82 --- /dev/null +++ b/packages/harness-deepagents/src/index.ts @@ -0,0 +1,8 @@ +import { createDeepAgents } from './deepagents-harness'; + +/** Default `deepagents` harness instance; equivalent to `createDeepAgents()`. */ +export const deepAgents = createDeepAgents(); + +export { createDeepAgents } from './deepagents-harness'; +export type { DeepAgentsHarnessSettings } from './deepagents-harness'; +export type { DeepAgentsAuthOptions } from './deepagents-auth'; diff --git a/packages/harness-deepagents/tsconfig.build.json b/packages/harness-deepagents/tsconfig.build.json new file mode 100644 index 000000000000..80b6a0a84612 --- /dev/null +++ b/packages/harness-deepagents/tsconfig.build.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "composite": false + } +} diff --git a/packages/harness-deepagents/tsconfig.json b/packages/harness-deepagents/tsconfig.json new file mode 100644 index 000000000000..3ebec75b3995 --- /dev/null +++ b/packages/harness-deepagents/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "./node_modules/@vercel/ai-tsconfig/ts-library.json", + "compilerOptions": { + "composite": true, + "rootDir": "src", + "outDir": "dist" + }, + "exclude": [ + "dist", + "build", + "node_modules", + "tsup.config.ts" + ], + "references": [ + { "path": "../harness" }, + { "path": "../provider-utils" } + ] +} diff --git a/packages/harness-deepagents/tsup.config.ts b/packages/harness-deepagents/tsup.config.ts new file mode 100644 index 000000000000..bfda94f1bd22 --- /dev/null +++ b/packages/harness-deepagents/tsup.config.ts @@ -0,0 +1,32 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig([ + { + entry: { index: 'src/index.ts' }, + format: ['esm'], + target: 'es2022', + dts: true, + sourcemap: true, + }, + { + entry: { 'bridge/index': 'src/bridge/index.ts' }, + format: ['esm'], + target: 'es2022', + outExtension: () => ({ js: '.mjs' }), + dts: false, + sourcemap: true, + platform: 'node', + // The shared bridge runtime (`@ai-sdk/harness/bridge`) must be INLINED — + // the sandbox only installs the bridge's own deps (src/bridge/package.json), + // so a bare import would not resolve there. The runtime SDKs the bridge + // imports are installed in-sandbox and stay external. + noExternal: ['@ai-sdk/harness'], + external: [ + 'deepagents', + '@langchain/core', + '@langchain/langgraph', + 'ws', + 'zod', + ], + }, +]); diff --git a/packages/harness-deepagents/turbo.json b/packages/harness-deepagents/turbo.json new file mode 100644 index 000000000000..620b8380e744 --- /dev/null +++ b/packages/harness-deepagents/turbo.json @@ -0,0 +1,12 @@ +{ + "extends": [ + "//" + ], + "tasks": { + "build": { + "outputs": [ + "**/dist/**" + ] + } + } +} diff --git a/packages/harness-deepagents/vitest.node.config.js b/packages/harness-deepagents/vitest.node.config.js new file mode 100644 index 000000000000..34079d16828e --- /dev/null +++ b/packages/harness-deepagents/vitest.node.config.js @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + include: ['**/*.test.ts', '**/*.test.tsx'], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c36accdbfa2..adaa07f18b8e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -307,6 +307,9 @@ importers: '@ai-sdk/harness-codex': specifier: workspace:* version: link:../../packages/harness-codex + '@ai-sdk/harness-deepagents': + specifier: workspace:* + version: link:../../packages/harness-deepagents '@ai-sdk/harness-pi': specifier: workspace:* version: link:../../packages/harness-pi @@ -599,6 +602,9 @@ importers: '@ai-sdk/harness-codex': specifier: workspace:* version: link:../../packages/harness-codex + '@ai-sdk/harness-deepagents': + specifier: workspace:* + version: link:../../packages/harness-deepagents '@ai-sdk/harness-pi': specifier: workspace:* version: link:../../packages/harness-pi @@ -678,6 +684,9 @@ importers: '@ai-sdk/harness-codex': specifier: workspace:* version: link:../../packages/harness-codex + '@ai-sdk/harness-deepagents': + specifier: workspace:* + version: link:../../packages/harness-deepagents '@ai-sdk/harness-pi': specifier: workspace:* version: link:../../packages/harness-pi @@ -2597,6 +2606,46 @@ importers: specifier: 5.8.3 version: 5.8.3 + packages/harness-deepagents: + dependencies: + '@ai-sdk/harness': + specifier: workspace:* + version: link:../harness + '@ai-sdk/provider-utils': + specifier: workspace:* + version: link:../provider-utils + ws: + specifier: 8.21.0 + version: 8.21.0 + zod: + specifier: 3.25.76 + version: 3.25.76 + devDependencies: + '@langchain/core': + specifier: ^1.1.44 + version: 1.1.46(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0) + '@langchain/langgraph': + specifier: ^1.3.0 + version: 1.3.0(@langchain/core@1.1.46(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0))(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(svelte@5.55.7)(vue@3.5.38(typescript@5.8.3))(ws@8.21.0)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@3.25.76) + '@types/node': + specifier: 22.19.19 + version: 22.19.19 + '@types/ws': + specifier: ^8.5.13 + version: 8.18.1 + '@vercel/ai-tsconfig': + specifier: workspace:* + version: link:../../tools/tsconfig + deepagents: + specifier: 1.10.2 + version: 1.10.2(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(langsmith@0.7.1(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(svelte@5.55.7)(vue@3.5.38(typescript@5.8.3))(ws@8.21.0)(zod-to-json-schema@3.25.2(zod@3.25.76)) + tsup: + specifier: ^8.5.1 + version: 8.5.1(@swc/core@1.15.3(@swc/helpers@0.5.21))(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.0)(typescript@5.8.3)(yaml@2.9.0) + typescript: + specifier: 5.8.3 + version: 5.8.3 + packages/harness-pi: dependencies: '@ai-sdk/harness': @@ -13753,6 +13802,11 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepagents@1.10.2: + resolution: {integrity: sha512-Ptp+t/FgIvMhDbVK0ml3IHcNx3gog3Cbqx+s88H4Hz8ieHG7svuR+/4Mawc/g14FY7mCls7Y8gCcrGb0i3Mi4w==} + peerDependencies: + langsmith: '>=0.6.0 <1.0.0' + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -25708,6 +25762,26 @@ snapshots: - openai - ws + '@langchain/langgraph-sdk@1.9.2(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(svelte@5.55.7)(vue@3.5.38(typescript@5.8.3))(ws@8.21.0)': + dependencies: + '@langchain/core': 1.1.46(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0) + '@langchain/protocol': 0.0.15 + '@types/json-schema': 7.0.15 + p-queue: 9.2.0 + p-retry: 7.1.1 + uuid: 13.0.2 + optionalDependencies: + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + svelte: 5.55.7 + vue: 3.5.38(typescript@5.8.3) + transitivePeerDependencies: + - '@opentelemetry/api' + - '@opentelemetry/exporter-trace-otlp-proto' + - '@opentelemetry/sdk-trace-base' + - openai + - ws + '@langchain/langgraph-sdk@1.9.2(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@4.4.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(svelte@5.55.7)(vue@3.5.38(typescript@5.8.3))(ws@8.21.0)': dependencies: '@langchain/core': 1.1.46(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) @@ -25758,6 +25832,50 @@ snapshots: - vue - ws + '@langchain/langgraph@1.3.0(@langchain/core@1.1.46(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0))(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(svelte@5.55.7)(vue@3.5.38(typescript@5.8.3))(ws@8.21.0)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@3.25.76)': + dependencies: + '@langchain/core': 1.1.46(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0) + '@langchain/langgraph-checkpoint': 1.0.2(@langchain/core@1.1.46(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0)) + '@langchain/langgraph-sdk': 1.9.2(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(svelte@5.55.7)(vue@3.5.38(typescript@5.8.3))(ws@8.21.0) + '@langchain/protocol': 0.0.15 + '@standard-schema/spec': 1.1.0 + uuid: 10.0.0 + zod: 3.25.76 + optionalDependencies: + zod-to-json-schema: 3.25.2(zod@3.25.76) + transitivePeerDependencies: + - '@opentelemetry/api' + - '@opentelemetry/exporter-trace-otlp-proto' + - '@opentelemetry/sdk-trace-base' + - openai + - react + - react-dom + - svelte + - vue + - ws + + '@langchain/langgraph@1.3.0(@langchain/core@1.1.46(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0))(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(svelte@5.55.7)(vue@3.5.38(typescript@5.8.3))(ws@8.21.0)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@4.4.3)': + dependencies: + '@langchain/core': 1.1.46(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0) + '@langchain/langgraph-checkpoint': 1.0.2(@langchain/core@1.1.46(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0)) + '@langchain/langgraph-sdk': 1.9.2(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(svelte@5.55.7)(vue@3.5.38(typescript@5.8.3))(ws@8.21.0) + '@langchain/protocol': 0.0.15 + '@standard-schema/spec': 1.1.0 + uuid: 10.0.0 + zod: 4.4.3 + optionalDependencies: + zod-to-json-schema: 3.25.2(zod@3.25.76) + transitivePeerDependencies: + - '@opentelemetry/api' + - '@opentelemetry/exporter-trace-otlp-proto' + - '@opentelemetry/sdk-trace-base' + - openai + - react + - react-dom + - svelte + - vue + - ws + '@langchain/langgraph@1.3.0(@langchain/core@1.1.46(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@4.4.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(svelte@5.55.7)(vue@3.5.38(typescript@5.8.3))(ws@8.21.0)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)': dependencies: '@langchain/core': 1.1.46(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) @@ -26443,7 +26561,7 @@ snapshots: vite-plugin-inspect: 0.8.9(@nuxt/kit@3.21.5(magicast@0.3.5))(rollup@4.62.0)(vite@7.3.5(@types/node@22.19.19)(jiti@2.7.0)(less@4.4.0)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.48.0)(tsx@4.22.0)(yaml@2.9.0)) vite-plugin-vue-inspector: 5.1.3(vite@7.3.5(@types/node@22.19.19)(jiti@2.7.0)(less@4.4.0)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.48.0)(tsx@4.22.0)(yaml@2.9.0)) which: 3.0.1 - ws: 8.20.1 + ws: 8.21.0 transitivePeerDependencies: - bufferutil - rollup @@ -33016,6 +33134,29 @@ snapshots: deep-is@0.1.4: optional: true + deepagents@1.10.2(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(langsmith@0.7.1(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(svelte@5.55.7)(vue@3.5.38(typescript@5.8.3))(ws@8.21.0)(zod-to-json-schema@3.25.2(zod@3.25.76)): + dependencies: + '@langchain/core': 1.1.46(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0) + '@langchain/langgraph': 1.3.0(@langchain/core@1.1.46(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0))(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(svelte@5.55.7)(vue@3.5.38(typescript@5.8.3))(ws@8.21.0)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@4.4.3) + '@langchain/langgraph-sdk': 1.9.2(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(svelte@5.55.7)(vue@3.5.38(typescript@5.8.3))(ws@8.21.0) + fast-glob: 3.3.3 + langchain: 1.4.0(@langchain/core@1.1.46(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0))(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(svelte@5.55.7)(vue@3.5.38(typescript@5.8.3))(ws@8.21.0)(zod-to-json-schema@3.25.2(zod@3.25.76)) + langsmith: 0.7.1(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0) + micromatch: 4.0.8 + yaml: 2.9.0 + zod: 4.4.3 + transitivePeerDependencies: + - '@opentelemetry/api' + - '@opentelemetry/exporter-trace-otlp-proto' + - '@opentelemetry/sdk-trace-base' + - openai + - react + - react-dom + - svelte + - vue + - ws + - zod-to-json-schema + deepmerge@4.3.1: {} default-browser-id@5.0.1: {} @@ -35685,7 +35826,7 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - ws: 8.20.1 + ws: 8.21.0 xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil @@ -35917,6 +36058,25 @@ snapshots: - ws - zod-to-json-schema + langchain@1.4.0(@langchain/core@1.1.46(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0))(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(svelte@5.55.7)(vue@3.5.38(typescript@5.8.3))(ws@8.21.0)(zod-to-json-schema@3.25.2(zod@3.25.76)): + dependencies: + '@langchain/core': 1.1.46(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0) + '@langchain/langgraph': 1.3.0(@langchain/core@1.1.46(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0))(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(svelte@5.55.7)(vue@3.5.38(typescript@5.8.3))(ws@8.21.0)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@3.25.76) + '@langchain/langgraph-checkpoint': 1.0.2(@langchain/core@1.1.46(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0)) + langsmith: 0.7.1(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0) + zod: 3.25.76 + transitivePeerDependencies: + - '@opentelemetry/api' + - '@opentelemetry/exporter-trace-otlp-proto' + - '@opentelemetry/sdk-trace-base' + - openai + - react + - react-dom + - svelte + - vue + - ws + - zod-to-json-schema + langsmith@0.6.3(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(openai@6.38.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0): dependencies: p-queue: 6.6.2 diff --git a/tsconfig.json b/tsconfig.json index e537f9b9c4fb..fa68e84605c8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -84,6 +84,9 @@ { "path": "packages/harness-codex" }, + { + "path": "packages/harness-deepagents" + }, { "path": "packages/harness-pi" },