@ai-sdk/harness-deepagents 1.0.39 → 1.0.40

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.
@@ -13,23 +13,21 @@ import { Command, MemorySaver } from '@langchain/langgraph';
13
13
  import { createDeepAgent } from 'deepagents';
14
14
  import type { StartMessage } from '../deepagents-bridge-protocol';
15
15
  import { buildInterruptOn, collectActionRequests } from './approvals';
16
+ import {
17
+ createDeepAgentsStreamEventState,
18
+ createEmitStreamEvent,
19
+ endReasoningBlock,
20
+ endTextBlock,
21
+ flushStep,
22
+ toCommonName,
23
+ type DeepAgentsStreamEvent,
24
+ } from './create-emit-stream-event';
16
25
  import { jsonSchemaToZodObject } from './json-schema-to-zod';
17
26
  import { createLocalShellBackend } from './local-shell-backend';
18
27
  import { createBuiltinToolFilteringMiddleware } from './tool-filtering';
19
28
 
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
29
  const HARNESS_CLIENT_APP = procEnv.AI_SDK_HARNESS_CLIENT_APP;
28
30
 
29
- function toCommonName(nativeName: string): string {
30
- return NATIVE_TO_COMMON[nativeName] ?? nativeName;
31
- }
32
-
33
31
  function parseArgs(rawArgs: string[]): Record<string, string> {
34
32
  const out: Record<string, string> = {};
35
33
  for (let i = 0; i < rawArgs.length; i++) {
@@ -68,21 +66,6 @@ function buildModel(rawModel: string | undefined) {
68
66
  });
69
67
  }
70
68
 
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
69
  const args = parseArgs(argv.slice(2));
87
70
  const workdir = args.workdir;
88
71
  const bridgeStateDir = args.bridgeStateDir;
@@ -162,75 +145,21 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
162
145
  });
163
146
  }
164
147
 
165
- emit({
166
- type: 'stream-start',
167
- ...(start.model ? { modelId: start.model } : {}),
168
- });
169
-
170
148
  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
- };
149
+ const streamEventState = createDeepAgentsStreamEventState();
150
+ const emitStreamEvent = createEmitStreamEvent({
151
+ state: streamEventState,
152
+ configuredModel: start.model,
153
+ hostToolNames,
154
+ emit,
155
+ });
229
156
 
230
157
  const config = {
231
158
  version: 'v2' as const,
232
159
  configurable: { thread_id: 'bridge-session' },
233
- recursionLimit: start.recursionLimit ?? 100,
160
+ ...(start.recursionLimit != null
161
+ ? { recursionLimit: start.recursionLimit }
162
+ : {}),
234
163
  signal: turn.abortSignal,
235
164
  };
236
165
 
@@ -256,122 +185,7 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
256
185
  const stream = await agent.streamEvents(resumeInput as never, config);
257
186
 
258
187
  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
- }
188
+ emitStreamEvent(event as DeepAgentsStreamEvent);
375
189
  }
376
190
 
377
191
  const actionRequests = await readPendingApprovals();
@@ -383,8 +197,8 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
383
197
  > = [];
384
198
  for (const action of actionRequests) {
385
199
  const approvalId = `approval-${randomUUID()}`;
386
- endTextBlock();
387
- endReasoningBlock();
200
+ endTextBlock({ state: streamEventState, emit });
201
+ endReasoningBlock({ state: streamEventState, emit });
388
202
  emit({
389
203
  type: 'tool-call',
390
204
  toolCallId: approvalId,
@@ -398,12 +212,12 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
398
212
  approvalId,
399
213
  toolCallId: approvalId,
400
214
  });
401
- flushStep();
215
+ flushStep({ state: streamEventState, emit });
402
216
  const decision = await turn.requestToolApproval(approvalId);
403
217
  if (decision.approved) {
404
- const queue = approvedToolQueue.get(action.name) ?? [];
218
+ const queue = streamEventState.approvedToolQueue.get(action.name) ?? [];
405
219
  queue.push(approvalId);
406
- approvedToolQueue.set(action.name, queue);
220
+ streamEventState.approvedToolQueue.set(action.name, queue);
407
221
  decisions.push({ type: 'approve' });
408
222
  } else {
409
223
  // Rejected tools never execute, so surface the outcome as the result now.
@@ -423,15 +237,15 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
423
237
  resumeInput = new Command({ resume: { decisions } });
424
238
  }
425
239
 
426
- endTextBlock();
427
- endReasoningBlock();
428
- flushStep();
240
+ endTextBlock({ state: streamEventState, emit });
241
+ endReasoningBlock({ state: streamEventState, emit });
242
+ flushStep({ state: streamEventState, emit });
429
243
  emit({
430
244
  type: 'finish',
431
245
  finishReason: { unified: 'stop' },
432
246
  totalUsage: {
433
- inputTokens: { total: inputTokens },
434
- outputTokens: { total: outputTokens },
247
+ inputTokens: { total: streamEventState.inputTokens },
248
+ outputTokens: { total: streamEventState.outputTokens },
435
249
  },
436
250
  });
437
251
  }
@@ -91,7 +91,10 @@ export type DeepAgentsHarnessSettings = {
91
91
  readonly port?: number;
92
92
  /** Maximum milliseconds to wait for the bridge to advertise its port. Defaults to 120000. */
93
93
  readonly startupTimeoutMs?: number;
94
- /** Max LangGraph super-steps per turn before it errors. Defaults to 100; raise for long multi-step tasks. */
94
+ /**
95
+ * Maximum LangGraph super-steps per turn before it errors.
96
+ * When omitted, the Deep Agents default applies.
97
+ */
95
98
  readonly recursionLimit?: number;
96
99
  };
97
100