@botbotgo/agent-harness 0.0.350 → 0.0.352

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,2 +1,2 @@
1
- export declare const AGENT_HARNESS_VERSION = "0.0.350";
1
+ export declare const AGENT_HARNESS_VERSION = "0.0.352";
2
2
  export declare const AGENT_HARNESS_RELEASE_DATE = "2026-04-24";
@@ -1,2 +1,2 @@
1
- export const AGENT_HARNESS_VERSION = "0.0.350";
1
+ export const AGENT_HARNESS_VERSION = "0.0.352";
2
2
  export const AGENT_HARNESS_RELEASE_DATE = "2026-04-24";
@@ -1 +1,5 @@
1
- Your previous response was rejected because it claimed execution without any tool calls. Your next response must contain real tool calls only. If this is a multi-step task and the runtime todo board does not exist yet, call write_todos first. If the todo board already exists or prior tool evidence is already available, do not restart planning or ask the user for more details; continue with the next concrete tool call instead. Do not describe completed work until the tool calls have actually run.
1
+ Your previous response was rejected because it did not produce concrete execution evidence from the tools configured for this agent. Your next response must contain real execution tool calls only.
2
+
3
+ Do not call planning-only tools such as write_todos or read_todos in this retry. Do not restart the plan, ask for more details, or describe completed work. Select one configured non-planning tool that can advance the original request, call it with concrete arguments, and then continue from that tool result.
4
+
5
+ If this agent has no configured non-planning tool that can advance the original request, return a blocker instead of pretending the work is complete.
@@ -5,6 +5,8 @@ import { invokeRuntimeWithLocalTools } from "./invoke-runtime.js";
5
5
  import { buildInvocationRequest } from "../model/invocation-request.js";
6
6
  import { UPSTREAM_REQUEST_CONFIG_KEY, UPSTREAM_SESSION_CONFIG_KEY } from "../upstream-configurable-keys.js";
7
7
  import { extractVisibleOutput, tryParseJson } from "../../parsing/output-parsing.js";
