@botbotgo/agent-harness 0.0.79 → 0.0.81

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.
@@ -4,11 +4,29 @@ import { readdir, readFile } from "node:fs/promises";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
5
5
  import { parseAllDocuments } from "yaml";
6
6
  import { resolveIsolatedResourceModulePath } from "../resource/isolation.js";
7
- import { resolveResourcePackageRoot } from "../resource/sources.js";
7
+ import { isExternalSourceLocator, resolveResourcePackageRoot } from "../resource/sources.js";
8
8
  import { discoverToolModuleDefinitions, isSupportedToolModulePath } from "../tool-modules.js";
9
9
  import { fileExists, listFilesRecursive, readYamlOrJson } from "../utils/fs.js";
10
10
  const MODEL_FILENAMES = ["models.yaml", "models.yml"];
11
11
  const CONVENTIONAL_OBJECT_DIRECTORIES = ["tools"];
12
+ const MODULE_AGENT_FILENAMES = ["agent.yaml", "agent.yml"];
13
+ const MODULE_TOOL_FILENAMES = ["tool.yaml", "tool.yml"];
14
+ const MODULE_TOOL_ENTRY_FILENAMES = ["index.mjs", "index.js", "index.cjs"];
15
+ function moduleCollectionRoot(root, kind) {
16
+ return path.join(root, "modules", kind);
17
+ }
18
+ function isModuleDefinitionPath(sourcePath, kind) {
19
+ const normalized = sourcePath.split(path.sep).join("/");
20
+ const suffix = kind === "agents" ? "/agent.yaml" : "/tool.yaml";
21
+ const altSuffix = kind === "agents" ? "/agent.yml" : "/tool.yml";
22
+ return normalized.includes(`/modules/${kind}/`) && (normalized.endsWith(suffix) || normalized.endsWith(altSuffix));
23
+ }
24
+ function moduleRootForSourcePath(sourcePath, kind) {
25
+ if (!isModuleDefinitionPath(sourcePath, kind)) {
26
+ return undefined;
27
+ }
28
+ return path.dirname(sourcePath);
29
+ }
12
30
  function conventionalConfigRoot(root) {
13
31
  if (path.basename(root) === "config" && existsSync(root) && statSync(root).isDirectory()) {
14
32
  return root;
@@ -236,6 +254,36 @@ function cloneConfigValue(value) {
236
254
  }
237
255
  return value;
238
256
  }
257
+ function isModuleRelativePathCandidate(value) {
258
+ return !path.isAbsolute(value) && !isExternalSourceLocator(value) && !value.includes("://");
259
+ }
260
+ function resolveModuleRelativePath(value, moduleRoot) {
261
+ if (!moduleRoot || !isModuleRelativePathCandidate(value)) {
262
+ return value;
263
+ }
264
+ return path.resolve(moduleRoot, value);
265
+ }
266
+ function normalizeModulePromptConfig(value, moduleRoot) {
267
+ if (!moduleRoot || typeof value !== "object" || value === null || Array.isArray(value)) {
268
+ return value;
269
+ }
270
+ const typed = { ...value };
271
+ if (typeof typed.path === "string") {
272
+ typed.path = resolveModuleRelativePath(typed.path, moduleRoot);
273
+ }
274
+ return typed;
275
+ }
276
+ function normalizeModuleAgentConfig(config, moduleRoot) {
277
+ if (!moduleRoot) {
278
+ return config;
279
+ }
280
+ return {
281
+ ...config,
282
+ ...(config.systemPrompt !== undefined
283
+ ? { systemPrompt: normalizeModulePromptConfig(config.systemPrompt, moduleRoot) }
284
+ : {}),
285
+ };
286
+ }
239
287
  function readPassthroughConfig(item, consumedKeys) {
240
288
  const passthrough = Object.fromEntries(Object.entries(item)
241
289
  .filter(([key]) => !consumedKeys.includes(key))
@@ -277,7 +325,10 @@ function readExecutionObjectArray(item, key) {
277
325
  function readSharedAgentConfig(config) {
278
326
  const middleware = readMiddlewareArray(config.middleware);
279
327
  return {
280
- ...(typeof config.systemPrompt === "string" ? { systemPrompt: config.systemPrompt } : {}),
328
+ ...((typeof config.systemPrompt === "string" && config.systemPrompt)
329
+ || (typeof config.systemPrompt === "object" && config.systemPrompt && !Array.isArray(config.systemPrompt))
330
+ ? { systemPrompt: cloneConfigValue(config.systemPrompt) }
331
+ : {}),
281
332
  ...((typeof config.checkpointer === "object" && config.checkpointer) || typeof config.checkpointer === "boolean"
282
333
  ? { checkpointer: config.checkpointer }
283
334
  : {}),
@@ -349,8 +400,9 @@ function readDeepAgentConfig(item) {
349
400
  };
350
401
  }
351
402
  export function parseAgentItem(item, sourcePath) {
403
+ const moduleRoot = moduleRootForSourcePath(sourcePath, "agents");
352
404
  const subagentRefs = readExecutionRefArray(item, "subagents");
353
- const subagentPathRefs = readExecutionPathArray(item, "subagents");
405
+ const subagentPathRefs = readExecutionPathArray(item, "subagents").map((entry) => resolveModuleRelativePath(entry, moduleRoot));
354
406
  const executionMode = String(resolveExecutionBackend(item) ?? "deepagent");
355
407
  const runtime = readRuntimeConfig(item);
356
408
  return {
@@ -364,12 +416,12 @@ export function parseAgentItem(item, sourcePath) {
364
416
  runRoot: typeof runtime?.runRoot === "string" ? runtime.runRoot : undefined,
365
417
  toolRefs: readExecutionRefArray(item, "tools"),
366
418
  mcpServers: readExecutionObjectArray(item, "mcpServers"),
367
- skillPathRefs: readExecutionPathArray(item, "skills"),
368
- memorySources: readExecutionPathArray(item, "memory"),
419
+ skillPathRefs: readExecutionPathArray(item, "skills").map((entry) => resolveModuleRelativePath(entry, moduleRoot)),
420
+ memorySources: readExecutionPathArray(item, "memory").map((entry) => resolveModuleRelativePath(entry, moduleRoot)),
369
421
  subagentRefs,
370
422
  subagentPathRefs,
371
- langchainAgentConfig: readLangchainAgentConfig(item),
372
- deepAgentConfig: readDeepAgentConfig(item),
423
+ langchainAgentConfig: normalizeModuleAgentConfig(readLangchainAgentConfig(item), moduleRoot),
424
+ deepAgentConfig: normalizeModuleAgentConfig(readDeepAgentConfig(item), moduleRoot),
373
425
  sourcePath,
374
426
  };
375
427
  }
@@ -465,6 +517,23 @@ async function loadConfigAgentsForRoot(configRoot, mergedAgents) {
465
517
  mergeAgentRecord(mergedAgents, item, sourcePath);
466
518
  }
467
519
  }
520
+ async function loadModuleAgentsForRoot(root, mergedAgents) {
521
+ const modulesRoot = moduleCollectionRoot(root, "agents");
522
+ if (!(await fileExists(modulesRoot))) {
523
+ return;
524
+ }
525
+ const entries = await readdir(modulesRoot, { withFileTypes: true });
526
+ for (const entry of entries.filter((candidate) => candidate.isDirectory()).sort((left, right) => left.name.localeCompare(right.name))) {
527
+ const moduleRoot = path.join(modulesRoot, entry.name);
528
+ for (const { item, sourcePath } of await readNamedYamlItems(moduleRoot, [...MODULE_AGENT_FILENAMES])) {
529
+ const normalizedItem = typeof item.id === "string" && item.id.trim() ? item : { ...item, id: entry.name };
530
+ if (!isAgentKind(normalizedItem.kind)) {
531
+ continue;
532
+ }
533
+ mergeAgentRecord(mergedAgents, normalizedItem, sourcePath);
534
+ }
535
+ }
536
+ }
468
537
  async function loadConventionalObjectsForRoot(root, mergedObjects) {
469
538
  for (const directory of CONVENTIONAL_OBJECT_DIRECTORIES) {
470
539
  for (const objectRoot of conventionalDirectoryRoots(root, directory)) {
@@ -485,6 +554,61 @@ async function loadConventionalObjectsForRoot(root, mergedObjects) {
485
554
  }
486
555
  }
487
556
  }
557
+ async function readModuleToolItems(root) {
558
+ const modulesRoot = moduleCollectionRoot(root, "tools");
559
+ if (!(await fileExists(modulesRoot))) {
560
+ return [];
561
+ }
562
+ const entries = await readdir(modulesRoot, { withFileTypes: true });
563
+ const records = [];
564
+ for (const entry of entries.filter((candidate) => candidate.isDirectory()).sort((left, right) => left.name.localeCompare(right.name))) {
565
+ const moduleRoot = path.join(modulesRoot, entry.name);
566
+ for (const { item, sourcePath } of await readNamedYamlItems(moduleRoot, [...MODULE_TOOL_FILENAMES])) {
567
+ const normalizedItem = typeof item.id === "string" && item.id.trim() ? item : { ...item, id: entry.name };
568
+ const workspaceObject = parseWorkspaceObject(normalizedItem, sourcePath);
569
+ if (!workspaceObject || workspaceObject.kind !== "tool") {
570
+ continue;
571
+ }
572
+ const implementation = asObject(normalizedItem.implementation);
573
+ const explicitPath = typeof implementation?.path === "string" ? path.resolve(moduleRoot, implementation.path) : undefined;
574
+ const discoveredPath = explicitPath ??
575
+ MODULE_TOOL_ENTRY_FILENAMES.map((filename) => path.join(moduleRoot, filename)).find((candidate) => existsSync(candidate));
576
+ const inferredType = typeof normalizedItem.type === "string"
577
+ ? normalizedItem.type
578
+ : normalizedItem.refs !== undefined || normalizedItem.bundle !== undefined
579
+ ? "bundle"
580
+ : normalizedItem.providerTool !== undefined || normalizedItem.provider !== undefined
581
+ ? "provider"
582
+ : normalizedItem.backend !== undefined || normalizedItem.operation !== undefined
583
+ ? "backend"
584
+ : normalizedItem.mcp !== undefined
585
+ ? "mcp"
586
+ : "function";
587
+ if (inferredType === "function" && !discoveredPath) {
588
+ throw new Error(`Module tool ${workspaceObject.id} must define implementation.path or provide index.mjs|index.js|index.cjs`);
589
+ }
590
+ records.push({
591
+ item: {
592
+ ...normalizedItem,
593
+ ...(typeof implementation?.export === "string" && !normalizedItem.implementationName
594
+ ? { implementationName: implementation.export }
595
+ : {}),
596
+ },
597
+ sourcePath: discoveredPath ?? sourcePath,
598
+ });
599
+ }
600
+ }
601
+ return records;
602
+ }
603
+ async function loadModuleObjectsForRoot(root, mergedObjects) {
604
+ for (const { item, sourcePath } of await readModuleToolItems(root)) {
605
+ const workspaceObject = parseWorkspaceObject(item, sourcePath);
606
+ if (!workspaceObject) {
607
+ continue;
608
+ }
609
+ mergeWorkspaceObjectRecord(mergedObjects, workspaceObject, item, sourcePath);
610
+ }
611
+ }
488
612
  async function loadConfigObjectsForRoot(root, configRoot, mergedObjects) {
489
613
  if (!conventionalConfigRoot(root)) {
490
614
  return;
@@ -645,8 +769,10 @@ export async function loadWorkspaceObjects(workspaceRoot, options = {}) {
645
769
  const configRoot = conventionalConfigRoot(root) ?? root;
646
770
  await loadNamedModelsForRoot(configRoot, mergedObjects);
647
771
  await loadConfigAgentsForRoot(configRoot, mergedAgents);
772
+ await loadModuleAgentsForRoot(root, mergedAgents);
648
773
  await loadConventionalObjectsForRoot(root, mergedObjects);
649
774
  await loadConfigObjectsForRoot(root, configRoot, mergedObjects);
775
+ await loadModuleObjectsForRoot(root, mergedObjects);
650
776
  await loadRootObjects(root, mergedObjects);
651
777
  }
652
778
  const agents = Array.from(mergedAgents.values()).map(({ item, sourcePath }) => parseAgentItem(item, sourcePath));
@@ -19,6 +19,9 @@ export type RecoveryConfig = {
19
19
  };
20
20
  export type ConcurrencyConfig = {
21
21
  maxConcurrentRuns: number;
22
+ leaseMs: number;
23
+ heartbeatIntervalMs: number;
24
+ heartbeatTimeoutMs: number;
22
25
  };
23
26
  export type ProviderRetryConfig = {
24
27
  maxAttempts: number;
@@ -1,3 +1,4 @@
1
+ import { readFileSync } from "node:fs";
1
2
  import path from "node:path";
2
3
  function getRoutingObject(refs) {
3
4
  const runtimeDefaults = getRuntimeDefaults(refs);
@@ -63,7 +64,27 @@ export function getConcurrencyConfig(refs) {
63
64
  concurrency.maxConcurrentRuns > 0
64
65
  ? Math.floor(concurrency.maxConcurrentRuns)
65
66
  : 3;
66
- return { maxConcurrentRuns };
67
+ const leaseMs = typeof concurrency.leaseMs === "number" &&
68
+ Number.isFinite(concurrency.leaseMs) &&
69
+ concurrency.leaseMs > 0
70
+ ? Math.floor(concurrency.leaseMs)
71
+ : 30_000;
72
+ const heartbeatIntervalMs = typeof concurrency.heartbeatIntervalMs === "number" &&
73
+ Number.isFinite(concurrency.heartbeatIntervalMs) &&
74
+ concurrency.heartbeatIntervalMs > 0
75
+ ? Math.floor(concurrency.heartbeatIntervalMs)
76
+ : 5_000;
77
+ const heartbeatTimeoutMs = typeof concurrency.heartbeatTimeoutMs === "number" &&
78
+ Number.isFinite(concurrency.heartbeatTimeoutMs) &&
79
+ concurrency.heartbeatTimeoutMs > 0
80
+ ? Math.floor(concurrency.heartbeatTimeoutMs)
81
+ : Math.max(leaseMs, heartbeatIntervalMs * 3);
82
+ return {
83
+ maxConcurrentRuns,
84
+ leaseMs,
85
+ heartbeatIntervalMs,
86
+ heartbeatTimeoutMs,
87
+ };
67
88
  }
68
89
  export function getResilienceConfig(refs) {
69
90
  const runtimeDefaults = getRuntimeDefaults(refs);
@@ -210,6 +231,14 @@ export function resolvePromptValue(promptConfig) {
210
231
  if (typeof promptConfig === "string" && promptConfig.trim()) {
211
232
  return promptConfig;
212
233
  }
234
+ if (typeof promptConfig === "object" && promptConfig !== null && !Array.isArray(promptConfig)) {
235
+ const promptPath = typeof promptConfig.path === "string"
236
+ ? promptConfig.path
237
+ : undefined;
238
+ if (promptPath?.trim()) {
239
+ return readFileSync(promptPath, "utf8");
240
+ }
241
+ }
213
242
  return undefined;
214
243
  }
215
244
  export function resolveRefId(ref) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.79",
3
+ "version": "0.0.81",
4
4
  "description": "Workspace runtime for multi-agent applications",
5
5
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",
@@ -53,7 +53,7 @@
53
53
  "scripts": {
54
54
  "build": "rm -rf dist tsconfig.tsbuildinfo && tsc -p tsconfig.json && cp -R config dist/",
55
55
  "check": "tsc -p tsconfig.json --noEmit",
56
- "test": "vitest run test/hello-file.test.ts test/public-api.test.ts test/runtime-health.test.ts test/memory-runtime.test.ts test/sqlite-persistence.test.ts test/runtime-record-maintenance.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/stock-research-app-config.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/product-boundary-docs.test.ts test/long-term-memory-docs.test.ts test/local-docs-persistence-inventory.test.ts test/docs-site.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 test/config-secrets.test.ts test/init-command.test.ts test/coding-agent-guide.test.ts",
56
+ "test": "vitest run test/hello-file.test.ts test/public-api.test.ts test/runtime-health.test.ts test/memory-runtime.test.ts test/sqlite-persistence.test.ts test/runtime-queue-lease.test.ts test/runtime-cancel.test.ts test/runtime-record-maintenance.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/stock-research-app-config.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/product-boundary-docs.test.ts test/long-term-memory-docs.test.ts test/local-docs-persistence-inventory.test.ts test/docs-site.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 test/config-secrets.test.ts test/init-command.test.ts test/coding-agent-guide.test.ts",
57
57
  "test:real-providers": "vitest run test/real-provider-harness.test.ts",
58
58
  "release:prepare": "npm version patch --no-git-tag-version && node ./scripts/sync-example-version.mjs",
59
59
  "release:pack": "npm pack --dry-run",