@botbotgo/agent-harness 0.0.36 → 0.0.38

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.
@@ -236,6 +236,7 @@ export type RunListeners = {
236
236
  onToolResult?: (item: {
237
237
  toolName: string;
238
238
  output: unknown;
239
+ isError?: boolean;
239
240
  }) => void | Promise<void>;
240
241
  };
241
242
  export type MessageContentPart = {
@@ -289,6 +290,7 @@ export type HarnessStreamItem = {
289
290
  agentId: string;
290
291
  toolName: string;
291
292
  output: unknown;
293
+ isError?: boolean;
292
294
  };
293
295
  export type TranscriptMessage = {
294
296
  role: "user" | "assistant";
@@ -1 +1 @@
1
- export declare const AGENT_HARNESS_VERSION = "0.0.35";
1
+ export declare const AGENT_HARNESS_VERSION = "0.0.37";
@@ -1 +1 @@
1
- export const AGENT_HARNESS_VERSION = "0.0.35";
1
+ export const AGENT_HARNESS_VERSION = "0.0.37";
@@ -32,6 +32,16 @@ function asObject(value) {
32
32
  function resolveTimeoutMs(value) {
33
33
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
34
34
  }
35
+ function computeRemainingTimeoutMs(deadlineAt, fallbackTimeoutMs) {
36
+ if (!deadlineAt) {
37
+ return fallbackTimeoutMs;
38
+ }
39
+ const remaining = deadlineAt - Date.now();
40
+ if (remaining <= 0) {
41
+ return 0;
42
+ }
43
+ return fallbackTimeoutMs ? Math.min(fallbackTimeoutMs, remaining) : remaining;
44
+ }
35
45
  function isPlaceholderApiKey(value) {
36
46
  return typeof value === "string" && value.trim().toLowerCase() === "dummy";
37
47
  }
@@ -209,11 +219,27 @@ export class AgentRuntimeAdapter {
209
219
  });
210
220
  });
211
221
  }
