@ai-sdk/harness-deepagents 1.0.39 → 1.0.41
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/CHANGELOG.md +17 -0
- package/dist/bridge/index.mjs +238 -172
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/bridge/create-emit-stream-event.ts +322 -0
- package/src/bridge/index.ts +30 -216
- package/src/deepagents-harness.ts +4 -1
package/src/bridge/index.ts
CHANGED
|
@@ -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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|