@botbotgo/agent-harness 0.0.345 → 0.0.347

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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,72 @@ 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
+ if (builtinTools?.modelExposed !== false) {
144
+ addIssue(issues, "agent.orchestrator.model_exposed_builtins", `Delegating agent ${agent.id} should set config.builtinTools.modelExposed: false so raw built-in tools do not compete with specialist routing.`);
145
+ }
45
146
  if (!systemPrompt?.trim()) {
46
147
  addIssue(issues, "agent.orchestrator.missing_prompt", `Delegating agent ${agent.id} should define a systemPrompt that explains decomposition, delegation, synthesis, and stop conditions.`);
47
148
  }
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
149
  }
52
150
  if (isSubagent) {
53
151
  if (!systemPrompt?.trim()) {
54
152
  addIssue(issues, "agent.subagent.missing_prompt", `Subagent ${agent.id} should define a systemPrompt that makes its operating boundary and output contract explicit.`);
55
153
  }
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
154
  if (agent.executionMode === "deepagent" && hasTools && responseFormat === undefined) {
60
155
  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
156
  }
157
+ if (agent.executionMode === "deepagent" && hasTools && responseFormat !== undefined) {
158
+ validateResponseFormatTerminalStatus(agent, responseFormat, issues);
159
+ }
160
+ if (hasTools && agent.skillPathRefs.length === 0) {
161
+ 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.`);
162
+ }
163
+ }
164
+ const toolNames = collectAgentToolNames(agent, tools, ownsDelegation);
165
+ for (const skillPath of agent.skillPathRefs) {
166
+ const metadata = validateSkillMetadata(skillPath);
167
+ for (const allowedTool of metadata.allowedTools ?? []) {
168
+ if (!toolNames.has(allowedTool)) {
169
+ 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.`);
170
+ }
171
+ }
62
172
  }
63
- }
64
- function stripFrontmatter(document) {
65
- return document.replace(/^---\s*\n[\s\S]*?\n---\s*(?:\n|$)/, "");
66
173
  }
67
174
  function validateSkillContract(skillRoot, issues) {
68
175
  const metadata = validateSkillMetadata(skillRoot);
69
- const document = readFileSync(path.join(skillRoot, "SKILL.md"), "utf8");
70
- const body = stripFrontmatter(document);
71
176
  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.`);
177
+ if (!metadata.description?.trim()) {
178
+ addIssue(issues, "skill.description.missing", `Skill ${skillName} must define a frontmatter description so agents can compare its boundary without reading the whole document.`);
80
179
  }
81
180
  }
82
181
  function validateToolContract(tool, issues) {
@@ -85,9 +184,6 @@ function validateToolContract(tool, issues) {
85
184
  addIssue(issues, "tool.description.too_short", `Tool ${tool.id} should use a more specific description that explains invocation boundaries and argument expectations.`);
86
185
  return;
87
186
  }
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
187
  }
92
188
  export function validateFrameworkContracts(input) {
93
189
  const mode = normalizeMode(input.mode);
@@ -95,12 +191,12 @@ export function validateFrameworkContracts(input) {
95
191
  return;
96
192
  }
97
193
  const issues = [];
98
- const referencedSubagentIds = new Set(input.agents.flatMap((agent) => agent.subagentRefs.map((ref) => ref.replace(/^agent\//, ""))));
194
+ const referencedSubagentIds = new Set(input.agents.flatMap((agent) => agent.subagentRefs.map(resolveRefId)));
99
195
  for (const agent of input.agents) {
100
196
  if (!isWorkspaceOwnedPath(agent.sourcePath, input.ownedRoots)) {
101
197
  continue;
102
198
  }
103
- validateAgentContract(agent, referencedSubagentIds, issues);
199
+ validateAgentContract(agent, referencedSubagentIds, input.tools, issues);
104
200
  }
105
201
  for (const [skillName, skillRoot] of input.skillRegistry) {
106
202
  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.345",
3
+ "version": "0.0.347",
4
4
  "description": "Workspace runtime for multi-agent applications",
5
5
  "license": "MIT",
6
6
  "type": "module",