From ac9d4d5ed90686afa04001167723d0318a63b553 Mon Sep 17 00:00:00 2001 From: mlekhi Date: Wed, 17 Jun 2026 18:38:02 -0700 Subject: [PATCH 01/39] feat(harness-deepagents): add DeepAgents HarnessV1 adapter --- packages/harness-deepagents/README.md | 61 ++ packages/harness-deepagents/package.json | 73 +++ .../harness-deepagents/src/bridge/bridge.py | 26 + .../src/bridge/bridge_runtime.py | 14 + .../src/bridge/requirements.txt | 7 + .../harness-deepagents/src/deepagents-auth.ts | 86 +++ .../src/deepagents-bridge-protocol.ts | 37 ++ .../src/deepagents-harness.ts | 583 ++++++++++++++++++ packages/harness-deepagents/src/index.ts | 12 + .../harness-deepagents/tsconfig.build.json | 6 + packages/harness-deepagents/tsconfig.json | 18 + packages/harness-deepagents/tsup.config.ts | 16 + packages/harness-deepagents/turbo.json | 12 + .../harness-deepagents/vitest.node.config.js | 8 + pnpm-lock.yaml | 39 +- tsconfig.json | 3 + 16 files changed, 997 insertions(+), 4 deletions(-) create mode 100644 packages/harness-deepagents/README.md create mode 100644 packages/harness-deepagents/package.json create mode 100644 packages/harness-deepagents/src/bridge/bridge.py create mode 100644 packages/harness-deepagents/src/bridge/bridge_runtime.py create mode 100644 packages/harness-deepagents/src/bridge/requirements.txt create mode 100644 packages/harness-deepagents/src/deepagents-auth.ts create mode 100644 packages/harness-deepagents/src/deepagents-bridge-protocol.ts create mode 100644 packages/harness-deepagents/src/deepagents-harness.ts create mode 100644 packages/harness-deepagents/src/index.ts create mode 100644 packages/harness-deepagents/tsconfig.build.json create mode 100644 packages/harness-deepagents/tsconfig.json create mode 100644 packages/harness-deepagents/tsup.config.ts create mode 100644 packages/harness-deepagents/turbo.json create mode 100644 packages/harness-deepagents/vitest.node.config.js diff --git a/packages/harness-deepagents/README.md b/packages/harness-deepagents/README.md new file mode 100644 index 000000000000..329fe51a36d5 --- /dev/null +++ b/packages/harness-deepagents/README.md @@ -0,0 +1,61 @@ +# @ai-sdk/harness-deepagents + +A [HarnessV1](../harness) adapter that runs [DeepAgents](https://github.com/deep-agents/deepagents) +(a LangGraph-based agent) as a coding-agent runtime inside an AI SDK sandbox. + +DeepAgents is Python-based, so this is a **bridge-backed** harness: the runtime +runs inside the sandbox via a Python bridge (`python3 bridge.py`) that speaks the +harness-v1 wire protocol, while the host adapter drives turns over a WebSocket. + +> **Status: scaffolding.** The package structure, host adapter shape, built-in +> tool definitions, and auth resolution are in place. The session lifecycle +> (`doStart`) and the Python bridge are not implemented yet — see the plan for +> the phased rollout (happy-path single/multi-turn first; detach/resume/approvals +> follow up). + +## Setup + +```bash +pnpm add @ai-sdk/harness-deepagents @ai-sdk/harness +``` + +Requires a sandbox image with `python3.13` available; the harness installs the +Python dependencies (`requirements.txt`) into its bootstrap directory 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. With none configured the adapter falls back to the ambient +Vercel AI Gateway credentials (`AI_GATEWAY_API_KEY`, then `VERCEL_OIDC_TOKEN`), +then ambient `ANTHROPIC_*`. Pin explicitly with: + +```ts +createDeepAgents({ + model: 'claude-sonnet-4', + auth: { anthropic: { apiKey: process.env.ANTHROPIC_TEAM_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..42c44125afee --- /dev/null +++ b/packages/harness-deepagents/package.json @@ -0,0 +1,73 @@ +{ + "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.mkdir('dist/bridge', { recursive: true }); for (const f of ['bridge.py', 'bridge_runtime.py', 'requirements.txt']) { await fs.copyFile('src/bridge/' + f, 'dist/bridge/' + f); } })\"", + "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.20.1", + "zod": "3.25.76" + }, + "devDependencies": { + "@types/node": "22.19.19", + "@types/ws": "^8.5.13", + "@vercel/ai-tsconfig": "workspace:*", + "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/bridge.py b/packages/harness-deepagents/src/bridge/bridge.py new file mode 100644 index 000000000000..94c394085e87 --- /dev/null +++ b/packages/harness-deepagents/src/bridge/bridge.py @@ -0,0 +1,26 @@ +"""DeepAgents harness bridge entrypoint (runs inside the sandbox). + +Phase 3 placeholder. This process is launched by the host adapter +(`createDeepAgents().doStart`) as `python3 /tmp/harness/deepagents/bridge.py`. + +Responsibilities (to be implemented in Phase 3 — Option A, the Python bridge +speaks the harness-v1 wire protocol directly): + + - Bind a WebSocket server and print the harness-v1 `bridge-ready` JSON line + (`{"type": "bridge-ready", "port": }`) to stdout. + - Authenticate the host connection via the bridge token, then send + `bridge-hello`. + - Drive DeepAgents (`create_deep_agent()`) per inbound `start` command and + translate LangGraph `astream_events(v=2)` into harness-v1 stream parts + (stream-start / text-* / reasoning-* / tool-call / tool-approval-request / + tool-result / finish-step / finish / error). + - Consume the shared inbound command vocabulary (tool-result, + tool-approval-response, user-message, abort, shutdown, resume, detach). + - Maintain the seq event-log + resume replay and lifecycle files. + +The reusable transport lives in `bridge_runtime.py`. +""" + +raise NotImplementedError( + "DeepAgents Python bridge is not implemented yet (Phase 3)." +) diff --git a/packages/harness-deepagents/src/bridge/bridge_runtime.py b/packages/harness-deepagents/src/bridge/bridge_runtime.py new file mode 100644 index 000000000000..eaf683597584 --- /dev/null +++ b/packages/harness-deepagents/src/bridge/bridge_runtime.py @@ -0,0 +1,14 @@ +"""Reusable harness-v1 bridge transport for the DeepAgents Python bridge. + +Phase 3 placeholder. This module will own the transport concerns that +`@ai-sdk/harness/bridge` (`runBridge`) provides for Node bridges, re-implemented +in Python so the DeepAgents runtime can speak the harness-v1 wire protocol +directly (Option A): + + - WebSocket server + bridge-token auth + - the `bridge-ready` stdout announcement and `bridge-hello` handshake + - the in-memory event log with monotonic `seq`, plus resume replay + - the lifecycle / meta state files on disk + +`bridge.py` supplies only the DeepAgents-specific turn driver on top of this. +""" diff --git a/packages/harness-deepagents/src/bridge/requirements.txt b/packages/harness-deepagents/src/bridge/requirements.txt new file mode 100644 index 000000000000..9d1bbac4f173 --- /dev/null +++ b/packages/harness-deepagents/src/bridge/requirements.txt @@ -0,0 +1,7 @@ +# Installed in-sandbox at bootstrap (python3.13). Pinned to match the +# known-good versions from the agent-harness-sdk DeepAgents adapter. +deepagents==0.6.1 +websockets==16.0 +jsonschema==4.26.0 +langchain-anthropic==1.4.3 +langchain-openai==1.2.1 diff --git a/packages/harness-deepagents/src/deepagents-auth.ts b/packages/harness-deepagents/src/deepagents-auth.ts new file mode 100644 index 000000000000..7a0813406034 --- /dev/null +++ b/packages/harness-deepagents/src/deepagents-auth.ts @@ -0,0 +1,86 @@ +import { getAiGatewayAuthFromEnv } from '@ai-sdk/harness/utils'; + +export type DeepAgentsAuthOptions = { + readonly anthropic?: { + readonly apiKey?: string; + readonly authToken?: string; + readonly baseUrl?: string; + }; + readonly gateway?: { + readonly apiKey?: string; + readonly baseUrl?: string; + }; +}; + +const DEFAULT_ANTHROPIC_BASE_URL = 'https://api.anthropic.com'; + +/** + * Resolve the environment-variable blob the DeepAgents (LangChain) bridge needs. + * Precedence: + * + * 1. Explicit `auth.anthropic` — pin to direct Anthropic auth. + * 2. Explicit `auth.gateway` — pin to Vercel AI Gateway (routed via the + * Anthropic-compatible surface; LangChain reads `ANTHROPIC_*`). + * 3. Auto-detect from the host process env: gateway first + * (`AI_GATEWAY_API_KEY` / `VERCEL_OIDC_TOKEN`), then ambient `ANTHROPIC_*`. + * + * DeepAgents resolves models through LangChain, which reads `ANTHROPIC_API_KEY` + * / `ANTHROPIC_BASE_URL`. When routing through the gateway we point those at the + * gateway base URL and reuse the gateway key. + */ +export function resolveDeepAgentsEnv( + auth: DeepAgentsAuthOptions | undefined, + processEnv: Record = process.env, +): Record { + if (auth?.anthropic) { + return pickAnthropic({ explicit: auth.anthropic, processEnv }); + } + + const gatewayAuthFromEnv = getAiGatewayAuthFromEnv({ env: processEnv }); + + if (auth?.gateway) { + return pickGateway({ explicit: auth.gateway, gatewayAuthFromEnv }); + } + if (gatewayAuthFromEnv.apiKey) { + return pickGateway({ explicit: {}, gatewayAuthFromEnv }); + } + return 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 pickGateway({ + explicit, + gatewayAuthFromEnv, +}: { + explicit: NonNullable; + gatewayAuthFromEnv: ReturnType; +}): Record { + const apiKey = explicit.apiKey ?? gatewayAuthFromEnv.apiKey; + const baseUrl = explicit.baseUrl ?? gatewayAuthFromEnv.baseUrl; + const env: Record = {}; + if (apiKey) { + env.AI_GATEWAY_API_KEY = apiKey; + env.ANTHROPIC_API_KEY = apiKey; + } + env.AI_GATEWAY_BASE_URL = baseUrl; + env.ANTHROPIC_BASE_URL = baseUrl; + return env; +} + +export { DEFAULT_ANTHROPIC_BASE_URL }; 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..ec82b77c123d --- /dev/null +++ b/packages/harness-deepagents/src/deepagents-bridge-protocol.ts @@ -0,0 +1,37 @@ +import { + harnessV1BridgeInboundCommandSchemas, + harnessV1BridgeOutboundMessageSchema, + harnessV1BridgeReadySchema, + harnessV1BridgeStartBaseSchema, +} from '@ai-sdk/harness'; +import { z } from 'zod/v4'; + +/* + * DeepAgents' bridge wire protocol. The outbound events, transport frames, + * shared inbound commands, and `bridge-ready` line all come from the shared + * `@ai-sdk/harness` protocol — the only DeepAgents-specific piece is the + * `start` payload. + */ + +export const outboundMessageSchema = harnessV1BridgeOutboundMessageSchema; +export type OutboundMessage = z.infer; + +export const startMessageSchema = harnessV1BridgeStartBaseSchema.extend({ + /* + * Free-form session instructions. `create_deep_agent()` takes no + * `instructions` parameter, so the bridge prepends this to the first user + * message of a fresh session. The host sends it only on the first turn. + */ + instructions: 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.ts b/packages/harness-deepagents/src/deepagents-harness.ts new file mode 100644 index 000000000000..85c1dd7b5469 --- /dev/null +++ b/packages/harness-deepagents/src/deepagents-harness.ts @@ -0,0 +1,583 @@ +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 HarnessV1NetworkSandboxSession, + 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 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; + +/* + * Bootstrap lives in /tmp because it's pure derived state — the harness can + * reinstall the Python deps and bridge files on any fresh sandbox from the + * recipe. Persistence comes from the sandbox provider's snapshot, not the path. + */ +const BOOTSTRAP_DIR = '/tmp/harness/deepagents'; + +const DEEPAGENTS_DEFAULT_CONTEXT_WINDOW = 200_000; + +export type DeepAgentsHarnessSettings = { + readonly auth?: DeepAgentsAuthOptions; + /** + * Model id the underlying DeepAgents (LangChain) runtime should use, e.g. + * `claude-sonnet-4`. The bridge converts this to LangChain colon format + * internally (`anthropic:claude-sonnet-4`). + */ + readonly model?: string; + /** + * Override the port the bridge binds inside the sandbox. By default the + * adapter uses the first port the sandbox declares via `sandbox.ports`. + */ + readonly port?: number; + /** Maximum milliseconds to wait for the bridge to advertise its port. Defaults to 120000. */ + readonly startupTimeoutMs?: number; +}; + +/* + * Every native tool the DeepAgents (LangGraph) runtime exposes as a + * model-callable tool, keyed by the cross-harness common name the bridge emits + * as `toolName` on the wire. The native LangGraph names are recorded via + * `nativeName`. DeepAgents' `search` maps to the common `grep` capability — + * `/ai` has no `searchFiles` common name — and the bridge maps each tool's + * native argument names (`path`/`query`) onto the standard common fields + * (`file_path`/`pattern`). + */ +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: 'Write a file', + inputSchema: z.object({ file_path: z.string(), content: z.string() }), + }), + bash: commonTool('bash', { + nativeName: 'shell', + toolUseKind: 'bash', + description: 'Execute a shell command', + inputSchema: z.object({ command: z.string() }), + }), + grep: commonTool('grep', { + nativeName: 'search', + toolUseKind: 'readonly', + description: 'Search file contents with regex', + inputSchema: z.object({ pattern: z.string() }), + }), +} as const satisfies Record>; + +export function createDeepAgents( + settings: DeepAgentsHarnessSettings = {}, +): HarnessV1 { + let cachedBootstrap: HarnessV1Bootstrap | undefined; + + return { + specificationVersion: 'harness-v1', + harnessId: 'deepagents', + builtinTools: DEEPAGENTS_BUILTIN_TOOLS, + // DeepAgents supports approvals upstream, but the happy-path first cut ships + // with `permissionMode: 'allow-all'` only; approvals land in a follow-up. + supportsBuiltinToolApprovals: false, + getBootstrap: async () => { + if (cachedBootstrap != null) return cachedBootstrap; + const [bridge, bridgeRuntime, requirements] = await Promise.all([ + readBridgeAsset('bridge.py'), + readBridgeAsset('bridge_runtime.py'), + readBridgeAsset('requirements.txt'), + ]); + cachedBootstrap = { + harnessId: 'deepagents', + bootstrapDir: BOOTSTRAP_DIR, + files: [ + { path: `${BOOTSTRAP_DIR}/bridge.py`, content: bridge }, + { + path: `${BOOTSTRAP_DIR}/bridge_runtime.py`, + content: bridgeRuntime, + }, + { path: `${BOOTSTRAP_DIR}/requirements.txt`, content: requirements }, + ], + commands: [ + { command: `mkdir -p ${BOOTSTRAP_DIR}` }, + { + command: `python3 -m pip install --no-cache-dir -r ${BOOTSTRAP_DIR}/requirements.txt`, + }, + ], + }; + return cachedBootstrap; + }, + doStart: async startOpts => { + if ( + startOpts.permissionMode != null && + startOpts.permissionMode !== 'allow-all' + ) { + throw new HarnessCapabilityUnsupportedError({ + message: + "Harness 'deepagents' does not support built-in tool approval requests yet; use permissionMode: 'allow-all'.", + harnessId: 'deepagents', + }); + } + + // Happy-path first cut: cross-process resume / turn continuation is a + // follow-up. DeepAgents' conversation state is in-memory (LangGraph + // MemorySaver) and does not survive a bridge restart, so resuming a prior + // session is not yet sound. + if (startOpts.resumeFrom != null || startOpts.continueFrom != null) { + throw new HarnessCapabilityUnsupportedError({ + message: + "Harness 'deepagents' does not support resuming a session yet; start a fresh session.", + harnessId: 'deepagents', + }); + } + + const sandboxSession = startOpts.sandboxSession; + const session = sandboxSession.restricted(); + + 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; + + const port = resolveBridgePort(sandboxSession, settings.port); + const token = randomBytes(32).toString('hex'); + + // DeepAgents reads skills from a single combined `.skills.md` file in the + // working directory. + if (startOpts.skills && startOpts.skills.length > 0) { + await writeSkills({ + sandbox: session, + workDir, + skills: startOpts.skills, + abortSignal: startOpts.abortSignal, + }); + } + + const env = { + ...resolveDeepAgentsEnv(settings.auth), + BRIDGE_CHANNEL_TOKEN: token, + BRIDGE_WS_PORT: String(port), + }; + + await session.run({ + command: `mkdir -p ${workDir} ${bridgeStateDir}`, + abortSignal: startOpts.abortSignal, + }); + + await markBridgeStarting({ + sandbox: session, + bridgeStateDir, + bridgeType: 'deepagents', + abortSignal: startOpts.abortSignal, + }); + + const proc = await session.spawn({ + command: `python3 ${BOOTSTRAP_DIR}/bridge.py --workdir ${workDir} --bridge-state-dir ${bridgeStateDir} --bootstrap-dir ${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, + }); + }, + }; +} + +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}`); +} + +async function writeSkills({ + sandbox, + workDir, + skills, + abortSignal, +}: { + sandbox: ReturnType; + workDir: string; + skills: ReadonlyArray; + abortSignal?: AbortSignal; +}): Promise { + const combined = skills + .map(skill => `## ${skill.name}\n${skill.description}\n\n${skill.content}`) + .join('\n\n---\n\n'); + await sandbox.writeTextFile({ + path: `${workDir}/.skills.md`, + content: combined, + abortSignal, + }); +} + +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, +}: { + sessionId: string; + channel: DeepAgentsChannel; + proc: Experimental_SandboxProcess; + model: string | undefined; +}): HarnessV1Session { + let stopped = false; + let instructionsApplied = false; + + 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 }); + }, + done, + }; + }; + + const unsupported = (capability: string): never => { + throw new HarnessCapabilityUnsupportedError({ + harnessId: 'deepagents', + message: `Harness 'deepagents' does not support ${capability} yet.`, + }); + }; + + return { + sessionId, + isResume: false, + 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 } : {}), + }); + + return control; + }, + doContinueTurn: async () => unsupported('turn continuation'), + doSuspendTurn: async () => unsupported('suspending a turn'), + doDetach: async () => unsupported('detaching a session'), + 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); + // DeepAgents holds conversation state in memory only, so there is no + // durable runtime state to export. The sandbox snapshot taken during the + // subsequent `sandboxSession.stop()` preserves the filesystem. + 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, +): Promise { + channel.beginClose(); + try { + if (!channel.isClosed()) { + channel.send({ type: 'shutdown' }); + } + } catch {} + let stopTimer: ReturnType | undefined; + try { + 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 a `HarnessV1Prompt` to the plain user text the bridge forwards to the + * DeepAgents runtime. File and image parts are not yet supported — throw rather + * than silently drop them. + */ +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..d34cc7a361d2 --- /dev/null +++ b/packages/harness-deepagents/src/index.ts @@ -0,0 +1,12 @@ +import { createDeepAgents } from './deepagents-harness'; + +/** + * Default `deepagents` harness instance with no overrides — suitable for the + * common case where the runtime's defaults are fine. 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..307328fe3179 --- /dev/null +++ b/packages/harness-deepagents/tsup.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'tsup'; + +// Only the host-side adapter is compiled by tsup. The agent runtime is a +// Python bridge (`src/bridge/*.py`) that runs inside the sandbox; its files are +// shipped verbatim via the `copy-bridge-assets` script and its dependencies are +// installed in-sandbox from `requirements.txt` at bootstrap time — they are +// never bundled here. +export default defineConfig([ + { + entry: { index: 'src/index.ts' }, + format: ['esm'], + target: 'es2022', + dts: true, + sourcemap: true, + }, +]); 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 927edeee96f0..18adc8c04520 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2597,6 +2597,37 @@ 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.20.1 + version: 8.21.0 + zod: + specifier: 3.25.76 + version: 3.25.76 + devDependencies: + '@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 + 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': @@ -26454,7 +26485,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 @@ -35705,7 +35736,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 @@ -35732,7 +35763,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 @@ -42072,7 +42103,7 @@ snapshots: sockjs: 0.3.24 spdy: 4.0.2 webpack-dev-middleware: 7.4.2(tslib@2.8.1)(webpack@5.105.0(esbuild@0.28.0)(lightningcss@1.32.0)(postcss@8.5.12)) - ws: 8.20.1 + ws: 8.21.0 optionalDependencies: webpack: 5.105.0(esbuild@0.28.0)(lightningcss@1.32.0)(postcss@8.5.12) transitivePeerDependencies: 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" }, From 55ff849923a13f176ac43ededf47c036d193cf8b Mon Sep 17 00:00:00 2001 From: mlekhi Date: Wed, 17 Jun 2026 18:39:00 -0700 Subject: [PATCH 02/39] test(harness-deepagents): cover factory, auth, and bootstrap --- .../src/deepagents-auth.test.ts | 46 ++++++++++ .../src/deepagents-harness.test.ts | 83 +++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 packages/harness-deepagents/src/deepagents-auth.test.ts create mode 100644 packages/harness-deepagents/src/deepagents-harness.test.ts 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..4c306e547efc --- /dev/null +++ b/packages/harness-deepagents/src/deepagents-auth.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from 'vitest'; +import { resolveDeepAgentsEnv } from './deepagents-auth'; + +describe('resolveDeepAgentsEnv', () => { + it('pins explicit anthropic auth', () => { + const env = resolveDeepAgentsEnv( + { anthropic: { apiKey: 'sk-ant', baseUrl: 'https://example.test' } }, + {}, + ); + expect(env).toEqual({ + ANTHROPIC_API_KEY: 'sk-ant', + ANTHROPIC_BASE_URL: 'https://example.test', + }); + }); + + it('pins explicit gateway auth and mirrors it onto ANTHROPIC_*', () => { + const env = resolveDeepAgentsEnv({ gateway: { apiKey: 'gw-key' } }, {}); + expect(env.AI_GATEWAY_API_KEY).toBe('gw-key'); + expect(env.ANTHROPIC_API_KEY).toBe('gw-key'); + expect(env.AI_GATEWAY_BASE_URL).toBe('https://ai-gateway.vercel.sh'); + expect(env.ANTHROPIC_BASE_URL).toBe('https://ai-gateway.vercel.sh'); + }); + + it('falls back to ambient gateway env before ambient anthropic', () => { + const env = resolveDeepAgentsEnv(undefined, { + 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 gateway key', () => { + const env = resolveDeepAgentsEnv(undefined, { + 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(undefined, { + ANTHROPIC_API_KEY: 'ambient-ant', + }); + expect(env).toEqual({ ANTHROPIC_API_KEY: 'ambient-ant' }); + }); +}); 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..612d5f44e0bb --- /dev/null +++ b/packages/harness-deepagents/src/deepagents-harness.test.ts @@ -0,0 +1,83 @@ +import { + HarnessCapabilityUnsupportedError, + type HarnessV1StartOptions, +} from '@ai-sdk/harness'; +import { describe, expect, it } from 'vitest'; +import { + createDeepAgents, + DEEPAGENTS_BUILTIN_TOOLS, + DEEPAGENTS_DEFAULT_CONTEXT_WINDOW, +} from './deepagents-harness'; + +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(false); + }); + + it('exposes the native LangGraph tool names via builtin tools', () => { + expect(Object.keys(DEEPAGENTS_BUILTIN_TOOLS).sort()).toEqual([ + 'bash', + 'grep', + 'read', + 'write', + ]); + expect(DEEPAGENTS_BUILTIN_TOOLS.read.nativeName).toBe('read_file'); + expect(DEEPAGENTS_BUILTIN_TOOLS.write.nativeName).toBe('write_file'); + expect(DEEPAGENTS_BUILTIN_TOOLS.bash.nativeName).toBe('shell'); + expect(DEEPAGENTS_BUILTIN_TOOLS.grep.nativeName).toBe('search'); + }); + + it('has a default context window', () => { + expect(DEEPAGENTS_DEFAULT_CONTEXT_WINDOW).toBe(200_000); + }); + + it('ships the python bridge files and a pip 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.py'), + expect.stringContaining('bridge_runtime.py'), + expect.stringContaining('requirements.txt'), + ]), + ); + const commands = bootstrap.commands.map(c => c.command).join('\n'); + expect(commands).toContain('pip install'); + expect(commands).toContain('requirements.txt'); + }); + + 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('rejects a non-allow-all permission mode', async () => { + const harness = createDeepAgents(); + await expect( + harness.doStart({ + permissionMode: 'allow-reads', + } as unknown as HarnessV1StartOptions), + ).rejects.toBeInstanceOf(HarnessCapabilityUnsupportedError); + }); + + it('rejects resuming a session', async () => { + const harness = createDeepAgents(); + await expect( + harness.doStart({ + resumeFrom: { + type: 'resume-session', + harnessId: 'deepagents', + specificationVersion: 'harness-v1', + data: {}, + }, + } as unknown as HarnessV1StartOptions), + ).rejects.toBeInstanceOf(HarnessCapabilityUnsupportedError); + }); +}); From fb7288490ead4d2843e7e12367ea66babe1c728e Mon Sep 17 00:00:00 2001 From: mlekhi Date: Thu, 18 Jun 2026 14:48:31 -0700 Subject: [PATCH 03/39] feat(harness-deepagents): drive JS deepagents via a Node bridge --- .changeset/deepagents-harness.md | 5 + .../05-harness-adapters.mdx | 3 +- .../02-ai-sdk-harnesses/04-deepagents.mdx | 187 ++++++++ packages/harness-deepagents/README.md | 33 +- packages/harness-deepagents/package.json | 4 +- .../harness-deepagents/src/bridge/bridge.py | 26 - .../src/bridge/bridge_runtime.py | 14 - .../harness-deepagents/src/bridge/index.ts | 308 ++++++++++++ .../src/bridge/package.json | 12 + .../src/bridge/pnpm-lock.yaml | 446 ++++++++++++++++++ .../src/bridge/requirements.txt | 7 - .../src/deepagents-bridge-protocol.test.ts | 99 ++++ .../src/deepagents-harness.test.ts | 31 +- .../src/deepagents-harness.ts | 23 +- packages/harness-deepagents/tsup.config.ts | 20 +- pnpm-lock.yaml | 117 +++++ 16 files changed, 1247 insertions(+), 88 deletions(-) create mode 100644 .changeset/deepagents-harness.md create mode 100644 content/providers/02-ai-sdk-harnesses/04-deepagents.mdx delete mode 100644 packages/harness-deepagents/src/bridge/bridge.py delete mode 100644 packages/harness-deepagents/src/bridge/bridge_runtime.py create mode 100644 packages/harness-deepagents/src/bridge/index.ts create mode 100644 packages/harness-deepagents/src/bridge/package.json create mode 100644 packages/harness-deepagents/src/bridge/pnpm-lock.yaml delete mode 100644 packages/harness-deepagents/src/bridge/requirements.txt create mode 100644 packages/harness-deepagents/src/deepagents-bridge-protocol.test.ts diff --git a/.changeset/deepagents-harness.md b/.changeset/deepagents-harness.md new file mode 100644 index 000000000000..164118b58198 --- /dev/null +++ b/.changeset/deepagents-harness.md @@ -0,0 +1,5 @@ +--- +'@ai-sdk/harness-deepagents': patch +--- + +Add `@ai-sdk/harness-deepagents`, a HarnessV1 adapter for LangChain's LangGraph-based DeepAgents runtime. The bridge-backed adapter runs a Node bridge inside the sandbox (driving the `deepagents` npm package via `createDeepAgent` + `streamEvents`, on the shared `@ai-sdk/harness/bridge` transport) and supports single- and multi-turn-within-session prompts, host-executed tools, skills, and the `read`/`write`/`bash`/`grep` built-ins. diff --git a/content/docs/03-ai-sdk-harnesses/05-harness-adapters.mdx b/content/docs/03-ai-sdk-harnesses/05-harness-adapters.mdx index de55a1d85e62..2023766c3cab 100644 --- a/content/docs/03-ai-sdk-harnesses/05-harness-adapters.mdx +++ b/content/docs/03-ai-sdk-harnesses/05-harness-adapters.mdx @@ -16,12 +16,12 @@ The AI SDK includes the following harness adapters: - [Claude Code](/providers/ai-sdk-harnesses/claude-code) (`@ai-sdk/harness-claude-code`) - [Codex](/providers/ai-sdk-harnesses/codex) (`@ai-sdk/harness-codex`) +- [DeepAgents](/providers/ai-sdk-harnesses/deepagents) (`@ai-sdk/harness-deepagents`) - [Pi](/providers/ai-sdk-harnesses/pi) (`@ai-sdk/harness-pi`) ### Coming Soon - Amp (`@ai-sdk/harness-amp`) -- DeepAgents (`@ai-sdk/harness-deepagents`) - Goose (`@ai-sdk/harness-goose`) - Mastra (`@ai-sdk/harness-mastra`) - OpenCode (`@ai-sdk/harness-opencode`) @@ -32,4 +32,5 @@ The AI SDK includes the following harness adapters: | ------------------------------------------------------ | ---------------- | ------------------- | ------------------- | ---------------------- | | [Claude Code](/providers/ai-sdk-harnesses/claude-code) | Sandbox bridge | | | | | [Codex](/providers/ai-sdk-harnesses/codex) | Sandbox bridge | | | | +| [DeepAgents](/providers/ai-sdk-harnesses/deepagents) | Sandbox bridge | | | | | [Pi](/providers/ai-sdk-harnesses/pi) | Host process | | | | diff --git a/content/providers/02-ai-sdk-harnesses/04-deepagents.mdx b/content/providers/02-ai-sdk-harnesses/04-deepagents.mdx new file mode 100644 index 000000000000..216f550f09da --- /dev/null +++ b/content/providers/02-ai-sdk-harnesses/04-deepagents.mdx @@ -0,0 +1,187 @@ +--- +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 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 + +By default, authentication is resolved from the host environment and forwarded +to the sandbox bridge. The adapter checks for AI Gateway credentials first, then +ambient Anthropic credentials. + +Supported environment variables: + +- `AI_GATEWAY_API_KEY` +- `VERCEL_OIDC_TOKEN` +- `AI_GATEWAY_BASE_URL` +- `ANTHROPIC_API_KEY` +- `ANTHROPIC_AUTH_TOKEN` +- `ANTHROPIC_BASE_URL` + +You can also pass explicit auth settings: + +```ts +const harness = createDeepAgents({ + auth: { + anthropic: { + apiKey: process.env.ANTHROPIC_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 written to a combined `.skills.md` file in the +working directory and prepended to the agent's prompt. + +## 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/packages/harness-deepagents/README.md b/packages/harness-deepagents/README.md index 329fe51a36d5..8a0e7a1ef7ea 100644 --- a/packages/harness-deepagents/README.md +++ b/packages/harness-deepagents/README.md @@ -1,17 +1,21 @@ # @ai-sdk/harness-deepagents -A [HarnessV1](../harness) adapter that runs [DeepAgents](https://github.com/deep-agents/deepagents) -(a LangGraph-based agent) as a coding-agent runtime inside an AI SDK sandbox. - -DeepAgents is Python-based, so this is a **bridge-backed** harness: the runtime -runs inside the sandbox via a Python bridge (`python3 bridge.py`) that speaks the -harness-v1 wire protocol, while the host adapter drives turns over a WebSocket. - -> **Status: scaffolding.** The package structure, host adapter shape, built-in -> tool definitions, and auth resolution are in place. The session lifecycle -> (`doStart`) and the Python bridge are not implemented yet — see the plan for -> the phased rollout (happy-path single/multi-turn first; detach/resume/approvals -> follow up). +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 implemented, pending live validation.** The host adapter +> (`doStart` + session: `doPromptTurn`/`doStop`/`doDestroy`) and the Node bridge +> (driving the `deepagents` npm package via `createDeepAgent` + `streamEvents`) +> are in place for single- and multi-turn-within-session use. Turn +> continuation, suspend/detach, cross-process resume, and built-in tool +> approvals throw `HarnessCapabilityUnsupportedError` and are follow-ups. The +> bridge has not yet been exercised against a live sandbox. ## Setup @@ -19,8 +23,9 @@ harness-v1 wire protocol, while the host adapter drives turns over a WebSocket. pnpm add @ai-sdk/harness-deepagents @ai-sdk/harness ``` -Requires a sandbox image with `python3.13` available; the harness installs the -Python dependencies (`requirements.txt`) into its bootstrap directory at startup. +The harness installs the +bridge's Node dependencies (the `deepagents` package and LangChain) into its +bootstrap directory via `pnpm` at startup. ## Usage diff --git a/packages/harness-deepagents/package.json b/packages/harness-deepagents/package.json index 42c44125afee..9700e1f82508 100644 --- a/packages/harness-deepagents/package.json +++ b/packages/harness-deepagents/package.json @@ -21,7 +21,7 @@ "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.mkdir('dist/bridge', { recursive: true }); for (const f of ['bridge.py', 'bridge_runtime.py', 'requirements.txt']) { await fs.copyFile('src/bridge/' + f, 'dist/bridge/' + f); } })\"", + "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", @@ -42,9 +42,11 @@ "zod": "3.25.76" }, "devDependencies": { + "@langchain/core": "^1.1.44", "@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" }, diff --git a/packages/harness-deepagents/src/bridge/bridge.py b/packages/harness-deepagents/src/bridge/bridge.py deleted file mode 100644 index 94c394085e87..000000000000 --- a/packages/harness-deepagents/src/bridge/bridge.py +++ /dev/null @@ -1,26 +0,0 @@ -"""DeepAgents harness bridge entrypoint (runs inside the sandbox). - -Phase 3 placeholder. This process is launched by the host adapter -(`createDeepAgents().doStart`) as `python3 /tmp/harness/deepagents/bridge.py`. - -Responsibilities (to be implemented in Phase 3 — Option A, the Python bridge -speaks the harness-v1 wire protocol directly): - - - Bind a WebSocket server and print the harness-v1 `bridge-ready` JSON line - (`{"type": "bridge-ready", "port": }`) to stdout. - - Authenticate the host connection via the bridge token, then send - `bridge-hello`. - - Drive DeepAgents (`create_deep_agent()`) per inbound `start` command and - translate LangGraph `astream_events(v=2)` into harness-v1 stream parts - (stream-start / text-* / reasoning-* / tool-call / tool-approval-request / - tool-result / finish-step / finish / error). - - Consume the shared inbound command vocabulary (tool-result, - tool-approval-response, user-message, abort, shutdown, resume, detach). - - Maintain the seq event-log + resume replay and lifecycle files. - -The reusable transport lives in `bridge_runtime.py`. -""" - -raise NotImplementedError( - "DeepAgents Python bridge is not implemented yet (Phase 3)." -) diff --git a/packages/harness-deepagents/src/bridge/bridge_runtime.py b/packages/harness-deepagents/src/bridge/bridge_runtime.py deleted file mode 100644 index eaf683597584..000000000000 --- a/packages/harness-deepagents/src/bridge/bridge_runtime.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Reusable harness-v1 bridge transport for the DeepAgents Python bridge. - -Phase 3 placeholder. This module will own the transport concerns that -`@ai-sdk/harness/bridge` (`runBridge`) provides for Node bridges, re-implemented -in Python so the DeepAgents runtime can speak the harness-v1 wire protocol -directly (Option A): - - - WebSocket server + bridge-token auth - - the `bridge-ready` stdout announcement and `bridge-hello` handshake - - the in-memory event log with monotonic `seq`, plus resume replay - - the lifecycle / meta state files on disk - -`bridge.py` supplies only the DeepAgents-specific turn driver on top of this. -""" diff --git a/packages/harness-deepagents/src/bridge/index.ts b/packages/harness-deepagents/src/bridge/index.ts new file mode 100644 index 000000000000..e777e2590b78 --- /dev/null +++ b/packages/harness-deepagents/src/bridge/index.ts @@ -0,0 +1,308 @@ +// Long-running process that runs alongside the DeepAgents (LangGraph JS) +// runtime inside the sandbox. The generic transport — WebSocket server, token +// auth, single-flight reconnect, the in-memory event log + `seq`, resume +// replay, and the lifecycle/meta files — lives in the shared +// `@ai-sdk/harness/bridge` runtime. This file supplies only the +// DeepAgents-specific turn driver: it builds an agent with `createDeepAgent()` +// and translates its `streamEvents` output into harness-v1 stream parts. +// +// CONSTRAINT — the third-party imports below are NEVER bundled into the +// compiled `bridge/index.mjs`. They are declared `external` in tsup.config.ts +// and resolved at runtime from the node_modules this bridge installs *inside +// the sandbox* from `src/bridge/package.json` (and its pinned +// `pnpm-lock.yaml`). When adding/changing a third-party import here you MUST +// keep all three in sync: the import below, the `external` array in +// tsup.config.ts, and the dependency in `src/bridge/package.json`. + +import { randomUUID } from 'node:crypto'; +import { readFile } from 'node:fs/promises'; +import { argv } from 'node:process'; +import { + runBridge, + type BridgeEvent, + type BridgeTurn, +} from '@ai-sdk/harness/bridge'; +import { tool } from '@langchain/core/tools'; +import { createDeepAgent, LocalShellBackend } from 'deepagents'; +import { z } from 'zod/v4'; +import type { StartMessage } from '../deepagents-bridge-protocol'; + +// Native LangGraph tool name -> harness-v1 common name. +const NATIVE_TO_COMMON: Readonly> = { + read_file: 'read', + write_file: 'write', + shell: 'bash', + search: 'grep', +}; + +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 resolves model strings as `provider:model`; the host sends +// `provider/model`. +function parseModelName(raw: string): string { + return raw.includes('/') ? raw.replace('/', ':') : 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 so the LangGraph +// checkpointer accumulates conversation state. Host tools close over a getter +// for the live turn rather than a fixed one. +let agent: ReturnType | undefined; +let currentTurn: BridgeTurn | undefined; + +const jsonTypeToZod: Record z.ZodTypeAny> = { + string: () => z.string(), + integer: () => z.number(), + number: () => z.number(), + boolean: () => z.boolean(), + object: () => z.record(z.string(), z.unknown()), + array: () => z.array(z.unknown()), +}; + +function schemaFromJson(input: unknown) { + const obj = + input && typeof input === 'object' + ? (input as { + properties?: Record; + required?: string[]; + }) + : {}; + const properties = obj.properties ?? {}; + const required = new Set(obj.required ?? []); + const shape: Record = {}; + for (const [name, def] of Object.entries(properties)) { + const base = ( + jsonTypeToZod[def?.type ?? 'string'] ?? jsonTypeToZod.string + )(); + shape[name] = required.has(name) ? base : base.optional(); + } + return z.object(shape); +} + +// Host-defined tools become LangChain tools that round-trip through the host: +// emit a `tool-call` (providerExecuted=false), then block on the host's +// `tool-result` before returning to LangGraph. +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: schemaFromJson(schema.inputSchema), + }, + ), + ); +} + +async function readSkillsBlock(): Promise { + try { + const content = await readFile(`${workdir}/.skills.md`, 'utf8'); + return content.trim() ? `## Available Skills\n\n${content}` : ''; + } catch { + return ''; + } +} + +function buildSystemPrompt(start: StartMessage, skillsBlock: string): string { + return [start.instructions ?? '', skillsBlock].filter(Boolean).join('\n\n'); +} + +async function runTurn(start: StartMessage, turn: BridgeTurn): Promise { + currentTurn = turn; + const emit = (event: Record) => + turn.emit(event as BridgeEvent); + + if (!agent) { + const skillsBlock = await readSkillsBlock(); + agent = createDeepAgent({ + model: parseModelName(start.model ?? 'claude-sonnet-4'), + tools: buildHostTools(start.tools), + backend: new LocalShellBackend({ rootDir: workdir }), + systemPrompt: buildSystemPrompt(start, skillsBlock) || undefined, + checkpointer: true, + }); + } + + 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; + const activeToolRunIds = new Set(); + + 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 emitText = (delta: string) => + emit({ type: 'text-delta', id: ensureTextBlock(), delta }); + const emitReasoning = (delta: string) => { + if (!reasoningBlockId) { + reasoningBlockId = `reasoning-${randomUUID()}`; + emit({ type: 'reasoning-start', id: reasoningBlockId }); + } + emit({ type: 'reasoning-delta', id: reasoningBlockId, delta }); + }; + + const stream = await agent.streamEvents( + { messages: [{ role: 'user', content: start.prompt }] }, + { + version: 'v2', + configurable: { thread_id: 'bridge-session' }, + recursionLimit: 50, + signal: turn.abortSignal, + }, + ); + + 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) { + inputTokens = Math.max(inputTokens, usage.input_tokens ?? 0); + outputTokens = Math.max(outputTokens, usage.output_tokens ?? 0); + } + } 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 inside the tool fn; only the + // runtime's built-in tools are surfaced here (providerExecuted). + if (!hostToolNames.has(toolName)) { + endTextBlock(); + emit({ + type: 'tool-call', + toolCallId: runId, + toolName: toCommonName(toolName), + input: JSON.stringify(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: runId, + toolName: toCommonName(toolName), + result: output ?? null, + }); + } + if (runId) activeToolRunIds.delete(runId); + } else if (kind === 'on_chain_end' && event.name === 'agent') { + endTextBlock(); + emit({ + type: 'finish-step', + finishReason: { unified: 'stop' }, + usage: { + inputTokens: { total: inputTokens }, + outputTokens: { total: outputTokens }, + }, + }); + } + } + + endTextBlock(); + if (reasoningBlockId) emit({ type: 'reasoning-end', id: reasoningBlockId }); + 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/package.json b/packages/harness-deepagents/src/bridge/package.json new file mode 100644 index 000000000000..787a33b84ecf --- /dev/null +++ b/packages/harness-deepagents/src/bridge/package.json @@ -0,0 +1,12 @@ +{ + "name": "harness-deepagents-bridge", + "version": "0.0.0", + "private": true, + "type": "module", + "dependencies": { + "@langchain/core": "^1.1.44", + "deepagents": "1.10.2", + "ws": "8.20.1", + "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..61a8e8048236 --- /dev/null +++ b/packages/harness-deepagents/src/bridge/pnpm-lock.yaml @@ -0,0 +1,446 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@langchain/core': + specifier: ^1.1.44 + version: 1.1.49(ws@8.20.1) + deepagents: + specifier: 1.10.2 + version: 1.10.2(langsmith@0.7.10(ws@8.20.1))(ws@8.20.1) + ws: + specifier: 8.20.1 + version: 8.20.1 + zod: + specifier: ^4.3.6 + version: 4.4.3 + +packages: + + '@cfworker/json-schema@4.1.1': + resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==} + + '@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/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'} + + '@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'} + + 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==} + + 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 + + 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==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ws@8.20.1: + resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} + 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: + + '@cfworker/json-schema@4.1.1': {} + + '@langchain/core@1.1.49(ws@8.20.1)': + dependencies: + '@cfworker/json-schema': 4.1.1 + '@standard-schema/spec': 1.1.0 + js-tiktoken: 1.0.21 + langsmith: 0.7.10(ws@8.20.1) + 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(ws@8.20.1))': + dependencies: + '@langchain/core': 1.1.49(ws@8.20.1) + + '@langchain/langgraph-sdk@1.9.22(@langchain/core@1.1.49(ws@8.20.1))': + dependencies: + '@langchain/core': 1.1.49(ws@8.20.1) + '@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(ws@8.20.1))(zod@4.4.3)': + dependencies: + '@langchain/core': 1.1.49(ws@8.20.1) + '@langchain/langgraph-checkpoint': 1.1.1(@langchain/core@1.1.49(ws@8.20.1)) + '@langchain/langgraph-sdk': 1.9.22(@langchain/core@1.1.49(ws@8.20.1)) + '@langchain/protocol': 0.0.16 + '@standard-schema/spec': 1.1.0 + zod: 4.4.3 + transitivePeerDependencies: + - react + - react-dom + - svelte + - vue + + '@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 + + '@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(ws@8.20.1))(ws@8.20.1): + dependencies: + '@langchain/core': 1.1.49(ws@8.20.1) + '@langchain/langgraph': 1.4.2(@langchain/core@1.1.49(ws@8.20.1))(zod@4.4.3) + '@langchain/langgraph-sdk': 1.9.22(@langchain/core@1.1.49(ws@8.20.1)) + fast-glob: 3.3.3 + langchain: 1.4.5(@langchain/core@1.1.49(ws@8.20.1))(ws@8.20.1) + langsmith: 0.7.10(ws@8.20.1) + 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 + + 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 + + langchain@1.4.5(@langchain/core@1.1.49(ws@8.20.1))(ws@8.20.1): + dependencies: + '@langchain/core': 1.1.49(ws@8.20.1) + '@langchain/langgraph': 1.4.2(@langchain/core@1.1.49(ws@8.20.1))(zod@4.4.3) + '@langchain/langgraph-checkpoint': 1.1.1(@langchain/core@1.1.49(ws@8.20.1)) + langsmith: 0.7.10(ws@8.20.1) + 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(ws@8.20.1): + dependencies: + p-queue: 6.6.2 + optionalDependencies: + ws: 8.20.1 + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mustache@4.2.0: {} + + 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 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + ws@8.20.1: {} + + yaml@2.9.0: {} + + zod@4.4.3: {} diff --git a/packages/harness-deepagents/src/bridge/requirements.txt b/packages/harness-deepagents/src/bridge/requirements.txt deleted file mode 100644 index 9d1bbac4f173..000000000000 --- a/packages/harness-deepagents/src/bridge/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -# Installed in-sandbox at bootstrap (python3.13). Pinned to match the -# known-good versions from the agent-harness-sdk DeepAgents adapter. -deepagents==0.6.1 -websockets==16.0 -jsonschema==4.26.0 -langchain-anthropic==1.4.3 -langchain-openai==1.2.1 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-harness.test.ts b/packages/harness-deepagents/src/deepagents-harness.test.ts index 612d5f44e0bb..32e3bd28c10f 100644 --- a/packages/harness-deepagents/src/deepagents-harness.test.ts +++ b/packages/harness-deepagents/src/deepagents-harness.test.ts @@ -2,13 +2,30 @@ import { HarnessCapabilityUnsupportedError, type HarnessV1StartOptions, } from '@ai-sdk/harness'; -import { describe, expect, it } from 'vitest'; +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(); @@ -34,21 +51,21 @@ describe('createDeepAgents', () => { expect(DEEPAGENTS_DEFAULT_CONTEXT_WINDOW).toBe(200_000); }); - it('ships the python bridge files and a pip install command in its bootstrap', async () => { + 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.py'), - expect.stringContaining('bridge_runtime.py'), - expect.stringContaining('requirements.txt'), + 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('pip install'); - expect(commands).toContain('requirements.txt'); + expect(commands).toContain('pnpm'); + expect(commands).toContain('install'); }); it('caches the bootstrap across calls', async () => { diff --git a/packages/harness-deepagents/src/deepagents-harness.ts b/packages/harness-deepagents/src/deepagents-harness.ts index 85c1dd7b5469..f86c419da033 100644 --- a/packages/harness-deepagents/src/deepagents-harness.ts +++ b/packages/harness-deepagents/src/deepagents-harness.ts @@ -38,7 +38,7 @@ type DeepAgentsChannel = SandboxChannel; /* * Bootstrap lives in /tmp because it's pure derived state — the harness can - * reinstall the Python deps and bridge files on any fresh sandbox from the + * reinstall the bridge's Node deps and files on any fresh sandbox from the * recipe. Persistence comes from the sandbox provider's snapshot, not the path. */ const BOOTSTRAP_DIR = '/tmp/harness/deepagents'; @@ -112,26 +112,23 @@ export function createDeepAgents( supportsBuiltinToolApprovals: false, getBootstrap: async () => { if (cachedBootstrap != null) return cachedBootstrap; - const [bridge, bridgeRuntime, requirements] = await Promise.all([ - readBridgeAsset('bridge.py'), - readBridgeAsset('bridge_runtime.py'), - readBridgeAsset('requirements.txt'), + 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.py`, content: bridge }, - { - path: `${BOOTSTRAP_DIR}/bridge_runtime.py`, - content: bridgeRuntime, - }, - { path: `${BOOTSTRAP_DIR}/requirements.txt`, content: requirements }, + { 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: `python3 -m pip install --no-cache-dir -r ${BOOTSTRAP_DIR}/requirements.txt`, + command: `pnpm --dir ${BOOTSTRAP_DIR} install --frozen-lockfile --store-dir ${BOOTSTRAP_DIR}/.pnpm-store`, }, ], }; @@ -213,7 +210,7 @@ export function createDeepAgents( }); const proc = await session.spawn({ - command: `python3 ${BOOTSTRAP_DIR}/bridge.py --workdir ${workDir} --bridge-state-dir ${bridgeStateDir} --bootstrap-dir ${BOOTSTRAP_DIR}`, + command: `node ${BOOTSTRAP_DIR}/bridge.mjs --workdir ${workDir} --bridge-state-dir ${bridgeStateDir} --bootstrap-dir ${BOOTSTRAP_DIR}`, env, abortSignal: startOpts.abortSignal, }); diff --git a/packages/harness-deepagents/tsup.config.ts b/packages/harness-deepagents/tsup.config.ts index 307328fe3179..0713a3d1b70d 100644 --- a/packages/harness-deepagents/tsup.config.ts +++ b/packages/harness-deepagents/tsup.config.ts @@ -1,10 +1,5 @@ import { defineConfig } from 'tsup'; -// Only the host-side adapter is compiled by tsup. The agent runtime is a -// Python bridge (`src/bridge/*.py`) that runs inside the sandbox; its files are -// shipped verbatim via the `copy-bridge-assets` script and its dependencies are -// installed in-sandbox from `requirements.txt` at bootstrap time — they are -// never bundled here. export default defineConfig([ { entry: { index: 'src/index.ts' }, @@ -13,4 +8,19 @@ export default defineConfig([ 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', 'ws', 'zod'], + }, ]); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 18adc8c04520..67371058771b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2612,6 +2612,9 @@ importers: 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) '@types/node': specifier: 22.19.19 version: 22.19.19 @@ -2621,6 +2624,9 @@ importers: '@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) @@ -13793,6 +13799,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'} @@ -25750,6 +25761,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) @@ -25800,6 +25831,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) @@ -33067,6 +33142,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: {} @@ -35968,6 +36066,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 From 4a51784c4cfe27fde7836c2ca831d89d76a53812 Mon Sep 17 00:00:00 2001 From: mlekhi Date: Thu, 18 Jun 2026 14:58:01 -0700 Subject: [PATCH 04/39] adding changelog --- .../02-ai-sdk-harnesses/04-deepagents.mdx | 187 ------------------ packages/harness-deepagents/CHANGELOG.md | 1 + 2 files changed, 1 insertion(+), 187 deletions(-) delete mode 100644 content/providers/02-ai-sdk-harnesses/04-deepagents.mdx create mode 100644 packages/harness-deepagents/CHANGELOG.md diff --git a/content/providers/02-ai-sdk-harnesses/04-deepagents.mdx b/content/providers/02-ai-sdk-harnesses/04-deepagents.mdx deleted file mode 100644 index 216f550f09da..000000000000 --- a/content/providers/02-ai-sdk-harnesses/04-deepagents.mdx +++ /dev/null @@ -1,187 +0,0 @@ ---- -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 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 - -By default, authentication is resolved from the host environment and forwarded -to the sandbox bridge. The adapter checks for AI Gateway credentials first, then -ambient Anthropic credentials. - -Supported environment variables: - -- `AI_GATEWAY_API_KEY` -- `VERCEL_OIDC_TOKEN` -- `AI_GATEWAY_BASE_URL` -- `ANTHROPIC_API_KEY` -- `ANTHROPIC_AUTH_TOKEN` -- `ANTHROPIC_BASE_URL` - -You can also pass explicit auth settings: - -```ts -const harness = createDeepAgents({ - auth: { - anthropic: { - apiKey: process.env.ANTHROPIC_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 written to a combined `.skills.md` file in the -working directory and prepended to the agent's prompt. - -## 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/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 From c7ba88278023970363c77bb426f1a36440b8a84d Mon Sep 17 00:00:00 2001 From: mlekhi Date: Thu, 18 Jun 2026 16:57:09 -0700 Subject: [PATCH 05/39] feat(harness-deepagents): align with maintainer conventions + add examples --- .changeset/deepagents-harness.md | 4 +- .../05-harness-adapters.mdx | 3 +- .../02-ai-sdk-harnesses/05-deepagents.mdx | 187 ++++++++++++++++++ examples/ai-functions/package.json | 1 + .../harness-agent/deepagents/generate-text.ts | 34 ++++ .../harness-agent/deepagents/multi-turn.ts | 41 ++++ .../harness-agent/deepagents/stream-text.ts | 37 ++++ .../harness-agent/deepagents/with-skills.ts | 74 +++++++ .../harness-agent/deepagents/with-tools.ts | 53 +++++ .../agents/deepagents/basic-agent.ts | 36 ++++ .../harness/deepagents/basic.ts | 8 + examples/harness-e2e-tui/package.json | 1 + pnpm-lock.yaml | 6 + 13 files changed, 481 insertions(+), 4 deletions(-) create mode 100644 content/providers/02-ai-sdk-harnesses/05-deepagents.mdx create mode 100644 examples/ai-functions/src/harness-agent/deepagents/generate-text.ts create mode 100644 examples/ai-functions/src/harness-agent/deepagents/multi-turn.ts create mode 100644 examples/ai-functions/src/harness-agent/deepagents/stream-text.ts create mode 100644 examples/ai-functions/src/harness-agent/deepagents/with-skills.ts create mode 100644 examples/ai-functions/src/harness-agent/deepagents/with-tools.ts create mode 100644 examples/harness-e2e-tui/agents/deepagents/basic-agent.ts create mode 100644 examples/harness-e2e-tui/harness/deepagents/basic.ts diff --git a/.changeset/deepagents-harness.md b/.changeset/deepagents-harness.md index 164118b58198..0c81807e9472 100644 --- a/.changeset/deepagents-harness.md +++ b/.changeset/deepagents-harness.md @@ -1,5 +1,5 @@ --- -'@ai-sdk/harness-deepagents': patch +'@ai-sdk/harness-deepagents': major --- -Add `@ai-sdk/harness-deepagents`, a HarnessV1 adapter for LangChain's LangGraph-based DeepAgents runtime. The bridge-backed adapter runs a Node bridge inside the sandbox (driving the `deepagents` npm package via `createDeepAgent` + `streamEvents`, on the shared `@ai-sdk/harness/bridge` transport) and supports single- and multi-turn-within-session prompts, host-executed tools, skills, and the `read`/`write`/`bash`/`grep` built-ins. +feat(harness-deepagents): implement harness adapter \ No newline at end of file diff --git a/content/docs/03-ai-sdk-harnesses/05-harness-adapters.mdx b/content/docs/03-ai-sdk-harnesses/05-harness-adapters.mdx index 2023766c3cab..de55a1d85e62 100644 --- a/content/docs/03-ai-sdk-harnesses/05-harness-adapters.mdx +++ b/content/docs/03-ai-sdk-harnesses/05-harness-adapters.mdx @@ -16,12 +16,12 @@ The AI SDK includes the following harness adapters: - [Claude Code](/providers/ai-sdk-harnesses/claude-code) (`@ai-sdk/harness-claude-code`) - [Codex](/providers/ai-sdk-harnesses/codex) (`@ai-sdk/harness-codex`) -- [DeepAgents](/providers/ai-sdk-harnesses/deepagents) (`@ai-sdk/harness-deepagents`) - [Pi](/providers/ai-sdk-harnesses/pi) (`@ai-sdk/harness-pi`) ### Coming Soon - Amp (`@ai-sdk/harness-amp`) +- DeepAgents (`@ai-sdk/harness-deepagents`) - Goose (`@ai-sdk/harness-goose`) - Mastra (`@ai-sdk/harness-mastra`) - OpenCode (`@ai-sdk/harness-opencode`) @@ -32,5 +32,4 @@ The AI SDK includes the following harness adapters: | ------------------------------------------------------ | ---------------- | ------------------- | ------------------- | ---------------------- | | [Claude Code](/providers/ai-sdk-harnesses/claude-code) | Sandbox bridge | | | | | [Codex](/providers/ai-sdk-harnesses/codex) | Sandbox bridge | | | | -| [DeepAgents](/providers/ai-sdk-harnesses/deepagents) | Sandbox bridge | | | | | [Pi](/providers/ai-sdk-harnesses/pi) | Host process | | | | 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..216f550f09da --- /dev/null +++ b/content/providers/02-ai-sdk-harnesses/05-deepagents.mdx @@ -0,0 +1,187 @@ +--- +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 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 + +By default, authentication is resolved from the host environment and forwarded +to the sandbox bridge. The adapter checks for AI Gateway credentials first, then +ambient Anthropic credentials. + +Supported environment variables: + +- `AI_GATEWAY_API_KEY` +- `VERCEL_OIDC_TOKEN` +- `AI_GATEWAY_BASE_URL` +- `ANTHROPIC_API_KEY` +- `ANTHROPIC_AUTH_TOKEN` +- `ANTHROPIC_BASE_URL` + +You can also pass explicit auth settings: + +```ts +const harness = createDeepAgents({ + auth: { + anthropic: { + apiKey: process.env.ANTHROPIC_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 written to a combined `.skills.md` file in the +working directory and prepended to the agent's prompt. + +## 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/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/with-skills.ts b/examples/ai-functions/src/harness-agent/deepagents/with-skills.ts new file mode 100644 index 000000000000..60c8af71c545 --- /dev/null +++ b/examples/ai-functions/src/harness-agent/deepagents/with-skills.ts @@ -0,0 +1,74 @@ +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 domain-specific reference material the agent loads on demand + * when it decides — based on the skill's name and description — that the + * current task is relevant. The DeepAgents harness writes the skills to a + * combined `.skills.md` in the working directory and prepends them to the + * system prompt. + */ +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-tui/agents/deepagents/basic-agent.ts b/examples/harness-e2e-tui/agents/deepagents/basic-agent.ts new file mode 100644 index 000000000000..f822ee2e07b4 --- /dev/null +++ b/examples/harness-e2e-tui/agents/deepagents/basic-agent.ts @@ -0,0 +1,36 @@ +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` directly rather than `InferAgentUIMessage` — see the note in the OpenCode/Codex basic agents for why the + * structural inference is side-stepped via the `tools` field. + */ +export type DeepAgentsHarnessAgentMessage = UIMessage< + unknown, + never, + InferUITools +>; 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/package.json b/examples/harness-e2e-tui/package.json index a2b6343fc300..a4ba6ecb7f31 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/pnpm-lock.yaml b/pnpm-lock.yaml index 67371058771b..cd100584cbf9 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 @@ -678,6 +681,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 From 585d0bbea00e1950b7b42e3f1345c1867d170d9d Mon Sep 17 00:00:00 2001 From: mlekhi Date: Thu, 18 Jun 2026 17:18:37 -0700 Subject: [PATCH 06/39] fix(harness-deepagents): validate against live sandbox --- packages/harness-deepagents/README.md | 14 +- packages/harness-deepagents/package.json | 1 + .../harness-deepagents/src/bridge/index.ts | 37 +++-- .../src/bridge/package.json | 3 + .../src/bridge/pnpm-lock.yaml | 155 +++++++++++++++--- packages/harness-deepagents/tsup.config.ts | 8 +- pnpm-lock.yaml | 3 + 7 files changed, 178 insertions(+), 43 deletions(-) diff --git a/packages/harness-deepagents/README.md b/packages/harness-deepagents/README.md index 8a0e7a1ef7ea..43c02864f7da 100644 --- a/packages/harness-deepagents/README.md +++ b/packages/harness-deepagents/README.md @@ -9,13 +9,13 @@ 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 implemented, pending live validation.** The host adapter -> (`doStart` + session: `doPromptTurn`/`doStop`/`doDestroy`) and the Node bridge -> (driving the `deepagents` npm package via `createDeepAgent` + `streamEvents`) -> are in place for single- and multi-turn-within-session use. Turn -> continuation, suspend/detach, cross-process resume, and built-in tool -> approvals throw `HarnessCapabilityUnsupportedError` and are follow-ups. The -> bridge has not yet been exercised against a live sandbox. +> **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 diff --git a/packages/harness-deepagents/package.json b/packages/harness-deepagents/package.json index 9700e1f82508..b6d85cafc55b 100644 --- a/packages/harness-deepagents/package.json +++ b/packages/harness-deepagents/package.json @@ -43,6 +43,7 @@ }, "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:*", diff --git a/packages/harness-deepagents/src/bridge/index.ts b/packages/harness-deepagents/src/bridge/index.ts index e777e2590b78..333769dc1ceb 100644 --- a/packages/harness-deepagents/src/bridge/index.ts +++ b/packages/harness-deepagents/src/bridge/index.ts @@ -23,6 +23,7 @@ import { type BridgeTurn, } from '@ai-sdk/harness/bridge'; import { tool } from '@langchain/core/tools'; +import { MemorySaver } from '@langchain/langgraph'; import { createDeepAgent, LocalShellBackend } from 'deepagents'; import { z } from 'zod/v4'; import type { StartMessage } from '../deepagents-bridge-protocol'; @@ -158,7 +159,9 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise { tools: buildHostTools(start.tools), backend: new LocalShellBackend({ rootDir: workdir }), systemPrompt: buildSystemPrompt(start, skillsBlock) || undefined, - checkpointer: true, + // A real checkpointer instance (not `true`, which LangGraph rejects for + // root graphs) gives multi-turn memory within this bridge process. + checkpointer: new MemorySaver(), }); } @@ -243,6 +246,28 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise { inputTokens = Math.max(inputTokens, usage.input_tokens ?? 0); outputTokens = Math.max(outputTokens, usage.output_tokens ?? 0); } + } else if (kind === 'on_chat_model_end') { + // LangChain.js reports final token usage on the model-end event (not on + // the streamed chunks). Each model call is one step boundary. + const output = data.output as + | { + usage_metadata?: { input_tokens?: number; output_tokens?: number }; + } + | undefined; + const usage = output?.usage_metadata; + if (usage) { + inputTokens += usage.input_tokens ?? 0; + outputTokens += usage.output_tokens ?? 0; + } + endTextBlock(); + turn.emit({ + type: 'finish-step', + finishReason: { unified: 'stop' }, + usage: { + inputTokens: { total: usage?.input_tokens ?? 0 }, + outputTokens: { total: usage?.output_tokens ?? 0 }, + }, + }); } else if (kind === 'on_tool_start') { const toolName = (event.name as string) ?? 'unknown'; const runId = (event.run_id as string) ?? ''; @@ -276,16 +301,6 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise { }); } if (runId) activeToolRunIds.delete(runId); - } else if (kind === 'on_chain_end' && event.name === 'agent') { - endTextBlock(); - emit({ - type: 'finish-step', - finishReason: { unified: 'stop' }, - usage: { - inputTokens: { total: inputTokens }, - outputTokens: { total: outputTokens }, - }, - }); } } diff --git a/packages/harness-deepagents/src/bridge/package.json b/packages/harness-deepagents/src/bridge/package.json index 787a33b84ecf..a4a5bab52908 100644 --- a/packages/harness-deepagents/src/bridge/package.json +++ b/packages/harness-deepagents/src/bridge/package.json @@ -4,7 +4,10 @@ "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.20.1", "zod": "^4.3.6" diff --git a/packages/harness-deepagents/src/bridge/pnpm-lock.yaml b/packages/harness-deepagents/src/bridge/pnpm-lock.yaml index 61a8e8048236..071d26155e9c 100644 --- a/packages/harness-deepagents/src/bridge/pnpm-lock.yaml +++ b/packages/harness-deepagents/src/bridge/pnpm-lock.yaml @@ -8,12 +8,21 @@ importers: .: dependencies: + '@langchain/anthropic': + specifier: ^1.0.0 + version: 1.4.1(@langchain/core@1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1)) '@langchain/core': specifier: ^1.1.44 - version: 1.1.49(ws@8.20.1) + version: 1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1) + '@langchain/langgraph': + specifier: ^1.3.0 + version: 1.4.2(@langchain/core@1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1))(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.20.1)(zod@4.4.3))(ws@8.20.1))(ws@8.20.1) deepagents: specifier: 1.10.2 - version: 1.10.2(langsmith@0.7.10(ws@8.20.1))(ws@8.20.1) + version: 1.10.2(langsmith@0.7.10(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1))(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1) ws: specifier: 8.20.1 version: 8.20.1 @@ -23,9 +32,28 @@ importers: 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'} @@ -65,6 +93,12 @@ packages: 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==} @@ -80,6 +114,9 @@ packages: 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==} @@ -108,6 +145,9 @@ packages: 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==} @@ -138,6 +178,10 @@ packages: 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'} @@ -176,6 +220,17 @@ packages: 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'} @@ -214,10 +269,16 @@ packages: 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.20.1: resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} engines: {node: '>=10.0.0'} @@ -240,14 +301,29 @@ packages: 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/core@1.1.49(ws@8.20.1)': + '@langchain/anthropic@1.4.1(@langchain/core@1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1))': + dependencies: + '@anthropic-ai/sdk': 0.103.0(zod@4.4.3) + '@langchain/core': 1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1) + zod: 4.4.3 + + '@langchain/core@1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1)': dependencies: '@cfworker/json-schema': 4.1.1 '@standard-schema/spec': 1.1.0 js-tiktoken: 1.0.21 - langsmith: 0.7.10(ws@8.20.1) + langsmith: 0.7.10(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1) mustache: 4.2.0 p-queue: 6.6.2 zod: 4.4.3 @@ -258,23 +334,23 @@ snapshots: - openai - ws - '@langchain/langgraph-checkpoint@1.1.1(@langchain/core@1.1.49(ws@8.20.1))': + '@langchain/langgraph-checkpoint@1.1.1(@langchain/core@1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1))': dependencies: - '@langchain/core': 1.1.49(ws@8.20.1) + '@langchain/core': 1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1) - '@langchain/langgraph-sdk@1.9.22(@langchain/core@1.1.49(ws@8.20.1))': + '@langchain/langgraph-sdk@1.9.22(@langchain/core@1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1))': dependencies: - '@langchain/core': 1.1.49(ws@8.20.1) + '@langchain/core': 1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1) '@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(ws@8.20.1))(zod@4.4.3)': + '@langchain/langgraph@1.4.2(@langchain/core@1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1))(zod@4.4.3)': dependencies: - '@langchain/core': 1.1.49(ws@8.20.1) - '@langchain/langgraph-checkpoint': 1.1.1(@langchain/core@1.1.49(ws@8.20.1)) - '@langchain/langgraph-sdk': 1.9.22(@langchain/core@1.1.49(ws@8.20.1)) + '@langchain/core': 1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1) + '@langchain/langgraph-checkpoint': 1.1.1(@langchain/core@1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1)) + '@langchain/langgraph-sdk': 1.9.22(@langchain/core@1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1)) '@langchain/protocol': 0.0.16 '@standard-schema/spec': 1.1.0 zod: 4.4.3 @@ -284,6 +360,15 @@ snapshots: - svelte - vue + '@langchain/openai@1.4.7(@langchain/core@1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1))(ws@8.20.1)': + dependencies: + '@langchain/core': 1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1) + js-tiktoken: 1.0.21 + openai: 6.43.0(ws@8.20.1)(zod@4.4.3) + zod: 4.4.3 + transitivePeerDependencies: + - ws + '@langchain/protocol@0.0.16': {} '@nodelib/fs.scandir@2.1.5': @@ -298,6 +383,8 @@ snapshots: '@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': {} @@ -308,14 +395,14 @@ snapshots: dependencies: fill-range: 7.1.1 - deepagents@1.10.2(langsmith@0.7.10(ws@8.20.1))(ws@8.20.1): + deepagents@1.10.2(langsmith@0.7.10(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1))(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1): dependencies: - '@langchain/core': 1.1.49(ws@8.20.1) - '@langchain/langgraph': 1.4.2(@langchain/core@1.1.49(ws@8.20.1))(zod@4.4.3) - '@langchain/langgraph-sdk': 1.9.22(@langchain/core@1.1.49(ws@8.20.1)) + '@langchain/core': 1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1) + '@langchain/langgraph': 1.4.2(@langchain/core@1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1))(zod@4.4.3) + '@langchain/langgraph-sdk': 1.9.22(@langchain/core@1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1)) fast-glob: 3.3.3 - langchain: 1.4.5(@langchain/core@1.1.49(ws@8.20.1))(ws@8.20.1) - langsmith: 0.7.10(ws@8.20.1) + langchain: 1.4.5(@langchain/core@1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1))(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1) + langsmith: 0.7.10(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1) micromatch: 4.0.8 yaml: 2.9.0 zod: 4.4.3 @@ -343,6 +430,8 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-sha256@1.3.0: {} + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -369,12 +458,17 @@ snapshots: dependencies: base64-js: 1.5.1 - langchain@1.4.5(@langchain/core@1.1.49(ws@8.20.1))(ws@8.20.1): + json-schema-to-ts@3.1.1: dependencies: - '@langchain/core': 1.1.49(ws@8.20.1) - '@langchain/langgraph': 1.4.2(@langchain/core@1.1.49(ws@8.20.1))(zod@4.4.3) - '@langchain/langgraph-checkpoint': 1.1.1(@langchain/core@1.1.49(ws@8.20.1)) - langsmith: 0.7.10(ws@8.20.1) + '@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.20.1)(zod@4.4.3))(ws@8.20.1))(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1): + dependencies: + '@langchain/core': 1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1) + '@langchain/langgraph': 1.4.2(@langchain/core@1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1))(zod@4.4.3) + '@langchain/langgraph-checkpoint': 1.1.1(@langchain/core@1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1)) + langsmith: 0.7.10(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1) zod: 4.4.3 transitivePeerDependencies: - '@opentelemetry/api' @@ -388,10 +482,11 @@ snapshots: - ws - zod-to-json-schema - langsmith@0.7.10(ws@8.20.1): + langsmith@0.7.10(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1): dependencies: p-queue: 6.6.2 optionalDependencies: + openai: 6.43.0(ws@8.20.1)(zod@4.4.3) ws: 8.20.1 merge2@1.4.1: {} @@ -403,6 +498,11 @@ snapshots: mustache@4.2.0: {} + openai@6.43.0(ws@8.20.1)(zod@4.4.3): + optionalDependencies: + ws: 8.20.1 + zod: 4.4.3 + p-finally@1.0.0: {} p-queue@6.6.2: @@ -435,10 +535,17 @@ snapshots: 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.20.1: {} yaml@2.9.0: {} diff --git a/packages/harness-deepagents/tsup.config.ts b/packages/harness-deepagents/tsup.config.ts index 0713a3d1b70d..bfda94f1bd22 100644 --- a/packages/harness-deepagents/tsup.config.ts +++ b/packages/harness-deepagents/tsup.config.ts @@ -21,6 +21,12 @@ export default defineConfig([ // 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', 'ws', 'zod'], + external: [ + 'deepagents', + '@langchain/core', + '@langchain/langgraph', + 'ws', + 'zod', + ], }, ]); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd100584cbf9..d305f54d91e9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2621,6 +2621,9 @@ importers: '@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 From f2ab7a603b2fc3285d6e1fecfb973672125c6bd8 Mon Sep 17 00:00:00 2001 From: mlekhi Date: Thu, 18 Jun 2026 17:33:03 -0700 Subject: [PATCH 07/39] cleanup --- .../harness-agent/deepagents/with-skills.ts | 8 +--- .../agents/deepagents/basic-agent.ts | 6 +-- .../harness-deepagents/src/bridge/index.ts | 40 +++++-------------- .../harness-deepagents/src/deepagents-auth.ts | 15 +------ .../src/deepagents-bridge-protocol.ts | 14 +------ .../src/deepagents-harness.ts | 33 +++------------ packages/harness-deepagents/src/index.ts | 6 +-- 7 files changed, 21 insertions(+), 101 deletions(-) diff --git a/examples/ai-functions/src/harness-agent/deepagents/with-skills.ts b/examples/ai-functions/src/harness-agent/deepagents/with-skills.ts index 60c8af71c545..15320ace7ffe 100644 --- a/examples/ai-functions/src/harness-agent/deepagents/with-skills.ts +++ b/examples/ai-functions/src/harness-agent/deepagents/with-skills.ts @@ -4,13 +4,7 @@ import { createVercelSandbox } from '@ai-sdk/sandbox-vercel'; import { printFullStream } from '../../lib/print-full-stream'; import { run } from '../../lib/run'; -/* - * Skills are domain-specific reference material the agent loads on demand - * when it decides — based on the skill's name and description — that the - * current task is relevant. The DeepAgents harness writes the skills to a - * combined `.skills.md` in the working directory and prepends them to the - * system prompt. - */ +// Skills are loaded on demand by name/description; the harness writes them to `.skills.md`. run(async () => { const sandbox = createVercelSandbox({ runtime: 'node24', diff --git a/examples/harness-e2e-tui/agents/deepagents/basic-agent.ts b/examples/harness-e2e-tui/agents/deepagents/basic-agent.ts index f822ee2e07b4..996396d7b404 100644 --- a/examples/harness-e2e-tui/agents/deepagents/basic-agent.ts +++ b/examples/harness-e2e-tui/agents/deepagents/basic-agent.ts @@ -24,11 +24,7 @@ export const deepAgentsHarnessAgent = new HarnessAgent({ }, }); -/* - * Derived from `agent.tools` directly rather than `InferAgentUIMessage` — see the note in the OpenCode/Codex basic agents for why the - * structural inference is side-stepped via the `tools` field. - */ +// Derived from `agent.tools` (not InferAgentUIMessage) — see Codex/OpenCode basic agents. export type DeepAgentsHarnessAgentMessage = UIMessage< unknown, never, diff --git a/packages/harness-deepagents/src/bridge/index.ts b/packages/harness-deepagents/src/bridge/index.ts index 333769dc1ceb..0e05a46f3a44 100644 --- a/packages/harness-deepagents/src/bridge/index.ts +++ b/packages/harness-deepagents/src/bridge/index.ts @@ -1,18 +1,5 @@ -// Long-running process that runs alongside the DeepAgents (LangGraph JS) -// runtime inside the sandbox. The generic transport — WebSocket server, token -// auth, single-flight reconnect, the in-memory event log + `seq`, resume -// replay, and the lifecycle/meta files — lives in the shared -// `@ai-sdk/harness/bridge` runtime. This file supplies only the -// DeepAgents-specific turn driver: it builds an agent with `createDeepAgent()` -// and translates its `streamEvents` output into harness-v1 stream parts. -// -// CONSTRAINT — the third-party imports below are NEVER bundled into the -// compiled `bridge/index.mjs`. They are declared `external` in tsup.config.ts -// and resolved at runtime from the node_modules this bridge installs *inside -// the sandbox* from `src/bridge/package.json` (and its pinned -// `pnpm-lock.yaml`). When adding/changing a third-party import here you MUST -// keep all three in sync: the import below, the `external` array in -// tsup.config.ts, and the dependency in `src/bridge/package.json`. +// In-sandbox turn driver: builds a `createDeepAgent()` agent and maps its `streamEvents` to harness-v1 parts; transport is `@ai-sdk/harness/bridge`. +// Third-party imports below stay external (tsup) and resolve from src/bridge/package.json in-sandbox — keep import, externals, and deps in sync. import { randomUUID } from 'node:crypto'; import { readFile } from 'node:fs/promises'; @@ -55,8 +42,7 @@ function parseArgs(rawArgs: string[]): Record { return out; } -// LangChain resolves model strings as `provider:model`; the host sends -// `provider/model`. +// LangChain wants `provider:model`; the host sends `provider/model`. function parseModelName(raw: string): string { return raw.includes('/') ? raw.replace('/', ':') : raw; } @@ -70,9 +56,7 @@ if (!workdir || !bridgeStateDir) { process.exit(1); } -// One agent per bridge process, reused across turns so the LangGraph -// checkpointer accumulates conversation state. Host tools close over a getter -// for the live turn rather than a fixed one. +// One agent per bridge process, reused across turns; host tools read the live turn via `currentTurn`. let agent: ReturnType | undefined; let currentTurn: BridgeTurn | undefined; @@ -105,9 +89,7 @@ function schemaFromJson(input: unknown) { return z.object(shape); } -// Host-defined tools become LangChain tools that round-trip through the host: -// emit a `tool-call` (providerExecuted=false), then block on the host's -// `tool-result` before returning to LangGraph. +// 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( @@ -155,12 +137,12 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise { if (!agent) { const skillsBlock = await readSkillsBlock(); agent = createDeepAgent({ - model: parseModelName(start.model ?? 'claude-sonnet-4'), + // 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: buildSystemPrompt(start, skillsBlock) || undefined, - // A real checkpointer instance (not `true`, which LangGraph rejects for - // root graphs) gives multi-turn memory within this bridge process. + // Real instance (LangGraph rejects `true` for root graphs); gives multi-turn memory. checkpointer: new MemorySaver(), }); } @@ -247,8 +229,7 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise { outputTokens = Math.max(outputTokens, usage.output_tokens ?? 0); } } else if (kind === 'on_chat_model_end') { - // LangChain.js reports final token usage on the model-end event (not on - // the streamed chunks). Each model call is one step boundary. + // 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 }; @@ -272,8 +253,7 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise { 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 inside the tool fn; only the - // runtime's built-in tools are surfaced here (providerExecuted). + // Host tools emit their own tool-call; only surface builtin (providerExecuted) tools here. if (!hostToolNames.has(toolName)) { endTextBlock(); emit({ diff --git a/packages/harness-deepagents/src/deepagents-auth.ts b/packages/harness-deepagents/src/deepagents-auth.ts index 7a0813406034..b0a2a66fcf7d 100644 --- a/packages/harness-deepagents/src/deepagents-auth.ts +++ b/packages/harness-deepagents/src/deepagents-auth.ts @@ -14,20 +14,7 @@ export type DeepAgentsAuthOptions = { const DEFAULT_ANTHROPIC_BASE_URL = 'https://api.anthropic.com'; -/** - * Resolve the environment-variable blob the DeepAgents (LangChain) bridge needs. - * Precedence: - * - * 1. Explicit `auth.anthropic` — pin to direct Anthropic auth. - * 2. Explicit `auth.gateway` — pin to Vercel AI Gateway (routed via the - * Anthropic-compatible surface; LangChain reads `ANTHROPIC_*`). - * 3. Auto-detect from the host process env: gateway first - * (`AI_GATEWAY_API_KEY` / `VERCEL_OIDC_TOKEN`), then ambient `ANTHROPIC_*`. - * - * DeepAgents resolves models through LangChain, which reads `ANTHROPIC_API_KEY` - * / `ANTHROPIC_BASE_URL`. When routing through the gateway we point those at the - * gateway base URL and reuse the gateway key. - */ +// Resolve bridge env vars: explicit anthropic/gateway, else ambient gateway then anthropic. export function resolveDeepAgentsEnv( auth: DeepAgentsAuthOptions | undefined, processEnv: Record = process.env, diff --git a/packages/harness-deepagents/src/deepagents-bridge-protocol.ts b/packages/harness-deepagents/src/deepagents-bridge-protocol.ts index ec82b77c123d..a7a471881943 100644 --- a/packages/harness-deepagents/src/deepagents-bridge-protocol.ts +++ b/packages/harness-deepagents/src/deepagents-bridge-protocol.ts @@ -6,22 +6,12 @@ import { } from '@ai-sdk/harness'; import { z } from 'zod/v4'; -/* - * DeepAgents' bridge wire protocol. The outbound events, transport frames, - * shared inbound commands, and `bridge-ready` line all come from the shared - * `@ai-sdk/harness` protocol — the only DeepAgents-specific piece is the - * `start` payload. - */ - +// 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({ - /* - * Free-form session instructions. `create_deep_agent()` takes no - * `instructions` parameter, so the bridge prepends this to the first user - * message of a fresh session. The host sends it only on the first turn. - */ + // Prepended to the first user message (createDeepAgent takes no instructions param). instructions: z.string().optional(), }); diff --git a/packages/harness-deepagents/src/deepagents-harness.ts b/packages/harness-deepagents/src/deepagents-harness.ts index f86c419da033..416bad60aaea 100644 --- a/packages/harness-deepagents/src/deepagents-harness.ts +++ b/packages/harness-deepagents/src/deepagents-harness.ts @@ -36,41 +36,22 @@ import { type DeepAgentsChannel = SandboxChannel; -/* - * Bootstrap lives in /tmp because it's pure derived state — the harness can - * reinstall the bridge's Node deps and files on any fresh sandbox from the - * recipe. Persistence comes from the sandbox provider's snapshot, not the path. - */ +// Pure derived state in /tmp; reinstalled per sandbox, persistence is the provider snapshot. const BOOTSTRAP_DIR = '/tmp/harness/deepagents'; const DEEPAGENTS_DEFAULT_CONTEXT_WINDOW = 200_000; export type DeepAgentsHarnessSettings = { readonly auth?: DeepAgentsAuthOptions; - /** - * Model id the underlying DeepAgents (LangChain) runtime should use, e.g. - * `claude-sonnet-4`. The bridge converts this to LangChain colon format - * internally (`anthropic:claude-sonnet-4`). - */ + /** Model id for the DeepAgents runtime, e.g. `claude-sonnet-4` (converted to `provider:model`). */ readonly model?: string; - /** - * Override the port the bridge binds inside the sandbox. By default the - * adapter uses the first port the sandbox declares via `sandbox.ports`. - */ + /** 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; }; -/* - * Every native tool the DeepAgents (LangGraph) runtime exposes as a - * model-callable tool, keyed by the cross-harness common name the bridge emits - * as `toolName` on the wire. The native LangGraph names are recorded via - * `nativeName`. DeepAgents' `search` maps to the common `grep` capability — - * `/ai` has no `searchFiles` common name — and the bridge maps each tool's - * native argument names (`path`/`query`) onto the standard common fields - * (`file_path`/`pattern`). - */ +// Native LangGraph tools keyed by cross-harness common name; `search`→`grep` (no `searchFiles` common name). const DEEPAGENTS_BUILTIN_TOOLS = { read: commonTool('read', { nativeName: 'read_file', @@ -555,11 +536,7 @@ async function teardown( } } -/* - * Reduce a `HarnessV1Prompt` to the plain user text the bridge forwards to the - * DeepAgents runtime. File and image parts are not yet supported — throw rather - * than silently drop them. - */ +// 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; diff --git a/packages/harness-deepagents/src/index.ts b/packages/harness-deepagents/src/index.ts index d34cc7a361d2..5d0a1b3aba82 100644 --- a/packages/harness-deepagents/src/index.ts +++ b/packages/harness-deepagents/src/index.ts @@ -1,10 +1,6 @@ import { createDeepAgents } from './deepagents-harness'; -/** - * Default `deepagents` harness instance with no overrides — suitable for the - * common case where the runtime's defaults are fine. Equivalent to - * `createDeepAgents()`. - */ +/** Default `deepagents` harness instance; equivalent to `createDeepAgents()`. */ export const deepAgents = createDeepAgents(); export { createDeepAgents } from './deepagents-harness'; From efde65851e1dea1ee3ef409981e25c83f7470504 Mon Sep 17 00:00:00 2001 From: mlekhi Date: Thu, 18 Jun 2026 18:45:27 -0700 Subject: [PATCH 08/39] feat(harness-deepagents): add session lifecycle + e2e-next example --- .../agent/harness/deepagents/basic-agent.ts | 30 +++ .../app/api/harness/deepagents/basic/route.ts | 35 ++++ .../app/harness/deepagents/basic/page.tsx | 19 ++ examples/harness-e2e-next/app/page.tsx | 5 + .../components/deepagents-harness-chat.tsx | 125 ++++++++++++ examples/harness-e2e-next/package.json | 1 + .../src/deepagents-harness.test.ts | 13 +- .../src/deepagents-harness.ts | 185 +++++++++++++++--- pnpm-lock.yaml | 3 + 9 files changed, 373 insertions(+), 43 deletions(-) create mode 100644 examples/harness-e2e-next/agent/harness/deepagents/basic-agent.ts create mode 100644 examples/harness-e2e-next/app/api/harness/deepagents/basic/route.ts create mode 100644 examples/harness-e2e-next/app/harness/deepagents/basic/page.tsx create mode 100644 examples/harness-e2e-next/components/deepagents-harness-chat.tsx 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/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/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/page.tsx b/examples/harness-e2e-next/app/page.tsx index fd7324c9058f..c189aeb75677 100644 --- a/examples/harness-e2e-next/app/page.tsx +++ b/examples/harness-e2e-next/app/page.tsx @@ -31,6 +31,11 @@ const HARNESSES = [ 'weather-approval', ], }, + { + slug: 'deepagents', + label: 'DeepAgents', + variants: ['basic'], + }, { 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/package.json b/examples/harness-e2e-next/package.json index d18ffc262242..58d9078c4782 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/packages/harness-deepagents/src/deepagents-harness.test.ts b/packages/harness-deepagents/src/deepagents-harness.test.ts index 32e3bd28c10f..b2c31fe7d061 100644 --- a/packages/harness-deepagents/src/deepagents-harness.test.ts +++ b/packages/harness-deepagents/src/deepagents-harness.test.ts @@ -84,17 +84,8 @@ describe('createDeepAgents', () => { ).rejects.toBeInstanceOf(HarnessCapabilityUnsupportedError); }); - it('rejects resuming a session', async () => { + it('exposes a lifecycle state schema for resume payloads', () => { const harness = createDeepAgents(); - await expect( - harness.doStart({ - resumeFrom: { - type: 'resume-session', - harnessId: 'deepagents', - specificationVersion: 'harness-v1', - data: {}, - }, - } as unknown as HarnessV1StartOptions), - ).rejects.toBeInstanceOf(HarnessCapabilityUnsupportedError); + expect(harness.lifecycleStateSchema).toBeDefined(); }); }); diff --git a/packages/harness-deepagents/src/deepagents-harness.ts b/packages/harness-deepagents/src/deepagents-harness.ts index 416bad60aaea..c2d96f3f4e72 100644 --- a/packages/harness-deepagents/src/deepagents-harness.ts +++ b/packages/harness-deepagents/src/deepagents-harness.ts @@ -8,6 +8,7 @@ import { type HarnessV1, type HarnessV1Bootstrap, type HarnessV1BuiltinTool, + type HarnessV1ContinueTurnState, type HarnessV1NetworkSandboxSession, type HarnessV1Prompt, type HarnessV1PromptControl, @@ -41,6 +42,18 @@ const BOOTSTRAP_DIR = '/tmp/harness/deepagents'; const DEEPAGENTS_DEFAULT_CONTEXT_WINDOW = 200_000; +// Live bridge coordinates returned by doDetach/doSuspendTurn so a later process can reattach. +const bridgeCoordsSchema = z.object({ + port: z.number(), + token: z.string(), + lastSeenEventId: z.number(), + sandboxId: z.string().optional(), +}); +const deepAgentsResumeStateSchema = z.object({ + bridge: bridgeCoordsSchema.optional(), +}); +type DeepAgentsBridgeCoords = z.infer; + export type DeepAgentsHarnessSettings = { readonly auth?: DeepAgentsAuthOptions; /** Model id for the DeepAgents runtime, e.g. `claude-sonnet-4` (converted to `provider:model`). */ @@ -91,6 +104,7 @@ export function createDeepAgents( // DeepAgents supports approvals upstream, but the happy-path first cut ships // with `permissionMode: 'allow-all'` only; approvals land in a follow-up. supportsBuiltinToolApprovals: false, + lifecycleStateSchema: deepAgentsResumeStateSchema, getBootstrap: async () => { if (cachedBootstrap != null) return cachedBootstrap; const [bridge, pkg, lock] = await Promise.all([ @@ -127,20 +141,17 @@ export function createDeepAgents( }); } - // Happy-path first cut: cross-process resume / turn continuation is a - // follow-up. DeepAgents' conversation state is in-memory (LangGraph - // MemorySaver) and does not survive a bridge restart, so resuming a prior - // session is not yet sound. - if (startOpts.resumeFrom != null || startOpts.continueFrom != null) { - throw new HarnessCapabilityUnsupportedError({ - message: - "Harness 'deepagents' does not support resuming a session yet; start a fresh session.", - harnessId: 'deepagents', - }); - } - 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}`; @@ -158,6 +169,39 @@ export function createDeepAgents( ) : undefined; + // Attach: reopen a socket to the still-running bridge. A between-turn + // resume attaches plainly; a suspended in-flight turn (continueFrom) + // replays past the cursor. If the bridge is gone the open throws and we + // 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, + }); + } catch { + // Bridge no longer reachable — recover by respawning below. + } + } + const port = resolveBridgePort(sandboxSession, settings.port); const token = randomBytes(32).toString('hex'); @@ -179,7 +223,7 @@ export function createDeepAgents( }; await session.run({ - command: `mkdir -p ${workDir} ${bridgeStateDir}`, + command: `mkdir -p ${shellQuote(workDir)} ${shellQuote(bridgeStateDir)}`, abortSignal: startOpts.abortSignal, }); @@ -191,7 +235,7 @@ export function createDeepAgents( }); const proc = await session.spawn({ - command: `node ${BOOTSTRAP_DIR}/bridge.mjs --workdir ${workDir} --bridge-state-dir ${bridgeStateDir} --bootstrap-dir ${BOOTSTRAP_DIR}`, + command: `node ${BOOTSTRAP_DIR}/bridge.mjs --workdir ${shellQuote(workDir)} --bridge-state-dir ${shellQuote(bridgeStateDir)} --bootstrap-dir ${shellQuote(BOOTSTRAP_DIR)}`, env, abortSignal: startOpts.abortSignal, }); @@ -228,6 +272,10 @@ export function createDeepAgents( channel, proc, model: settings.model, + bridgePort: boundPort, + bridgeToken: token, + sandboxId, + isResume, }); }, }; @@ -286,6 +334,10 @@ async function writeSkills({ }); } +function shellQuote(value: string): string { + return `'${value.replace(/'/g, `'\\''`)}'`; +} + function openWebSocket(url: string): Promise { return new Promise((resolve, reject) => { const ws = new WebSocket(url); @@ -328,14 +380,24 @@ function createSession({ channel, proc, model, + bridgePort, + bridgeToken, + sandboxId, + isResume, }: { sessionId: string; channel: DeepAgentsChannel; - proc: Experimental_SandboxProcess; + // 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; }): HarnessV1Session { let stopped = false; - let instructionsApplied = false; + // A resumed session already applied its instructions in the original first message. + let instructionsApplied = isResume; const wireTurn = (turnOpts: { emit: (event: HarnessV1StreamPart) => void; @@ -451,7 +513,7 @@ function createSession({ return { sessionId, - isResume: false, + isResume, modelId: model, doPromptTurn: async promptOpts => { const control = wireTurn({ @@ -477,9 +539,66 @@ function createSession({ return control; }, - doContinueTurn: async () => unsupported('turn continuation'), - doSuspendTurn: async () => unsupported('suspending a turn'), - doDetach: async () => unsupported('detaching a session'), + doContinueTurn: async continueOpts => { + // Attach/replay: doStart reopened the channel with `{ resume: true }`, so + // the bridge replays everything past the cursor (incl. a `finish` if the + // turn ended during the gap). No `start` is sent — issuing one would clear + // the replay log and begin a new turn. + 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 + // so a future process reattaches via these coordinates. + 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) { @@ -489,9 +608,9 @@ function createSession({ } stopped = true; await teardown(channel, proc); - // DeepAgents holds conversation state in memory only, so there is no - // durable runtime state to export. The sandbox snapshot taken during the - // subsequent `sandboxSession.stop()` preserves the filesystem. + // Conversation state is in-memory; tearing the bridge down loses it. The + // sandbox snapshot preserves the filesystem, so the next session resumes + // the workspace but not the prior conversation. const payload: HarnessV1ResumeSessionState = { type: 'resume-session', harnessId: 'deepagents', @@ -510,7 +629,7 @@ function createSession({ async function teardown( channel: DeepAgentsChannel, - proc: Experimental_SandboxProcess, + proc: Experimental_SandboxProcess | undefined, ): Promise { channel.beginClose(); try { @@ -520,17 +639,19 @@ async function teardown( } catch {} let stopTimer: ReturnType | undefined; try { - await Promise.race([ - proc.wait(), - new Promise(resolve => { - stopTimer = setTimeout(resolve, 5000); - stopTimer.unref?.(); - }), - ]); + 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(); + await proc?.kill(); } catch {} channel.close(); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d305f54d91e9..c003dd04a5b6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -602,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 From f88416c551c9bdfef7a79064cf1b4897ee5b7bd0 Mon Sep 17 00:00:00 2001 From: mlekhi Date: Thu, 18 Jun 2026 18:52:09 -0700 Subject: [PATCH 09/39] feat(harness-deepagents): provider-aware auth (anthropic + openai) --- .../02-ai-sdk-harnesses/05-deepagents.mdx | 20 +-- packages/harness-deepagents/README.md | 12 +- .../src/deepagents-auth.test.ts | 96 ++++++++++++--- .../harness-deepagents/src/deepagents-auth.ts | 114 +++++++++++++++--- .../src/deepagents-harness.ts | 2 +- 5 files changed, 195 insertions(+), 49 deletions(-) diff --git a/content/providers/02-ai-sdk-harnesses/05-deepagents.mdx b/content/providers/02-ai-sdk-harnesses/05-deepagents.mdx index 216f550f09da..bc193258d163 100644 --- a/content/providers/02-ai-sdk-harnesses/05-deepagents.mdx +++ b/content/providers/02-ai-sdk-harnesses/05-deepagents.mdx @@ -111,7 +111,7 @@ const harness = createDeepAgents({ Settings: -- `auth`: Anthropic or AI Gateway authentication 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. @@ -119,9 +119,10 @@ Settings: ## Authentication -By default, authentication is resolved from the host environment and forwarded -to the sandbox bridge. The adapter checks for AI Gateway credentials first, then -ambient Anthropic credentials. +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: @@ -131,14 +132,19 @@ Supported environment variables: - `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: +You can also pass explicit auth settings (`anthropic`, `openai`, or `gateway`): ```ts const harness = createDeepAgents({ + model: 'openai/gpt-5', auth: { - anthropic: { - apiKey: process.env.ANTHROPIC_API_KEY, + openai: { + apiKey: process.env.OPENAI_API_KEY, }, }, }); diff --git a/packages/harness-deepagents/README.md b/packages/harness-deepagents/README.md index 43c02864f7da..71f293b4aa67 100644 --- a/packages/harness-deepagents/README.md +++ b/packages/harness-deepagents/README.md @@ -43,14 +43,16 @@ Configure the model and auth via `createDeepAgents({ model, auth })`. ## Auth -Auth is optional. With none configured the adapter falls back to the ambient -Vercel AI Gateway credentials (`AI_GATEWAY_API_KEY`, then `VERCEL_OIDC_TOKEN`), -then ambient `ANTHROPIC_*`. Pin explicitly with: +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: 'claude-sonnet-4', - auth: { anthropic: { apiKey: process.env.ANTHROPIC_TEAM_KEY } }, + model: 'openai/gpt-5', + auth: { openai: { apiKey: process.env.OPENAI_API_KEY } }, }); ``` diff --git a/packages/harness-deepagents/src/deepagents-auth.test.ts b/packages/harness-deepagents/src/deepagents-auth.test.ts index 4c306e547efc..b068d560a285 100644 --- a/packages/harness-deepagents/src/deepagents-auth.test.ts +++ b/packages/harness-deepagents/src/deepagents-auth.test.ts @@ -1,46 +1,108 @@ import { describe, expect, it } from 'vitest'; -import { resolveDeepAgentsEnv } from './deepagents-auth'; +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( - { anthropic: { apiKey: 'sk-ant', baseUrl: 'https://example.test' } }, - {}, - ); + 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 gateway auth and mirrors it onto ANTHROPIC_*', () => { - const env = resolveDeepAgentsEnv({ gateway: { apiKey: 'gw-key' } }, {}); + 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.AI_GATEWAY_BASE_URL).toBe('https://ai-gateway.vercel.sh'); 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 anthropic', () => { - const env = resolveDeepAgentsEnv(undefined, { - AI_GATEWAY_API_KEY: 'ambient-gw', - ANTHROPIC_API_KEY: 'ambient-ant', + 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 gateway key', () => { - const env = resolveDeepAgentsEnv(undefined, { - VERCEL_OIDC_TOKEN: 'oidc-token', + 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(undefined, { - ANTHROPIC_API_KEY: 'ambient-ant', + 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 index b0a2a66fcf7d..000ace2558ff 100644 --- a/packages/harness-deepagents/src/deepagents-auth.ts +++ b/packages/harness-deepagents/src/deepagents-auth.ts @@ -6,32 +6,75 @@ export type DeepAgentsAuthOptions = { 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; }; }; -const DEFAULT_ANTHROPIC_BASE_URL = 'https://api.anthropic.com'; +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 bridge env vars: explicit anthropic/gateway, else ambient gateway then anthropic. -export function resolveDeepAgentsEnv( - auth: DeepAgentsAuthOptions | undefined, - processEnv: Record = process.env, -): Record { - if (auth?.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({ explicit: auth.gateway, gatewayAuthFromEnv }); + return pickGateway({ + provider, + explicit: auth.gateway, + gatewayAuthFromEnv, + }); } if (gatewayAuthFromEnv.apiKey) { - return pickGateway({ explicit: {}, gatewayAuthFromEnv }); + return pickGateway({ provider, explicit: {}, gatewayAuthFromEnv }); } - return pickAnthropic({ processEnv }); + + return provider === 'openai' + ? pickOpenAI({ processEnv }) + : pickAnthropic({ processEnv }); } function pickAnthropic({ @@ -51,23 +94,56 @@ function pickAnthropic({ 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 = explicit.baseUrl ?? gatewayAuthFromEnv.baseUrl; + const baseUrl = gatewayBaseUrl( + explicit.baseUrl ?? gatewayAuthFromEnv.baseUrl, + provider, + ); const env: Record = {}; - if (apiKey) { - env.AI_GATEWAY_API_KEY = apiKey; - env.ANTHROPIC_API_KEY = apiKey; + 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; } - env.AI_GATEWAY_BASE_URL = baseUrl; - env.ANTHROPIC_BASE_URL = baseUrl; return env; } - -export { DEFAULT_ANTHROPIC_BASE_URL }; diff --git a/packages/harness-deepagents/src/deepagents-harness.ts b/packages/harness-deepagents/src/deepagents-harness.ts index c2d96f3f4e72..49294bbfd5e9 100644 --- a/packages/harness-deepagents/src/deepagents-harness.ts +++ b/packages/harness-deepagents/src/deepagents-harness.ts @@ -217,7 +217,7 @@ export function createDeepAgents( } const env = { - ...resolveDeepAgentsEnv(settings.auth), + ...resolveDeepAgentsEnv({ auth: settings.auth, model: settings.model }), BRIDGE_CHANNEL_TOKEN: token, BRIDGE_WS_PORT: String(port), }; From 46d50d25c67b83a5a6ed6f66d9fe226a6ac9a145 Mon Sep 17 00:00:00 2001 From: mlekhi Date: Thu, 18 Jun 2026 19:08:06 -0700 Subject: [PATCH 10/39] =?UTF-8?q?supporting=20recursive=20JSON-schema?= =?UTF-8?q?=E2=86=92zod=20for=20custom=20tools?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../harness-deepagents/src/bridge/index.ts | 33 +------- .../src/bridge/json-schema-to-zod.test.ts | 81 +++++++++++++++++++ .../src/bridge/json-schema-to-zod.ts | 64 +++++++++++++++ 3 files changed, 147 insertions(+), 31 deletions(-) create mode 100644 packages/harness-deepagents/src/bridge/json-schema-to-zod.test.ts create mode 100644 packages/harness-deepagents/src/bridge/json-schema-to-zod.ts diff --git a/packages/harness-deepagents/src/bridge/index.ts b/packages/harness-deepagents/src/bridge/index.ts index 0e05a46f3a44..9bedc4666102 100644 --- a/packages/harness-deepagents/src/bridge/index.ts +++ b/packages/harness-deepagents/src/bridge/index.ts @@ -12,8 +12,8 @@ import { import { tool } from '@langchain/core/tools'; import { MemorySaver } from '@langchain/langgraph'; import { createDeepAgent, LocalShellBackend } from 'deepagents'; -import { z } from 'zod/v4'; import type { StartMessage } from '../deepagents-bridge-protocol'; +import { jsonSchemaToZodObject } from './json-schema-to-zod'; // Native LangGraph tool name -> harness-v1 common name. const NATIVE_TO_COMMON: Readonly> = { @@ -60,35 +60,6 @@ if (!workdir || !bridgeStateDir) { let agent: ReturnType | undefined; let currentTurn: BridgeTurn | undefined; -const jsonTypeToZod: Record z.ZodTypeAny> = { - string: () => z.string(), - integer: () => z.number(), - number: () => z.number(), - boolean: () => z.boolean(), - object: () => z.record(z.string(), z.unknown()), - array: () => z.array(z.unknown()), -}; - -function schemaFromJson(input: unknown) { - const obj = - input && typeof input === 'object' - ? (input as { - properties?: Record; - required?: string[]; - }) - : {}; - const properties = obj.properties ?? {}; - const required = new Set(obj.required ?? []); - const shape: Record = {}; - for (const [name, def] of Object.entries(properties)) { - const base = ( - jsonTypeToZod[def?.type ?? 'string'] ?? jsonTypeToZod.string - )(); - shape[name] = required.has(name) ? base : base.optional(); - } - return z.object(shape); -} - // 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 => @@ -110,7 +81,7 @@ function buildHostTools(toolSchemas: StartMessage['tools']) { { name: schema.name, description: schema.description ?? '', - schema: schemaFromJson(schema.inputSchema), + schema: jsonSchemaToZodObject(schema.inputSchema), }, ), ); 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; +} From cad12fae13c7e180fd56a2dae26ef27553bc20aa Mon Sep 17 00:00:00 2001 From: mlekhi Date: Mon, 22 Jun 2026 12:20:11 -0700 Subject: [PATCH 11/39] bumping ws version --- packages/harness-deepagents/package.json | 2 +- .../src/bridge/package.json | 2 +- .../src/bridge/pnpm-lock.yaml | 82 +++++++++---------- pnpm-lock.yaml | 2 +- 4 files changed, 44 insertions(+), 44 deletions(-) diff --git a/packages/harness-deepagents/package.json b/packages/harness-deepagents/package.json index b6d85cafc55b..2bc19f1d1184 100644 --- a/packages/harness-deepagents/package.json +++ b/packages/harness-deepagents/package.json @@ -38,7 +38,7 @@ "dependencies": { "@ai-sdk/harness": "workspace:*", "@ai-sdk/provider-utils": "workspace:*", - "ws": "^8.20.1", + "ws": "8.21.0", "zod": "3.25.76" }, "devDependencies": { diff --git a/packages/harness-deepagents/src/bridge/package.json b/packages/harness-deepagents/src/bridge/package.json index a4a5bab52908..464723d373e5 100644 --- a/packages/harness-deepagents/src/bridge/package.json +++ b/packages/harness-deepagents/src/bridge/package.json @@ -9,7 +9,7 @@ "@langchain/langgraph": "^1.3.0", "@langchain/openai": "^1.0.0", "deepagents": "1.10.2", - "ws": "8.20.1", + "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 index 071d26155e9c..9777468ff24f 100644 --- a/packages/harness-deepagents/src/bridge/pnpm-lock.yaml +++ b/packages/harness-deepagents/src/bridge/pnpm-lock.yaml @@ -10,22 +10,22 @@ importers: dependencies: '@langchain/anthropic': specifier: ^1.0.0 - version: 1.4.1(@langchain/core@1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1)) + 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.20.1)(zod@4.4.3))(ws@8.20.1) + 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.20.1)(zod@4.4.3))(ws@8.20.1))(zod@4.4.3) + 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.20.1)(zod@4.4.3))(ws@8.20.1))(ws@8.20.1) + 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.20.1)(zod@4.4.3))(ws@8.20.1))(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1) + 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.20.1 - version: 8.20.1 + specifier: 8.21.0 + version: 8.21.0 zod: specifier: ^4.3.6 version: 4.4.3 @@ -279,8 +279,8 @@ packages: ts-algebra@2.0.0: resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} - ws@8.20.1: - resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -312,18 +312,18 @@ snapshots: '@cfworker/json-schema@4.1.1': {} - '@langchain/anthropic@1.4.1(@langchain/core@1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.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.20.1)(zod@4.4.3))(ws@8.20.1) + '@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.20.1)(zod@4.4.3))(ws@8.20.1)': + '@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.20.1)(zod@4.4.3))(ws@8.20.1) + 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 @@ -334,23 +334,23 @@ snapshots: - openai - ws - '@langchain/langgraph-checkpoint@1.1.1(@langchain/core@1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1))': + '@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.20.1)(zod@4.4.3))(ws@8.20.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.20.1)(zod@4.4.3))(ws@8.20.1))': + '@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.20.1)(zod@4.4.3))(ws@8.20.1) + '@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.20.1)(zod@4.4.3))(ws@8.20.1))(zod@4.4.3)': + '@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.20.1)(zod@4.4.3))(ws@8.20.1) - '@langchain/langgraph-checkpoint': 1.1.1(@langchain/core@1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1)) - '@langchain/langgraph-sdk': 1.9.22(@langchain/core@1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1)) + '@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 @@ -360,11 +360,11 @@ snapshots: - svelte - vue - '@langchain/openai@1.4.7(@langchain/core@1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1))(ws@8.20.1)': + '@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.20.1)(zod@4.4.3))(ws@8.20.1) + '@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.20.1)(zod@4.4.3) + openai: 6.43.0(ws@8.21.0)(zod@4.4.3) zod: 4.4.3 transitivePeerDependencies: - ws @@ -395,14 +395,14 @@ snapshots: dependencies: fill-range: 7.1.1 - deepagents@1.10.2(langsmith@0.7.10(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1))(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.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.20.1)(zod@4.4.3))(ws@8.20.1) - '@langchain/langgraph': 1.4.2(@langchain/core@1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1))(zod@4.4.3) - '@langchain/langgraph-sdk': 1.9.22(@langchain/core@1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1)) + '@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.20.1)(zod@4.4.3))(ws@8.20.1))(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1) - langsmith: 0.7.10(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1) + 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 @@ -463,12 +463,12 @@ snapshots: '@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.20.1)(zod@4.4.3))(ws@8.20.1))(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1): + 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.20.1)(zod@4.4.3))(ws@8.20.1) - '@langchain/langgraph': 1.4.2(@langchain/core@1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1))(zod@4.4.3) - '@langchain/langgraph-checkpoint': 1.1.1(@langchain/core@1.1.49(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1)) - langsmith: 0.7.10(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1) + '@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' @@ -482,12 +482,12 @@ snapshots: - ws - zod-to-json-schema - langsmith@0.7.10(openai@6.43.0(ws@8.20.1)(zod@4.4.3))(ws@8.20.1): + 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.20.1)(zod@4.4.3) - ws: 8.20.1 + openai: 6.43.0(ws@8.21.0)(zod@4.4.3) + ws: 8.21.0 merge2@1.4.1: {} @@ -498,9 +498,9 @@ snapshots: mustache@4.2.0: {} - openai@6.43.0(ws@8.20.1)(zod@4.4.3): + openai@6.43.0(ws@8.21.0)(zod@4.4.3): optionalDependencies: - ws: 8.20.1 + ws: 8.21.0 zod: 4.4.3 p-finally@1.0.0: {} @@ -546,7 +546,7 @@ snapshots: ts-algebra@2.0.0: {} - ws@8.20.1: {} + ws@8.21.0: {} yaml@2.9.0: {} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 82f85b5a61ed..adaa07f18b8e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2615,7 +2615,7 @@ importers: specifier: workspace:* version: link:../provider-utils ws: - specifier: ^8.20.1 + specifier: 8.21.0 version: 8.21.0 zod: specifier: 3.25.76 From b3121f4a3821e2bdd00e8a7d1f4424394b841d3e Mon Sep 17 00:00:00 2001 From: mlekhi Date: Mon, 22 Jun 2026 14:38:32 -0700 Subject: [PATCH 12/39] adding full tool list --- .../02-ai-sdk-harnesses/05-deepagents.mdx | 6 +- .../harness-deepagents/src/bridge/index.ts | 45 +++--- .../src/deepagents-bridge-protocol.ts | 2 + .../src/deepagents-harness.test.ts | 16 +- .../src/deepagents-harness.ts | 138 +++++++++++++----- 5 files changed, 144 insertions(+), 63 deletions(-) diff --git a/content/providers/02-ai-sdk-harnesses/05-deepagents.mdx b/content/providers/02-ai-sdk-harnesses/05-deepagents.mdx index bc193258d163..bce9b58a58fa 100644 --- a/content/providers/02-ai-sdk-harnesses/05-deepagents.mdx +++ b/content/providers/02-ai-sdk-harnesses/05-deepagents.mdx @@ -164,8 +164,10 @@ const sandbox = createVercelSandbox({ ## Skills -Skills passed to the session are written to a combined `.skills.md` file in the -working directory and prepended to the agent's prompt. +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 diff --git a/packages/harness-deepagents/src/bridge/index.ts b/packages/harness-deepagents/src/bridge/index.ts index 9bedc4666102..e35720299840 100644 --- a/packages/harness-deepagents/src/bridge/index.ts +++ b/packages/harness-deepagents/src/bridge/index.ts @@ -1,8 +1,6 @@ -// In-sandbox turn driver: builds a `createDeepAgent()` agent and maps its `streamEvents` to harness-v1 parts; transport is `@ai-sdk/harness/bridge`. -// Third-party imports below stay external (tsup) and resolve from src/bridge/package.json in-sandbox — keep import, externals, and deps in sync. +// 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 { readFile } from 'node:fs/promises'; import { argv } from 'node:process'; import { runBridge, @@ -15,12 +13,12 @@ import { createDeepAgent, LocalShellBackend } from 'deepagents'; import type { StartMessage } from '../deepagents-bridge-protocol'; import { jsonSchemaToZodObject } from './json-schema-to-zod'; -// Native LangGraph tool name -> harness-v1 common name. +// 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', - shell: 'bash', - search: 'grep', + edit_file: 'edit', + execute: 'bash', }; function toCommonName(nativeName: string): string { @@ -47,6 +45,21 @@ 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; @@ -87,32 +100,20 @@ function buildHostTools(toolSchemas: StartMessage['tools']) { ); } -async function readSkillsBlock(): Promise { - try { - const content = await readFile(`${workdir}/.skills.md`, 'utf8'); - return content.trim() ? `## Available Skills\n\n${content}` : ''; - } catch { - return ''; - } -} - -function buildSystemPrompt(start: StartMessage, skillsBlock: string): string { - return [start.instructions ?? '', skillsBlock].filter(Boolean).join('\n\n'); -} - async function runTurn(start: StartMessage, turn: BridgeTurn): Promise { currentTurn = turn; const emit = (event: Record) => turn.emit(event as BridgeEvent); if (!agent) { - const skillsBlock = await readSkillsBlock(); 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: buildSystemPrompt(start, skillsBlock) || undefined, + systemPrompt: start.instructions || undefined, + // Native skills loaded from the host-materialized source dir (on-demand, with working file refs). + ...(start.skillsPath ? { skills: [start.skillsPath] } : {}), // Real instance (LangGraph rejects `true` for root graphs); gives multi-turn memory. checkpointer: new MemorySaver(), }); @@ -231,7 +232,7 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise { type: 'tool-call', toolCallId: runId, toolName: toCommonName(toolName), - input: JSON.stringify(data.input ?? {}), + input: toToolCallInput(data.input), providerExecuted: true, nativeName: toolName, }); diff --git a/packages/harness-deepagents/src/deepagents-bridge-protocol.ts b/packages/harness-deepagents/src/deepagents-bridge-protocol.ts index a7a471881943..f500b2f3aabb 100644 --- a/packages/harness-deepagents/src/deepagents-bridge-protocol.ts +++ b/packages/harness-deepagents/src/deepagents-bridge-protocol.ts @@ -13,6 +13,8 @@ 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; diff --git a/packages/harness-deepagents/src/deepagents-harness.test.ts b/packages/harness-deepagents/src/deepagents-harness.test.ts index b2c31fe7d061..94a9ee571776 100644 --- a/packages/harness-deepagents/src/deepagents-harness.test.ts +++ b/packages/harness-deepagents/src/deepagents-harness.test.ts @@ -34,17 +34,27 @@ describe('createDeepAgents', () => { expect(harness.supportsBuiltinToolApprovals).toBe(false); }); - it('exposes the native LangGraph tool names via builtin tools', () => { + 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.bash.nativeName).toBe('shell'); - expect(DEEPAGENTS_BUILTIN_TOOLS.grep.nativeName).toBe('search'); + 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', () => { diff --git a/packages/harness-deepagents/src/deepagents-harness.ts b/packages/harness-deepagents/src/deepagents-harness.ts index 49294bbfd5e9..c4c15a7e4a89 100644 --- a/packages/harness-deepagents/src/deepagents-harness.ts +++ b/packages/harness-deepagents/src/deepagents-harness.ts @@ -22,7 +22,7 @@ import { SandboxChannel, waitForBridgeReady, } from '@ai-sdk/harness/utils'; -import type { Experimental_SandboxProcess } from '@ai-sdk/provider-utils'; +import { tool, type Experimental_SandboxProcess } from '@ai-sdk/provider-utils'; import { WebSocket } from 'ws'; import { z } from 'zod'; import { @@ -40,6 +40,9 @@ 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; // Live bridge coordinates returned by doDetach/doSuspendTurn so a later process can reattach. @@ -64,7 +67,7 @@ export type DeepAgentsHarnessSettings = { readonly startupTimeoutMs?: number; }; -// Native LangGraph tools keyed by cross-harness common name; `search`→`grep` (no `searchFiles` common name). +// 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', @@ -75,21 +78,53 @@ const DEEPAGENTS_BUILTIN_TOOLS = { write: commonTool('write', { nativeName: 'write_file', toolUseKind: 'edit', - description: 'Write a file', + 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: 'shell', + nativeName: 'execute', toolUseKind: 'bash', - description: 'Execute a shell command', + description: 'Run a shell command', inputSchema: z.object({ command: z.string() }), }), grep: commonTool('grep', { - nativeName: 'search', + nativeName: 'grep', + toolUseKind: 'readonly', + description: 'Search file contents', + inputSchema: z.object({ pattern: z.string() }), + }), + glob: commonTool('glob', { + nativeName: 'glob', toolUseKind: 'readonly', - description: 'Search file contents with regex', + 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( @@ -101,8 +136,7 @@ export function createDeepAgents( specificationVersion: 'harness-v1', harnessId: 'deepagents', builtinTools: DEEPAGENTS_BUILTIN_TOOLS, - // DeepAgents supports approvals upstream, but the happy-path first cut ships - // with `permissionMode: 'allow-all'` only; approvals land in a follow-up. + // Happy-path ships `permissionMode: 'allow-all'` only; approvals are a follow-up. supportsBuiltinToolApprovals: false, lifecycleStateSchema: deepAgentsResumeStateSchema, getBootstrap: async () => { @@ -169,10 +203,7 @@ export function createDeepAgents( ) : undefined; - // Attach: reopen a socket to the still-running bridge. A between-turn - // resume attaches plainly; a suspended in-flight turn (continueFrom) - // replays past the cursor. If the bridge is gone the open throws and we - // fall through to a fresh spawn. + // Attach to the still-running bridge (continueFrom replays past the cursor); on failure fall through to a fresh spawn. if (coords) { try { const attachUrl = @@ -205,16 +236,20 @@ export function createDeepAgents( const port = resolveBridgePort(sandboxSession, settings.port); const token = randomBytes(32).toString('hex'); - // DeepAgents reads skills from a single combined `.skills.md` file in the - // working directory. - if (startOpts.skills && startOpts.skills.length > 0) { + // 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, + 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 }), @@ -276,6 +311,7 @@ export function createDeepAgents( bridgeToken: token, sandboxId, isResume, + skillsPath, }); }, }; @@ -313,6 +349,7 @@ async function readBridgeAsset(name: string): Promise { 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, @@ -324,14 +361,47 @@ async function writeSkills({ skills: ReadonlyArray; abortSignal?: AbortSignal; }): Promise { - const combined = skills - .map(skill => `## ${skill.name}\n${skill.description}\n\n${skill.content}`) - .join('\n\n---\n\n'); - await sandbox.writeTextFile({ - path: `${workDir}/.skills.md`, - content: combined, - abortSignal, - }); + 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 { @@ -384,6 +454,7 @@ function createSession({ bridgeToken, sandboxId, isResume, + skillsPath, }: { sessionId: string; channel: DeepAgentsChannel; @@ -394,6 +465,7 @@ function createSession({ bridgeToken: string; sandboxId: string; isResume: boolean; + skillsPath?: string; }): HarnessV1Session { let stopped = false; // A resumed session already applied its instructions in the original first message. @@ -535,15 +607,13 @@ function createSession({ inputSchema: t.inputSchema, })), ...(model ? { model } : {}), + ...(skillsPath ? { skillsPath } : {}), }); return control; }, doContinueTurn: async continueOpts => { - // Attach/replay: doStart reopened the channel with `{ resume: true }`, so - // the bridge replays everything past the cursor (incl. a `finish` if the - // turn ended during the gap). No `start` is sent — issuing one would clear - // the replay log and begin a new turn. + // 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, @@ -556,8 +626,7 @@ function createSession({ ); } stopped = true; - // Freeze the active turn at the cursor, leaving the bridge running so the - // next slice replays the tail. + // 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', @@ -581,8 +650,7 @@ function createSession({ ); } stopped = true; - // Park between turns: close the host socket but leave the bridge running - // so a future process reattaches via these coordinates. + // 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', @@ -608,9 +676,7 @@ function createSession({ } stopped = true; await teardown(channel, proc); - // Conversation state is in-memory; tearing the bridge down loses it. The - // sandbox snapshot preserves the filesystem, so the next session resumes - // the workspace but not the prior conversation. + // 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', From aaab5c52a1e5cf1dae48bc9f2c86780f59eef3f6 Mon Sep 17 00:00:00 2001 From: mlekhi Date: Mon, 22 Jun 2026 15:02:13 -0700 Subject: [PATCH 13/39] adding full test coverage for e2e-tui --- .../agents/deepagents/ai-sdk-coding-agent.ts | 52 ++++++++++++++++++ .../agents/deepagents/weather-agent.ts | 47 ++++++++++++++++ .../deepagents/weather-approval-agent.ts | 53 +++++++++++++++++++ .../harness/deepagents/ai-sdk-coding.ts | 8 +++ .../harness/deepagents/weather-approval.ts | 8 +++ .../harness/deepagents/weather.ts | 8 +++ 6 files changed, 176 insertions(+) create mode 100644 examples/harness-e2e-tui/agents/deepagents/ai-sdk-coding-agent.ts create mode 100644 examples/harness-e2e-tui/agents/deepagents/weather-agent.ts create mode 100644 examples/harness-e2e-tui/agents/deepagents/weather-approval-agent.ts create mode 100644 examples/harness-e2e-tui/harness/deepagents/ai-sdk-coding.ts create mode 100644 examples/harness-e2e-tui/harness/deepagents/weather-approval.ts create mode 100644 examples/harness-e2e-tui/harness/deepagents/weather.ts 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/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/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', +}); From 7d63a1f7a4734dcf648d11fde0a7fc04dd5fa050 Mon Sep 17 00:00:00 2001 From: mlekhi Date: Mon, 22 Jun 2026 15:09:26 -0700 Subject: [PATCH 14/39] adding examples for manual test coverage --- .../src/harness-agent/deepagents/attach.ts | 55 +++++++++++++++ .../harness-agent/deepagents/bash-shell.ts | 30 ++++++++ .../deepagents/custom-tool-approval.ts | 69 +++++++++++++++++++ .../src/harness-agent/deepagents/file-edit.ts | 45 ++++++++++++ .../deepagents/typed-builtin-tools.ts | 46 +++++++++++++ 5 files changed, 245 insertions(+) create mode 100644 examples/ai-functions/src/harness-agent/deepagents/attach.ts create mode 100644 examples/ai-functions/src/harness-agent/deepagents/bash-shell.ts create mode 100644 examples/ai-functions/src/harness-agent/deepagents/custom-tool-approval.ts create mode 100644 examples/ai-functions/src/harness-agent/deepagents/file-edit.ts create mode 100644 examples/ai-functions/src/harness-agent/deepagents/typed-builtin-tools.ts 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/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/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); + } +}); From 664ce01eced276cda2fb6ccfbe8d9cc607d55218 Mon Sep 17 00:00:00 2001 From: mlekhi Date: Mon, 22 Jun 2026 15:42:05 -0700 Subject: [PATCH 15/39] adding test coverage for e2e-next --- .../harness/deepagents/ai-sdk-coding-agent.ts | 52 +++++++ .../agent/harness/deepagents/weather-agent.ts | 47 +++++++ .../deepagents/weather-approval-agent.ts | 53 ++++++++ .../harness/deepagents/ai-sdk-coding/route.ts | 41 ++++++ .../deepagents/basic-with-stop/route.ts | 35 +++++ .../deepagents/weather-approval/route.ts | 42 ++++++ .../api/harness/deepagents/weather/route.ts | 41 ++++++ .../harness/deepagents/ai-sdk-coding/page.tsx | 19 +++ .../deepagents/basic-with-stop/page.tsx | 19 +++ .../deepagents/weather-approval/page.tsx | 19 +++ .../app/harness/deepagents/weather/page.tsx | 19 +++ examples/harness-e2e-next/app/page.tsx | 8 +- .../weather-deepagents-harness-chat.tsx | 128 ++++++++++++++++++ 13 files changed, 522 insertions(+), 1 deletion(-) create mode 100644 examples/harness-e2e-next/agent/harness/deepagents/ai-sdk-coding-agent.ts create mode 100644 examples/harness-e2e-next/agent/harness/deepagents/weather-agent.ts create mode 100644 examples/harness-e2e-next/agent/harness/deepagents/weather-approval-agent.ts create mode 100644 examples/harness-e2e-next/app/api/harness/deepagents/ai-sdk-coding/route.ts create mode 100644 examples/harness-e2e-next/app/api/harness/deepagents/basic-with-stop/route.ts create mode 100644 examples/harness-e2e-next/app/api/harness/deepagents/weather-approval/route.ts create mode 100644 examples/harness-e2e-next/app/api/harness/deepagents/weather/route.ts create mode 100644 examples/harness-e2e-next/app/harness/deepagents/ai-sdk-coding/page.tsx create mode 100644 examples/harness-e2e-next/app/harness/deepagents/basic-with-stop/page.tsx create mode 100644 examples/harness-e2e-next/app/harness/deepagents/weather-approval/page.tsx create mode 100644 examples/harness-e2e-next/app/harness/deepagents/weather/page.tsx create mode 100644 examples/harness-e2e-next/components/weather-deepagents-harness-chat.tsx 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/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/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/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 c189aeb75677..7c66b7a9f439 100644 --- a/examples/harness-e2e-next/app/page.tsx +++ b/examples/harness-e2e-next/app/page.tsx @@ -34,7 +34,13 @@ const HARNESSES = [ { slug: 'deepagents', label: 'DeepAgents', - variants: ['basic'], + variants: [ + 'basic', + 'basic-with-stop', + 'ai-sdk-coding', + 'weather', + 'weather-approval', + ], }, { slug: 'pi', 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 })} + /> +
+ ); +} From 82d6d4b4fa58d3feb060726506b915ef3f4de371 Mon Sep 17 00:00:00 2001 From: mlekhi Date: Mon, 22 Jun 2026 16:08:49 -0700 Subject: [PATCH 16/39] adding attach flag --- packages/harness-deepagents/src/deepagents-harness.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/harness-deepagents/src/deepagents-harness.ts b/packages/harness-deepagents/src/deepagents-harness.ts index c4c15a7e4a89..54cb1dcb91b7 100644 --- a/packages/harness-deepagents/src/deepagents-harness.ts +++ b/packages/harness-deepagents/src/deepagents-harness.ts @@ -227,6 +227,7 @@ export function createDeepAgents( bridgeToken: coords.token, sandboxId, isResume: true, + attached: true, }); } catch { // Bridge no longer reachable — recover by respawning below. @@ -311,6 +312,8 @@ export function createDeepAgents( bridgeToken: token, sandboxId, isResume, + // Freshly spawned bridge — it must receive the instructions on the first prompt. + attached: false, skillsPath, }); }, @@ -454,6 +457,7 @@ function createSession({ bridgeToken, sandboxId, isResume, + attached, skillsPath, }: { sessionId: string; @@ -465,11 +469,14 @@ function createSession({ 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; }): HarnessV1Session { let stopped = false; - // A resumed session already applied its instructions in the original first message. - let instructionsApplied = isResume; + let instructionsApplied = attached; const wireTurn = (turnOpts: { emit: (event: HarnessV1StreamPart) => void; From 6f8d41ae523bd234c746db983cac93fc60b076db Mon Sep 17 00:00:00 2001 From: mlekhi Date: Mon, 22 Jun 2026 16:15:07 -0700 Subject: [PATCH 17/39] adding endReasoningBlock() --- packages/harness-deepagents/src/bridge/index.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/harness-deepagents/src/bridge/index.ts b/packages/harness-deepagents/src/bridge/index.ts index e35720299840..2643c0db7d71 100644 --- a/packages/harness-deepagents/src/bridge/index.ts +++ b/packages/harness-deepagents/src/bridge/index.ts @@ -144,9 +144,19 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise { textBlockId = undefined; } }; - const emitText = (delta: string) => + 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 }); @@ -213,6 +223,7 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise { outputTokens += usage.output_tokens ?? 0; } endTextBlock(); + endReasoningBlock(); turn.emit({ type: 'finish-step', finishReason: { unified: 'stop' }, @@ -228,6 +239,7 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise { // Host tools emit their own tool-call; only surface builtin (providerExecuted) tools here. if (!hostToolNames.has(toolName)) { endTextBlock(); + endReasoningBlock(); emit({ type: 'tool-call', toolCallId: runId, @@ -257,7 +269,7 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise { } endTextBlock(); - if (reasoningBlockId) emit({ type: 'reasoning-end', id: reasoningBlockId }); + endReasoningBlock(); emit({ type: 'finish', finishReason: { unified: 'stop' }, From eab34b364a7b2ee56057652fa2f3d50a4c9c93c7 Mon Sep 17 00:00:00 2001 From: mlekhi Date: Mon, 22 Jun 2026 17:01:38 -0700 Subject: [PATCH 18/39] adding built in tool approvals --- .../deepagents/builtin-tool-approval.ts | 52 ++++ .../src/bridge/approvals.test.ts | 75 +++++ .../src/bridge/approvals.ts | 61 ++++ .../harness-deepagents/src/bridge/index.ts | 263 ++++++++++++------ .../src/deepagents-harness.test.ts | 15 +- .../src/deepagents-harness.ts | 30 +- 6 files changed, 381 insertions(+), 115 deletions(-) create mode 100644 examples/ai-functions/src/harness-agent/deepagents/builtin-tool-approval.ts create mode 100644 packages/harness-deepagents/src/bridge/approvals.test.ts create mode 100644 packages/harness-deepagents/src/bridge/approvals.ts 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/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 index 2643c0db7d71..f271d8597f99 100644 --- a/packages/harness-deepagents/src/bridge/index.ts +++ b/packages/harness-deepagents/src/bridge/index.ts @@ -8,9 +8,10 @@ import { type BridgeTurn, } from '@ai-sdk/harness/bridge'; import { tool } from '@langchain/core/tools'; -import { MemorySaver } from '@langchain/langgraph'; +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). @@ -105,6 +106,7 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise { 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. @@ -114,6 +116,8 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise { 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(), }); @@ -130,6 +134,9 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise { let inputTokens = 0; let outputTokens = 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) { @@ -164,108 +171,188 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise { emit({ type: 'reasoning-delta', id: reasoningBlockId, delta }); }; - const stream = await agent.streamEvents( - { messages: [{ role: 'user', content: start.prompt }] }, - { - version: 'v2', - configurable: { thread_id: 'bridge-session' }, - recursionLimit: 50, - signal: turn.abortSignal, - }, - ); + 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 }], + }; - for await (const event of stream) { - const kind = event.event; - const data = (event.data ?? {}) as Record; + while (true) { + const stream = await agent.streamEvents(resumeInput as never, config); - 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 }; + 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); + } } - | 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) { + inputTokens = Math.max(inputTokens, usage.input_tokens ?? 0); + outputTokens = Math.max(outputTokens, 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; + if (usage) { + inputTokens += usage.input_tokens ?? 0; + outputTokens += usage.output_tokens ?? 0; + } + endTextBlock(); + endReasoningBlock(); + turn.emit({ + type: 'finish-step', + finishReason: { unified: 'stop' }, + usage: { + inputTokens: { total: usage?.input_tokens ?? 0 }, + outputTokens: { total: usage?.output_tokens ?? 0 }, + }, + }); + } 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, + }); } } - } - const usage = chunk.usage_metadata; - if (usage) { - inputTokens = Math.max(inputTokens, usage.input_tokens ?? 0); - outputTokens = Math.max(outputTokens, 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 }; + } 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; } - | undefined; - const usage = output?.usage_metadata; - if (usage) { - inputTokens += usage.input_tokens ?? 0; - outputTokens += usage.output_tokens ?? 0; + 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(); - turn.emit({ - type: 'finish-step', - finishReason: { unified: 'stop' }, - usage: { - inputTokens: { total: usage?.input_tokens ?? 0 }, - outputTokens: { total: usage?.output_tokens ?? 0 }, - }, + emit({ + type: 'tool-call', + toolCallId: approvalId, + toolName: toCommonName(action.name), + input: JSON.stringify(action.args ?? {}), + providerExecuted: true, + nativeName: action.name, }); - } 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)) { - 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-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: runId, - toolName: toCommonName(toolName), - result: output ?? null, + toolCallId: approvalId, + toolName: toCommonName(action.name), + result: decision.reason ?? 'Rejected by user.', + }); + decisions.push({ + type: 'reject', + ...(decision.reason ? { message: decision.reason } : {}), }); } - if (runId) activeToolRunIds.delete(runId); } + + resumeInput = new Command({ resume: { decisions } }); } endTextBlock(); diff --git a/packages/harness-deepagents/src/deepagents-harness.test.ts b/packages/harness-deepagents/src/deepagents-harness.test.ts index 94a9ee571776..fe5759122bae 100644 --- a/packages/harness-deepagents/src/deepagents-harness.test.ts +++ b/packages/harness-deepagents/src/deepagents-harness.test.ts @@ -1,7 +1,3 @@ -import { - HarnessCapabilityUnsupportedError, - type HarnessV1StartOptions, -} from '@ai-sdk/harness'; import type * as NodeFsPromises from 'node:fs/promises'; import { describe, expect, it, vi } from 'vitest'; import { @@ -31,7 +27,7 @@ describe('createDeepAgents', () => { const harness = createDeepAgents(); expect(harness.specificationVersion).toBe('harness-v1'); expect(harness.harnessId).toBe('deepagents'); - expect(harness.supportsBuiltinToolApprovals).toBe(false); + expect(harness.supportsBuiltinToolApprovals).toBe(true); }); it('lists every model-callable DeepAgents built-in tool', () => { @@ -85,15 +81,6 @@ describe('createDeepAgents', () => { expect(a).toBe(b); }); - it('rejects a non-allow-all permission mode', async () => { - const harness = createDeepAgents(); - await expect( - harness.doStart({ - permissionMode: 'allow-reads', - } as unknown as HarnessV1StartOptions), - ).rejects.toBeInstanceOf(HarnessCapabilityUnsupportedError); - }); - 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 index 54cb1dcb91b7..ac857a6a95b5 100644 --- a/packages/harness-deepagents/src/deepagents-harness.ts +++ b/packages/harness-deepagents/src/deepagents-harness.ts @@ -10,6 +10,7 @@ import { type HarnessV1BuiltinTool, type HarnessV1ContinueTurnState, type HarnessV1NetworkSandboxSession, + type HarnessV1PermissionMode, type HarnessV1Prompt, type HarnessV1PromptControl, type HarnessV1ResumeSessionState, @@ -136,8 +137,8 @@ export function createDeepAgents( specificationVersion: 'harness-v1', harnessId: 'deepagents', builtinTools: DEEPAGENTS_BUILTIN_TOOLS, - // Happy-path ships `permissionMode: 'allow-all'` only; approvals are a follow-up. - supportsBuiltinToolApprovals: false, + // Built-in tool approvals are gated in-bridge via DeepAgents' interruptOn (HITL) middleware. + supportsBuiltinToolApprovals: true, lifecycleStateSchema: deepAgentsResumeStateSchema, getBootstrap: async () => { if (cachedBootstrap != null) return cachedBootstrap; @@ -164,17 +165,7 @@ export function createDeepAgents( return cachedBootstrap; }, doStart: async startOpts => { - if ( - startOpts.permissionMode != null && - startOpts.permissionMode !== 'allow-all' - ) { - throw new HarnessCapabilityUnsupportedError({ - message: - "Harness 'deepagents' does not support built-in tool approval requests yet; use permissionMode: 'allow-all'.", - harnessId: 'deepagents', - }); - } - + const permissionMode = startOpts.permissionMode; const sandboxSession = startOpts.sandboxSession; const session = sandboxSession.restricted(); const sandboxId = sandboxSession.id; @@ -228,6 +219,7 @@ export function createDeepAgents( sandboxId, isResume: true, attached: true, + permissionMode, }); } catch { // Bridge no longer reachable — recover by respawning below. @@ -315,6 +307,7 @@ export function createDeepAgents( // Freshly spawned bridge — it must receive the instructions on the first prompt. attached: false, skillsPath, + permissionMode, }); }, }; @@ -459,6 +452,7 @@ function createSession({ isResume, attached, skillsPath, + permissionMode, }: { sessionId: string; channel: DeepAgentsChannel; @@ -474,6 +468,7 @@ function createSession({ // 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; @@ -579,6 +574,14 @@ function createSession({ 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, }; }; @@ -615,6 +618,7 @@ function createSession({ })), ...(model ? { model } : {}), ...(skillsPath ? { skillsPath } : {}), + ...(permissionMode ? { permissionMode } : {}), }); return control; From 4740f6c51ca32d08340df6ebe8ff71c474f55fd6 Mon Sep 17 00:00:00 2001 From: mlekhi Date: Mon, 22 Jun 2026 22:44:32 -0700 Subject: [PATCH 19/39] fixing CI checks --- .github/konsistent.json | 2 ++ packages/harness-deepagents/README.md | 10 +++++----- packages/harness-deepagents/src/deepagents-harness.ts | 6 +++--- 3 files changed, 10 insertions(+), 8 deletions(-) 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/packages/harness-deepagents/README.md b/packages/harness-deepagents/README.md index 71f293b4aa67..2fd93554b744 100644 --- a/packages/harness-deepagents/README.md +++ b/packages/harness-deepagents/README.md @@ -59,10 +59,10 @@ createDeepAgents({ ## Built-in tools | Common name | Native (LangGraph) tool | -| --- | --- | -| `read` | `read_file` | -| `write` | `write_file` | -| `bash` | `shell` | -| `grep` | `search` | +| ----------- | ----------------------- | +| `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/src/deepagents-harness.ts b/packages/harness-deepagents/src/deepagents-harness.ts index ac857a6a95b5..111a7ddca803 100644 --- a/packages/harness-deepagents/src/deepagents-harness.ts +++ b/packages/harness-deepagents/src/deepagents-harness.ts @@ -47,16 +47,16 @@ const SKILLS_SOURCE_PATH = '/.deepagents/skills'; const DEEPAGENTS_DEFAULT_CONTEXT_WINDOW = 200_000; // Live bridge coordinates returned by doDetach/doSuspendTurn so a later process can reattach. -const bridgeCoordsSchema = z.object({ +const deepAgentsBridgeCoordsSchema = z.object({ port: z.number(), token: z.string(), lastSeenEventId: z.number(), sandboxId: z.string().optional(), }); const deepAgentsResumeStateSchema = z.object({ - bridge: bridgeCoordsSchema.optional(), + bridge: deepAgentsBridgeCoordsSchema.optional(), }); -type DeepAgentsBridgeCoords = z.infer; +type DeepAgentsBridgeCoords = z.infer; export type DeepAgentsHarnessSettings = { readonly auth?: DeepAgentsAuthOptions; From 7262e2d489f63c4b66fb09d525b9ac72806c4234 Mon Sep 17 00:00:00 2001 From: mlekhi Date: Mon, 22 Jun 2026 23:18:09 -0700 Subject: [PATCH 20/39] attempt to fix konsistent --- .../src/deepagents-harness.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/harness-deepagents/src/deepagents-harness.ts b/packages/harness-deepagents/src/deepagents-harness.ts index 111a7ddca803..2d67b37903b5 100644 --- a/packages/harness-deepagents/src/deepagents-harness.ts +++ b/packages/harness-deepagents/src/deepagents-harness.ts @@ -46,6 +46,16 @@ 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(), @@ -58,16 +68,6 @@ const deepAgentsResumeStateSchema = z.object({ }); type DeepAgentsBridgeCoords = z.infer; -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; -}; - // 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', { From 335517a59437b883753dfb54d2dc13b332e40aef Mon Sep 17 00:00:00 2001 From: mlekhi Date: Mon, 22 Jun 2026 23:53:59 -0700 Subject: [PATCH 21/39] count model-call usage once in totalUsage --- .../harness-deepagents/src/bridge/index.ts | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/harness-deepagents/src/bridge/index.ts b/packages/harness-deepagents/src/bridge/index.ts index f271d8597f99..2dd86381a045 100644 --- a/packages/harness-deepagents/src/bridge/index.ts +++ b/packages/harness-deepagents/src/bridge/index.ts @@ -133,6 +133,9 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise { 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(); @@ -235,8 +238,14 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise { } const usage = chunk.usage_metadata; if (usage) { - inputTokens = Math.max(inputTokens, usage.input_tokens ?? 0); - outputTokens = Math.max(outputTokens, usage.output_tokens ?? 0); + 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. @@ -249,18 +258,21 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise { } | undefined; const usage = output?.usage_metadata; - if (usage) { - inputTokens += usage.input_tokens ?? 0; - outputTokens += usage.output_tokens ?? 0; - } + // 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: usage?.input_tokens ?? 0 }, - outputTokens: { total: usage?.output_tokens ?? 0 }, + inputTokens: { total: stepInput }, + outputTokens: { total: stepOutput }, }, }); } else if (kind === 'on_tool_start') { From c2655c2bb9a6660a86127ac11a58b27666272dba Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Tue, 23 Jun 2026 10:13:47 -0500 Subject: [PATCH 22/39] remove unnecessary konsistent kebabToCamelMap entry --- .github/konsistent.json | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/konsistent.json b/.github/konsistent.json index 04ba5d5c4f0a..e59918f0ecf8 100644 --- a/.github/konsistent.json +++ b/.github/konsistent.json @@ -356,7 +356,6 @@ "togetherai": "TogetherAI" }, "kebabToCamelMap": { - "deepagents": "deepAgents", "lmnt": "lmnt" } } From 380d096d88954466821333747f01f19663e1f705 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Tue, 23 Jun 2026 12:03:49 -0500 Subject: [PATCH 23/39] use Deep Agents as user-facing name per official naming --- packages/harness-deepagents/README.md | 4 ++-- packages/harness-deepagents/src/bridge/index.ts | 4 ++-- packages/harness-deepagents/src/deepagents-bridge-protocol.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/harness-deepagents/README.md b/packages/harness-deepagents/README.md index 2fd93554b744..d53d4cc75cdf 100644 --- a/packages/harness-deepagents/README.md +++ b/packages/harness-deepagents/README.md @@ -1,10 +1,10 @@ # @ai-sdk/harness-deepagents -A [HarnessV1](../harness) adapter that runs [DeepAgents](https://github.com/langchain-ai/deepagentsjs) +A [HarnessV1](../harness) adapter that runs [Deep Agents](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 +This is a **bridge-backed** harness: the Deep Agents 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. diff --git a/packages/harness-deepagents/src/bridge/index.ts b/packages/harness-deepagents/src/bridge/index.ts index 2dd86381a045..898bcd5f227b 100644 --- a/packages/harness-deepagents/src/bridge/index.ts +++ b/packages/harness-deepagents/src/bridge/index.ts @@ -14,7 +14,7 @@ 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). +// Native Deep Agents 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', @@ -109,7 +109,7 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise { const interruptOn = buildInterruptOn(start.permissionMode); if (!agent) { agent = createDeepAgent({ - // Defer to DeepAgents' own default when the host configured no model. + // Defer to Deep Agents's own default when the host configured no model. ...(start.model ? { model: parseModelName(start.model) } : {}), tools: buildHostTools(start.tools), backend: new LocalShellBackend({ rootDir: workdir }), diff --git a/packages/harness-deepagents/src/deepagents-bridge-protocol.ts b/packages/harness-deepagents/src/deepagents-bridge-protocol.ts index f500b2f3aabb..1a2540a3fd6f 100644 --- a/packages/harness-deepagents/src/deepagents-bridge-protocol.ts +++ b/packages/harness-deepagents/src/deepagents-bridge-protocol.ts @@ -6,7 +6,7 @@ import { } from '@ai-sdk/harness'; import { z } from 'zod/v4'; -// DeepAgents bridge wire protocol; only the `start` payload is adapter-specific. +// Deep Agents bridge wire protocol; only the `start` payload is adapter-specific. export const outboundMessageSchema = harnessV1BridgeOutboundMessageSchema; export type OutboundMessage = z.infer; From 60631ce6312f573b5f65b826b0f69e8cfef3bf75 Mon Sep 17 00:00:00 2001 From: mlekhi Date: Tue, 23 Jun 2026 09:55:38 -0700 Subject: [PATCH 24/39] Anthropic-only model routing --- .../02-ai-sdk-harnesses/05-deepagents.mdx | 30 +++--- .../harness-agent/deepagents/gateway-model.ts | 57 ++++++++++ packages/harness-deepagents/package.json | 1 + .../harness-deepagents/src/bridge/index.ts | 20 +++- .../src/bridge/package.json | 1 - .../src/bridge/pnpm-lock.yaml | 19 +--- .../src/deepagents-auth.test.ts | 67 ++++-------- .../harness-deepagents/src/deepagents-auth.ts | 102 +++--------------- .../src/deepagents-harness.ts | 2 +- packages/harness-deepagents/tsup.config.ts | 1 + pnpm-lock.yaml | 69 +++++++++--- 11 files changed, 177 insertions(+), 192 deletions(-) create mode 100644 examples/ai-functions/src/harness-agent/deepagents/gateway-model.ts diff --git a/content/providers/02-ai-sdk-harnesses/05-deepagents.mdx b/content/providers/02-ai-sdk-harnesses/05-deepagents.mdx index bce9b58a58fa..3b7aa75d86d3 100644 --- a/content/providers/02-ai-sdk-harnesses/05-deepagents.mdx +++ b/content/providers/02-ai-sdk-harnesses/05-deepagents.mdx @@ -111,18 +111,21 @@ const harness = createDeepAgents({ 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. +- `auth`: Anthropic or AI Gateway authentication settings. +- `model`: model id passed to the DeepAgents (LangChain) runtime. Through AI + Gateway, use the `creator/model` slug (e.g. `anthropic/claude-sonnet-4-6`, + `google/gemini-2.5-flash`, `openai/gpt-4.1-mini`). - `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. +DeepAgents always drives the Anthropic client. Non-Anthropic models reach it +through AI Gateway's Anthropic-compatible endpoint, which translates to any +model (Gemini, OpenAI, etc.), tool calls included. Authentication is resolved +from the host environment and forwarded to the sandbox bridge: explicit +Anthropic auth first, then AI Gateway credentials, then ambient Anthropic +credentials. Supported environment variables: @@ -132,19 +135,16 @@ Supported environment variables: - `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`): +You can also pass explicit auth settings (`anthropic` or `gateway`). To run a +non-Anthropic model, route it through AI Gateway: ```ts const harness = createDeepAgents({ - model: 'openai/gpt-5', + model: 'google/gemini-2.5-flash', auth: { - openai: { - apiKey: process.env.OPENAI_API_KEY, + gateway: { + apiKey: process.env.AI_GATEWAY_API_KEY, }, }, }); diff --git a/examples/ai-functions/src/harness-agent/deepagents/gateway-model.ts b/examples/ai-functions/src/harness-agent/deepagents/gateway-model.ts new file mode 100644 index 000000000000..b33364150c8a --- /dev/null +++ b/examples/ai-functions/src/harness-agent/deepagents/gateway-model.ts @@ -0,0 +1,57 @@ +import { HarnessAgent } from '@ai-sdk/harness/agent'; +import { createDeepAgents } 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'; + +// Runs a non-Anthropic model through AI Gateway's Anthropic-compatible endpoint. +// Requires AI_GATEWAY_API_KEY (or VERCEL_OIDC_TOKEN) in the environment. +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: createDeepAgents({ + model: process.env.PROBE_MODEL ?? 'google/gemini-2.5-flash', + }), + 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? 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/packages/harness-deepagents/package.json b/packages/harness-deepagents/package.json index 2bc19f1d1184..4fb9f6c33340 100644 --- a/packages/harness-deepagents/package.json +++ b/packages/harness-deepagents/package.json @@ -42,6 +42,7 @@ "zod": "3.25.76" }, "devDependencies": { + "@langchain/anthropic": "^1.0.0", "@langchain/core": "^1.1.44", "@langchain/langgraph": "^1.3.0", "@types/node": "22.19.19", diff --git a/packages/harness-deepagents/src/bridge/index.ts b/packages/harness-deepagents/src/bridge/index.ts index 2dd86381a045..2eab7f30262f 100644 --- a/packages/harness-deepagents/src/bridge/index.ts +++ b/packages/harness-deepagents/src/bridge/index.ts @@ -7,6 +7,7 @@ import { type BridgeEvent, type BridgeTurn, } from '@ai-sdk/harness/bridge'; +import { ChatAnthropic } from '@langchain/anthropic'; import { tool } from '@langchain/core/tools'; import { Command, MemorySaver } from '@langchain/langgraph'; import { createDeepAgent, LocalShellBackend } from 'deepagents'; @@ -41,9 +42,19 @@ function parseArgs(rawArgs: string[]): Record { return out; } -// LangChain wants `provider:model`; the host sends `provider/model`. -function parseModelName(raw: string): string { - return raw.includes('/') ? raw.replace('/', ':') : raw; +// Always drive the Anthropic client. Through the gateway, models keep their +// `creator/model` slug (gateway translates); direct Anthropic wants the bare id. +function buildModel(rawModel: string | undefined) { + if (!rawModel) return undefined; + const baseUrl = process.env.ANTHROPIC_BASE_URL; + const model = baseUrl ? rawModel : rawModel.replace(/^anthropic[/:]/, ''); + return new ChatAnthropic({ + model, + ...(process.env.ANTHROPIC_API_KEY + ? { apiKey: process.env.ANTHROPIC_API_KEY } + : {}), + ...(baseUrl ? { anthropicApiUrl: baseUrl } : {}), + }); } // LangChain reports some built-in tool args wrapped as `{ input: "" }`; unwrap to the inner JSON so AI SDK validates the real shape. @@ -108,9 +119,10 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise { const interruptOn = buildInterruptOn(start.permissionMode); if (!agent) { + const model = buildModel(start.model); agent = createDeepAgent({ // Defer to DeepAgents' own default when the host configured no model. - ...(start.model ? { model: parseModelName(start.model) } : {}), + ...(model ? { model } : {}), tools: buildHostTools(start.tools), backend: new LocalShellBackend({ rootDir: workdir }), systemPrompt: start.instructions || undefined, diff --git a/packages/harness-deepagents/src/bridge/package.json b/packages/harness-deepagents/src/bridge/package.json index 464723d373e5..dc123c75a7a0 100644 --- a/packages/harness-deepagents/src/bridge/package.json +++ b/packages/harness-deepagents/src/bridge/package.json @@ -7,7 +7,6 @@ "@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 index 9777468ff24f..2741d72104fc 100644 --- a/packages/harness-deepagents/src/bridge/pnpm-lock.yaml +++ b/packages/harness-deepagents/src/bridge/pnpm-lock.yaml @@ -17,9 +17,6 @@ importers: '@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) @@ -93,12 +90,6 @@ packages: 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==} @@ -360,15 +351,6 @@ snapshots: - 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': @@ -502,6 +484,7 @@ snapshots: optionalDependencies: ws: 8.21.0 zod: 4.4.3 + optional: true p-finally@1.0.0: {} diff --git a/packages/harness-deepagents/src/deepagents-auth.test.ts b/packages/harness-deepagents/src/deepagents-auth.test.ts index b068d560a285..3616cc0bacee 100644 --- a/packages/harness-deepagents/src/deepagents-auth.test.ts +++ b/packages/harness-deepagents/src/deepagents-auth.test.ts @@ -1,30 +1,5 @@ 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'); - }); -}); +import { resolveDeepAgentsEnv } from './deepagents-auth'; describe('resolveDeepAgentsEnv', () => { it('pins explicit anthropic auth', () => { @@ -40,18 +15,15 @@ describe('resolveDeepAgentsEnv', () => { }); }); - it('pins explicit openai auth for an openai model', () => { + it('passes through an anthropic auth token', () => { const env = resolveDeepAgentsEnv({ - model: 'openai/gpt-5', - auth: { openai: { apiKey: 'sk-oai', organization: 'org_1' } }, + auth: { anthropic: { authToken: 'tok' } }, processEnv: {}, }); - expect(env.OPENAI_API_KEY).toBe('sk-oai'); - expect(env.OPENAI_ORGANIZATION).toBe('org_1'); - expect(env.ANTHROPIC_API_KEY).toBeUndefined(); + expect(env).toEqual({ ANTHROPIC_AUTH_TOKEN: 'tok' }); }); - it('routes an anthropic model through the gateway (no /v1 suffix)', () => { + it('routes through the gateway anthropic endpoint (no /v1 suffix)', () => { const env = resolveDeepAgentsEnv({ auth: { gateway: { apiKey: 'gw-key' } }, processEnv: {}, @@ -59,21 +31,25 @@ describe('resolveDeepAgentsEnv', () => { 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)', () => { + it('trims a trailing slash from a custom gateway base url', () => { const env = resolveDeepAgentsEnv({ - model: 'openai/gpt-5', - auth: { gateway: { apiKey: 'gw-key' } }, + auth: { gateway: { apiKey: 'gw-key', baseUrl: 'https://gw.test/' } }, 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(); + expect(env.ANTHROPIC_BASE_URL).toBe('https://gw.test'); }); - it('falls back to ambient gateway env before ambient provider creds', () => { + it('prefers explicit anthropic auth over ambient gateway creds', () => { + const env = resolveDeepAgentsEnv({ + auth: { anthropic: { apiKey: 'sk-ant' } }, + processEnv: { AI_GATEWAY_API_KEY: 'ambient-gw' }, + }); + expect(env).toEqual({ ANTHROPIC_API_KEY: 'sk-ant' }); + }); + + it('falls back to ambient gateway env before ambient anthropic creds', () => { const env = resolveDeepAgentsEnv({ processEnv: { AI_GATEWAY_API_KEY: 'ambient-gw', @@ -82,6 +58,7 @@ describe('resolveDeepAgentsEnv', () => { }); expect(env.AI_GATEWAY_API_KEY).toBe('ambient-gw'); expect(env.ANTHROPIC_API_KEY).toBe('ambient-gw'); + expect(env.ANTHROPIC_BASE_URL).toBe('https://ai-gateway.vercel.sh'); }); it('falls back to ambient OIDC token as the gateway key', () => { @@ -97,12 +74,4 @@ describe('resolveDeepAgentsEnv', () => { }); 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 index 000ace2558ff..73e96cf36a38 100644 --- a/packages/harness-deepagents/src/deepagents-auth.ts +++ b/packages/harness-deepagents/src/deepagents-auth.ts @@ -6,75 +6,35 @@ export type DeepAgentsAuthOptions = { 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. +// DeepAgents always drives the Anthropic client. Non-Anthropic models reach it +// through AI Gateway's Anthropic-compatible endpoint, which translates to any +// model (Gemini, OpenAI, etc.), tool calls included. 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) { + if (auth?.anthropic) { return pickAnthropic({ explicit: auth.anthropic, processEnv }); } const gatewayAuthFromEnv = getAiGatewayAuthFromEnv({ env: processEnv }); if (auth?.gateway) { - return pickGateway({ - provider, - explicit: auth.gateway, - gatewayAuthFromEnv, - }); + return pickGateway({ explicit: auth.gateway, gatewayAuthFromEnv }); } if (gatewayAuthFromEnv.apiKey) { - return pickGateway({ provider, explicit: {}, gatewayAuthFromEnv }); + return pickGateway({ explicit: {}, gatewayAuthFromEnv }); } - return provider === 'openai' - ? pickOpenAI({ processEnv }) - : pickAnthropic({ processEnv }); + return pickAnthropic({ processEnv }); } function pickAnthropic({ @@ -94,56 +54,24 @@ function pickAnthropic({ 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, + // The Anthropic SDK appends `/v1/messages`, so the gateway base stays at its root. + const baseUrl = (explicit.baseUrl ?? gatewayAuthFromEnv.baseUrl).replace( + /\/+$/, + '', ); 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; + if (apiKey) { + env.AI_GATEWAY_API_KEY = apiKey; + env.ANTHROPIC_API_KEY = apiKey; } + env.ANTHROPIC_BASE_URL = baseUrl; return env; } diff --git a/packages/harness-deepagents/src/deepagents-harness.ts b/packages/harness-deepagents/src/deepagents-harness.ts index 2d67b37903b5..2c2acd30a554 100644 --- a/packages/harness-deepagents/src/deepagents-harness.ts +++ b/packages/harness-deepagents/src/deepagents-harness.ts @@ -245,7 +245,7 @@ export function createDeepAgents( : undefined; const env = { - ...resolveDeepAgentsEnv({ auth: settings.auth, model: settings.model }), + ...resolveDeepAgentsEnv({ auth: settings.auth }), BRIDGE_CHANNEL_TOKEN: token, BRIDGE_WS_PORT: String(port), }; diff --git a/packages/harness-deepagents/tsup.config.ts b/packages/harness-deepagents/tsup.config.ts index bfda94f1bd22..15431e68d069 100644 --- a/packages/harness-deepagents/tsup.config.ts +++ b/packages/harness-deepagents/tsup.config.ts @@ -23,6 +23,7 @@ export default defineConfig([ noExternal: ['@ai-sdk/harness'], external: [ 'deepagents', + '@langchain/anthropic', '@langchain/core', '@langchain/langgraph', 'ws', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e7c99342e1d9..1aa30d114613 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2555,7 +2555,7 @@ importers: devDependencies: '@anthropic-ai/claude-agent-sdk': specifier: 0.3.177 - version: 0.3.177(@anthropic-ai/sdk@0.91.1(zod@3.25.76))(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(zod@3.25.76) + version: 0.3.177(@anthropic-ai/sdk@0.103.0(zod@3.25.76))(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(zod@3.25.76) '@modelcontextprotocol/sdk': specifier: 1.29.0 version: 1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76) @@ -2627,6 +2627,9 @@ importers: specifier: 3.25.76 version: 3.25.76 devDependencies: + '@langchain/anthropic': + specifier: ^1.0.0 + version: 1.5.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)) '@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) @@ -4265,6 +4268,15 @@ packages: '@modelcontextprotocol/sdk': ^1.29.0 zod: ^4.0.0 + '@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 + '@anthropic-ai/sdk@0.91.1': resolution: {integrity: sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==} hasBin: true @@ -7565,6 +7577,12 @@ packages: '@kwsites/promise-deferred@1.1.1': resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==} + '@langchain/anthropic@1.5.0': + resolution: {integrity: sha512-IhWeK87QRAYyhlvSE0p+Zh4oylnZoa+16XP7XgvsPsLEbW4YMueVYl7aT4EsbsM37xbr3l/P7euIRdowc8WfOA==} + engines: {node: '>=20'} + peerDependencies: + '@langchain/core': ^1.2.0 + '@langchain/core@1.1.46': resolution: {integrity: sha512-i8rDC83BpItxChCw4Lf+6tAr+k+OUcbirc5ZkrhI9ywYWmvxegUljLGOGYvtJNTbEAIFkhYIODPE5QRqyjF6sA==} engines: {node: '>=20'} @@ -10960,6 +10978,9 @@ packages: '@speed-highlight/core@1.2.17': resolution: {integrity: sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg==} + '@stablelib/base64@1.0.1': + resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} + '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} @@ -14495,6 +14516,9 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + fast-string-truncated-width@3.0.3: resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} @@ -19142,6 +19166,9 @@ packages: standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + standardwebhooks@1.0.0: + resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} + statuses@1.5.0: resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} engines: {node: '>= 0.6'} @@ -20936,18 +20963,6 @@ packages: resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - ws@8.20.1: - resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} - 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 - ws@8.21.0: resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} engines: {node: '>=10.0.0'} @@ -21575,9 +21590,9 @@ snapshots: '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.177': optional: true - '@anthropic-ai/claude-agent-sdk@0.3.177(@anthropic-ai/sdk@0.91.1(zod@3.25.76))(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(zod@3.25.76)': + '@anthropic-ai/claude-agent-sdk@0.3.177(@anthropic-ai/sdk@0.103.0(zod@3.25.76))(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(zod@3.25.76)': dependencies: - '@anthropic-ai/sdk': 0.91.1(zod@3.25.76) + '@anthropic-ai/sdk': 0.103.0(zod@3.25.76) '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76) zod: 3.25.76 optionalDependencies: @@ -21590,6 +21605,13 @@ snapshots: '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.3.177 '@anthropic-ai/claude-agent-sdk-win32-x64': 0.3.177 + '@anthropic-ai/sdk@0.103.0(zod@3.25.76)': + dependencies: + json-schema-to-ts: 3.1.1 + standardwebhooks: 1.0.0 + optionalDependencies: + zod: 3.25.76 + '@anthropic-ai/sdk@0.91.1(zod@3.25.76)': dependencies: json-schema-to-ts: 3.1.1 @@ -25732,6 +25754,12 @@ snapshots: '@kwsites/promise-deferred@1.1.1': {} + '@langchain/anthropic@1.5.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))': + dependencies: + '@anthropic-ai/sdk': 0.103.0(zod@3.25.76) + '@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) + zod: 3.25.76 + '@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)': dependencies: '@cfworker/json-schema': 4.1.1 @@ -29596,6 +29624,8 @@ snapshots: '@speed-highlight/core@1.2.17': {} + '@stablelib/base64@1.0.1': {} + '@standard-schema/spec@1.0.0': {} '@standard-schema/spec@1.1.0': {} @@ -34294,6 +34324,8 @@ snapshots: fast-safe-stringify@2.1.1: {} + fast-sha256@1.3.0: {} + fast-string-truncated-width@3.0.3: {} fast-string-width@3.0.2: @@ -40429,6 +40461,11 @@ snapshots: standard-as-callback@2.1.0: {} + standardwebhooks@1.0.0: + dependencies: + '@stablelib/base64': 1.0.1 + fast-sha256: 1.3.0 + statuses@1.5.0: {} statuses@2.0.1: {} @@ -42949,8 +42986,6 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 4.1.0 - ws@8.20.1: {} - ws@8.21.0: {} wsl-utils@0.1.0: From 131031ef403e42e5dace615f238924ae54dbcf3a Mon Sep 17 00:00:00 2001 From: mlekhi Date: Tue, 23 Jun 2026 11:37:13 -0700 Subject: [PATCH 25/39] correct step boundaries and suppress subagent leakage --- .../harness-deepagents/src/bridge/index.ts | 55 ++++++++++++------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/packages/harness-deepagents/src/bridge/index.ts b/packages/harness-deepagents/src/bridge/index.ts index 2eab7f30262f..fa2a7f437f32 100644 --- a/packages/harness-deepagents/src/bridge/index.ts +++ b/packages/harness-deepagents/src/bridge/index.ts @@ -148,7 +148,8 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise { // 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(); + // Top-level step usage is buffered at model-end and flushed as finish-step only after the step's tools run. + let pendingStep: { input: number; output: number } | undefined; // 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(); @@ -185,6 +186,19 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise { } emit({ type: 'reasoning-delta', id: reasoningBlockId, delta }); }; + // Close the buffered top-level step; called when the next step starts and at turn end so finish-step lands after the step's tools. + const flushStep = () => { + if (!pendingStep) return; + emit({ + type: 'finish-step', + finishReason: { unified: 'stop' }, + usage: { + inputTokens: { total: pendingStep.input }, + outputTokens: { total: pendingStep.output }, + }, + }); + pendingStep = undefined; + }; const config = { version: 'v2' as const, @@ -217,10 +231,17 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise { for await (const event of stream) { const kind = event.event; const data = (event.data ?? {}) as Record; + // Subagent (e.g. `task`) events carry a `|`-delimited checkpoint namespace; keep their internals out of the top-level stream. + const ns = + (event as { metadata?: { langgraph_checkpoint_ns?: string } }).metadata + ?.langgraph_checkpoint_ns ?? ''; + const nested = ns.includes('|'); - if (kind === 'on_chat_model_stream') { - const parentIds = (event as { parent_ids?: string[] }).parent_ids ?? []; - if (parentIds.some(id => activeToolRunIds.has(id))) continue; + if (kind === 'on_chat_model_start') { + // A new top-level model call means the previous step's tools have run; close it now. + if (!nested) flushStep(); + } else if (kind === 'on_chat_model_stream') { + if (nested) continue; const chunk = data.chunk as | { content?: unknown; @@ -277,22 +298,18 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise { outputTokens += stepOutput; streamedStepInput = 0; streamedStepOutput = 0; - endTextBlock(); - endReasoningBlock(); - turn.emit({ - type: 'finish-step', - finishReason: { unified: 'stop' }, - usage: { - inputTokens: { total: stepInput }, - outputTokens: { total: stepOutput }, - }, - }); + // Nested (subagent) calls still count toward total usage, but only top-level calls bound a visible step. + if (!nested) { + endTextBlock(); + endReasoningBlock(); + // Buffer the step; flushStep emits finish-step after this step's tools run (next start / turn end). + pendingStep = { input: stepInput, output: 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)) { + // Host tools emit their own tool-call; surface only top-level builtin (providerExecuted) tools. + if (!nested && !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. @@ -314,7 +331,7 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise { } else if (kind === 'on_tool_end') { const toolName = (event.name as string) ?? 'unknown'; const runId = (event.run_id as string) ?? ''; - if (!hostToolNames.has(toolName)) { + if (!nested && !hostToolNames.has(toolName)) { let output: unknown = data.output ?? ''; if (output && typeof output === 'object' && 'content' in output) { output = (output as { content: unknown }).content; @@ -327,7 +344,6 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise { }); approvedRunIds.delete(runId); } - if (runId) activeToolRunIds.delete(runId); } } @@ -381,6 +397,7 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise { endTextBlock(); endReasoningBlock(); + flushStep(); emit({ type: 'finish', finishReason: { unified: 'stop' }, From dd75fbdcc83b5de003d722c8f8fa2f2ab66689a9 Mon Sep 17 00:00:00 2001 From: mlekhi Date: Tue, 23 Jun 2026 13:03:59 -0700 Subject: [PATCH 26/39] write skills to \$HOME and discover workdir skills --- .../02-ai-sdk-harnesses/05-deepagents.mdx | 8 ++- .../harness-agent/deepagents/with-skills.ts | 2 +- .../harness-deepagents/src/bridge/index.ts | 4 +- .../src/deepagents-bridge-protocol.ts | 4 +- .../src/deepagents-harness.ts | 60 +++++++++++++------ 5 files changed, 52 insertions(+), 26 deletions(-) diff --git a/content/providers/02-ai-sdk-harnesses/05-deepagents.mdx b/content/providers/02-ai-sdk-harnesses/05-deepagents.mdx index 3b7aa75d86d3..8a68949706d2 100644 --- a/content/providers/02-ai-sdk-harnesses/05-deepagents.mdx +++ b/content/providers/02-ai-sdk-harnesses/05-deepagents.mdx @@ -165,9 +165,11 @@ const sandbox = createVercelSandbox({ ## 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. +(`/SKILL.md` plus any attached files) under `$HOME/.agents/skills/` in the +sandbox (outside the work dir, so they can't clash with cloned code), and loaded +via DeepAgents' `skills` option — so the agent loads them on demand and skill +file references resolve. Skills already present under `/.agents/skills/` +(e.g. in a cloned repo) are also discovered. ## Built-in Tools diff --git a/examples/ai-functions/src/harness-agent/deepagents/with-skills.ts b/examples/ai-functions/src/harness-agent/deepagents/with-skills.ts index 15320ace7ffe..4cbd1a371df7 100644 --- a/examples/ai-functions/src/harness-agent/deepagents/with-skills.ts +++ b/examples/ai-functions/src/harness-agent/deepagents/with-skills.ts @@ -4,7 +4,7 @@ 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`. +// Skills are loaded on demand by name/description; the harness writes them under `$HOME/.agents/skills`. run(async () => { const sandbox = createVercelSandbox({ runtime: 'node24', diff --git a/packages/harness-deepagents/src/bridge/index.ts b/packages/harness-deepagents/src/bridge/index.ts index 4895d1591f7c..782cc1b5ec52 100644 --- a/packages/harness-deepagents/src/bridge/index.ts +++ b/packages/harness-deepagents/src/bridge/index.ts @@ -126,8 +126,8 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise { 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] } : {}), + // Native skills loaded from the source dirs ($HOME-materialized + for repo-provided skills). + ...(start.skillsPaths?.length ? { skills: start.skillsPaths } : {}), // 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. diff --git a/packages/harness-deepagents/src/deepagents-bridge-protocol.ts b/packages/harness-deepagents/src/deepagents-bridge-protocol.ts index 1a2540a3fd6f..e89cedc46242 100644 --- a/packages/harness-deepagents/src/deepagents-bridge-protocol.ts +++ b/packages/harness-deepagents/src/deepagents-bridge-protocol.ts @@ -13,8 +13,8 @@ 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(), + // In-backend skills source dirs ($HOME and ), passed to createDeepAgent({ skills }). + skillsPaths: z.array(z.string()).optional(), }); export type StartMessage = z.infer; diff --git a/packages/harness-deepagents/src/deepagents-harness.ts b/packages/harness-deepagents/src/deepagents-harness.ts index 2c2acd30a554..28ac9ba135a1 100644 --- a/packages/harness-deepagents/src/deepagents-harness.ts +++ b/packages/harness-deepagents/src/deepagents-harness.ts @@ -41,8 +41,8 @@ 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'; +// Skills source subpath, written under $HOME (out of the work dir so it can't clash with code cloned into the work dir) and also discovered from for repo-provided skills. +const SKILLS_SOURCE_PATH = '/.agents/skills'; const DEEPAGENTS_DEFAULT_CONTEXT_WINDOW = 200_000; @@ -229,20 +229,24 @@ export function createDeepAgents( 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) { + // Always discover repo-provided skills under /.agents/skills (e.g. a cloned repo); a missing dir is tolerated by deepagents. + // Absolute paths: LocalShellBackend (non-virtual) treats a leading-slash path as a real fs path. + const skillsPaths = [`${workDir}${SKILLS_SOURCE_PATH}`]; + // Host-provided skills go to $HOME (out of the work dir) and take priority (listed last → wins on name collision). + if ((startOpts.skills?.length ?? 0) > 0) { + const homeDir = await resolveSandboxHomeDir({ + sandbox: session, + abortSignal: startOpts.abortSignal, + }); + const homeSkillsRoot = `${homeDir}${SKILLS_SOURCE_PATH}`; await writeSkills({ sandbox: session, - workDir, + root: homeSkillsRoot, skills: startOpts.skills ?? [], abortSignal: startOpts.abortSignal, }); + skillsPaths.push(homeSkillsRoot); } - // 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 }), @@ -306,7 +310,7 @@ export function createDeepAgents( isResume, // Freshly spawned bridge — it must receive the instructions on the first prompt. attached: false, - skillsPath, + skillsPaths, permissionMode, }); }, @@ -345,19 +349,39 @@ async function readBridgeAsset(name: string): Promise { 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. +// Resolve the sandbox $HOME so skills can be written outside the work dir. +async function resolveSandboxHomeDir({ + sandbox, + abortSignal, +}: { + sandbox: ReturnType; + abortSignal?: AbortSignal; +}): Promise { + const result = await sandbox.run({ + command: 'printf "%s" "$HOME"', + abortSignal, + }); + const homeDir = result.stdout.trim(); + if (result.exitCode !== 0 || !homeDir.startsWith('/')) { + throw new Error( + `Unable to resolve sandbox HOME directory: ${result.stderr || result.stdout}`, + ); + } + return homeDir; +} + +// Materialize each skill as a native deepagents `/SKILL.md` folder (+ attached files) under the given root, so skills load on demand and file references resolve. async function writeSkills({ sandbox, - workDir, + root, skills, abortSignal, }: { sandbox: ReturnType; - workDir: string; + root: 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}`; @@ -451,7 +475,7 @@ function createSession({ sandboxId, isResume, attached, - skillsPath, + skillsPaths, permissionMode, }: { sessionId: string; @@ -467,7 +491,7 @@ function createSession({ // 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; + skillsPaths?: string[]; permissionMode?: HarnessV1PermissionMode; }): HarnessV1Session { let stopped = false; @@ -617,7 +641,7 @@ function createSession({ inputSchema: t.inputSchema, })), ...(model ? { model } : {}), - ...(skillsPath ? { skillsPath } : {}), + ...(skillsPaths?.length ? { skillsPaths } : {}), ...(permissionMode ? { permissionMode } : {}), }); From 68a17027819a1ce1fbd50a102a2b4698d25bf34d Mon Sep 17 00:00:00 2001 From: Maya <121539073+mlekhi@users.noreply.github.com> Date: Tue, 23 Jun 2026 13:38:46 -0700 Subject: [PATCH 27/39] feat(harness-deepagents): add workflow-harness e2e and suspend-turn examples (#16332) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the workflow-harness examples for Deep Agents — same set #16315 added for Claude Code, Codex, and Pi. Stacked on `harness-deepagents` (#16261). New files: - `suspend-turn.ts` example (stream → suspend → continue) - `workflow/` route + step + workflow modules (durable multi-turn chat) - `workflow/page.tsx` + a link on the home page Tested live against a Vercel Sandbox: turn 1 wrote a file, turn 2 reused the warm session and read it back. Type-check, lint, and format pass. --- .../harness-agent/deepagents/suspend-turn.ts | 65 +++++++++++++++++++ .../api/harness/deepagents/workflow/route.ts | 34 ++++++++++ .../deepagents/workflow/run-slice-step.ts | 23 +++++++ .../harness/deepagents/workflow/workflow.ts | 25 +++++++ .../app/harness/deepagents/workflow/page.tsx | 19 ++++++ examples/harness-e2e-next/app/page.tsx | 1 + 6 files changed, 167 insertions(+) create mode 100644 examples/ai-functions/src/harness-agent/deepagents/suspend-turn.ts create mode 100644 examples/harness-e2e-next/app/api/harness/deepagents/workflow/route.ts create mode 100644 examples/harness-e2e-next/app/api/harness/deepagents/workflow/run-slice-step.ts create mode 100644 examples/harness-e2e-next/app/api/harness/deepagents/workflow/workflow.ts create mode 100644 examples/harness-e2e-next/app/harness/deepagents/workflow/page.tsx diff --git a/examples/ai-functions/src/harness-agent/deepagents/suspend-turn.ts b/examples/ai-functions/src/harness-agent/deepagents/suspend-turn.ts new file mode 100644 index 000000000000..2e1e34a5851e --- /dev/null +++ b/examples/ai-functions/src/harness-agent/deepagents/suspend-turn.ts @@ -0,0 +1,65 @@ +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'; + +const prompt = ` +Create a complete retro Snake game in this workspace. + +Requirements: +- Write a single playable HTML file named snake.html. +- Include keyboard controls, score, game-over and restart behavior. +- Use a pixel-art visual style with no external assets. +- After writing the file, inspect it and make one improvement pass. +`; + +const suspendAfterMs = 10000; + +function wait({ ms }: { ms: number }) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +run(async () => { + const sandbox = createVercelSandbox({ + runtime: 'node24', + ports: [4000], + timeout: 10 * 60 * 1000, + }); + const agent = new HarnessAgent({ + harness: deepAgents, + sandbox, + }); + + let exitCode = 0; + let session = await agent.createSession(); + try { + console.log('--- turn 1: stream ---'); + const result = await agent.stream({ session, prompt }); + const stream = printFullStream({ result }); + + await wait({ ms: suspendAfterMs }); + + console.log('\n--- suspend turn ---'); + const continueFrom = await session.suspendTurn(); + await stream; + console.log('continueFrom:', JSON.stringify(continueFrom)); + + console.log('--- continue turn ---'); + session = await agent.createSession({ + sessionId: session.sessionId, + continueFrom, + }); + const continued = await agent.continueStream({ session }); + await printFullStream({ result: continued }); + + console.log('finishReason:', await continued.finishReason); + console.log('usage:', await continued.usage); + } catch (err) { + exitCode = 1; + console.error('[example] failed:', err); + } finally { + await session.destroy(); + process.exit(exitCode); + } +}); diff --git a/examples/harness-e2e-next/app/api/harness/deepagents/workflow/route.ts b/examples/harness-e2e-next/app/api/harness/deepagents/workflow/route.ts new file mode 100644 index 000000000000..a84969142e36 --- /dev/null +++ b/examples/harness-e2e-next/app/api/harness/deepagents/workflow/route.ts @@ -0,0 +1,34 @@ +import { latestUserMessage } from '@/util/latest-user-message'; +import { + convertToModelMessages, + createUIMessageStreamResponse, + type UIMessage, + type UIMessageChunk, +} from 'ai'; +import { start } from 'workflow/api'; +import { deepAgentsCodingWorkflow } from './workflow'; + +// Durable multi-turn DeepAgents chat via the Workflow DevKit; the `'use workflow'` orchestration lives in `./workflow` (kept `ai`-free) and this is the plain POST handler. +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 prompt = latestUserMessage(await convertToModelMessages(body.messages)); + if (!prompt) { + return new Response('No user message to run', { status: 400 }); + } + + // The chat id is the stable harness session id; the session owns history, so we send only the newest user message. + const run = await start(deepAgentsCodingWorkflow, [ + { prompt, sessionId: body.id }, + ]); + + return createUIMessageStreamResponse({ + stream: run.readable as ReadableStream, + }); +} diff --git a/examples/harness-e2e-next/app/api/harness/deepagents/workflow/run-slice-step.ts b/examples/harness-e2e-next/app/api/harness/deepagents/workflow/run-slice-step.ts new file mode 100644 index 000000000000..703e35a47b07 --- /dev/null +++ b/examples/harness-e2e-next/app/api/harness/deepagents/workflow/run-slice-step.ts @@ -0,0 +1,23 @@ +import { + runHarnessAgentSlice, + type HarnessWorkflowState, +} from '@ai-sdk/workflow-harness'; + +// Slice step in its own module; the agent is dynamically imported so its `@vercel/sandbox` deps stay out of the workflow bundle. See the claude-code slice step for the full rationale. + +// Demo budget lowered from the 750s production default so slicing is observable. +const DEMO_SLICE_TIMEOUT_SECONDS = 30; + +export async function runDeepAgentsSlice( + state: HarnessWorkflowState, +): Promise { + 'use step'; + + const { deepAgentsHarnessAgent } = + await import('@/agent/harness/deepagents/basic-agent'); + return runHarnessAgentSlice({ + agent: deepAgentsHarnessAgent, + state, + sliceTimeoutSeconds: DEMO_SLICE_TIMEOUT_SECONDS, + }); +} diff --git a/examples/harness-e2e-next/app/api/harness/deepagents/workflow/workflow.ts b/examples/harness-e2e-next/app/api/harness/deepagents/workflow/workflow.ts new file mode 100644 index 000000000000..47d4890b2424 --- /dev/null +++ b/examples/harness-e2e-next/app/api/harness/deepagents/workflow/workflow.ts @@ -0,0 +1,25 @@ +import { + loadResumeStep, + persistResumeStep, +} from '@/util/workflow-resume-steps'; +import { + createHarnessWorkflowState, + finalizeHarnessWorkflow, + type HarnessWorkflowInput, +} from '@ai-sdk/workflow-harness'; +import { runDeepAgentsSlice } from './run-slice-step'; + +// The `'use workflow'` function lives in its own `ai`-free module (not `route.ts`) so the DevKit's generated step/flow bundle doesn't pull in `@ai-sdk/gateway`/`@vercel/oidc` and crash. See the claude-code workflow module for the full rationale. +export async function deepAgentsCodingWorkflow( + input: Pick, +) { + 'use workflow'; + + const resumeFrom = await loadResumeStep(input.sessionId); + let state = createHarnessWorkflowState({ ...input, resumeFrom }); + while (state.status === 'running' || state.status === 'timed_out') { + state = await runDeepAgentsSlice(state); + } + await persistResumeStep(state.sessionId, state.resumeFrom); + return finalizeHarnessWorkflow(state); +} diff --git a/examples/harness-e2e-next/app/harness/deepagents/workflow/page.tsx b/examples/harness-e2e-next/app/harness/deepagents/workflow/page.tsx new file mode 100644 index 000000000000..74d5c8216f0a --- /dev/null +++ b/examples/harness-e2e-next/app/harness/deepagents/workflow/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 — Workflow', +}; + +const STORAGE_KEY = 'harness-deepagents-workflow-chat-id'; + +export default function HarnessDeepAgentsWorkflowPage() { + return ( + + + + ); +} diff --git a/examples/harness-e2e-next/app/page.tsx b/examples/harness-e2e-next/app/page.tsx index 12ba23c68b02..be14a206bac0 100644 --- a/examples/harness-e2e-next/app/page.tsx +++ b/examples/harness-e2e-next/app/page.tsx @@ -43,6 +43,7 @@ const HARNESSES = [ 'ai-sdk-coding', 'weather', 'weather-approval', + 'workflow', ], }, { From 3374ba89215a5856ad924b7d05732b7b649fe854 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Wed, 24 Jun 2026 09:47:32 -0500 Subject: [PATCH 28/39] update docs --- .../05-harness-adapters.mdx | 3 +- .../02-ai-sdk-harnesses/05-deepagents.mdx | 28 +++++++++---------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/content/docs/03-ai-sdk-harnesses/05-harness-adapters.mdx b/content/docs/03-ai-sdk-harnesses/05-harness-adapters.mdx index 1cc9b079281a..a2838536cf6d 100644 --- a/content/docs/03-ai-sdk-harnesses/05-harness-adapters.mdx +++ b/content/docs/03-ai-sdk-harnesses/05-harness-adapters.mdx @@ -16,13 +16,13 @@ The AI SDK includes the following harness adapters: - [Claude Code](/providers/ai-sdk-harnesses/claude-code) (`@ai-sdk/harness-claude-code`) - [Codex](/providers/ai-sdk-harnesses/codex) (`@ai-sdk/harness-codex`) +- [Deep Agents](/providers/ai-sdk-harnesses/deepagents) (`@ai-sdk/harness-deepagents`) - [OpenCode](/providers/ai-sdk-harnesses/opencode) (`@ai-sdk/harness-opencode`) - [Pi](/providers/ai-sdk-harnesses/pi) (`@ai-sdk/harness-pi`) ### Coming Soon - Amp (`@ai-sdk/harness-amp`) -- DeepAgents (`@ai-sdk/harness-deepagents`) - Goose (`@ai-sdk/harness-goose`) - Mastra (`@ai-sdk/harness-mastra`) @@ -32,5 +32,6 @@ The AI SDK includes the following harness adapters: | ------------------------------------------------------ | ---------------- | ------------------- | ------------------- | ---------------------- | | [Claude Code](/providers/ai-sdk-harnesses/claude-code) | Sandbox bridge | | | | | [Codex](/providers/ai-sdk-harnesses/codex) | Sandbox bridge | | | | +| [Deep Agents](/providers/ai-sdk-harnesses/deepagents) | Sandbox bridge | | | | | [OpenCode](/providers/ai-sdk-harnesses/opencode) | Sandbox bridge | | | | | [Pi](/providers/ai-sdk-harnesses/pi) | Host process | | | | diff --git a/content/providers/02-ai-sdk-harnesses/05-deepagents.mdx b/content/providers/02-ai-sdk-harnesses/05-deepagents.mdx index 8a68949706d2..9f1280728110 100644 --- a/content/providers/02-ai-sdk-harnesses/05-deepagents.mdx +++ b/content/providers/02-ai-sdk-harnesses/05-deepagents.mdx @@ -1,15 +1,15 @@ --- -title: DeepAgents -description: Learn how to use the DeepAgents harness adapter. +title: Deep Agents +description: Learn how to use the Deep Agents harness adapter. --- -# DeepAgents Harness +# Deep Agents Harness -The DeepAgents harness adapter connects `HarnessAgent` to -[DeepAgents](https://github.com/deep-agents/deepagents), a LangGraph-based agent +The Deep Agents harness adapter connects `HarnessAgent` to +[Deep Agents](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. +`deepagents` package and streams its `streamEvents` output back to the host over a +sandbox-exposed WebSocket. Harness packages are **experimental**. Expect breaking changes between @@ -112,7 +112,7 @@ const harness = createDeepAgents({ Settings: - `auth`: Anthropic or AI Gateway authentication settings. -- `model`: model id passed to the DeepAgents (LangChain) runtime. Through AI +- `model`: model id passed to the Deep Agents (LangChain) runtime. Through AI Gateway, use the `creator/model` slug (e.g. `anthropic/claude-sonnet-4-6`, `google/gemini-2.5-flash`, `openai/gpt-4.1-mini`). - `port`: bridge port override. @@ -120,7 +120,7 @@ Settings: ## Authentication -DeepAgents always drives the Anthropic client. Non-Anthropic models reach it +Deep Agents always drives the Anthropic client. Non-Anthropic models reach it through AI Gateway's Anthropic-compatible endpoint, which translates to any model (Gemini, OpenAI, etc.), tool calls included. Authentication is resolved from the host environment and forwarded to the sandbox bridge: explicit @@ -152,7 +152,7 @@ const harness = createDeepAgents({ ## Sandbox -DeepAgents requires a network sandbox with at least one exposed port, +Deep Agents requires a network sandbox with at least one exposed port, e.g. `@ai-sdk/sandbox-vercel`: ```ts @@ -164,16 +164,16 @@ const sandbox = createVercelSandbox({ ## Skills -Skills passed to the session are materialized as native DeepAgents skill folders +Skills passed to the session are materialized as native Deep Agents skill folders (`/SKILL.md` plus any attached files) under `$HOME/.agents/skills/` in the sandbox (outside the work dir, so they can't clash with cloned code), and loaded -via DeepAgents' `skills` option — so the agent loads them on demand and skill +via Deep Agents' `skills` option — so the agent loads them on demand and skill file references resolve. Skills already present under `/.agents/skills/` (e.g. in a cloned repo) are also discovered. ## Built-in Tools -The adapter exposes these common DeepAgents built-ins through `agent.tools`: +The adapter exposes these common Deep Agents built-ins through `agent.tools`: - `read` (native `read_file`) - `write` (native `write_file`) @@ -185,7 +185,7 @@ The adapter exposes these common DeepAgents built-ins through `agent.tools`: - **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 + supported yet — Deep Agents holds conversation state in memory (LangGraph `MemorySaver`), which does not survive a bridge restart. These methods throw `HarnessCapabilityUnsupportedError`. - **Manual compaction** is not supported. From 3e095678dbb9fa5e56b1fad97efca1719101c4b1 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Wed, 24 Jun 2026 09:49:26 -0500 Subject: [PATCH 29/39] replace user-facing name DeepAgents with Deep Agents per their brand --- .../src/harness-agent/deepagents/typed-builtin-tools.ts | 2 +- .../app/harness/deepagents/ai-sdk-coding/page.tsx | 2 +- .../app/harness/deepagents/basic-with-stop/page.tsx | 2 +- examples/harness-e2e-next/app/harness/deepagents/basic/page.tsx | 2 +- .../app/harness/deepagents/weather-approval/page.tsx | 2 +- .../harness-e2e-next/app/harness/deepagents/weather/page.tsx | 2 +- .../harness-e2e-next/app/harness/deepagents/workflow/page.tsx | 2 +- examples/harness-e2e-next/app/page.tsx | 2 +- .../harness-e2e-next/components/deepagents-harness-chat.tsx | 2 +- .../components/weather-deepagents-harness-chat.tsx | 2 +- examples/harness-e2e-tui/harness/deepagents/ai-sdk-coding.ts | 2 +- examples/harness-e2e-tui/harness/deepagents/basic.ts | 2 +- examples/harness-e2e-tui/harness/deepagents/weather-approval.ts | 2 +- examples/harness-e2e-tui/harness/deepagents/weather.ts | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) 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 index 832756e9aebc..d37912da4e14 100644 --- a/examples/ai-functions/src/harness-agent/deepagents/typed-builtin-tools.ts +++ b/examples/ai-functions/src/harness-agent/deepagents/typed-builtin-tools.ts @@ -6,7 +6,7 @@ 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) +// Deep Agents's 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({ 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 index fb607997ac54..6c6ea09fe7b7 100644 --- 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 @@ -2,7 +2,7 @@ import ChatIdProvider from '@/components/chat-id-provider'; import DeepAgentsHarnessChat from '@/components/deepagents-harness-chat'; export const metadata = { - title: 'DeepAgents — AI SDK Coding', + title: 'Deep Agents — AI SDK Coding', }; const STORAGE_KEY = 'harness-deepagents-ai-sdk-coding-chat-id'; 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 index 061ca9383553..029c9a54abed 100644 --- 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 @@ -2,7 +2,7 @@ import ChatIdProvider from '@/components/chat-id-provider'; import DeepAgentsHarnessChat from '@/components/deepagents-harness-chat'; export const metadata = { - title: 'DeepAgents — Basic (with stop)', + title: 'Deep Agents — Basic (with stop)', }; const STORAGE_KEY = 'harness-deepagents-basic-with-stop-chat-id'; diff --git a/examples/harness-e2e-next/app/harness/deepagents/basic/page.tsx b/examples/harness-e2e-next/app/harness/deepagents/basic/page.tsx index 91ad46da119f..c3b27b287f01 100644 --- a/examples/harness-e2e-next/app/harness/deepagents/basic/page.tsx +++ b/examples/harness-e2e-next/app/harness/deepagents/basic/page.tsx @@ -2,7 +2,7 @@ import ChatIdProvider from '@/components/chat-id-provider'; import DeepAgentsHarnessChat from '@/components/deepagents-harness-chat'; export const metadata = { - title: 'DeepAgents — Basic', + title: 'Deep Agents — Basic', }; const STORAGE_KEY = 'harness-deepagents-basic-chat-id'; 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 index 78567a0ca0f7..abf36480dc20 100644 --- a/examples/harness-e2e-next/app/harness/deepagents/weather-approval/page.tsx +++ b/examples/harness-e2e-next/app/harness/deepagents/weather-approval/page.tsx @@ -2,7 +2,7 @@ import ChatIdProvider from '@/components/chat-id-provider'; import WeatherDeepAgentsHarnessChat from '@/components/weather-deepagents-harness-chat'; export const metadata = { - title: 'DeepAgents — Weather Approval', + title: 'Deep Agents — Weather Approval', }; const STORAGE_KEY = 'harness-deepagents-weather-approval-chat-id'; diff --git a/examples/harness-e2e-next/app/harness/deepagents/weather/page.tsx b/examples/harness-e2e-next/app/harness/deepagents/weather/page.tsx index 13cef11b3928..8202fbc7ad47 100644 --- a/examples/harness-e2e-next/app/harness/deepagents/weather/page.tsx +++ b/examples/harness-e2e-next/app/harness/deepagents/weather/page.tsx @@ -2,7 +2,7 @@ import ChatIdProvider from '@/components/chat-id-provider'; import WeatherDeepAgentsHarnessChat from '@/components/weather-deepagents-harness-chat'; export const metadata = { - title: 'DeepAgents — Weather', + title: 'Deep Agents — Weather', }; const STORAGE_KEY = 'harness-deepagents-weather-chat-id'; diff --git a/examples/harness-e2e-next/app/harness/deepagents/workflow/page.tsx b/examples/harness-e2e-next/app/harness/deepagents/workflow/page.tsx index 74d5c8216f0a..784f360a3446 100644 --- a/examples/harness-e2e-next/app/harness/deepagents/workflow/page.tsx +++ b/examples/harness-e2e-next/app/harness/deepagents/workflow/page.tsx @@ -2,7 +2,7 @@ import ChatIdProvider from '@/components/chat-id-provider'; import DeepAgentsHarnessChat from '@/components/deepagents-harness-chat'; export const metadata = { - title: 'DeepAgents — Workflow', + title: 'Deep Agents — Workflow', }; const STORAGE_KEY = 'harness-deepagents-workflow-chat-id'; diff --git a/examples/harness-e2e-next/app/page.tsx b/examples/harness-e2e-next/app/page.tsx index 1c6721b65656..f7ad4426b52f 100644 --- a/examples/harness-e2e-next/app/page.tsx +++ b/examples/harness-e2e-next/app/page.tsx @@ -36,7 +36,7 @@ const HARNESSES = [ }, { slug: 'deepagents', - label: 'DeepAgents', + label: 'Deep Agents', variants: [ 'basic', 'basic-with-stop', diff --git a/examples/harness-e2e-next/components/deepagents-harness-chat.tsx b/examples/harness-e2e-next/components/deepagents-harness-chat.tsx index 5d60d00943a4..2029246c837d 100644 --- a/examples/harness-e2e-next/components/deepagents-harness-chat.tsx +++ b/examples/harness-e2e-next/components/deepagents-harness-chat.tsx @@ -27,7 +27,7 @@ export default function DeepAgentsHarnessChat({ return (
-

DeepAgents — {exampleLabel}

+

Deep Agents — {exampleLabel}

chat id: {chatId}