@botbotgo/agent-harness 0.0.307 → 0.0.308

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
@@ -469,9 +469,9 @@ import { AgentHarnessRuntime, createAgentHarness } from "@botbotgo/agent-harness
469
469
  const runtime: AgentHarnessRuntime = await createAgentHarness("/absolute/path/to/workspace");
470
470
  ```
471
471
 
472
- `createAgentHarness(...)` loads the workspace, resolves `resources/`, initializes persistence under `runtimeRoot`, and starts runtime maintenance.
472
+ `createAgentHarness(...)` loads the workspace, resolves workspace sources, initializes persistence under `runtimeRoot`, and starts runtime maintenance.
473
473
 
474
- `runtime.spec.resources` may attach multiple extra resource packages. Each entry may point either to a package root that contains `resources/`, or directly to a resource folder that contains its own `package.json`.
474
+ `runtime.spec.sources` is the primary public discovery surface for local tools, package tools, and skill packages.
475
475
 
476
476
  ```yaml
477
477
  apiVersion: agent-harness/v1alpha1
@@ -479,11 +479,29 @@ kind: Runtime
479
479
  metadata:
480
480
  name: default
481
481
  spec:
482
- resources:
483
- - ../shared-tools
484
- - file:../shared-skills
482
+ sources:
483
+ tools:
484
+ - file://./resources/tools
485
+ - file://../shared-tools
486
+ - npm://@acme/agent-tools
487
+ skills:
488
+ - file://./resources/skills
489
+ - https://example.com/skills/review/SKILL.md
485
490
  ```
486
491
 
492
+ Tool-source rules:
493
+
494
+ - `file://...` scans only the configured folder
495
+ - `npm://...` resolves one package, auto-installs it when missing, and discovers exported `tool({...})` definitions from the package entry
496
+ - tool discovery never traverses `node_modules/**`
497
+
498
+ Skill-source rules:
499
+
500
+ - `file://...` accepts a skill collection folder, a single skill root, or a direct `SKILL.md` path
501
+ - `http://...` and `https://...` currently accept a single remote `SKILL.md`
502
+
503
+ `runtime.spec.resources` remains supported as a compatibility path for attached resource packages.
504
+
487
505
  `createAgentHarness(..., { load })` accepts workspace loading controls.
488
506
 
489
507
  Merge order is deterministic:
@@ -739,8 +757,10 @@ Discovery rules:
739
757
 
740
758
  - every YAML document under `config/**` is discovered recursively; filenames and subfolders are organizational only
741
759
  - YAML object semantics come from `kind`, `metadata.name` or `id`, and object content rather than the file path
742
- - local tools are auto-discovered from `resources/tools/**/*.js|*.mjs|*.cjs` when they export `tool({...})`
743
- - skills are auto-discovered from `resources/skills/**/SKILL.md`
760
+ - `Runtime.spec.sources.tools` defaults to `file://./resources/tools`
761
+ - `Runtime.spec.sources.skills` defaults to `file://./resources/skills`
762
+ - local file-based tools are auto-discovered from each configured tool folder when modules export `tool({...})`
763
+ - file-based skills are auto-discovered from each configured skill source
744
764
  - a minimal workspace can start with only `config/models.yaml`; the repository defaults provide the `Runtime`, the default `orchestra` host, and runtime-managed durable memory with `enabled: true`
745
765
  - when you do not override runtime placement, harness-owned generated state is written under `./.botbotgo/`
746
766
 
@@ -768,7 +788,9 @@ For DeepAgents-backed agents, the runtime still keeps an internal compatibility
768
788
 
769
789
  Default wiring guidance:
770
790
 
771
- - let workspace startup scan local and attached `resources` packages into one registry
791
+ - let `Runtime.spec.sources` declare the tool and skill roots the workspace owns
792
+ - let workspace startup scan only those declared sources into one registry
793
+ - let workspace startup scan local and attached `resources` packages into one registry when compatibility paths are still in use
772
794
  - let agents whitelist tools and skills by name
773
795
  - keep `config/catalogs/tools.yaml` for reusable shared tools
774
796
  - keep `config/catalogs/mcp.yaml` for shared MCP server definitions
@@ -810,6 +832,8 @@ Important fields:
810
832
 
811
833
  - `runtimeRoot`
812
834
  - `concurrency.maxConcurrentRequests`
835
+ - `sources.tools`
836
+ - `sources.skills`
813
837
  - `routing.defaultAgentId`
814
838
  - `routing.rules`
815
839
  - `toolModuleDiscovery.scope`
@@ -826,7 +850,17 @@ Important fields:
826
850
  - `maintenance.checkpoints` trims backend checkpoint state used for resume/recovery
827
851
  - `maintenance.records` trims harness-owned terminal session/request records stored in `runtime.sqlite`
828
852
 
829
- `toolModuleDiscovery.scope` controls how local `resources/tools/`-style module discovery walks tool directories:
853
+ `sources.tools` controls which tool roots or packages participate in workspace discovery:
854
+
855
+ - `file://...` for folder scanning
856
+ - `npm://...` for package-entry discovery and auto-install when missing
857
+
858
+ `sources.skills` controls which skill folders or skill documents participate in workspace discovery:
859
+
860
+ - `file://...` for local folders, skill roots, or direct `SKILL.md`
861
+ - `http://...` / `https://...` for one remote `SKILL.md`
862
+
863
+ `toolModuleDiscovery.scope` controls how local `resources/tools/`-style file discovery walks tool directories:
830
864
 
831
865
  - `recursive` is the default and keeps scanning nested folders
832
866
  - `top-level` limits module discovery to files directly under each tool root while leaving YAML catalogs recursive
package/README.zh.md CHANGED
@@ -464,9 +464,9 @@ import { AgentHarnessRuntime, createAgentHarness } from "@botbotgo/agent-harness
464
464
  const runtime: AgentHarnessRuntime = await createAgentHarness("/absolute/path/to/workspace");
