@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.
@@ -2,6 +2,7 @@ import { resolveDeterministicFinalOutput, } from "../../adapter/invocation-resul
2
2
  import { AGENT_INTERRUPT_SENTINEL_PREFIX, RuntimeOperationTimeoutError } from "../../agent-runtime-adapter.js";
3
3
  import { ExecutionReconciliationError } from "../../adapter/flow/stream-runtime.js";
4
4
  import { buildRequestPlanState, summarizeBuiltinWriteTodosArgs } from "../../adapter/runtime-adapter-support.js";
5
+ import { mapTerminalStatusToPlanItemStatus, mapTerminalStatusToRequestState, readTerminalExecutionStatus, } from "../../adapter/terminal-status.js";
5
6
  import { sanitizeVisibleText } from "../../parsing/output-parsing.js";
6
7
  import { describeRuntimeError, renderRuntimeFailure, renderToolFailure } from "../../support/harness-support.js";
7
8
  import { getBindingPrimaryModel } from "../../support/compiled-binding.js";
@@ -37,27 +38,6 @@ function planStateHasActiveItems(planState) {
37
38
  }
38
39
  return planState.summary.pending > 0 || planState.summary.inProgress > 0;
39
40
  }
40
- function readTerminalStructuredStatus(value) {
41
- if (typeof value === "string") {
42
- try {
43
- return readTerminalStructuredStatus(JSON.parse(value));
44
- }
45
- catch {
46
- return /^\s*Status:\s*completed\b/im.test(value) ? "completed" : null;
47
- }
48
- }
49
- if (typeof value !== "object" || value === null) {
50
- return null;
51
- }
52
- const typed = value;
53
- if (typed.status === "completed") {
54
- return typed.status;
55
- }
56
- return (readTerminalStructuredStatus(typed.structuredResponse)
57
- ?? readTerminalStructuredStatus(typed.content)
58
- ?? readTerminalStructuredStatus(typed.output)
59
- ?? readTerminalStructuredStatus(typed.data));
60
- }
61
41
  function isSubstantiveTerminalAssistantOutput(value) {
62
42
  const normalized = sanitizeVisibleText(value).trim();
63
43
  if (normalized.length < 80) {
@@ -898,10 +878,10 @@ export async function* streamHarnessRun(options) {
898
878
  }
899
879
  }
900
880
  const terminalStructuredStatus = normalizedChunk.toolName === "task"
901
- ? readTerminalStructuredStatus(normalizedChunk.output)
881
+ ? readTerminalExecutionStatus(normalizedChunk.output)
902
882
  : null;
903
883
  if (terminalStructuredStatus && currentPlanState && planStateHasActiveItems(currentPlanState)) {
904
- const reconciledPlanState = reconcilePlanStateToTerminalStatus(currentPlanState, terminalStructuredStatus, new Date().toISOString());
884
+ const reconciledPlanState = reconcilePlanStateToTerminalStatus(currentPlanState, mapTerminalStatusToPlanItemStatus(terminalStructuredStatus), new Date().toISOString());
905
885
  const signature = buildPlanStateSignature(reconciledPlanState);
906
886
  if (signature !== lastPlanStateSignature) {
907
887
  const previousPlanState = currentPlanState;
@@ -1005,9 +985,9 @@ export async function* streamHarnessRun(options) {
1005
985
  }
1006
986
  }
1007
987
  currentPlanState = await refreshPlanStateFromPersistence(options, currentPlanState);
1008
- const terminalStructuredStatus = readTerminalStructuredStatus(actual.structuredResponse);
988
+ const terminalStructuredStatus = readTerminalExecutionStatus(actual.structuredResponse);
1009
989
  if (terminalStructuredStatus && currentPlanState && planStateHasActiveItems(currentPlanState)) {
1010
- const reconciledPlanState = reconcilePlanStateToTerminalStatus(currentPlanState, terminalStructuredStatus, new Date().toISOString());
990
+ const reconciledPlanState = reconcilePlanStateToTerminalStatus(currentPlanState, mapTerminalStatusToPlanItemStatus(terminalStructuredStatus), new Date().toISOString());
1011
991
  const signature = buildPlanStateSignature(reconciledPlanState);
1012
992
  if (signature !== lastPlanStateSignature) {
1013
993
  const previousPlanState = currentPlanState;
@@ -1075,8 +1055,10 @@ export async function* streamHarnessRun(options) {
1075
1055
  content: assistantOutput,
1076
1056
  };
1077
1057
  }
1058
+ const terminalStatus = readTerminalExecutionStatus(assistantOutput);
1059
+ const terminalRequestState = mapTerminalStatusToRequestState(terminalStatus);
1078
1060
  await options.appendAssistantMessage(options.sessionId, options.requestId, assistantOutput);
1079
- const completedEvent = await options.setRequestStateAndEmit(options.sessionId, options.requestId, 6, "completed", {
1061
+ const completedEvent = await options.setRequestStateAndEmit(options.sessionId, options.requestId, 6, terminalRequestState, {
1080
1062
  previousState: "running",
1081
1063
  });
1082
1064
  yield {
@@ -1089,9 +1071,10 @@ export async function* streamHarnessRun(options) {
1089
1071
  sessionId: options.sessionId,
1090
1072
  requestId: options.requestId,
1091
1073
  agentId: currentAgentId,
1092
- state: "completed",
1074
+ state: terminalRequestState,
1093
1075
  output: assistantOutput,
1094
1076
  finalMessageText: assistantOutput,
1077
+ ...(terminalStatus ? { metadata: { terminalStatus } } : {}),
1095
1078
  },
1096
1079
  };
1097
1080
  }
@@ -1101,9 +1084,9 @@ export async function* streamHarnessRun(options) {
1101
1084
  executedToolResults,
1102
1085
  });
1103
1086
  if (!assistantOutput && sawSuccessfulToolResult && deterministicToolEvidenceOutput) {
1104
- const terminalStructuredStatus = readTerminalStructuredStatus(deterministicToolEvidenceOutput);
1087
+ const terminalStructuredStatus = readTerminalExecutionStatus(deterministicToolEvidenceOutput);
1105
1088
  if (terminalStructuredStatus && currentPlanState && planStateHasActiveItems(currentPlanState)) {
1106
- const reconciledPlanState = reconcilePlanStateToTerminalStatus(currentPlanState, terminalStructuredStatus, new Date().toISOString());
1089
+ const reconciledPlanState = reconcilePlanStateToTerminalStatus(currentPlanState, mapTerminalStatusToPlanItemStatus(terminalStructuredStatus), new Date().toISOString());
1107
1090
  const signature = buildPlanStateSignature(reconciledPlanState);
1108
1091
  if (signature !== lastPlanStateSignature) {
1109
1092
  const previousPlanState = currentPlanState;
@@ -1137,7 +1120,9 @@ export async function* streamHarnessRun(options) {
1137
1120
  agentId: currentAgentId,
1138
1121
  content: deterministicToolEvidenceOutput,
1139
1122
  };
1140
- const completedEvent = await options.setRequestStateAndEmit(options.sessionId, options.requestId, 6, "completed", {
1123
+ const terminalStatus = readTerminalExecutionStatus(deterministicToolEvidenceOutput);
1124
+ const terminalRequestState = mapTerminalStatusToRequestState(terminalStatus);
1125
+ const completedEvent = await options.setRequestStateAndEmit(options.sessionId, options.requestId, 6, terminalRequestState, {
1141
1126
  previousState: "running",
1142
1127
  });
1143
1128
  yield {
@@ -1150,11 +1135,12 @@ export async function* streamHarnessRun(options) {
1150
1135
  sessionId: options.sessionId,
1151
1136
  requestId: options.requestId,
1152
1137
  agentId: currentAgentId,
1153
- state: "completed",
1138
+ state: terminalRequestState,
1154
1139
  output: deterministicToolEvidenceOutput,
1155
1140
  finalMessageText: deterministicToolEvidenceOutput,
1156
1141
  metadata: {
1157
1142
  executedToolResults,
1143
+ ...(terminalStatus ? { terminalStatus } : {}),
1158
1144
  },
1159
1145
  },
1160
1146
  };
@@ -7,10 +7,10 @@ export declare function isRepairableWriteTodosEmptyFailure(error: unknown): bool
7
7
  export declare function isToolCallRecoveryFailure(error: unknown): boolean;
8
8
  export declare function isRetrySafeInvalidToolSelectionError(value: unknown): boolean;
9
9
  export declare function shouldValidateExecutionWithoutToolEvidence(request: unknown): boolean;
10
- export declare function shouldRequireVisibleTodoPlan(request: unknown): boolean;
11
10
  export declare function resolveMissingPlanRecoveryInstruction(params: {
12
11
  request: unknown;
13
12
  assistantText?: string;
13
+ requiresPlan?: boolean;
14
14
  hasPlanStateEvidence?: boolean;
15
15
  hasWriteTodosEvidence?: boolean;
16
16
  hasToolResultEvidence?: boolean;
@@ -23,6 +23,7 @@ export declare function resolveExecutionWithoutToolEvidenceTextInstruction(reque
23
23
  hasPlanStateEvidence?: boolean;
24
24
  hasOpenTaskDelegation?: boolean;
25
25
  hasMissingDelegatedExecutionEvidence?: boolean;
26
+ requiresPlan?: boolean;
26
27
  }): string | null;
27
28
  export declare function resolveToolCallRecoveryInstruction(error: unknown): string | null;
28
29
  export declare function appendToolRecoveryInstruction(input: unknown, instruction: string): unknown;
@@ -111,34 +111,10 @@ export function shouldValidateExecutionWithoutToolEvidence(request) {
111
111
  }
112
112
  return readSystemInstructionText(request).length > 0;
113
113
  }
114
- export function shouldRequireVisibleTodoPlan(request) {
115
- const userText = readLatestUserRequestText(request).toLowerCase();
116
- if (!userText) {
117
- return false;
118
- }
119
- return [
120
- "investigate",
121
- "investigation",
122
- "issue",
123
- "issues",
124
- "rca",
125
- "root cause",
126
- "go deeper",
127
- "deep research",
128
- "debug",
129
- "排查",
130
- "调查",
131
- "问题",
132
- "根因",
133
- "故障",
134
- "集群",
135
- "cluster",
136
- ].some((keyword) => userText.includes(keyword));
137
- }
138
114
  export function resolveMissingPlanRecoveryInstruction(params) {
139
115
  const hasPlanEvidence = params.hasWriteTodosEvidence === true
140
116
  || params.hasPlanStateEvidence === true;
141
- if (!shouldRequireVisibleTodoPlan(params.request) || hasPlanEvidence) {
117
+ if (params.requiresPlan !== true || hasPlanEvidence) {
142
118
  return null;
143
119
  }
144
120
  if (params.hasToolResultEvidence === true) {
@@ -158,6 +134,7 @@ export function resolveExecutionWithoutToolEvidenceTextInstruction(request, assi
158
134
  const missingPlanRecoveryInstruction = resolveMissingPlanRecoveryInstruction({
159
135
  request,
160
136
  assistantText: normalizedText,
137
+ requiresPlan: resultEvidence.requiresPlan,
161
138
  hasWriteTodosEvidence: resultEvidence.hasWriteTodosEvidence,
162
139
  hasPlanStateEvidence: resultEvidence.hasIncompletePlanState === true || resultEvidence.hasPlanStateEvidence === true,
163
140
  hasToolResultEvidence: resultEvidence.hasToolResultEvidence,
@@ -11,6 +11,7 @@ export declare const WRITE_TODOS_REQUIRED_PLAN_INSTRUCTION: string;
11
11
  export declare const EXECUTION_WITH_TOOL_EVIDENCE_INSTRUCTION: string;
12
12
  export declare const EXECUTION_WITH_TOOL_EVIDENCE_RETRY_INSTRUCTION: string;
13
13
  export declare const AUTONOMOUS_INVESTIGATION_RECOVERY_INSTRUCTION: string;
14
+ export declare const DELEGATED_TASK_FAILURE_RECOVERY_INSTRUCTION: string;
14
15
  export declare const INTERNAL_RUNTIME_SPILL_PATH_INSTRUCTION: string;
15
16
  export declare const WORKSPACE_RELATIVE_PATH_INSTRUCTION: string;
16
17
  export declare function renderDurableMemoryContextPrompt(memoryContext: string): string;
@@ -14,6 +14,7 @@ export const WRITE_TODOS_REQUIRED_PLAN_INSTRUCTION = readRuntimePrompt("write-to
14
14
  export const EXECUTION_WITH_TOOL_EVIDENCE_INSTRUCTION = readRuntimePrompt("execution-with-tool-evidence");
15
15
  export const EXECUTION_WITH_TOOL_EVIDENCE_RETRY_INSTRUCTION = readRuntimePrompt("execution-with-tool-evidence-retry");
16
16
  export const AUTONOMOUS_INVESTIGATION_RECOVERY_INSTRUCTION = readRuntimePrompt("autonomous-investigation-recovery");
17
+ export const DELEGATED_TASK_FAILURE_RECOVERY_INSTRUCTION = readRuntimePrompt("delegated-task-failure-recovery");
17
18
  export const INTERNAL_RUNTIME_SPILL_PATH_INSTRUCTION = readRuntimePrompt("internal-runtime-spill-path");
18
19
  export const WORKSPACE_RELATIVE_PATH_INSTRUCTION = readRuntimePrompt("workspace-relative-path");
19
20
  export function renderDurableMemoryContextPrompt(memoryContext) {
@@ -199,6 +199,15 @@ function resolveResponseFormat(agent) {
199
199
  function resolveContextSchema(agent) {
200
200
  return getAgentExecutionConfigValue(agent, "contextSchema");
201
201
  }
202
+ function resolveExecutionContract(agent) {
203
+ const value = getAgentExecutionObject(agent, "executionContract");
204
+ if (!value) {
205
+ return undefined;
206
+ }
207
+ return {
208
+ ...(value.requiresPlan === true ? { requiresPlan: true } : {}),
209
+ };
210
+ }
202
211
  function resolveCompiledMiddleware(agent, models) {
203
212
  const middleware = getAgentExecutionConfigValue(agent, "middleware");
204
213
  return compileMiddlewareConfigs(middleware, models, agent.id);
@@ -399,6 +408,7 @@ export function compileBinding(workspaceRoot, agent, agents, referencedSubagentI
399
408
  : undefined;
400
409
  const runtimeGovernanceDefaults = asObject(runtimeDefaults?.governance);
401
410
  const runtimeObservabilityDefaults = asObject(runtimeDefaults?.observability);
411
+ const executionContract = resolveExecutionContract(agent);
402
412
  const compiledFilesystemConfig = agent.executionMode === "langchain-v1"
403
413
  ? mergeConfigObjects(runtimeFilesystemDefaults, getAgentExecutionObject(agent, "filesystem", { executionMode: "langchain-v1" }))
404
414
  : undefined;
@@ -417,6 +427,7 @@ export function compileBinding(workspaceRoot, agent, agents, referencedSubagentI
417
427
  resilience,
418
428
  ...(runtimeGovernanceDefaults ? { governance: runtimeGovernanceDefaults } : {}),
419
429
  ...(runtimeObservabilityDefaults ? { observability: runtimeObservabilityDefaults } : {}),
430
+ ...(executionContract ? { executionContract } : {}),
420
431
  ...(agent.executionMode === "deepagent"
421
432
  ? {
422
433
  deepagent: {
@@ -1,8 +1,11 @@
1
- import { readFileSync } from "node:fs";
2
1
  import path from "node:path";
3
2
  import { validateSkillMetadata } from "../runtime/skills/skill-metadata.js";
4
3
  import { getAgentExecutionConfigValue } from "./support/agent-execution-config.js";
5
4
  import { resolvePromptValue } from "./support/workspace-ref-utils.js";
5
+ const FORBIDDEN_GENERAL_PURPOSE_SUBAGENT_NAME = "general-purpose";
6
+ const FRAMEWORK_AGENT_TOOL_NAMES = new Set(["task"]);
7
+ const FRAMEWORK_EXECUTION_TOOL_NAMES = new Set(["write_todos", "read_todos"]);
8
+ const TERMINAL_STATUS_VALUES = new Set(["completed", "blocked", "failed", "refused"]);
6
9
  function normalizeMode(mode) {
7
10
  if (mode === "warn" || mode === "error") {
8
11
  return mode;
@@ -29,7 +32,76 @@ function isWorkspaceOwnedPath(candidate, roots) {
29
32
  function addIssue(issues, code, message) {
30
33
  issues.push({ code, message });
31
34
  }
32
- function validateAgentContract(agent, referencedSubagentIds, issues) {
35
+ function stripRefPrefix(value, prefix) {
36
+ return value.startsWith(prefix) ? value.slice(prefix.length) : value;
37
+ }
38
+ function resolveRefId(value) {
39
+ return stripRefPrefix(stripRefPrefix(value, "agent/"), "tool/");
40
+ }
41
+ function readBuiltinToolsConfig(agent) {
42
+ const value = getAgentExecutionConfigValue(agent, "builtinTools");
43
+ return typeof value === "object" && value && !Array.isArray(value)
44
+ ? value
45
+ : undefined;
46
+ }
47
+ function readExecutionContractConfig(agent) {
48
+ const value = getAgentExecutionConfigValue(agent, "executionContract");
49
+ return typeof value === "object" && value && !Array.isArray(value)
50
+ ? value
51
+ : undefined;
52
+ }
53
+ function collectAgentToolNames(agent, tools, ownsDelegation) {
54
+ const names = new Set(FRAMEWORK_EXECUTION_TOOL_NAMES);
55
+ if (ownsDelegation) {
56
+ for (const toolName of FRAMEWORK_AGENT_TOOL_NAMES) {
57
+ names.add(toolName);
58
+ }
59
+ }
60
+ for (const ref of agent.toolRefs) {
61
+ const tool = tools.get(resolveRefId(ref));
62
+ if (tool) {
63
+ names.add(tool.id);
64
+ names.add(tool.name);
65
+ }
66
+ names.add(resolveRefId(ref));
67
+ }
68
+ for (const binding of agent.toolBindings ?? []) {
69
+ const tool = tools.get(resolveRefId(binding.ref));
70
+ if (tool) {
71
+ names.add(tool.id);
72
+ names.add(tool.name);
73
+ }
74
+ names.add(resolveRefId(binding.ref));
75
+ }
76
+ for (const tool of agent.inlineTools ?? []) {
77
+ names.add(tool.id);
78
+ names.add(tool.name);
79
+ }
80
+ return names;
81
+ }
82
+ function hasDuplicateValues(values) {
83
+ return new Set(values).size !== values.length;
84
+ }
85
+ function readObject(value) {
86
+ return typeof value === "object" && value !== null && !Array.isArray(value)
87
+ ? value
88
+ : undefined;
89
+ }
90
+ function validateResponseFormatTerminalStatus(agent, responseFormat, issues) {
91
+ const schema = readObject(responseFormat);
92
+ const properties = readObject(schema?.properties);
93
+ const statusProperty = readObject(properties?.status);
94
+ const required = Array.isArray(schema?.required) ? schema.required : [];
95
+ if (!statusProperty || !required.includes("status")) {
96
+ addIssue(issues, "agent.response_format.missing_terminal_status", `Agent ${agent.id} responseFormat must require a status field so parents can distinguish completed, blocked, failed, and refused terminal states.`);
97
+ return;
98
+ }
99
+ const statusEnum = Array.isArray(statusProperty.enum) ? statusProperty.enum : [];
100
+ if (!Array.from(TERMINAL_STATUS_VALUES).every((value) => statusEnum.includes(value))) {
101
+ addIssue(issues, "agent.response_format.incomplete_terminal_status_enum", `Agent ${agent.id} responseFormat status enum must include completed, blocked, failed, and refused.`);
102
+ }
103
+ }
104
+ function validateAgentContract(agent, referencedSubagentIds, tools, issues) {
33
105
  const description = agent.description.trim();
34
106
  const systemPrompt = resolvePromptValue(getAgentExecutionConfigValue(agent, "systemPrompt"), path.dirname(agent.sourcePath));
35
107
  const ownsDelegation = agent.subagentRefs.length > 0 || agent.subagentPathRefs.length > 0 || (agent.asyncSubagents?.length ?? 0) > 0;
@@ -38,45 +110,76 @@ function validateAgentContract(agent, referencedSubagentIds, issues) {
38
110
  || (agent.toolBindings?.length ?? 0) > 0
39
111
  || (agent.inlineTools?.length ?? 0) > 0;
40
112
  const responseFormat = getAgentExecutionConfigValue(agent, "responseFormat");
113
+ const builtinTools = readBuiltinToolsConfig(agent);
114
+ const executionContract = readExecutionContractConfig(agent);
115
+ const localSubagentNames = [
116
+ ...agent.subagentRefs.map(resolveRefId),
117
+ ...(agent.asyncSubagents ?? []).map((subagent) => subagent.name),
118
+ ];
119
+ if (agent.id === FORBIDDEN_GENERAL_PURPOSE_SUBAGENT_NAME) {
120
+ addIssue(issues, "agent.general_purpose.forbidden", `Agent ${agent.id} uses the reserved general-purpose subagent name. Define explicit specialists with narrow responsibilities instead.`);
121
+ }
122
+ for (const asyncSubagent of agent.asyncSubagents ?? []) {
123
+ if (asyncSubagent.name === FORBIDDEN_GENERAL_PURPOSE_SUBAGENT_NAME) {
124
+ addIssue(issues, "agent.general_purpose.forbidden", `Agent ${agent.id} defines async subagent ${asyncSubagent.name}. Define explicit specialists with narrow responsibilities instead.`);
125
+ }
126
+ }
127
+ if (localSubagentNames.includes(FORBIDDEN_GENERAL_PURPOSE_SUBAGENT_NAME)) {
128
+ addIssue(issues, "agent.general_purpose.forbidden", `Agent ${agent.id} references reserved subagent name ${FORBIDDEN_GENERAL_PURPOSE_SUBAGENT_NAME}. Define explicit specialists with narrow responsibilities instead.`);
129
+ }
130
+ if (hasDuplicateValues(localSubagentNames)) {
131
+ addIssue(issues, "agent.subagent.duplicate_name", `Agent ${agent.id} exposes duplicate subagent names. Each delegated capability must have one stable owner.`);
132
+ }
41
133
  if (description.length < 24) {
42
134
  addIssue(issues, "agent.description.too_short", `Agent ${agent.id} should use a more specific description that explains when it should be used.`);
43
135
  }
136
+ if (executionContract?.requiresPlan === true && builtinTools?.todos === false) {
137
+ addIssue(issues, "agent.execution_contract.plan_without_todos", `Agent ${agent.id} requires plan evidence but disables todo tools. Enable todo tools or remove config.executionContract.requiresPlan.`);
138
+ }
44
139
  if (ownsDelegation) {
140
+ if (hasTools) {
141
+ addIssue(issues, "agent.orchestrator.mixed_tool_surface", `Delegating agent ${agent.id} defines both subagents and direct tools. Keep routing agents focused on delegation, and move execution tools to specialist agents.`);
142
+ }
143
+ const modelExposedBuiltins = builtinTools?.modelExposed;
144
+ const exposesOnlyTask = Array.isArray(modelExposedBuiltins)
145
+ && modelExposedBuiltins.length === 1
146
+ && modelExposedBuiltins[0] === "task";
147
+ if (modelExposedBuiltins !== false && !exposesOnlyTask) {
148
+ addIssue(issues, "agent.orchestrator.model_exposed_builtins", `Delegating agent ${agent.id} should expose only the task builtin or set config.builtinTools.modelExposed: false so raw built-in tools do not compete with specialist routing.`);
149
+ }
45
150
  if (!systemPrompt?.trim()) {
46
151
  addIssue(issues, "agent.orchestrator.missing_prompt", `Delegating agent ${agent.id} should define a systemPrompt that explains decomposition, delegation, synthesis, and stop conditions.`);
47
152
  }
48
- if (!/(delegate|delegation|subagent|decompose|synthesi|answer directly|parallel)/i.test(description)) {
49
- addIssue(issues, "agent.orchestrator.description_boundary", `Delegating agent ${agent.id} description should make its delegation boundary explicit, for example when it should answer directly versus delegate.`);
50
- }
51
153
  }
52
154
  if (isSubagent) {
53
155
  if (!systemPrompt?.trim()) {
54
156
  addIssue(issues, "agent.subagent.missing_prompt", `Subagent ${agent.id} should define a systemPrompt that makes its operating boundary and output contract explicit.`);
55
157
  }
56
- if (!/(use this when|when the task|for .*?(analysis|research|search|debug|review|triage|inspection|extraction|comparison|validation|implementation))/i.test(description)) {
57
- addIssue(issues, "agent.subagent.description_trigger", `Subagent ${agent.id} description should clarify when it should be delegated to and what narrow task class it owns.`);
58
- }
59
158
  if (agent.executionMode === "deepagent" && hasTools && responseFormat === undefined) {
60
159
  addIssue(issues, "agent.subagent.deepagent.missing_response_format", `DeepAgents subagent ${agent.id} exposes tools, so it should define config.responseFormat to guarantee a stable task result for its parent agent.`);
61
160
  }
161
+ if (agent.executionMode === "deepagent" && hasTools && responseFormat !== undefined) {
162
+ validateResponseFormatTerminalStatus(agent, responseFormat, issues);
163
+ }
164
+ if (hasTools && agent.skillPathRefs.length === 0) {
165
+ addIssue(issues, "agent.subagent.tools_without_skills", `Subagent ${agent.id} exposes execution tools but no skills. Add skills that describe tool-selection workflows and boundaries.`);
166
+ }
167
+ }
168
+ const toolNames = collectAgentToolNames(agent, tools, ownsDelegation);
169
+ for (const skillPath of agent.skillPathRefs) {
170
+ const metadata = validateSkillMetadata(skillPath);
171
+ for (const allowedTool of metadata.allowedTools ?? []) {
172
+ if (!toolNames.has(allowedTool)) {
173
+ addIssue(issues, "agent.skill.allowed_tool_unavailable", `Agent ${agent.id} attaches skill ${metadata.name}, but that skill allows tool ${allowedTool} which is not available to the agent.`);
174
+ }
175
+ }
62
176
  }
63
- }
64
- function stripFrontmatter(document) {
65
- return document.replace(/^---\s*\n[\s\S]*?\n---\s*(?:\n|$)/, "");
66
177
  }
67
178
  function validateSkillContract(skillRoot, issues) {
68
179
  const metadata = validateSkillMetadata(skillRoot);
69
- const document = readFileSync(path.join(skillRoot, "SKILL.md"), "utf8");
70
- const body = stripFrontmatter(document);
71
180
  const skillName = metadata.name || path.basename(skillRoot);
72
- if (!/(Use this skill when|Use this when)/i.test(body)) {
73
- addIssue(issues, "skill.missing_trigger", `Skill ${skillName} should explain when it should be used, preferably with a clear "Use this skill when..." trigger.`);
74
- }
75
- if (!/(## Workflow|^## Workflow|^\d+\.\s)/m.test(body)) {
76
- addIssue(issues, "skill.missing_workflow", `Skill ${skillName} should define an explicit workflow instead of only background prose.`);
77
- }
78
- if (!/(## Rules|Do not|Output|Caveat|Caveats)/i.test(body)) {
79
- addIssue(issues, "skill.missing_boundaries", `Skill ${skillName} should include execution boundaries such as rules, non-goals, caveats, or output expectations.`);
181
+ if (!metadata.description?.trim()) {
182
+ addIssue(issues, "skill.description.missing", `Skill ${skillName} must define a frontmatter description so agents can compare its boundary without reading the whole document.`);
80
183
  }
81
184
  }
82
185
  function validateToolContract(tool, issues) {
@@ -85,9 +188,6 @@ function validateToolContract(tool, issues) {
85
188
  addIssue(issues, "tool.description.too_short", `Tool ${tool.id} should use a more specific description that explains invocation boundaries and argument expectations.`);
86
189
  return;
87
190
  }
88
- if (!/(Use this when|Do not use|Before calling)/i.test(description)) {
89
- addIssue(issues, "tool.description.missing_boundary", `Tool ${tool.id} description should describe when to call it and, ideally, when not to call it or what must be true before calling it.`);
90
- }
91
191
  }
92
192
  export function validateFrameworkContracts(input) {
93
193
  const mode = normalizeMode(input.mode);
@@ -95,12 +195,12 @@ export function validateFrameworkContracts(input) {
95
195
  return;
96
196
  }
97
197
  const issues = [];
98
- const referencedSubagentIds = new Set(input.agents.flatMap((agent) => agent.subagentRefs.map((ref) => ref.replace(/^agent\//, ""))));
198
+ const referencedSubagentIds = new Set(input.agents.flatMap((agent) => agent.subagentRefs.map(resolveRefId)));
99
199
  for (const agent of input.agents) {
100
200
  if (!isWorkspaceOwnedPath(agent.sourcePath, input.ownedRoots)) {
101
201
  continue;
102
202
  }
103
- validateAgentContract(agent, referencedSubagentIds, issues);
203
+ validateAgentContract(agent, referencedSubagentIds, input.tools, issues);
104
204
  }
105
205
  for (const [skillName, skillRoot] of input.skillRegistry) {
106
206
  if (!isWorkspaceOwnedPath(skillRoot, input.ownedRoots)) {
@@ -29,6 +29,7 @@ const CONSUMED_AGENT_CONFIG_KEYS = [
29
29
  "filesystem",
30
30
  "builtinTools",
31
31
  "interactionMode",
32
+ "executionContract",
32
33
  ];
33
34
  const NON_AGENT_CONFIG_ITEM_KEYS = [
34
35
  "id",
@@ -65,6 +66,7 @@ const MIGRATED_AGENT_CONFIG_KEYS = [
65
66
  "filesystem",
66
67
  "builtinTools",
67
68
  "interactionMode",
69
+ "executionContract",
68
70
  ];
69
71
  function normalizeAgentItemForMerge(item) {
70
72
  const normalized = { ...item };
@@ -267,6 +269,7 @@ function readSharedAgentConfig(config) {
267
269
  ...(config.includeAgentName === "inline" ? { includeAgentName: "inline" } : {}),
268
270
  ...(config.version === "v1" || config.version === "v2" ? { version: config.version } : {}),
269
271
  ...(typeof config.filesystem === "object" && config.filesystem ? { filesystem: config.filesystem } : {}),
272
+ ...(typeof config.executionContract === "object" && config.executionContract ? { executionContract: config.executionContract } : {}),
270
273
  ...(backend ? { backend } : {}),
271
274
  ...(store ? { store } : {}),
272
275
  ...(middleware ? { middleware } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.346",
3
+ "version": "0.0.348",
4
4
  "description": "Workspace runtime for multi-agent applications",
5
5
  "license": "MIT",
6
6
  "type": "module",