@botbotgo/agent-harness 0.0.379 → 0.0.381

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.379";
1
+ export declare const AGENT_HARNESS_VERSION = "0.0.381";
2
2
  export declare const AGENT_HARNESS_RELEASE_DATE = "2026-04-30";
@@ -1,2 +1,2 @@
1
- export const AGENT_HARNESS_VERSION = "0.0.379";
1
+ export const AGENT_HARNESS_VERSION = "0.0.381";
2
2
  export const AGENT_HARNESS_RELEASE_DATE = "2026-04-30";
@@ -212,14 +212,14 @@ function isTodoPlanningToolName(name) {
212
212
  || name === "tool_call_write_todos"
213
213
  || name === "tool_call_read_todos";
214
214
  }
215
- function shouldLimitPromptedJsonToolsToPlanning(input) {
215
+ function shouldLimitToolsToPlanning(input) {
216
216
  const text = stringifyNodeLlamaCppInput(input);
217
217
  return text.includes("required visible planning contract")
218
218
  && !hasPriorToolResultForToolName(input, "write_todos")
219
219
  && !hasPriorToolResultForToolName(input, "tool_call_write_todos");
220
220
  }
221
- function selectPromptedJsonToolsForTurn(input, boundTools) {
222
- if (!shouldLimitPromptedJsonToolsToPlanning(input)) {
221
+ function selectPlanningToolsForTurn(input, boundTools) {
222
+ if (!shouldLimitToolsToPlanning(input)) {
223
223
  return boundTools;
224
224
  }
225
225
  const planningTools = boundTools.filter((tool) => isTodoPlanningToolName(readBoundToolName(tool)));
@@ -270,26 +270,52 @@ function normalizeProviderFacingInput(input) {
270
270
  }
271
271
  return input;
272
272
  }
273
- function createProviderToolMessageCompatModel(model) {
273
+ function createProviderToolMessageCompatModel(model, boundTools = []) {
274
+ const resolveTargetForTurn = (input) => {
275
+ if (boundTools.length === 0 || typeof model.bindTools !== "function") {
276
+ return model;
277
+ }
278
+ const effectiveBoundTools = selectPlanningToolsForTurn(input, boundTools);
279
+ return model.bindTools.call(model, effectiveBoundTools);
280
+ };
274
281
  return new Proxy(model, {
275
282
  has(target, prop) {
276
- if (prop === "bindTools" || prop === "invoke" || prop === "stream" || prop === "withConfig") {
283
+ if (prop === "bindTools" || prop === "invoke" || prop === "stream" || prop === "streamEvents" || prop === "withConfig") {
277
284
  return true;
278
285
  }
279
286
  return prop in target;
280
287
  },
281
288
  get(target, prop, receiver) {
282
289
  if (prop === "bindTools") {
283
- return (tools) => {
284
- const bound = target.bindTools.call(target, tools);
285
- return createProviderToolMessageCompatModel(bound);
286
- };
290
+ return (tools) => createProviderToolMessageCompatModel(target, tools);
287
291
  }
288
292
  if (prop === "invoke") {
289
- return (input, config) => target.invoke.call(target, normalizeProviderFacingInput(input), config);
293
+ return (input, config) => {
294
+ const effectiveTarget = resolveTargetForTurn(input);
295
+ return effectiveTarget.invoke.call(effectiveTarget, normalizeProviderFacingInput(input), config);
296
+ };
290
297
  }
291
298
  if (prop === "stream") {
292
- return (input, config) => target.stream.call(target, normalizeProviderFacingInput(input), config);
299
+ return (input, config) => {
300
+ const effectiveTarget = resolveTargetForTurn(input);
301
+ return effectiveTarget.stream.call(effectiveTarget, normalizeProviderFacingInput(input), config);
302
+ };
303
+ }
304
+ if (prop === "streamEvents") {
305
+ return (input, config) => {
306
+ const effectiveTarget = resolveTargetForTurn(input);
307
+ if (typeof effectiveTarget.streamEvents === "function") {
308
+ return effectiveTarget.streamEvents.call(effectiveTarget, normalizeProviderFacingInput(input), config);
309
+ }
310
+ return (async function* () {
311
+ const output = await effectiveTarget.invoke.call(effectiveTarget, normalizeProviderFacingInput(input), config);
312
+ yield {
313
+ event: "on_chat_model_end",
314
+ name: typeof effectiveTarget.constructor?.name === "string" ? effectiveTarget.constructor.name : "ChatModel",
315
+ data: { output },
316
+ };
317
+ })();
318
+ };
293
319
  }
294
320
  if (prop === "withConfig" && typeof target.withConfig === "function") {
295
321
  return (config) => createProviderToolMessageCompatModel(target.withConfig.call(target, config));
@@ -463,6 +489,65 @@ function normalizeParsedToolCall(payload) {
463
489
  : salvageToolArgs(argsCandidate) ?? {};
464
490
  return { name, args };
465
491
  }
492
+ function extractFallbackTodoContentsFromText(text) {
493
+ const normalized = text.trim();
494
+ if (!normalized) {
495
+ return [];
496
+ }
497
+ const candidates = normalized
498
+ .split(/\r?\n/)
499
+ .map((line) => line
500
+ .replace(/^\s*(?:[-*+]\s+|\d+[.)]\s+|\[[ x~!-]\]\s+)/i, "")
501
+ .replace(/^\s*(?:todo|step|task)\s*\d*\s*[:.-]\s*/i, "")
502
+ .trim())
503
+ .filter((line) => line.length >= 12
504
+ && !/^(?:status|summary|findings|blockers|next actions)\s*[::]?$/i.test(line)
505
+ && !/\b(?:plan|todo|steps?)\s*[::]\s*$/i.test(line)
506
+ && !isLowSignalPlanningLine(line));
507
+ const seen = new Set();
508
+ return candidates.filter((line) => {
509
+ const key = line.toLowerCase();
510
+ if (seen.has(key)) {
511
+ return false;
512
+ }
513
+ seen.add(key);
514
+ return true;
515
+ }).slice(0, 6);
516
+ }
517
+ function isLowSignalPlanningLine(value) {
518
+ const normalized = value.trim().toLowerCase();
519
+ return (normalized.length < 12
520
+ || /^#+\s*/.test(normalized)
521
+ || /^(?:ok|okay|sure|understood|got it|plan|todo|steps?)\.?$/.test(normalized));
522
+ }
523
+ function buildFallbackPlanningToolCall(input, tools, rawText) {
524
+ const toolName = tools.map((tool) => readBoundToolName(tool)).find((name) => name === "write_todos" || name === "tool_call_write_todos");
525
+ if (!toolName) {
526
+ return null;
527
+ }
528
+ const modelPlannedItems = extractFallbackTodoContentsFromText(rawText);
529
+ const fallbackItems = modelPlannedItems.length >= 2
530
+ ? modelPlannedItems
531
+ : [
532
+ "Gather concrete evidence for the requested investigation",
533
+ "Inspect the most relevant runtime signals and tool outputs",
534
+ "Analyze the evidence to identify root cause and impact",
535
+ "Produce the final RCA report with blockers and next actions",
536
+ ];
537
+ const todos = fallbackItems.map((content, index) => ({
538
+ content,
539
+ status: index === 0 ? "in_progress" : "pending",
540
+ }));
541
+ return new AIMessage({
542
+ content: "",
543
+ tool_calls: [{
544
+ id: `tool-${Math.random().toString(36).slice(2, 10)}`,
545
+ name: toolName,
546
+ args: { todos },
547
+ type: "tool_call",
548
+ }],
549
+ });
550
+ }
466
551
  function formatBoundToolInstruction(tool) {
467
552
  if (typeof tool !== "object" || tool === null) {
468
553
  return null;
@@ -485,11 +570,20 @@ function withPromptedJsonToolPrompt(input, tools, options = {}) {
485
570
  if (toolInstructions.length === 0) {
486
571
  return stringifyNodeLlamaCppInput(input);
487
572
  }
573
+ const forcedPlanningInstruction = options.forcePlanningToolCall
574
+ ? [
575
+ "Required planning tool call:",
576
+ "You must call write_todos now before any domain tool or final answer.",
577
+ "Return exactly one JSON object for write_todos with concrete todo items and statuses.",
578
+ "Do not write prose, markdown, analysis, or a plain-text plan.",
579
+ ].join("\n")
580
+ : "";
488
581
  const systemContent = `${NODE_LLAMA_CPP_TOOL_CALL_INSTRUCTION}\n\n${toolInstructions.join("\n\n")}`;
489
582
  const prompt = stringifyNodeLlamaCppInput(input);
490
583
  return [
491
584
  options.suppressThinking ? NO_THINK_CONTROL_TOKEN : "",
492
585
  systemContent,
586
+ forcedPlanningInstruction,
493
587
  prompt,
494
588
  PROMPTED_JSON_FINAL_TOOL_CALL_REMINDER,
495
589
  ].filter(Boolean).join("\n\n");
@@ -497,7 +591,7 @@ function withPromptedJsonToolPrompt(input, tools, options = {}) {
497
591
  function createPromptedJsonToolBindableModel(model, boundTools = [], options = {}) {
498
592
  return new Proxy(model, {
499
593
  has(target, prop) {
500
- if (prop === "bindTools" || prop === "invoke" || prop === "stream" || prop === "withConfig") {
594
+ if (prop === "bindTools" || prop === "invoke" || prop === "stream" || prop === "streamEvents" || prop === "withConfig") {
501
595
  return true;
502
596
  }
503
597
  return prop in target;
@@ -508,14 +602,23 @@ function createPromptedJsonToolBindableModel(model, boundTools = [], options = {
508
602
  }
509
603
  if (prop === "invoke") {
510
604
  return async (input, config) => {
511
- const effectiveBoundTools = selectPromptedJsonToolsForTurn(input, boundTools);
512
- const rawResult = await target.invoke(effectiveBoundTools.length > 0 ? withPromptedJsonToolPrompt(input, effectiveBoundTools, options) : input, config);
605
+ const effectiveBoundTools = selectPlanningToolsForTurn(input, boundTools);
606
+ const forcePlanningToolCall = shouldLimitToolsToPlanning(input);
607
+ const rawResult = await target.invoke(effectiveBoundTools.length > 0
608
+ ? withPromptedJsonToolPrompt(input, effectiveBoundTools, { ...options, forcePlanningToolCall })
609
+ : input, config);
513
610
  if (effectiveBoundTools.length === 0) {
514
611
  return rawResult;
515
612
  }
516
613
  const text = readModelText(rawResult);
517
614
  const parsedToolCall = normalizeParsedToolCall(extractToolCallPayload(text));
518
615
  if (!parsedToolCall) {
616
+ if (forcePlanningToolCall) {
617
+ const fallbackToolCall = buildFallbackPlanningToolCall(input, effectiveBoundTools, text);
618
+ if (fallbackToolCall) {
619
+ return fallbackToolCall;
620
+ }
621
+ }
519
622
  return rawResult;
520
623
  }
521
624
  if (hasPriorToolResultForToolName(input, parsedToolCall.name)) {
@@ -540,6 +643,18 @@ function createPromptedJsonToolBindableModel(model, boundTools = [], options = {
540
643
  })();
541
644
  };
542
645
  }
646
+ if (prop === "streamEvents") {
647
+ return async (input, config) => {
648
+ const value = await receiver.invoke(input, config);
649
+ return (async function* () {
650
+ yield {
651
+ event: "on_chat_model_end",
652
+ name: typeof target.constructor?.name === "string" ? target.constructor.name : "ChatModel",
653
+ data: { output: value },
654
+ };
655
+ })();
656
+ };
657
+ }
543
658
  if (prop === "withConfig" && typeof target.withConfig === "function") {
544
659
  return (config) => createPromptedJsonToolBindableModel(target.withConfig(config), boundTools, options);
545
660
  }
@@ -547,7 +662,7 @@ function createPromptedJsonToolBindableModel(model, boundTools = [], options = {
547
662
  return typeof member === "function" ? member.bind(target) : member;
548
663
  },
549
664
  getOwnPropertyDescriptor(target, prop) {
550
- if (prop === "bindTools" || prop === "invoke" || prop === "stream" || prop === "withConfig") {
665
+ if (prop === "bindTools" || prop === "invoke" || prop === "stream" || prop === "streamEvents" || prop === "withConfig") {
551
666
  return {
552
667
  configurable: true,
553
668
  enumerable: false,
@@ -48,6 +48,23 @@ function isSubstantiveTerminalAssistantOutput(value) {
48
48
  }
49
49
  return true;
50
50
  }
51
+ function inferPlanItemStatusFromTerminalAssistantOutput(value) {
52
+ const terminalStatus = readTerminalExecutionStatus(value);
53
+ if (terminalStatus) {
54
+ return mapTerminalStatusToPlanItemStatus(terminalStatus);
55
+ }
56
+ const normalized = sanitizeVisibleText(value).trim().toLowerCase();
57
+ if (!normalized) {
58
+ return null;
59
+ }
60
+ if (normalized.startsWith("runtime_error=")
61
+ || /\bterminated\b/i.test(normalized)
62
+ || /\b(?:blocked|blocker|failed|failure|refused|unable to complete|could not complete)\b/i.test(normalized)
63
+ || /(?:执行失败|未能完成|无法完成|阻塞|失败)/u.test(normalized)) {
64
+ return "failed";
65
+ }
66
+ return isSubstantiveTerminalAssistantOutput(value) ? "completed" : null;
67
+ }
51
68
  function reconcilePlanStateToTerminalStatus(planState, status, updatedAt) {
52
69
  const items = planState.items.map((item) => ({
53
70
  ...item,
@@ -236,7 +253,10 @@ function getPlanStateFromUpstreamEvent(input) {
236
253
  return null;
237
254
  }
238
255
  const typed = input.event;
239
- const todos = extractTodosArray(typed.data?.output)
256
+ const isWriteTodosStart = typed.event === "on_tool_start"
257
+ && typed.name === "write_todos";
258
+ const todos = (isWriteTodosStart ? extractTodosArray(typed.data?.input) : null)
259
+ ?? extractTodosArray(typed.data?.output)
240
260
  ?? extractTodosArray(typed.data?.chunk);
241
261
  if (!todos) {
242
262
  return null;
@@ -1023,8 +1043,9 @@ export async function* streamHarnessRun(options) {
1023
1043
  }
1024
1044
  }
1025
1045
  currentPlanState = await refreshPlanStateFromPersistence(options, currentPlanState);
1026
- if (isSubstantiveTerminalAssistantOutput(assistantOutput) && planStateHasActiveItems(currentPlanState)) {
1027
- const reconciledPlanState = reconcilePlanStateToTerminalStatus(currentPlanState, "completed", new Date().toISOString());
1046
+ const terminalAssistantPlanItemStatus = inferPlanItemStatusFromTerminalAssistantOutput(assistantOutput);
1047
+ if (terminalAssistantPlanItemStatus && planStateHasActiveItems(currentPlanState)) {
1048
+ const reconciledPlanState = reconcilePlanStateToTerminalStatus(currentPlanState, terminalAssistantPlanItemStatus, new Date().toISOString());
1028
1049
  const signature = buildPlanStateSignature(reconciledPlanState);
1029
1050
  if (signature !== lastPlanStateSignature) {
1030
1051
  const previousPlanState = currentPlanState;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.379",
3
+ "version": "0.0.381",
4
4
  "description": "Workspace runtime for multi-agent applications",
5
5
  "license": "MIT",
6
6
  "type": "module",