@botbotgo/agent-harness 0.0.351 → 0.0.353

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.
Files changed (32) hide show
  1. package/README.md +2 -1
  2. package/README.zh.md +2 -1
  3. package/dist/api.d.ts +3 -0
  4. package/dist/api.js +3 -0
  5. package/dist/cli/options.js +1 -0
  6. package/dist/cli/runtime-commands.js +12 -1
  7. package/dist/cli/runtime-output.d.ts +1 -0
  8. package/dist/cli/runtime-output.js +31 -0
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.js +1 -1
  11. package/dist/package-version.d.ts +1 -1
  12. package/dist/package-version.js +1 -1
  13. package/dist/resources/prompts/runtime/execution-with-tool-evidence-retry.md +5 -1
  14. package/dist/runtime/adapter/flow/invocation-flow.js +79 -8
  15. package/dist/runtime/adapter/invocation-result.d.ts +7 -0
  16. package/dist/runtime/adapter/invocation-result.js +95 -7
  17. package/dist/runtime/adapter/local-tool-invocation.js +23 -5
  18. package/dist/runtime/adapter/middleware-assembly.js +29 -2
  19. package/dist/runtime/adapter/resilience.d.ts +1 -0
  20. package/dist/runtime/adapter/resilience.js +2 -1
  21. package/dist/runtime/adapter/terminal-status.js +2 -2
  22. package/dist/runtime/agent-runtime-adapter.js +13 -3
  23. package/dist/runtime/harness/events/event-sink.js +19 -2
  24. package/dist/runtime/harness/system/boundary-analysis.d.ts +42 -0
  25. package/dist/runtime/harness/system/boundary-analysis.js +234 -0
  26. package/dist/runtime/harness.d.ts +3 -0
  27. package/dist/runtime/harness.js +29 -8
  28. package/dist/runtime/parsing/output-content.js +7 -2
  29. package/dist/runtime/parsing/output-recovery.js +6 -2
  30. package/dist/runtime/parsing/output-tool-args.d.ts +4 -0
  31. package/dist/runtime/parsing/output-tool-args.js +114 -4
  32. package/package.json +1 -1
@@ -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
  }
