@botbotgo/agent-harness 0.0.343 → 0.0.345

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.
@@ -176,6 +176,34 @@ export async function streamChatMessage(input) {
176
176
  }
177
177
  stderrWriteChain = enqueueChatWrite(input.stderr, input.stderrStream, stderrWriteChain, message);
178
178
  };
179
+ const emitProgressCommentary = (rawText, agentId) => {
180
+ const lines = rawText.split("\n").filter((line) => line.length > 0);
181
+ if (lines.length === 0) {
182
+ return;
183
+ }
184
+ const now = Date.now();
185
+ const linePrefix = `[${formatPerfClock(now)} +${formatElapsed(now)}]${formatAgentProgressLabel(agentId || latestAgentId)}`;
186
+ const serializedLines = lines
187
+ .map((line) => {
188
+ const signature = `${agentId || latestAgentId}|${line}`;
189
+ if (renderedTodoTransitionLines.has(signature)) {
190
+ return null;
191
+ }
192
+ renderedTodoTransitionLines.add(signature);
193
+ return `${linePrefix} ${line}\n`;
194
+ })
195
+ .filter((line) => line !== null);
196
+ if (serializedLines.length === 0) {
197
+ return;
198
+ }
199
+ if (input.requestEvents && input.liveRequestTree && !suppressRequestTreeRendering && lastRenderedRequestTree) {
200
+ liveRequestAnnotations.push(...serializedLines);
201
+ clearLiveRequestTree();
202
+ drawLiveRequestTree();
203
+ return;
204
+ }
205
+ writeChatStderr(serializedLines.join(""));
206
+ };
179
207
  const clearIdleProgressTimer = () => {
180
208
  if (idleProgressTimer) {
181
209
  clearTimeout(idleProgressTimer);
@@ -184,7 +212,7 @@ export async function streamChatMessage(input) {
184
212
  };
185
213
  const scheduleIdleProgress = () => {
186
214
  clearIdleProgressTimer();
187
- if (idleProgressMs <= 0 || wroteContent || wroteRenderableBlocks) {
215
+ if (idleProgressMs <= 0) {
188
216
  return;
189
217
  }
190
218
  idleProgressTimer = setTimeout(() => {
@@ -247,10 +275,11 @@ export async function streamChatMessage(input) {
247
275
  continue;
248
276
  }
249
277
  const text = `TODO ${label}: ${todo.content}.`;
250
- if (renderedTodoTransitionLines.has(text)) {
278
+ const signature = `${snapshot.agentId || latestAgentId || ""}|${text}`;
279
+ if (renderedTodoTransitionLines.has(signature)) {
251
280
  continue;
252
281
  }
253
- renderedTodoTransitionLines.add(text);
282
+ renderedTodoTransitionLines.add(signature);
254
283
  lines.push(`[${formatPerfClock(Date.now())} +${formatElapsed(Date.now())}]${formatAgentProgressLabel(snapshot.agentId)} ${text}\n`);
255
284
  }
256
285
  previousTodoStatuses = nextStatuses;
@@ -354,17 +383,7 @@ export async function streamChatMessage(input) {
354
383
  if (wroteContent || wroteRenderableBlocks) {
355
384
  return;
356
385
  }
357
- if (renderedTodoTransitionLines.has(delta.text)) {
358
- return;
359
- }
360
- const progressLine = `[${formatPerfClock(Date.now())} +${formatElapsed(Date.now())}]${formatAgentProgressLabel(delta.agentId)} ${delta.text}\n`;
361
- if (input.requestEvents && input.liveRequestTree && !suppressRequestTreeRendering && lastRenderedRequestTree) {
362
- liveRequestAnnotations.push(progressLine);
363
- clearLiveRequestTree();
364
- drawLiveRequestTree();
365
- return;
366
- }
367
- writeChatStderr(progressLine);
386
+ emitProgressCommentary(delta.text, delta.agentId);
368
387
  }
369
388
  },
370
389
  });
@@ -161,10 +161,12 @@ export function renderChatRuntimeFailure(output, modelInfo) {
161
161
  if (!normalized.includes("fetch failed") &&
162
162
  !normalized.includes("connection error") &&
163
163
  !normalized.includes("timed out") &&
164
+ !normalized.includes("error 524") &&
164
165
  !normalized.includes("404 page not found")) {
165
166
  return output;
166
167
  }
167
168
  const lines = [trimmed];
169
+ const isTimeoutStyleError = /(\berror 524\b|\btimeout\b|\btimed out\b)/i.test(trimmed);
168
170
  if (modelInfo?.provider || modelInfo?.model) {
169
171
  lines.push(`provider=${modelInfo?.provider ?? "unknown"}${modelInfo?.model ? ` model=${modelInfo.model}` : ""}`);
170
172
  }
@@ -178,6 +180,9 @@ export function renderChatRuntimeFailure(output, modelInfo) {
178
180
  if (hint) {
179
181
  lines.push(hint);
180
182
  }
183
+ if (isTimeoutStyleError) {
184
+ lines.push("Hint: this usually indicates the model endpoint dropped the connection before producing a full answer.");
185
+ }
181
186
  return lines.join("\n");
182
187
  }
183
188
  export async function probeChatWorkspace(input) {
package/dist/cli.js CHANGED
@@ -4,5 +4,5 @@ import { runCli, resolveInvokedCliHref } from "./cli/main.js";
4
4
  const invokedPath = resolveInvokedCliHref(process.argv[1]);
5
5
  if (import.meta.url === invokedPath) {
6
6
  const exitCode = await runCli(process.argv.slice(2));
7
- process.exitCode = exitCode;
7
+ process.exit(exitCode);
8
8
  }
@@ -1,2 +1,2 @@
1
- export declare const AGENT_HARNESS_VERSION = "0.0.343";
1
+ export declare const AGENT_HARNESS_VERSION = "0.0.345";
2
2
  export declare const AGENT_HARNESS_RELEASE_DATE = "2026-04-24";
@@ -1,2 +1,2 @@
1
- export const AGENT_HARNESS_VERSION = "0.0.343";
1
+ export const AGENT_HARNESS_VERSION = "0.0.345";
2
2
  export const AGENT_HARNESS_RELEASE_DATE = "2026-04-24";
@@ -84,6 +84,19 @@ function hasMissingDelegatedExecutionEvidence(evidence) {
84
84
  function hasMissingDelegatedFindings(evidence) {
85
85
  return evidence.hasDelegatedAgentWithConfiguredTools && evidence.hasOnlyPlaceholderTaskCompletion;
86
86
  }
87
+ function isRuntimeFailureOutput(value) {
88
+ return value.trim().startsWith("runtime_error=");
89
+ }
90
+ function resolveStreamedRuntimeFailureRecoveryInstruction(output, evidence) {
91
+ if (!isRuntimeFailureOutput(output)) {
92
+ return null;
93
+ }
94
+ const hasExecutionEvidence = evidence.hasToolResultEvidence
95
+ || evidence.hasPlanStateEvidence
96
+ || evidence.hasOpenTaskDelegation
97
+ || evidence.hasDelegatedExecutionToolEvidence;
98
+ return hasExecutionEvidence ? null : EXECUTION_WITH_TOOL_EVIDENCE_RETRY_INSTRUCTION;
99
+ }
87
100
  function resolveDelegatedExecutionRecoveryInstruction(evidence) {
88
101
  if (hasMissingDelegatedFindings(evidence)
89
102
  || (evidence.hasOpenTaskDelegation
@@ -442,6 +455,9 @@ export async function* streamRuntimeExecution(options) {
442
455
  hasMissingDelegatedExecutionEvidence: hasMissingDelegatedExecutionEvidence(streamedExecutionEvidence),
443
456
  })
444
457
  : null;
458
+ const streamedRuntimeFailureRecoveryInstruction = projectionState.emittedOutput
459
+ ? resolveStreamedRuntimeFailureRecoveryInstruction(projectionState.emittedOutput, streamedExecutionEvidence)
460
+ : null;
445
461
  const missingPlanRecoveryInstruction = !hasUnresolvedExecution(streamedExecutionEvidence) && !delegatedExecutionRecoveryInstruction
446
462
  ? resolveMissingPlanRecoveryInstruction({
447
463
  request,
@@ -453,7 +469,10 @@ export async function* streamRuntimeExecution(options) {
453
469
  : null;
454
470
  const retryInstruction = !emittedUnsafeStreamSideEffects && sawRetrySafeInvalidToolSelectionError
455
471
  ? INVALID_TOOL_SELECTION_RECOVERY_INSTRUCTION
456
- : delegatedExecutionRecoveryInstruction ?? missingPlanRecoveryInstruction ?? executionWithoutToolEvidenceInstruction;
472
+ : delegatedExecutionRecoveryInstruction
473
+ ?? streamedRuntimeFailureRecoveryInstruction
474
+ ?? missingPlanRecoveryInstruction
475
+ ?? executionWithoutToolEvidenceInstruction;
457
476
  if (retryInstruction) {
458
477
  let retried;
459
478
  retried = await options.invoke(options.applyToolRecoveryInstruction(options.binding, retryInstruction), options.input, options.sessionId, options.runtimeOptions.requestId ?? options.sessionId, undefined, options.history, options.runtimeOptions);
@@ -32,6 +32,10 @@ export function resolveStreamIdleTimeout(binding) {
32
32
  }
33
33
  const BUILTIN_RETRYABLE_PROVIDER_MESSAGES = [
34
34
  "unexpected eof",
35
+ "other side closed",
36
+ "socket hang up",
37
+ "connection reset",
38
+ "econnreset",
35
39
  ];
36
40
  function isEmptyFinalAiMessageError(error) {
37
41
  const message = error instanceof Error ? error.message : String(error);
@@ -332,7 +332,16 @@ function normalizeStreamChunk(chunk) {
332
332
  return { kind: "content", content: chunk.content ?? "" };
333
333
  }
334
334
  function normalizeCommentaryText(value) {
335
- return value.replace(/\s+/g, " ").trim();
335
+ return value
336
+ .split("\n")
337
+ .map((line) => {
338
+ const leading = /^\s*/.exec(line)?.[0] ?? "";
339
+ const body = line.slice(leading.length).replace(/[^\S\n]+/g, " ").trimEnd();
340
+ return `${leading}${body}`;
341
+ })
342
+ .join("\n")
343
+ .replace(/\n{3,}/g, "\n\n")
344
+ .trim();
336
345
  }
337
346
  function ensureCommentarySentence(value) {
338
347
  const normalized = normalizeCommentaryText(value);
@@ -359,9 +368,9 @@ function summarizePlanState(planState) {
359
368
  return "[ ]";
360
369
  }
361
370
  };
362
- const items = planState.items.slice(0, 6).map((item) => `${statusMarker(item.status)} ${item.content}`);
363
- const suffix = planState.items.length > items.length ? ` | +${planState.items.length - items.length} more` : "";
364
- return `TODO: ${items.join(" | ")}${suffix}`;
371
+ const items = planState.items.slice(0, 6).map((item) => ` ${statusMarker(item.status)} ${item.content}`);
372
+ const suffix = planState.items.length > items.length ? [` ... ${planState.items.length - items.length} more`] : [];
373
+ return ["TODO", ...items, ...suffix].join("\n");
365
374
  }
366
375
  function summarizePlanStateTerminalTransitions(previousPlanState, nextPlanState) {
367
376
  const previousByKey = new Map((previousPlanState?.items ?? []).map((item) => [normalizePlanItemKey(item), item]));
@@ -523,6 +532,45 @@ function createRuntimeMemoryRecallSteps(sessionId, requestId, items) {
523
532
  },
524
533
  ];
525
534
  }
535
+ function readObject(value) {
536
+ return typeof value === "object" && value !== null ? value : null;
537
+ }
538
+ function isFallbackSafeUpstreamStartupEvent(event, rootAgentId) {
539
+ const typed = readObject(event);
540
+ const eventName = typeof typed?.event === "string" ? typed.event : "";
541
+ if (eventName.startsWith("on_chat_model_") || eventName.startsWith("on_llm_")) {
542
+ return true;
543
+ }
544
+ if (!eventName.startsWith("on_chain_")) {
545
+ return false;
546
+ }
547
+ if (typed?.run_type === "tool") {
548
+ return false;
549
+ }
550
+ const name = typeof typed?.name === "string" ? typed.name : "";
551
+ const metadata = readObject(typed?.metadata);
552
+ const langgraphNode = typeof metadata?.langgraph_node === "string" ? metadata.langgraph_node : "";
553
+ const langchainAgentName = typeof metadata?.lc_agent_name === "string" ? metadata.lc_agent_name : "";
554
+ return (name === "__start__"
555
+ || langgraphNode === "__start__"
556
+ || langgraphNode === "model_request"
557
+ || name.endsWith(".before_agent")
558
+ || name.endsWith(".before_model")
559
+ || (name === rootAgentId && langchainAgentName === rootAgentId));
560
+ }
561
+ function planStateHasAnyItems(planState) {
562
+ return (planState?.items.length ?? 0) > 0;
563
+ }
564
+ function isStartupRuntimeFailureOutput(value) {
565
+ const normalized = value.trim().toLowerCase();
566
+ if (!normalized.startsWith("runtime_error=")) {
567
+ return false;
568
+ }
569
+ return ((normalized.includes("terminated") && normalized.includes("other side closed"))
570
+ || normalized.includes("socket hang up")
571
+ || normalized.includes("connection reset")
572
+ || normalized.includes("econnreset"));
573
+ }
526
574
  export async function* streamHarnessRun(options) {
527
575
  const priorHistoryPromise = Promise.resolve(options.isNewSession ? [] : undefined).then((historyHint) => historyHint ?? options.loadPriorHistory(options.sessionId, options.requestId));
528
576
  yield { type: "event", event: await options.requestCreatedEventPromise };
@@ -533,6 +581,7 @@ export async function* streamHarnessRun(options) {
533
581
  let emitted = false;
534
582
  let streamActivityObserved = false;
535
583
  let nonUpstreamStreamActivityObserved = false;
584
+ let observedOnlyFallbackSafeUpstreamEvents = true;
536
585
  let assistantOutput = "";
537
586
  let assistantOutputCameFromInvokeFallback = false;
538
587
  const bufferAssistantTextUntilCompletion = true;
@@ -589,6 +638,9 @@ export async function* streamHarnessRun(options) {
589
638
  streamActivityObserved = true;
590
639
  const normalizedChunk = normalizeStreamChunk(rawChunk);
591
640
  if (normalizedChunk.kind === "upstream-event") {
641
+ if (!isFallbackSafeUpstreamStartupEvent(normalizedChunk.event, options.selectedAgentId)) {
642
+ observedOnlyFallbackSafeUpstreamEvents = false;
643
+ }
592
644
  const upstreamPlanState = getPlanStateFromUpstreamEvent({
593
645
  sessionId: options.sessionId,
594
646
  requestId: options.requestId,
@@ -833,6 +885,15 @@ export async function* streamHarnessRun(options) {
833
885
  emitted = emitted || assistantOutput.length > 0;
834
886
  }
835
887
  currentPlanState = await refreshPlanStateFromPersistence(options, currentPlanState);
888
+ if (assistantOutput
889
+ && isStartupRuntimeFailureOutput(assistantOutput)
890
+ && executedToolResults.length === 0
891
+ && !sawSuccessfulToolResult
892
+ && observedOnlyFallbackSafeUpstreamEvents
893
+ && !planStateHasAnyItems(currentPlanState)) {
894
+ assistantOutput = "";
895
+ emitted = false;
896
+ }
836
897
  if (!assistantOutput) {
837
898
  const actual = await options.invokeWithHistory(options.binding, options.input, options.sessionId, options.requestId);
838
899
  if (Array.isArray(actual.contentBlocks) && actual.contentBlocks.length > 0) {
@@ -1019,8 +1080,12 @@ export async function* streamHarnessRun(options) {
1019
1080
  }
1020
1081
  const shouldRetryAfterStreamingCompatibilityError = !assistantOutput &&
1021
1082
  isOpenAICompatibleStreamingCompatibilityError(options.binding, error);
1022
- if ((emitted || streamActivityObserved)
1023
- && !shouldRetryAfterStreamingCompatibilityError) {
1083
+ const streamHadExecutionSideEffects = emitted
1084
+ || nonUpstreamStreamActivityObserved
1085
+ || sawSuccessfulToolResult
1086
+ || executedToolResults.length > 0
1087
+ || !observedOnlyFallbackSafeUpstreamEvents;
1088
+ if (streamHadExecutionSideEffects && !shouldRetryAfterStreamingCompatibilityError) {
1024
1089
  const runtimeFailure = renderRuntimeFailure(error);
1025
1090
  const detailedError = describeRuntimeError(error);
1026
1091
  yield {
@@ -132,6 +132,7 @@ export declare class AgentHarnessRuntime {
132
132
  private emit;
133
133
  private trackBackgroundTask;
134
134
  private scheduleBackgroundStartupTask;
135
+ private drainBackgroundTasksForClose;
135
136
  private resolveToolMcpServerTools;
136
137
  private loadPriorHistory;
137
138
  private loadRequestInput;
@@ -46,6 +46,7 @@ import { defaultRequestedAgentId, prepareRunStart } from "./harness/run/start-ru
46
46
  import { buildRequestInspectionRecord, buildSessionInspectionRecord, deleteSessionRecord, getPublicApproval, listPublicApprovals, } from "./harness/run/session-records.js";
47
47
  import { createKnowledgeModule } from "../knowledge/index.js";
48
48
  import { createProceduralMemoryManager, ProceduralMemoryFormationSync, readProceduralMemoryRuntimeConfig, } from "../knowledge/procedural/index.js";
49
+ const BACKGROUND_TASK_CLOSE_DRAIN_TIMEOUT_MS = 1_000;
49
50
  const ACTIVE_REQUEST_STATES = [
50
51
  "queued",
51
52
  "claimed",
@@ -945,6 +946,23 @@ export class AgentHarnessRuntime {
945
946
  scheduleBackgroundStartupTask(task) {
946
947
  this.trackBackgroundTask(task.then(() => undefined).catch(() => undefined));
947
948
  }
949
+ async drainBackgroundTasksForClose() {
950
+ const tasks = Array.from(this.backgroundTasks);
951
+ if (tasks.length === 0) {
952
+ return;
953
+ }
954
+ let timeoutHandle;
955
+ await Promise.race([
956
+ Promise.allSettled(tasks),
957
+ new Promise((resolve) => {
958
+ timeoutHandle = setTimeout(resolve, BACKGROUND_TASK_CLOSE_DRAIN_TIMEOUT_MS);
959
+ timeoutHandle.unref?.();
960
+ }),
961
+ ]);
962
+ if (timeoutHandle) {
963
+ clearTimeout(timeoutHandle);
964
+ }
965
+ }
948
966
  resolveToolMcpServerTools(agentId) {
949
967
  return resolveWorkspaceAgentTools({
950
968
  workspace: this.workspace,
@@ -1375,7 +1393,7 @@ export class AgentHarnessRuntime {
1375
1393
  this.unregisterMem0IngestionSync();
1376
1394
  this.unregisterRuntimeMemoryFormationSync();
1377
1395
  this.unregisterProceduralMemoryFormationSync();
1378
- await Promise.allSettled(Array.from(this.backgroundTasks));
1396
+ await this.drainBackgroundTasksForClose();
1379
1397
  await this.sessionMemorySync?.close();
1380
1398
  await this.runtimeMemorySync?.close();
1381
1399
  await this.mem0IngestionSync?.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.343",
3
+ "version": "0.0.345",
4
4
  "description": "Workspace runtime for multi-agent applications",
5
5
  "license": "MIT",
6
6
  "type": "module",