@ai-sdk/harness-deepagents 0.0.0-6b196531-20260710185421

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.
@@ -0,0 +1,443 @@
1
+ // 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.
2
+
3
+ import { randomUUID } from 'node:crypto';
4
+ import { argv, env as procEnv } from 'node:process';
5
+ import {
6
+ runBridge,
7
+ type BridgeEvent,
8
+ type BridgeTurn,
9
+ } from '@ai-sdk/harness/bridge';
10
+ import { ChatAnthropic } from '@langchain/anthropic';
11
+ import { tool } from '@langchain/core/tools';
12
+ import { Command, MemorySaver } from '@langchain/langgraph';
13
+ import { createDeepAgent } from 'deepagents';
14
+ import type { StartMessage } from '../deepagents-bridge-protocol';
15
+ import { buildInterruptOn, collectActionRequests } from './approvals';
16
+ import { jsonSchemaToZodObject } from './json-schema-to-zod';
17
+ import { createLocalShellBackend } from './local-shell-backend';
18
+ import { createBuiltinToolFilteringMiddleware } from './tool-filtering';
19
+
20
+ // Native Deep Agents tool name -> harness-v1 common name (renames only; grep/glob/ls/task/write_todos forward unchanged).
21
+ const NATIVE_TO_COMMON: Readonly<Record<string, string>> = {
22
+ read_file: 'read',
23
+ write_file: 'write',
24
+ edit_file: 'edit',
25
+ execute: 'bash',
26
+ };
27
+ const HARNESS_CLIENT_APP = procEnv.AI_SDK_HARNESS_CLIENT_APP;
28
+
29
+ function toCommonName(nativeName: string): string {
30
+ return NATIVE_TO_COMMON[nativeName] ?? nativeName;
31
+ }
32
+
33
+ function parseArgs(rawArgs: string[]): Record<string, string> {
34
+ const out: Record<string, string> = {};
35
+ for (let i = 0; i < rawArgs.length; i++) {
36
+ const arg = rawArgs[i];
37
+ if (arg.startsWith('--')) {
38
+ const key = arg
39
+ .slice(2)
40
+ .replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
41
+ out[key] = rawArgs[i + 1];
42
+ i++;
43
+ }
44
+ }
45
+ return out;
46
+ }
47
+
48
+ // Always drive the Anthropic client. Through the gateway, models keep their
49
+ // `creator/model` slug (gateway translates); direct Anthropic wants the bare id.
50
+ function buildModel(rawModel: string | undefined) {
51
+ if (!rawModel) return undefined;
52
+ const baseUrl = procEnv.ANTHROPIC_BASE_URL;
53
+ const model = baseUrl ? rawModel : rawModel.replace(/^anthropic[/:]/, '');
54
+ return new ChatAnthropic({
55
+ model,
56
+ ...(procEnv.ANTHROPIC_API_KEY ? { apiKey: procEnv.ANTHROPIC_API_KEY } : {}),
57
+ ...(baseUrl ? { anthropicApiUrl: baseUrl } : {}),
58
+ ...(procEnv.AI_GATEWAY_API_KEY && HARNESS_CLIENT_APP
59
+ ? {
60
+ clientOptions: {
61
+ defaultHeaders: {
62
+ 'User-Agent': HARNESS_CLIENT_APP,
63
+ 'x-client-app': HARNESS_CLIENT_APP,
64
+ },
65
+ },
66
+ }
67
+ : {}),
68
+ });
69
+ }
70
+
71
+ // LangChain reports some built-in tool args wrapped as `{ input: "<json>" }`; unwrap to the inner JSON so AI SDK validates the real shape.
72
+ function toToolCallInput(raw: unknown): string {
73
+ if (
74
+ raw &&
75
+ typeof raw === 'object' &&
76
+ !Array.isArray(raw) &&
77
+ Object.keys(raw).length === 1 &&
78
+ typeof (raw as { input?: unknown }).input === 'string'
79
+ ) {
80
+ const inner = (raw as { input: string }).input;
81
+ if (/^\s*[[{]/.test(inner)) return inner;
82
+ }
83
+ return JSON.stringify(raw ?? {});
84
+ }
85
+
86
+ const args = parseArgs(argv.slice(2));
87
+ const workdir = args.workdir;
88
+ const bridgeStateDir = args.bridgeStateDir;
89
+ if (!workdir || !bridgeStateDir) {
90
+ // eslint-disable-next-line no-console
91
+ console.error('deepagents bridge: missing --workdir / --bridge-state-dir');
92
+ process.exit(1);
93
+ }
94
+
95
+ // One agent per bridge process, reused across turns; host tools read the live turn via `currentTurn`.
96
+ let agent: ReturnType<typeof createDeepAgent> | undefined;
97
+ let currentTurn: BridgeTurn | undefined;
98
+
99
+ // Host tools become LangChain tools that emit a `tool-call` and block on the host's `tool-result`.
100
+ function buildHostTools(toolSchemas: StartMessage['tools']) {
101
+ return (toolSchemas ?? []).map(schema =>
102
+ tool(
103
+ async (input: Record<string, unknown>) => {
104
+ const turn = currentTurn;
105
+ if (!turn) throw new Error('no active turn');
106
+ const toolCallId = `${schema.name}-${randomUUID()}`;
107
+ turn.emit({
108
+ type: 'tool-call',
109
+ toolCallId,
110
+ toolName: schema.name,
111
+ input: JSON.stringify(input),
112
+ providerExecuted: false,
113
+ } as BridgeEvent);
114
+ const { output } = await turn.requestToolResult(toolCallId);
115
+ return typeof output === 'string' ? output : JSON.stringify(output);
116
+ },
117
+ {
118
+ name: schema.name,
119
+ description: schema.description ?? '',
120
+ schema: jsonSchemaToZodObject(schema.inputSchema),
121
+ },
122
+ ),
123
+ );
124
+ }
125
+
126
+ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
127
+ currentTurn = turn;
128
+ const emit = (event: Record<string, unknown>) =>
129
+ turn.emit(event as BridgeEvent);
130
+
131
+ const interruptOn = buildInterruptOn(
132
+ start.permissionMode,
133
+ start.builtinToolFiltering,
134
+ );
135
+ if (!agent) {
136
+ const model = buildModel(start.model);
137
+ const builtinToolFilteringMiddleware = createBuiltinToolFilteringMiddleware(
138
+ {
139
+ builtinToolFiltering: start.builtinToolFiltering,
140
+ emit: event => {
141
+ const turn = currentTurn;
142
+ if (!turn) throw new Error('no active turn');
143
+ turn.emit(event as BridgeEvent);
144
+ },
145
+ },
146
+ );
147
+ agent = createDeepAgent({
148
+ // Defer to Deep Agents's own default when the host configured no model.
149
+ ...(model ? { model } : {}),
150
+ tools: buildHostTools(start.tools),
151
+ backend: createLocalShellBackend({ rootDir: workdir }),
152
+ systemPrompt: start.instructions || undefined,
153
+ // Native skills loaded from the source dirs ($HOME-materialized + <workDir> for repo-provided skills).
154
+ ...(start.skillsPaths?.length ? { skills: start.skillsPaths } : {}),
155
+ ...(builtinToolFilteringMiddleware
156
+ ? { middleware: [builtinToolFilteringMiddleware] }
157
+ : {}),
158
+ // Gate built-in tools behind HITL approval when the permission mode requires it.
159
+ ...(interruptOn ? { interruptOn } : {}),
160
+ // Real instance (LangGraph rejects `true` for root graphs); gives multi-turn memory.
161
+ checkpointer: new MemorySaver(),
162
+ });
163
+ }
164
+
165
+ emit({
166
+ type: 'stream-start',
167
+ ...(start.model ? { modelId: start.model } : {}),
168
+ });
169
+
170
+ const hostToolNames = new Set((start.tools ?? []).map(t => t.name));
171
+ let textBlockId: string | undefined;
172
+ let reasoningBlockId: string | undefined;
173
+ let inputTokens = 0;
174
+ let outputTokens = 0;
175
+ // Per-call streamed-usage fallback (max over chunks), used only when model-end carries no usage.
176
+ let streamedStepInput = 0;
177
+ let streamedStepOutput = 0;
178
+ // Top-level step usage is buffered at model-end and flushed as finish-step only after the step's tools run.
179
+ let pendingStep: { input: number; output: number } | undefined;
180
+ // Approval-gated tools are announced before execution; these tie the later run back to the approval id and dedup the call.
181
+ const approvedToolQueue = new Map<string, string[]>();
182
+ const approvedRunIds = new Map<string, string>();
183
+
184
+ const ensureTextBlock = (): string => {
185
+ if (!textBlockId) {
186
+ textBlockId = `text-${randomUUID()}`;
187
+ emit({ type: 'text-start', id: textBlockId });
188
+ }
189
+ return textBlockId;
190
+ };
191
+ const endTextBlock = () => {
192
+ if (textBlockId) {
193
+ emit({ type: 'text-end', id: textBlockId });
194
+ textBlockId = undefined;
195
+ }
196
+ };
197
+ const endReasoningBlock = () => {
198
+ if (reasoningBlockId) {
199
+ emit({ type: 'reasoning-end', id: reasoningBlockId });
200
+ reasoningBlockId = undefined;
201
+ }
202
+ };
203
+ // Text and reasoning are mutually exclusive open blocks: starting one closes the other.
204
+ const emitText = (delta: string) => {
205
+ endReasoningBlock();
206
+ emit({ type: 'text-delta', id: ensureTextBlock(), delta });
207
+ };
208
+ const emitReasoning = (delta: string) => {
209
+ endTextBlock();
210
+ if (!reasoningBlockId) {
211
+ reasoningBlockId = `reasoning-${randomUUID()}`;
212
+ emit({ type: 'reasoning-start', id: reasoningBlockId });
213
+ }
214
+ emit({ type: 'reasoning-delta', id: reasoningBlockId, delta });
215
+ };
216
+ // 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.
217
+ const flushStep = () => {
218
+ if (!pendingStep) return;
219
+ emit({
220
+ type: 'finish-step',
221
+ finishReason: { unified: 'stop' },
222
+ usage: {
223
+ inputTokens: { total: pendingStep.input },
224
+ outputTokens: { total: pendingStep.output },
225
+ },
226
+ });
227
+ pendingStep = undefined;
228
+ };
229
+
230
+ const config = {
231
+ version: 'v2' as const,
232
+ configurable: { thread_id: 'bridge-session' },
233
+ recursionLimit: start.recursionLimit ?? 100,
234
+ signal: turn.abortSignal,
235
+ };
236
+
237
+ // After a stream segment ends, return the tool calls paused by HITL interrupts (empty when the turn is truly done).
238
+ const readPendingApprovals = async () => {
239
+ try {
240
+ const state = (await agent!.getState({
241
+ configurable: { thread_id: 'bridge-session' },
242
+ })) as { tasks?: Array<{ interrupts?: Array<{ value?: unknown }> }> };
243
+ return collectActionRequests(
244
+ (state.tasks ?? []).flatMap(t => t.interrupts ?? []),
245
+ );
246
+ } catch {
247
+ return [];
248
+ }
249
+ };
250
+
251
+ let resumeInput: unknown = {
252
+ messages: [{ role: 'user', content: start.prompt }],
253
+ };
254
+
255
+ while (true) {
256
+ const stream = await agent.streamEvents(resumeInput as never, config);
257
+
258
+ for await (const event of stream) {
259
+ const kind = event.event;
260
+ const data = (event.data ?? {}) as Record<string, unknown>;
261
+ // Subagent (e.g. `task`) events carry a `|`-delimited checkpoint namespace; keep their internals out of the top-level stream.
262
+ const ns =
263
+ (event as { metadata?: { langgraph_checkpoint_ns?: string } }).metadata
264
+ ?.langgraph_checkpoint_ns ?? '';
265
+ const nested = ns.includes('|');
266
+
267
+ if (kind === 'on_chat_model_start') {
268
+ // A new top-level model call means the previous step's tools have run; close it now.
269
+ if (!nested) flushStep();
270
+ } else if (kind === 'on_chat_model_stream') {
271
+ if (nested) continue;
272
+ const chunk = data.chunk as
273
+ | {
274
+ content?: unknown;
275
+ usage_metadata?: {
276
+ input_tokens?: number;
277
+ output_tokens?: number;
278
+ };
279
+ }
280
+ | undefined;
281
+ if (!chunk) continue;
282
+ const content = chunk.content;
283
+ if (typeof content === 'string' && content) {
284
+ emitText(content);
285
+ } else if (Array.isArray(content)) {
286
+ for (const block of content) {
287
+ if (block && typeof block === 'object') {
288
+ const b = block as {
289
+ type?: string;
290
+ text?: string;
291
+ thinking?: string;
292
+ };
293
+ if (b.type === 'text' && b.text) emitText(b.text);
294
+ else if (b.type === 'thinking' && b.thinking)
295
+ emitReasoning(b.thinking);
296
+ }
297
+ }
298
+ }
299
+ const usage = chunk.usage_metadata;
300
+ if (usage) {
301
+ streamedStepInput = Math.max(
302
+ streamedStepInput,
303
+ usage.input_tokens ?? 0,
304
+ );
305
+ streamedStepOutput = Math.max(
306
+ streamedStepOutput,
307
+ usage.output_tokens ?? 0,
308
+ );
309
+ }
310
+ } else if (kind === 'on_chat_model_end') {
311
+ // Final usage lands on model-end, not the chunks; each model call is one step.
312
+ const output = data.output as
313
+ | {
314
+ usage_metadata?: {
315
+ input_tokens?: number;
316
+ output_tokens?: number;
317
+ };
318
+ }
319
+ | undefined;
320
+ const usage = output?.usage_metadata;
321
+ // One model call = one step; count its usage exactly once (model-end usage, else the streamed max).
322
+ const stepInput = usage?.input_tokens ?? streamedStepInput;
323
+ const stepOutput = usage?.output_tokens ?? streamedStepOutput;
324
+ inputTokens += stepInput;
325
+ outputTokens += stepOutput;
326
+ streamedStepInput = 0;
327
+ streamedStepOutput = 0;
328
+ // Nested (subagent) calls still count toward total usage, but only top-level calls bound a visible step.
329
+ if (!nested) {
330
+ endTextBlock();
331
+ endReasoningBlock();
332
+ // Buffer the step; flushStep emits finish-step after this step's tools run (next start / turn end).
333
+ pendingStep = { input: stepInput, output: stepOutput };
334
+ }
335
+ } else if (kind === 'on_tool_start') {
336
+ const toolName = (event.name as string) ?? 'unknown';
337
+ const runId = (event.run_id as string) ?? '';
338
+ // Host tools emit their own tool-call; surface only top-level builtin (providerExecuted) tools.
339
+ if (!nested && !hostToolNames.has(toolName)) {
340
+ const queued = approvedToolQueue.get(toolName);
341
+ if (queued && queued.length > 0) {
342
+ // Already announced at approval time; tie this run to that id and don't re-emit the call.
343
+ const approvalId = queued.shift()!;
344
+ if (runId) approvedRunIds.set(runId, approvalId);
345
+ } else {
346
+ endTextBlock();
347
+ endReasoningBlock();
348
+ emit({
349
+ type: 'tool-call',
350
+ toolCallId: runId,
351
+ toolName: toCommonName(toolName),
352
+ input: toToolCallInput(data.input),
353
+ providerExecuted: true,
354
+ nativeName: toolName,
355
+ });
356
+ }
357
+ }
358
+ } else if (kind === 'on_tool_end') {
359
+ const toolName = (event.name as string) ?? 'unknown';
360
+ const runId = (event.run_id as string) ?? '';
361
+ if (!nested && !hostToolNames.has(toolName)) {
362
+ let output: unknown = data.output ?? '';
363
+ if (output && typeof output === 'object' && 'content' in output) {
364
+ output = (output as { content: unknown }).content;
365
+ }
366
+ emit({
367
+ type: 'tool-result',
368
+ toolCallId: approvedRunIds.get(runId) ?? runId,
369
+ toolName: toCommonName(toolName),
370
+ result: output ?? null,
371
+ });
372
+ approvedRunIds.delete(runId);
373
+ }
374
+ }
375
+ }
376
+
377
+ const actionRequests = await readPendingApprovals();
378
+ if (actionRequests.length === 0) break;
379
+
380
+ // HITL paused the run: announce each gated call, collect host decisions, then resume.
381
+ const decisions: Array<
382
+ { type: 'approve' } | { type: 'reject'; message?: string }
383
+ > = [];
384
+ for (const action of actionRequests) {
385
+ const approvalId = `approval-${randomUUID()}`;
386
+ endTextBlock();
387
+ endReasoningBlock();
388
+ emit({
389
+ type: 'tool-call',
390
+ toolCallId: approvalId,
391
+ toolName: toCommonName(action.name),
392
+ input: JSON.stringify(action.args ?? {}),
393
+ providerExecuted: true,
394
+ nativeName: action.name,
395
+ });
396
+ emit({
397
+ type: 'tool-approval-request',
398
+ approvalId,
399
+ toolCallId: approvalId,
400
+ });
401
+ flushStep();
402
+ const decision = await turn.requestToolApproval(approvalId);
403
+ if (decision.approved) {
404
+ const queue = approvedToolQueue.get(action.name) ?? [];
405
+ queue.push(approvalId);
406
+ approvedToolQueue.set(action.name, queue);
407
+ decisions.push({ type: 'approve' });
408
+ } else {
409
+ // Rejected tools never execute, so surface the outcome as the result now.
410
+ emit({
411
+ type: 'tool-result',
412
+ toolCallId: approvalId,
413
+ toolName: toCommonName(action.name),
414
+ result: decision.reason ?? 'Rejected by user.',
415
+ });
416
+ decisions.push({
417
+ type: 'reject',
418
+ ...(decision.reason ? { message: decision.reason } : {}),
419
+ });
420
+ }
421
+ }
422
+
423
+ resumeInput = new Command({ resume: { decisions } });
424
+ }
425
+
426
+ endTextBlock();
427
+ endReasoningBlock();
428
+ flushStep();
429
+ emit({
430
+ type: 'finish',
431
+ finishReason: { unified: 'stop' },
432
+ totalUsage: {
433
+ inputTokens: { total: inputTokens },
434
+ outputTokens: { total: outputTokens },
435
+ },
436
+ });
437
+ }
438
+
439
+ await runBridge<StartMessage>({
440
+ bridgeType: 'deepagents',
441
+ bridgeStateDir: bridgeStateDir!,
442
+ onStart: runTurn,
443
+ });
@@ -0,0 +1,64 @@
1
+ import { z } from 'zod/v4';
2
+
3
+ export type JsonSchemaObject = {
4
+ type?: string | string[];
5
+ description?: string;
6
+ properties?: Record<string, JsonSchemaObject>;
7
+ required?: string[];
8
+ items?: JsonSchemaObject;
9
+ nullable?: boolean;
10
+ };
11
+
12
+ // Convert a host tool's JSON Schema to a zod object for LangChain's `tool()`.
13
+ export function jsonSchemaToZodObject(input: unknown) {
14
+ const schema =
15
+ input && typeof input === 'object' ? (input as JsonSchemaObject) : {};
16
+ return z.object(toZodShape(schema));
17
+ }
18
+
19
+ function toZodShape(schema: JsonSchemaObject): Record<string, z.ZodTypeAny> {
20
+ if (!schema.properties) return {};
21
+ const required = new Set(schema.required ?? []);
22
+ const shape: Record<string, z.ZodTypeAny> = {};
23
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
24
+ const propType = toZodType(propSchema);
25
+ shape[key] = required.has(key) ? propType : propType.optional();
26
+ }
27
+ return shape;
28
+ }
29
+
30
+ function toZodType(schema: JsonSchemaObject | undefined): z.ZodTypeAny {
31
+ if (!schema) return z.any();
32
+ const types = Array.isArray(schema.type)
33
+ ? schema.type.filter(t => t !== 'null')
34
+ : ([schema.type].filter(Boolean) as string[]);
35
+ let zType: z.ZodTypeAny;
36
+ switch (types[0]) {
37
+ case 'string':
38
+ zType = z.string();
39
+ break;
40
+ case 'number':
41
+ zType = z.number();
42
+ break;
43
+ case 'integer':
44
+ zType = z.number().int();
45
+ break;
46
+ case 'boolean':
47
+ zType = z.boolean();
48
+ break;
49
+ case 'array':
50
+ zType = z.array(toZodType(schema.items));
51
+ break;
52
+ case 'object':
53
+ zType = z.object(toZodShape(schema));
54
+ break;
55
+ case 'null':
56
+ zType = z.null();
57
+ break;
58
+ default:
59
+ zType = z.any();
60
+ }
61
+ if (schema.description) zType = zType.describe(schema.description);
62
+ if (schema.nullable) zType = zType.nullable();
63
+ return zType;
64
+ }
@@ -0,0 +1,19 @@
1
+ import { LocalShellBackend } from 'deepagents';
2
+
3
+ export const SANDBOX_PATH_FALLBACK =
4
+ '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin';
5
+
6
+ export function createLocalShellBackend({
7
+ rootDir,
8
+ env = process.env,
9
+ }: {
10
+ readonly rootDir: string;
11
+ readonly env?: NodeJS.ProcessEnv;
12
+ }): LocalShellBackend {
13
+ return new LocalShellBackend({
14
+ rootDir,
15
+ env: {
16
+ PATH: env.PATH ?? SANDBOX_PATH_FALLBACK,
17
+ },
18
+ });
19
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "harness-deepagents-bridge",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "dependencies": {
7
+ "@langchain/anthropic": "^1.0.0",
8
+ "@langchain/core": "^1.1.44",
9
+ "@langchain/langgraph": "^1.3.0",
10
+ "deepagents": "1.10.2",
11
+ "ws": "8.21.0",
12
+ "zod": "^4.3.6"
13
+ }
14
+ }