8
+ import { salvageJsonToolCalls } from "../../parsing/output-tool-args.js";
9
+ import { isEmptyFinalAiMessageError } from "../resilience.js";
8
10
  function readBindingExecutionParams(binding) {
9
11
  const params = binding.execution?.params ?? binding.deepAgentParams ?? binding.langchainAgentParams;
10
12
  return {
@@ -48,6 +50,34 @@ function hasNativeTaskDelegationIntent(value) {
48
50
  return hasNativeTaskDelegationIntent(typed.tool_calls) || hasNativeTaskDelegationIntent(typed.messages);
49
51
  }
50
52
  function readStructuredToolCall(value) {
53
+ const salvaged = salvageJsonToolCalls(value)[0];
54
+ if (salvaged) {
55
+ return salvaged;
56
+ }
57
+ if (Array.isArray(value)) {
58
+ for (const item of value) {
59
+ const nested = readStructuredToolCall(item);
60
+ if (nested) {
61
+ return nested;
62
+ }
63
+ }
64
+ return null;
65
+ }
66
+ if (typeof value === "object" && value !== null) {
67
+ const typed = value;
68
+ const fromOutput = typed.output !== undefined ? readStructuredToolCall(typed.output) : null;
69
+ if (fromOutput) {
70
+ return fromOutput;
71
+ }
72
+ const fromContent = typed.content !== undefined ? readStructuredToolCall(typed.content) : null;
73
+ if (fromContent) {
74
+ return fromContent;
75
+ }
76
+ const fromMessages = typed.messages !== undefined ? readStructuredToolCall(typed.messages) : null;
77
+ if (fromMessages) {
78
+ return fromMessages;
79
+ }
80
+ }
51
81
  const text = typeof value === "string" ? value.trim() : "";
52
82
  const parsed = text ? (tryParseJson(text) ?? extractFirstJsonObject(text)) : value;
53
83
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
@@ -116,7 +146,7 @@ async function replayStructuredTaskToolCall(input) {
116
146
  return input.invocation;
117
147
  }
118
148
  const visibleOutput = extractVisibleOutput(input.invocation.result);
119
- const toolCall = readStructuredToolCall(visibleOutput);
149
+ const toolCall = readStructuredToolCall(visibleOutput) ?? readStructuredToolCall(input.invocation.result);
120
150
  if (toolCall?.name !== "task") {
121
151
  return input.invocation;
122
152
  }
@@ -179,6 +209,20 @@ function buildDelegationOnlyRecoveryInstruction(binding, input) {
179
209
  JSON.stringify(input),
180
210
  ].join("\n");
181
211
  }
212
+ function buildEmptyAssistantRecoveryInstruction() {
213
+ return [
214
+ "/no_think",
215
+ "Runtime correction: your previous assistant response was empty.",
216
+ "Continue the original request now.",
217
+ "If an available tool is needed, call exactly one valid tool with complete arguments.",
218
+ "If no tool is needed, return a non-empty final answer.",
219
+ "Do not return empty content.",
220
+ ].join("\n");
221
+ }
222
+ function isEmptyAssistantOutputError(error) {
223
+ const message = error instanceof Error ? error.message : String(error);
224
+ return isEmptyFinalAiMessageError(error) || message === "empty_final_output";
225
+ }
182
226
  function appendUserRecoveryInstruction(input, instruction) {
183
227
  const content = [
184
228
  "Runtime correction:",
@@ -246,12 +290,39 @@ export async function executeRequestInvocation(options) {
246
290
  if (!result) {
247
291
  throw new Error("Agent invocation returned no result");
248
292
  }
249
- return finalizeRequestResult({
250
- bindingAgentId: options.binding.agent.id,
251
- sessionId: options.sessionId,
252
- requestId: options.requestId,
253
- result,
254
- executedToolResults,
255
- });
293
+ try {
294
+ return finalizeRequestResult({
295
+ bindingAgentId: options.binding.agent.id,
296
+ sessionId: options.sessionId,
297
+ requestId: options.requestId,
298
+ binding: options.binding,
299
+ result,
300
+ executedToolResults,
301
+ });
302
+ }
303
+ catch (error) {
304
+ if (options.resumePayload !== undefined || !isEmptyAssistantOutputError(error)) {
305
+ throw error;
306
+ }
307
+ const shouldUseDelegationRecovery = isDelegationOnlyBinding(options.binding)
308
+ && !hasTaskDelegationEvidence(executedToolResults)
309
+ && !hasNativeTaskDelegationIntent(result);
310
+ const recoveredRequest = appendUserRecoveryInstruction(request, shouldUseDelegationRecovery
311
+ ? buildDelegationOnlyRecoveryInstruction(options.binding, options.input)
312
+ : buildEmptyAssistantRecoveryInstruction());
313
+ const recoveredInvocation = await replayStructuredTaskToolCall({
314
+ invocation: await invokeOnce(recoveredRequest),
315
+ builtinExecutableTools: builtinExecutableTools,
316
+ toolRuntimeContext: invokeOptions.toolRuntimeContext,
317
+ });
318
+ return finalizeRequestResult({
319
+ bindingAgentId: options.binding.agent.id,
320
+ sessionId: options.sessionId,
321
+ requestId: options.requestId,
322
+ binding: options.binding,
323
+ result: recoveredInvocation.result,
324
+ executedToolResults: recoveredInvocation.executedToolResults,
325
+ });
326
+ }
256
327
  }
257
328
  export const executeRuntimeInvocation = executeRequestInvocation;
@@ -16,6 +16,13 @@ export declare function finalizeRequestResult(params: {
16
16
  bindingAgentId: string;
17
17
  sessionId: string;
18
18
  requestId: string;
19
+ binding?: {
20
+ harnessRuntime?: {
21
+ executionContract?: {
22
+ requiresPlan?: boolean;
23
+ };
24
+ };
25
+ };
19
26
  result: Record<string, unknown>;
20
27
  executedToolResults: ExecutedToolResult[];
21
28
  }): RequestResult;
@@ -1,5 +1,5 @@
1
1
  import { containsLikelySkillDocument, extractContentBlocks, extractEmptyAssistantMessageFailure, extractOutputContent, extractToolFallbackContext, extractVisibleOutput, isLikelyToolArgsObject, sanitizeVisibleText, tryParseJson, } from "../parsing/output-parsing.js";
2
- import { salvageFunctionLikeToolCall } from "../parsing/output-tool-args.js";
2
+ import { salvageFunctionLikeToolCall, salvageJsonToolCalls } from "../parsing/output-tool-args.js";
3
3
  import { buildStateSnapshot } from "./model/message-assembly.js";
4
4
  import { asRecord } from "./tool/resolved-tool.js";
5
5
  import { renderToolFailure } from "../support/harness-support.js";
@@ -12,9 +12,30 @@ function looksLikeLeakedToolCallText(value) {
12
12
  if (salvageFunctionLikeToolCall(normalized)) {
13
13
  return true;
14
14
  }
15
+ if (salvageJsonToolCalls(normalized).length > 0) {
16
+ return true;
17
+ }
15
18
  const prefixedToolCallMatch = /^(?:\s*(?:Ready|Understood|Okay|Ok|Got it|Sure|All set|What is your request|Please provide a task for me to orchestrate)[.:?!]?\s*)+([A-Za-z_][A-Za-z0-9_]*\([\s\S]*\))\s*$/u.exec(normalized);
16
19
  return !!(prefixedToolCallMatch && salvageFunctionLikeToolCall(prefixedToolCallMatch[1]));
17
20
  }
21
+ function hasIncompleteStateSnapshotPlan(stateSnapshot) {
22
+ if (typeof stateSnapshot !== "object" || stateSnapshot === null) {
23
+ return false;
24
+ }
25
+ const todos = stateSnapshot.todos;
26
+ if (!Array.isArray(todos)) {
27
+ return false;
28
+ }
29
+ return todos.some((todo) => {
30
+ if (typeof todo !== "object" || todo === null) {
31
+ return false;
32
+ }
33
+ const status = typeof todo.status === "string"
34
+ ? todo.status.trim().toLowerCase()
35
+ : "";
36
+ return status === "pending" || status === "in_progress";
37
+ });
38
+ }
18
39
  function isPlaceholderTaskCompletion(value) {
19
40
  const normalized = sanitizeVisibleText(value).trim();
20
41
  return normalized === "Task completed";
@@ -113,6 +134,62 @@ function extractLatestSuccessfulNonTodoToolResultText(executedToolResults) {
113
134
  ?? candidates.at(-1)
114
135
  ?? "";
115
136
  }
137
+ function readSerializedMessageType(value) {
138
+ if (typeof value !== "object" || value === null) {
139
+ return "";
140
+ }
141
+ const id = value.id;
142
+ if (!Array.isArray(id)) {
143
+ return "";
144
+ }
145
+ return id.map((item) => typeof item === "string" ? item : "").filter(Boolean).join(".");
146
+ }
147
+ function readToolMessageRecord(value) {
148
+ if (typeof value !== "object" || value === null) {
149
+ return null;
150
+ }
151
+ const typed = value;
152
+ const messageType = readSerializedMessageType(value);
153
+ const kwargs = typeof typed.kwargs === "object" && typed.kwargs !== null ? typed.kwargs : undefined;
154
+ const isToolMessage = typed.role === "tool"
155
+ || typed.type === "tool"
156
+ || messageType.endsWith("ToolMessage")
157
+ || kwargs?.name !== undefined && kwargs?.content !== undefined;
158
+ if (!isToolMessage) {
159
+ return null;
160
+ }
161
+ const toolNameCandidate = kwargs?.name ?? typed.name;
162
+ const toolName = typeof toolNameCandidate === "string" ? toolNameCandidate.trim() : "";
163
+ if (!toolName) {
164
+ return null;
165
+ }
166
+ const output = kwargs?.content ?? typed.content ?? "";
167
+ const status = kwargs?.status ?? typed.status;
168
+ const outputText = typeof output === "string" ? output : extractVisibleOutput(output);
169
+ return {
170
+ toolName,
171
+ output,
172
+ ...(status === "error" || looksLikeToolBlocker(outputText) ? { isError: true } : {}),
173
+ };
174
+ }
175
+ function extractUpstreamToolResults(value, seen = new Set()) {
176
+ if (typeof value !== "object" || value === null || seen.has(value)) {
177
+ return [];
178
+ }
179
+ seen.add(value);
180
+ if (Array.isArray(value)) {
181
+ return value.flatMap((item) => extractUpstreamToolResults(item, seen));
182
+ }
183
+ const direct = readToolMessageRecord(value);
184
+ if (direct) {
185
+ return [direct];
186
+ }
187
+ const typed = value;
188
+ return [
189
+ ...extractUpstreamToolResults(typed.messages, seen),
190
+ ...extractUpstreamToolResults(typed.output, seen),
191
+ ];
192
+ }
116
193
  function hasDelegationBlocker(executedToolResults) {
117
194
  return executedToolResults.some((toolResult) => {
118
195
  if (toolResult.toolName !== "task") {
@@ -236,7 +313,11 @@ export function extractToolResultFindingsText(executedToolResults) {
236
313
  return extractLatestSuccessfulNonTodoToolResultText(executedToolResults);
237
314
  }
238
315
  export function finalizeRequestResult(params) {
239
- const { bindingAgentId, sessionId, requestId, result, executedToolResults } = params;
316
+ const { bindingAgentId, sessionId, requestId, binding, result, executedToolResults } = params;
317
+ const allExecutedToolResults = [
318
+ ...executedToolResults,
319
+ ...extractUpstreamToolResults(result),
320
+ ];
240
321
  const interruptContent = Array.isArray(result.__interrupt__) && result.__interrupt__.length > 0 ? JSON.stringify(result.__interrupt__) : undefined;
241
322
  const extractedOutput = extractVisibleOutput(result);
242
323
  const visibleOutput = extractedOutput && !isLikelyToolArgsObject(tryParseJson(extractedOutput)) ? extractedOutput : "";
@@ -257,7 +338,7 @@ export function finalizeRequestResult(params) {
257
338
  && contentBlocks.length === 0
258
339
  && structuredResponse === undefined
259
340
  && !files
260
- && executedToolResults.length === 0
341
+ && allExecutedToolResults.length === 0
261
342
  && hasEmptyFinalMessage(result)
262
343
  && !hasFinalMessageToolCalls(result)) {
263
344
  throw new Error("empty_final_output");
@@ -266,20 +347,27 @@ export function finalizeRequestResult(params) {
266
347
  const output = resolveDeterministicFinalOutput({
267
348
  visibleOutput,
268
349
  toolFallback,
269
- executedToolResults,
350
+ executedToolResults: allExecutedToolResults,
270
351
  })
271
352
  || (containsLikelySkillDocument(result) ? "" : serializedResult);
272
353
  const finalMessageText = sanitizeVisibleText(output);
273
354
  const terminalStatus = structuredTerminalStatus ?? readTerminalExecutionStatus(finalMessageText);
274
355
  const stateSnapshot = buildStateSnapshot(result);
275
- const memoryCandidates = executedToolResults.flatMap((toolResult) => toolResult.memoryCandidates ?? []);
356
+ const hasIncompleteRequiredPlan = binding?.harnessRuntime?.executionContract?.requiresPlan === true
357
+ && hasIncompleteStateSnapshotPlan(stateSnapshot);
358
+ const hasTerminalToolBlocker = looksLikeToolBlocker(finalMessageText);
359
+ const memoryCandidates = allExecutedToolResults.flatMap((toolResult) => toolResult.memoryCandidates ?? []);
276
360
  return {
277
361
  sessionId,
278
362
  requestId,
279
363
  agentId: bindingAgentId,
280
364
  state: Array.isArray(result.__interrupt__) && result.__interrupt__.length > 0
281
365
  ? "waiting_for_approval"
282
- : mapTerminalStatusToRequestState(terminalStatus),
366
+ : hasIncompleteRequiredPlan
367
+ ? "failed"
368
+ : hasTerminalToolBlocker
369
+ ? "failed"
370
+ : mapTerminalStatusToRequestState(terminalStatus),
283
371
  interruptContent,
284
372
  output: finalMessageText,
285
373
  finalMessageText,
@@ -287,7 +375,7 @@ export function finalizeRequestResult(params) {
287
375
  ...(contentBlocks.length > 0 ? { contentBlocks } : {}),
288
376
  ...(structuredResponse !== undefined ? { structuredResponse } : {}),
289
377
  metadata: {
290
- ...(executedToolResults.length > 0 ? { executedToolResults } : {}),
378
+ ...(allExecutedToolResults.length > 0 ? { executedToolResults: allExecutedToolResults } : {}),
291
379
  ...(memoryCandidates.length > 0 ? { memoryCandidates } : {}),
292
380
  ...(structuredResponse !== undefined ? { structuredResponse } : {}),
293
381
  ...(terminalStatus ? { terminalStatus } : {}),
@@ -4,7 +4,8 @@ import { canReplayToolCallsLocally } from "./tool/tool-replay.js";
4
4
  import { extractToolCallsFromResult, normalizeToolArgsForSchema, stringifyToolOutput } from "./tool/tool-arguments.js";
5
5
  import { extractMemoryCandidatesFromToolOutput } from "../harness/system/runtime-memory-candidates.js";
6
6
  import { maybePersistLargeToolOutput } from "./tool/tool-output-artifacts.js";
7
- import { appendToolRecoveryInstruction, extractVisibleOutput, resolveMissingPlanRecoveryInstruction, resolveExecutionWithoutToolEvidenceTextInstruction, sanitizeVisibleText, } from "../parsing/output-parsing.js";
7
+ import { appendToolRecoveryInstruction, extractVisibleOutput, resolveMissingPlanRecoveryInstruction, resolveExecutionWithoutToolEvidenceTextInstruction, resolveToolCallRecoveryInstruction, sanitizeVisibleText, STRICT_TOOL_JSON_INSTRUCTION, } from "../parsing/output-parsing.js";
8
+ import { salvageJsonToolCalls } from "../parsing/output-tool-args.js";
8
9
  import { AUTONOMOUS_INVESTIGATION_RECOVERY_INSTRUCTION } from "../prompts/runtime-prompts.js";
9
10
  const TOOL_FOLLOW_UP_INSTRUCTION = "One or more tool results are already available in this conversation. Answer the user's current request directly from the existing context and tool results. Do not ask the user to repeat inputs that are already present above.";
10
11
  function readPlanStateSummary(output) {
@@ -43,6 +44,17 @@ function hasNonTodoToolEvidence(executedToolResults) {
43
44
  function hasPlanStateEvidence(executedToolResults) {
44
45
  return executedToolResults.some((item) => item.toolName === "write_todos" || item.toolName === "read_todos" || readPlanStateSummary(item.output) !== null);
45
46
  }
47
+ function latestToolErrorRecoveryInstruction(executedToolResults) {
48
+ const latest = executedToolResults.at(-1);
49
+ if (!latest || latest.isError !== true) {
50
+ return null;
51
+ }
52
+ const message = typeof latest.output === "string" ? latest.output : JSON.stringify(latest.output);
53
+ return resolveToolCallRecoveryInstruction(new Error(message)) ?? AUTONOMOUS_INVESTIGATION_RECOVERY_INSTRUCTION;
54
+ }
55
+ function terminalToolErrorRecoveryInstruction(terminalText) {
56
+ return resolveToolCallRecoveryInstruction(new Error(terminalText));
57
+ }
46
58
  function requiresPlanEvidence(binding) {
47
59
  return binding.harnessRuntime.executionContract?.requiresPlan === true;
48
60
  }
@@ -83,18 +95,24 @@ export async function runLocalToolInvocationLoop({ binding, request, primaryTool
83
95
  if (toolCalls.length === 0) {
84
96
  const terminalText = sanitizeVisibleText(extractVisibleOutput(result) || "");
85
97
  const hasIncompletePlanState = hasIncompleteExecutedPlan(executedToolResults);
98
+ const shouldEnforceIncompletePlan = requiresPlanEvidence(binding) && hasIncompletePlanState;
86
99
  const hasExecutionBeyondTodoPlanning = hasNonTodoToolEvidence(executedToolResults);
87
- const recoveryInstruction = terminalText
100
+ const toolErrorRecoveryInstruction = latestToolErrorRecoveryInstruction(executedToolResults)
101
+ ?? terminalToolErrorRecoveryInstruction(terminalText);
102
+ const leakedJsonToolCallRecoveryInstruction = terminalText && salvageJsonToolCalls(terminalText).length > 0
103
+ ? STRICT_TOOL_JSON_INSTRUCTION
104
+ : null;
105
+ const recoveryInstruction = toolErrorRecoveryInstruction ?? leakedJsonToolCallRecoveryInstruction ?? (terminalText
88
106
  ? resolveExecutionWithoutToolEvidenceTextInstruction(activeRequest, terminalText, false, {
89
107
  hasWriteTodosEvidence: executedToolResults.some((item) => item.toolName === "write_todos"),
90
108
  hasToolResultEvidence: hasExecutionBeyondTodoPlanning,
91
109
  hasPlanStateEvidence: hasPlanStateEvidence(executedToolResults),
92
- hasIncompletePlanState: hasExecutionBeyondTodoPlanning && hasIncompletePlanState,
110
+ hasIncompletePlanState: shouldEnforceIncompletePlan,
93
111
  requiresPlan: requiresPlanEvidence(binding),
94
112
  })
95
- : hasIncompletePlanState && hasExecutionBeyondTodoPlanning
113
+ : shouldEnforceIncompletePlan
96
114
  ? AUTONOMOUS_INVESTIGATION_RECOVERY_INSTRUCTION
97
- : null;
115
+ : null);
98
116
  if (recoveryInstruction) {
99
117
  if (iteration + 1 === maxToolIterations) {
100
118
  throw new Error(`Tool-calling loop exceeded the maximum of ${maxToolIterations} iterations`);
@@ -9,9 +9,35 @@ import { resolveDeclaredMiddleware } from "./tool/declared-middleware.js";
9
9
  import { UPSTREAM_SESSION_CONFIG_KEY } from "./upstream-configurable-keys.js";
10
10
  import { bindingHasLangChainSubagentSupport, bindingHasMiddlewareKind, getBindingBuiltinToolsConfig, getBindingExecutionKind, getBindingGeneralPurposeAgent, getBindingDeepAgentSubagents, getBindingInterruptCompatibilityRules, getBindingMiddlewareConfigs, getBindingMemorySources, getBindingPrimaryModel, getBindingPrimaryTools, getBindingSkills, getBindingSubagents, getBindingTaskDescription, isDeepAgentBinding, isLangChainBinding, } from "../support/compiled-binding.js";
11
11
  import { materializeDeepAgentSkillSourcePaths } from "./compat/deepagent-compat.js";
12
- import { DEFAULT_SUBAGENT_PROMPT } from "../prompts/runtime-prompts.js";
12
+ import { DEFAULT_SUBAGENT_PROMPT, EXECUTION_WITH_TOOL_EVIDENCE_RETRY_INSTRUCTION, } from "../prompts/runtime-prompts.js";
13
13
  import { createContextHygieneMiddleware } from "./middleware/context-hygiene.js";
14
14
  const INVALID_TOOL_MESSAGE_BLOCK_TYPES = new Set(["tool_use", "thinking", "redacted_thinking"]);
15
+ const DEFAULT_BUILTIN_TASK_TIMEOUT_MS = 180_000;
16
+ function resolveBuiltinTaskTimeoutMs(model) {
17
+ const timeout = model?.init?.timeout;
18
+ return typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0
19
+ ? Math.floor(timeout)
20
+ : DEFAULT_BUILTIN_TASK_TIMEOUT_MS;
21
+ }
22
+ async function withBuiltinTaskTimeout(producer, timeoutMs, subagentName) {
23
+ let timeoutHandle;
24
+ try {
25
+ return await Promise.race([
26
+ producer(),
27
+ new Promise((_, reject) => {
28
+ timeoutHandle = setTimeout(() => {
29
+ reject(new Error(`Delegated agent ${subagentName} timed out after ${timeoutMs}ms.`));
30
+ }, timeoutMs);
31
+ timeoutHandle.unref?.();
32
+ }),
33
+ ]);
34
+ }
35
+ finally {
36
+ if (timeoutHandle) {
37
+ clearTimeout(timeoutHandle);
38
+ }
39
+ }
40
+ }
15
41
  function extractDeepAgentTaskContent(result) {
16
42
  if (typeof result !== "object" || result === null) {
17
43
  return undefined;
@@ -32,6 +58,92 @@ function extractDeepAgentTaskContent(result) {
32
58
  }
33
59
  return undefined;
34
60
  }
61
+ function readMessageType(message) {
62
+ if (typeof message !== "object" || message === null) {
63
+ return "";
64
+ }
65
+ if (typeof message._getType === "function") {
66
+ const typeName = message._getType();
67
+ return typeof typeName === "string" ? typeName : "";
68
+ }
69
+ const typed = message;
70
+ if (typeof typed.type === "string") {
71
+ return typed.type;
72
+ }
73
+ const kwargs = typeof typed.kwargs === "object" && typed.kwargs !== null
74
+ ? typed.kwargs
75
+ : undefined;
76
+ if (typeof kwargs?.type === "string") {
77
+ return kwargs.type;
78
+ }
79
+ const lcKwargs = typeof typed.lc_kwargs === "object" && typed.lc_kwargs !== null
80
+ ? typed.lc_kwargs
81
+ : undefined;
82
+ return typeof lcKwargs?.type === "string" ? lcKwargs.type : "";
83
+ }
84
+ function readMessageName(message) {
85
+ if (typeof message !== "object" || message === null) {
86
+ return "";
87
+ }
88
+ const typed = message;
89
+ if (typeof typed.name === "string") {
90
+ return typed.name;
91
+ }
92
+ const kwargs = typeof typed.kwargs === "object" && typed.kwargs !== null
93
+ ? typed.kwargs
94
+ : undefined;
95
+ if (typeof kwargs?.name === "string") {
96
+ return kwargs.name;
97
+ }
98
+ const lcKwargs = typeof typed.lc_kwargs === "object" && typed.lc_kwargs !== null
99
+ ? typed.lc_kwargs
100
+ : undefined;
101
+ return typeof lcKwargs?.name === "string" ? lcKwargs.name : "";
102
+ }
103
+ function readMessages(result) {
104
+ if (typeof result !== "object" || result === null) {
105
+ return [];
106
+ }
107
+ const messages = result.messages;
108
+ return Array.isArray(messages) ? messages : [];
109
+ }
110
+ function readToolNames(tools) {
111
+ if (!Array.isArray(tools)) {
112
+ return new Set();
113
+ }
114
+ return new Set(tools
115
+ .map((tool) => {
116
+ if (typeof tool !== "object" || tool === null) {
117
+ return "";
118
+ }
119
+ const name = tool.name;
120
+ return typeof name === "string" ? name : "";
121
+ })
122
+ .filter((name) => name.length > 0));
123
+ }
124
+ function hasSubagentExecutionToolEvidence(result, resolvedTools, configuredTools) {
125
+ const requiredToolNames = new Set([
126
+ ...readToolNames(configuredTools),
127
+ ...readToolNames(resolvedTools),
128
+ ]);
129
+ if (requiredToolNames.size === 0) {
130
+ return true;
131
+ }
132
+ for (const message of readMessages(result)) {
133
+ const typeName = readMessageType(message);
134
+ if (typeName !== "tool" && typeName !== "ToolMessage") {
135
+ continue;
136
+ }
137
+ const name = readMessageName(message);
138
+ if (name === "write_todos" || name === "read_todos") {
139
+ continue;
140
+ }
141
+ if (requiredToolNames.has(name)) {
142
+ return true;
143
+ }
144
+ }
145
+ return false;
146
+ }
35
147
  export function extractSubagentRequestText(state) {
36
148
  if (!isRecord(state)) {
37
149
  return "";
@@ -257,6 +369,7 @@ export async function invokeBuiltinTaskTool(input) {
257
369
  const builtinBackend = input.resolveBuiltinMiddlewareBackend(input.binding, input.options);
258
370
  const resolvedSubagents = await input.resolveSubagents(compiledSubagents, input.binding);
259
371
  const selectedSubagent = resolvedSubagents.find((subagent) => subagent.name === subagentType);
372
+ const selectedCompiledSubagent = compiledSubagents.find((subagent) => subagent.name === subagentType);
260
373
  if (!selectedSubagent) {
261
374
  const allowed = resolvedSubagents.map((subagent) => subagent.name);
262
375
  throw new Error(`Error: invoked agent of type ${subagentType}, the only allowed types are ${allowed.map((name) => `\`${name}\``).join(", ")}`);
@@ -282,9 +395,17 @@ export async function invokeBuiltinTaskTool(input) {
282
395
  configurable: { [UPSTREAM_SESSION_CONFIG_KEY]: `${input.binding.agent.id}:builtin-task` },
283
396
  ...(input.options?.context ? { context: input.options.context } : {}),
284
397
  };
285
- const result = await runnable.invoke({
286
- messages: [new HumanMessage({ content: description })],
287
- }, invokeConfig);
398
+ const taskTimeoutMs = resolveBuiltinTaskTimeoutMs(selectedCompiledSubagent?.model ?? primaryModel);
399
+ const invokeSubagent = (content) => withBuiltinTaskTimeout(() => runnable.invoke({
400
+ messages: [new HumanMessage({ content })],
401
+ }, invokeConfig), taskTimeoutMs, selectedSubagent.name);
402
+ let result = await invokeSubagent(description);
403
+ if (!hasSubagentExecutionToolEvidence(result, resolvedSubagentTools, selectedCompiledSubagent?.tools)) {
404
+ result = await invokeSubagent([description, EXECUTION_WITH_TOOL_EVIDENCE_RETRY_INSTRUCTION].filter(Boolean).join("\n\n"));
405
+ if (!hasSubagentExecutionToolEvidence(result, resolvedSubagentTools, selectedCompiledSubagent?.tools)) {
406
+ throw new Error(`Delegated agent ${selectedSubagent.name} completed without tool execution evidence.`);
407
+ }
408
+ }
288
409
  const structuredResponse = typeof result === "object" && result !== null && "structuredResponse" in result
289
410
  ? result.structuredResponse
290
411
  : undefined;
@@ -8,5 +8,6 @@ export type ProviderRetryPolicy = {
8
8
  backoffMs: number;
9
9
  retryableMessages: string[];
10
10
  };
11
+ export declare function isEmptyFinalAiMessageError(error: unknown): boolean;
11
12
  export declare function resolveProviderRetryPolicy(binding: CompiledAgentBinding): ProviderRetryPolicy;
12
13
  export declare function isRetryableProviderError(binding: CompiledAgentBinding, error: unknown): boolean;
@@ -31,13 +31,14 @@ export function resolveStreamIdleTimeout(binding) {
31
31
  return 60_000;
32
32
  }
33
33
  const BUILTIN_RETRYABLE_PROVIDER_MESSAGES = [
34
+ "eof",
34
35
  "unexpected eof",
35
36
  "other side closed",
36
37
  "socket hang up",
37
38
  "connection reset",
38
39
  "econnreset",
39
40
  ];
40
- function isEmptyFinalAiMessageError(error) {
41
+ export function isEmptyFinalAiMessageError(error) {
41
42
  const message = error instanceof Error ? error.message : String(error);
42
43
  return message.toLowerCase().startsWith("empty_final_ai_message:");
43
44
  }
@@ -11,10 +11,10 @@ function normalizeTerminalStatus(value) {
11
11
  function readStatusLine(value) {
12
12
  for (const line of value.split("\n")) {
13
13
  const [key, ...rest] = line.split(":");
14
- if (key?.trim().toLowerCase() !== "status") {
14
+ if (key?.trim().replaceAll("*", "").toLowerCase() !== "status") {
15
15
  continue;
16
16
  }
17
- const statusValue = rest.join(":").trim().split(/\s+/)[0];
17
+ const statusValue = rest.join(":").trim().replaceAll("*", "").split(/\s+/)[0];
18
18
  const status = normalizeTerminalStatus(statusValue);
19
19
  if (status) {
20
20
  return status;
@@ -10,7 +10,7 @@ import { executeRequestInvocation } from "./adapter/flow/invocation-flow.js";
10
10
  import { streamRuntimeExecution } from "./adapter/flow/stream-runtime.js";
11
11
  import { applyToolRecoveryInstruction as applyToolRecoveryInstructionHelper, applyStrictToolJsonInstruction as applyStrictToolJsonInstructionHelper, callRuntimeWithToolParseRecovery as callRuntimeWithToolParseRecoveryHelper, createModelFallbackRunnable as createModelFallbackRunnableHelper, invokeWithProviderRetry as invokeWithProviderRetryHelper, iterateWithTimeout as iterateWithTimeoutHelper, materializeModelStream as materializeModelStreamHelper, RuntimeOperationTimeoutError, withRuntimeTimeout, } from "./adapter/runtime-shell.js";
12
12
  import { extractSubagentRequestText, invokeBuiltinTaskTool as invokeBuiltinTaskToolHelper, materializeAutomaticSummarizationMiddleware as materializeAutomaticSummarizationMiddlewareHelper, resolveBuiltinMiddlewareBackend as resolveBuiltinMiddlewareBackendHelper, resolveBuiltinMiddlewareTools as resolveBuiltinMiddlewareToolsHelper, resolveLangChainRuntimeExtensionMiddleware as resolveLangChainRuntimeExtensionMiddlewareHelper, resolveMiddleware as resolveMiddlewareHelper, resolveSubagents as resolveSubagentsHelper, wrapRequestResultAsSubagentResponse, } from "./adapter/middleware-assembly.js";
13
- import { resolveBindingTimeout, resolveStreamIdleTimeout, } from "./adapter/resilience.js";
13
+ import { isEmptyFinalAiMessageError, resolveBindingTimeout, resolveStreamIdleTimeout, } from "./adapter/resilience.js";
14
14
  import { createResolvedModel } from "./adapter/model/model-providers.js";
15
15
  import { renderDirectWorkspaceListing, shouldDirectlyListWorkspaceFiles } from "./adapter/direct-builtin-utility.js";
16
16
  import { resolveAdapterTools } from "./adapter/tool-resolution.js";
@@ -585,7 +585,7 @@ export class AgentRuntimeAdapter {
585
585
  sessionId,
586
586
  requestId,
587
587
  });
588
- return this.invokeWithProviderRetry(binding, async () => executeRequestInvocation({
588
+ const invokeRequest = () => executeRequestInvocation({
589
589
  binding,
590
590
  input,
591
591
  sessionId,
@@ -600,7 +600,17 @@ export class AgentRuntimeAdapter {
600
600
  getToolNameMapping: (currentBinding) => this.getToolNameMapping(currentBinding),
601
601
  resolveBuiltinMiddlewareTools: (currentBinding, currentOptions) => this.resolveBuiltinMiddlewareTools(currentBinding, { ...currentOptions, sessionId, requestId }),
602
602
  callRuntimeWithToolParseRecovery,
603
- }));
603
+ });
604
+ try {
605
+ return await invokeRequest();
606
+ }
607
+ catch (error) {
608
+ if (!isEmptyFinalAiMessageError(error)) {
609
+ throw error;
610
+ }
611
+ this.invalidateBindingRuntimeCaches(binding);
612
+ return invokeRequest();
613
+ }
604
614
  }
605
615
  async *stream(binding, input, sessionId, history = [], options = {}) {
606
616
  const directListing = await this.tryHandleDirectWorkspaceListing(binding, input, {
@@ -1,5 +1,6 @@
1
1
  import { EventEmitter } from "node:events";
2
2
  import { getEventSubscribers } from "../../../tooling/extensions.js";
3
+ const EVENT_PROJECTION_DRAIN_TIMEOUT_MS = 1_000;
3
4
  function dispatchListener(listener, event) {
4
5
  void Promise.resolve(listener(event));
5
6
  }
@@ -34,8 +35,24 @@ export class RuntimeEventSinkImpl {
34
35
  return () => this.projections.delete(projection);
35
36
  }
36
37
  async drain() {
37
- while (this.inflightProjectionTasks.size > 0) {
38
- await Promise.allSettled(Array.from(this.inflightProjectionTasks));
38
+ let timeoutHandle;
39
+ try {
40
+ await Promise.race([
41
+ (async () => {
42
+ while (this.inflightProjectionTasks.size > 0) {
43
+ await Promise.allSettled(Array.from(this.inflightProjectionTasks));
44
+ }
45
+ })(),
46
+ new Promise((resolve) => {
47
+ timeoutHandle = setTimeout(resolve, EVENT_PROJECTION_DRAIN_TIMEOUT_MS);
48
+ timeoutHandle.unref?.();
49
+ }),
50
+ ]);
51
+ }
52
+ finally {
53
+ if (timeoutHandle) {
54
+ clearTimeout(timeoutHandle);
55
+ }
39
56
  }
40
57
  }
41
58
  }
@@ -133,6 +133,7 @@ export declare class AgentHarnessRuntime {
133
133
  private trackBackgroundTask;
134
134
  private scheduleBackgroundStartupTask;
135
135
  private drainBackgroundTasksForClose;
136
+ private closeStageWithTimeout;
136
137
  private resolveToolMcpServerTools;
137
138
  private loadPriorHistory;
138
139
  private loadRequestInput;
@@ -47,6 +47,7 @@ import { buildRequestInspectionRecord, buildSessionInspectionRecord, deleteSessi
47
47
  import { createKnowledgeModule } from "../knowledge/index.js";
48
48
  import { createProceduralMemoryManager, ProceduralMemoryFormationSync, readProceduralMemoryRuntimeConfig, } from "../knowledge/procedural/index.js";
49
49
  const BACKGROUND_TASK_CLOSE_DRAIN_TIMEOUT_MS = 1_000;
50
+ const CLOSE_STAGE_TIMEOUT_MS = 1_000;
50
51
  const ACTIVE_REQUEST_STATES = [
51
52
  "queued",
52
53
  "claimed",
@@ -963,6 +964,22 @@ export class AgentHarnessRuntime {
963
964
  clearTimeout(timeoutHandle);
964
965
  }
965
966
  }
967
+ async closeStageWithTimeout(_label, stage) {
968
+ if (!stage) {
969
+ return;
970
+ }
971
+ let timeoutHandle;
972
+ await Promise.race([
973
+ stage().then(() => undefined).catch(() => undefined),
974
+ new Promise((resolve) => {
975
+ timeoutHandle = setTimeout(resolve, CLOSE_STAGE_TIMEOUT_MS);
976
+ timeoutHandle.unref?.();
977
+ }),
978
+ ]);
979
+ if (timeoutHandle) {
980
+ clearTimeout(timeoutHandle);
981
+ }
982
+ }
966
983
  resolveToolMcpServerTools(agentId) {
967
984
  return resolveWorkspaceAgentTools({
968
985
  workspace: this.workspace,
@@ -1386,20 +1403,20 @@ export class AgentHarnessRuntime {
1386
1403
  return;
1387
1404
  }
1388
1405
  this.closed = true;
1389
- await this.healthMonitor?.stop();
1390
- await this.eventBus.drain();
1406
+ await this.closeStageWithTimeout("healthMonitor.stop", () => this.healthMonitor?.stop() ?? Promise.resolve());
1407
+ await this.closeStageWithTimeout("eventBus.drain", () => this.eventBus.drain());
1391
1408
  this.unregisterSessionMemorySync();
1392
1409
  this.unregisterRuntimeMemorySync();
1393
1410
  this.unregisterMem0IngestionSync();
1394
1411
  this.unregisterRuntimeMemoryFormationSync();
1395
1412
  this.unregisterProceduralMemoryFormationSync();
1396
1413
  await this.drainBackgroundTasksForClose();
1397
- await this.sessionMemorySync?.close();
1398
- await this.runtimeMemorySync?.close();
1399
- await this.mem0IngestionSync?.close();
1400
- await this.runtimeMemoryFormationSync?.close();
1401
- await this.proceduralMemoryFormationSync?.close();
1402
- await closeMcpClientsForWorkspace(this.workspace);
1414
+ await this.closeStageWithTimeout("sessionMemorySync.close", () => this.sessionMemorySync?.close() ?? Promise.resolve());
1415
+ await this.closeStageWithTimeout("runtimeMemorySync.close", () => this.runtimeMemorySync?.close() ?? Promise.resolve());
1416
+ await this.closeStageWithTimeout("mem0IngestionSync.close", () => this.mem0IngestionSync?.close() ?? Promise.resolve());
1417
+ await this.closeStageWithTimeout("runtimeMemoryFormationSync.close", () => this.runtimeMemoryFormationSync?.close() ?? Promise.resolve());
1418
+ await this.closeStageWithTimeout("proceduralMemoryFormationSync.close", () => this.proceduralMemoryFormationSync?.close() ?? Promise.resolve());
1419
+ await this.closeStageWithTimeout("closeMcpClientsForWorkspace", () => closeMcpClientsForWorkspace(this.workspace));
1403
1420
  this.initialized = false;
1404
1421
  }
1405
1422
  async stop() {
@@ -1,5 +1,5 @@
1
1
  import { AIMessage } from "langchain";
2
- import { salvageFunctionLikeToolCall, salvageToolArgs, isLikelyToolArgsObject, normalizeKnownToolArgs, tryParseJson } from "./output-tool-args.js";
2
+ import { salvageFunctionLikeToolCall, salvageJsonToolCalls, salvageToolArgs, isLikelyToolArgsObject, normalizeKnownToolArgs, tryParseJson } from "./output-tool-args.js";
3
3
  function consumeLeadingFunctionLikeToolCall(value) {
4
4
  const match = /^([A-Za-z_][A-Za-z0-9_]*)\(/.exec(value);
5
5
  if (!match) {
@@ -495,8 +495,12 @@ function normalizeAgentMessage(value) {
495
495
  const functionLikeToolCall = normalizedToolCalls.length === 0 && recoveredToolCalls.length === 0 && typeof normalizedContent === "string"
496
496
  ? salvageFunctionLikeToolCall(normalizedContent)
497
497
  : null;
498
+ const jsonToolCalls = normalizedToolCalls.length === 0 && recoveredToolCalls.length === 0 && !functionLikeToolCall && typeof normalizedContent === "string"
499
+ ? salvageJsonToolCalls(normalizedContent)
500
+ : [];
501
+ const hasRecoveredContentToolCalls = Boolean(functionLikeToolCall) || jsonToolCalls.length > 0;
498
502
  return new AIMessage({
499
- content: functionLikeToolCall ? "" : normalizedContent,
503
+ content: hasRecoveredContentToolCalls ? "" : normalizedContent,
500
504
  name: typeof typed.name === "string" ? typed.name : undefined,
501
505
  additional_kwargs: typeof typed.additional_kwargs === "object" && typed.additional_kwargs ? typed.additional_kwargs : {},
502
506
  response_metadata: typeof typed.response_metadata === "object" && typed.response_metadata ? typed.response_metadata : {},
@@ -505,6 +509,7 @@ function normalizeAgentMessage(value) {
505
509
  ...normalizedToolCalls,
506
510
  ...recoveredToolCalls,
507
511
  ...(functionLikeToolCall ? [{ name: functionLikeToolCall.name, args: functionLikeToolCall.args }] : []),
512
+ ...jsonToolCalls.map((toolCall) => ({ name: toolCall.name, args: toolCall.args })),
508
513
  ],
509
514
  invalid_tool_calls: normalizedInvalidToolCalls.filter((toolCall) => toolCall.type !== "tool_call"),
510
515
  usage_metadata: typeof typed.usage_metadata === "object" && typed.usage_metadata ? typed.usage_metadata : undefined,
@@ -1,5 +1,6 @@
1
1
  import { AUTONOMOUS_INVESTIGATION_RECOVERY_INSTRUCTION, EXECUTION_WITH_TOOL_EVIDENCE_RETRY_INSTRUCTION, INTERNAL_RUNTIME_SPILL_PATH_INSTRUCTION, STRICT_TOOL_JSON_INSTRUCTION, WORKSPACE_RELATIVE_PATH_INSTRUCTION, WRITE_TODOS_DESCRIPTIVE_CONTENT_INSTRUCTION, WRITE_TODOS_FULL_ENTRY_INSTRUCTION, WRITE_TODOS_NON_EMPTY_INITIAL_LIST_INSTRUCTION, WRITE_TODOS_REQUIRED_PLAN_INSTRUCTION, } from "../prompts/runtime-prompts.js";
2
2
  import { wrapNormalizedMessage, readTextContent } from "./output-content.js";
3
+ import { salvageJsonToolCalls } from "./output-tool-args.js";
3
4
  function collectRequestMessages(request) {
4
5
  if (typeof request !== "object" || !request || Array.isArray(request)) {
5
6
  return [];
@@ -145,12 +146,15 @@ export function resolveExecutionWithoutToolEvidenceTextInstruction(request, assi
145
146
  const hasUnfinishedExecution = resultEvidence.hasIncompletePlanState === true
146
147
  || resultEvidence.hasOpenTaskDelegation === true
147
148
  || resultEvidence.hasMissingDelegatedExecutionEvidence === true;
148
- if (!normalizedText || !hasUnfinishedExecution) {
149
- return null;
149
+ if (salvageJsonToolCalls(normalizedText).length > 0) {
150
+ return STRICT_TOOL_JSON_INSTRUCTION;
150
151
  }
151
152
  const hasExecutionEvidence = toolCallEvidence
152
153
  || resultEvidence.hasWriteTodosEvidence === true
153
154
  || resultEvidence.hasToolResultEvidence === true;
155
+ if (!normalizedText || !hasUnfinishedExecution) {
156
+ return null;
157
+ }
154
158
  return hasExecutionEvidence
155
159
  ? AUTONOMOUS_INVESTIGATION_RECOVERY_INSTRUCTION
156
160
  : EXECUTION_WITH_TOOL_EVIDENCE_RETRY_INSTRUCTION;
@@ -4,5 +4,9 @@ export declare function salvageFunctionLikeToolCall(value: unknown): {
4
4
  args: Record<string, unknown>;
5
5
  } | null;
6
6
  export declare function salvageToolArgs(value: unknown): Record<string, unknown> | null;
7
+ export declare function salvageJsonToolCalls(value: unknown): Array<{
8
+ name: string;
9
+ args: Record<string, unknown>;
10
+ }>;
7
11
  export declare function normalizeKnownToolArgs(toolName: unknown, args: Record<string, unknown>): Record<string, unknown>;
8
12
  export declare function isLikelyToolArgsObject(value: unknown): boolean;
@@ -112,8 +112,8 @@ export function salvageFunctionLikeToolCall(value) {
112
112
  }
113
113
  return { name, args: normalizeKnownToolArgs(name, args) };
114
114
  }
115
- function extractBalancedJsonObject(value) {
116
- const start = value.indexOf("{");
115
+ function extractBalancedJsonValue(value, openChar, closeChar) {
116
+ const start = value.indexOf(openChar);
117
117
  if (start < 0)
118
118
  return null;
119
119
  let depth = 0;
@@ -139,11 +139,11 @@ function extractBalancedJsonObject(value) {
139
139
  inString = true;
140
140
  continue;
141
141
  }
142
- if (char === "{") {
142
+ if (char === openChar) {
143
143
  depth += 1;
144
144
  continue;
145
145
  }
146
- if (char === "}") {
146
+ if (char === closeChar) {
147
147
  depth -= 1;
148
148
  if (depth === 0) {
149
149
  return value.slice(start, index + 1);
@@ -152,6 +152,59 @@ function extractBalancedJsonObject(value) {
152
152
  }
153
153
  return null;
154
154
  }
155
+ function extractBalancedJsonObject(value) {
156
+ return extractBalancedJsonValue(value, "{", "}");
157
+ }
158
+ function extractBalancedJsonArray(value) {
159
+ return extractBalancedJsonValue(value, "[", "]");
160
+ }
161
+ function closeJsonContainerSuffix(value) {
162
+ const trimmed = value.trim();
163
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
164
+ return null;
165
+ }
166
+ const stack = [];
167
+ let inString = false;
168
+ let escaping = false;
169
+ for (const char of trimmed) {
170
+ if (inString) {
171
+ if (escaping) {
172
+ escaping = false;
173
+ continue;
174
+ }
175
+ if (char === "\\") {
176
+ escaping = true;
177
+ continue;
178
+ }
179
+ if (char === "\"") {
180
+ inString = false;
181
+ }
182
+ continue;
183
+ }
184
+ if (char === "\"") {
185
+ inString = true;
186
+ continue;
187
+ }
188
+ if (char === "{") {
189
+ stack.push("}");
190
+ continue;
191
+ }
192
+ if (char === "[") {
193
+ stack.push("]");
194
+ continue;
195
+ }
196
+ if (char === "}" || char === "]") {
197
+ const expected = stack.pop();
198
+ if (expected !== char) {
199
+ return null;
200
+ }
201
+ }
202
+ }
203
+ if (inString || stack.length === 0) {
204
+ return null;
205
+ }
206
+ return `${trimmed}${stack.reverse().join("")}`;
207
+ }
155
208
  export function salvageToolArgs(value) {
156
209
  if (typeof value === "object" && value && !Array.isArray(value)) {
157
210
  return value;
@@ -174,6 +227,63 @@ export function salvageToolArgs(value) {
174
227
  const parsed = tryParseJson(embedded);
175
228
  return typeof parsed === "object" && parsed && !Array.isArray(parsed) ? parsed : null;
176
229
  }
230
+ function normalizeJsonToolCallPayload(payload) {
231
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
232
+ return null;
233
+ }
234
+ const typed = payload;
235
+ const functionPayload = typeof typed.function === "object" && typed.function !== null ? typed.function : undefined;
236
+ const nameCandidate = typed.name ?? typed.tool ?? functionPayload?.name;
237
+ const name = typeof nameCandidate === "string" ? nameCandidate.trim() : "";
238
+ if (!name) {
239
+ return null;
240
+ }
241
+ const argsCandidate = typed.arguments ?? typed.args ?? typed.parameters ?? typed.input ?? functionPayload?.arguments ?? {};
242
+ const args = Array.isArray(argsCandidate)
243
+ ? { args: argsCandidate }
244
+ : salvageToolArgs(argsCandidate) ?? {};
245
+ return { name, args: normalizeKnownToolArgs(name, args) };
246
+ }
247
+ export function salvageJsonToolCalls(value) {
248
+ const payload = typeof value === "string"
249
+ ? (() => {
250
+ const trimmed = value.trim();
251
+ if (!trimmed) {
252
+ return null;
253
+ }
254
+ const direct = tryParseJson(trimmed);
255
+ if (direct) {
256
+ return direct;
257
+ }
258
+ const closed = closeJsonContainerSuffix(trimmed);
259
+ if (closed) {
260
+ const parsed = tryParseJson(closed);
261
+ if (parsed) {
262
+ return parsed;
263
+ }
264
+ }
265
+ const embeddedArray = extractBalancedJsonArray(trimmed);
266
+ if (embeddedArray) {
267
+ const parsed = tryParseJson(embeddedArray);
268
+ if (parsed) {
269
+ return parsed;
270
+ }
271
+ }
272
+ const embeddedObject = extractBalancedJsonObject(trimmed);
273
+ if (embeddedObject) {
274
+ const parsed = tryParseJson(embeddedObject);
275
+ if (parsed) {
276
+ return parsed;
277
+ }
278
+ }
279
+ return null;
280
+ })()
281
+ : value;
282
+ const candidates = Array.isArray(payload) ? payload : [payload];
283
+ return candidates
284
+ .map((item) => normalizeJsonToolCallPayload(item))
285
+ .filter((item) => item !== null);
286
+ }
177
287
  function normalizeWriteTodosArgs(args) {
178
288
  if (!Array.isArray(args.todos)) {
179
289
  return args;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.350",
3
+ "version": "0.0.352",
4
4
  "description": "Workspace runtime for multi-agent applications",
5
5
  "license": "MIT",
6
6
  "type": "module",