465
465
  ```
466
466
 
467
- `createAgentHarness(...)` 会加载工作区、解析 `resources/`、在 `runtimeRoot` 下初始化持久化,并启动运行时维护任务。
467
+ `createAgentHarness(...)` 会加载工作区、解析工作区 source、在 `runtimeRoot` 下初始化持久化,并启动运行时维护任务。
468
468
 
469
- `runtime.spec.resources` 可以挂载多个额外资源包。每个条目既可以指向“包根目录下带有 `resources/` 的目录”,也可以直接指向“自身带有 `package.json` 的资源目录”。
469
+ `runtime.spec.sources` 现在是本地工具、npm 工具包与 skill package 的主公开发现入口。
470
470
 
471
471
  ```yaml
472
472
  apiVersion: agent-harness/v1alpha1
@@ -474,11 +474,29 @@ kind: Runtime
474
474
  metadata:
475
475
  name: default
476
476
  spec:
477
- resources:
478
- - ../shared-tools
479
- - file:../shared-skills
477
+ sources:
478
+ tools:
479
+ - file://./resources/tools
480
+ - file://../shared-tools
481
+ - npm://@acme/agent-tools
482
+ skills:
483
+ - file://./resources/skills
484
+ - https://example.com/skills/review/SKILL.md
480
485
  ```
481
486
 
487
+ tool source 规则:
488
+
489
+ - `file://...` 只扫描显式配置的目录
490
+ - `npm://...` 只解析单个包;本地缺失时会自动安装,再从包入口发现导出的 `tool({...})`
491
+ - tool 发现永远不会遍历 `node_modules/**`
492
+
493
+ skill source 规则:
494
+
495
+ - `file://...` 可以指向 skill 集合目录、单个 skill root,或直接指向 `SKILL.md`
496
+ - `http://...` 与 `https://...` 当前只支持一个远程 `SKILL.md`
497
+
498
+ `runtime.spec.resources` 仍保留为兼容旧 attached resource package 的路径。
499
+
482
500
  `createAgentHarness(..., { load })` 支持工作区加载控制。
483
501
 
484
502
  合并顺序是确定性的:
@@ -710,8 +728,10 @@ await stop(runtime);
710
728
 
711
729
  - `config/**` 下的所有 YAML 文档都会被递归发现;文件名与子目录只用于组织,不参与语义
712
730
  - YAML 对象语义由 `kind`、`metadata.name` 或 `id` 以及对象内容决定,而不是由文件路径决定
713
- - 本地工具会从 `resources/tools/**/*.js|*.mjs|*.cjs` 中自动发现,前提是模块导出 `tool({...})`
714
- - skills 会从 `resources/skills/**/SKILL.md` 自动发现
731
+ - `Runtime.spec.sources.tools` 默认值是 `file://./resources/tools`
732
+ - `Runtime.spec.sources.skills` 默认值是 `file://./resources/skills`
733
+ - file-based 工具会从每个声明的 tool source 自动发现,前提是模块导出 `tool({...})`
734
+ - file-based skills 会从每个声明的 skill source 自动发现
715
735
  - 一个最小工作区只放 `config/models.yaml` 也可以启动;仓库默认值会补上 `Runtime`、默认 `orchestra` host,以及默认开启的 runtime-managed durable memory
716
736
  - 如果不显式覆盖 runtime 放置位置,harness 生成的数据默认写到 `./.botbotgo/`
717
737
 
@@ -740,7 +760,9 @@ await stop(runtime);
740
760
 
741
761
  配置大致分这几层(由下至上叠加):
742
762
 
743
- - workspace 启动时扫描本地与附加的 `resources` 包,建立统一 registry
763
+ - 先由 `Runtime.spec.sources` 声明工作区实际拥有的 tool / skill 来源
764
+ - workspace 启动时只扫描这些显式声明的 source,并建立统一 registry
765
+ - workspace 启动时扫描本地与附加的 `resources` 包,建立统一 registry;这条兼容路径仍可继续工作
744
766
  - 各 agent 再按名称白名单选用 tools 与 skills
745
767
  - `config/runtime/workspace.yaml` 承载运行时策略
746
768
  - `config/catalogs/*.yaml` 承载可复用对象目录
@@ -776,6 +798,8 @@ await stop(runtime);
776
798
 
777
799
  - `runtimeRoot`
778
800
  - `concurrency.maxConcurrentRequests`
801
+ - `sources.tools`
802
+ - `sources.skills`
779
803
  - `routing.defaultAgentId`
780
804
  - `routing.rules`
781
805
  - `routing.systemPrompt`
@@ -793,7 +817,17 @@ await stop(runtime);
793
817
  - `maintenance.checkpoints` 清理后端用于 resume/recovery 的 checkpoint 状态
794
818
  - `maintenance.records` 清理 harness 自己保存在 `runtime.sqlite` 中、已结束的 session/request 记录
795
819
 
796
- `toolModuleDiscovery.scope` 用来控制本地 `resources/tools/` 这类 tool module 目录的发现范围:
820
+ `sources.tools` 用来声明哪些 tool root package 参与工作区发现:
821
+
822
+ - `file://...` 用于目录扫描
823
+ - `npm://...` 用于包入口发现;本地缺失时自动安装
824
+
825
+ `sources.skills` 用来声明哪些 skill 目录或 skill 文档参与工作区发现:
826
+
827
+ - `file://...` 用于本地目录、skill root 或直接 `SKILL.md`
828
+ - `http://...` / `https://...` 用于单个远程 `SKILL.md`
829
+
830
+ `toolModuleDiscovery.scope` 用来控制本地 `resources/tools/` 风格 file source 的发现范围:
797
831
 
798
832
  - `recursive` 是默认值,会继续扫描嵌套目录
799
833
  - `top-level` 只发现每个工具根目录下一层文件,同时保留 YAML catalog 的递归发现
@@ -29,6 +29,22 @@ spec:
29
29
  # agent-harness feature: stable runtime profile identifier for this data folder.
30
30
  profile: default
31
31
 
