@botbotgo/agent-harness 0.0.66 → 0.0.67

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.
@@ -1 +1 @@
1
- export declare const AGENT_HARNESS_VERSION = "0.0.65";
1
+ export declare const AGENT_HARNESS_VERSION = "0.0.66";
@@ -1 +1 @@
1
- export const AGENT_HARNESS_VERSION = "0.0.65";
1
+ export const AGENT_HARNESS_VERSION = "0.0.66";
@@ -42,6 +42,9 @@ export declare class AgentRuntimeAdapter {
42
42
  private compileInterruptOn;
43
43
  private resolveInterruptOn;
44
44
  private resolveFilesystemBackend;
45
+ private resolveBuiltinMiddlewareBackend;
46
+ private invokeBuiltinTaskTool;
47
+ private resolveBuiltinMiddlewareTools;
45
48
  private resolveLangChainAutomaticMiddleware;
46
49
  private resolveMiddleware;
47
50
  private resolveCheckpointer;
@@ -3,7 +3,8 @@ import { existsSync, statSync } from "node:fs";
3
3
  import { cp, mkdir, rm } from "node:fs/promises";
4
4
  import { Command, MemorySaver } from "@langchain/langgraph";
5
5
  import { tool as createLangChainTool } from "@langchain/core/tools";
6
- import { createDeepAgent, createMemoryMiddleware, createSkillsMiddleware, createSubAgentMiddleware, FilesystemBackend, } from "deepagents";
6
+ import { HumanMessage, ToolMessage } from "@langchain/core/messages";
7
+ import { DEFAULT_SUBAGENT_PROMPT, createDeepAgent, createMemoryMiddleware, createPatchToolCallsMiddleware, createSkillsMiddleware, createSummarizationMiddleware, createSubAgentMiddleware, FilesystemBackend, StateBackend, isSandboxBackend, } from "deepagents";
7
8
  import { ChatAnthropic } from "@langchain/anthropic";
8
9
  import { tools as anthropicProviderTools } from "@langchain/anthropic";
9
10
  import { ChatGoogle } from "@langchain/google";
@@ -12,7 +13,7 @@ import { ChatOpenAI } from "@langchain/openai";
12
13
  import { tools as openAIProviderTools } from "@langchain/openai";
13
14
  import { createAgent, humanInTheLoopMiddleware, initChatModel } from "langchain";
14
15
  import { z } from "zod";
15
- import { extractEmptyAssistantMessageFailure, extractContentBlocks, extractOutputContent, extractReasoningText, extractToolFallbackContext, extractVisibleOutput, isLikelyToolArgsObject, isToolCallParseFailure, STRICT_TOOL_JSON_INSTRUCTION, sanitizeVisibleText, tryParseJson, wrapResolvedModel, } from "./parsing/output-parsing.js";
16
+ import { extractEmptyAssistantMessageFailure, extractContentBlocks, extractOutputContent, extractReasoningText, extractToolFallbackContext, extractVisibleOutput, salvageToolArgs, isLikelyToolArgsObject, isToolCallParseFailure, STRICT_TOOL_JSON_INSTRUCTION, sanitizeVisibleText, tryParseJson, wrapResolvedModel, } from "./parsing/output-parsing.js";
16
17
  import { computeIncrementalOutput, extractAgentStep, extractInterruptPayload, extractReasoningStreamOutput, extractStateStreamOutput, extractVisibleStreamOutput, extractTerminalStreamOutput, extractToolResult, normalizeTerminalOutputKey, readStreamDelta, } from "./parsing/stream-event-parsing.js";
17
18
  import { wrapToolForExecution } from "./tool-hitl.js";
18
19
  import { resolveDeclaredMiddleware } from "./declared-middleware.js";
@@ -24,6 +25,17 @@ function countConfiguredTools(binding) {
24
25
  }
25
26
  const AGENT_INTERRUPT_SENTINEL_PREFIX = "__agent_harness_interrupt__:";
26
27
  const MODEL_SAFE_TOOL_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
28
+ const UPSTREAM_BUILTIN_MIDDLEWARE_TOOL_NAMES = Object.freeze([
29
+ "write_todos",
30
+ "ls",
31
+ "read_file",
32
+ "write_file",
33
+ "edit_file",
34
+ "glob",
35
+ "grep",
36
+ "execute",
37
+ "task",
38
+ ]);
27
39
  class RuntimeOperationTimeoutError extends Error {
28
40
  operation;
29
41
  timeoutMs;
@@ -152,7 +164,8 @@ function normalizeOpenAICompatibleInit(init) {
152
164
  return normalized;
153
165
  }
154
166
  function sanitizeToolNameForModel(name) {
155
- const sanitized = name
167
+ const withoutNamespace = name.includes(".") ? name.split(".").at(-1) ?? name : name;
168
+ const sanitized = withoutNamespace
156
169
  .replaceAll(".", "__")
157
170
  .replace(/[^a-zA-Z0-9_-]+/g, "_")
158
171
  .replace(/^_+|_+$/g, "");
@@ -252,6 +265,193 @@ function normalizeResolvedToolSchema(resolvedTool) {
252
265
  }
253
266
  return z.object({}).passthrough();
254
267
  }
268
+ function isRecord(value) {
269
+ return typeof value === "object" && value !== null && !Array.isArray(value);
270
+ }
271
+ function isObject(value) {
272
+ return isRecord(value);
273
+ }
274
+ function stringifyToolOutput(output) {
275
+ if (typeof output === "string") {
276
+ return output;
277
+ }
278
+ try {
279
+ return JSON.stringify(output);
280
+ }
281
+ catch {
282
+ return `${String(output)}`;
283
+ }
284
+ }
285
+ function normalizeToolArgsForSchema(args, schema) {
286
+ const schemaDef = isObject(schema) ? schema._def : undefined;
287
+ const shape = schemaDef
288
+ ? isRecord(schemaDef.shape)
289
+ ? schemaDef.shape
290
+ : typeof schemaDef.shape === "function"
291
+ ? schemaDef.shape()
292
+ : undefined
293
+ : undefined;
294
+ if (!shape || !isRecord(shape)) {
295
+ return args;
296
+ }
297
+ const keys = Object.keys(shape);
298
+ if (keys.length !== 1) {
299
+ return args;
300
+ }
301
+ const [expectedKey] = keys;
302
+ if (expectedKey in args) {
303
+ return args;
304
+ }
305
+ const aliasesByExpected = {
306
+ city: ["location", "locality", "place"],
307
+ location: ["city", "city_name"],
308
+ };
309
+ const aliases = aliasesByExpected[expectedKey] ?? [];
310
+ const aliasKey = aliases.find((candidate) => candidate in args);
311
+ if (!aliasKey || !(aliasKey in args)) {
312
+ return args;
313
+ }
314
+ return {
315
+ ...args,
316
+ [expectedKey]: args[aliasKey],
317
+ };
318
+ }
319
+ function extractToolCallsFromResult(result) {
320
+ const messages = isRecord(result) && Array.isArray(result.messages) ? result.messages : [];
321
+ const lastMessage = messages.at(-1);
322
+ if (!isObject(lastMessage)) {
323
+ return [];
324
+ }
325
+ const messageKwargs = isObject(lastMessage.kwargs) ? lastMessage.kwargs : undefined;
326
+ const rawToolCalls = Array.isArray(lastMessage.tool_calls)
327
+ ? (lastMessage.tool_calls ?? [])
328
+ : Array.isArray(messageKwargs?.tool_calls)
329
+ ? messageKwargs.tool_calls
330
+ : [];
331
+ return rawToolCalls
332
+ .map((toolCall) => {
333
+ if (!isObject(toolCall)) {
334
+ return null;
335
+ }
336
+ const functionPayload = isObject(toolCall.function) ? toolCall.function : undefined;
337
+ const name = typeof toolCall.name === "string"
338
+ ? toolCall.name
339
+ : typeof functionPayload?.name === "string"
340
+ ? functionPayload.name
341
+ : null;
342
+ if (!name) {
343
+ return null;
344
+ }
345
+ const rawArgs = salvageToolArgs(toolCall.args ?? functionPayload?.arguments) ?? {};
346
+ if (!isObject(rawArgs)) {
347
+ return null;
348
+ }
349
+ const id = typeof toolCall.id === "string" ? toolCall.id : undefined;
350
+ return { id, name, args: rawArgs };
351
+ })
352
+ .filter((item) => item !== null);
353
+ }
354
+ function truncateLines(lines, maxChars = 12_000) {
355
+ const joined = lines.join("\n");
356
+ if (joined.length <= maxChars) {
357
+ return joined;
358
+ }
359
+ return `${joined.slice(0, maxChars - 18)}\n...[truncated]`;
360
+ }
361
+ function summarizeBuiltinWriteTodosArgs(args) {
362
+ const todos = Array.isArray(args.todos) ? args.todos : [];
363
+ const items = todos.flatMap((todo) => {
364
+ if (!isObject(todo)) {
365
+ return [];
366
+ }
367
+ const content = typeof todo.content === "string" && todo.content.trim().length > 0
368
+ ? todo.content.trim()
369
+ : typeof todo.description === "string" && todo.description.trim().length > 0
370
+ ? todo.description.trim()
371
+ : "";
372
+ const status = typeof todo.status === "string" && todo.status.trim().length > 0 ? todo.status.trim() : "pending";
373
+ return content ? [{ content, status }] : [];
374
+ });
375
+ return {
376
+ total: items.length,
377
+ pending: items.filter((item) => item.status !== "completed").length,
378
+ completed: items.filter((item) => item.status === "completed").length,
379
+ items,
380
+ };
381
+ }
382
+ function createModelFacingToolNameCandidates(toolName) {
383
+ const trimmed = toolName.trim();
384
+ if (!trimmed) {
385
+ return [];
386
+ }
387
+ const lastSegment = trimmed.split(".").at(-1) ?? "";
388
+ const candidates = new Set([
389
+ trimmed,
390
+ sanitizeToolNameForModel(trimmed),
391
+ lastSegment,
392
+ lastSegment.trim() ? sanitizeToolNameForModel(lastSegment) : "",
393
+ trimmed.replace(/[^a-zA-Z0-9_]+/g, ""),
394
+ ]);
395
+ candidates.delete("");
396
+ return [...candidates];
397
+ }
398
+ function createModelFacingToolNameLookupCandidates(toolName) {
399
+ const trimmed = toolName.trim();
400
+ if (!trimmed) {
401
+ return [];
402
+ }
403
+ const candidates = new Set();
404
+ const add = (value) => {
405
+ for (const candidate of createModelFacingToolNameCandidates(value)) {
406
+ candidates.add(candidate);
407
+ }
408
+ };
409
+ const addSuffixStripped = (value) => {
410
+ const stripped = value.includes(".") ? value.split(".").at(-1) ?? value : value;
411
+ if (stripped !== value) {
412
+ add(stripped);
413
+ }
414
+ };
415
+ add(trimmed);
416
+ if (trimmed.startsWith("functions.")) {
417
+ const stripped = trimmed.substring("functions.".length);
418
+ add(stripped);
419
+ addSuffixStripped(stripped);
420
+ }
421
+ else if (trimmed.startsWith("function.")) {
422
+ const stripped = trimmed.substring("function.".length);
423
+ add(stripped);
424
+ addSuffixStripped(stripped);
425
+ }
426
+ else {
427
+ addSuffixStripped(trimmed);
428
+ }
429
+ return [...candidates];
430
+ }
431
+ function resolveModelFacingToolName(toolName, mapping, tools) {
432
+ const candidateNames = createModelFacingToolNameLookupCandidates(toolName);
433
+ const candidateSet = new Set(candidateNames);
434
+ for (const candidate of candidateNames) {
435
+ const mappedToolName = mapping.modelFacingToOriginal.get(candidate);
436
+ if (mappedToolName) {
437
+ return mappedToolName;
438
+ }
439
+ }
440
+ for (const candidate of candidateNames) {
441
+ const directMatch = tools.find((tool) => tool.name === candidate);
442
+ if (directMatch) {
443
+ return directMatch.name;
444
+ }
445
+ }
446
+ const candidateMatches = tools.filter((tool) => {
447
+ const modelFacingCandidates = createModelFacingToolNameCandidates(tool.name);
448
+ return modelFacingCandidates.some((candidate) => candidateSet.has(candidate));
449
+ });
450
+ if (candidateMatches.length === 1) {
451
+ return candidateMatches[0].name;
452
+ }
453
+ return toolName;
454
+ }
255
455
  function asStructuredExecutableTool(resolvedTool, modelFacingName, description) {
256
456
  if (!hasCallableToolHandler(resolvedTool)) {
257
457
  return resolvedTool;
@@ -589,6 +789,228 @@ export class AgentRuntimeAdapter {
589
789
  : 10,
590
790
  });
591
791
  }
792
+ resolveBuiltinMiddlewareBackend(binding, options = {}) {
793
+ const runtimeState = {
794
+ ...(options.state ?? {}),
795
+ ...(isRecord(options.files) ? { files: options.files } : {}),
796
+ };
797
+ const runtimeLike = {
798
+ state: runtimeState,
799
+ store: this.options.storeResolver?.(binding),
800
+ };
801
+ const configuredBackend = isDeepAgentBinding(binding)
802
+ ? this.options.backendResolver?.(binding)
803
+ : this.resolveFilesystemBackend(binding);
804
+ if (typeof configuredBackend === "function") {
805
+ return configuredBackend(runtimeLike);
806
+ }
807
+ if (configuredBackend) {
808
+ return configuredBackend;
809
+ }
810
+ return new StateBackend(runtimeLike);
811
+ }
812
+ async invokeBuiltinTaskTool(binding, input, options = {}) {
813
+ if (!isDeepAgentBinding(binding)) {
814
+ throw new Error("The built-in task tool is only available for deepagent bindings.");
815
+ }
816
+ const params = getBindingDeepAgentParams(binding);
817
+ if (!params) {
818
+ throw new Error(`Agent ${binding.agent.id} has no deepagent params`);
819
+ }
820
+ const typedInput = isObject(input) ? input : {};
821
+ const description = typeof typedInput.description === "string" ? typedInput.description : "";
822
+ const subagentType = typeof typedInput.subagent_type === "string" ? typedInput.subagent_type : "";
823
+ const builtinBackend = this.resolveBuiltinMiddlewareBackend(binding, options);
824
+ const resolvedSubagents = await this.resolveSubagents(params.subagents, binding);
825
+ const selectedSubagent = resolvedSubagents.find((subagent) => subagent.name === subagentType);
826
+ if (!selectedSubagent) {
827
+ const allowed = [
828
+ ...resolvedSubagents.map((subagent) => subagent.name),
829
+ ...(params.generalPurposeAgent ? ["general-purpose"] : []),
830
+ ];
831
+ throw new Error(`Error: invoked agent of type ${subagentType}, the only allowed types are ${allowed.map((name) => `\`${name}\``).join(", ")}`);
832
+ }
833
+ const summarizationModel = selectedSubagent.model
834
+ ? await this.resolveModel(selectedSubagent.model)
835
+ : await this.resolveModel(params.model);
836
+ const middleware = [
837
+ ...(selectedSubagent.skills?.length
838
+ ? [createSkillsMiddleware({ backend: builtinBackend, sources: selectedSubagent.skills })]
839
+ : []),
840
+ ...(selectedSubagent.memory?.length
841
+ ? [createMemoryMiddleware({ backend: builtinBackend, sources: selectedSubagent.memory })]
842
+ : []),
843
+ ...(selectedSubagent.middleware ??
844
+ [
845
+ createPatchToolCallsMiddleware(),
846
+ createSummarizationMiddleware({
847
+ model: summarizationModel,
848
+ backend: builtinBackend,
849
+ }),
850
+ ]),
851
+ ...(selectedSubagent.interruptOn
852
+ ? [humanInTheLoopMiddleware({
853
+ interruptOn: this.compileInterruptOn(selectedSubagent.tools ?? [], selectedSubagent.interruptOn),
854
+ })]
855
+ : []),
856
+ ];
857
+ const runnable = createAgent({
858
+ model: (selectedSubagent.model ?? (await this.resolveModel(params.model))),
859
+ tools: (selectedSubagent.tools ?? this.resolveTools(params.tools, binding)),
860
+ systemPrompt: selectedSubagent.systemPrompt ?? DEFAULT_SUBAGENT_PROMPT,
861
+ middleware: middleware,
862
+ responseFormat: selectedSubagent.responseFormat,
863
+ contextSchema: selectedSubagent.contextSchema,
864
+ name: selectedSubagent.name,
865
+ description: selectedSubagent.description,
866
+ });
867
+ const result = await runnable.invoke({ messages: [new HumanMessage({ content: description })] }, { configurable: { thread_id: `${binding.agent.id}:builtin-task` }, ...(options.context ? { context: options.context } : {}) });
868
+ const visibleOutput = extractVisibleOutput(result);
869
+ const fallbackOutput = extractToolFallbackContext(result);
870
+ return visibleOutput || fallbackOutput || JSON.stringify(result);
871
+ }
872
+ async resolveBuiltinMiddlewareTools(binding, options = {}) {
873
+ const tools = new Map();
874
+ const backend = this.resolveBuiltinMiddlewareBackend(binding, options);
875
+ tools.set("write_todos", {
876
+ name: "write_todos",
877
+ schema: z.object({
878
+ todos: z.array(z.object({}).passthrough()).optional(),
879
+ }).passthrough(),
880
+ invoke: async (input) => {
881
+ const args = isObject(input) ? input : {};
882
+ const summary = summarizeBuiltinWriteTodosArgs(args);
883
+ return {
884
+ ok: true,
885
+ tool: "write_todos",
886
+ message: `Tracked ${summary.total} todo item(s).`,
887
+ summary,
888
+ };
889
+ },
890
+ });
891
+ tools.set("ls", {
892
+ name: "ls",
893
+ schema: z.object({ path: z.string().optional().default("/") }).passthrough(),
894
+ invoke: async (input) => {
895
+ const targetPath = isObject(input) && typeof input.path === "string" ? input.path : "/";
896
+ const infos = (await Promise.resolve(backend.lsInfo?.(targetPath))) ?? [];
897
+ if (infos.length === 0) {
898
+ return `No files found in ${targetPath}`;
899
+ }
900
+ return truncateLines(infos.map((info) => info.is_dir ? `${info.path} (directory)` : `${info.path}${info.size ? ` (${info.size} bytes)` : ""}`));
901
+ },
902
+ });
903
+ tools.set("read_file", {
904
+ name: "read_file",
905
+ schema: z.object({
906
+ file_path: z.string(),
907
+ offset: z.number().optional(),
908
+ limit: z.number().optional(),
909
+ }).passthrough(),
910
+ invoke: async (input) => {
911
+ const typed = isObject(input) ? input : {};
912
+ const filePath = typeof typed.file_path === "string" ? typed.file_path : "";
913
+ const offset = typeof typed.offset === "number" ? typed.offset : 0;
914
+ const limit = typeof typed.limit === "number" ? typed.limit : 500;
915
+ return Promise.resolve(backend.read?.(filePath, offset, limit)) ?? "";
916
+ },
917
+ });
918
+ tools.set("write_file", {
919
+ name: "write_file",
920
+ schema: z.object({ file_path: z.string(), content: z.string().optional() }).passthrough(),
921
+ invoke: async (input) => {
922
+ const typed = isObject(input) ? input : {};
923
+ const result = await Promise.resolve(backend.write?.(typeof typed.file_path === "string" ? typed.file_path : "", typeof typed.content === "string" ? typed.content : ""));
924
+ return result?.error ?? `Successfully wrote to '${result?.path ?? (typed.file_path ?? "")}'`;
925
+ },
926
+ });
927
+ tools.set("edit_file", {
928
+ name: "edit_file",
929
+ schema: z.object({
930
+ file_path: z.string(),
931
+ old_string: z.string(),
932
+ new_string: z.string(),
933
+ replace_all: z.boolean().optional(),
934
+ }).passthrough(),
935
+ invoke: async (input) => {
936
+ const typed = isObject(input) ? input : {};
937
+ const result = await Promise.resolve(backend.edit?.(typeof typed.file_path === "string" ? typed.file_path : "", typeof typed.old_string === "string" ? typed.old_string : "", typeof typed.new_string === "string" ? typed.new_string : "", typed.replace_all === true));
938
+ return result?.error ?? `Successfully replaced ${result?.occurrences ?? 0} occurrence(s) in '${result?.path ?? (typed.file_path ?? "")}'`;
939
+ },
940
+ });
941
+ tools.set("glob", {
942
+ name: "glob",
943
+ schema: z.object({ pattern: z.string(), path: z.string().optional().default("/") }).passthrough(),
944
+ invoke: async (input) => {
945
+ const typed = isObject(input) ? input : {};
946
+ const pattern = typeof typed.pattern === "string" ? typed.pattern : "";
947
+ const targetPath = typeof typed.path === "string" ? typed.path : "/";
948
+ const infos = (await Promise.resolve(backend.globInfo?.(pattern, targetPath))) ?? [];
949
+ if (infos.length === 0) {
950
+ return `No files found matching pattern '${pattern}'`;
951
+ }
952
+ return truncateLines(infos.map((info) => info.path));
953
+ },
954
+ });
955
+ tools.set("grep", {
956
+ name: "grep",
957
+ schema: z.object({
958
+ pattern: z.string(),
959
+ path: z.string().optional().default("/"),
960
+ glob: z.string().nullable().optional(),
961
+ }).passthrough(),
962
+ invoke: async (input) => {
963
+ const typed = isObject(input) ? input : {};
964
+ const result = await Promise.resolve(backend.grepRaw?.(typeof typed.pattern === "string" ? typed.pattern : "", typeof typed.path === "string" ? typed.path : "/", typeof typed.glob === "string" ? typed.glob : null));
965
+ if (typeof result === "string") {
966
+ return result;
967
+ }
968
+ if (!result || result.length === 0) {
969
+ return `No matches found for pattern '${typeof typed.pattern === "string" ? typed.pattern : ""}'`;
970
+ }
971
+ const lines = [];
972
+ let currentFile = "";
973
+ for (const match of result) {
974
+ if (match.path !== currentFile) {
975
+ currentFile = match.path;
976
+ lines.push(`\n${currentFile}:`);
977
+ }
978
+ lines.push(` ${match.line}: ${match.text}`);
979
+ }
980
+ return truncateLines(lines);
981
+ },
982
+ });
983
+ tools.set("execute", {
984
+ name: "execute",
985
+ schema: z.object({ command: z.string() }).passthrough(),
986
+ invoke: async (input) => {
987
+ if (!isSandboxBackend(backend) || typeof backend.execute !== "function") {
988
+ return "Error: Execution not available. This agent's backend does not support command execution (SandboxBackendProtocol).";
989
+ }
990
+ const typed = isObject(input) ? input : {};
991
+ const result = await Promise.resolve(backend.execute(typeof typed.command === "string" ? typed.command : ""));
992
+ const parts = [result.output];
993
+ if (result.exitCode !== null) {
994
+ parts.push(`\n[Command ${result.exitCode === 0 ? "succeeded" : "failed"} with exit code ${result.exitCode}]`);
995
+ }
996
+ if (result.truncated) {
997
+ parts.push("\n[Output was truncated due to size limits]");
998
+ }
999
+ return parts.join("");
1000
+ },
1001
+ });
1002
+ if (isDeepAgentBinding(binding)) {
1003
+ tools.set("task", {
1004
+ name: "task",
1005
+ schema: z.object({
1006
+ description: z.string(),
1007
+ subagent_type: z.string(),
1008
+ }).passthrough(),
1009
+ invoke: async (input) => this.invokeBuiltinTaskTool(binding, input, options),
1010
+ });
1011
+ }
1012
+ return tools;
1013
+ }
592
1014
  async resolveLangChainAutomaticMiddleware(binding) {
593
1015
  const params = getBindingLangChainParams(binding);
594
1016
  if (!params) {
@@ -781,17 +1203,117 @@ export class AgentRuntimeAdapter {
781
1203
  ? this.buildInvocationRequest(binding, history, input, options)
782
1204
  : new Command({ resume: resumePayload });
783
1205
  let result;
784
- try {
785
- const runnable = await this.create(binding);
786
- result = (await this.withTimeout(() => runnable.invoke(request, { configurable: { thread_id: threadId }, ...(options.context ? { context: options.context } : {}) }), this.resolveBindingTimeout(binding), "agent invoke", "invoke"));
1206
+ const callRuntime = async (activeBinding, activeRequest) => {
1207
+ const runnable = await this.create(activeBinding);
1208
+ return (await this.withTimeout(() => runnable.invoke(activeRequest, { configurable: { thread_id: threadId }, ...(options.context ? { context: options.context } : {}) }), this.resolveBindingTimeout(activeBinding), "agent invoke", "invoke"));
1209
+ };
1210
+ const callRuntimeWithToolParseRecovery = async (activeRequest) => {
1211
+ try {
1212
+ return await callRuntime(binding, activeRequest);
1213
+ }
1214
+ catch (error) {
1215
+ if (resumePayload !== undefined || !isToolCallParseFailure(error)) {
1216
+ throw error;
1217
+ }
1218
+ return callRuntime(this.applyStrictToolJsonInstruction(binding), activeRequest);
1219
+ }
1220
+ };
1221
+ const executedToolResults = [];
1222
+ if (resumePayload !== undefined) {
1223
+ result = await callRuntimeWithToolParseRecovery(request);
787
1224
  }
788
- catch (error) {
789
- if (resumePayload !== undefined || !isToolCallParseFailure(error)) {
790
- throw error;
1225
+ else {
1226
+ const primaryTools = getBindingPrimaryTools(binding);
1227
+ const defersToUpstreamHitlExecution = primaryTools.some((tool) => tool.hitl?.enabled === true);
1228
+ if (defersToUpstreamHitlExecution) {
1229
+ result = await callRuntimeWithToolParseRecovery(request);
1230
+ }
1231
+ else {
1232
+ const resolvedTools = this.resolveTools(primaryTools, binding);
1233
+ const toolNameMapping = this.buildToolNameMapping(primaryTools);
1234
+ const executableTools = new Map();
1235
+ const builtinExecutableTools = await this.resolveBuiltinMiddlewareTools(binding, options);
1236
+ for (let index = 0; index < primaryTools.length; index += 1) {
1237
+ const compiledTool = primaryTools[index];
1238
+ const resolvedTool = resolvedTools[index];
1239
+ if (!compiledTool || !resolvedTool || !hasCallableToolHandler(resolvedTool)) {
1240
+ continue;
1241
+ }
1242
+ const handler = async (toolInput) => {
1243
+ const callable = typeof resolvedTool.invoke === "function"
1244
+ ? resolvedTool.invoke
1245
+ : typeof resolvedTool.call === "function"
1246
+ ? resolvedTool.call
1247
+ : resolvedTool.func;
1248
+ if (!callable) {
1249
+ throw new Error(`Tool ${compiledTool.name} has no callable handler.`);
1250
+ }
1251
+ return Promise.resolve(callable.call(resolvedTool, toolInput, options.context ? { context: options.context } : undefined));
1252
+ };
1253
+ const modelFacingName = toolNameMapping.originalToModelFacing.get(compiledTool.name) ?? compiledTool.name;
1254
+ const normalizedSchema = normalizeResolvedToolSchema(resolvedTool);
1255
+ executableTools.set(modelFacingName, {
1256
+ name: compiledTool.name,
1257
+ schema: normalizedSchema,
1258
+ invoke: handler,
1259
+ });
1260
+ executableTools.set(compiledTool.name, {
1261
+ name: compiledTool.name,
1262
+ schema: normalizedSchema,
1263
+ invoke: handler,
1264
+ });
1265
+ }
1266
+ let activeRequest = request;
1267
+ let currentMessages = Array.isArray(activeRequest.messages) ? [...activeRequest.messages] : [];
1268
+ const maxToolIterations = 8;
1269
+ for (let iteration = 0; iteration < maxToolIterations; iteration += 1) {
1270
+ result = await callRuntimeWithToolParseRecovery(activeRequest);
1271
+ const toolCalls = extractToolCallsFromResult(result);
1272
+ if (toolCalls.length === 0) {
1273
+ break;
1274
+ }
1275
+ if (iteration + 1 === maxToolIterations) {
1276
+ throw new Error(`Tool-calling loop exceeded the maximum of ${maxToolIterations} iterations`);
1277
+ }
1278
+ const resultMessages = result.messages;
1279
+ const nextMessages = Array.isArray(resultMessages)
1280
+ ? [...resultMessages]
1281
+ : [...currentMessages];
1282
+ for (let toolIndex = 0; toolIndex < toolCalls.length; toolIndex += 1) {
1283
+ const toolCall = toolCalls[toolIndex];
1284
+ const resolvedToolName = resolveModelFacingToolName(toolCall.name, toolNameMapping, primaryTools);
1285
+ const executable = executableTools.get(toolCall.name) ?? executableTools.get(resolvedToolName);
1286
+ const builtinExecutable = builtinExecutableTools.get(toolCall.name) ??
1287
+ builtinExecutableTools.get(resolvedToolName) ??
1288
+ createModelFacingToolNameLookupCandidates(toolCall.name)
1289
+ .map((candidate) => builtinExecutableTools.get(candidate))
1290
+ .find((candidate) => candidate !== undefined);
1291
+ const activeExecutable = executable ?? builtinExecutable;
1292
+ if (!activeExecutable) {
1293
+ throw new Error(`Tool ${toolCall.name} is not configured for this agent.`);
1294
+ }
1295
+ const normalizedArgs = normalizeToolArgsForSchema(toolCall.args, activeExecutable.schema);
1296
+ const toolResult = await activeExecutable.invoke(normalizedArgs);
1297
+ executedToolResults.push({
1298
+ toolName: activeExecutable.name,
1299
+ output: toolResult,
1300
+ });
1301
+ nextMessages.push(new ToolMessage({
1302
+ name: activeExecutable.name,
1303
+ tool_call_id: toolCall.id ?? `tool-${iteration + 1}-${toolIndex + 1}`,
1304
+ content: stringifyToolOutput(toolResult),
1305
+ }));
1306
+ }
1307
+ currentMessages = nextMessages;
1308
+ activeRequest = {
1309
+ ...activeRequest,
1310
+ messages: currentMessages,
1311
+ };
1312
+ }
791
1313
  }
792
- const retriedBinding = this.applyStrictToolJsonInstruction(binding);
793
- const runnable = await this.create(retriedBinding);
794
- result = (await this.withTimeout(() => runnable.invoke(this.buildInvocationRequest(retriedBinding, history, input, options), { configurable: { thread_id: threadId }, ...(options.context ? { context: options.context } : {}) }), this.resolveBindingTimeout(retriedBinding), "agent invoke", "invoke"));
1314
+ }
1315
+ if (!result) {
1316
+ throw new Error("Agent invocation returned no result");
795
1317
  }
796
1318
  const interruptContent = Array.isArray(result.__interrupt__) && result.__interrupt__.length > 0 ? JSON.stringify(result.__interrupt__) : undefined;
797
1319
  const extractedOutput = extractVisibleOutput(result);
@@ -819,6 +1341,7 @@ export class AgentRuntimeAdapter {
819
1341
  ...(contentBlocks.length > 0 ? { contentBlocks } : {}),
820
1342
  ...(structuredResponse !== undefined ? { structuredResponse } : {}),
821
1343
  metadata: {
1344
+ ...(executedToolResults.length > 0 ? { executedToolResults } : {}),
822
1345
  ...(structuredResponse !== undefined ? { structuredResponse } : {}),
823
1346
  ...(outputContent !== undefined ? { outputContent } : {}),
824
1347
  ...(contentBlocks.length > 0 ? { contentBlocks } : {}),
@@ -833,6 +1356,12 @@ export class AgentRuntimeAdapter {
833
1356
  const invokeTimeoutMs = this.resolveBindingTimeout(binding);
834
1357
  const streamIdleTimeoutMs = this.resolveStreamIdleTimeout(binding);
835
1358
  const streamDeadlineAt = invokeTimeoutMs ? Date.now() + invokeTimeoutMs : undefined;
1359
+ const primaryTools = getBindingPrimaryTools(binding);
1360
+ const toolNameMapping = this.buildToolNameMapping(primaryTools);
1361
+ const primaryModel = getBindingPrimaryModel(binding);
1362
+ const forceInvokeFallback = isLangChainBinding(binding) &&
1363
+ primaryTools.length > 0 &&
1364
+ primaryModel?.provider === "openai-compatible";
836
1365
  if (isLangChainBinding(binding)) {
837
1366
  const langchainParams = getBindingLangChainParams(binding);
838
1367
  const resolvedModel = (await this.resolveModel(langchainParams.model));
@@ -867,7 +1396,7 @@ export class AgentRuntimeAdapter {
867
1396
  }
868
1397
  const runnable = await this.create(binding);
869
1398
  const request = this.buildInvocationRequest(binding, history, input, options);
870
- if (typeof runnable.streamEvents === "function") {
1399
+ if (!forceInvokeFallback && typeof runnable.streamEvents === "function") {
871
1400
  const events = await this.withTimeout(() => runnable.streamEvents(request, { configurable: { thread_id: threadId }, version: "v2", ...(options.context ? { context: options.context } : {}) }), computeRemainingTimeoutMs(streamDeadlineAt, invokeTimeoutMs), "agent streamEvents start", "stream");
872
1401
  const allowVisibleStreamDeltas = isLangChainBinding(binding);
873
1402
  let emittedOutput = "";
@@ -912,7 +1441,13 @@ export class AgentRuntimeAdapter {
912
1441
  const toolResult = extractToolResult(event);
913
1442
  if (toolResult) {
914
1443
  emittedToolError = emittedToolError || toolResult.isError === true;
915
- yield { kind: "tool-result", toolName: toolResult.toolName, output: toolResult.output, isError: toolResult.isError };
1444
+ const resolvedToolName = resolveModelFacingToolName(toolResult.toolName, toolNameMapping, primaryTools);
1445
+ yield {
1446
+ kind: "tool-result",
1447
+ toolName: resolvedToolName,
1448
+ output: toolResult.output,
1449
+ isError: toolResult.isError,
1450
+ };
916
1451
  }
917
1452
  const output = extractTerminalStreamOutput(event);
918
1453
  if (output) {
@@ -934,7 +1469,7 @@ export class AgentRuntimeAdapter {
934
1469
  return;
935
1470
  }
936
1471
  }
937
- if (isLangChainBinding(binding) && typeof runnable.stream === "function") {
1472
+ if (!forceInvokeFallback && isLangChainBinding(binding) && typeof runnable.stream === "function") {
938
1473
  const stream = await this.withTimeout(() => runnable.stream(request, { configurable: { thread_id: threadId } }), computeRemainingTimeoutMs(streamDeadlineAt, invokeTimeoutMs), "agent stream start", "stream");
939
1474
  let emitted = false;
940
1475
  for await (const chunk of this.iterateWithTimeout(stream, streamIdleTimeoutMs, "agent stream", streamDeadlineAt, invokeTimeoutMs)) {
@@ -954,6 +1489,17 @@ export class AgentRuntimeAdapter {
954
1489
  }
955
1490
  }
956
1491
  const result = await this.invoke(binding, input, threadId, threadId);
1492
+ const executedToolResults = Array.isArray(result.metadata?.executedToolResults)
1493
+ ? result.metadata.executedToolResults
1494
+ : [];
1495
+ for (const toolResult of executedToolResults) {
1496
+ yield {
1497
+ kind: "tool-result",
1498
+ toolName: toolResult.toolName,
1499
+ output: toolResult.output,
1500
+ isError: toolResult.isError,
1501
+ };
1502
+ }
957
1503
  if (result.output) {
958
1504
  yield { kind: "content", content: sanitizeVisibleText(result.output) };
959
1505
  }
@@ -100,6 +100,7 @@ export declare class AgentHarnessRuntime {
100
100
  run(options: RunOptions): Promise<RunResult>;
101
101
  streamEvents(options: RunStartOptions): AsyncGenerator<HarnessStreamItem>;
102
102
  resume(options: ResumeOptions): Promise<RunResult>;
103
+ private buildResumePayload;
103
104
  restartConversation(options: RestartConversationOptions): Promise<RunResult & {
104
105
  restart: Record<string, string>;
105
106
  }>;
@@ -15,7 +15,7 @@ import { FileBackedStore } from "./store.js";
15
15
  import { CheckpointMaintenanceLoop, discoverCheckpointMaintenanceTargets, readCheckpointMaintenanceConfig, } from "./checkpoint-maintenance.js";
16
16
  import { extractMessageText, normalizeMessageContent } from "../utils/message-content.js";
17
17
  import { createToolMcpServerFromTools, serveToolsOverStdioFromHarness } from "../mcp.js";
18
- import { getBindingAdapterKind, getBindingPrimaryTools, getBindingStoreConfig } from "./support/compiled-binding.js";
18
+ import { getBindingAdapterKind, getBindingPrimaryTools, getBindingStoreConfig, isDeepAgentBinding } from "./support/compiled-binding.js";
19
19
  import { describeWorkspaceInventory, listAgentSkills as listWorkspaceAgentSkills, } from "./inventory.js";
20
20
  export class AgentHarnessRuntime {
21
21
  workspace;
@@ -401,6 +401,9 @@ export class AgentHarnessRuntime {
401
401
  const event = createHarnessEvent(threadId, runId, sequence, eventType, payload, source);
402
402
  await this.persistence.appendEvent(event);
403
403
  this.eventBus.publish(event);
404
+ if (this.threadMemorySync.shouldHandle(event)) {
405
+ await this.threadMemorySync.handleEvent(event);
406
+ }
404
407
  return event;
405
408
  }
406
409
  async ensureThreadStarted(selectedAgentId, binding, input, existingThreadId) {
@@ -1066,6 +1069,7 @@ export class AgentHarnessRuntime {
1066
1069
  if (!binding) {
1067
1070
  throw new Error(`Unknown agent ${thread.agentId}`);
1068
1071
  }
1072
+ const resumePayload = this.buildResumePayload(binding, approval, options);
1069
1073
  await this.persistence.setRunState(threadId, runId, "resuming", `checkpoints/${threadId}/${runId}/cp-1`);
1070
1074
  const releaseRunSlot = await this.acquireRunSlot(threadId, runId, "resuming");
1071
1075
  try {
@@ -1073,9 +1077,7 @@ export class AgentHarnessRuntime {
1073
1077
  kind: "approval-decision",
1074
1078
  savedAt: new Date().toISOString(),
1075
1079
  checkpointRef: `checkpoints/${threadId}/${runId}/cp-1`,
1076
- resumePayload: options.decision === "edit" && options.editedInput
1077
- ? { decision: "edit", editedInput: options.editedInput }
1078
- : (options.decision ?? "approve"),
1080
+ resumePayload,
1079
1081
  attempts: 0,
1080
1082
  });
1081
1083
  await this.emit(threadId, runId, 5, "run.resumed", {
@@ -1095,11 +1097,8 @@ export class AgentHarnessRuntime {
1095
1097
  const history = await this.persistence.listThreadMessages(threadId);
1096
1098
  const priorHistory = history.filter((message) => message.runId !== runId);
1097
1099
  const runInput = await this.loadRunInput(threadId, runId);
1098
- const resumeDecision = options.decision === "edit" && options.editedInput
1099
- ? { decision: "edit", editedInput: options.editedInput }
1100
- : (options.decision ?? "approve");
1101
1100
  try {
1102
- const actual = await this.runtimeAdapter.invoke(binding, "", threadId, runId, resumeDecision, priorHistory);
1101
+ const actual = await this.runtimeAdapter.invoke(binding, "", threadId, runId, resumePayload, priorHistory);
1103
1102
  await this.persistence.clearRecoveryIntent(threadId, runId);
1104
1103
  const finalized = await this.finalizeContinuedRun(threadId, runId, runInput, actual, {
1105
1104
  previousState: "resuming",
@@ -1120,6 +1119,43 @@ export class AgentHarnessRuntime {
1120
1119
  releaseRunSlot();
1121
1120
  }
1122
1121
  }
1122
+ buildResumePayload(binding, approval, options) {
1123
+ if (!isDeepAgentBinding(binding)) {
1124
+ return options.decision === "edit" && options.editedInput
1125
+ ? { decision: "edit", editedInput: options.editedInput }
1126
+ : (options.decision ?? "approve");
1127
+ }
1128
+ const decisionType = options.decision ?? "approve";
1129
+ if (decisionType === "edit" && options.editedInput) {
1130
+ return {
1131
+ decisions: [
1132
+ {
1133
+ type: "edit",
1134
+ editedAction: {
1135
+ name: approval.toolName,
1136
+ args: options.editedInput,
1137
+ },
1138
+ },
1139
+ ],
1140
+ };
1141
+ }
1142
+ if (decisionType === "reject") {
1143
+ return {
1144
+ decisions: [
1145
+ {
1146
+ type: "reject",
1147
+ },
1148
+ ],
1149
+ };
1150
+ }
1151
+ return {
1152
+ decisions: [
1153
+ {
1154
+ type: "approve",
1155
+ },
1156
+ ],
1157
+ };
1158
+ }
1123
1159
  async restartConversation(options) {
1124
1160
  const thread = await this.getSession(options.threadId);
1125
1161
  if (!thread) {
@@ -130,23 +130,26 @@ export function createPendingApproval(threadId, runId, checkpointRef, input, int
130
130
  }
131
131
  export function inferRoutingBindings(workspace) {
132
132
  const hostBindings = Array.from(workspace.bindings.values()).filter((binding) => binding.harnessRuntime.hostFacing);
133
- const researchBinding = hostBindings.find((binding) => binding.agent.id === "research-lite" || binding.agent.id === "research");
134
- const directBinding = hostBindings.find((binding) => binding.agent.id === "direct");
135
- const delegationHosts = hostBindings.filter((binding) => isDelegationCapableBinding(binding));
136
- const lightweightHosts = hostBindings.filter((binding) => !isDelegationCapableBinding(binding));
137
- const defaultOrchestratingHost = hostBindings.find((binding) => binding.agent.id === "orchestra") ??
133
+ const deepAgentHosts = hostBindings.filter((binding) => binding.agent.executionMode === "deepagent" || Boolean(binding.deepAgentParams));
134
+ const routingHosts = deepAgentHosts.length > 0 ? deepAgentHosts : hostBindings;
135
+ const researchBinding = routingHosts.find((binding) => binding.agent.id === "research-lite" || binding.agent.id === "research");
136
+ const directBinding = routingHosts.find((binding) => binding.agent.id === "direct");
137
+ const delegationHosts = routingHosts.filter((binding) => isDelegationCapableBinding(binding));
138
+ const lightweightHosts = routingHosts.filter((binding) => !isDelegationCapableBinding(binding));
139
+ const defaultOrchestratingHost = routingHosts.find((binding) => binding.agent.id === "orchestra") ??
138
140
  delegationHosts.find((binding) => (binding.deepAgentParams?.subagents.length ?? 0) > 0) ??
139
141
  delegationHosts[0];
140
142
  const delegationPreferredSecondary = delegationHosts.find((binding) => (binding.deepAgentParams?.subagents.length ?? 0) > 0) ??
141
143
  delegationHosts[0];
142
144
  const genericLightweightHost = lightweightHosts.find((binding) => binding.agent.id !== researchBinding?.agent.id);
143
- const primaryBinding = defaultOrchestratingHost ?? directBinding ?? genericLightweightHost ?? hostBindings[0];
145
+ const primaryBinding = defaultOrchestratingHost ?? directBinding ?? genericLightweightHost ?? routingHosts[0] ?? hostBindings[0];
144
146
  const secondaryBinding = genericLightweightHost && genericLightweightHost.agent.id !== primaryBinding?.agent.id
145
147
  ? genericLightweightHost
146
148
  : directBinding && directBinding.agent.id !== primaryBinding?.agent.id
147
149
  ? directBinding
148
150
  : delegationPreferredSecondary && delegationPreferredSecondary.agent.id !== primaryBinding?.agent.id
149
151
  ? delegationPreferredSecondary
150
- : hostBindings.find((binding) => binding.agent.id !== primaryBinding?.agent.id);
152
+ : routingHosts.find((binding) => binding.agent.id !== primaryBinding?.agent.id) ??
153
+ (deepAgentHosts.length > 0 ? undefined : hostBindings.find((binding) => binding.agent.id !== primaryBinding?.agent.id));
151
154
  return { primaryBinding, secondaryBinding, researchBinding, hostBindings };
152
155
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.66",
3
+ "version": "0.0.67",
4
4
  "description": "Workspace runtime for multi-agent applications",
5
5
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",