@botbotgo/agent-harness 0.0.347 → 0.0.349

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2 +1,2 @@
1
- export declare const AGENT_HARNESS_VERSION = "0.0.347";
1
+ export declare const AGENT_HARNESS_VERSION = "0.0.349";
2
2
  export declare const AGENT_HARNESS_RELEASE_DATE = "2026-04-24";
@@ -1,2 +1,2 @@
1
- export const AGENT_HARNESS_VERSION = "0.0.347";
1
+ export const AGENT_HARNESS_VERSION = "0.0.349";
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);
@@ -705,7 +705,8 @@ export async function* streamRuntimeExecution(options) {
705
705
  hasToolResultEvidence: invokeExecutionEvidence.hasSuccessfulNonTodoToolResultEvidence,
706
706
  })
707
707
  : null;
708
- const effectiveInvokeFallbackRecoveryInstruction = invokeFallbackMissingPlanRecoveryInstruction ?? invokeFallbackRecoveryInstruction;
708
+ const effectiveInvokeFallbackRecoveryInstruction = invokeFallbackMissingPlanRecoveryInstruction
709
+ ?? invokeFallbackRecoveryInstruction;
709
710
  if (effectiveInvokeFallbackRecoveryInstruction) {
710
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);
711
712
  const recoveredToolResults = Array.isArray(recovered.metadata?.executedToolResults)
@@ -13,8 +13,16 @@ const NODE_LLAMA_CPP_TOOL_CALL_INSTRUCTION = [
13
13
  "If you need a tool, respond with only one JSON object.",
14
14
  'Use this exact shape: {"name":"tool_name","arguments":{"key":"value"}}',
15
15
  "Do not add markdown, prose, or code fences unless the output is wrapped inside <tool_call>...</tool_call>.",
16
+ "When the latest user message explicitly requests a tool call or provides a tool-call JSON object, call that tool instead of answering locally.",
17
+ "If the conversation already contains TOOL_RESULT for the requested work, answer from that result instead of repeating the same tool call.",
16
18
  "If no tool is needed, answer normally.",
17
19
  ].join("\n");