@@ -0,0 +1,42 @@
1
+ import type { WorkspaceBundle } from "../../../contracts/types.js";
2
+ export type BoundaryFindingSeverity = "info" | "warning" | "error";
3
+ export type BoundaryFinding = {
4
+ severity: BoundaryFindingSeverity;
5
+ code: string;
6
+ message: string;
7
+ agentId?: string;
8
+ parentAgentId?: string;
9
+ toolName?: string;
10
+ skillName?: string;
11
+ skillPath?: string;
12
+ relatedAgents?: string[];
13
+ sourcePath?: string;
14
+ };
15
+ export type BoundaryAgentSurface = {
16
+ id: string;
17
+ description: string;
18
+ tools: string[];
19
+ skills: string[];
20
+ subagents: string[];
21
+ sourcePath?: string;
22
+ parentAgentId?: string;
23
+ };
24
+ export type BoundaryAnalysisSummary = {
25
+ agentCount: number;
26
+ toolCount: number;
27
+ skillCount: number;
28
+ findingCount: number;
29
+ errorCount: number;
30
+ warningCount: number;
31
+ infoCount: number;
32
+ };
33
+ export type WorkspaceBoundaryAnalysis = {
34
+ workspaceRoot: string;
35
+ summary: BoundaryAnalysisSummary;
36
+ agents: BoundaryAgentSurface[];
37
+ findings: BoundaryFinding[];
38
+ };
39
+ export type BoundaryAnalysisOptions = {
40
+ includeInfo?: boolean;
41
+ };
42
+ export declare function analyzeWorkspaceBoundaries(workspace: WorkspaceBundle, options?: BoundaryAnalysisOptions): WorkspaceBoundaryAnalysis;
@@ -0,0 +1,234 @@
1
+ import { existsSync, readdirSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { readSkillMetadata } from "../../skills/skill-metadata.js";
4
+ import { getBindingPrimaryTools, getBindingSkills, getBindingSubagents, } from "../../support/compiled-binding.js";
5
+ const BUILTIN_SKILL_TOOLS = new Set(["read_todos", "write_todos"]);
6
+ function uniqueSorted(values) {
7
+ return Array.from(new Set(values.filter((value) => value.trim().length > 0))).sort();
8
+ }
9
+ function addFinding(findings, options, finding) {
10
+ if (finding.severity === "info" && options.includeInfo === false) {
11
+ return;
12
+ }
13
+ findings.push(finding);
14
+ }
15
+ function discoverWorkspaceSkillPaths(workspaceRoot, referencedSkillPaths) {
16
+ const skillPaths = new Set(referencedSkillPaths);
17
+ const skillsRoot = path.join(workspaceRoot, "resources", "skills");
18
+ if (!existsSync(skillsRoot)) {
19
+ return uniqueSorted(Array.from(skillPaths));
20
+ }
21
+ for (const entry of readdirSync(skillsRoot)) {
22
+ const skillPath = path.join(skillsRoot, entry);
23
+ try {
24
+ if (statSync(skillPath).isDirectory() && existsSync(path.join(skillPath, "SKILL.md"))) {
25
+ skillPaths.add(skillPath);
26
+ }
27
+ }
28
+ catch {
29
+ continue;
30
+ }
31
+ }
32
+ return uniqueSorted(Array.from(skillPaths));
33
+ }
34
+ function describeTopLevelSurface(binding) {
35
+ return {
36
+ id: binding.agent.id,
37
+ description: binding.agent.description,
38
+ tools: uniqueSorted(getBindingPrimaryTools(binding).map((tool) => tool.name)),
39
+ skills: uniqueSorted(getBindingSkills(binding)),
40
+ subagents: uniqueSorted(getBindingSubagents(binding).map((subagent) => subagent.name)),
41
+ sourcePath: binding.agent.sourcePath,
42
+ };
43
+ }
44
+ function describeSubagentSurface(subagent, parentAgentId) {
45
+ return {
46
+ id: subagent.name,
47
+ description: subagent.description,
48
+ tools: uniqueSorted((subagent.tools ?? []).map((tool) => tool.name)),
49
+ skills: uniqueSorted(subagent.skills ?? []),
50
+ subagents: [],
51
+ parentAgentId,
52
+ };
53
+ }
54
+ function addOwner(index, key, agentId) {
55
+ const owners = index.get(key) ?? [];
56
+ owners.push(agentId);
57
+ index.set(key, owners);
58
+ }
59
+ function stripAgentRefPrefix(value) {
60
+ const prefix = "agent/";
61
+ return value.startsWith(prefix) ? value.slice(prefix.length) : value;
62
+ }
63
+ export function analyzeWorkspaceBoundaries(workspace, options = {}) {
64
+ const findings = [];
65
+ const agentSurfaces = [];
66
+ const referencedSkillPaths = [];
67
+ const toolOwners = new Map();
68
+ const skillOwners = new Map();
69
+ const skillSurfaceOwners = new Map();
70
+ for (const binding of workspace.bindings.values()) {
71
+ const topLevel = describeTopLevelSurface(binding);
72
+ agentSurfaces.push(topLevel);
73
+ for (const skillPath of topLevel.skills) {
74
+ referencedSkillPaths.push(skillPath);
75
+ }
76
+ for (const subagent of getBindingSubagents(binding)) {
77
+ const surface = describeSubagentSurface(subagent, binding.agent.id);
78
+ agentSurfaces.push(surface);
79
+ for (const skillPath of surface.skills) {
80
+ referencedSkillPaths.push(skillPath);
81
+ }
82
+ }
83
+ }
84
+ for (const surface of agentSurfaces) {
85
+ if (surface.description.trim().length === 0) {
86
+ addFinding(findings, options, {
87
+ severity: "warning",
88
+ code: "agent-missing-description",
89
+ message: `Agent '${surface.id}' has no description, making routing boundaries ambiguous.`,
90
+ agentId: surface.id,
91
+ parentAgentId: surface.parentAgentId,
92
+ sourcePath: surface.sourcePath,
93
+ });
94
+ }
95
+ if (surface.subagents.length > 0 && (surface.tools.length > 0 || surface.skills.length > 0)) {
96
+ addFinding(findings, options, {
97
+ severity: "warning",
98
+ code: "mixed-routing-and-execution-surface",
99
+ message: `Agent '${surface.id}' declares subagents and also exposes local tools or skills.`,
100
+ agentId: surface.id,
101
+ sourcePath: surface.sourcePath,
102
+ });
103
+ }
104
+ if (surface.subagents.length === 0 && surface.tools.length === 0 && surface.skills.length === 0) {
105
+ addFinding(findings, options, {
106
+ severity: "info",
107
+ code: "empty-execution-surface",
108
+ message: `Agent '${surface.id}' has no tools, skills, or subagents declared.`,
109
+ agentId: surface.id,
110
+ parentAgentId: surface.parentAgentId,
111
+ sourcePath: surface.sourcePath,
112
+ });
113
+ }
114
+ for (const toolName of surface.tools) {
115
+ addOwner(toolOwners, toolName, surface.id);
116
+ }
117
+ for (const skillPath of surface.skills) {
118
+ addOwner(skillOwners, skillPath, surface.id);
119
+ skillSurfaceOwners.set(skillPath, [...(skillSurfaceOwners.get(skillPath) ?? []), surface]);
120
+ }
121
+ }
122
+ for (const agent of workspace.agents.values()) {
123
+ for (const subagentRef of agent.subagentRefs) {
124
+ const subagentId = stripAgentRefPrefix(subagentRef);
125
+ if (!workspace.agents.has(subagentId)) {
126
+ addFinding(findings, options, {
127
+ severity: "error",
128
+ code: "missing-subagent-reference",
129
+ message: `Agent '${agent.id}' references missing subagent '${subagentRef}'.`,
130
+ agentId: agent.id,
131
+ sourcePath: agent.sourcePath,
132
+ });
133
+ }
134
+ }
135
+ }
136
+ for (const [toolName, owners] of toolOwners) {
137
+ const uniqueOwners = uniqueSorted(owners);
138
+ if (uniqueOwners.length > 1) {
139
+ addFinding(findings, options, {
140
+ severity: "info",
141
+ code: "shared-tool-surface",
142
+ message: `Tool '${toolName}' is exposed by multiple agents; review whether the shared surface is intentional.`,
143
+ toolName,
144
+ relatedAgents: uniqueOwners,
145
+ });
146
+ }
147
+ }
148
+ for (const [skillPath, owners] of skillOwners) {
149
+ const uniqueOwners = uniqueSorted(owners);
150
+ if (uniqueOwners.length > 1) {
151
+ const metadata = readSkillMetadata(skillPath);
152
+ addFinding(findings, options, {
153
+ severity: "info",
154
+ code: "shared-skill-surface",
155
+ message: `Skill '${metadata.name}' is exposed by multiple agents; review whether the shared surface is intentional.`,
156
+ skillName: metadata.name,
157
+ skillPath,
158
+ relatedAgents: uniqueOwners,
159
+ });
160
+ }
161
+ }
162
+ for (const tool of workspace.tools.values()) {
163
+ if (!toolOwners.has(tool.name)) {
164
+ addFinding(findings, options, {
165
+ severity: "warning",
166
+ code: "unreferenced-tool",
167
+ message: `Tool '${tool.name}' is defined but not exposed by any agent or subagent.`,
168
+ toolName: tool.name,
169
+ sourcePath: tool.sourcePath,
170
+ });
171
+ }
172
+ }
173
+ const discoveredSkillPaths = discoverWorkspaceSkillPaths(workspace.workspaceRoot, referencedSkillPaths);
174
+ for (const skillPath of discoveredSkillPaths) {
175
+ const metadata = readSkillMetadata(skillPath);
176
+ const owners = skillOwners.get(skillPath) ?? [];
177
+ if (owners.length === 0) {
178
+ addFinding(findings, options, {
179
+ severity: "warning",
180
+ code: "unreferenced-skill",
181
+ message: `Skill '${metadata.name}' is present but not exposed by any agent or subagent.`,
182
+ skillName: metadata.name,
183
+ skillPath,
184
+ });
185
+ continue;
186
+ }
187
+ if (!metadata.allowedTools || metadata.allowedTools.length === 0) {
188
+ addFinding(findings, options, {
189
+ severity: "warning",
190
+ code: "skill-missing-allowed-tools",
191
+ message: `Skill '${metadata.name}' does not declare allowed-tools.`,
192
+ skillName: metadata.name,
193
+ skillPath,
194
+ relatedAgents: uniqueSorted(owners),
195
+ });
196
+ continue;
197
+ }
198
+ for (const surface of skillSurfaceOwners.get(skillPath) ?? []) {
199
+ const owner = surface.parentAgentId ? `${surface.parentAgentId}/${surface.id}` : surface.id;
200
+ const availableTools = new Set([...surface.tools, ...BUILTIN_SKILL_TOOLS]);
201
+ for (const allowedTool of metadata.allowedTools) {
202
+ if (!availableTools.has(allowedTool)) {
203
+ addFinding(findings, options, {
204
+ severity: "error",
205
+ code: "skill-allowed-tool-unavailable",
206
+ message: `Skill '${metadata.name}' allows tool '${allowedTool}', but agent '${owner}' does not expose it.`,
207
+ agentId: surface.id,
208
+ parentAgentId: surface.parentAgentId,
209
+ toolName: allowedTool,
210
+ skillName: metadata.name,
211
+ skillPath,
212
+ });
213
+ }
214
+ }
215
+ }
216
+ }
217
+ const errorCount = findings.filter((finding) => finding.severity === "error").length;
218
+ const warningCount = findings.filter((finding) => finding.severity === "warning").length;
219
+ const infoCount = findings.filter((finding) => finding.severity === "info").length;
220
+ return {
221
+ workspaceRoot: workspace.workspaceRoot,
222
+ summary: {
223
+ agentCount: agentSurfaces.length,
224
+ toolCount: workspace.tools.size,
225
+ skillCount: discoveredSkillPaths.length,
226
+ findingCount: findings.length,
227
+ errorCount,
228
+ warningCount,
229
+ infoCount,
230
+ },
231
+ agents: agentSurfaces,
232
+ findings,
233
+ };
234
+ }
@@ -1,6 +1,7 @@
1
1
  import type { ApprovalRecord, ArtifactListing, CancelOptions, HarnessEvent, HarnessStreamItem, RuntimeHealthSnapshot, RuntimeOperatorOverview, ListMemoriesInput, ListMemoriesResult, MessageContent, RemoveMemoryInput, RequestPlanState, RequestOptions, RequestRecord, RequestResult, RequestSummary, RequestStartOptions, RestartConversationOptions, RuntimeAdapterOptions, RuntimeArtifactWriteInput, RuntimeEvaluationExport, RuntimeEvaluationExportInput, RuntimeEvaluationReplayInput, RuntimeEvaluationReplayResult, RuntimeRequestPackage, RuntimeRequestPackageInput, RuntimeSessionPackage, RuntimeSessionPackageInput, ResumeOptions, MemoryRecord, MemorizeInput, MemorizeResult, RecallInput, RecallResult, UpdateMemoryInput, SessionSummary, SessionRecord, SessionListSummary, WorkspaceBundle } from "../contracts/types.js";
2
2
  import { type RuntimeMcpServerOptions, type ToolMcpServerOptions } from "../mcp.js";
3
3
  import { type InventoryAgentRecord, type InventorySkillRecord } from "./harness/system/inventory.js";
4
+ import { type BoundaryAnalysisOptions, type WorkspaceBoundaryAnalysis } from "./harness/system/boundary-analysis.js";
4
5
  import type { RequirementAssessmentOptions } from "./harness/system/skill-requirements.js";
5
6
  import { SystemScheduleManager } from "./scheduling/system-schedule-manager.js";
6
7
  export declare class AgentHarnessRuntime {
@@ -119,6 +120,7 @@ export declare class AgentHarnessRuntime {
119
120
  workspaceRoot: string;
120
121
  agents: InventoryAgentRecord[];
121
122
  };
123
+ analyzeWorkspaceBoundaries(options?: BoundaryAnalysisOptions): WorkspaceBoundaryAnalysis;
122
124
  private deleteSessionCheckpoints;
123
125
  deleteSession(sessionId: string): Promise<boolean>;
124
126
  createToolMcpServer(options: ToolMcpServerOptions): Promise<import("@modelcontextprotocol/sdk/server/mcp").McpServer>;
@@ -133,6 +135,7 @@ export declare class AgentHarnessRuntime {
133
135
  private trackBackgroundTask;
134
136
  private scheduleBackgroundStartupTask;
135
137
  private drainBackgroundTasksForClose;
138
+ private closeStageWithTimeout;
136
139
  private resolveToolMcpServerTools;
137
140
  private loadPriorHistory;
138
141
  private loadRequestInput;
@@ -30,6 +30,7 @@ import { closeMcpClientsForWorkspace } from "../resource/mcp/tool-support.js";
30
30
  import { getBindingRuntimeExecutionMode, } from "./support/compiled-binding.js";
31
31
  import { bindingSupportsRunningReplay, getWorkspaceBinding, resolveWorkspaceAgentTools, } from "./harness/bindings.js";
32
32
  import { describeWorkspaceInventory, getAgentInventoryRecord, listAgentSkills as listWorkspaceAgentSkills, } from "./harness/system/inventory.js";
33
+ import { analyzeWorkspaceBoundaries, } from "./harness/system/boundary-analysis.js";
33
34
  import { createDefaultHealthSnapshot, isInventoryEnabled, isSessionMemorySyncEnabled, } from "./harness/runtime-defaults.js";
34
35
  import { cloneRequestRecord, cloneSessionRecord, deriveRequestInputFromTranscript, mergeMemoryItems, summarizeApprovalEvidence, toPublicHarnessStreamItem, toPublicRequestResultShape, toSessionListSummary, } from "./harness/public-shapes.js";
35
36
  import { Mem0IngestionSync, Mem0SemanticRecall, readMem0RuntimeConfig, } from "./harness/system/mem0-ingestion-sync.js";
@@ -47,6 +48,7 @@ import { buildRequestInspectionRecord, buildSessionInspectionRecord, deleteSessi
47
48
  import { createKnowledgeModule } from "../knowledge/index.js";
48
49
  import { createProceduralMemoryManager, ProceduralMemoryFormationSync, readProceduralMemoryRuntimeConfig, } from "../knowledge/procedural/index.js";
49
50
  const BACKGROUND_TASK_CLOSE_DRAIN_TIMEOUT_MS = 1_000;
51
+ const CLOSE_STAGE_TIMEOUT_MS = 1_000;
50
52
  const ACTIVE_REQUEST_STATES = [
51
53
  "queued",
52
54
  "claimed",
@@ -876,6 +878,9 @@ export class AgentHarnessRuntime {
876
878
  ...options,
877
879
  });
878
880
  }
881
+ analyzeWorkspaceBoundaries(options = {}) {
882
+ return analyzeWorkspaceBoundaries(this.workspace, options);
883
+ }
879
884
  async deleteSessionCheckpoints(sessionId) {
880
885
  const resolver = this.resolvedRuntimeAdapterOptions.checkpointerResolver;
881
886
  if (!resolver) {
@@ -963,6 +968,22 @@ export class AgentHarnessRuntime {
963
968
  clearTimeout(timeoutHandle);
964
969
  }
965
970
  }
971
+ async closeStageWithTimeout(_label, stage) {
972
+ if (!stage) {
973
+ return;
974
+ }
975
+ let timeoutHandle;
976
+ await Promise.race([
977
+ stage().then(() => undefined).catch(() => undefined),
978
+ new Promise((resolve) => {
979
+ timeoutHandle = setTimeout(resolve, CLOSE_STAGE_TIMEOUT_MS);
980
+ timeoutHandle.unref?.();
981
+ }),
982
+ ]);
983
+ if (timeoutHandle) {
984
+ clearTimeout(timeoutHandle);
985
+ }
986
+ }
966
987
  resolveToolMcpServerTools(agentId) {
967
988
  return resolveWorkspaceAgentTools({
968
989
  workspace: this.workspace,
@@ -1386,20 +1407,20 @@ export class AgentHarnessRuntime {
1386
1407
  return;
1387
1408
  }
1388
1409
  this.closed = true;
1389
- await this.healthMonitor?.stop();
1390
- await this.eventBus.drain();
1410
+ await this.closeStageWithTimeout("healthMonitor.stop", () => this.healthMonitor?.stop() ?? Promise.resolve());
1411
+ await this.closeStageWithTimeout("eventBus.drain", () => this.eventBus.drain());
1391
1412
  this.unregisterSessionMemorySync();
1392
1413
  this.unregisterRuntimeMemorySync();
1393
1414
  this.unregisterMem0IngestionSync();
1394
1415
  this.unregisterRuntimeMemoryFormationSync();
1395
1416
  this.unregisterProceduralMemoryFormationSync();
1396
1417
  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);
1418
+ await this.closeStageWithTimeout("sessionMemorySync.close", () => this.sessionMemorySync?.close() ?? Promise.resolve());
1419
+ await this.closeStageWithTimeout("runtimeMemorySync.close", () => this.runtimeMemorySync?.close() ?? Promise.resolve());
1420
+ await this.closeStageWithTimeout("mem0IngestionSync.close", () => this.mem0IngestionSync?.close() ?? Promise.resolve());
1421
+ await this.closeStageWithTimeout("runtimeMemoryFormationSync.close", () => this.runtimeMemoryFormationSync?.close() ?? Promise.resolve());
1422
+ await this.closeStageWithTimeout("proceduralMemoryFormationSync.close", () => this.proceduralMemoryFormationSync?.close() ?? Promise.resolve());
1423
+ await this.closeStageWithTimeout("closeMcpClientsForWorkspace", () => closeMcpClientsForWorkspace(this.workspace));
1403
1424
  this.initialized = false;
1404
1425
  }
1405
1426
  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;