@botbotgo/agent-harness 0.0.27 → 0.0.29

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.
@@ -13,9 +13,8 @@ spec:
13
13
  provider: ollama
14
14
  # LangChain aligned feature: concrete embedding model identifier passed to the provider integration.
15
15
  model: nomic-embed-text
16
- init:
17
- # LangChain aligned feature: provider-specific initialization options for embeddings.
18
- baseUrl: https://ollama-rtx-4070.easynet.world/
16
+ # LangChain aligned feature: provider-specific initialization options for embeddings.
17
+ baseUrl: https://ollama-rtx-4070.easynet.world/
19
18
 
20
19
  # ===================
21
20
  # DeepAgents Features
@@ -19,11 +19,11 @@ spec:
19
19
  # LangChain aligned feature: concrete model identifier passed to the selected provider integration.
20
20
  # Example values depend on `provider`, such as `gpt-oss:latest` for `ollama`.
21
21
  model: gpt-oss:latest
22
- init:
23
- # LangChain aligned feature: provider-specific initialization options.
24
- # Available keys are provider-specific; common examples include `baseUrl`, `temperature`, and auth/client settings.
25
- # `baseUrl` configures the Ollama-compatible endpoint used by the model client.
26
- # For `openai-compatible`, `baseUrl` is normalized into the ChatOpenAI `configuration.baseURL` field.
27
- baseUrl: https://ollama-rtx-4070.easynet.world/
28
- # LangChain aligned feature: provider/model initialization option controlling sampling temperature.
29
- temperature: 0.2
22
+ # LangChain aligned feature: provider-specific initialization options.
23
+ # Write these fields directly on the model object.
24
+ # Common examples include `baseUrl`, `temperature`, and auth/client settings.
25
+ # `baseUrl` configures the Ollama-compatible endpoint used by the model client.
26
+ # For `openai-compatible`, `baseUrl` is normalized into the ChatOpenAI `configuration.baseURL` field.
27
+ baseUrl: https://ollama-rtx-4070.easynet.world/
28
+ # LangChain aligned feature: provider/model initialization option controlling sampling temperature.
29
+ temperature: 0.2
@@ -237,9 +237,17 @@ export type RunListeners = {
237
237
  output: unknown;
238
238
  }) => void | Promise<void>;
239
239
  };
240
+ export type MessageContentPart = {
241
+ type: "text";
242
+ text: string;
243
+ } | {
244
+ type: "image_url";
245
+ image_url: string;
246
+ };
247
+ export type MessageContent = string | MessageContentPart[];
240
248
  export type RunStartOptions = {
241
249
  agentId?: string;
242
- input: string;
250
+ input: MessageContent;
243
251
  threadId?: string;
244
252
  listeners?: RunListeners;
245
253
  };
@@ -283,7 +291,7 @@ export type HarnessStreamItem = {
283
291
  };
284
292
  export type TranscriptMessage = {
285
293
  role: "user" | "assistant";
286
- content: string;
294
+ content: MessageContent;
287
295
  runId: string;
288
296
  createdAt: string;
289
297
  };
@@ -1 +1 @@
1
- export declare const AGENT_HARNESS_VERSION = "0.0.26";
1
+ export declare const AGENT_HARNESS_VERSION = "0.0.28";
@@ -1 +1 @@
1
- export const AGENT_HARNESS_VERSION = "0.0.26";
1
+ export const AGENT_HARNESS_VERSION = "0.0.28";
@@ -1,4 +1,4 @@
1
- import type { CompiledAgentBinding, RunResult, RuntimeAdapterOptions, TranscriptMessage } from "../contracts/types.js";
1
+ import type { CompiledAgentBinding, MessageContent, RunResult, RuntimeAdapterOptions, TranscriptMessage } from "../contracts/types.js";
2
2
  import { type RuntimeStreamChunk } from "./parsing/stream-event-parsing.js";
