@ai-sdk/harness-deepagents 1.0.71 → 1.0.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/harness-deepagents",
3
- "version": "1.0.71",
3
+ "version": "1.0.73",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "ws": "8.21.0",
30
- "@ai-sdk/harness": "1.0.71",
30
+ "@ai-sdk/harness": "1.0.73",
31
31
  "@ai-sdk/provider-utils": "5.0.27"
32
32
  },
33
33
  "peerDependencies": {
@@ -63,12 +63,14 @@ export function createEmitStreamEvent({
63
63
  configuredModel,
64
64
  hostToolNames,
65
65
  mcpToolNames,
66
+ structuredOutputToolNames = new Set(),
66
67
  emit,
67
68
  }: {
68
69
  state: DeepAgentsStreamEventState;
69
70
  configuredModel: string | undefined;
70
71
  hostToolNames: ReadonlySet<string>;
71
72
  mcpToolNames: ReadonlySet<string>;
73
+ structuredOutputToolNames?: ReadonlySet<string>;
72
74
  emit: Emit;
73
75
  }): (event: DeepAgentsStreamEvent) => void {
74
76
  return event => {
@@ -173,6 +175,7 @@ export function createEmitStreamEvent({
173
175
  }
174
176
  } else if (kind === 'on_tool_start') {
175
177
  const toolName = event.name ?? 'unknown';
178
+ if (structuredOutputToolNames.has(toolName)) return;
176
179
  const runId = event.run_id ?? '';
177
180
  // Host tools emit their own tool-call; surface only top-level builtin (providerExecuted) tools.
178
181
  if (!nested && !hostToolNames.has(toolName)) {
@@ -199,6 +202,7 @@ export function createEmitStreamEvent({
199
202
  }
200
203
  } else if (kind === 'on_tool_end') {
201
204
  const toolName = event.name ?? 'unknown';
205
+ if (structuredOutputToolNames.has(toolName)) return;
202
206
  const runId = event.run_id ?? '';
203
207
  if (!nested && !hostToolNames.has(toolName)) {
204
208
  const dynamic = state.dynamicToolRunIds.delete(runId);
@@ -15,7 +15,7 @@ import {
15
15
  type ClientConfig,
16
16
  } from '@langchain/mcp-adapters';
17
17
  import { createDeepAgent } from 'deepagents';
18
- import { createMiddleware } from 'langchain';
18
+ import { createMiddleware, toolStrategy } from 'langchain';
19
19
  import type { StartMessage } from '../deepagents-bridge-protocol';
20
20
  import { buildInterruptOn, collectActionRequests } from './approvals';
21
21
  import {
@@ -131,6 +131,30 @@ let agent: ReturnType<typeof createDeepAgent> | undefined;
131
131
  let currentTurn: BridgeTurn | undefined;
132
132
  let mcpClient: MultiServerMCPClient | undefined;
133
133
  let mcpToolNames = new Set<string>();
134
+ let currentResponseFormat: ReturnType<typeof toolStrategy> | undefined;
135
+
136
+ type DeepAgentsJsonSchema = Record<string, unknown> & {
137
+ type:
138
+ | 'null'
139
+ | 'boolean'
140
+ | 'object'
141
+ | 'array'
142
+ | 'number'
143
+ | 'string'
144
+ | 'integer';
145
+ };
146
+
147
+ const responseFormatMiddleware = createMiddleware({
148
+ name: 'HarnessResponseFormat',
149
+ wrapModelCall(request, handler) {
150
+ return handler({
151
+ ...request,
152
+ ...(currentResponseFormat == null
153
+ ? {}
154
+ : { responseFormat: currentResponseFormat }),
155
+ });
156
+ },
157
+ });
134
158
 
135
159
  // Host tools become LangChain tools that emit a `tool-call` and block on the host's `tool-result`.
136
160
  function buildHostTools(toolSchemas: StartMessage['tools']) {
@@ -161,6 +185,10 @@ function buildHostTools(toolSchemas: StartMessage['tools']) {
161
185
 
162
186
  async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
163
187
  currentTurn = turn;
188
+ currentResponseFormat =
189
+ start.responseFormat?.type === 'json' && start.responseFormat.schema != null
190
+ ? toolStrategy(start.responseFormat.schema as DeepAgentsJsonSchema)
191
+ : undefined;
164
192
  const emit = (event: Record<string, unknown>) =>
165
193
  turn.emit(event as BridgeEvent);
166
194
 
@@ -191,6 +219,7 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
191
219
  },
192
220
  );
193
221
  const middleware = [
222
+ responseFormatMiddleware,
194
223
  ...(reasoningMiddleware ? [reasoningMiddleware] : []),
195
224
  ...(builtinToolFilteringMiddleware
196
225
  ? [builtinToolFilteringMiddleware]
@@ -230,6 +259,9 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
230
259
  configuredModel: start.model,
231
260
  hostToolNames,
232
261
  mcpToolNames,
262
+ structuredOutputToolNames: new Set(
263
+ currentResponseFormat?.map(format => format.name) ?? [],
264
+ ),
233
265
  emit,
234
266
  });
235
267
 
@@ -259,12 +291,33 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
259
291
  let resumeInput: unknown = {
260
292
  messages: [{ role: 'user', content: start.prompt }],
261
293
  };
294
+ let emittedStructuredOutput = false;
262
295
 
263
296
  while (true) {
264
297
  const stream = await agent.streamEvents(resumeInput as never, config);
265
298
 
266
299
  for await (const event of stream) {
267
300
  emitStreamEvent(event as DeepAgentsStreamEvent);
301
+ const streamEvent = event as DeepAgentsStreamEvent;
302
+ const namespace = streamEvent.metadata?.langgraph_checkpoint_ns ?? '';
303
+ const output = (streamEvent.data as { output?: unknown } | undefined)
304
+ ?.output as { structuredResponse?: unknown } | undefined;
305
+ if (
306
+ !emittedStructuredOutput &&
307
+ streamEvent.event === 'on_chain_end' &&
308
+ !namespace.includes('|') &&
309
+ output?.structuredResponse !== undefined
310
+ ) {
311
+ const id = `structured-output-${randomUUID()}`;
312
+ emit({ type: 'text-start', id });
313
+ emit({
314
+ type: 'text-delta',
315
+ id,
316
+ delta: JSON.stringify(output.structuredResponse),
317
+ });
318
+ emit({ type: 'text-end', id });
319
+ emittedStructuredOutput = true;
320
+ }
268
321
  }
269
322
 
270
323
  const actionRequests = await readPendingApprovals();
@@ -1,6 +1,44 @@
1
- import { getAiGatewayAuthFromEnv } from '@ai-sdk/harness/utils';
1
+ import type { HarnessV1RequestTransformation } from '@ai-sdk/harness';
2
+ import {
3
+ createCredentialRequestTransformation,
4
+ getAiGatewayAuthFromEnv,
5
+ } from '@ai-sdk/harness/utils';
2
6
 
3
- export type DeepAgentsAuthenticationMode = 'auto' | 'anthropic' | 'ai-gateway';
7
+ export const DEEPAGENTS_CREDENTIAL_ENVIRONMENT_VARIABLES = [
8
+ 'AI_GATEWAY_API_KEY',
9
+ 'ANTHROPIC_API_KEY',
10
+ 'ANTHROPIC_AUTH_TOKEN',
11
+ ] as const;
12
+
13
+ export function createDeepAgentsRequestTransformations(
14
+ env: Record<string, string>,
15
+ auth: DeepAgentsResolvedAuthenticationMode,
16
+ ): HarnessV1RequestTransformation[] {
17
+ const headers: Record<string, string> = {};
18
+ if (env.ANTHROPIC_API_KEY) {
19
+ headers['x-api-key'] = env.ANTHROPIC_API_KEY;
20
+ }
21
+ if (env.ANTHROPIC_AUTH_TOKEN) {
22
+ headers.Authorization = `Bearer ${env.ANTHROPIC_AUTH_TOKEN}`;
23
+ }
24
+ return Object.keys(headers).length === 0
25
+ ? []
26
+ : [
27
+ createCredentialRequestTransformation({
28
+ baseUrl:
29
+ auth === 'ai-gateway'
30
+ ? env.ANTHROPIC_BASE_URL
31
+ : (env.ANTHROPIC_BASE_URL ?? 'https://api.anthropic.com'),
32
+ headers,
33
+ }),
34
+ ];
35
+ }
36
+
37
+ export type DeepAgentsResolvedAuthenticationMode = 'anthropic' | 'ai-gateway';
38
+
39
+ export type DeepAgentsAuthenticationMode =
40
+ | DeepAgentsResolvedAuthenticationMode
41
+ | 'auto';
4
42
 
5
43
  /**
6
44
  * @deprecated Passing an object to auth options is deprecated. Use a `DeepAgentsAuthenticationMode` string value ("auto" | "anthropic" | "ai-gateway") instead, and pass credentials via environment variables.
@@ -51,6 +89,24 @@ export function resolveDeepAgentsEnv({
51
89
  return pickAnthropic({ processEnv });
52
90
  }
53
91
 
92
+ export function resolveDeepAgentsAuthenticationMode({
93
+ auth,
94
+ processEnv = process.env,
95
+ }: {
96
+ auth?: DeepAgentsAuthOptions;
97
+ processEnv?: Record<string, string | undefined>;
98
+ }): DeepAgentsResolvedAuthenticationMode {
99
+ if (auth === 'anthropic' || (typeof auth !== 'string' && auth?.anthropic)) {
100
+ return 'anthropic';
101
+ }
102
+ if (auth === 'ai-gateway' || (typeof auth !== 'string' && auth?.gateway)) {
103
+ return 'ai-gateway';
104
+ }
105
+ return getAiGatewayAuthFromEnv({ env: processEnv }).apiKey
106
+ ? 'ai-gateway'
107
+ : 'anthropic';
108
+ }
109
+
54
110
  function normalizeDeepAgentsAuthToLegacyAuth(
55
111
  auth: DeepAgentsAuthOptions | undefined,
56
112
  ): LegacyDeepAgentsAuthOptions | undefined {
@@ -0,0 +1,87 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { fileURLToPath } from 'node:url';
3
+ import type { HarnessV1Bootstrap } from '@ai-sdk/harness';
4
+
5
+ /*
6
+ * Bootstrap is derived state stored under the sandbox's default working
7
+ * directory so snapshot-capable providers preserve its installation and
8
+ * recipe marker without requiring root filesystem access.
9
+ */
10
+ export const DEEPAGENTS_BOOTSTRAP_DIR = '.harness-bootstrap/deepagents';
11
+
12
+ /*
13
+ * Pinned ripgrep release and per-architecture tarball checksums are verified
14
+ * before installation.
15
+ */
16
+ const RIPGREP_VERSION = '14.1.1';
17
+ const RIPGREP_SHA256_X64 =
18
+ '4cf9f2741e6c465ffdb7c26f38056a59e2a2544b51f7cc128ef28337eeae4d8e';
19
+ const RIPGREP_SHA256_ARM =
20
+ 'c827481c4ff4ea10c9dc7a4022c8de5db34a5737cb74484d62eb94a95841ab2f';
21
+
22
+ let cachedBootstrap: HarnessV1Bootstrap | undefined;
23
+
24
+ export async function getDeepAgentsBootstrap(): Promise<HarnessV1Bootstrap> {
25
+ if (cachedBootstrap != null) return cachedBootstrap;
26
+ const [bridge, pkg, lock] = await Promise.all([
27
+ readBridgeAsset('index.mjs'),
28
+ readBridgeAsset('package.json'),
29
+ readBridgeAsset('pnpm-lock.yaml'),
30
+ ]);
31
+ cachedBootstrap = {
32
+ harnessId: 'deepagents',
33
+ bootstrapDir: DEEPAGENTS_BOOTSTRAP_DIR,
34
+ files: [
35
+ { path: `${DEEPAGENTS_BOOTSTRAP_DIR}/bridge.mjs`, content: bridge },
36
+ { path: `${DEEPAGENTS_BOOTSTRAP_DIR}/package.json`, content: pkg },
37
+ { path: `${DEEPAGENTS_BOOTSTRAP_DIR}/pnpm-lock.yaml`, content: lock },
38
+ ],
39
+ commands: [
40
+ { command: installRipgrepCommand() },
41
+ {
42
+ command: 'pnpm install --frozen-lockfile --store-dir .pnpm-store',
43
+ },
44
+ ],
45
+ };
46
+ return cachedBootstrap;
47
+ }
48
+
49
+ /*
50
+ * DeepAgents' grep shells out to `rg`. Without it, the fallback reads the
51
+ * entire workdir, including node_modules, into memory and can run out of
52
+ * memory. The checksum-verified installation is skipped when `rg` exists.
53
+ */
54
+ function installRipgrepCommand(): string {
55
+ const v = RIPGREP_VERSION;
56
+ return [
57
+ 'command -v rg >/dev/null 2>&1 || {',
58
+ 'case "$(uname -m)" in',
59
+ `aarch64) a=aarch64-unknown-linux-gnu; sha=${RIPGREP_SHA256_ARM} ;;`,
60
+ `*) a=x86_64-unknown-linux-musl; sha=${RIPGREP_SHA256_X64} ;;`,
61
+ 'esac;',
62
+ `f=/tmp/ripgrep-${v}.tar.gz;`,
63
+ `curl -fsSL "https://github.com/BurntSushi/ripgrep/releases/download/${v}/ripgrep-${v}-$a.tar.gz" -o "$f"`,
64
+ '&& echo "$sha $f" | sha256sum -c -',
65
+ '&& tar xzf "$f" -C /tmp',
66
+ `&& mv "/tmp/ripgrep-${v}-$a/rg" /usr/local/bin/rg && chmod +x /usr/local/bin/rg;`,
67
+ '}',
68
+ ].join(' ');
69
+ }
70
+
71
+ async function readBridgeAsset(name: string): Promise<string> {
72
+ const candidates = [
73
+ new URL(`./bridge/${name}`, import.meta.url),
74
+ new URL(`../bridge/${name}`, import.meta.url),
75
+ ];
76
+ let lastErr: unknown;
77
+ for (const url of candidates) {
78
+ try {
79
+ return await readFile(fileURLToPath(url), 'utf8');
80
+ } catch (err) {
81
+ const code = (err as NodeJS.ErrnoException).code;
82
+ if (code !== 'ENOENT') throw err;
83
+ lastErr = err;
84
+ }
85
+ }
86
+ throw lastErr ?? new Error(`bridge asset not found: ${name}`);
87
+ }
@@ -1,13 +1,10 @@
1
1
  import { randomBytes } from 'node:crypto';
2
- import { readFile } from 'node:fs/promises';
3
2
  import { posix } from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
3
  import {
6
4
  commonTool,
7
5
  HarnessCapabilityUnsupportedError,
8
6
  harnessV1DiagnosticFromBridgeFrame,
9
7
  type HarnessV1,
10
- type HarnessV1Bootstrap,
11
8
  type HarnessV1BuiltinTool,
12
9
  type HarnessV1BuiltinToolFiltering,
13
10
  type HarnessV1ContinueTurnState,
@@ -15,6 +12,7 @@ import {
15
12
  type HarnessV1PermissionMode,
16
13
  type HarnessV1Prompt,
17
14
  type HarnessV1PromptControl,
15
+ type HarnessV1PortEndpoint,
18
16
  type HarnessV1ResumeSessionState,
19
17
  type HarnessV1Session,
20
18
  type HarnessV1Skill,
@@ -26,9 +24,11 @@ import {
26
24
  createBridgeStartupError,
27
25
  drainBridgeProcessStream,
28
26
  forwardBridgeProcessStream,
27
+ maskSandboxCredentials,
29
28
  resolveSandboxHomeDir,
30
29
  SandboxChannel,
31
30
  shellQuote,
31
+ warnCredentialBrokeringUnavailable,
32
32
  waitForBridgeReady,
33
33
  writeSkills as writeHarnessSkills,
34
34
  } from '@ai-sdk/harness/utils';
@@ -36,6 +36,13 @@ import { tool, type Experimental_SandboxProcess } from '@ai-sdk/provider-utils';
36
36
  import { WebSocket } from 'ws';
37
37
  import { z } from 'zod/v4';
38
38
  import {
39
+ DEEPAGENTS_BOOTSTRAP_DIR as BOOTSTRAP_DIR,
40
+ getDeepAgentsBootstrap,
41
+ } from './deepagents-bootstrap';
42
+ import {
43
+ createDeepAgentsRequestTransformations,
44
+ DEEPAGENTS_CREDENTIAL_ENVIRONMENT_VARIABLES,
45
+ resolveDeepAgentsAuthenticationMode,
39
46
  resolveDeepAgentsEnv,
40
47
  type DeepAgentsAuthOptions,
41
48
  } from './deepagents-auth';
@@ -48,12 +55,6 @@ import { VERSION } from './version';
48
55
 
49
56
  type DeepAgentsChannel = SandboxChannel<OutboundMessage, InboundMessage>;
50
57
 
51
- /*
52
- * Bootstrap is derived state stored under the sandbox's default working
53
- * directory so snapshot-capable providers preserve its installation and
54
- * recipe marker without requiring root filesystem access.
55
- */
56
- const BOOTSTRAP_DIR = '.harness-bootstrap/deepagents';
57
58
  /**
58
59
  * Value to use in User-Agent and `x-client-app` headers.
59
60
  */
@@ -73,32 +74,6 @@ export type DeepAgentsThinkingConfig =
73
74
  readonly type: 'disabled';
74
75
  };
75
76
 
76
- // Pinned ripgrep release + per-arch tarball checksums (verified before install).
77
- const RIPGREP_VERSION = '14.1.1';
78
- const RIPGREP_SHA256_X64 =
79
- '4cf9f2741e6c465ffdb7c26f38056a59e2a2544b51f7cc128ef28337eeae4d8e';
80
- const RIPGREP_SHA256_ARM =
81
- 'c827481c4ff4ea10c9dc7a4022c8de5db34a5737cb74484d62eb94a95841ab2f';
82
-
83
- // Idempotent, checksum-verified install of a static ripgrep binary into a PATH dir.
84
- // DeepAgents' grep shells out to `rg`; without it, its fallback reads the whole workdir (incl. node_modules) into memory and OOMs. Skipped if `rg` already exists.
85
- function installRipgrepCommand(): string {
86
- const v = RIPGREP_VERSION;
87
- return [
88
- 'command -v rg >/dev/null 2>&1 || {',
89
- 'case "$(uname -m)" in',
90
- `aarch64) a=aarch64-unknown-linux-gnu; sha=${RIPGREP_SHA256_ARM} ;;`,
91
- `*) a=x86_64-unknown-linux-musl; sha=${RIPGREP_SHA256_X64} ;;`,
92
- 'esac;',
93
- `f=/tmp/ripgrep-${v}.tar.gz;`,
94
- `curl -fsSL "https://github.com/BurntSushi/ripgrep/releases/download/${v}/ripgrep-${v}-$a.tar.gz" -o "$f"`,
95
- '&& echo "$sha $f" | sha256sum -c -',
96
- '&& tar xzf "$f" -C /tmp',
97
- `&& mv "/tmp/ripgrep-${v}-$a/rg" /usr/local/bin/rg && chmod +x /usr/local/bin/rg;`,
98
- '}',
99
- ].join(' ');
100
- }
101
-
102
77
  // 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 <workDir> for repo-provided skills.
103
78
  const SKILLS_SOURCE_PATH = '/.agents/skills';
104
79
 
@@ -212,8 +187,6 @@ type DeepAgentsBridgeCoords = z.infer<typeof deepAgentsBridgeCoordsSchema>;
212
187
  export function createDeepAgents(
213
188
  settings: DeepAgentsHarnessSettings = {},
214
189
  ): HarnessV1<typeof DEEPAGENTS_BUILTIN_TOOLS> {
215
- let cachedBootstrap: HarnessV1Bootstrap | undefined;
216
-
217
190
  return {
218
191
  specificationVersion: 'harness-v1',
219
192
  harnessId: 'deepagents',
@@ -221,33 +194,35 @@ export function createDeepAgents(
221
194
  // Built-in tool approvals are gated in-bridge via DeepAgents' interruptOn (HITL) middleware.
222
195
  supportsBuiltinToolApprovals: true,
223
196
  lifecycleStateSchema: deepAgentsResumeStateSchema,
224
- getBootstrap: async () => {
225
- if (cachedBootstrap != null) return cachedBootstrap;
226
- const [bridge, pkg, lock] = await Promise.all([
227
- readBridgeAsset('index.mjs'),
228
- readBridgeAsset('package.json'),
229
- readBridgeAsset('pnpm-lock.yaml'),
230
- ]);
231
- cachedBootstrap = {
232
- harnessId: 'deepagents',
233
- bootstrapDir: BOOTSTRAP_DIR,
234
- files: [
235
- { path: `${BOOTSTRAP_DIR}/bridge.mjs`, content: bridge },
236
- { path: `${BOOTSTRAP_DIR}/package.json`, content: pkg },
237
- { path: `${BOOTSTRAP_DIR}/pnpm-lock.yaml`, content: lock },
238
- ],
239
- commands: [
240
- { command: installRipgrepCommand() },
241
- {
242
- command: 'pnpm install --frozen-lockfile --store-dir .pnpm-store',
243
- },
244
- ],
245
- };
246
- return cachedBootstrap;
247
- },
197
+ getBootstrap: getDeepAgentsBootstrap,
248
198
  doStart: async startOpts => {
249
199
  const permissionMode = startOpts.permissionMode;
250
200
  const sandboxSession = startOpts.sandboxSession;
201
+ const authenticationMode = resolveDeepAgentsAuthenticationMode({
202
+ auth: settings.auth,
203
+ });
204
+ const resolvedAuthEnvironment = resolveDeepAgentsEnv({
205
+ auth: settings.auth,
206
+ });
207
+ let sandboxAuthEnvironment = resolvedAuthEnvironment;
208
+ if (sandboxSession.addRequestTransformations != null) {
209
+ const requestTransformations = createDeepAgentsRequestTransformations(
210
+ resolvedAuthEnvironment,
211
+ authenticationMode,
212
+ );
213
+ if (requestTransformations.length > 0) {
214
+ await sandboxSession.addRequestTransformations(
215
+ requestTransformations,
216
+ );
217
+ }
218
+ sandboxAuthEnvironment = maskSandboxCredentials({
219
+ environment: resolvedAuthEnvironment,
220
+ credentialEnvironmentVariables:
221
+ DEEPAGENTS_CREDENTIAL_ENVIRONMENT_VARIABLES,
222
+ });
223
+ } else {
224
+ warnCredentialBrokeringUnavailable();
225
+ }
251
226
  const session = sandboxSession.restricted();
252
227
  const sandboxId = sandboxSession.id;
253
228
  const bootstrapDir = posix.resolve(
@@ -286,13 +261,16 @@ export function createDeepAgents(
286
261
  // Attach to the still-running bridge (continueFrom replays past the cursor); on failure fall through to a fresh spawn.
287
262
  if (coords) {
288
263
  try {
289
- const attachUrl =
290
- (await sandboxSession.getPortUrl({
291
- port: coords.port,
292
- protocol: 'ws',
293
- })) + `?agent_bridge_token=${encodeURIComponent(coords.token)}`;
264
+ const endpoint = await sandboxSession.getPortEndpoint({
265
+ port: coords.port,
266
+ protocol: 'ws',
267
+ });
268
+ const attachEndpoint = withBridgeToken({
269
+ endpoint,
270
+ token: coords.token,
271
+ });
294
272
  const attachChannel: DeepAgentsChannel = new SandboxChannel({
295
- connect: () => openWebSocket(attachUrl),
273
+ connect: () => openWebSocket(attachEndpoint),
296
274
  outboundSchema: outboundMessageSchema,
297
275
  initialLastSeenEventId: coords.lastSeenEventId,
298
276
  onDiagnostic,
@@ -347,7 +325,7 @@ export function createDeepAgents(
347
325
  }
348
326
 
349
327
  const env = {
350
- ...resolveDeepAgentsEnv({ auth: settings.auth }),
328
+ ...sandboxAuthEnvironment,
351
329
  AI_SDK_HARNESS_CLIENT_APP: DEEPAGENTS_CLIENT_APP,
352
330
  BRIDGE_CHANNEL_TOKEN: token,
353
331
  BRIDGE_WS_PORT: String(port),
@@ -404,14 +382,14 @@ export function createDeepAgents(
404
382
  });
405
383
  void drainBridgeProcessStream(proc.stdout);
406
384
 
407
- const wsUrl =
408
- (await sandboxSession.getPortUrl({
409
- port: boundPort,
410
- protocol: 'ws',
411
- })) + `?agent_bridge_token=${encodeURIComponent(token)}`;
385
+ const endpoint = await sandboxSession.getPortEndpoint({
386
+ port: boundPort,
387
+ protocol: 'ws',
388
+ });
389
+ const bridgeEndpoint = withBridgeToken({ endpoint, token });
412
390
 
413
391
  const channel: DeepAgentsChannel = new SandboxChannel({
414
- connect: () => openWebSocket(wsUrl),
392
+ connect: () => openWebSocket(bridgeEndpoint),
415
393
  outboundSchema: outboundMessageSchema,
416
394
  onDiagnostic,
417
395
  onBridgeError,
@@ -455,24 +433,6 @@ function resolveBridgePort(
455
433
  });
456
434
  }
457
435
 
458
- async function readBridgeAsset(name: string): Promise<string> {
459
- const candidates = [
460
- new URL(`./bridge/${name}`, import.meta.url),
461
- new URL(`../bridge/${name}`, import.meta.url),
462
- ];
463
- let lastErr: unknown;
464
- for (const url of candidates) {
465
- try {
466
- return await readFile(fileURLToPath(url), 'utf8');
467
- } catch (err) {
468
- const code = (err as NodeJS.ErrnoException).code;
469
- if (code !== 'ENOENT') throw err;
470
- lastErr = err;
471
- }
472
- }
473
- throw lastErr ?? new Error(`bridge asset not found: ${name}`);
474
- }
475
-
476
436
  // Materialize each skill as a native deepagents `<name>/SKILL.md` folder (+ attached files) under the given root, so skills load on demand and file references resolve.
477
437
  async function writeSkills({
478
438
  sandbox,
@@ -503,9 +463,14 @@ async function writeSkills({
503
463
  });
504
464
  }
505
465
 
506
- function openWebSocket(url: string): Promise<WebSocket> {
466
+ function openWebSocket({
467
+ url,
468
+ headers,
469
+ }: HarnessV1PortEndpoint): Promise<WebSocket> {
507
470
  return new Promise((resolve, reject) => {
508
- const ws = new WebSocket(url);
471
+ const ws = new WebSocket(url, {
472
+ headers: headers == null ? undefined : { ...headers },
473
+ });
509
474
  const onOpen = () => {
510
475
  ws.off('error', onError);
511
476
  resolve(ws);
@@ -519,6 +484,18 @@ function openWebSocket(url: string): Promise<WebSocket> {
519
484
  });
520
485
  }
521
486
 
487
+ function withBridgeToken({
488
+ endpoint,
489
+ token,
490
+ }: {
491
+ endpoint: HarnessV1PortEndpoint;
492
+ token: string;
493
+ }): HarnessV1PortEndpoint {
494
+ const bridgeUrl = new URL(endpoint.url);
495
+ bridgeUrl.searchParams.set('agent_bridge_token', token);
496
+ return { ...endpoint, url: bridgeUrl.toString() };
497
+ }
498
+
522
499
  function createSession({
523
500
  sessionId,
524
501
  channel,
@@ -691,6 +668,16 @@ function createSession({
691
668
  isResume,
692
669
  modelId: model,
693
670
  doPromptTurn: async promptOpts => {
671
+ if (
672
+ promptOpts.responseFormat?.type === 'json' &&
673
+ promptOpts.responseFormat.schema == null
674
+ ) {
675
+ throw new HarnessCapabilityUnsupportedError({
676
+ message:
677
+ "Harness 'deepagents' requires a JSON schema for structured output.",
678
+ harnessId: 'deepagents',
679
+ });
680
+ }
694
681
  const control = wireTurn({
695
682
  emit: promptOpts.emit,
696
683
  abortSignal: promptOpts.abortSignal,
@@ -709,6 +696,9 @@ function createSession({
709
696
  description: t.description,
710
697
  inputSchema: t.inputSchema,
711
698
  })),
699
+ ...(promptOpts.responseFormat == null
700
+ ? {}
701
+ : { responseFormat: promptOpts.responseFormat }),
712
702
  ...(model ? { model } : {}),
713
703
  ...(thinking ? { thinking } : {}),
714
704
  ...(effort ? { effort } : {}),
@@ -722,6 +712,16 @@ function createSession({
722
712
  return control;
723
713
  },
724
714
  doContinueTurn: async continueOpts => {
715
+ if (
716
+ continueOpts.responseFormat?.type === 'json' &&
717
+ continueOpts.responseFormat.schema == null
718
+ ) {
719
+ throw new HarnessCapabilityUnsupportedError({
720
+ message:
721
+ "Harness 'deepagents' requires a JSON schema for structured output.",
722
+ harnessId: 'deepagents',
723
+ });
724
+ }
725
725
  // Attach/replay: doStart opened with `{ resume: true }` so the bridge replays past the cursor; no `start` is sent (that would clear the replay log).
726
726
  return wireTurn({
727
727
  emit: continueOpts.emit,