@botbotgo/agent-harness 0.0.134 → 0.0.135

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.
@@ -6,6 +6,7 @@ import { resolveIsolatedResourceModulePath } from "../resource/isolation.js";
6
6
  import { isExternalSourceLocator, resolveResourcePackageRoot } from "../resource/sources.js";
7
7
  import { discoverToolModuleDefinitions, isSupportedToolModulePath } from "../tool-modules.js";
8
8
  import { fileExists } from "../utils/fs.js";
9
+ import { parseToolObject } from "./resource-compilers.js";
9
10
  import { readNamedModelItems, readNamedYamlItems, readYamlItems, } from "./yaml-object-reader.js";
10
11
  export { normalizeYamlItem, readYamlItems } from "./yaml-object-reader.js";
11
12
  const CONVENTIONAL_OBJECT_DIRECTORIES = ["tools"];
@@ -93,6 +94,89 @@ function readRefArray(items) {
93
94
  : undefined)
94
95
  .filter((item) => Boolean(item));
95
96
  }
97
+ function readToolBindingArray(items) {
98
+ return toArray(items)
99
+ .map((item) => {
100
+ if (typeof item === "string") {
101
+ return { ref: item };
102
+ }
103
+ if (typeof item !== "object" || !item || !("ref" in item) || typeof item.ref !== "string") {
104
+ return undefined;
105
+ }
106
+ const { ref, ...rest } = item;
107
+ const overrides = Object.keys(rest).length > 0
108
+ ? cloneConfigValue(rest)
109
+ : undefined;
110
+ return {
111
+ ref,
112
+ ...(overrides ? { overrides } : {}),
113
+ };
114
+ })
115
+ .filter((item) => Boolean(item));
116
+ }
117
+ function readInlineToolObjects(items, sourcePath, agentId) {
118
+ const sourceDir = path.dirname(sourcePath);
119
+ const bindings = [];
120
+ const inlineTools = [];
121
+ toArray(items).forEach((item, index) => {
122
+ if (typeof item === "string") {
123
+ bindings.push({ ref: item });
124
+ return;
125
+ }
126
+ if (typeof item !== "object" || !item) {
127
+ return;
128
+ }
129
+ if ("ref" in item && typeof item.ref === "string") {
130
+ const { ref, ...rest } = item;
131
+ const overrides = Object.keys(rest).length > 0
132
+ ? cloneConfigValue(rest)
133
+ : undefined;
134
+ bindings.push({ ref, ...(overrides ? { overrides } : {}) });
135
+ return;
136
+ }
137
+ const raw = cloneConfigValue(item);
138
+ const implementation = asObject(raw.implementation);
139
+ const name = typeof raw.name === "string" && raw.name.trim().length > 0
140
+ ? raw.name.trim()
141
+ : typeof raw.id === "string" && raw.id.trim().length > 0
142
+ ? raw.id.trim()
143
+ : `tool-${index + 1}`;
144
+ const syntheticId = `${agentId}__${name}`;
145
+ const implementationPath = typeof implementation?.path === "string"
146
+ ? resolveModuleRelativePath(path.resolve(sourceDir, implementation.path), undefined)
147
+ : undefined;
148
+ const inferredType = typeof raw.type === "string"
149
+ ? raw.type
150
+ : raw.refs !== undefined || raw.bundle !== undefined
151
+ ? "bundle"
152
+ : raw.providerTool !== undefined || raw.provider !== undefined
153
+ ? "provider"
154
+ : raw.backend !== undefined || raw.operation !== undefined
155
+ ? "backend"
156
+ : raw.mcp !== undefined
157
+ ? "mcp"
158
+ : "function";
159
+ if (inferredType === "function" && !implementationPath) {
160
+ throw new Error(`Inline tool ${name} must define implementation.path`);
161
+ }
162
+ const workspaceObject = {
163
+ id: syntheticId,
164
+ kind: "tool",
165
+ sourcePath: implementationPath ?? sourcePath,
166
+ value: {
167
+ id: syntheticId,
168
+ ...raw,
169
+ ...(typeof implementation?.export === "string" && typeof raw.implementationName !== "string"
170
+ ? { implementationName: implementation.export }
171
+ : {}),
172
+ },
173
+ };
174
+ const parsedTool = parseToolObject(workspaceObject);
175
+ inlineTools.push(parsedTool);
176
+ bindings.push({ ref: `tool/${syntheticId}` });
177
+ });
178
+ return { bindings, inlineTools };
179
+ }
96
180
  function readPathArray(items) {
97
181
  return toArray(items)
98
182
  .map((item) => typeof item === "string"
@@ -347,6 +431,7 @@ function readAgentConfig(item, options = {}) {
347
431
  export function parseAgentItem(item, sourcePath) {
348
432
  const moduleRoot = moduleRootForSourcePath(sourcePath, "agents");
349
433
  const subagentRefs = readExecutionValue(item, "subagents", readRefArray);
434
+ const { bindings: toolBindings, inlineTools } = readInlineToolObjects(readExecutionValue(item, "tools", toArray), sourcePath, String(item.id));
350
435
  const subagentPathRefs = readExecutionValue(item, "subagents", readPathArray).map((entry) => resolveModuleRelativePath(entry, moduleRoot));
351
436
  const executionMode = String(resolveExecutionBackend(item) ?? "deepagent");
352
437
  const runtime = readRuntimeConfig(item);
@@ -360,7 +445,9 @@ export function parseAgentItem(item, sourcePath) {
360
445
  description: String(item.description ?? ""),
361
446
  modelRef: readExecutionValue(item, "modelRef", readSingleRef) ?? "",
362
447
  runRoot: typeof runtime?.runRoot === "string" ? runtime.runRoot : undefined,
363
- toolRefs: readExecutionValue(item, "tools", readRefArray),
448
+ toolRefs: toolBindings.map((binding) => binding.ref),
449
+ toolBindings,
450
+ inlineTools,
364
451
  mcpServers: readExecutionValue(item, "mcpServers", readObjectArray),
365
452
  skillPathRefs: readExecutionValue(item, "skills", readPathArray).map((entry) => resolveModuleRelativePath(entry, moduleRoot)),
366
453
  memorySources: readExecutionValue(item, "memory", readPathArray).map((entry) => resolveModuleRelativePath(entry, moduleRoot)),
@@ -409,9 +496,19 @@ function mergeValues(base, override) {
409
496
  }
410
497
  function mergeRawItemRecord(records, key, item, sourcePath) {
411
498
  const current = records.get(key);
499
+ const mergedItem = current ? mergeValues(current.item, item) : item;
500
+ const kind = typeof mergedItem.kind === "string" ? mergedItem.kind : undefined;
501
+ const type = typeof mergedItem.type === "string" ? mergedItem.type : undefined;
502
+ const resolvedSourcePath = current &&
503
+ kind === "tool" &&
504
+ type === "function" &&
505
+ isSupportedToolModulePath(current.sourcePath) &&
506
+ !isSupportedToolModulePath(sourcePath)
507
+ ? current.sourcePath
508
+ : sourcePath;
412
509
  const mergedRecord = {
413
- item: current ? mergeValues(current.item, item) : item,
414
- sourcePath,
510
+ item: mergedItem,
511
+ sourcePath: resolvedSourcePath,
415
512
  };
416
513
  records.set(key, mergedRecord);
417
514
  return mergedRecord;
@@ -248,7 +248,13 @@ export function parseToolObject(object) {
248
248
  ...(mcpServerConfig && Object.keys(mcpServerConfig).length > 0 ? { mcpServer: mcpServerConfig } : {}),
249
249
  }
250
250
  : undefined),
251
+ subprocess: value.subprocess === true,
251
252
  inputSchemaRef: typeof asObject(value.inputSchema)?.ref === "string" ? String(asObject(value.inputSchema)?.ref) : undefined,
253
+ embeddingModelRef: typeof value.embeddingModelRef === "string"
254
+ ? value.embeddingModelRef
255
+ : typeof asObject(value.embeddingModel)?.ref === "string"
256
+ ? String(asObject(value.embeddingModel)?.ref)
257
+ : undefined,
252
258
  backendOperation: typeof backend?.operation === "string"
253
259
  ? backend.operation
254
260
  : typeof value.operation === "string"
@@ -20,6 +20,18 @@ function toMcpServerConfig(server) {
20
20
  function readStringArray(value) {
21
21
  return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
22
22
  }
23
+ function readStringRecord(value) {
24
+ if (typeof value !== "object" || !value || Array.isArray(value)) {
25
+ return undefined;
26
+ }
27
+ const entries = Object.entries(value).filter((entry) => typeof entry[1] === "string");
28
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
29
+ }
30
+ function asObject(value) {
31
+ return typeof value === "object" && value !== null && !Array.isArray(value)
32
+ ? value
33
+ : undefined;
34
+ }
23
35
  function compileRegexList(value, label) {
24
36
  return readStringArray(value).map((pattern) => {
25
37
  try {
@@ -53,6 +65,52 @@ function shouldIncludeRemoteMcpTool(filter, toolName) {
53
65
  }
54
66
  return true;
55
67
  }
68
+ function normalizeAgentMcpServerUsage(item) {
69
+ const config = asObject(item.config);
70
+ if (!config) {
71
+ return item;
72
+ }
73
+ return {
74
+ ...config,
75
+ ...item,
76
+ };
77
+ }
78
+ function hasMcpServerUsageOverrides(item) {
79
+ return [
80
+ "transport",
81
+ "command",
82
+ "args",
83
+ "env",
84
+ "cwd",
85
+ "url",
86
+ "token",
87
+ "headers",
88
+ ].some((key) => item[key] !== undefined);
89
+ }
90
+ function mergeReferencedMcpServer(referencedServer, item, agentId, name, sourcePath) {
91
+ return {
92
+ id: `${agentId}.${name}`,
93
+ transport: item.transport === "stdio" || item.transport === "http" || item.transport === "sse" || item.transport === "websocket"
94
+ ? item.transport
95
+ : referencedServer.transport,
96
+ url: typeof item.url === "string" ? item.url : referencedServer.url,
97
+ command: typeof item.command === "string" ? item.command : referencedServer.command,
98
+ args: Array.isArray(item.args)
99
+ ? item.args.filter((entry) => typeof entry === "string")
100
+ : referencedServer.args,
101
+ env: {
102
+ ...(referencedServer.env ?? {}),
103
+ ...(readStringRecord(item.env) ?? {}),
104
+ },
105
+ cwd: typeof item.cwd === "string" ? item.cwd : referencedServer.cwd,
106
+ token: typeof item.token === "string" ? item.token : referencedServer.token,
107
+ headers: {
108
+ ...(referencedServer.headers ?? {}),
109
+ ...(readStringRecord(item.headers) ?? {}),
110
+ },
111
+ sourcePath,
112
+ };
113
+ }
56
114
  async function hydrateExternalToolSource(tools, source, workspaceRoot) {
57
115
  const externalRoot = await ensureExternalResourceSource(source, workspaceRoot);
58
116
  const discoveredToolRefs = [];
@@ -102,35 +160,51 @@ export async function hydrateResourceAndExternalTools(tools, toolSourceRefs, wor
102
160
  name: existing?.name || resourceTool.name,
103
161
  description: existing?.description || resourceTool.description,
104
162
  config: existing?.config,
163
+ embeddingModelRef: existing?.embeddingModelRef,
105
164
  backendOperation: existing?.backendOperation ?? resourceTool.backendOperation,
106
165
  bundleRefs: existing?.bundleRefs ?? [],
107
166
  hitl: existing?.hitl ?? resourceTool.hitl,
108
167
  retryable: existing?.retryable ?? resourceTool.retryable,
109
- sourcePath: existing?.sourcePath ?? resourceTool.toolPath,
168
+ sourcePath: resourceTool.toolPath,
110
169
  });
111
170
  }
112
171
  }
113
172
  export async function hydrateAgentMcpTools(agents, mcpServers, tools) {
114
173
  for (const agent of agents) {
115
174
  const discoveredRefs = new Set(agent.toolRefs);
116
- for (const item of agent.mcpServers ?? []) {
175
+ for (const rawItem of agent.mcpServers ?? []) {
176
+ const item = normalizeAgentMcpServerUsage(rawItem);
177
+ const serverRef = typeof item.ref === "string" ? item.ref.trim() : "";
178
+ const referencedServerId = serverRef.startsWith("mcp/") ? serverRef.slice(4) : serverRef;
179
+ const referencedServer = referencedServerId ? mcpServers.get(referencedServerId) : undefined;
117
180
  const name = typeof item.name === "string" && item.name.trim()
118
181
  ? item.name.trim()
119
- : typeof item.id === "string" && item.id.trim()
120
- ? item.id.trim()
121
- : "";
182
+ : referencedServer?.id
183
+ ? referencedServer.id
184
+ : typeof item.id === "string" && item.id.trim()
185
+ ? item.id.trim()
186
+ : "";
122
187
  if (!name) {
123
188
  throw new Error(`Agent ${agent.id} has an MCP server entry without a name`);
124
189
  }
125
- const serverId = `${agent.id}.${name}`;
126
- const parsedServer = parseMcpServerObject({
127
- id: serverId,
128
- kind: "mcp",
129
- sourcePath: agent.sourcePath,
130
- value: item,
131
- });
190
+ let serverId = referencedServer?.id;
191
+ let parsedServer = referencedServer;
192
+ if (!parsedServer) {
193
+ serverId = `${agent.id}.${name}`;
194
+ parsedServer = parseMcpServerObject({
195
+ id: serverId,
196
+ kind: "mcp",
197
+ sourcePath: agent.sourcePath,
198
+ value: item,
199
+ });
200
+ mcpServers.set(serverId, parsedServer);
201
+ }
202
+ else if (referencedServer && hasMcpServerUsageOverrides(item)) {
203
+ parsedServer = mergeReferencedMcpServer(referencedServer, item, agent.id, name, agent.sourcePath);
204
+ serverId = parsedServer.id;
205
+ mcpServers.set(serverId, parsedServer);
206
+ }
132
207
  const filter = compileMcpToolFilter(item);
133
- mcpServers.set(serverId, parsedServer);
134
208
  const remoteTools = await listRemoteMcpTools(toMcpServerConfig(parsedServer));
135
209
  for (const remoteTool of remoteTools) {
136
210
  if (!shouldIncludeRemoteMcpTool(filter, remoteTool.name)) {
@@ -3,9 +3,31 @@ import { readdir } from "node:fs/promises";
3
3
  import { parseAllDocuments } from "yaml";
4
4
  import { fileExists, listFilesRecursive, readYamlOrJson } from "../utils/fs.js";
5
5
  const MODEL_FILENAMES = ["models.yaml", "models.yml"];
6
+ const ENV_PLACEHOLDER_PATTERN = /\$\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g;
6
7
  function asObject(value) {
7
8
  return typeof value === "object" && value ? value : undefined;
8
9
  }
10
+ function interpolateEnvString(value, sourcePath) {
11
+ return value.replaceAll(ENV_PLACEHOLDER_PATTERN, (_match, envName) => {
12
+ const resolved = process.env[envName];
13
+ if (typeof resolved !== "string" || resolved.length === 0) {
14
+ throw new Error(`YAML document ${sourcePath} references missing environment variable ${envName}`);
15
+ }
16
+ return resolved;
17
+ });
18
+ }
19
+ function interpolateEnvPlaceholders(value, sourcePath) {
20
+ if (typeof value === "string") {
21
+ return interpolateEnvString(value, sourcePath);
22
+ }
23
+ if (Array.isArray(value)) {
24
+ return value.map((entry) => interpolateEnvPlaceholders(entry, sourcePath));
25
+ }
26
+ if (typeof value === "object" && value !== null) {
27
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, interpolateEnvPlaceholders(entry, sourcePath)]));
28
+ }
29
+ return value;
30
+ }
9
31
  function normalizeCatalogSpec(document, options = {}) {
10
32
  const typed = asObject(document);
11
33
  const spec = typed?.spec;
@@ -116,15 +138,17 @@ async function objectItemsFromDocument(document, sourcePath) {
116
138
  : undefined;
117
139
  const catalogItems = catalogKind === "Models"
118
140
  ? normalizeCatalogSpec(document, { defaultKind: "Model" })
119
- : catalogKind === "Stores"
120
- ? normalizeCatalogSpec(document)
121
- : catalogKind === "Backends"
122
- ? normalizeCatalogSpec(document, { defaultKind: "Backend" })
123
- : catalogKind === "Tools"
124
- ? normalizeCatalogSpec(document, { defaultKind: "Tool" })
125
- : catalogKind === "McpServers"
126
- ? normalizeCatalogSpec(document, { defaultKind: "McpServer" })
127
- : [];
141
+ : catalogKind === "EmbeddingModels"
142
+ ? normalizeCatalogSpec(document, { defaultKind: "EmbeddingModel" })
143
+ : catalogKind === "Stores"
144
+ ? normalizeCatalogSpec(document)
145
+ : catalogKind === "Backends"
146
+ ? normalizeCatalogSpec(document, { defaultKind: "Backend" })
147
+ : catalogKind === "Tools"
148
+ ? normalizeCatalogSpec(document, { defaultKind: "Tool" })
149
+ : catalogKind === "McpServers"
150
+ ? normalizeCatalogSpec(document, { defaultKind: "McpServer" })
151
+ : [];
128
152
  if (catalogItems.length > 0) {
129
153
  return catalogItems;
130
154
  }
@@ -148,7 +172,8 @@ export async function readYamlItems(root, relativeDir, options = {}) {
148
172
  for (const filePath of files) {
149
173
  const parsedDocuments = parseAllDocuments(await readYamlOrJson(filePath));
150
174
  for (const parsedDocument of parsedDocuments) {
151
- for (const item of await objectItemsFromDocument(parsedDocument.toJSON(), filePath)) {
175
+ const resolvedDocument = interpolateEnvPlaceholders(parsedDocument.toJSON(), filePath);
176
+ for (const item of await objectItemsFromDocument(resolvedDocument, filePath)) {
152
177
  records.push({ item: normalizeYamlItem(item), sourcePath: filePath });
153
178
  }
154
179
  }
@@ -164,7 +189,8 @@ export async function readNamedYamlItems(root, filenames) {
164
189
  }
165
190
  const parsedDocuments = parseAllDocuments(await readYamlOrJson(filePath));
166
191
  for (const parsedDocument of parsedDocuments) {
167
- for (const item of await objectItemsFromDocument(parsedDocument.toJSON(), filePath)) {
192
+ const resolvedDocument = interpolateEnvPlaceholders(parsedDocument.toJSON(), filePath);
193
+ for (const item of await objectItemsFromDocument(resolvedDocument, filePath)) {
168
194
  records.push({ item: normalizeYamlItem(item), sourcePath: filePath });
169
195
  }
170
196
  }
@@ -190,7 +216,8 @@ export async function readNamedModelItems(root) {
190
216
  for (const filePath of [...filePaths].sort()) {
191
217
  const parsedDocuments = parseAllDocuments(await readYamlOrJson(filePath));
192
218
  for (const parsedDocument of parsedDocuments) {
193
- for (const item of await objectItemsFromDocument(parsedDocument.toJSON(), filePath)) {
219
+ const resolvedDocument = interpolateEnvPlaceholders(parsedDocument.toJSON(), filePath);
220
+ for (const item of await objectItemsFromDocument(resolvedDocument, filePath)) {
194
221
  const normalized = normalizeYamlItem(item);
195
222
  if (normalized.kind === "model" && typeof normalized.id === "string" && normalized.id.trim()) {
196
223
  records.push({ item: normalized, sourcePath: filePath });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.134",
3
+ "version": "0.0.135",
4
4
  "description": "Workspace runtime for multi-agent applications",
5
5
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",