212
- async *iterateWithTimeout(iterable, timeoutMs, operation) {
222
+ async *iterateWithTimeout(iterable, timeoutMs, operation, deadlineAt, deadlineTimeoutMs) {
213
223
  const iterator = iterable[Symbol.asyncIterator]();
214
224
  try {
215
225
  for (;;) {
216
- const next = await this.withTimeout(() => iterator.next(), timeoutMs, operation, "stream");
226
+ const effectiveTimeoutMs = computeRemainingTimeoutMs(deadlineAt, timeoutMs);
227
+ if (effectiveTimeoutMs !== undefined && effectiveTimeoutMs <= 0) {
228
+ throw new RuntimeOperationTimeoutError(operation, deadlineTimeoutMs ?? timeoutMs ?? 0, "invoke");
229
+ }
230
+ let next;
231
+ try {
232
+ next = await this.withTimeout(() => iterator.next(), effectiveTimeoutMs, operation, "stream");
233
+ }
234
+ catch (error) {
235
+ if (error instanceof RuntimeOperationTimeoutError &&
236
+ deadlineAt &&
237
+ deadlineTimeoutMs &&
238
+ effectiveTimeoutMs !== timeoutMs) {
239
+ throw new RuntimeOperationTimeoutError(operation, deadlineTimeoutMs, "invoke");
240
+ }
241
+ throw error;
242
+ }
217
243
  if (next.done) {
218
244
  return;
219
245
  }
@@ -591,6 +617,7 @@ export class AgentRuntimeAdapter {
591
617
  try {
592
618
  const invokeTimeoutMs = this.resolveBindingTimeout(binding);
593
619
  const streamIdleTimeoutMs = this.resolveStreamIdleTimeout(binding);
620
+ const streamDeadlineAt = invokeTimeoutMs ? Date.now() + invokeTimeoutMs : undefined;
594
621
  if (binding.langchainAgentParams) {
595
622
  const langchainParams = binding.langchainAgentParams;
596
623
  const resolvedModel = (await this.resolveModel(binding.langchainAgentParams.model));
@@ -605,8 +632,8 @@ export class AgentRuntimeAdapter {
605
632
  // agent loop and only adds an extra model round-trip before the runnable path.
606
633
  if (canUseDirectModelStream && typeof model.stream === "function") {
607
634
  let emitted = false;
608
- const stream = await this.withTimeout(() => model.stream(this.buildRawModelMessages(langchainParams.systemPrompt, history, input)), invokeTimeoutMs, "model stream start", "stream");
609
- for await (const chunk of this.iterateWithTimeout(stream, streamIdleTimeoutMs, "model stream")) {
635
+ const stream = await this.withTimeout(() => model.stream(this.buildRawModelMessages(langchainParams.systemPrompt, history, input)), computeRemainingTimeoutMs(streamDeadlineAt, invokeTimeoutMs), "model stream start", "stream");
636
+ for await (const chunk of this.iterateWithTimeout(stream, streamIdleTimeoutMs, "model stream", streamDeadlineAt, invokeTimeoutMs)) {
610
637
  const delta = readStreamDelta(chunk);
611
638
  if (delta) {
612
639
  emitted = true;
@@ -626,12 +653,13 @@ export class AgentRuntimeAdapter {
626
653
  const runnable = await this.create(binding);
627
654
  const request = { messages: this.buildAgentMessages(history, input) };
628
655
  if (typeof runnable.streamEvents === "function") {
629
- const events = await this.withTimeout(() => runnable.streamEvents(request, { configurable: { thread_id: threadId }, version: "v2" }), invokeTimeoutMs, "agent streamEvents start", "stream");
656
+ const events = await this.withTimeout(() => runnable.streamEvents(request, { configurable: { thread_id: threadId }, version: "v2" }), computeRemainingTimeoutMs(streamDeadlineAt, invokeTimeoutMs), "agent streamEvents start", "stream");
630
657
  const allowVisibleStreamDeltas = Boolean(binding.langchainAgentParams);
631
658
  let emittedOutput = "";
659
+ let emittedToolError = false;
632
660
  const seenTerminalOutputs = new Set();
633
661
  let lastStep = "";
634
- for await (const event of this.iterateWithTimeout(events, streamIdleTimeoutMs, "agent streamEvents")) {
662
+ for await (const event of this.iterateWithTimeout(events, streamIdleTimeoutMs, "agent streamEvents", streamDeadlineAt, invokeTimeoutMs)) {
635
663
  const interruptPayload = extractInterruptPayload(event);
636
664
  if (interruptPayload) {
637
665
  yield { kind: "interrupt", content: interruptPayload };
@@ -668,7 +696,8 @@ export class AgentRuntimeAdapter {
668
696
  }
669
697
  const toolResult = extractToolResult(event);
670
698
  if (toolResult) {
671
- yield { kind: "tool-result", toolName: toolResult.toolName, output: toolResult.output };
699
+ emittedToolError = emittedToolError || toolResult.isError === true;
700
+ yield { kind: "tool-result", toolName: toolResult.toolName, output: toolResult.output, isError: toolResult.isError };
672
701
  }
673
702
  const output = extractTerminalStreamOutput(event);
674
703
  if (output) {
@@ -686,14 +715,14 @@ export class AgentRuntimeAdapter {
686
715
  }
687
716
  }
688
717
  }
689
- if (emittedOutput) {
718
+ if (emittedOutput || emittedToolError) {
690
719
  return;
691
720
  }
692
721
  }
693
722
  if (binding.langchainAgentParams && typeof runnable.stream === "function") {
694
- const stream = await this.withTimeout(() => runnable.stream(request, { configurable: { thread_id: threadId } }), invokeTimeoutMs, "agent stream start", "stream");
723
+ const stream = await this.withTimeout(() => runnable.stream(request, { configurable: { thread_id: threadId } }), computeRemainingTimeoutMs(streamDeadlineAt, invokeTimeoutMs), "agent stream start", "stream");
695
724
  let emitted = false;
696
- for await (const chunk of this.iterateWithTimeout(stream, streamIdleTimeoutMs, "agent stream")) {
725
+ for await (const chunk of this.iterateWithTimeout(stream, streamIdleTimeoutMs, "agent stream", streamDeadlineAt, invokeTimeoutMs)) {
697
726
  const delta = readStreamDelta(chunk);
698
727
  if (delta) {
699
728
  emitted = true;
@@ -6,7 +6,7 @@ import { createResourceBackendResolver, createResourceToolResolver } from "../re
6
6
  import { EventBus } from "./event-bus.js";
7
7
  import { PolicyEngine } from "./policy-engine.js";
8
8
  import { getRoutingSystemPrompt } from "../workspace/support/workspace-ref-utils.js";
9
- import { createHarnessEvent, createPendingApproval, heuristicRoute, inferRoutingBindings, resolveDeterministicRouteIntent, renderRuntimeFailure, requiresResearchRoute, } from "./support/harness-support.js";
9
+ import { createHarnessEvent, createPendingApproval, heuristicRoute, inferRoutingBindings, resolveDeterministicRouteIntent, renderRuntimeFailure, renderToolFailure, requiresResearchRoute, } from "./support/harness-support.js";
10
10
  import { createCheckpointerForConfig, createStoreForConfig } from "./support/runtime-factories.js";
11
11
  import { resolveCompiledEmbeddingModel, resolveCompiledEmbeddingModelRef } from "./support/embedding-models.js";
12
12
  import { resolveCompiledVectorStore, resolveCompiledVectorStoreRef } from "./support/vector-stores.js";
@@ -429,6 +429,7 @@ export class AgentHarness {
429
429
  await this.notifyListener(listeners.onToolResult, {
430
430
  toolName: item.toolName,
431
431
  output: item.output,
432
+ isError: item.isError,
432
433
  });
433
434
  }
434
435
  }
@@ -543,6 +544,7 @@ export class AgentHarness {
543
544
  try {
544
545
  const priorHistory = await this.loadPriorHistory(threadId, runId);
545
546
  let assistantOutput = "";
547
+ const toolErrors = [];
546
548
  for await (const chunk of this.runtimeAdapter.stream(binding, options.input, threadId, priorHistory)) {
547
549
  if (chunk) {
548
550
  const normalizedChunk = typeof chunk === "string"
@@ -591,6 +593,9 @@ export class AgentHarness {
591
593
  continue;
592
594
  }
593
595
  if (normalizedChunk.kind === "tool-result") {
596
+ if (normalizedChunk.isError) {
597
+ toolErrors.push(renderToolFailure(normalizedChunk.toolName, normalizedChunk.output));
598
+ }
594
599
  yield {
595
600
  type: "tool-result",
596
601
  threadId,
@@ -598,6 +603,7 @@ export class AgentHarness {
598
603
  agentId: selectedAgentId,
599
604
  toolName: normalizedChunk.toolName,
600
605
  output: normalizedChunk.output,
606
+ isError: normalizedChunk.isError,
601
607
  };
602
608
  continue;
603
609
  }
@@ -606,6 +612,11 @@ export class AgentHarness {
606
612
  yield await this.emitOutputDeltaAndCreateItem(threadId, runId, selectedAgentId, normalizedChunk.content);
607
613
  }
608
614
  }
615
+ if (!assistantOutput && toolErrors.length > 0) {
616
+ assistantOutput = toolErrors.join("\n\n");
617
+ emitted = true;
618
+ yield await this.emitOutputDeltaAndCreateItem(threadId, runId, selectedAgentId, assistantOutput);
619
+ }
609
620
  if (!assistantOutput) {
610
621
  const actual = await this.invokeWithHistory(binding, options.input, threadId, runId);
611
622
  if (actual.output) {
@@ -14,6 +14,7 @@ export type RuntimeStreamChunk = {
14
14
  kind: "tool-result";
15
15
  toolName: string;
16
16
  output: unknown;
17
+ isError?: boolean;
17
18
  };
18
19
  export declare function extractTerminalStreamOutput(event: unknown): string;
19
20
  export declare function extractReasoningStreamOutput(event: unknown): string;
@@ -23,6 +24,7 @@ export declare function extractAgentStep(event: unknown): string | null;
23
24
  export declare function extractToolResult(event: unknown): {
24
25
  toolName: string;
25
26
  output: unknown;
27
+ isError?: boolean;
26
28
  } | null;
27
29
  export declare function extractInterruptPayload(event: unknown): string | null;
28
30
  export declare function normalizeTerminalOutputKey(value: string): string;
@@ -50,6 +50,31 @@ function summarizeStepValue(value) {
50
50
  function humanizeEventName(name) {
51
51
  return name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[._-]+/g, " ").replace(/\s+/g, " ").trim().toLowerCase();
52
52
  }
53
+ function readToolErrorText(value) {
54
+ if (typeof value === "string") {
55
+ return value;
56
+ }
57
+ if (value instanceof Error) {
58
+ return value.message;
59
+ }
60
+ if (typeof value !== "object" || !value) {
61
+ return "";
62
+ }
63
+ const record = value;
64
+ for (const candidate of [record.error, record.message, record.detail, record.details]) {
65
+ if (typeof candidate === "string" && candidate.trim()) {
66
+ return candidate;
67
+ }
68
+ }
69
+ return "";
70
+ }
71
+ function isErrorLikeToolOutput(value) {
72
+ const message = readToolErrorText(value).trim();
73
+ if (!message) {
74
+ return false;
75
+ }
76
+ return /(^|\b)(error|exception|failed|failure|denied|timed out|timeout|not permitted|eperm|eacces)(\b|:)/i.test(message);
77
+ }
53
78
  export function extractTerminalStreamOutput(event) {
54
79
  if (typeof event !== "object" || !event)
55
80
  return "";
@@ -109,6 +134,10 @@ export function extractAgentStep(event) {
109
134
  const outputSummary = formatStepValue(typed.data?.output, 80);
110
135
  return outputSummary ? `tool ${name || "tool"} done ${outputSummary}` : `tool ${name || "tool"} done`;
111
136
  }
137
+ if (typed.event === "on_tool_error" || (typed.event === "on_chain_error" && typed.run_type === "tool")) {
138
+ const errorSummary = formatStepValue(typed.data?.error ?? typed.data?.output, 80);
139
+ return errorSummary ? `tool ${name || "tool"} error ${errorSummary}` : `tool ${name || "tool"} error`;
140
+ }
112
141
  if (typed.event === "on_chat_model_start") {
113
142
  const inputTools = typed.data?.input?.tools;
114
143
  const tools = typed.invocation_params?.tools ?? inputTools ?? [];
@@ -148,14 +177,17 @@ export function extractToolResult(event) {
148
177
  return null;
149
178
  const typed = event;
150
179
  const isToolEnd = typed.event === "on_tool_end" || (typed.event === "on_chain_end" && typed.run_type === "tool");
180
+ const isToolError = typed.event === "on_tool_error" || (typed.event === "on_chain_error" && typed.run_type === "tool");
151
181
  const toolName = typeof typed.name === "string" ? typed.name : "";
152
- if (!isToolEnd || !toolName) {
182
+ if ((!isToolEnd && !isToolError) || !toolName) {
153
183
  return null;
154
184
  }
155
- const rawOutput = typed.data?.output;
185
+ const rawOutput = isToolError ? typed.data?.error ?? typed.data?.output : typed.data?.output;
186
+ const normalizedOutput = typeof rawOutput === "string" ? parseMaybeJson(rawOutput) : rawOutput;
156
187
  return {
157
188
  toolName,
158
- output: typeof rawOutput === "string" ? parseMaybeJson(rawOutput) : rawOutput,
189
+ output: normalizedOutput,
190
+ isError: isToolError || isErrorLikeToolOutput(normalizedOutput),
159
191
  };
160
192
  }
161
193
  export function extractInterruptPayload(event) {
@@ -1,5 +1,6 @@
1
1
  import type { ApprovalRecord, HarnessEvent, WorkspaceBundle } from "../../contracts/types.js";
2
2
  export declare function renderRuntimeFailure(error: unknown): string;
3
+ export declare function renderToolFailure(toolName: string, output: unknown): string;
3
4
  export declare function parseInterruptContent(content: string): {
4
5
  toolName?: string;
5
6
  toolId?: string;
@@ -3,6 +3,33 @@ export function renderRuntimeFailure(error) {
3
3
  const message = error instanceof Error ? error.message : String(error);
4
4
  return `runtime_error=${message}`.trim();
5
5
  }
6
+ function readToolErrorMessage(output) {
7
+ if (typeof output === "string" && output.trim()) {
8
+ return output.trim();
9
+ }
10
+ if (output instanceof Error) {
11
+ return output.message;
12
+ }
13
+ if (typeof output !== "object" || !output) {
14
+ return "";
15
+ }
16
+ const record = output;
17
+ for (const candidate of [record.error, record.message, record.detail, record.details]) {
18
+ if (typeof candidate === "string" && candidate.trim()) {
19
+ return candidate.trim();
20
+ }
21
+ }
22
+ try {
23
+ return JSON.stringify(output);
24
+ }
25
+ catch {
26
+ return String(output);
27
+ }
28
+ }
29
+ export function renderToolFailure(toolName, output) {
30
+ const detail = readToolErrorMessage(output);
31
+ return detail ? `Tool ${toolName} failed: ${detail}` : `Tool ${toolName} failed.`;
32
+ }
6
33
  export function parseInterruptContent(content) {
7
34
  try {
8
35
  const raw = JSON.parse(content);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.36",
3
+ "version": "0.0.38",
4
4
  "description": "Agent Harness framework package",
5
5
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",