@botbotgo/agent-harness 0.0.49 → 0.0.50

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.
package/README.md CHANGED
@@ -81,10 +81,12 @@ try {
81
81
  - YAML-defined host routing and runtime policy
82
82
  - LangChain v1 and DeepAgents backend adaptation
83
83
  - Auto-discovered local tools and SKILL packages
84
+ - provider-native tools, MCP tools, and workspace-local tool modules
85
+ - persisted threads, runs, approvals, lifecycle events, and queued runs
86
+ - runtime-managed recovery and checkpoint maintenance
87
+ - structured output and multimodal content preservation in run results
84
88
  - MCP bridge support for agent-declared MCP servers
85
89
  - MCP server support for exposing harness tools outward
86
- - Persisted threads, runs, approvals, lifecycle events, and queued runs
87
- - Runtime-managed recovery and checkpoint maintenance
88
90
 
89
91
  ## How To Use
90
92
 
@@ -120,7 +122,7 @@ const result = await run(runtime, {
120
122
  });
121
123
  ```
122
124
 
123
- Each run creates or continues a persisted thread and returns `threadId`, `runId`, `state`, and the final user-facing output text.
125
+ `run(runtime, { ... })` creates or continues a persisted thread and returns `threadId`, `runId`, `state`, and a simple text `output`. When upstream returns richer output, the runtime also preserves `outputContent`, `contentBlocks`, and `structuredResponse` without making the basic API larger.
124
126
 
125
127
  Use `invocation` as the runtime-facing request envelope:
126
128
 
@@ -149,6 +151,9 @@ const result = await run(runtime, {
149
151
  onChunk(chunk) {
150
152
  process.stdout.write(chunk);
151
153
  },
154
+ onContentBlocks(blocks) {
155
+ console.log(blocks);
156
+ },
152
157
  onEvent(event) {
153
158
  console.log(event.eventType, event.payload);
154
159
  },
@@ -244,8 +249,6 @@ Core workspace files:
244
249
  - `resources/tools/`
245
250
  - `resources/skills/`
246
251
 
247
- ### Client-Configurable YAML Reference
248
-
249
252
  There are three configuration layers:
250
253
 
251
254
  - runtime policy in `config/workspace.yaml`
@@ -406,6 +409,8 @@ Client-configurable agent fields include:
406
409
  - `spec.responseFormat`
407
410
  - `spec.contextSchema`
408
411
 
412
+ For backend-specific agent options, prefer passing the upstream concept directly in YAML. The loader keeps a small stable product shape, but it also preserves agent-level passthrough fields so new LangChain v1 or DeepAgents parameters can flow into adapters without expanding the public API surface.
413
+
409
414
  ### `resources/`
410
415
 
411
416
  Use `resources/` for executable local extensions:
@@ -426,6 +431,7 @@ SKILL packages are discovered from `resources/skills/` and attached to agents th
426
431
  - upstream LangChain v1 and DeepAgents concepts should be expressed as directly as possible in YAML
427
432
  - recovery, approvals, threads, runs, and events are runtime concepts, not backend-specific escape hatches
428
433
  - backend implementation details should stay internal unless product requirements force exposure
434
+ - application-level orchestration and lifecycle management stays in the harness
429
435
 
430
436
  In short: `agent-harness` is a public runtime contract generic enough to survive backend changes, while the deep execution semantics stay upstream.
431
437
 
@@ -96,6 +96,7 @@ export type LangChainAgentParams = {
96
96
  responseFormat?: unknown;
97
97
  contextSchema?: unknown;
98
98
  middleware?: Array<Record<string, unknown>>;
99
+ passthrough?: Record<string, unknown>;
99
100
  subagents?: CompiledSubAgent[];
100
101
  memory?: string[];
101
102
  skills?: string[];
@@ -118,6 +119,7 @@ export type CompiledSubAgent = {
118
119
  responseFormat?: unknown;
119
120
  contextSchema?: unknown;
120
121
  middleware?: Array<Record<string, unknown>>;
122
+ passthrough?: Record<string, unknown>;
121
123
  };
122
124
  export type DeepAgentParams = {
123
125
  model: CompiledModel;
@@ -126,6 +128,7 @@ export type DeepAgentParams = {
126
128
  responseFormat?: unknown;
127
129
  contextSchema?: unknown;
128
130
  middleware?: Array<Record<string, unknown>>;
131
+ passthrough?: Record<string, unknown>;
129
132
  description: string;
130
133
  subagents: CompiledSubAgent[];
131
134
  interruptOn?: Record<string, boolean | object>;
@@ -257,6 +260,9 @@ export type RunResult = {
257
260
  state: RunState;
258
261
  output: string;
259
262
  finalMessageText?: string;
263
+ outputContent?: unknown;
264
+ contentBlocks?: unknown[];
265
+ structuredResponse?: unknown;
260
266
  interruptContent?: string;
261
267
  agentId?: string;
262
268
  approvalId?: string;
@@ -267,6 +273,7 @@ export type RunResult = {
267
273
  };
268
274
  export type RunListeners = {
269
275
  onChunk?: (chunk: string) => void | Promise<void>;
276
+ onContentBlocks?: (blocks: unknown[]) => void | Promise<void>;
270
277
  onEvent?: (event: HarnessEvent) => void | Promise<void>;
271
278
  onReasoning?: (chunk: string) => void | Promise<void>;
272
279
  onStep?: (step: string) => void | Promise<void>;
@@ -318,6 +325,12 @@ export type HarnessStreamItem = {
318
325
  runId: string;
319
326
  agentId: string;
320
327
  content: string;
328
+ } | {
329
+ type: "content-blocks";
330
+ threadId: string;
331
+ runId: string;
332
+ agentId: string;
333
+ contentBlocks: unknown[];
321
334
  } | {
322
335
  type: "reasoning";
323
336
  threadId: string;
@@ -1 +1 @@
1
- export declare const AGENT_HARNESS_VERSION = "0.0.48";
1
+ export declare const AGENT_HARNESS_VERSION = "0.0.49";
@@ -1 +1 @@
1
- export const AGENT_HARNESS_VERSION = "0.0.48";
1
+ export const AGENT_HARNESS_VERSION = "0.0.49";
@@ -1,18 +1,4 @@
1
1
  export declare function escapeHtml(value: string): string;
2
- /** CSS class for anchors that should open in the host app embedded browser (Wallee). */
3
- export declare const WALLEE_OUTPUT_LINK_CLASS = "wallee-output-link";
4
- /** `data-wallee-url` — target URL for the embedded browser (http/https only). */
5
- export declare const WALLEE_BROWSER_URL_ATTR = "data-wallee-url";
6
- export declare function isAllowedWalleeBrowserUrl(url: string): boolean;
7
- /**
8
- * Escape plain text and wrap http(s) URLs in Wallee output anchors (open in host embedded browser).
9
- */
10
- export declare function linkifyPlainTextForWalleeBrowser(text: string): string;
11
- /**
12
- * Like {@link markdownToHtml} but inline http(s) URLs and markdown links `[label](https://…)` render as
13
- * Wallee embedded-browser anchors (`wallee-output-link` + `data-wallee-url`).
14
- */
15
- export declare function markdownToWalleeOutputHtml(markdown: string): string;
16
2
  export declare function markdownToHtml(markdown: string): string;
17
3
  export declare function markdownToConsole(markdown: string): string;
18
4
  export declare function renderTemplate(data: Record<string, unknown>, template: string): string;
@@ -8,152 +8,6 @@ export function escapeHtml(value) {
8
8
  .replaceAll('"', "&quot;")
9
9
  .replaceAll("'", "&#39;");
10
10
  }
11
- /** CSS class for anchors that should open in the host app embedded browser (Wallee). */
12
- export const WALLEE_OUTPUT_LINK_CLASS = "wallee-output-link";
13
- /** `data-wallee-url` — target URL for the embedded browser (http/https only). */
14
- export const WALLEE_BROWSER_URL_ATTR = "data-wallee-url";
15
- export function isAllowedWalleeBrowserUrl(url) {
16
- try {
17
- const u = new URL(url);
18
- return u.protocol === "http:" || u.protocol === "https:";
19
- }
20
- catch {
21
- return false;
22
- }
23
- }
24
- function walleeOutputAnchor(url, labelEscaped) {
25
- return `<a class="${WALLEE_OUTPUT_LINK_CLASS}" ${WALLEE_BROWSER_URL_ATTR}="${escapeHtml(url)}" href="#">${labelEscaped}</a>`;
26
- }
27
- /**
28
- * Escape plain text and wrap http(s) URLs in Wallee output anchors (open in host embedded browser).
29
- */
30
- export function linkifyPlainTextForWalleeBrowser(text) {
31
- const urlRe = /\bhttps?:\/\/[^\s<>"']+/g;
32
- const parts = [];
33
- let last = 0;
34
- let m;
35
- while ((m = urlRe.exec(text)) !== null) {
36
- parts.push(escapeHtml(text.slice(last, m.index)));
37
- const raw = m[0];
38
- const trimmed = raw.replace(/[.,;:!?)\]]+$/u, "");
39
- const rest = raw.slice(trimmed.length);
40
- if (isAllowedWalleeBrowserUrl(trimmed)) {
41
- parts.push(walleeOutputAnchor(trimmed, escapeHtml(trimmed)));
42
- if (rest) {
43
- parts.push(escapeHtml(rest));
44
- }
45
- }
46
- else {
47
- parts.push(escapeHtml(raw));
48
- }
49
- last = m.index + raw.length;
50
- }
51
- parts.push(escapeHtml(text.slice(last)));
52
- return parts.join("");
53
- }
54
- function applyBasicInlineMarkdown(escaped) {
55
- return escaped
56
- .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
57
- .replace(/\*(.+?)\*/g, "<em>$1</em>")
58
- .replace(/`([^`]+)`/g, "<code>$1</code>");
59
- }
60
- function linkifyPlainTextSegmentWithWalleeMarkdown(text) {
61
- const mdLinkPattern = /\[([^\]]*)\]\((https?:\/\/[^)\s]+)\)/g;
62
- const segments = [];
63
- let lastIndex = 0;
64
- let m;
65
- while ((m = mdLinkPattern.exec(text)) !== null) {
66
- if (m.index > lastIndex) {
67
- segments.push({ type: "text", text: text.slice(lastIndex, m.index) });
68
- }
69
- segments.push({ type: "mdlink", label: m[1], url: m[2] });
70
- lastIndex = m.index + m[0].length;
71
- }
72
- if (lastIndex < text.length) {
73
- segments.push({ type: "text", text: text.slice(lastIndex) });
74
- }
75
- if (segments.length === 0) {
76
- segments.push({ type: "text", text });
77
- }
78
- return segments
79
- .map((seg) => {
80
- if (seg.type === "mdlink") {
81
- if (!isAllowedWalleeBrowserUrl(seg.url)) {
82
- return `${escapeHtml(seg.label)} (${escapeHtml(seg.url)})`;
83
- }
84
- return walleeOutputAnchor(seg.url, escapeHtml(seg.label));
85
- }
86
- return linkifyBareUrlsWithInlineMarkdown(seg.text);
87
- })
88
- .join("");
89
- }
90
- function linkifyBareUrlsWithInlineMarkdown(text) {
91
- const urlRe = /\bhttps?:\/\/[^\s<>"']+/g;
92
- const parts = [];
93
- let last = 0;
94
- let m;
95
- while ((m = urlRe.exec(text)) !== null) {
96
- parts.push(applyBasicInlineMarkdown(escapeHtml(text.slice(last, m.index))));
97
- const raw = m[0];
98
- const trimmed = raw.replace(/[.,;:!?)\]]+$/u, "");
99
- const rest = raw.slice(trimmed.length);
100
- if (isAllowedWalleeBrowserUrl(trimmed)) {
101
- parts.push(walleeOutputAnchor(trimmed, escapeHtml(trimmed)));
102
- if (rest) {
103
- parts.push(applyBasicInlineMarkdown(escapeHtml(rest)));
104
- }
105
- }
106
- else {
107
- parts.push(applyBasicInlineMarkdown(escapeHtml(raw)));
108
- }
109
- last = m.index + raw.length;
110
- }
111
- parts.push(applyBasicInlineMarkdown(escapeHtml(text.slice(last))));
112
- return parts.join("");
113
- }
114
- /**
115
- * Like {@link markdownToHtml} but inline http(s) URLs and markdown links `[label](https://…)` render as
116
- * Wallee embedded-browser anchors (`wallee-output-link` + `data-wallee-url`).
117
- */
118
- export function markdownToWalleeOutputHtml(markdown) {
119
- const normalized = markdown.replace(/\r\n/g, "\n");
120
- const blocks = normalized.split(/\n\n+/);
121
- const html = [];
122
- for (const block of blocks) {
123
- const trimmed = block.trim();
124
- if (!trimmed) {
125
- continue;
126
- }
127
- if (trimmed.startsWith("```") && trimmed.endsWith("```")) {
128
- const lines = trimmed.split("\n");
129
- const language = lines[0]?.slice(3).trim();
130
- const code = lines.slice(1, -1).join("\n");
131
- html.push(`<pre class="ah-code"><code${language ? ` data-language="${escapeHtml(language)}"` : ""}>${escapeHtml(code)}</code></pre>`);
132
- continue;
133
- }
134
- if (/^#{1,6}\s/.test(trimmed)) {
135
- const match = trimmed.match(/^(#{1,6})\s+(.*)$/);
136
- const level = match?.[1].length ?? 1;
137
- const content = linkifyPlainTextSegmentWithWalleeMarkdown(match?.[2] ?? trimmed);
138
- html.push(`<h${level}>${content}</h${level}>`);
139
- continue;
140
- }
141
- if (trimmed.split("\n").every((line) => /^[-*]\s+/.test(line))) {
142
- const items = trimmed
143
- .split("\n")
144
- .map((line) => line.replace(/^[-*]\s+/, ""))
145
- .map((line) => `<li>${linkifyPlainTextSegmentWithWalleeMarkdown(line)}</li>`)
146
- .join("");
147
- html.push(`<ul>${items}</ul>`);
148
- continue;
149
- }
150
- html.push(`<p>${trimmed
151
- .split("\n")
152
- .map((line) => linkifyPlainTextSegmentWithWalleeMarkdown(line))
153
- .join("<br />")}</p>`);
154
- }
155
- return html.join("");
156
- }
157
11
  function renderInlineMarkdown(text) {
158
12
  const escaped = escapeHtml(text);
159
13
  return escaped
@@ -9,7 +9,7 @@ import { ChatOpenAI } from "@langchain/openai";
9
9
  import { tools as openAIProviderTools } from "@langchain/openai";
10
10
  import { createAgent, humanInTheLoopMiddleware, initChatModel } from "langchain";
11
11
  import { z } from "zod";
12
- import { extractEmptyAssistantMessageFailure, extractReasoningText, extractToolFallbackContext, extractVisibleOutput, isLikelyToolArgsObject, isToolCallParseFailure, STRICT_TOOL_JSON_INSTRUCTION, sanitizeVisibleText, tryParseJson, wrapResolvedModel, } from "./parsing/output-parsing.js";
12
+ import { extractEmptyAssistantMessageFailure, extractContentBlocks, extractOutputContent, extractReasoningText, extractToolFallbackContext, extractVisibleOutput, isLikelyToolArgsObject, isToolCallParseFailure, STRICT_TOOL_JSON_INSTRUCTION, sanitizeVisibleText, tryParseJson, wrapResolvedModel, } from "./parsing/output-parsing.js";
13
13
  import { computeIncrementalOutput, extractAgentStep, extractInterruptPayload, extractReasoningStreamOutput, extractStateStreamOutput, extractVisibleStreamOutput, extractTerminalStreamOutput, extractToolResult, normalizeTerminalOutputKey, readStreamDelta, } from "./parsing/stream-event-parsing.js";
14
14
  import { wrapToolForExecution } from "./tool-hitl.js";
15
15
  import { resolveDeclaredMiddleware } from "./declared-middleware.js";
@@ -556,6 +556,7 @@ export class AgentRuntimeAdapter {
556
556
  async resolveSubagents(subagents, binding) {
557
557
  return Promise.all(subagents.map(async (subagent) => ({
558
558
  ...subagent,
559
+ ...(subagent.passthrough ?? {}),
559
560
  model: subagent.model ? (await this.resolveModel(subagent.model)) : undefined,
560
561
  tools: subagent.tools ? this.resolveTools(subagent.tools) : undefined,
561
562
  interruptOn: this.compileInterruptOn(subagent.tools ?? [], subagent.interruptOn),
@@ -578,6 +579,7 @@ export class AgentRuntimeAdapter {
578
579
  throw new Error(`Agent ${binding.agent.id} configures ${tools.length} tool(s), but resolved model ${params.model.id} does not support tool binding.`);
579
580
  }
580
581
  return createAgent({
582
+ ...(params.passthrough ?? {}),
581
583
  model: model,
582
584
  tools: tools,
583
585
  systemPrompt: params.systemPrompt,
@@ -598,6 +600,7 @@ export class AgentRuntimeAdapter {
598
600
  throw new Error(`Agent ${binding.agent.id} has no runnable params`);
599
601
  }
600
602
  const deepAgentConfig = {
603
+ ...(params.passthrough ?? {}),
601
604
  model: (await this.resolveModel(params.model)),
602
605
  tools: this.resolveTools(params.tools, binding),
603
606
  systemPrompt: params.systemPrompt,
@@ -672,6 +675,9 @@ export class AgentRuntimeAdapter {
672
675
  }
673
676
  const output = visibleOutput || synthesizedOutput || toolFallback || JSON.stringify(result, null, 2);
674
677
  const finalMessageText = sanitizeVisibleText(output);
678
+ const outputContent = extractOutputContent(result);
679
+ const contentBlocks = extractContentBlocks(result);
680
+ const structuredResponse = result.structuredResponse;
675
681
  return {
676
682
  threadId,
677
683
  runId,
@@ -680,8 +686,13 @@ export class AgentRuntimeAdapter {
680
686
  interruptContent,
681
687
  output: finalMessageText,
682
688
  finalMessageText,
689
+ ...(outputContent !== undefined ? { outputContent } : {}),
690
+ ...(contentBlocks.length > 0 ? { contentBlocks } : {}),
691
+ ...(structuredResponse !== undefined ? { structuredResponse } : {}),
683
692
  metadata: {
684
- ...(result.structuredResponse !== undefined ? { structuredResponse: result.structuredResponse } : {}),
693
+ ...(structuredResponse !== undefined ? { structuredResponse } : {}),
694
+ ...(outputContent !== undefined ? { outputContent } : {}),
695
+ ...(contentBlocks.length > 0 ? { contentBlocks } : {}),
685
696
  ...(asRecord(result.files) ? { files: asRecord(result.files) } : {}),
686
697
  ...(this.buildStateSnapshot(result) ? { stateSnapshot: this.buildStateSnapshot(result) } : {}),
687
698
  upstreamResult: result,
@@ -1,4 +1,4 @@
1
- import { anthropicPromptCachingMiddleware, contextEditingMiddleware, llmToolSelectorMiddleware, modelCallLimitMiddleware, modelFallbackMiddleware, modelRetryMiddleware, openAIModerationMiddleware, piiMiddleware, piiRedactionMiddleware, summarizationMiddleware, todoListMiddleware, toolCallLimitMiddleware, toolEmulatorMiddleware, toolRetryMiddleware, } from "langchain";
1
+ import { anthropicPromptCachingMiddleware, contextEditingMiddleware, humanInTheLoopMiddleware, llmToolSelectorMiddleware, modelCallLimitMiddleware, modelFallbackMiddleware, modelRetryMiddleware, openAIModerationMiddleware, piiMiddleware, summarizationMiddleware, todoListMiddleware, toolCallLimitMiddleware, toolEmulatorMiddleware, toolRetryMiddleware, } from "langchain";
2
2
  function asMiddlewareConfig(value) {
3
3
  return typeof value === "object" && value !== null && !Array.isArray(value) ? { ...value } : null;
4
4
  }
@@ -77,6 +77,9 @@ export async function resolveDeclaredMiddleware(middlewareConfigs, options) {
77
77
  case "toolEmulator":
78
78
  resolved.push(toolEmulatorMiddleware(runtimeConfig));
79
79
  break;
80
+ case "humanInTheLoop":
81
+ resolved.push(humanInTheLoopMiddleware(runtimeConfig));
82
+ break;
80
83
  case "openAIModeration":
81
84
  resolved.push(openAIModerationMiddleware(runtimeConfig));
82
85
  break;
@@ -89,9 +92,6 @@ export async function resolveDeclaredMiddleware(middlewareConfigs, options) {
89
92
  resolved.push(piiMiddleware(piiType, piiOptions));
90
93
  break;
91
94
  }
92
- case "piiRedaction":
93
- resolved.push(piiRedactionMiddleware(runtimeConfig));
94
- break;
95
95
  case "anthropicPromptCaching":
96
96
  resolved.push(anthropicPromptCachingMiddleware(runtimeConfig));
97
97
  break;
@@ -71,6 +71,7 @@ export declare class AgentHarnessRuntime {
71
71
  private checkpointRefForState;
72
72
  private finalizeContinuedRun;
73
73
  private emitOutputDeltaAndCreateItem;
74
+ private createContentBlocksItem;
74
75
  private emitRunCreated;
75
76
  private setRunStateAndEmit;
76
77
  private requestApprovalAndEmit;
@@ -525,6 +525,15 @@ export class AgentHarnessRuntime {
525
525
  content,
526
526
  };
527
527
  }
528
+ createContentBlocksItem(threadId, runId, agentId, contentBlocks) {
529
+ return {
530
+ type: "content-blocks",
531
+ threadId,
532
+ runId,
533
+ agentId,
534
+ contentBlocks,
535
+ };
536
+ }
528
537
  async emitRunCreated(threadId, runId, payload) {
529
538
  return this.emit(threadId, runId, 1, "run.created", payload);
530
539
  }
@@ -678,6 +687,10 @@ export class AgentHarnessRuntime {
678
687
  await this.notifyListener(listeners.onChunk, item.content);
679
688
  continue;
680
689
  }
690
+ if (item.type === "content-blocks") {
691
+ await this.notifyListener(listeners.onContentBlocks, item.contentBlocks);
692
+ continue;
693
+ }
681
694
  if (item.type === "reasoning") {
682
695
  await this.notifyListener(listeners.onReasoning, item.content);
683
696
  continue;
@@ -890,6 +903,9 @@ export class AgentHarnessRuntime {
890
903
  }
891
904
  if (!assistantOutput) {
892
905
  const actual = await this.invokeWithHistory(binding, options.input, threadId, runId);
906
+ if (Array.isArray(actual.contentBlocks) && actual.contentBlocks.length > 0) {
907
+ yield this.createContentBlocksItem(threadId, runId, selectedAgentId, actual.contentBlocks);
908
+ }
893
909
  if (actual.output) {
894
910
  assistantOutput = actual.output;
895
911
  emitted = true;
@@ -949,6 +965,9 @@ export class AgentHarnessRuntime {
949
965
  try {
950
966
  const actual = await this.invokeWithHistory(binding, options.input, threadId, runId);
951
967
  await this.appendAssistantMessage(threadId, runId, actual.output);
968
+ if (Array.isArray(actual.contentBlocks) && actual.contentBlocks.length > 0) {
969
+ yield this.createContentBlocksItem(threadId, runId, selectedAgentId, actual.contentBlocks);
970
+ }
952
971
  if (actual.output) {
953
972
  yield await this.emitOutputDeltaAndCreateItem(threadId, runId, selectedAgentId, actual.output);
954
973
  }
@@ -6,6 +6,8 @@ export declare function readTextContent(value: unknown): string;
6
6
  export declare function hasToolCalls(value: unknown): boolean;
7
7
  export declare function extractToolFallbackContext(value: unknown): string;
8
8
  export declare function extractVisibleOutput(value: unknown): string;
9
+ export declare function extractOutputContent(value: unknown): unknown;
10
+ export declare function extractContentBlocks(value: unknown): unknown[];
9
11
  export declare function extractEmptyAssistantMessageFailure(value: unknown): string;
10
12
  export declare function isToolCallParseFailure(error: unknown): boolean;
11
13
  export declare const STRICT_TOOL_JSON_INSTRUCTION = "When calling tools, return only the tool call itself. The arguments must be a pure JSON object with no explanatory text before or after it.";
@@ -166,8 +166,8 @@ function extractMessageContent(message) {
166
166
  if (typeof message !== "object" || !message)
167
167
  return "";
168
168
  const typed = message;
169
- if (typeof typed.content === "string")
170
- return typed.content;
169
+ if (typed.content !== undefined)
170
+ return readTextContent(typed.content);
171
171
  if (typeof typed.kwargs === "object" && typed.kwargs) {
172
172
  return readTextContent(typed.kwargs.content);
173
173
  }
@@ -341,6 +341,79 @@ export function extractVisibleOutput(value) {
341
341
  }
342
342
  return "";
343
343
  }
344
+ function isContentBlock(value) {
345
+ return typeof value === "object" && value !== null && typeof value.type === "string";
346
+ }
347
+ function normalizeContentBlocks(value) {
348
+ if (!Array.isArray(value)) {
349
+ return [];
350
+ }
351
+ return value.filter(isContentBlock).map((block) => ({ ...block }));
352
+ }
353
+ function extractStructuredOutputContent(value) {
354
+ if (typeof value !== "object" || !value)
355
+ return undefined;
356
+ const typed = value;
357
+ if (typed.output && typeof typed.output === "object") {
358
+ const nested = extractStructuredOutputContent(typed.output);
359
+ if (nested !== undefined)
360
+ return nested;
361
+ }
362
+ if (typed.content !== undefined) {
363
+ return typed.content;
364
+ }
365
+ if (!Array.isArray(typed.messages)) {
366
+ return undefined;
367
+ }
368
+ for (let index = typed.messages.length - 1; index >= 0; index -= 1) {
369
+ const message = typed.messages[index];
370
+ if (typeof message !== "object" || !message)
371
+ continue;
372
+ const ids = Array.isArray(message.id)
373
+ ? (message.id.filter((item) => typeof item === "string"))
374
+ : [];
375
+ const typeName = ids.at(-1);
376
+ const runtimeType = typeof message._getType === "function"
377
+ ? message._getType()
378
+ : typeof message.getType === "function"
379
+ ? message.getType()
380
+ : undefined;
381
+ if (!(typeName === "AIMessage" || runtimeType === "ai")) {
382
+ continue;
383
+ }
384
+ if (hasToolCalls(message)) {
385
+ continue;
386
+ }
387
+ const directContent = message.content;
388
+ if (directContent !== undefined) {
389
+ return directContent;
390
+ }
391
+ const kwargs = typeof message.kwargs === "object" && message.kwargs
392
+ ? (message.kwargs)
393
+ : undefined;
394
+ if (kwargs?.content !== undefined) {
395
+ return kwargs.content;
396
+ }
397
+ }
398
+ return undefined;
399
+ }
400
+ export function extractOutputContent(value) {
401
+ const content = extractStructuredOutputContent(value);
402
+ if (content !== undefined) {
403
+ return content;
404
+ }
405
+ return undefined;
406
+ }
407
+ export function extractContentBlocks(value) {
408
+ const outputContent = extractOutputContent(value);
409
+ if (outputContent === undefined) {
410
+ return [];
411
+ }
412
+ if (typeof outputContent === "string") {
413
+ return outputContent.trim() ? [{ type: "text", text: outputContent }] : [];
414
+ }
415
+ return normalizeContentBlocks(outputContent);
416
+ }
344
417
  export function extractEmptyAssistantMessageFailure(value) {
345
418
  if (typeof value !== "object" || !value)
346
419
  return "";
@@ -105,6 +105,9 @@ function buildSubagent(agent, workspaceRoot, models, tools, parentSkills, parent
105
105
  responseFormat: agent.deepAgentConfig?.responseFormat,
106
106
  contextSchema: agent.deepAgentConfig?.contextSchema,
107
107
  middleware: compileMiddlewareConfigs(agent.deepAgentConfig?.middleware, models, agent.id),
108
+ passthrough: typeof agent.deepAgentConfig?.passthrough === "object" && agent.deepAgentConfig.passthrough
109
+ ? { ...agent.deepAgentConfig.passthrough }
110
+ : undefined,
108
111
  };
109
112
  }
110
113
  function resolveDirectPrompt(agent) {
@@ -243,6 +246,9 @@ export function compileBinding(workspaceRoot, agent, agents, referencedSubagentI
243
246
  responseFormat: agent.langchainAgentConfig?.responseFormat,
244
247
  contextSchema: agent.langchainAgentConfig?.contextSchema,
245
248
  middleware: compileMiddlewareConfigs(agent.langchainAgentConfig?.middleware, models, agent.id),
249
+ passthrough: typeof agent.langchainAgentConfig?.passthrough === "object" && agent.langchainAgentConfig.passthrough
250
+ ? { ...agent.langchainAgentConfig.passthrough }
251
+ : undefined,
246
252
  subagents: agent.subagentRefs.map((ref) => {
247
253
  const subagent = agents.get(resolveRefId(ref));
248
254
  if (!subagent) {
@@ -282,6 +288,9 @@ export function compileBinding(workspaceRoot, agent, agents, referencedSubagentI
282
288
  responseFormat: agent.deepAgentConfig?.responseFormat,
283
289
  contextSchema: agent.deepAgentConfig?.contextSchema,
284
290
  middleware: compileMiddlewareConfigs(agent.deepAgentConfig?.middleware, models, agent.id),
291
+ passthrough: typeof agent.deepAgentConfig?.passthrough === "object" && agent.deepAgentConfig.passthrough
292
+ ? { ...agent.deepAgentConfig.passthrough }
293
+ : undefined,
285
294
  description: agent.description,
286
295
  subagents: agent.subagentRefs.map((ref) => {
287
296
  const subagent = agents.get(resolveRefId(ref));
@@ -215,6 +215,21 @@ function readExecutionConfig(value) {
215
215
  ? { ...value }
216
216
  : undefined;
217
217
  }
218
+ function cloneConfigValue(value) {
219
+ if (Array.isArray(value)) {
220
+ return value.map((item) => cloneConfigValue(item));
221
+ }
222
+ if (typeof value === "object" && value !== null) {
223
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, cloneConfigValue(entry)]));
224
+ }
225
+ return value;
226
+ }
227
+ function readPassthroughConfig(item, consumedKeys) {
228
+ const passthrough = Object.fromEntries(Object.entries(item)
229
+ .filter(([key]) => !consumedKeys.includes(key))
230
+ .map(([key, value]) => [key, cloneConfigValue(value)]));
231
+ return Object.keys(passthrough).length > 0 ? passthrough : undefined;
232
+ }
218
233
  function resolveExecutionBackend(item, current) {
219
234
  const execution = readExecutionConfig(item.execution) ?? readExecutionConfig(current?.execution);
220
235
  const backend = typeof execution?.backend === "string"
@@ -232,6 +247,33 @@ function resolveExecutionBackend(item, current) {
232
247
  }
233
248
  function readSharedAgentConfig(item) {
234
249
  const middleware = readMiddlewareArray(item.middleware);
250
+ const passthrough = readPassthroughConfig(item, [
251
+ "id",
252
+ "kind",
253
+ "description",
254
+ "modelRef",
255
+ "runRoot",
256
+ "tools",
257
+ "mcpServers",
258
+ "skills",
259
+ "memory",
260
+ "subagents",
261
+ "execution",
262
+ "capabilities",
263
+ "systemPrompt",
264
+ "checkpointer",
265
+ "interruptOn",
266
+ "stateSchema",
267
+ "responseFormat",
268
+ "contextSchema",
269
+ "includeAgentName",
270
+ "version",
271
+ "middleware",
272
+ "backend",
273
+ "store",
274
+ "taskDescription",
275
+ "generalPurposeAgent",
276
+ ]);
235
277
  return {
236
278
  ...(typeof item.systemPrompt === "string" ? { systemPrompt: item.systemPrompt } : {}),
237
279
  ...((typeof item.checkpointer === "object" && item.checkpointer) || typeof item.checkpointer === "boolean"
@@ -244,6 +286,7 @@ function readSharedAgentConfig(item) {
244
286
  ...(item.includeAgentName === "inline" ? { includeAgentName: "inline" } : {}),
245
287
  ...(item.version === "v1" || item.version === "v2" ? { version: item.version } : {}),
246
288
  ...(middleware ? { middleware } : {}),
289
+ ...(passthrough ? { passthrough } : {}),
247
290
  };
248
291
  }
249
292
  function readLangchainAgentConfig(item) {
@@ -38,6 +38,9 @@ function validateMiddlewareConfig(agent) {
38
38
  if (kind === "modelFallback" && !Array.isArray(typed.fallbackModels) && !Array.isArray(typed.models)) {
39
39
  throw new Error(`Agent ${agent.id} modelFallback middleware requires fallbackModels or models`);
40
40
  }
41
+ if (kind === "humanInTheLoop" && typeof typed.interruptOn !== "object") {
42
+ throw new Error(`Agent ${agent.id} humanInTheLoop middleware requires interruptOn`);
43
+ }
41
44
  if (kind === "pii" && typeof typed.piiType !== "string") {
42
45
  throw new Error(`Agent ${agent.id} pii middleware requires piiType`);
43
46
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.49",
3
+ "version": "0.0.50",
4
4
  "description": "Workspace runtime for multi-agent applications",
5
5
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",
@@ -26,11 +26,6 @@
26
26
  "types": "./dist/tools.d.ts",
27
27
  "import": "./dist/tools.js",
28
28
  "default": "./dist/tools.js"
29
- },
30
- "./presentation": {
31
- "types": "./dist/presentation.d.ts",
32
- "import": "./dist/presentation.js",
33
- "default": "./dist/presentation.js"
34
29
  }
35
30
  },
36
31
  "dependencies": {
@@ -38,7 +33,7 @@
38
33
  "@langchain/community": "^1.1.24",
39
34
  "@langchain/core": "^1.1.33",
40
35
  "@langchain/google": "^0.1.7",
41
- "@langchain/langgraph": "^1.2.3",
36
+ "@langchain/langgraph": "^1.2.5",
42
37
  "@langchain/langgraph-checkpoint-sqlite": "^1.0.1",
43
38
  "@langchain/ollama": "^1.2.6",
44
39
  "@langchain/openai": "^1.1.0",
@@ -46,7 +41,7 @@
46
41
  "@llamaindex/ollama": "^0.1.23",
47
42
  "@modelcontextprotocol/sdk": "^1.12.0",
48
43
  "deepagents": "1.8.4",
49
- "langchain": "1.2.34",
44
+ "langchain": "^1.2.36",
50
45
  "llamaindex": "^0.12.1",
51
46
  "mustache": "^4.2.0",
52
47
  "yaml": "^2.8.1",
@@ -55,9 +50,8 @@
55
50
  "scripts": {
56
51
  "build": "rm -rf dist tsconfig.tsbuildinfo && tsc -p tsconfig.json && cp -R config dist/",
57
52
  "check": "tsc -p tsconfig.json --noEmit",
58
- "test": "vitest run test/public-api.test.ts test/resource-optional-provider.test.ts test/resource-isolation.test.ts test/stock-research-app-load-harness.test.ts test/stock-research-app-run.test.ts test/release-workflow.test.ts test/release-version.test.ts test/gitignore.test.ts test/package-lock.test.ts test/readme.test.ts test/runtime-adapter-regressions.test.ts test/runtime-recovery.test.ts test/tool-extension-gaps.test.ts test/checkpoint-maintenance.test.ts test/llamaindex-dependency-compat.test.ts test/skill-standard.test.ts test/routing-config.test.ts test/workspace-compat-regressions.test.ts test/upstream-compat-regressions.test.ts test/embedded-browser-bookmarks.test.ts test/presentation-wallee.test.ts",
53
+ "test": "vitest run test/public-api.test.ts test/resource-optional-provider.test.ts test/resource-isolation.test.ts test/stock-research-app-load-harness.test.ts test/stock-research-app-run.test.ts test/release-workflow.test.ts test/release-version.test.ts test/gitignore.test.ts test/package-lock.test.ts test/readme.test.ts test/runtime-adapter-regressions.test.ts test/runtime-capabilities.test.ts test/runtime-recovery.test.ts test/tool-extension-gaps.test.ts test/checkpoint-maintenance.test.ts test/llamaindex-dependency-compat.test.ts test/skill-standard.test.ts test/routing-config.test.ts test/workspace-compat-regressions.test.ts test/upstream-compat-regressions.test.ts test/yaml-format.test.ts",
59
54
  "test:real-providers": "vitest run test/real-provider-harness.test.ts",
60
- "test:integration": "npm run build && node scripts/integration-wallee-browser.mjs",
61
55
  "release:prepare": "npm version patch --no-git-tag-version && node ./scripts/sync-example-version.mjs",
62
56
  "release:pack": "npm pack --dry-run",
63
57
  "release:publish": "npm publish --access public --registry https://registry.npmjs.org/"