3
3
  type RunnableLike = {
4
4
  invoke: (input: unknown, config?: Record<string, unknown>) => Promise<unknown>;
@@ -26,10 +26,10 @@ export declare class AgentRuntimeAdapter {
26
26
  private buildRouteSystemPrompt;
27
27
  private resolveSubagents;
28
28
  create(binding: CompiledAgentBinding): Promise<RunnableLike>;
29
- route(input: string, primaryBinding: CompiledAgentBinding, secondaryBinding: CompiledAgentBinding, options?: {
29
+ route(input: MessageContent, primaryBinding: CompiledAgentBinding, secondaryBinding: CompiledAgentBinding, options?: {
30
30
  systemPrompt?: string;
31
31
  }): Promise<string>;
32
- invoke(binding: CompiledAgentBinding, input: string, threadId: string, runId: string, resumePayload?: unknown, history?: TranscriptMessage[]): Promise<RunResult>;
33
- stream(binding: CompiledAgentBinding, input: string, threadId: string, history?: TranscriptMessage[]): AsyncGenerator<RuntimeStreamChunk | string>;
32
+ invoke(binding: CompiledAgentBinding, input: MessageContent, threadId: string, runId: string, resumePayload?: unknown, history?: TranscriptMessage[]): Promise<RunResult>;
33
+ stream(binding: CompiledAgentBinding, input: MessageContent, threadId: string, history?: TranscriptMessage[]): AsyncGenerator<RuntimeStreamChunk | string>;
34
34
  }
35
35
  export { AgentRuntimeAdapter as RuntimeAdapter, AGENT_INTERRUPT_SENTINEL_PREFIX, AGENT_INTERRUPT_SENTINEL_PREFIX as INTERRUPT_SENTINEL_PREFIX, };
@@ -9,22 +9,50 @@ import { extractEmptyAssistantMessageFailure, extractReasoningText, extractToolF
9
9
  import { extractAgentStep, extractInterruptPayload, extractReasoningStreamOutput, extractTerminalStreamOutput, extractToolResult, normalizeTerminalOutputKey, readStreamDelta, } from "./parsing/stream-event-parsing.js";
10
10
  import { wrapToolForExecution } from "./tool-hitl.js";
11
11
  import { resolveDeclaredMiddleware } from "./declared-middleware.js";
12
+ import { extractMessageText, normalizeMessageContent } from "../utils/message-content.js";
12
13
  const AGENT_INTERRUPT_SENTINEL_PREFIX = "__agent_harness_interrupt__:";
13
14
  const MODEL_SAFE_TOOL_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
14
15
  function asObject(value) {
15
16
  return typeof value === "object" && value ? value : undefined;
16
17
  }
18
+ function isPlaceholderApiKey(value) {
19
+ return typeof value === "string" && value.trim().toLowerCase() === "dummy";
20
+ }
21
+ function buildAuthOmittingFetch(baseFetch = fetch) {
22
+ return async (input, init) => {
23
+ const sanitizedHeaders = new Headers(input instanceof Request ? input.headers : undefined);
24
+ const initHeaders = new Headers(init?.headers);
25
+ initHeaders.forEach((value, key) => {
26
+ sanitizedHeaders.set(key, value);
27
+ });
28
+ sanitizedHeaders.delete("authorization");
29
+ if (input instanceof Request) {
30
+ return baseFetch(new Request(input, {
31
+ ...init,
32
+ headers: sanitizedHeaders,
33
+ }));
34
+ }
35
+ return baseFetch(input, {
36
+ ...init,
37
+ headers: sanitizedHeaders,
38
+ });
39
+ };
40
+ }
17
41
  function normalizeOpenAICompatibleInit(init) {
18
42
  const normalized = { ...init };
19
43
  const configuration = asObject(init.configuration) ?? {};
20
44
  const baseUrl = typeof init.baseUrl === "string" && init.baseUrl.trim() ? init.baseUrl.trim() : undefined;
21
- if (baseUrl && typeof configuration.baseURL !== "string") {
22
- normalized.configuration = {
23
- ...configuration,
24
- baseURL: baseUrl,
25
- };
45
+ const omitAuthHeader = init.omitAuthHeader === true || isPlaceholderApiKey(init.apiKey);
46
+ const nextConfiguration = { ...configuration };
47
+ if (baseUrl && typeof nextConfiguration.baseURL !== "string") {
48
+ nextConfiguration.baseURL = baseUrl;
49
+ }
50
+ if (omitAuthHeader) {
51
+ nextConfiguration.fetch = buildAuthOmittingFetch(typeof configuration.fetch === "function" ? configuration.fetch : fetch);
26
52
  }
53
+ normalized.configuration = nextConfiguration;
27
54
  delete normalized.baseUrl;
55
+ delete normalized.omitAuthHeader;
28
56
  return normalized;
29
57
  }
30
58
  function sanitizeToolNameForModel(name) {
@@ -172,7 +200,7 @@ export class AgentRuntimeAdapter {
172
200
  },
173
201
  {
174
202
  role: "user",
175
- content: `Original user request:\n${input}\n\nTool results:\n${toolContext}`,
203
+ content: `Original user request:\n${extractMessageText(input)}\n\nTool results:\n${toolContext}`,
176
204
  },
177
205
  ]);
178
206
  return sanitizeVisibleText(extractVisibleOutput(synthesized));
@@ -181,8 +209,8 @@ export class AgentRuntimeAdapter {
181
209
  if (this.options.modelResolver) {
182
210
  return wrapResolvedModel(await this.options.modelResolver(model.id));
183
211
  }
184
- if (model.provider === "ollama" && typeof model.init.baseUrl === "string" && model.init.baseUrl.trim()) {
185
- return wrapResolvedModel(new ChatOllama({ model: model.model, baseUrl: model.init.baseUrl, ...model.init }));
212
+ if (model.provider === "ollama") {
213
+ return wrapResolvedModel(new ChatOllama({ model: model.model, ...model.init }));
186
214
  }
187
215
  if (model.provider === "openai-compatible") {
188
216
  return wrapResolvedModel(new ChatOpenAI({ model: model.model, ...normalizeOpenAICompatibleInit(model.init) }));
@@ -203,8 +231,8 @@ export class AgentRuntimeAdapter {
203
231
  }
204
232
  buildAgentMessages(history, input) {
205
233
  return [
206
- ...history.map((item) => ({ role: item.role, content: item.content })),
207
- { role: "user", content: input },
234
+ ...history.map((item) => ({ role: item.role, content: normalizeMessageContent(item.content) })),
235
+ { role: "user", content: normalizeMessageContent(input) },
208
236
  ];
209
237
  }
210
238
  buildRawModelMessages(systemPrompt, history, input) {
@@ -374,7 +402,7 @@ export class AgentRuntimeAdapter {
374
402
  role: "system",
375
403
  content: this.buildRouteSystemPrompt(primaryBinding, secondaryBinding, options.systemPrompt),
376
404
  },
377
- { role: "user", content: input },
405
+ { role: "user", content: extractMessageText(input) },
378
406
  ]);
379
407
  const content = typeof result === "string"
380
408
  ? result
@@ -1,4 +1,4 @@
1
- import type { ApprovalRecord, CompiledTool, HarnessEvent, HarnessStreamItem, RunStartOptions, RestartConversationOptions, RuntimeAdapterOptions, ResumeOptions, RunOptions, RunResult, ThreadSummary, ThreadRecord, WorkspaceBundle } from "../contracts/types.js";
1
+ import type { ApprovalRecord, CompiledTool, HarnessEvent, HarnessStreamItem, MessageContent, RunStartOptions, RestartConversationOptions, RuntimeAdapterOptions, ResumeOptions, RunOptions, RunResult, ThreadSummary, ThreadRecord, WorkspaceBundle } from "../contracts/types.js";
2
2
  export declare class AgentHarness {
3
3
  private readonly workspace;
4
4
  private readonly runtimeAdapterOptions;
@@ -40,7 +40,7 @@ export declare class AgentHarness {
40
40
  private getSession;
41
41
  getThread(threadId: string): Promise<ThreadRecord | null>;
42
42
  listPendingApprovals(): Promise<ApprovalRecord[]>;
43
- routeAgent(input: string, options?: {
43
+ routeAgent(input: MessageContent, options?: {
44
44
  threadId?: string;
45
45
  }): Promise<string>;
46
46
  private emit;
@@ -13,6 +13,7 @@ import { resolveCompiledVectorStore, resolveCompiledVectorStoreRef } from "./sup
13
13
  import { ThreadMemorySync } from "./thread-memory-sync.js";
14
14
  import { FileBackedStore } from "./store.js";
15
15
  import { CheckpointMaintenanceLoop, discoverCheckpointMaintenanceTargets, readCheckpointMaintenanceConfig, } from "./checkpoint-maintenance.js";
16
+ import { extractMessageText, normalizeMessageContent } from "../utils/message-content.js";
16
17
  export class AgentHarness {
17
18
  workspace;
18
19
  runtimeAdapterOptions;
@@ -39,27 +40,28 @@ export class AgentHarness {
39
40
  }
40
41
  heuristicRoute(input) {
41
42
  const { primaryBinding, secondaryBinding } = inferRoutingBindings(this.workspace);
42
- return heuristicRoute(input, primaryBinding, secondaryBinding);
43
+ return heuristicRoute(extractMessageText(input), primaryBinding, secondaryBinding);
43
44
  }
44
45
  async buildRoutingInput(input, threadId) {
46
+ const inputText = extractMessageText(input);
45
47
  if (!threadId) {
46
- return input;
48
+ return inputText;
47
49
  }
48
50
  const history = await this.persistence.listThreadMessages(threadId);
49
- const priorHistory = history.filter((message) => message.content.trim());
51
+ const priorHistory = history.filter((message) => extractMessageText(message.content).trim());
50
52
  if (priorHistory.length === 0) {
51
- return input;
53
+ return inputText;
52
54
  }
53
55
  const recentTurns = priorHistory.slice(-6).map((message) => {
54
56
  const role = message.role === "assistant" ? "assistant" : "user";
55
- const compact = message.content.replace(/\s+/g, " ").trim();
57
+ const compact = extractMessageText(message.content).replace(/\s+/g, " ").trim();
56
58
  return `${role}: ${compact.slice(0, 240)}`;
57
59
  });
58
60
  return [
59
61
  "Recent conversation context:",
60
62
  ...recentTurns,
61
63
  "",
62
- `Current user request: ${input}`,
64
+ `Current user request: ${inputText}`,
63
65
  ].join("\n");
64
66
  }
65
67
  async resolveSelectedAgentId(input, requestedAgentId, threadId) {
@@ -234,25 +236,25 @@ export class AgentHarness {
234
236
  }
235
237
  async routeAgent(input, options = {}) {
236
238
  const routingInput = await this.buildRoutingInput(input, options.threadId);
237
- const normalized = input.trim().toLowerCase();
239
+ const normalized = extractMessageText(input).trim().toLowerCase();
238
240
  const { primaryBinding, secondaryBinding, researchBinding } = inferRoutingBindings(this.workspace);
239
241
  const deterministicIntent = resolveDeterministicRouteIntent(normalized);
240
242
  if (requiresResearchRoute(normalized)) {
241
243
  const explicitResearchBinding = this.workspace.bindings.get("research-lite") ?? this.workspace.bindings.get("research");
242
244
  const hostResearchBinding = explicitResearchBinding?.harnessRuntime.hostFacing !== false ? explicitResearchBinding : researchBinding;
243
- return hostResearchBinding?.agent.id ?? secondaryBinding?.agent.id ?? primaryBinding?.agent.id ?? heuristicRoute(input, primaryBinding, secondaryBinding);
245
+ return hostResearchBinding?.agent.id ?? secondaryBinding?.agent.id ?? primaryBinding?.agent.id ?? heuristicRoute(extractMessageText(input), primaryBinding, secondaryBinding);
244
246
  }
245
247
  if (secondaryBinding?.deepAgentParams) {
246
248
  return secondaryBinding.agent.id;
247
249
  }
248
250
  if (deterministicIntent === "primary") {
249
- return primaryBinding?.agent.id ?? heuristicRoute(input, primaryBinding, secondaryBinding);
251
+ return primaryBinding?.agent.id ?? heuristicRoute(extractMessageText(input), primaryBinding, secondaryBinding);
250
252
  }
251
253
  if (deterministicIntent === "secondary") {
252
- return secondaryBinding?.agent.id ?? primaryBinding?.agent.id ?? heuristicRoute(input, primaryBinding, secondaryBinding);
254
+ return secondaryBinding?.agent.id ?? primaryBinding?.agent.id ?? heuristicRoute(extractMessageText(input), primaryBinding, secondaryBinding);
253
255
  }
254
256
  if (!primaryBinding || !secondaryBinding) {
255
- return heuristicRoute(input, primaryBinding, secondaryBinding);
257
+ return heuristicRoute(extractMessageText(input), primaryBinding, secondaryBinding);
256
258
  }
257
259
  try {
258
260
  return await this.runtimeAdapter.route(routingInput, primaryBinding, secondaryBinding, {
@@ -260,7 +262,7 @@ export class AgentHarness {
260
262
  });
261
263
  }
262
264
  catch {
263
- return heuristicRoute(input, primaryBinding, secondaryBinding);
265
+ return heuristicRoute(extractMessageText(input), primaryBinding, secondaryBinding);
264
266
  }
265
267
  }
266
268
  async emit(threadId, runId, sequence, eventType, payload, source = "runtime") {
@@ -284,7 +286,7 @@ export class AgentHarness {
284
286
  }
285
287
  await this.persistence.appendThreadMessage(threadId, {
286
288
  role: "user",
287
- content: input,
289
+ content: normalizeMessageContent(input),
288
290
  runId,
289
291
  createdAt,
290
292
  });
@@ -359,7 +361,7 @@ export class AgentHarness {
359
361
  });
360
362
  }
361
363
  async persistApproval(threadId, runId, checkpointRef, input, interruptContent) {
362
- const approval = createPendingApproval(threadId, runId, checkpointRef, input, interruptContent);
364
+ const approval = createPendingApproval(threadId, runId, checkpointRef, extractMessageText(input), interruptContent);
363
365
  await this.persistence.createApproval(approval);
364
366
  const artifact = await this.persistence.createArtifact(threadId, runId, {
365
367
  artifactId: `artifact-approval-${runId}`,
@@ -1,8 +1,9 @@
1
+ import { extractMessageText } from "../utils/message-content.js";
1
2
  function excerpt(message) {
2
3
  if (!message?.content) {
3
4
  return "(none)";
4
5
  }
5
- const normalized = message.content.replace(/\s+/g, " ").trim();
6
+ const normalized = extractMessageText(message.content).replace(/\s+/g, " ").trim();
6
7
  return normalized.length > 240 ? `${normalized.slice(0, 237)}...` : normalized;
7
8
  }
8
9
  function renderStatusMarkdown(thread, messages) {
@@ -0,0 +1,5 @@
1
+ import type { MessageContent, MessageContentPart } from "../contracts/types.js";
2
+ export declare function isMessageContentPart(value: unknown): value is MessageContentPart;
3
+ export declare function isMessageContent(value: unknown): value is MessageContent;
4
+ export declare function extractMessageText(content: MessageContent): string;
5
+ export declare function normalizeMessageContent(content: MessageContent): MessageContent;
@@ -0,0 +1,30 @@
1
+ export function isMessageContentPart(value) {
2
+ if (typeof value !== "object" || !value) {
3
+ return false;
4
+ }
5
+ const typed = value;
6
+ return typed.type === "text" || typed.type === "image_url";
7
+ }
8
+ export function isMessageContent(value) {
9
+ return typeof value === "string" || (Array.isArray(value) && value.every(isMessageContentPart));
10
+ }
11
+ export function extractMessageText(content) {
12
+ if (typeof content === "string") {
13
+ return content;
14
+ }
15
+ return content
16
+ .filter((part) => part.type === "text")
17
+ .map((part) => part.text)
18
+ .join("");
19
+ }
20
+ export function normalizeMessageContent(content) {
21
+ if (typeof content === "string") {
22
+ return content;
23
+ }
24
+ return content.map((part) => {
25
+ if (part.type === "text") {
26
+ return { type: "text", text: part.text };
27
+ }
28
+ return { type: "image_url", image_url: part.image_url };
29
+ });
30
+ }
@@ -22,6 +22,19 @@ function mergeObjects(base, extra) {
22
22
  ...(extra ?? {}),
23
23
  };
24
24
  }
25
+ function extractTopLevelInitFields(value, reservedKeys) {
26
+ const reserved = new Set(reservedKeys);
27
+ const entries = Object.entries(value).filter(([key]) => !reserved.has(key));
28
+ if (entries.length === 0) {
29
+ return undefined;
30
+ }
31
+ return Object.fromEntries(entries);
32
+ }
33
+ function assertNoInitBlock(value, kind, id) {
34
+ if (value.init !== undefined) {
35
+ throw new Error(`${kind} ${id} must define provider options at the top level; nested init is not supported`);
36
+ }
37
+ }
25
38
  function parseHitlPolicy(value) {
26
39
  const record = asObject(value);
27
40
  if (!record) {
@@ -38,9 +51,10 @@ function parseHitlPolicy(value) {
38
51
  }
39
52
  export function parseModelObject(object) {
40
53
  const value = object.value;
54
+ assertNoInitBlock(value, "Model", object.id);
41
55
  const provider = String(value.provider ?? "").trim();
42
56
  const model = String((value.model ?? value.name) ?? "").trim();
43
- const init = asObject(value.init) ?? {};
57
+ const init = extractTopLevelInitFields(value, ["provider", "model", "name", "init", "clientRef", "fallbacks", "metadata"]) ?? {};
44
58
  const fallbacks = Array.isArray(value.fallbacks)
45
59
  ? value.fallbacks
46
60
  .map((item) => (typeof item === "object" && item && "ref" in item ? String(item.ref) : undefined))
@@ -59,9 +73,10 @@ export function parseModelObject(object) {
59
73
  }
60
74
  export function parseEmbeddingModelObject(object) {
61
75
  const value = object.value;
76
+ assertNoInitBlock(value, "EmbeddingModel", object.id);
62
77
  const provider = String(value.provider ?? "").trim();
63
78
  const model = String((value.model ?? value.name) ?? "").trim();
64
- const init = asObject(value.init) ?? {};
79
+ const init = extractTopLevelInitFields(value, ["provider", "model", "name", "init", "clientRef", "metadata"]) ?? {};
65
80
  return {
66
81
  id: object.id,
67
82
  provider,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.27",
3
+ "version": "0.0.29",
4
4
  "description": "Agent Harness framework package",
5
5
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",
@@ -51,6 +51,7 @@
51
51
  "build": "rm -rf dist tsconfig.tsbuildinfo && tsc -p tsconfig.json && cp -R config dist/",
52
52
  "check": "tsc -p tsconfig.json --noEmit",
53
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/tool-extension-gaps.test.ts test/checkpoint-maintenance.test.ts test/llamaindex-dependency-compat.test.ts",
54
+ "test:real-providers": "vitest run test/real-provider-harness.test.ts",
54
55
  "release:prepare": "npm version patch --no-git-tag-version && node ./scripts/sync-example-version.mjs",
55
56
  "release:pack": "npm pack --dry-run",
56
57
  "release:publish": "npm publish --access public --registry https://registry.npmjs.org/"