@botbotgo/agent-harness 0.0.306 → 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.
@@ -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);
@@ -0,0 +1,40 @@
1
+ You are deciding whether a completed run should produce reusable procedural memory.
2
+
3
+ Focus areas:
4
+ {{focus}}
5
+
6
+ Session:
7
+ {{sessionId}}
8
+
9
+ Request:
10
+ {{requestId}}
11
+
12
+ Candidate:
13
+ {{candidateJson}}
14
+
15
+ Existing procedural memory:
16
+ {{existingRecords}}
17
+
18
+ Return exactly one JSON object.
19
+
20
+ Rules:
21
+ - Store only reusable experience, tactics, workflows, debugging lessons, or failure-prevention guidance.
22
+ - Ignore pure personal facts, profile facts, location changes, and one-off durable knowledge facts unless they imply a reusable procedure.
23
+ - Prefer concise, imperative procedural guidance that can help a future run act better.
24
+ - Avoid duplicating existing procedural memory.
25
+ - Default kind must be "procedural".
26
+ - Use scope "workspace" for repo/workspace habits, "project" for broader project rules, "agent" for agent-specific operating habits, and "user" only when the reusable procedure is clearly personal to the user.
27
+
28
+ If there is no reusable procedural lesson, return:
29
+ {"store":false}
30
+
31
+ If there is a reusable lesson, return:
32
+ {
33
+ "store": true,
34
+ "summary": "short summary",
35
+ "content": "clear reusable procedure or lesson",
36
+ "kind": "procedural",
37
+ "scope": "workspace",
38
+ "tags": ["workflow_patterns"],
39
+ "confidence": 0.78
40
+ }
@@ -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);
@@ -36,6 +36,12 @@ export declare class AgentHarnessRuntime {
36
36
  private readonly knowledgeModule;
37
37
  private readonly runtimeMemoryFormationSync;
38
38
  private readonly unregisterRuntimeMemoryFormationSync;
39
+ private readonly proceduralMemoryConfig;
40
+ private readonly proceduralMemoryStore;
41
+ private readonly proceduralMemoryManager;
42
+ private readonly proceduralMemoryModule;
43
+ private readonly proceduralMemoryFormationSync;
44
+ private readonly unregisterProceduralMemoryFormationSync;
39
45
  private readonly resolvedRuntimeAdapterOptions;
40
46
  private readonly scheduleManager;
41
47
  private readonly healthMonitor;
@@ -35,7 +35,7 @@ import { Mem0IngestionSync, Mem0SemanticRecall, readMem0RuntimeConfig, } from ".
35
35
  import { createRuntimeMemoryManager, RuntimeMemoryFormationSync, readRuntimeMemoryFormationConfig, } from "./harness/system/runtime-memory-manager.js";
36
36
  import { readRuntimeMemoryMaintenanceConfig, readRuntimeMemoryPolicyConfig, resolveMemoryNamespace, } from "./harness/system/runtime-memory-policy.js";
37
37
  import { resolveRuntimeAdapterOptions } from "./support/runtime-adapter-options.js";
38
- import { resolveKnowledgeStorePath } from "./support/runtime-layout.js";
38
+ import { resolveKnowledgeStorePath, resolveProceduralMemoryStorePath } from "./support/runtime-layout.js";
39
39
  import { SystemScheduleManager } from "./scheduling/system-schedule-manager.js";
40
40
  import { initializeHarnessRuntime, reclaimExpiredClaimedRequests as reclaimHarnessExpiredClaimedRequests, recoverStartupRequests as recoverHarnessStartupRequests, isStaleRunningRequest as isHarnessStaleRunningRequest, } from "./harness/run/startup-runtime.js";
41
41
  import { traceStartupStage } from "./startup-tracing.js";
@@ -44,6 +44,7 @@ import { streamHarnessRun } from "./harness/run/stream-run.js";
44
44
  import { defaultRequestedAgentId, prepareRunStart } from "./harness/run/start-run.js";
45
45
  import { buildRequestInspectionRecord, buildSessionInspectionRecord, deleteSessionRecord, getPublicApproval, listPublicApprovals, } from "./harness/run/session-records.js";
46
46
  import { createKnowledgeModule } from "../knowledge/index.js";
47
+ import { createProceduralMemoryManager, ProceduralMemoryFormationSync, readProceduralMemoryRuntimeConfig, } from "../procedural/index.js";
47
48
  const ACTIVE_REQUEST_STATES = [
48
49
  "queued",
49
50
  "claimed",
@@ -132,6 +133,17 @@ function toPublicHarnessStreamItem(item) {
132
133
  return item;
133
134
  }
134
135
  }
136
+ function mergeMemoryItems(...groups) {
137
+ const merged = new Map();
138
+ for (const group of groups) {
139
+ for (const item of group) {
140
+ if (!merged.has(item.id)) {
141
+ merged.set(item.id, item);
142
+ }
143
+ }
144
+ }
145
+ return Array.from(merged.values());
146
+ }
135
147
  function normalizeSessionListText(content, limit) {
136
148
  if (!content) {
137
149
  return undefined;
@@ -216,6 +228,12 @@ export class AgentHarnessRuntime {
216
228
  knowledgeModule;
217
229
  runtimeMemoryFormationSync;
218
230
  unregisterRuntimeMemoryFormationSync;
231
+ proceduralMemoryConfig;
232
+ proceduralMemoryStore;
233
+ proceduralMemoryManager;
234
+ proceduralMemoryModule;
235
+ proceduralMemoryFormationSync;
236
+ unregisterProceduralMemoryFormationSync;
219
237
  resolvedRuntimeAdapterOptions;
220
238
  scheduleManager;
221
239
  healthMonitor;
@@ -461,6 +479,82 @@ export class AgentHarnessRuntime {
461
479
  this.runtimeMemoryFormationSync = null;
462
480
  this.unregisterRuntimeMemoryFormationSync = () => { };
463
481
  }
482
+ const proceduralMemoryConfig = readProceduralMemoryRuntimeConfig(this.defaultRuntimeEntryBinding?.harnessRuntime.proceduralMemory);
483
+ this.proceduralMemoryConfig = proceduralMemoryConfig?.enabled ? proceduralMemoryConfig : null;
484
+ const proceduralStoreConfig = this.proceduralMemoryConfig?.store && Object.keys(this.proceduralMemoryConfig.store).length > 0
485
+ ? this.proceduralMemoryConfig.store
486
+ : {
487
+ kind: "SqliteStore",
488
+ path: resolveProceduralMemoryStorePath(runtimeRoot),
489
+ };
490
+ this.proceduralMemoryStore = this.proceduralMemoryConfig
491
+ ? resolveStoreFromConfig(this.stores, proceduralStoreConfig, runtimeRoot) ?? null
492
+ : null;
493
+ this.proceduralMemoryManager =
494
+ this.defaultRuntimeEntryBinding && this.proceduralMemoryConfig
495
+ ? createProceduralMemoryManager({
496
+ workspace: this.workspace,
497
+ binding: this.defaultRuntimeEntryBinding,
498
+ config: this.proceduralMemoryConfig,
499
+ modelResolver: this.resolvedRuntimeAdapterOptions.modelResolver,
500
+ })
501
+ : null;
502
+ this.proceduralMemoryModule =
503
+ this.proceduralMemoryConfig && this.proceduralMemoryStore
504
+ ? createKnowledgeModule({
505
+ store: this.proceduralMemoryStore,
506
+ policy: null,
507
+ maintenanceConfig: null,
508
+ resolveNamespace: (scope, context) => {
509
+ const binding = this.defaultRuntimeEntryBinding;
510
+ if (!binding) {
511
+ const identifier = scope === "session"
512
+ ? context.sessionId
513
+ : scope === "agent"
514
+ ? context.agentId
515
+ : scope === "workspace"
516
+ ? context.workspaceId
517
+ : scope === "user"
518
+ ? context.userId ?? "default"
519
+ : context.projectId ?? context.workspaceId;
520
+ return ["procedural", `${scope}s`, identifier ?? "default"];
521
+ }
522
+ return this.resolveMemoryNamespace(scope, binding, {
523
+ sessionId: context.sessionId,
524
+ agentId: context.agentId,
525
+ userId: context.userId,
526
+ projectId: context.projectId,
527
+ });
528
+ },
529
+ resolveVectorStore: async () => null,
530
+ transformCandidates: this.proceduralMemoryManager && this.defaultRuntimeEntryBinding
531
+ ? ({ candidates, context, existingRecords }) => this.proceduralMemoryManager.transform({
532
+ candidates,
533
+ binding: this.defaultRuntimeEntryBinding,
534
+ sessionId: context.sessionId ?? `procedural-api-${context.requestId ?? "unknown"}`,
535
+ requestId: context.requestId ?? createPersistentId(new Date(context.recordedAt ?? new Date().toISOString())),
536
+ recordedAt: context.recordedAt ?? new Date().toISOString(),
537
+ existingRecords,
538
+ })
539
+ : undefined,
540
+ })
541
+ : null;
542
+ if (this.proceduralMemoryConfig && this.proceduralMemoryModule) {
543
+ this.proceduralMemoryFormationSync = new ProceduralMemoryFormationSync(this.persistence, this.proceduralMemoryConfig, (input) => this.proceduralMemoryModule.memorizeCandidates(input.candidates, {
544
+ sessionId: input.sessionId,
545
+ requestId: input.requestId,
546
+ agentId: input.agentId,
547
+ workspaceId: this.getWorkspaceId(this.defaultRuntimeEntryBinding),
548
+ userId: input.userId,
549
+ projectId: input.projectId,
550
+ recordedAt: input.recordedAt,
551
+ }, { storeCandidateLog: false }).then(() => undefined), runtimeRoot);
552
+ this.unregisterProceduralMemoryFormationSync = this.eventBus.registerProjection(this.proceduralMemoryFormationSync);
553
+ }
554
+ else {
555
+ this.proceduralMemoryFormationSync = null;
556
+ this.unregisterProceduralMemoryFormationSync = () => { };
557
+ }
464
558
  this.recoveryConfig = getRecoveryConfig(workspace.refs);
465
559
  this.concurrencyConfig = getConcurrencyConfig(workspace.refs);
466
560
  const healthConfig = readHealthMonitorConfig(workspace);
@@ -514,6 +608,9 @@ export class AgentHarnessRuntime {
514
608
  scheduleBackgroundTask: (task) => this.scheduleBackgroundStartupTask(task),
515
609
  });
516
610
  this.scheduleBackgroundStartupTask(traceStartupStage("runtime.initialize.startupRecovery", () => this.recoverStartupRequests()));
611
+ if (this.proceduralMemoryStore) {
612
+ await this.proceduralMemoryStore.listNamespaces();
613
+ }
517
614
  this.initialized = true;
518
615
  }
519
616
  subscribe(listener) {
@@ -604,26 +701,44 @@ export class AgentHarnessRuntime {
604
701
  if (!binding) {
605
702
  throw new Error("recall requires a runtime entry binding.");
606
703
  }
607
- return this.knowledgeModule.recall(input, {
704
+ const context = {
608
705
  sessionId: input.sessionId,
609
706
  agentId: input.agentId ?? binding.agent.id,
610
707
  workspaceId: input.workspaceId ?? this.getWorkspaceId(binding),
611
708
  userId: input.userId,
612
709
  projectId: input.projectId,
613
- });
710
+ };
711
+ const [knowledge, procedural] = await Promise.all([
712
+ this.knowledgeModule.recall(input, context),
713
+ this.proceduralMemoryModule && this.proceduralMemoryConfig?.retrieval?.enabled !== false
714
+ ? this.proceduralMemoryModule.recall(input, context)
715
+ : Promise.resolve({ items: [] }),
716
+ ]);
717
+ return {
718
+ items: mergeMemoryItems(knowledge.items, procedural.items),
719
+ };
614
720
  }
615
721
  async listMemories(input = {}) {
616
722
  const binding = this.defaultRuntimeEntryBinding;
617
723
  if (!binding) {
618
724
  throw new Error("listMemories requires a runtime entry binding.");
619
725
  }
620
- return this.knowledgeModule.list(input, {
726
+ const context = {
621
727
  sessionId: input.sessionId,
622
728
  agentId: input.agentId,
623
729
  workspaceId: input.workspaceId ?? this.getWorkspaceId(binding),
624
730
  userId: input.userId,
625
731
  projectId: input.projectId,
626
- });
732
+ };
733
+ const [knowledge, procedural] = await Promise.all([
734
+ this.knowledgeModule.list(input, context),
735
+ this.proceduralMemoryModule
736
+ ? this.proceduralMemoryModule.list(input, context)
737
+ : Promise.resolve({ items: [] }),
738
+ ]);
739
+ return {
740
+ items: mergeMemoryItems(knowledge.items, procedural.items),
741
+ };
627
742
  }
628
743
  async updateMemory(input) {
629
744
  const binding = this.defaultRuntimeEntryBinding;
@@ -1029,27 +1144,40 @@ export class AgentHarnessRuntime {
1029
1144
  return path.basename(workspaceRoot) || "default";
1030
1145
  }
1031
1146
  async buildRuntimeMemoryContext(binding, sessionId, input) {
1032
- if (!this.runtimeMemoryPolicy) {
1147
+ if (!this.runtimeMemoryPolicy && !this.proceduralMemoryModule) {
1033
1148
  return undefined;
1034
1149
  }
1035
1150
  const query = extractMessageText(input ?? "").trim();
1036
1151
  if (!query) {
1037
1152
  return undefined;
1038
1153
  }
1039
- const promptRecall = await this.knowledgeModule.buildPromptRecall({
1040
- query,
1041
- topK: this.runtimeMemoryPolicy?.retrieval.maxPromptMemories ?? 8,
1042
- }, {
1154
+ const context = {
1043
1155
  sessionId,
1044
1156
  agentId: binding.agent.id,
1045
1157
  workspaceId: this.getWorkspaceId(binding),
1046
- });
1047
- if (!promptRecall.context?.trim()) {
1158
+ };
1159
+ const proceduralTopK = this.proceduralMemoryConfig?.retrieval?.maxPromptItems ?? 4;
1160
+ const [knowledgeRecall, proceduralRecall] = await Promise.all([
1161
+ this.runtimeMemoryPolicy
1162
+ ? this.knowledgeModule.buildPromptRecall({
1163
+ query,
1164
+ topK: this.runtimeMemoryPolicy?.retrieval.maxPromptMemories ?? 8,
1165
+ }, context)
1166
+ : Promise.resolve({ items: [], context: undefined }),
1167
+ this.proceduralMemoryModule && this.proceduralMemoryConfig?.retrieval?.enabled !== false
1168
+ ? this.proceduralMemoryModule.buildPromptRecall({
1169
+ query,
1170
+ topK: proceduralTopK,
1171
+ }, context)
1172
+ : Promise.resolve({ items: [], context: undefined }),
1173
+ ]);
1174
+ const promptParts = [knowledgeRecall.context?.trim(), proceduralRecall.context?.trim()].filter((value) => !!value);
1175
+ if (promptParts.length === 0) {
1048
1176
  return undefined;
1049
1177
  }
1050
1178
  return {
1051
- prompt: promptRecall.context,
1052
- items: promptRecall.items,
1179
+ prompt: promptParts.join("\n\n"),
1180
+ items: mergeMemoryItems(knowledgeRecall.items, proceduralRecall.items),
1053
1181
  };
1054
1182
  }
1055
1183
  async persistRuntimeMemoryCandidates(binding, sessionId, requestId, value) {
@@ -1374,11 +1502,13 @@ export class AgentHarnessRuntime {
1374
1502
  this.unregisterRuntimeMemorySync();
1375
1503
  this.unregisterMem0IngestionSync();
1376
1504
  this.unregisterRuntimeMemoryFormationSync();
1505
+ this.unregisterProceduralMemoryFormationSync();
1377
1506
  await Promise.allSettled(Array.from(this.backgroundTasks));
1378
1507
  await this.sessionMemorySync?.close();
1379
1508
  await this.runtimeMemorySync?.close();
1380
1509
  await this.mem0IngestionSync?.close();
1381
1510
  await this.runtimeMemoryFormationSync?.close();
1511
+ await this.proceduralMemoryFormationSync?.close();
1382
1512
  await closeMcpClientsForWorkspace(this.workspace);
1383
1513
  this.initialized = false;
1384
1514
  }
@@ -28,3 +28,10 @@ export declare function renderRuntimeMemoryMutationReconciliationPrompt(input: {
28
28
  candidate: MemoryCandidate;
29
29
  existingRecords: MemoryRecord[];
30
30
  }): string;
31
+ export declare function renderProceduralMemoryManagerPrompt(input: {
32
+ candidate: MemoryCandidate;
33
+ sessionId: string;
34
+ requestId: string;
35
+ focus: string[];
36
+ existingRecords: MemoryRecord[];
37
+ }): string;
@@ -53,3 +53,17 @@ export function renderRuntimeMemoryMutationReconciliationPrompt(input) {
53
53
  existingRecords: existing,
54
54
  });
55
55
  }
56
+ export function renderProceduralMemoryManagerPrompt(input) {
57
+ const existing = input.existingRecords.length === 0
58
+ ? "(none)"
59
+ : input.existingRecords
60
+ .map((record) => `- scope=${record.scope}; kind=${record.kind}; status=${record.status}; summary=${record.summary}; content=${record.content.replace(/\s+/g, " ").slice(0, 220)}`)
61
+ .join("\n");
62
+ return renderBundledTemplate("prompts/runtime/procedural-memory-manager.md", {
63
+ sessionId: input.sessionId,
64
+ requestId: input.requestId,
65
+ focus: input.focus.join(", "),
66
+ candidateJson: JSON.stringify(input.candidate, null, 2),
67
+ existingRecords: existing,
68
+ });
69
+ }
@@ -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);