20
+ const PROMPTED_JSON_FINAL_TOOL_CALL_REMINDER = [
21
+ "Final tool-call rule:",
22
+ "If the correct next step is a tool call, return exactly one JSON object and no prose.",
23
+ "If a TOOL_RESULT is already present for the requested work, do not repeat that tool call; answer normally.",
24
+ 'Shape: {"name":"tool_name","arguments":{}}',
25
+ ].join("\n");
18
26
  function readModelText(value) {
19
27
  if (typeof value === "string") {
20
28
  return value.trim();
@@ -175,6 +183,23 @@ function readToolMessageMetadata(value) {
175
183
  : undefined;
176
184
  return { name, toolCallId };
177
185
  }
186
+ function hasPriorToolResultForToolName(input, toolName) {
187
+ if (!toolName) {
188
+ return false;
189
+ }
190
+ if (Array.isArray(input)) {
191
+ return input.some((message) => {
192
+ if (mapMessageRole(message) !== "TOOL") {
193
+ return false;
194
+ }
195
+ return readToolMessageMetadata(message).name === toolName;
196
+ });
197
+ }
198
+ if (typeof input === "object" && input !== null && Array.isArray(input.messages)) {
199
+ return hasPriorToolResultForToolName(input.messages, toolName);
200
+ }
201
+ return false;
202
+ }
178
203
  function normalizeReadFileToolContent(name, content) {
179
204
  if (name !== "read_file") {
180
205
  return content;
@@ -274,22 +299,75 @@ function extractToolCallPayload(text) {
274
299
  if (direct) {
275
300
  return direct;
276
301
  }
277
- const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1]?.trim();
302
+ const fenced = extractFencePayload(trimmed);
278
303
  if (fenced) {
279
304
  const parsed = tryParseJson(fenced);
280
305
  if (parsed) {
281
306
  return parsed;
282
307
  }
283
308
  }
284
- const xml = trimmed.match(/<tool_call>\s*([\s\S]*?)\s*<\/tool_call>/i)?.[1]?.trim();
309
+ const xml = extractTaggedContent(trimmed, "tool_call");
285
310
  if (xml) {
286
311
  const parsed = tryParseJson(xml);
287
312
  if (parsed) {
288
313
  return parsed;
289
314
  }
290
315
  }
316
+ const toolCallsXml = extractTaggedContent(trimmed, "tool_calls");
317
+ if (toolCallsXml) {
318
+ const parsed = tryParseJson(toolCallsXml);
319
+ if (parsed) {
320
+ return parsed;
321
+ }
322
+ }
323
+ const codeBlock = extractToolCodePayload(trimmed);
324
+ if (codeBlock) {
325
+ return codeBlock;
326
+ }
291
327
  return null;
292
328
  }
329
+ function extractFencePayload(text) {
330
+ const start = text.indexOf("```");
331
+ if (start < 0) {
332
+ return null;
333
+ }
334
+ const contentStart = text.indexOf("\n", start + 3);
335
+ const bodyStart = contentStart >= 0 ? contentStart + 1 : start + 3;
336
+ const end = text.indexOf("```", bodyStart);
337
+ if (end < 0) {
338
+ return null;
339
+ }
340
+ return text.slice(bodyStart, end).trim();
341
+ }
342
+ function extractTaggedContent(text, tagName) {
343
+ const lowerText = text.toLowerCase();
344
+ const openTag = `<${tagName}>`;
345
+ const closeTag = `</${tagName}>`;
346
+ const start = lowerText.indexOf(openTag);
347
+ if (start < 0) {
348
+ return null;
349
+ }
350
+ const bodyStart = start + openTag.length;
351
+ const end = lowerText.indexOf(closeTag, bodyStart);
352
+ if (end < 0) {
353
+ return null;
354
+ }
355
+ return text.slice(bodyStart, end).trim();
356
+ }
357
+ function extractToolCodePayload(text) {
358
+ const name = extractTaggedContent(text, "tool_code");
359
+ if (!name) {
360
+ return null;
361
+ }
362
+ const rawArgs = extractTaggedContent(text, "tool_args");
363
+ const parsedArgs = rawArgs ? tryParseJson(rawArgs.trim()) : null;
364
+ const args = typeof parsedArgs === "object" && parsedArgs !== null && !Array.isArray(parsedArgs)
365
+ ? parsedArgs
366
+ : Array.isArray(parsedArgs)
367
+ ? { args: parsedArgs }
368
+ : {};
369
+ return { name, arguments: args };
370
+ }
293
371
  function normalizeParsedToolCall(payload) {
294
372
  if (typeof payload !== "object" || payload === null) {
295
373
  return null;
@@ -305,7 +383,9 @@ function normalizeParsedToolCall(payload) {
305
383
  return null;
306
384
  }
307
385
  const argsCandidate = typed.arguments ?? typed.args ?? typed.parameters ?? typed.input ?? functionPayload?.arguments ?? {};
308
- const args = salvageToolArgs(argsCandidate) ?? {};
386
+ const args = Array.isArray(argsCandidate)
387
+ ? { args: argsCandidate }
388
+ : salvageToolArgs(argsCandidate) ?? {};
309
389
  return { name, args };
310
390
  }
311
391
  function formatBoundToolInstruction(tool) {
@@ -325,16 +405,16 @@ function formatBoundToolInstruction(tool) {
325
405
  `Arguments JSON schema: ${JSON.stringify(schema)}`,
326
406
  ].filter(Boolean).join("\n");
327
407
  }
328
- function withNodeLlamaCppToolPrompt(input, tools) {
408
+ function withPromptedJsonToolPrompt(input, tools) {
329
409
  const toolInstructions = tools.map((tool) => formatBoundToolInstruction(tool)).filter((value) => Boolean(value));
330
410
  if (toolInstructions.length === 0) {
331
411
  return stringifyNodeLlamaCppInput(input);
332
412
  }
333
413
  const systemContent = `${NODE_LLAMA_CPP_TOOL_CALL_INSTRUCTION}\n\n${toolInstructions.join("\n\n")}`;
334
414
  const prompt = stringifyNodeLlamaCppInput(input);
335
- return [systemContent, prompt].filter(Boolean).join("\n\n");
415
+ return [systemContent, prompt, PROMPTED_JSON_FINAL_TOOL_CALL_REMINDER].filter(Boolean).join("\n\n");
336
416
  }
337
- function createNodeLlamaCppToolBindableModel(model, boundTools = []) {
417
+ function createPromptedJsonToolBindableModel(model, boundTools = []) {
338
418
  return new Proxy(model, {
339
419
  has(target, prop) {
340
420
  if (prop === "bindTools" || prop === "invoke" || prop === "stream" || prop === "withConfig") {
@@ -344,11 +424,11 @@ function createNodeLlamaCppToolBindableModel(model, boundTools = []) {
344
424
  },
345
425
  get(target, prop, receiver) {
346
426
  if (prop === "bindTools") {
347
- return (tools) => createNodeLlamaCppToolBindableModel(target, tools);
427
+ return (tools) => createPromptedJsonToolBindableModel(target, tools);
348
428
  }
349
429
  if (prop === "invoke") {
350
430
  return async (input, config) => {
351
- const rawResult = await target.invoke(boundTools.length > 0 ? withNodeLlamaCppToolPrompt(input, boundTools) : input, config);
431
+ const rawResult = await target.invoke(boundTools.length > 0 ? withPromptedJsonToolPrompt(input, boundTools) : input, config);
352
432
  if (boundTools.length === 0) {
353
433
  return rawResult;
354
434
  }
@@ -357,6 +437,9 @@ function createNodeLlamaCppToolBindableModel(model, boundTools = []) {
357
437
  if (!parsedToolCall) {
358
438
  return rawResult;
359
439
  }
440
+ if (hasPriorToolResultForToolName(input, parsedToolCall.name)) {
441
+ return rawResult;
442
+ }
360
443
  return new AIMessage({
361
444
  content: "",
362
445
  tool_calls: [{
@@ -377,7 +460,7 @@ function createNodeLlamaCppToolBindableModel(model, boundTools = []) {
377
460
  };
378
461
  }
379
462
  if (prop === "withConfig" && typeof target.withConfig === "function") {
380
- return (config) => createNodeLlamaCppToolBindableModel(target.withConfig(config), boundTools);
463
+ return (config) => createPromptedJsonToolBindableModel(target.withConfig(config), boundTools);
381
464
  }
382
465
  const member = Reflect.get(target, prop, receiver);
383
466
  return typeof member === "function" ? member.bind(target) : member;
@@ -409,7 +492,7 @@ async function createNodeLlamaCppModel(model) {
409
492
  }
410
493
  try {
411
494
  const { ChatLlamaCpp } = await import("@langchain/community/chat_models/llama_cpp");
412
- return createNodeLlamaCppToolBindableModel(await ChatLlamaCpp.initialize({
495
+ return createPromptedJsonToolBindableModel(await ChatLlamaCpp.initialize({
413
496
  ...model.init,
414
497
  modelPath,
415
498
  }));
@@ -423,10 +506,23 @@ export async function createResolvedModel(model, modelResolver) {
423
506
  return modelResolver(model.id);
424
507
  }
425
508
  if (model.provider === "ollama") {
426
- return createProviderToolMessageCompatModel(new ChatOllama({ model: model.model, ...model.init }));
509
+ const { toolCallingMode, ...init } = model.init ?? {};
510
+ const resolved = new ChatOllama({ model: model.model, ...init });
511
+ if (toolCallingMode === "prompted-json") {
512
+ return createPromptedJsonToolBindableModel(resolved);
513
+ }
514
+ return createProviderToolMessageCompatModel(resolved);
427
515
  }
428
516
  if (model.provider === "openai-compatible") {
429
- return createProviderToolMessageCompatModel(new ChatOpenAI({ model: model.model, ...normalizeOpenAICompatibleInit(model.init) }));
517
+ const { toolCallingMode, ...init } = model.init ?? {};
518
+ const resolved = new ChatOpenAI({
519
+ model: model.model,
520
+ ...normalizeOpenAICompatibleInit(init),
521
+ });
522
+ if (toolCallingMode === "prompted-json") {
523
+ return createPromptedJsonToolBindableModel(resolved);
524
+ }
525
+ return createProviderToolMessageCompatModel(resolved);
430
526
  }
431
527
  if (model.provider === "openai") {
432
528
  return createProviderToolMessageCompatModel(new ChatOpenAI({ model: model.model, ...model.init }));
@@ -57,9 +57,7 @@ export function buildDeepAgentSystemPromptWithCapabilityHierarchy(input) {
57
57
  const basePrompt = typeof input.systemPrompt === "string" ? input.systemPrompt : undefined;
58
58
  const skills = buildSkillCatalog(input.skills ?? []);
59
59
  const tools = input.tools ?? [];
60
- if (input.subagents.length === 0 && skills.length === 0 && tools.length === 0) {
61
- return input.systemPrompt;
62
- }
60
+ const hasNoConfiguredCapabilities = input.subagents.length === 0 && skills.length === 0 && tools.length === 0;
63
61
  const catalogPrompt = [
64
62
  "Capability selection hierarchy:",
65
63
  "1. If the current request fits an available subagent, delegate with the task tool before using local skills or raw tools.",
@@ -86,13 +84,30 @@ export function buildDeepAgentSystemPromptWithCapabilityHierarchy(input) {
86
84
  "Available skills for this agent:",
87
85
  ...skills.map(formatCapabilityLine),
88
86
  ]
89
- : []),
87
+ : [
88
+ "",
89
+ "Available skills for this agent: none.",
90
+ ]),
90
91
  ...(tools.length > 0
91
92
  ? [
92
93
  "",
93
94
  "Raw tools available to this agent:",
94
95
  ...tools.map(formatCapabilityLine),
95
96
  ]
97
+ : [
98
+ "",
99
+ "Raw tools available to this agent: none.",
100
+ "Do not mention, offer, or claim any raw tool that is not listed here.",
101
+ ]),
102
+ ...(hasNoConfiguredCapabilities
103
+ ? [
104
+ "",
105
+ "No-capability terminal rule:",
106
+ "This agent currently has no configured subagents, skills, or raw tools.",
107
+ "For requests that require execution, retrieval, live data, workspace inspection, or external evidence, return a direct refusal based on the missing configured capability.",
108
+ "Do not propose a tool-based next step, do not name any unlisted tool, and do not ask the user for links so this agent can fetch them.",
109
+ "The only valid next step is for the workspace owner to attach an appropriate subagent, skill, or tool and retry.",
110
+ ]
96
111
  : []),
97
112
  "",
98
113
  ].join("\n");
@@ -140,8 +140,12 @@ function validateAgentContract(agent, referencedSubagentIds, tools, issues) {
140
140
  if (hasTools) {
141
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
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.`);
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.`);
145
149
  }
146
150
  if (!systemPrompt?.trim()) {
147
151
  addIssue(issues, "agent.orchestrator.missing_prompt", `Delegating agent ${agent.id} should define a systemPrompt that explains decomposition, delegation, synthesis, and stop conditions.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.347",
3
+ "version": "0.0.349",
4
4
  "description": "Workspace runtime for multi-agent applications",
5
5
  "license": "MIT",
6
6
  "type": "module",