@botbotgo/agent-harness 0.0.346 → 0.0.348

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.
@@ -17,6 +17,7 @@ export type RequestResult = {
17
17
  artifacts?: ArtifactRecord[];
18
18
  metadata?: Record<string, unknown>;
19
19
  };
20
+ export type TerminalExecutionStatus = "completed" | "blocked" | "failed" | "refused";
20
21
  export type UpstreamRuntimeEvent = unknown;
21
22
  export type UpstreamRuntimeEventItem = {
22
23
  sessionId: string;
@@ -203,6 +203,9 @@ export type CompiledBuiltinToolsConfig = {
203
203
  todos?: boolean;
204
204
  modelExposed?: boolean | string[];
205
205
  };
206
+ export type CompiledExecutionContract = {
207
+ requiresPlan?: boolean;
208
+ };
206
209
  export type LangChainAgentParams = {
207
210
  model: CompiledModel;
208
211
  tools: CompiledTool[];
@@ -287,6 +290,7 @@ export type CompiledAgentBinding = {
287
290
  resilience?: Record<string, unknown>;
288
291
  governance?: Record<string, unknown>;
289
292
  observability?: Record<string, unknown>;
293
+ executionContract?: CompiledExecutionContract;
290
294
  deepagent?: {
291
295
  description?: string;
292
296
  passthrough?: Record<string, unknown>;
@@ -1,2 +1,2 @@
1
- export declare const AGENT_HARNESS_VERSION = "0.0.346";
1
+ export declare const AGENT_HARNESS_VERSION = "0.0.348";
2
2
  export declare const AGENT_HARNESS_RELEASE_DATE = "2026-04-24";
@@ -1,2 +1,2 @@
1
- export const AGENT_HARNESS_VERSION = "0.0.346";
1
+ export const AGENT_HARNESS_VERSION = "0.0.348";
2
2
  export const AGENT_HARNESS_RELEASE_DATE = "2026-04-24";
@@ -1,15 +1,20 @@
1
1
  import { CompositeBackend } from "deepagents";
2
2
  import type { RuntimeAdapterOptions, WorkspaceBundle } from "../../contracts/types.js";
3
- export declare function normalizeWorkspaceScopedPath(rootDir: string, inputPath: string): string;
3
+ export declare function normalizeWorkspaceScopedPath(rootDir: string, inputPath: string, options?: {
4
+ allowVirtualAbsolutePath?: boolean;
5
+ }): string;
4
6
  export declare class WorkspaceScopedBackend {
5
7
  private readonly backend;
8
+ private readonly options;
6
9
  readonly id?: string;
7
10
  readonly cwd: string;
8
11
  readonly rootDir: string;
9
12
  readonly root: string;
10
13
  readonly virtualMode?: boolean;
11
14
  readonly execute?: (command: string) => Promise<unknown>;
12
- constructor(backend: Record<string, unknown>, rootDir: string);
15
+ constructor(backend: Record<string, unknown>, rootDir: string, options?: {
16
+ allowVirtualAbsolutePath?: boolean;
17
+ });
13
18
  ls(filePath: string): unknown;
14
19
  read(filePath: string, offset?: number, limit?: number): unknown;
15
20
  readRaw(filePath: string): unknown;
@@ -25,7 +30,9 @@ declare class CompatibleCompositeBackend {
25
30
  readonly id?: string;
26
31
  readonly execute?: (command: string) => ReturnType<CompositeBackend["execute"]>;
27
32
  private readonly composite;
33
+ private readonly routePrefixes;
28
34
  constructor(defaultBackend: unknown, routes: Record<string, unknown>);
35
+ private normalizeCompositePath;
29
36
  ls(filePath: string): ReturnType<CompositeBackend["ls"]>;
30
37
  read(filePath: string, offset?: number, limit?: number): ReturnType<CompositeBackend["read"]>;
31
38
  readRaw(filePath: string): ReturnType<CompositeBackend["readRaw"]>;
@@ -30,7 +30,7 @@ function normalizeVirtualExecuteCommand(command, rootDir) {
30
30
  return `${prefix}${quote}${translatedPath}${quote}`;
31
31
  });
32
32
  }
33
- export function normalizeWorkspaceScopedPath(rootDir, inputPath) {
33
+ export function normalizeWorkspaceScopedPath(rootDir, inputPath, options = {}) {
34
34
  if (typeof inputPath !== "string" || inputPath.length === 0 || !path.isAbsolute(inputPath)) {
35
35
  return inputPath;
36
36
  }
@@ -53,21 +53,26 @@ export function normalizeWorkspaceScopedPath(rootDir, inputPath) {
53
53
  if (normalizedInputPath === normalizedRootDir || normalizedInputPath.startsWith(`${normalizedRootDir}${path.sep}`)) {
54
54
  return path.relative(normalizedRootDir, normalizedInputPath) || ".";
55
55
  }
56
+ if (options.allowVirtualAbsolutePath === true) {
57
+ return inputPath.replace(/\/+/g, "/");
58
+ }
56
59
  throw new Error(`Path '${inputPath}' is outside the workspace root '${normalizedRootDir}'. Use a workspace-relative path instead.`);
57
60
  }
58
- function normalizeWorkspaceScopedNullablePath(rootDir, inputPath) {
59
- return typeof inputPath === "string" ? normalizeWorkspaceScopedPath(rootDir, inputPath) : inputPath;
61
+ function normalizeWorkspaceScopedNullablePath(rootDir, inputPath, options = {}) {
62
+ return typeof inputPath === "string" ? normalizeWorkspaceScopedPath(rootDir, inputPath, options) : inputPath;
60
63
  }
61
64
  export class WorkspaceScopedBackend {
62
65
  backend;
66
+ options;
63
67
  id;
64
68
  cwd;
65
69
  rootDir;
66
70
  root;
67
71
  virtualMode;
68
72
  execute;
69
- constructor(backend, rootDir) {
73
+ constructor(backend, rootDir, options = {}) {
70
74
  this.backend = backend;
75
+ this.options = options;
71
76
  this.rootDir = path.resolve(rootDir);
72
77
  this.root = this.rootDir;
73
78
  this.cwd = this.rootDir;
@@ -78,34 +83,36 @@ export class WorkspaceScopedBackend {
78
83
  : undefined;
79
84
  }
80
85
  ls(filePath) {
81
- return this.backend.ls(normalizeWorkspaceScopedPath(this.rootDir, filePath));
86
+ return this.backend.ls(normalizeWorkspaceScopedPath(this.rootDir, filePath, this.options));
82
87
  }
83
88
  read(filePath, offset, limit) {
84
- return this.backend.read(normalizeWorkspaceScopedPath(this.rootDir, filePath), offset, limit);
89
+ return this.backend.read(normalizeWorkspaceScopedPath(this.rootDir, filePath, this.options), offset, limit);
85
90
  }
86
91
  readRaw(filePath) {
87
- return this.backend.readRaw(normalizeWorkspaceScopedPath(this.rootDir, filePath));
92
+ return this.backend.readRaw(normalizeWorkspaceScopedPath(this.rootDir, filePath, this.options));
88
93
  }
89
94
  grep(pattern, filePath, glob) {
90
- return this.backend.grep(pattern, normalizeWorkspaceScopedNullablePath(this.rootDir, filePath), glob);
95
+ return this.backend.grep(pattern, normalizeWorkspaceScopedNullablePath(this.rootDir, filePath, this.options), glob);
91
96
  }
92
97
  grepRaw(pattern, filePath, glob) {
93
- return this.backend.grepRaw(pattern, normalizeWorkspaceScopedNullablePath(this.rootDir, filePath), glob);
98
+ return this.backend.grepRaw(pattern, normalizeWorkspaceScopedNullablePath(this.rootDir, filePath, this.options), glob);
94
99
  }
95
100
  glob(pattern, filePath) {
96
- return this.backend.glob(pattern, typeof filePath === "string" ? normalizeWorkspaceScopedPath(this.rootDir, filePath) : filePath);
101
+ return this.backend.glob(pattern, typeof filePath === "string" ? normalizeWorkspaceScopedPath(this.rootDir, filePath, this.options) : filePath);
97
102
  }
98
103
  write(filePath, content) {
99
- return this.backend.write(normalizeWorkspaceScopedPath(this.rootDir, filePath), content);
104
+ return this.backend.write(normalizeWorkspaceScopedPath(this.rootDir, filePath, this.options), content);
100
105
  }
101
106
  edit(filePath, oldString, newString, replaceAll) {
102
- return this.backend.edit(normalizeWorkspaceScopedPath(this.rootDir, filePath), oldString, newString, replaceAll);
107
+ return this.backend.edit(normalizeWorkspaceScopedPath(this.rootDir, filePath, this.options), oldString, newString, replaceAll);
103
108
  }
104
109
  uploadFiles(files) {
105
110
  return this.backend.uploadFiles(files);
106
111
  }
107
112
  downloadFiles(paths) {
108
- const normalizedPaths = Array.isArray(paths) ? paths.map((currentPath) => normalizeWorkspaceScopedPath(this.rootDir, currentPath)) : paths;
113
+ const normalizedPaths = Array.isArray(paths)
114
+ ? paths.map((currentPath) => normalizeWorkspaceScopedPath(this.rootDir, currentPath, this.options))
115
+ : paths;
109
116
  return this.backend.downloadFiles(normalizedPaths);
110
117
  }
111
118
  }
@@ -113,8 +120,10 @@ class CompatibleCompositeBackend {
113
120
  id;
114
121
  execute;
115
122
  composite;
123
+ routePrefixes;
116
124
  constructor(defaultBackend, routes) {
117
125
  this.composite = new CompositeBackend(defaultBackend, routes);
126
+ this.routePrefixes = Object.keys(routes).filter((route) => route.startsWith("/"));
118
127
  const sandboxLike = defaultBackend;
119
128
  if (typeof sandboxLike.id === "string" && typeof sandboxLike.execute === "function") {
120
129
  this.id = sandboxLike.id;
@@ -125,32 +134,43 @@ class CompatibleCompositeBackend {
125
134
  : (command) => this.composite.execute(command);
126
135
  }
127
136
  }
137
+ normalizeCompositePath(filePath) {
138
+ if (!path.isAbsolute(filePath)) {
139
+ return filePath;
140
+ }
141
+ const normalized = filePath.replace(/\/+/g, "/");
142
+ if (this.routePrefixes.some((route) => normalized === route || normalized.startsWith(route))) {
143
+ return normalized;
144
+ }
145
+ return path.join(...normalized.split("/").filter(Boolean));
146
+ }
128
147
  ls(filePath) {
129
- return this.composite.ls(filePath);
148
+ return this.composite.ls(this.normalizeCompositePath(filePath));
130
149
  }
131
150
  read(filePath, offset, limit) {
132
- return this.composite.read(filePath, offset, limit);
151
+ return this.composite.read(this.normalizeCompositePath(filePath), offset, limit);
133
152
  }
134
153
  readRaw(filePath) {
135
- return this.composite.readRaw(filePath);
154
+ return this.composite.readRaw(this.normalizeCompositePath(filePath));
136
155
  }
137
156
  grep(pattern, filePath, glob) {
138
- return this.composite.grep(pattern, filePath ?? undefined, glob ?? undefined);
157
+ return this.composite.grep(pattern, filePath ? this.normalizeCompositePath(filePath) : undefined, glob ?? undefined);
139
158
  }
140
159
  glob(pattern, filePath) {
141
- return this.composite.glob(pattern, filePath);
160
+ return this.composite.glob(pattern, filePath ? this.normalizeCompositePath(filePath) : filePath);
142
161
  }
143
162
  write(filePath, content) {
144
- return this.composite.write(filePath, content);
163
+ return this.composite.write(this.normalizeCompositePath(filePath), content);
145
164
  }
146
165
  edit(filePath, oldString, newString, replaceAll) {
147
- return this.composite.edit(filePath, oldString, newString, replaceAll);
166
+ return this.composite.edit(this.normalizeCompositePath(filePath), oldString, newString, replaceAll);
148
167
  }
149
168
  uploadFiles(files) {
150
169
  return this.composite.uploadFiles(files);
151
170
  }
152
171
  downloadFiles(paths) {
153
- return this.composite.downloadFiles(paths);
172
+ const normalizedPaths = Array.isArray(paths) ? paths.map((currentPath) => this.normalizeCompositePath(currentPath)) : paths;
173
+ return this.composite.downloadFiles(normalizedPaths);
154
174
  }
155
175
  }
156
176
  function omitKind(config) {
@@ -289,7 +309,7 @@ export function createInlineBackendResolver(workspace) {
289
309
  const routeKind = typeof routeConfig?.kind === "string" ? routeConfig.kind : "StoreBackend";
290
310
  return [route, createInlineBackendInstance(workspace.workspaceRoot, routeKind, routeConfig, runtimeLike)];
291
311
  }));
292
- return new WorkspaceScopedBackend(new CompatibleCompositeBackend(createInlineBackendInstance(workspace.workspaceRoot, defaultBackendKind, stateConfig, runtimeLike), mappedRoutes), workspace.workspaceRoot);
312
+ return new WorkspaceScopedBackend(new CompatibleCompositeBackend(createInlineBackendInstance(workspace.workspaceRoot, defaultBackendKind, stateConfig, runtimeLike), mappedRoutes), workspace.workspaceRoot, { allowVirtualAbsolutePath: true });
293
313
  }
294
314
  default:
295
315
  return unsupportedInlineBackend(kind);
@@ -0,0 +1,8 @@
1
+ The delegated task failed. You are the routing/delegation parent agent, so you must not switch into local execution or start a new local plan.
2
+
3
+ Your next response has only two valid forms:
4
+
5
+ 1. Call the `task` tool again, preserving the user's original request and delegating to the same specialist or another explicit specialist whose configured responsibility clearly matches the original request.
6
+ 2. Return a final blocker report to the user explaining that delegated execution failed.
7
+
8
+ Do not call local execution tools, repository tools, web tools, shell tools, or `write_todos` from the parent agent after this delegated failure. Do not invent a new topic or downgrade the original request. If you continue execution, it must be through `task`.
@@ -1,4 +1,5 @@
1
1
  import { extractVisibleOutput, isToolCallRecoveryFailure, isRetrySafeInvalidToolSelectionError, resolveMissingPlanRecoveryInstruction, resolveExecutionWithoutToolEvidenceTextInstruction, shouldValidateExecutionWithoutToolEvidence, resolveToolCallRecoveryInstruction, sanitizeVisibleText, EXECUTION_WITH_TOOL_EVIDENCE_RETRY_INSTRUCTION, INVALID_TOOL_SELECTION_RECOVERY_INSTRUCTION, } from "../../parsing/output-parsing.js";
2
+ import { DELEGATED_TASK_FAILURE_RECOVERY_INSTRUCTION } from "../../prompts/runtime-prompts.js";
2
3
  import { buildInvocationRequest } from "../model/invocation-request.js";
3
4
  import { buildRawModelMessages } from "../model/message-assembly.js";
4
5
  import { projectRuntimeStreamEvent, createStreamEventProjectionState } from "../stream-event-projection.js";
@@ -55,6 +56,18 @@ function hasSuccessfulNonTodoToolEvidence(executedToolResults) {
55
56
  function hasSuccessfulTaskToolEvidence(executedToolResults) {
56
57
  return executedToolResults.some((item) => item.isError !== true && item.toolName === "task");
57
58
  }
59
+ function requiresPlanEvidence(binding) {
60
+ return binding.harnessRuntime?.executionContract?.requiresPlan === true;
61
+ }
62
+ function hasParentLocalToolExecutionAfterDelegationFailure(originalEvidence, executedToolResults) {
63
+ return originalEvidence.hasFailedTaskDelegation
64
+ && executedToolResults.some((item) => item.toolName !== "task");
65
+ }
66
+ function isDelegationFailureFinalReport(originalEvidence, executedToolResults, visibleOutput) {
67
+ return originalEvidence.hasFailedTaskDelegation
68
+ && executedToolResults.length === 0
69
+ && visibleOutput.trim().length > 0;
70
+ }
58
71
  function buildExecutionRecoveryEvidence(params) {
59
72
  const { projectionState, executedToolResults = [] } = params;
60
73
  return {
@@ -65,7 +78,8 @@ function buildExecutionRecoveryEvidence(params) {
65
78
  hasIncompletePlanState: projectionState.hasIncompletePlanState || hasIncompletePlanStateInExecutedToolResults(executedToolResults),
66
79
  hasPlanStateEvidence: projectionState.sawPlanState || hasIncompletePlanStateInExecutedToolResults(executedToolResults),
67
80
  hasOpenTaskDelegation: projectionState.openTaskDelegations > 0,
68
- hasFailedTaskDelegation: projectionState.hasFailedTaskDelegation,
81
+ hasFailedTaskDelegation: projectionState.hasFailedTaskDelegation
82
+ || executedToolResults.some((item) => item.toolName === "task" && item.isError === true),
69
83
  hasDelegatedAgentWithConfiguredTools: projectionState.sawDelegatedAgentWithConfiguredTools,
70
84
  hasDelegatedExecutionToolEvidence: projectionState.emittedDelegatedExecutionToolResult,
71
85
  hasOnlyPlaceholderTaskCompletion: projectionState.emittedSuccessfulTaskResult
@@ -98,6 +112,9 @@ function resolveStreamedRuntimeFailureRecoveryInstruction(output, evidence) {
98
112
  return hasExecutionEvidence ? null : EXECUTION_WITH_TOOL_EVIDENCE_RETRY_INSTRUCTION;
99
113
  }
100
114
  function resolveDelegatedExecutionRecoveryInstruction(evidence) {
115
+ if (evidence.hasFailedTaskDelegation) {
116
+ return DELEGATED_TASK_FAILURE_RECOVERY_INSTRUCTION;
117
+ }
101
118
  if (hasMissingDelegatedFindings(evidence)
102
119
  || (evidence.hasOpenTaskDelegation
103
120
  && evidence.hasDelegatedAgentWithConfiguredTools
@@ -400,6 +417,7 @@ export async function* streamRuntimeExecution(options) {
400
417
  ? resolveMissingPlanRecoveryInstruction({
401
418
  request,
402
419
  assistantText: terminalVisibleOutput,
420
+ requiresPlan: requiresPlanEvidence(options.binding),
403
421
  hasPlanStateEvidence: terminalExecutionEvidence.hasPlanStateEvidence,
404
422
  hasWriteTodosEvidence: terminalExecutionEvidence.hasPlanStateEvidence,
405
423
  hasToolResultEvidence: terminalExecutionEvidence.hasSuccessfulNonTodoToolResultEvidence,
@@ -443,8 +461,9 @@ export async function* streamRuntimeExecution(options) {
443
461
  throw error;
444
462
  }
445
463
  const streamedExecutionEvidence = buildExecutionRecoveryEvidence({ projectionState });
446
- const delegatedExecutionRecoveryInstruction = !emittedUnsafeStreamSideEffects
447
- ? resolveDelegatedExecutionRecoveryInstruction(streamedExecutionEvidence)
464
+ const streamedDelegatedRecoveryInstruction = resolveDelegatedExecutionRecoveryInstruction(streamedExecutionEvidence);
465
+ const delegatedExecutionRecoveryInstruction = !emittedUnsafeStreamSideEffects || streamedDelegatedRecoveryInstruction
466
+ ? streamedDelegatedRecoveryInstruction
448
467
  : null;
449
468
  if (hasUnresolvedExecution(streamedExecutionEvidence) && !delegatedExecutionRecoveryInstruction) {
450
469
  throw createUnresolvedExecutionError(streamedExecutionEvidence);
@@ -453,6 +472,7 @@ export async function* streamRuntimeExecution(options) {
453
472
  ? resolveExecutionWithoutToolEvidenceTextInstruction(request, projectionState.emittedOutput, false, {
454
473
  ...streamedExecutionEvidence,
455
474
  hasMissingDelegatedExecutionEvidence: hasMissingDelegatedExecutionEvidence(streamedExecutionEvidence),
475
+ requiresPlan: requiresPlanEvidence(options.binding),
456
476
  })
457
477
  : null;
458
478
  const streamedRuntimeFailureRecoveryInstruction = projectionState.emittedOutput
@@ -462,6 +482,7 @@ export async function* streamRuntimeExecution(options) {
462
482
  ? resolveMissingPlanRecoveryInstruction({
463
483
  request,
464
484
  assistantText: projectionState.emittedOutput,
485
+ requiresPlan: requiresPlanEvidence(options.binding),
465
486
  hasPlanStateEvidence: streamedExecutionEvidence.hasPlanStateEvidence,
466
487
  hasWriteTodosEvidence: streamedExecutionEvidence.hasPlanStateEvidence,
467
488
  hasToolResultEvidence: streamedExecutionEvidence.hasSuccessfulNonTodoToolResultEvidence,
@@ -484,17 +505,22 @@ export async function* streamRuntimeExecution(options) {
484
505
  projectionState: createStreamEventProjectionState(),
485
506
  executedToolResults,
486
507
  });
508
+ if (hasParentLocalToolExecutionAfterDelegationFailure(originalExecutionEvidence, executedToolResults)) {
509
+ throw new ExecutionReconciliationError("Agent attempted parent-local tool execution after delegated task failure; it must report a blocker or re-delegate with task.");
510
+ }
487
511
  const retriedVisibleOutput = retried.output ? toVisibleContent(retried.output) : "";
512
+ const retriedIsDelegationFailureFinalReport = isDelegationFailureFinalReport(originalExecutionEvidence, executedToolResults, retriedVisibleOutput);
488
513
  const retriedCarriesExecutionEvidence = retriedExecutionEvidence.hasToolResultEvidence
489
514
  || retriedExecutionEvidence.hasOpenTaskDelegation
490
515
  || retriedExecutionEvidence.hasDelegatedExecutionToolEvidence;
491
- const retriedHasUnresolvedExecution = hasUnresolvedExecution(retriedExecutionEvidence)
492
- || hasMissingDelegatedExecutionEvidence(retriedExecutionEvidence)
493
- || hasMissingDelegatedFindings(retriedExecutionEvidence)
494
- || (!retriedCarriesExecutionEvidence
495
- && (hasUnresolvedExecution(originalExecutionEvidence)
496
- || hasMissingDelegatedExecutionEvidence(originalExecutionEvidence)
497
- || hasMissingDelegatedFindings(originalExecutionEvidence)));
516
+ const retriedHasUnresolvedExecution = !retriedIsDelegationFailureFinalReport
517
+ && (hasUnresolvedExecution(retriedExecutionEvidence)
518
+ || hasMissingDelegatedExecutionEvidence(retriedExecutionEvidence)
519
+ || hasMissingDelegatedFindings(retriedExecutionEvidence)
520
+ || (!retriedCarriesExecutionEvidence
521
+ && (hasUnresolvedExecution(originalExecutionEvidence)
522
+ || hasMissingDelegatedExecutionEvidence(originalExecutionEvidence)
523
+ || hasMissingDelegatedFindings(originalExecutionEvidence))));
498
524
  const effectiveRecoveryEvidence = retriedCarriesExecutionEvidence
499
525
  ? retriedExecutionEvidence
500
526
  : {
@@ -666,18 +692,21 @@ export async function* streamRuntimeExecution(options) {
666
692
  ? resolveExecutionWithoutToolEvidenceTextInstruction(request, result.output, false, {
667
693
  ...invokeExecutionEvidence,
668
694
  hasMissingDelegatedExecutionEvidence: hasMissingDelegatedExecutionEvidence(invokeExecutionEvidence),
695
+ requiresPlan: requiresPlanEvidence(options.binding),
669
696
  })
670
697
  : resolveDelegatedExecutionRecoveryInstruction(invokeExecutionEvidence);
671
698
  const invokeFallbackMissingPlanRecoveryInstruction = !hasUnresolvedExecution(invokeExecutionEvidence) && !invokeFallbackRecoveryInstruction
672
699
  ? resolveMissingPlanRecoveryInstruction({
673
700
  request,
674
701
  assistantText: typeof result.output === "string" ? result.output : "",
702
+ requiresPlan: requiresPlanEvidence(options.binding),
675
703
  hasPlanStateEvidence: invokeExecutionEvidence.hasPlanStateEvidence,
676
704
  hasWriteTodosEvidence: invokeExecutionEvidence.hasPlanStateEvidence,
677
705
  hasToolResultEvidence: invokeExecutionEvidence.hasSuccessfulNonTodoToolResultEvidence,
678
706
  })
679
707
  : null;
680
- const effectiveInvokeFallbackRecoveryInstruction = invokeFallbackMissingPlanRecoveryInstruction ?? invokeFallbackRecoveryInstruction;
708
+ const effectiveInvokeFallbackRecoveryInstruction = invokeFallbackMissingPlanRecoveryInstruction
709
+ ?? invokeFallbackRecoveryInstruction;
681
710
  if (effectiveInvokeFallbackRecoveryInstruction) {
682
711
  const recovered = await options.invoke(options.applyToolRecoveryInstruction(options.binding, effectiveInvokeFallbackRecoveryInstruction), options.input, options.sessionId, options.runtimeOptions.requestId ?? options.sessionId, undefined, options.history, options.runtimeOptions);
683
712
  const recoveredToolResults = Array.isArray(recovered.metadata?.executedToolResults)
@@ -688,17 +717,22 @@ export async function* streamRuntimeExecution(options) {
688
717
  projectionState: createStreamEventProjectionState(),
689
718
  executedToolResults: recoveredToolResults,
690
719
  });
720
+ if (hasParentLocalToolExecutionAfterDelegationFailure(originalExecutionEvidence, recoveredToolResults)) {
721
+ throw new ExecutionReconciliationError("Agent attempted parent-local tool execution after delegated task failure; it must report a blocker or re-delegate with task.");
722
+ }
691
723
  const recoveredVisibleOutput = recovered.output ? toVisibleContent(recovered.output) : "";
724
+ const recoveredIsDelegationFailureFinalReport = isDelegationFailureFinalReport(originalExecutionEvidence, recoveredToolResults, recoveredVisibleOutput);
692
725
  const recoveredCarriesExecutionEvidence = recoveredExecutionEvidence.hasToolResultEvidence
693
726
  || recoveredExecutionEvidence.hasOpenTaskDelegation
694
727
  || recoveredExecutionEvidence.hasDelegatedExecutionToolEvidence;
695
- const recoveredHasUnresolvedExecution = hasUnresolvedExecution(recoveredExecutionEvidence)
696
- || hasMissingDelegatedExecutionEvidence(recoveredExecutionEvidence)
697
- || hasMissingDelegatedFindings(recoveredExecutionEvidence)
698
- || (!recoveredCarriesExecutionEvidence
699
- && (hasUnresolvedExecution(originalExecutionEvidence)
700
- || hasMissingDelegatedExecutionEvidence(originalExecutionEvidence)
701
- || hasMissingDelegatedFindings(originalExecutionEvidence)));
728
+ const recoveredHasUnresolvedExecution = !recoveredIsDelegationFailureFinalReport
729
+ && (hasUnresolvedExecution(recoveredExecutionEvidence)
730
+ || hasMissingDelegatedExecutionEvidence(recoveredExecutionEvidence)
731
+ || hasMissingDelegatedFindings(recoveredExecutionEvidence)
732
+ || (!recoveredCarriesExecutionEvidence
733
+ && (hasUnresolvedExecution(originalExecutionEvidence)
734
+ || hasMissingDelegatedExecutionEvidence(originalExecutionEvidence)
735
+ || hasMissingDelegatedFindings(originalExecutionEvidence))));
702
736
  const effectiveRecoveredEvidence = recoveredCarriesExecutionEvidence
703
737
  ? recoveredExecutionEvidence
704
738
  : {
@@ -3,6 +3,7 @@ import { salvageFunctionLikeToolCall } 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";
6
+ import { mapTerminalStatusToRequestState, readTerminalExecutionStatus } from "./terminal-status.js";
6
7
  function looksLikeLeakedToolCallText(value) {
7
8
  const normalized = sanitizeVisibleText(value).trim();
8
9
  if (!normalized) {
@@ -168,6 +169,29 @@ function extractDeterministicToolFailureReport(executedToolResults) {
168
169
  "- none",
169
170
  ].join("\n");
170
171
  }
172
+ function hasEmptyFinalMessage(result) {
173
+ const messages = Array.isArray(result.messages) ? result.messages : [];
174
+ const lastMessage = messages.at(-1);
175
+ if (!lastMessage || typeof lastMessage !== "object") {
176
+ return false;
177
+ }
178
+ const direct = lastMessage;
179
+ return direct.content === "" || direct.kwargs?.content === "" || direct.lc_kwargs?.content === "";
180
+ }
181
+ function hasFinalMessageToolCalls(result) {
182
+ const messages = Array.isArray(result.messages) ? result.messages : [];
183
+ const lastMessage = messages.at(-1);
184
+ if (!lastMessage || typeof lastMessage !== "object") {
185
+ return false;
186
+ }
187
+ const direct = lastMessage;
188
+ return Array.isArray(direct.tool_calls) && direct.tool_calls.length > 0
189
+ || Array.isArray(direct.invalid_tool_calls) && direct.invalid_tool_calls.length > 0
190
+ || Array.isArray(direct.kwargs?.tool_calls) && direct.kwargs.tool_calls.length > 0
191
+ || Array.isArray(direct.kwargs?.invalid_tool_calls) && direct.kwargs.invalid_tool_calls.length > 0
192
+ || Array.isArray(direct.lc_kwargs?.tool_calls) && direct.lc_kwargs.tool_calls.length > 0
193
+ || Array.isArray(direct.lc_kwargs?.invalid_tool_calls) && direct.lc_kwargs.invalid_tool_calls.length > 0;
194
+ }
171
195
  export function resolveDeterministicFinalOutput(params) {
172
196
  const visibleOutput = params.visibleOutput ?? "";
173
197
  const toolFallback = params.toolFallback ?? "";
@@ -178,6 +202,9 @@ export function resolveDeterministicFinalOutput(params) {
178
202
  const deterministicFailureReport = extractDeterministicToolFailureReport(executedToolResults);
179
203
  const delegatedTaskOutput = extractLatestSuccessfulTaskResultText(executedToolResults);
180
204
  const successfulToolOutput = extractLatestSuccessfulNonTodoToolResultText(executedToolResults);
205
+ if (sanitizedVisibleOutput && deterministicFailureReport && hasDelegationBlocker(executedToolResults) && !successfulToolOutput) {
206
+ return deterministicFailureReport;
207
+ }
181
208
  if (sanitizedVisibleOutput && successfulToolOutput && hasDelegationBlocker(executedToolResults)) {
182
209
  return deterministicFailureReport || delegatedTaskOutput || successfulToolOutput;
183
210
  }
@@ -215,9 +242,26 @@ export function finalizeRequestResult(params) {
215
242
  const visibleOutput = extractedOutput && !isLikelyToolArgsObject(tryParseJson(extractedOutput)) ? extractedOutput : "";
216
243
  const emptyAssistantMessageFailure = extractEmptyAssistantMessageFailure(result);
217
244
  const toolFallback = extractToolFallbackContext(result);
245
+ const outputContent = extractOutputContent(result);
246
+ const contentBlocks = extractContentBlocks(result);
247
+ const structuredResponse = result.structuredResponse;
248
+ const structuredTerminalStatus = readTerminalExecutionStatus(structuredResponse) ?? readTerminalExecutionStatus(result);
249
+ const files = asRecord(result.files);
218
250
  if (!visibleOutput && !toolFallback && emptyAssistantMessageFailure) {
219
251
  throw new Error(emptyAssistantMessageFailure);
220
252
  }
253
+ if (!visibleOutput
254
+ && !toolFallback
255
+ && interruptContent === undefined
256
+ && outputContent === undefined
257
+ && contentBlocks.length === 0
258
+ && structuredResponse === undefined
259
+ && !files
260
+ && executedToolResults.length === 0
261
+ && hasEmptyFinalMessage(result)
262
+ && !hasFinalMessageToolCalls(result)) {
263
+ throw new Error("empty_final_output");
264
+ }
221
265
  const serializedResult = JSON.stringify(result, null, 2);
222
266
  const output = resolveDeterministicFinalOutput({
223
267
  visibleOutput,
@@ -226,17 +270,16 @@ export function finalizeRequestResult(params) {
226
270
  })
227
271
  || (containsLikelySkillDocument(result) ? "" : serializedResult);
228
272
  const finalMessageText = sanitizeVisibleText(output);
229
- const outputContent = extractOutputContent(result);
230
- const contentBlocks = extractContentBlocks(result);
231
- const structuredResponse = result.structuredResponse;
232
- const files = asRecord(result.files);
273
+ const terminalStatus = structuredTerminalStatus ?? readTerminalExecutionStatus(finalMessageText);
233
274
  const stateSnapshot = buildStateSnapshot(result);
234
275
  const memoryCandidates = executedToolResults.flatMap((toolResult) => toolResult.memoryCandidates ?? []);
235
276
  return {
236
277
  sessionId,
237
278
  requestId,
238
279
  agentId: bindingAgentId,
239
- state: Array.isArray(result.__interrupt__) && result.__interrupt__.length > 0 ? "waiting_for_approval" : "completed",
280
+ state: Array.isArray(result.__interrupt__) && result.__interrupt__.length > 0
281
+ ? "waiting_for_approval"
282
+ : mapTerminalStatusToRequestState(terminalStatus),
240
283
  interruptContent,
241
284
  output: finalMessageText,
242
285
  finalMessageText,
@@ -247,6 +290,7 @@ export function finalizeRequestResult(params) {
247
290
  ...(executedToolResults.length > 0 ? { executedToolResults } : {}),
248
291
  ...(memoryCandidates.length > 0 ? { memoryCandidates } : {}),
249
292
  ...(structuredResponse !== undefined ? { structuredResponse } : {}),
293
+ ...(terminalStatus ? { terminalStatus } : {}),
250
294
  ...(outputContent !== undefined ? { outputContent } : {}),
251
295
  ...(contentBlocks.length > 0 ? { contentBlocks } : {}),
252
296
  ...(files ? { files } : {}),
@@ -43,6 +43,9 @@ function hasNonTodoToolEvidence(executedToolResults) {
43
43
  function hasPlanStateEvidence(executedToolResults) {
44
44
  return executedToolResults.some((item) => item.toolName === "write_todos" || item.toolName === "read_todos" || readPlanStateSummary(item.output) !== null);
45
45
  }
46
+ function requiresPlanEvidence(binding) {
47
+ return binding.harnessRuntime.executionContract?.requiresPlan === true;
48
+ }
46
49
  function extractLatestUserInput(request) {
47
50
  const typedRequest = request;
48
51
  const messages = Array.isArray(typedRequest.messages) ? typedRequest.messages : [];
@@ -87,6 +90,7 @@ export async function runLocalToolInvocationLoop({ binding, request, primaryTool
87
90
  hasToolResultEvidence: hasExecutionBeyondTodoPlanning,
88
91
  hasPlanStateEvidence: hasPlanStateEvidence(executedToolResults),
89
92
  hasIncompletePlanState: hasExecutionBeyondTodoPlanning && hasIncompletePlanState,
93
+ requiresPlan: requiresPlanEvidence(binding),
90
94
  })
91
95
  : hasIncompletePlanState && hasExecutionBeyondTodoPlanning
92
96
  ? AUTONOMOUS_INVESTIGATION_RECOVERY_INSTRUCTION
@@ -102,6 +106,7 @@ export async function runLocalToolInvocationLoop({ binding, request, primaryTool
102
106
  }
103
107
  const missingPlanRecoveryInstruction = resolveMissingPlanRecoveryInstruction({
104
108
  request: activeRequest,
109
+ requiresPlan: requiresPlanEvidence(binding),
105
110
  hasPlanStateEvidence: hasPlanStateEvidence(executedToolResults),
106
111
  hasWriteTodosEvidence: executedToolResults.some((item) => item.toolName === "write_todos"),
107
112
  hasToolResultEvidence: executedToolResults.length > 0 || toolCalls.length > 0,