32
+ # agent-harness feature: explicit tool and skill discovery sources.
33
+ # The default local workspace contract stays:
34
+ # - tools from ./resources/tools
35
+ # - skills from ./resources/skills
36
+ #
37
+ # Supported source forms today:
38
+ # - tools: file://<folder>, npm://<package-or-spec>
39
+ # - skills: file://<folder-or-SKILL.md>, http(s)://.../SKILL.md
40
+ #
41
+ # Discovery never traverses node_modules directories.
42
+ sources:
43
+ tools:
44
+ - file://./resources/tools
45
+ skills:
46
+ - file://./resources/skills
47
+
32
48
  # agent-harness feature: runtime-level task queue and maximum number of concurrent requests.
33
49
  # Additional requests wait in the harness queue until a slot becomes available.
34
50
  concurrency:
@@ -1 +1 @@
1
- export declare const AGENT_HARNESS_VERSION = "0.0.306";
1
+ export declare const AGENT_HARNESS_VERSION = "0.0.307";
@@ -1 +1 @@
1
- export const AGENT_HARNESS_VERSION = "0.0.306";
1
+ export const AGENT_HARNESS_VERSION = "0.0.307";
@@ -93,7 +93,9 @@ async function linkHarnessPackage(isolatedRoot) {
93
93
  }
94
94
  async function buildIsolatedResourceRoot(packageRoot) {
95
95
  const packageJsonPath = path.join(packageRoot, "package.json");
96
- const manifest = JSON.parse(await readFile(packageJsonPath, "utf8"));
96
+ const manifest = existsSync(packageJsonPath)
97
+ ? JSON.parse(await readFile(packageJsonPath, "utf8"))
98
+ : {};
97
99
  const isolatedRoot = createIsolatedSnapshotDir(packageRoot);
98
100
  await mkdir(path.dirname(isolatedRoot), { recursive: true });
99
101
  await cp(packageRoot, isolatedRoot, {
@@ -101,6 +103,9 @@ async function buildIsolatedResourceRoot(packageRoot) {
101
103
  force: true,
102
104
  filter: (source) => path.basename(source) !== "node_modules",
103
105
  });
106
+ if (!existsSync(path.join(isolatedRoot, "package.json"))) {
107
+ await cp(path.join(HARNESS_PACKAGE_ROOT, "resources", "package.json"), path.join(isolatedRoot, "package.json"));
108
+ }
104
109
  await mkdir(path.join(isolatedRoot, "node_modules"), { recursive: true });
105
110
  await linkDeclaredDependencies(isolatedRoot, packageRoot, manifest);
106
111
  await linkHarnessPackage(isolatedRoot);
@@ -2,6 +2,7 @@ import type { CompiledAgentBinding, MessageContent, TranscriptMessage } from "..
2
2
  export declare function buildAgentMessages(history: TranscriptMessage[], input: MessageContent, options?: {
3
3
  suppressExplicitResourceTurns?: boolean;
4
4
  suppressAssistantTurns?: boolean;
5
+ suppressHistoryTurns?: boolean;
5
6
  }): Array<{
6
7
  role: string;
7
8
  content: MessageContent;
@@ -84,6 +84,9 @@ function selectRelevantHistoryTurns(groupedHistory, inputText, options = {}) {
84
84
  }
85
85
  export function buildAgentMessages(history, input, options = {}) {
86
86
  const inputText = extractMessageText(input).trim();
87
+ if (options.suppressHistoryTurns) {
88
+ return [{ role: "user", content: normalizeMessageContent(input) }];
89
+ }
87
90
  const groupedHistory = history.reduce((groups, item) => {
88
91
  const current = groups.at(-1);
89
92
  if (current && current[0]?.requestId === item.requestId) {
@@ -156,6 +159,7 @@ export function buildInvocationRequest(binding, history, input, options = {}) {
156
159
  const messages = buildAgentMessages(history, input, {
157
160
  suppressExplicitResourceTurns: Boolean(memoryInstruction) && !hasExplicitResourceReference(inputText),
158
161
  suppressAssistantTurns: Boolean(memoryInstruction) && !hasExplicitResourceReference(inputText),
162
+ suppressHistoryTurns: Boolean(memoryInstruction) && !hasExplicitResourceReference(inputText),
159
163
  });
160
164
  const contextualFollowUpInstruction = buildContextualFollowUpInstruction(inputText, Boolean(memoryInstruction));
161
165
  const conversationLanguage = resolveConversationLanguage(history, inputText);
@@ -1,6 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { existsSync } from "node:fs";
3
3
  import { readdir } from "node:fs/promises";
4
+ import { pathToFileURL } from "node:url";
4
5
  import { ensureResourceSources } from "../resource/resource.js";
5
6
  import { ensureExternalResourceSource, isExternalSourceLocator, resolveResourcePackageRoot } from "../resource/sources.js";
6
7
  import { loadWorkspaceObjects, readToolModuleItems, readYamlItems } from "./object-loader.js";
@@ -10,10 +11,30 @@ import { validateAgent, validateTopology } from "./validate.js";
10
11
  import { compileBinding } from "./agent-binding-compiler.js";
11
12
  import { discoverSubagents, ensureDiscoverySources } from "./support/discovery.js";
12
13
  import { collectAgentDiscoverySourceRefs, collectToolSourceRefs } from "./support/source-collectors.js";
13
- import { getRoutingDefaultAgentId, getRuntimeResources, getRuntimeStorageRoots, getToolModuleDiscoveryConfig, getRoutingRules, resolveRefId, } from "./support/workspace-ref-utils.js";
14
+ import { getRoutingDefaultAgentId, getRuntimeSources, getRuntimeResources, getRuntimeStorageRoots, getToolModuleDiscoveryConfig, getRoutingRules, resolveRefId, } from "./support/workspace-ref-utils.js";
14
15
  import { hydrateAgentMcpTools, hydrateResourceAndExternalTools } from "./tool-hydration.js";
15
16
  import { traceStartupStage } from "../runtime/startup-tracing.js";
16
17
  import { shouldSkipScanDirectory } from "../utils/fs.js";
18
+ import { ensureRemoteSkillSource, ensureToolPackageSource, isFileSourceUri, isHttpSourceUri, isNpmSourceUri, resolveFileSourcePath, } from "./support/source-protocols.js";
19
+ import { discoverToolModuleDefinitions } from "../tool-modules.js";
20
+ function mergeObjectValues(base, override) {
21
+ if (override === undefined) {
22
+ return base;
23
+ }
24
+ if (typeof base === "object" &&
25
+ base &&
26
+ typeof override === "object" &&
27
+ override &&
28
+ !Array.isArray(base) &&
29
+ !Array.isArray(override)) {
30
+ const merged = { ...base };
31
+ for (const [key, value] of Object.entries(override)) {
32
+ merged[key] = key in merged ? mergeObjectValues(merged[key], value) : value;
33
+ }
34
+ return merged;
35
+ }
36
+ return override;
37
+ }
17
38
  function collectParsedResources(refs) {
18
39
  const embeddings = new Map();
19
40
  const mcpServers = new Map();
@@ -132,7 +153,7 @@ async function registerAttachedResourceTools(tools, resourceRoot, toolModuleDisc
132
153
  sourcePath,
133
154
  value: item,
134
155
  });
135
- tools.set(parsed.id, parsed);
156
+ tools.set(parsed.id, mergeParsedToolObject(tools.get(parsed.id), parsed));
136
157
  }
137
158
  for (const { item, sourcePath } of await readToolModuleItems(toolsRoot, { scope: toolModuleDiscoveryScope })) {
138
159
  const parsed = parseToolObject({
@@ -141,7 +162,7 @@ async function registerAttachedResourceTools(tools, resourceRoot, toolModuleDisc
141
162
  sourcePath,
142
163
  value: item,
143
164
  });
144
- tools.set(parsed.id, parsed);
165
+ tools.set(parsed.id, mergeParsedToolObject(tools.get(parsed.id), parsed));
145
166
  }
146
167
  }
147
168
  async function collectSkillRoots(root) {
@@ -160,6 +181,90 @@ async function collectSkillRoots(root) {
160
181
  return [];
161
182
  }
162
183
  }
184
+ async function resolveConfiguredSkillSourceRoots(sources, workspaceRoot) {
185
+ const resolved = [];
186
+ for (const source of sources) {
187
+ if (isFileSourceUri(source)) {
188
+ const resolvedPath = resolveFileSourcePath(source, workspaceRoot);
189
+ resolved.push(resolvedPath.endsWith(`${path.sep}SKILL.md`) || resolvedPath.endsWith("/SKILL.md")
190
+ ? path.dirname(resolvedPath)
191
+ : resolvedPath);
192
+ continue;
193
+ }
194
+ if (isHttpSourceUri(source)) {
195
+ resolved.push(await ensureRemoteSkillSource(source));
196
+ continue;
197
+ }
198
+ throw new Error(`Unsupported skill source ${source}. Use file:// or https://.`);
199
+ }
200
+ return resolved;
201
+ }
202
+ async function registerToolFolderSource(tools, folderRoot, toolModuleDiscoveryScope) {
203
+ for (const { item, sourcePath } of await readYamlItems(folderRoot, undefined, { recursive: true })) {
204
+ const parsed = parseToolObject({
205
+ id: typeof item.id === "string" ? item.id : path.basename(sourcePath).replace(/\.(yaml|yml|json)$/i, ""),
206
+ kind: "tool",
207
+ sourcePath,
208
+ value: item,
209
+ });
210
+ tools.set(parsed.id, mergeParsedToolObject(tools.get(parsed.id), parsed));
211
+ }
212
+ for (const { item, sourcePath } of await readToolModuleItems(folderRoot, { scope: toolModuleDiscoveryScope })) {
213
+ const parsed = parseToolObject({
214
+ id: String(item.id),
215
+ kind: "tool",
216
+ sourcePath,
217
+ value: item,
218
+ });
219
+ tools.set(parsed.id, mergeParsedToolObject(tools.get(parsed.id), parsed));
220
+ }
221
+ }
222
+ async function registerToolPackageSource(tools, source, workspaceRoot) {
223
+ const installed = await ensureToolPackageSource(source, workspaceRoot);
224
+ const imported = await import(pathToFileURL(installed.entryPath).href);
225
+ const definitions = discoverToolModuleDefinitions("", imported);
226
+ for (const definition of definitions) {
227
+ const parsed = parseToolObject({
228
+ id: definition.implementationName,
229
+ kind: "tool",
230
+ sourcePath: installed.entryPath,
231
+ value: {
232
+ kind: "tool",
233
+ id: definition.implementationName,
234
+ type: "function",
235
+ name: definition.implementationName,
236
+ description: definition.description,
237
+ implementationName: definition.implementationName,
238
+ hasModuleSchema: definition.hasModuleSchema,
239
+ ...(definition.modelSchema ? { modelSchema: definition.modelSchema } : {}),
240
+ ...(definition.retryable !== undefined ? { retryable: definition.retryable } : {}),
241
+ ...(definition.memory ? { config: { memory: definition.memory } } : {}),
242
+ },
243
+ });
244
+ tools.set(parsed.id, mergeParsedToolObject(tools.get(parsed.id), parsed));
245
+ }
246
+ }
247
+ function mergeParsedToolObject(current, incoming) {
248
+ if (!current) {
249
+ return incoming;
250
+ }
251
+ return {
252
+ ...current,
253
+ ...incoming,
254
+ config: mergeObjectValues(current.config, incoming.config),
255
+ subprocess: incoming.subprocess ?? current.subprocess,
256
+ inputSchemaRef: incoming.inputSchemaRef ?? current.inputSchemaRef,
257
+ hasModuleSchema: incoming.hasModuleSchema ?? current.hasModuleSchema,
258
+ modelSchema: incoming.modelSchema ?? current.modelSchema,
259
+ embeddingModelRef: incoming.embeddingModelRef ?? current.embeddingModelRef,
260
+ backendOperation: incoming.backendOperation ?? current.backendOperation,
261
+ mcpRef: incoming.mcpRef ?? current.mcpRef,
262
+ bundleRefs: incoming.bundleRefs.length > 0 ? incoming.bundleRefs : current.bundleRefs,
263
+ hitl: incoming.hitl ?? current.hitl,
264
+ retryable: incoming.retryable ?? current.retryable,
265
+ sourcePath: incoming.sourcePath,
266
+ };
267
+ }
163
268
  async function registerWorkspaceSkillRegistry(skillCollectionRoots) {
164
269
  const registry = new Map();
165
270
  for (const skillsRoot of skillCollectionRoots) {
@@ -227,10 +332,29 @@ export async function loadWorkspace(workspaceRoot, options = {}) {
227
332
  }
228
333
  const { embeddings, mcpServers, models, vectorStores, tools } = collectParsedResources(loaded.refs);
229
334
  const toolModuleDiscoveryConfig = getToolModuleDiscoveryConfig(loaded.refs);
335
+ const runtimeSources = getRuntimeSources(loaded.refs);
230
336
  await traceStartupStage("workspace.hydrate.agentMcpTools", () => hydrateAgentMcpTools(loaded.agents, mcpServers, tools), {
231
337
  workspaceRoot,
232
338
  agentCount: loaded.agents.length,
233
339
  });
340
+ for (const source of runtimeSources.tools) {
341
+ if (isFileSourceUri(source)) {
342
+ const folderRoot = resolveFileSourcePath(source, workspaceRoot);
343
+ await traceStartupStage("workspace.register.toolFolderSource", () => registerToolFolderSource(tools, folderRoot, toolModuleDiscoveryConfig.scope), {
344
+ workspaceRoot,
345
+ source,
346
+ });
347
+ continue;
348
+ }
349
+ if (isNpmSourceUri(source)) {
350
+ await traceStartupStage("workspace.register.toolPackageSource", () => registerToolPackageSource(tools, source, workspaceRoot), {
351
+ workspaceRoot,
352
+ source,
353
+ });
354
+ continue;
355
+ }
356
+ throw new Error(`Unsupported tool source ${source}. Use file:// or npm://.`);
357
+ }
234
358
  const configuredResources = Array.from(new Set([
235
359
  ...getRuntimeResources(loaded.refs),
236
360
  ...(options.resources ?? []),
@@ -246,11 +370,16 @@ export async function loadWorkspace(workspaceRoot, options = {}) {
246
370
  });
247
371
  }
248
372
  const localResourceRoot = resolveResourcePackageRoot(workspaceRoot);
249
- const skillCollectionRoots = [
373
+ const configuredSkillSourceRoots = await traceStartupStage("workspace.resolve.skillSources", () => resolveConfiguredSkillSourceRoots(runtimeSources.skills, workspaceRoot), {
374
+ workspaceRoot,
375
+ skillSourceCount: runtimeSources.skills.length,
376
+ });
377
+ const skillCollectionRoots = Array.from(new Set([
250
378
  path.join(workspaceRoot, "modules", "skills"),
251
379
  ...(localResourceRoot ? [path.join(localResourceRoot, "skills")] : []),
380
+ ...configuredSkillSourceRoots,
252
381
  ...resolvedConfiguredResources.map((resource) => path.join(resource.root, "skills")),
253
- ];
382
+ ]));
254
383
  const skillRegistry = await traceStartupStage("workspace.register.skillRegistry", () => registerWorkspaceSkillRegistry(skillCollectionRoots), {
255
384
  workspaceRoot,
256
385
  skillCollectionRootCount: skillCollectionRoots.length,
@@ -272,6 +401,7 @@ export async function loadWorkspace(workspaceRoot, options = {}) {
272
401
  validateToolNameConflicts(tools);
273
402
  const resources = Array.from(new Set([
274
403
  ...(localResourceRoot ? [localResourceRoot] : []),
404
+ ...runtimeSources.tools.filter((source) => isNpmSourceUri(source)),
275
405
  ...collectedResources,
276
406
  ]));
277
407
  await traceStartupStage("workspace.validate.resources", async () => {
@@ -6,6 +6,7 @@ import { resolveIsolatedResourceModulePath } from "../resource/isolation.js";
6
6
  import { isExternalSourceLocator, resolveResourcePackageRoot } from "../resource/sources.js";
7
7
  import { discoverToolModuleDefinitions, isSupportedToolModulePath, loadToolModuleDefinition } from "../tool-modules.js";
8
8
  import { fileExists, shouldSkipScanDirectory } from "../utils/fs.js";
9
+ import { isFileSourceUri, readRuntimeSources, resolveFileSourcePath } from "./support/source-protocols.js";
9
10
  import { readNamedYamlItems, readYamlItems, } from "./yaml-object-reader.js";
10
11
  export { normalizeYamlItem, readYamlItems } from "./yaml-object-reader.js";
11
12
  const CONVENTIONAL_OBJECT_DIRECTORIES = ["tools"];
@@ -682,23 +683,32 @@ function getMergedToolModuleDiscoveryScope(mergedObjects) {
682
683
  : {};
683
684
  return toolModuleDiscovery.scope === "top-level" ? "top-level" : "recursive";
684
685
  }
685
- async function loadConventionalObjectsForRoot(root, mergedObjects, toolModuleDiscoveryScope) {
686
- for (const directory of CONVENTIONAL_OBJECT_DIRECTORIES) {
687
- for (const objectRoot of conventionalDirectoryRoots(root, directory)) {
688
- for (const { item, sourcePath } of await readYamlItemsIgnoringNodeModules(objectRoot)) {
689
- const workspaceObject = parseWorkspaceObject(item, sourcePath);
690
- if (!workspaceObject) {
691
- continue;
692
- }
693
- mergeWorkspaceObjectRecord(mergedObjects, workspaceObject, item, sourcePath);
686
+ async function loadConventionalObjectsForRoot(root, runtimeRoot, mergedObjects, toolModuleDiscoveryScope) {
687
+ const runtimeDefaults = mergedObjects.get("runtime/default")?.item;
688
+ const configuredToolRoots = readRuntimeSources(runtimeDefaults).tools
689
+ .filter((source) => isFileSourceUri(source))
690
+ .map((source) => resolveFileSourcePath(source, runtimeRoot));
691
+ const conventionalToolRoots = CONVENTIONAL_OBJECT_DIRECTORIES.flatMap((directory) => conventionalDirectoryRoots(root, directory));
692
+ const objectRoots = root === runtimeRoot
693
+ ? Array.from(new Set([
694
+ ...conventionalToolRoots,
695
+ ...configuredToolRoots,
696
+ ]))
697
+ : conventionalToolRoots;
698
+ for (const objectRoot of objectRoots) {
699
+ for (const { item, sourcePath } of await readYamlItemsIgnoringNodeModules(objectRoot)) {
700
+ const workspaceObject = parseWorkspaceObject(item, sourcePath);
701
+ if (!workspaceObject) {
702
+ continue;
694
703
  }
695
- for (const { item, sourcePath } of await readToolModuleItems(objectRoot, { scope: toolModuleDiscoveryScope })) {
696
- const workspaceObject = parseWorkspaceObject(item, sourcePath);
697
- if (!workspaceObject) {
698
- continue;
699
- }
700
- mergeWorkspaceObjectRecord(mergedObjects, workspaceObject, item, sourcePath);
704
+ mergeWorkspaceObjectRecord(mergedObjects, workspaceObject, item, sourcePath);
705
+ }
706
+ for (const { item, sourcePath } of await readToolModuleItems(objectRoot, { scope: toolModuleDiscoveryScope })) {
707
+ const workspaceObject = parseWorkspaceObject(item, sourcePath);
708
+ if (!workspaceObject) {
709
+ continue;
701
710
  }
711
+ mergeWorkspaceObjectRecord(mergedObjects, workspaceObject, item, sourcePath);
702
712
  }
703
713
  }
704
714
  }
@@ -882,7 +892,7 @@ export async function readToolModuleItems(root, options = {}) {
882
892
  const records = [];
883
893
  for (const filePath of files) {
884
894
  const sourceText = await readFile(filePath, "utf8");
885
- const packageRoot = path.dirname(root);
895
+ const packageRoot = findToolPackageRoot(filePath);
886
896
  const isolatedSourcePath = await resolveIsolatedResourceModulePath(packageRoot, filePath);
887
897
  const imported = await import(pathToFileURL(isolatedSourcePath).href);
888
898
  const definitions = discoverToolModuleDefinitions(sourceText, imported);
@@ -958,7 +968,7 @@ export async function loadWorkspaceObjects(workspaceRoot, options = {}) {
958
968
  await loadConfigYamlForRoot(root, configRoot, mergedAgents, mergedObjects);
959
969
  await loadModuleAgentsForRoot(root, mergedAgents);
960
970
  if (root !== defaultRoot) {
961
- await loadConventionalObjectsForRoot(root, mergedObjects, getMergedToolModuleDiscoveryScope(mergedObjects));
971
+ await loadConventionalObjectsForRoot(root, workspaceRoot, mergedObjects, getMergedToolModuleDiscoveryScope(mergedObjects));
962
972
  }
963
973
  await loadModuleObjectsForRoot(root, mergedObjects);
964
974
  await loadRootObjects(root, mergedObjects);
@@ -302,9 +302,9 @@ export function parseToolObject(object) {
302
302
  ...(mcpServerConfig && Object.keys(mcpServerConfig).length > 0 ? { mcpServer: mcpServerConfig } : {}),
303
303
  }
304
304
  : undefined),
305
- subprocess: value.subprocess === true,
305
+ subprocess: value.subprocess === true ? true : undefined,
306
306
  inputSchemaRef: typeof asObject(value.inputSchema)?.ref === "string" ? String(asObject(value.inputSchema)?.ref) : undefined,
307
- hasModuleSchema: value.hasModuleSchema === true,
307
+ hasModuleSchema: value.hasModuleSchema === true ? true : undefined,
308
308
  modelSchema: asObject(value.modelSchema),
309
309
  embeddingModelRef: typeof value.embeddingModelRef === "string"
310
310
  ? value.embeddingModelRef
@@ -325,7 +325,7 @@ export function parseToolObject(object) {
325
325
  : undefined,
326
326
  bundleRefs,
327
327
  hitl: parseHitlPolicy(value.hitl),
328
- retryable: value.retryable === true,
328
+ retryable: value.retryable === true ? true : undefined,
329
329
  sourcePath: object.sourcePath,
330
330
  };
331
331
  }
@@ -0,0 +1,19 @@
1
+ export declare const DEFAULT_TOOL_SOURCE_URIS: string[];
2
+ export declare const DEFAULT_SKILL_SOURCE_URIS: string[];
3
+ export type RuntimeSourceConfig = {
4
+ tools: string[];
5
+ skills: string[];
6
+ };
7
+ type ToolPackageInstallation = {
8
+ entryPath: string;
9
+ packageRoot: string;
10
+ packageName: string;
11
+ };
12
+ export declare function readRuntimeSources(runtimeDefaults: Record<string, unknown> | undefined): RuntimeSourceConfig;
13
+ export declare function isFileSourceUri(value: string): boolean;
14
+ export declare function isNpmSourceUri(value: string): boolean;
15
+ export declare function isHttpSourceUri(value: string): boolean;
16
+ export declare function resolveFileSourcePath(uri: string, workspaceRoot: string): string;
17
+ export declare function ensureToolPackageSource(uri: string, workspaceRoot: string): Promise<ToolPackageInstallation>;
18
+ export declare function ensureRemoteSkillSource(uri: string): Promise<string>;
19
+ export {};
@@ -0,0 +1,192 @@
1
+ import { createRequire } from "node:module";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { createHash } from "node:crypto";
5
+ import { existsSync, readFileSync } from "node:fs";
6
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
7
+ import { execFile } from "node:child_process";
8
+ import { promisify } from "node:util";
9
+ import { parse as parseYaml } from "yaml";
10
+ const execFileAsync = promisify(execFile);
11
+ export const DEFAULT_TOOL_SOURCE_URIS = ["file://./resources/tools"];
12
+ export const DEFAULT_SKILL_SOURCE_URIS = ["file://./resources/skills"];
13
+ const require = createRequire(import.meta.url);
14
+ function normalizeSourceList(value, fallback) {
15
+ if (!Array.isArray(value)) {
16
+ return [...fallback];
17
+ }
18
+ return value
19
+ .filter((item) => typeof item === "string" && item.trim().length > 0)
20
+ .map((item) => item.trim());
21
+ }
22
+ export function readRuntimeSources(runtimeDefaults) {
23
+ const rawSources = typeof runtimeDefaults?.sources === "object" && runtimeDefaults.sources && !Array.isArray(runtimeDefaults.sources)
24
+ ? runtimeDefaults.sources
25
+ : {};
26
+ return {
27
+ tools: normalizeSourceList(rawSources.tools, DEFAULT_TOOL_SOURCE_URIS),
28
+ skills: normalizeSourceList(rawSources.skills, DEFAULT_SKILL_SOURCE_URIS),
29
+ };
30
+ }
31
+ export function isFileSourceUri(value) {
32
+ return value.startsWith("file://");
33
+ }
34
+ export function isNpmSourceUri(value) {
35
+ return value.startsWith("npm://");
36
+ }
37
+ export function isHttpSourceUri(value) {
38
+ return value.startsWith("http://") || value.startsWith("https://");
39
+ }
40
+ export function resolveFileSourcePath(uri, workspaceRoot) {
41
+ if (!isFileSourceUri(uri)) {
42
+ throw new Error(`Unsupported file source URI ${uri}`);
43
+ }
44
+ const rawPath = uri.slice("file://".length).trim();
45
+ if (!rawPath) {
46
+ throw new Error(`File source URI ${uri} must include a path`);
47
+ }
48
+ return path.isAbsolute(rawPath) ? rawPath : path.resolve(workspaceRoot, rawPath);
49
+ }
50
+ function parseToolPackageName(spec, workspaceRoot) {
51
+ if (spec.startsWith("file:")) {
52
+ const localPath = spec.slice("file:".length);
53
+ const resolvedPath = path.isAbsolute(localPath) ? localPath : path.resolve(workspaceRoot, localPath);
54
+ const packageJsonPath = path.join(resolvedPath, "package.json");
55
+ if (!existsSync(packageJsonPath)) {
56
+ throw new Error(`Package tool source ${spec} is missing package.json`);
57
+ }
58
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
59
+ if (typeof packageJson.name !== "string" || packageJson.name.trim().length === 0) {
60
+ throw new Error(`Package tool source ${spec} must define a package.json name`);
61
+ }
62
+ return packageJson.name.trim();
63
+ }
64
+ const match = spec.match(/^(@[^/]+\/[^@/]+|[^@/]+)(?:@.+)?$/);
65
+ if (!match?.[1]) {
66
+ throw new Error(`Unsupported package tool source ${spec}. Use npm://<package-name> or npm://<package-name>@<version>.`);
67
+ }
68
+ return match[1];
69
+ }
70
+ function packageInstallCacheRoot(spec) {
71
+ const digest = createHash("sha256").update(spec).digest("hex").slice(0, 16);
72
+ return path.join(os.tmpdir(), "agent-harness-package-tools", digest);
73
+ }
74
+ function resolvePackageEntry(packageRoot, pkg) {
75
+ const exportsField = pkg.exports;
76
+ if (typeof exportsField === "string") {
77
+ return path.resolve(packageRoot, exportsField);
78
+ }
79
+ if (exportsField && typeof exportsField === "object" && "." in exportsField) {
80
+ const rootExport = exportsField["."];
81
+ if (typeof rootExport === "string") {
82
+ return path.resolve(packageRoot, rootExport);
83
+ }
84
+ if (rootExport && typeof rootExport === "object") {
85
+ const importEntry = rootExport.import ?? rootExport.default;
86
+ if (typeof importEntry === "string") {
87
+ return path.resolve(packageRoot, importEntry);
88
+ }
89
+ }
90
+ }
91
+ if (typeof pkg.module === "string") {
92
+ return path.resolve(packageRoot, pkg.module);
93
+ }
94
+ if (typeof pkg.main === "string") {
95
+ return path.resolve(packageRoot, pkg.main);
96
+ }
97
+ return path.resolve(packageRoot, "index.js");
98
+ }
99
+ function resolveInstalledPackageRoot(packageName, workspaceRoot) {
100
+ const packageJsonRef = `${packageName}/package.json`;
101
+ const directNodeModulesPath = path.join(workspaceRoot, "node_modules", ...packageName.split("/"), "package.json");
102
+ if (existsSync(directNodeModulesPath)) {
103
+ return path.dirname(directNodeModulesPath);
104
+ }
105
+ for (const candidateRoot of [workspaceRoot, process.cwd()]) {
106
+ try {
107
+ const candidateRequire = createRequire(path.join(candidateRoot, "__agent_harness_package_resolve__.cjs"));
108
+ return path.dirname(candidateRequire.resolve(packageJsonRef));
109
+ }
110
+ catch {
111
+ continue;
112
+ }
113
+ }
114
+ return null;
115
+ }
116
+ async function installPackageToolSource(spec, workspaceRoot) {
117
+ const packageName = parseToolPackageName(spec, workspaceRoot);
118
+ const installRoot = packageInstallCacheRoot(spec);
119
+ const packageJsonPath = path.join(installRoot, "package.json");
120
+ await mkdir(installRoot, { recursive: true });
121
+ if (!existsSync(packageJsonPath)) {
122
+ await writeFile(packageJsonPath, `${JSON.stringify({ name: "agent-harness-package-tool-cache", private: true }, null, 2)}\n`, "utf8");
123
+ }
124
+ const installedRoot = path.join(installRoot, "node_modules", ...packageName.split("/"));
125
+ if (!existsSync(path.join(installedRoot, "package.json"))) {
126
+ await execFileAsync("npm", ["install", "--no-save", spec], {
127
+ cwd: installRoot,
128
+ maxBuffer: 1024 * 1024 * 20,
129
+ env: process.env,
130
+ });
131
+ }
132
+ const packageJson = JSON.parse(await readFile(path.join(installedRoot, "package.json"), "utf8"));
133
+ return {
134
+ packageName,
135
+ packageRoot: installedRoot,
136
+ entryPath: resolvePackageEntry(installedRoot, packageJson),
137
+ };
138
+ }
139
+ export async function ensureToolPackageSource(uri, workspaceRoot) {
140
+ if (!isNpmSourceUri(uri)) {
141
+ throw new Error(`Unsupported package source URI ${uri}`);
142
+ }
143
+ const spec = uri.slice("npm://".length).trim();
144
+ if (!spec) {
145
+ throw new Error(`Package source URI ${uri} must include a package name`);
146
+ }
147
+ const packageName = parseToolPackageName(spec, workspaceRoot);
148
+ const installedRoot = resolveInstalledPackageRoot(packageName, workspaceRoot);
149
+ if (installedRoot && !spec.startsWith("file:") && !spec.includes("@", packageName.startsWith("@") ? packageName.length : 0)) {
150
+ const packageJson = JSON.parse(await readFile(path.join(installedRoot, "package.json"), "utf8"));
151
+ return {
152
+ packageName,
153
+ packageRoot: installedRoot,
154
+ entryPath: resolvePackageEntry(installedRoot, packageJson),
155
+ };
156
+ }
157
+ return installPackageToolSource(spec, workspaceRoot);
158
+ }
159
+ function parseSkillFrontmatterName(document) {
160
+ const match = document.match(/^---\s*\n([\s\S]*?)\n---\s*(?:\n|$)/);
161
+ if (!match?.[1]) {
162
+ return undefined;
163
+ }
164
+ const parsed = parseYaml(match[1]);
165
+ return typeof parsed?.name === "string" && parsed.name.trim().length > 0 ? parsed.name.trim() : undefined;
166
+ }
167
+ function sanitizeSkillDirName(input) {
168
+ const normalized = input.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
169
+ return normalized || "remote-skill";
170
+ }
171
+ function remoteSkillCacheRoot(uri) {
172
+ const digest = createHash("sha256").update(uri).digest("hex").slice(0, 16);
173
+ return path.join(os.tmpdir(), "agent-harness-remote-skills", digest);
174
+ }
175
+ export async function ensureRemoteSkillSource(uri) {
176
+ if (!isHttpSourceUri(uri)) {
177
+ throw new Error(`Unsupported remote skill source ${uri}`);
178
+ }
179
+ const response = await fetch(uri);
180
+ if (!response.ok) {
181
+ throw new Error(`Remote skill source ${uri} returned ${response.status}`);
182
+ }
183
+ const document = await response.text();
184
+ const skillName = sanitizeSkillDirName(parseSkillFrontmatterName(document)
185
+ ?? path.basename(new URL(uri).pathname).replace(/\.md$/i, "")
186
+ ?? "remote-skill");
187
+ const cacheRoot = remoteSkillCacheRoot(uri);
188
+ const skillRoot = path.join(cacheRoot, skillName);
189
+ await mkdir(skillRoot, { recursive: true });
190
+ await writeFile(path.join(skillRoot, "SKILL.md"), document, "utf8");
191
+ return skillRoot;
192
+ }
@@ -44,6 +44,10 @@ export type RuntimeStorageRoots = {
44
44
  };
45
45
  export declare function getRuntimeStorageRoots(refs: Map<string, WorkspaceObject | ParsedAgentObject>, workspaceRoot: string): RuntimeStorageRoots;
46
46
  export declare function getRuntimeResources(refs: Map<string, WorkspaceObject | ParsedAgentObject>): string[];
47
+ export declare function getRuntimeSources(refs: Map<string, WorkspaceObject | ParsedAgentObject>): {
48
+ tools: string[];
49
+ skills: string[];
50
+ };
47
51
  export declare function getToolModuleDiscoveryConfig(refs: Map<string, WorkspaceObject | ParsedAgentObject>): ToolModuleDiscoveryConfig;
48
52
  export declare function getRuntimeMemoryDefaults(refs: Map<string, WorkspaceObject | ParsedAgentObject>): Record<string, unknown> | undefined;
49
53
  export declare function getProceduralMemoryDefaults(refs: Map<string, WorkspaceObject | ParsedAgentObject>): Record<string, unknown> | undefined;
@@ -1,5 +1,6 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
+ import { readRuntimeSources } from "./source-protocols.js";
3
4
  function getRoutingObject(refs) {
4
5
  const runtimeDefaults = getRuntimeDefaults(refs);
5
6
  return typeof runtimeDefaults?.routing === "object" && runtimeDefaults.routing
@@ -60,6 +61,9 @@ export function getRuntimeResources(refs) {
60
61
  .filter((value) => typeof value === "string" && value.trim().length > 0)
61
62
  .map((value) => value.trim());
62
63
  }
64
+ export function getRuntimeSources(refs) {
65
+ return readRuntimeSources(getRuntimeDefaults(refs));
66
+ }
63
67
  export function getToolModuleDiscoveryConfig(refs) {
64
68
  const runtimeDefaults = getRuntimeDefaults(refs);
65
69
  const toolModuleDiscovery = typeof runtimeDefaults?.toolModuleDiscovery === "object" && runtimeDefaults.toolModuleDiscovery
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.307",
3
+ "version": "0.0.308",
4
4
  "description": "Workspace runtime for multi-agent applications",
5
5
  "license": "MIT",
6
6
  "type": "module",