@botbotgo/agent-harness 0.0.22 → 0.0.23

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
@@ -9,7 +9,9 @@ It helps developers:
9
9
  - define agents in YAML
10
10
  - keep workspace behavior in `config/workspace.yaml`
11
11
  - keep shared bootstrap context in `config/agent-context.md`
12
- - discover tools and skills from `resources/`
12
+ - discover tools and SKILL packages from `resources/`
13
+ - bridge remote MCP servers into agent toolsets
14
+ - expose harness-managed tools as an MCP server
13
15
  - run through either LangChain v1 or DeepAgents with one app-facing API
14
16
 
15
17
  Why it works:
@@ -54,6 +56,8 @@ your-workspace/
54
56
  skills/
55
57
  ```
56
58
 
59
+ Use the standard layout only. Agent entry files must live under `config/agents/`; root-level files such as `agent.yaml`, `orchestra.yaml`, and `direct.yaml` are not loaded.
60
+
57
61
  Minimal usage:
58
62
 
59
63
  ```ts
@@ -78,7 +82,9 @@ try {
78
82
  - One workspace for agents, models, tools, and runtime behavior
79
83
  - One API for both LangChain v1 and DeepAgents
80
84
  - Built-in routing for direct and orchestration flows
81
- - Auto-discovered local tools and skills from `resources/tools/` and `resources/skills/`
85
+ - Auto-discovered local tools and SKILL packages from `resources/tools/` and `resources/skills/`
86
+ - MCP bridge support: agent YAML can mount remote or local MCP servers as agent tools
87
+ - MCP server support: expose harness-managed tools over stdio or an in-memory MCP server
82
88
  - Built-in thread state, approvals, resumable runs, and long-term memory
83
89
  - MCP server helpers plus background checkpoint maintenance
84
90
 
@@ -146,6 +152,49 @@ const result = await run(harness, {
146
152
  });
147
153
  ```
148
154
 
155
+ ### Use Skills And Local Tools
156
+
157
+ `agent-harness` treats `resources/skills/` as SKILL packages and `resources/tools/` as executable local tools.
158
+
159
+ Point agent YAML at both:
160
+
161
+ ```yaml
162
+ spec:
163
+ skills:
164
+ - path: resources/skills/reviewer
165
+ tools:
166
+ - ref: tool/local-toolset
167
+ ```
168
+
169
+ ### Bridge MCP Servers Into Agents
170
+
171
+ Use `mcpServers:` inside agent YAML to bridge MCP servers into the agent's tool list:
172
+
173
+ ```yaml
174
+ spec:
175
+ mcpServers:
176
+ - name: browser
177
+ command: node
178
+ args: ["./mcp-browser-server.mjs"]
179
+ - name: docs
180
+ transport: http
181
+ url: https://example.com/mcp
182
+ token: ${DOCS_MCP_TOKEN}
183
+ ```
184
+
185
+ The harness discovers MCP tools, filters them through the agent config, and exposes them to the runtime like any other tool.
186
+
187
+ ### Expose Harness Tools As An MCP Server
188
+
189
+ Use the MCP server helpers when another client should connect to the harness as an MCP server:
190
+
191
+ ```ts
192
+ import { createToolMcpServer, serveToolsOverStdio } from "@botbotgo/agent-harness";
193
+
194
+ const server = await createToolMcpServer(harness, { agentId: "orchestra" });
195
+ await serveToolsOverStdio(harness, { agentId: "orchestra" });
196
+ ```
197
+
149
198
  ### Read Back Thread State
150
199
 
151
200
  ```ts
@@ -221,6 +270,12 @@ Use `config/agents/*.yaml` to configure agents. Common fields include:
221
270
  Use `resources/` for executable extensions:
222
271
 
223
272
  - `resources/tools/` for local tool modules
224
- - `resources/skills/` for skill packages
273
+ - `resources/skills/` for SKILL packages
225
274
 
226
275
  Each resource package should include its own `package.json`.
276
+
277
+ ### Skills And MCP
278
+
279
+ - Use `resources/skills/` for SKILL packages that carry reusable instructions, templates, scripts, and metadata
280
+ - Use `mcpServers:` in `config/agents/*.yaml` when you want the harness to bridge external MCP tools into an agent
281
+ - Use `createToolMcpServer(...)` or `serveToolsOverStdio(...)` when you want the harness itself to act as an MCP server for another client
@@ -0,0 +1,29 @@
1
+ # agent-harness feature: schema version for this declarative config object.
2
+ apiVersion: agent-harness/v1alpha1
3
+ # agent-harness feature: object type for named model presets.
4
+ kind: Models
5
+ spec:
6
+ - name: default
7
+ # ====================
8
+ # LangChain v1 Features
9
+ # ====================
10
+ # LangChain aligned feature: provider family or integration namespace.
11
+ # Common options in this harness today include:
12
+ # - `ollama`
13
+ # - `openai`
14
+ # - `openai-compatible`
15
+ # - `anthropic`
16
+ # - `google` / `google-genai` / `gemini`
17
+ # The runtime adapter uses this to select the concrete LangChain chat model implementation.
18
+ provider: ollama
19
+ # LangChain aligned feature: concrete model identifier passed to the selected provider integration.
20
+ # Example values depend on `provider`, such as `gpt-oss:latest` for `ollama`.
21
+ model: gpt-oss:latest
22
+ init:
23
+ # LangChain aligned feature: provider-specific initialization options.
24
+ # Available keys are provider-specific; common examples include `baseUrl`, `temperature`, and auth/client settings.
25
+ # `baseUrl` configures the Ollama-compatible endpoint used by the model client.
26
+ # For `openai-compatible`, `baseUrl` is normalized into the ChatOpenAI `configuration.baseURL` field.
27
+ baseUrl: https://ollama-rtx-4070.easynet.world/
28
+ # LangChain aligned feature: provider/model initialization option controlling sampling temperature.
29
+ temperature: 0.2
@@ -44,6 +44,15 @@ export declare class AgentHarness {
44
44
  threadId?: string;
45
45
  }): Promise<string>;
46
46
  private emit;
47
+ private ensureThreadStarted;
48
+ private loadPriorHistory;
49
+ private appendAssistantMessage;
50
+ private invokeWithHistory;
51
+ private emitOutputDeltaAndCreateItem;
52
+ private emitRunCreated;
53
+ private setRunStateAndEmit;
54
+ private requestApprovalAndEmit;
55
+ private emitSyntheticFallback;
47
56
  private persistApproval;
48
57
  private resolveApprovalRecord;
49
58
  private isDecisionRun;
@@ -269,6 +269,95 @@ export class AgentHarness {
269
269
  this.eventBus.publish(event);
270
270
  return event;
271
271
  }
272
+ async ensureThreadStarted(selectedAgentId, binding, input, existingThreadId) {
273
+ const threadId = existingThreadId ?? createPersistentId();
274
+ const runId = createPersistentId();
275
+ const createdAt = new Date().toISOString();
276
+ if (!existingThreadId) {
277
+ await this.persistence.createThread({
278
+ threadId,
279
+ agentId: selectedAgentId,
280
+ runId,
281
+ status: "running",
282
+ createdAt,
283
+ });
284
+ }
285
+ await this.persistence.appendThreadMessage(threadId, {
286
+ role: "user",
287
+ content: input,
288
+ runId,
289
+ createdAt,
290
+ });
291
+ await this.persistence.createRun({
292
+ threadId,
293
+ runId,
294
+ agentId: binding.agent.id,
295
+ executionMode: binding.agent.executionMode,
296
+ createdAt,
297
+ });
298
+ return { threadId, runId, createdAt };
299
+ }
300
+ async loadPriorHistory(threadId, runId) {
301
+ const history = await this.persistence.listThreadMessages(threadId);
302
+ return history.filter((message) => message.runId !== runId);
303
+ }
304
+ async appendAssistantMessage(threadId, runId, content) {
305
+ if (!content) {
306
+ return;
307
+ }
308
+ await this.persistence.appendThreadMessage(threadId, {
309
+ role: "assistant",
310
+ content,
311
+ runId,
312
+ createdAt: new Date().toISOString(),
313
+ });
314
+ }
315
+ async invokeWithHistory(binding, input, threadId, runId, resumePayload) {
316
+ const priorHistory = await this.loadPriorHistory(threadId, runId);
317
+ return this.runtimeAdapter.invoke(binding, input, threadId, runId, resumePayload, priorHistory);
318
+ }
319
+ async emitOutputDeltaAndCreateItem(threadId, runId, agentId, content) {
320
+ await this.emit(threadId, runId, 3, "output.delta", {
321
+ content,
322
+ });
323
+ return {
324
+ type: "content",
325
+ threadId,
326
+ runId,
327
+ agentId,
328
+ content,
329
+ };
330
+ }
331
+ async emitRunCreated(threadId, runId, payload) {
332
+ return this.emit(threadId, runId, 1, "run.created", payload);
333
+ }
334
+ async setRunStateAndEmit(threadId, runId, sequence, state, options) {
335
+ await this.persistence.setRunState(threadId, runId, state, options.checkpointRef ?? null);
336
+ return this.emit(threadId, runId, sequence, "run.state.changed", {
337
+ previousState: options.previousState,
338
+ state,
339
+ checkpointRef: options.checkpointRef ?? null,
340
+ ...(options.error ? { error: options.error } : {}),
341
+ });
342
+ }
343
+ async requestApprovalAndEmit(threadId, runId, input, interruptContent, checkpointRef, sequence) {
344
+ const approval = await this.persistApproval(threadId, runId, checkpointRef, input, interruptContent);
345
+ const event = await this.emit(threadId, runId, sequence, "approval.requested", {
346
+ approvalId: approval.approvalId,
347
+ pendingActionId: approval.pendingActionId,
348
+ toolName: approval.toolName,
349
+ toolCallId: approval.toolCallId,
350
+ allowedDecisions: approval.allowedDecisions,
351
+ checkpointRef,
352
+ });
353
+ return { approval, event };
354
+ }
355
+ async emitSyntheticFallback(threadId, runId, selectedAgentId, error, sequence = 3) {
356
+ await this.emit(threadId, runId, sequence, "runtime.synthetic_fallback", {
357
+ reason: error instanceof Error ? error.message : String(error),
358
+ selectedAgentId,
359
+ });
360
+ }
272
361
  async persistApproval(threadId, runId, checkpointRef, input, interruptContent) {
273
362
  const approval = createPendingApproval(threadId, runId, checkpointRef, input, interruptContent);
274
363
  await this.persistence.createApproval(approval);
@@ -381,68 +470,25 @@ export class AgentHarness {
381
470
  if (!policyDecision.allowed) {
382
471
  throw new Error(`Policy evaluation blocked agent ${selectedAgentId}: ${policyDecision.reasons.join(", ")}`);
383
472
  }
384
- const threadId = options.threadId ?? createPersistentId();
385
- const runId = createPersistentId();
386
- const createdAt = new Date().toISOString();
387
- if (!options.threadId) {
388
- await this.persistence.createThread({
389
- threadId,
390
- agentId: selectedAgentId,
391
- runId,
392
- status: "running",
393
- createdAt,
394
- });
395
- }
396
- await this.persistence.appendThreadMessage(threadId, {
397
- role: "user",
398
- content: options.input,
399
- runId,
400
- createdAt,
401
- });
402
- await this.persistence.createRun({
403
- threadId,
404
- runId,
405
- agentId: binding.agent.id,
406
- executionMode: binding.agent.executionMode,
407
- createdAt,
408
- });
409
- await this.emit(threadId, runId, 1, "run.created", {
473
+ const { threadId, runId } = await this.ensureThreadStarted(selectedAgentId, binding, options.input, options.threadId);
474
+ await this.emitRunCreated(threadId, runId, {
410
475
  agentId: binding.agent.id,
411
476
  requestedAgentId: options.agentId ?? AUTO_AGENT_ID,
412
477
  selectedAgentId,
413
478
  executionMode: binding.agent.executionMode,
414
479
  });
415
480
  try {
416
- const history = await this.persistence.listThreadMessages(threadId);
417
- const priorHistory = history.filter((message) => message.runId !== runId);
418
- const actual = await this.runtimeAdapter.invoke(binding, options.input, threadId, runId, undefined, priorHistory);
481
+ const actual = await this.invokeWithHistory(binding, options.input, threadId, runId);
419
482
  let approval;
420
- if (actual.output) {
421
- await this.persistence.appendThreadMessage(threadId, {
422
- role: "assistant",
423
- content: actual.output,
424
- runId,
425
- createdAt: new Date().toISOString(),
426
- });
427
- }
428
- await this.persistence.setRunState(threadId, runId, actual.state, actual.state === "waiting_for_approval" ? `checkpoints/${threadId}/${runId}/cp-1` : null);
429
- if (actual.state === "waiting_for_approval") {
430
- const checkpointRef = `checkpoints/${threadId}/${runId}/cp-1`;
431
- approval = await this.persistApproval(threadId, runId, checkpointRef, options.input, actual.interruptContent);
432
- await this.emit(threadId, runId, 4, "approval.requested", {
433
- approvalId: approval.approvalId,
434
- pendingActionId: approval.pendingActionId,
435
- toolName: approval.toolName,
436
- toolCallId: approval.toolCallId,
437
- allowedDecisions: approval.allowedDecisions,
438
- checkpointRef,
439
- });
440
- }
441
- await this.emit(threadId, runId, 3, "run.state.changed", {
483
+ await this.appendAssistantMessage(threadId, runId, actual.output);
484
+ const checkpointRef = actual.state === "waiting_for_approval" ? `checkpoints/${threadId}/${runId}/cp-1` : null;
485
+ await this.setRunStateAndEmit(threadId, runId, 3, actual.state, {
442
486
  previousState: null,
443
- state: actual.state,
444
- checkpointRef: actual.state === "waiting_for_approval" ? `checkpoints/${threadId}/${runId}/cp-1` : null,
487
+ checkpointRef,
445
488
  });
489
+ if (actual.state === "waiting_for_approval") {
490
+ approval = (await this.requestApprovalAndEmit(threadId, runId, options.input, actual.interruptContent, checkpointRef, 4)).approval;
491
+ }
446
492
  return {
447
493
  ...actual,
448
494
  threadId,
@@ -453,15 +499,9 @@ export class AgentHarness {
453
499
  };
454
500
  }
455
501
  catch (error) {
456
- await this.emit(threadId, runId, 3, "runtime.synthetic_fallback", {
457
- reason: error instanceof Error ? error.message : String(error),
458
- selectedAgentId,
459
- });
460
- await this.persistence.setRunState(threadId, runId, "failed");
461
- await this.emit(threadId, runId, 4, "run.state.changed", {
502
+ await this.emitSyntheticFallback(threadId, runId, selectedAgentId, error);
503
+ await this.setRunStateAndEmit(threadId, runId, 4, "failed", {
462
504
  previousState: null,
463
- state: "failed",
464
- checkpointRef: null,
465
505
  error: error instanceof Error ? error.message : String(error),
466
506
  });
467
507
  return {
@@ -490,32 +530,8 @@ export class AgentHarness {
490
530
  return;
491
531
  }
492
532
  let emitted = false;
493
- const threadId = options.threadId ?? createPersistentId();
494
- const runId = createPersistentId();
495
- const createdAt = new Date().toISOString();
496
- if (!options.threadId) {
497
- await this.persistence.createThread({
498
- threadId,
499
- agentId: selectedAgentId,
500
- runId,
501
- status: "running",
502
- createdAt,
503
- });
504
- }
505
- await this.persistence.appendThreadMessage(threadId, {
506
- role: "user",
507
- content: options.input,
508
- runId,
509
- createdAt,
510
- });
511
- await this.persistence.createRun({
512
- threadId,
513
- runId,
514
- agentId: selectedAgentId,
515
- executionMode: binding.agent.executionMode,
516
- createdAt,
517
- });
518
- yield { type: "event", event: await this.emit(threadId, runId, 1, "run.created", {
533
+ const { threadId, runId } = await this.ensureThreadStarted(selectedAgentId, binding, options.input, options.threadId);
534
+ yield { type: "event", event: await this.emitRunCreated(threadId, runId, {
519
535
  agentId: selectedAgentId,
520
536
  requestedAgentId: options.agentId ?? AUTO_AGENT_ID,
521
537
  selectedAgentId,
@@ -523,8 +539,7 @@ export class AgentHarness {
523
539
  state: "running",
524
540
  }) };
525
541
  try {
526
- const history = await this.persistence.listThreadMessages(threadId);
527
- const priorHistory = history.filter((message) => message.runId !== runId);
542
+ const priorHistory = await this.loadPriorHistory(threadId, runId);
528
543
  let assistantOutput = "";
529
544
  for await (const chunk of this.runtimeAdapter.stream(binding, options.input, threadId, priorHistory)) {
530
545
  if (chunk) {
@@ -535,26 +550,18 @@ export class AgentHarness {
535
550
  : chunk;
536
551
  if (normalizedChunk.kind === "interrupt") {
537
552
  const checkpointRef = `checkpoints/${threadId}/${runId}/cp-1`;
538
- await this.persistence.setRunState(threadId, runId, "waiting_for_approval", checkpointRef);
539
- const approval = await this.persistApproval(threadId, runId, checkpointRef, options.input, normalizedChunk.content);
553
+ const waitingEvent = await this.setRunStateAndEmit(threadId, runId, 4, "waiting_for_approval", {
554
+ previousState: null,
555
+ checkpointRef,
556
+ });
557
+ const approvalRequest = await this.requestApprovalAndEmit(threadId, runId, options.input, normalizedChunk.content, checkpointRef, 5);
540
558
  yield {
541
559
  type: "event",
542
- event: await this.emit(threadId, runId, 4, "run.state.changed", {
543
- previousState: null,
544
- state: "waiting_for_approval",
545
- checkpointRef,
546
- }),
560
+ event: waitingEvent,
547
561
  };
548
562
  yield {
549
563
  type: "event",
550
- event: await this.emit(threadId, runId, 5, "approval.requested", {
551
- approvalId: approval.approvalId,
552
- pendingActionId: approval.pendingActionId,
553
- toolName: approval.toolName,
554
- toolCallId: approval.toolCallId,
555
- allowedDecisions: approval.allowedDecisions,
556
- checkpointRef,
557
- }),
564
+ event: approvalRequest.event,
558
565
  };
559
566
  return;
560
567
  }
@@ -594,98 +601,46 @@ export class AgentHarness {
594
601
  }
595
602
  emitted = true;
596
603
  assistantOutput += normalizedChunk.content;
597
- await this.emit(threadId, runId, 3, "output.delta", {
598
- content: normalizedChunk.content,
599
- });
600
- yield {
601
- type: "content",
602
- threadId,
603
- runId,
604
- agentId: selectedAgentId,
605
- content: normalizedChunk.content,
606
- };
604
+ yield await this.emitOutputDeltaAndCreateItem(threadId, runId, selectedAgentId, normalizedChunk.content);
607
605
  }
608
606
  }
609
607
  if (!assistantOutput) {
610
- const actual = await this.runtimeAdapter.invoke(binding, options.input, threadId, runId, undefined, priorHistory);
608
+ const actual = await this.invokeWithHistory(binding, options.input, threadId, runId);
611
609
  if (actual.output) {
612
610
  assistantOutput = actual.output;
613
611
  emitted = true;
614
- await this.emit(threadId, runId, 3, "output.delta", {
615
- content: actual.output,
616
- });
617
- yield {
618
- type: "content",
619
- threadId,
620
- runId,
621
- agentId: selectedAgentId,
622
- content: actual.output,
623
- };
612
+ yield await this.emitOutputDeltaAndCreateItem(threadId, runId, selectedAgentId, actual.output);
624
613
  }
625
614
  }
626
- if (assistantOutput) {
627
- await this.persistence.appendThreadMessage(threadId, {
628
- role: "assistant",
629
- content: assistantOutput,
630
- runId,
631
- createdAt: new Date().toISOString(),
632
- });
633
- }
634
- await this.persistence.setRunState(threadId, runId, "completed");
635
- yield { type: "event", event: await this.emit(threadId, runId, 4, "run.state.changed", {
615
+ await this.appendAssistantMessage(threadId, runId, assistantOutput);
616
+ yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 4, "completed", {
636
617
  previousState: null,
637
- state: "completed",
638
- checkpointRef: null,
639
618
  }) };
640
619
  return;
641
620
  }
642
621
  catch (error) {
643
622
  if (emitted) {
644
- await this.persistence.setRunState(threadId, runId, "failed");
645
- yield { type: "event", event: await this.emit(threadId, runId, 4, "run.state.changed", {
623
+ yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 4, "failed", {
646
624
  previousState: null,
647
- state: "failed",
648
625
  error: error instanceof Error ? error.message : String(error),
649
626
  }) };
650
627
  return;
651
628
  }
652
629
  try {
653
- const history = await this.persistence.listThreadMessages(threadId);
654
- const priorHistory = history.filter((message) => message.runId !== runId);
655
- const actual = await this.runtimeAdapter.invoke(binding, options.input, threadId, runId, undefined, priorHistory);
630
+ const actual = await this.invokeWithHistory(binding, options.input, threadId, runId);
631
+ await this.appendAssistantMessage(threadId, runId, actual.output);
656
632
  if (actual.output) {
657
- await this.persistence.appendThreadMessage(threadId, {
658
- role: "assistant",
659
- content: actual.output,
660
- runId,
661
- createdAt: new Date().toISOString(),
662
- });
663
- yield {
664
- type: "content",
665
- threadId,
666
- runId,
667
- agentId: selectedAgentId,
668
- content: actual.output,
669
- };
633
+ yield await this.emitOutputDeltaAndCreateItem(threadId, runId, selectedAgentId, actual.output);
670
634
  }
671
- await this.persistence.setRunState(threadId, runId, actual.state);
672
- yield { type: "event", event: await this.emit(threadId, runId, 4, "run.state.changed", {
635
+ yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 4, actual.state, {
673
636
  previousState: null,
674
- state: actual.state,
675
- checkpointRef: null,
676
637
  }) };
677
638
  return;
678
639
  }
679
640
  catch (invokeError) {
680
- await this.emit(threadId, runId, 3, "runtime.synthetic_fallback", {
681
- reason: invokeError instanceof Error ? invokeError.message : String(invokeError),
682
- selectedAgentId,
683
- });
684
- await this.persistence.setRunState(threadId, runId, "failed");
685
- yield { type: "event", event: await this.emit(threadId, runId, 4, "run.state.changed", {
641
+ await this.emitSyntheticFallback(threadId, runId, selectedAgentId, invokeError);
642
+ yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 4, "failed", {
686
643
  previousState: null,
687
- state: "failed",
688
- checkpointRef: null,
689
644
  error: invokeError instanceof Error ? invokeError.message : String(invokeError),
690
645
  }) };
691
646
  yield {
@@ -737,14 +692,7 @@ export class AgentHarness {
737
692
  ? { decision: "edit", editedInput: options.editedInput }
738
693
  : (options.decision ?? "approve");
739
694
  const actual = await this.runtimeAdapter.invoke(binding, "", threadId, runId, resumeDecision, priorHistory);
740
- if (actual.output) {
741
- await this.persistence.appendThreadMessage(threadId, {
742
- role: "assistant",
743
- content: actual.output,
744
- runId,
745
- createdAt: new Date().toISOString(),
746
- });
747
- }
695
+ await this.appendAssistantMessage(threadId, runId, actual.output);
748
696
  await this.persistence.setRunState(threadId, runId, actual.state, actual.state === "waiting_for_approval" ? `checkpoints/${threadId}/${runId}/cp-1` : null);
749
697
  await this.emit(threadId, runId, 7, "run.state.changed", {
750
698
  previousState: "resuming",
@@ -1,15 +1,12 @@
1
- import path from "node:path";
2
- import { createHash } from "node:crypto";
3
- import { ensureResourceSources, listResourceTools, listResourceToolsForSource } from "../resource/resource.js";
4
- import { listRemoteMcpTools } from "../resource/resource-impl.js";
5
- import { ensureExternalResourceSource, isExternalSourceLocator } from "../resource/sources.js";
6
- import { loadWorkspaceObjects, readToolModuleItems } from "./object-loader.js";
1
+ import { ensureResourceSources } from "../resource/resource.js";
2
+ import { loadWorkspaceObjects } from "./object-loader.js";
7
3
  import { parseEmbeddingModelObject, parseMcpServerObject, parseModelObject, parseToolObject, parseVectorStoreObject, validateEmbeddingModelObject, validateMcpServerObject, validateModelObject, validateToolObject, validateVectorStoreObject, } from "./resource-compilers.js";
8
4
  import { validateAgent, validateTopology } from "./validate.js";
9
5
  import { compileBinding } from "./agent-binding-compiler.js";
10
6
  import { discoverSubagents, ensureDiscoverySources } from "./support/discovery.js";
11
7
  import { collectAgentDiscoverySourceRefs, collectToolSourceRefs } from "./support/source-collectors.js";
12
8
  import { resolveRefId } from "./support/workspace-ref-utils.js";
9
+ import { hydrateAgentMcpTools, hydrateResourceAndExternalTools } from "./tool-hydration.js";
13
10
  function collectParsedResources(refs) {
14
11
  const embeddings = new Map();
15
12
  const mcpServers = new Map();
@@ -39,154 +36,6 @@ function collectParsedResources(refs) {
39
36
  }
40
37
  return { embeddings, mcpServers, models, vectorStores, tools };
41
38
  }
42
- async function hydrateResourceAndExternalTools(tools, toolSourceRefs, workspaceRoot) {
43
- for (const source of toolSourceRefs) {
44
- if (isExternalSourceLocator(source)) {
45
- const externalRoot = await ensureExternalResourceSource(source, workspaceRoot);
46
- const discoveredToolRefs = [];
47
- const sourcePrefix = `external.${createHash("sha256").update(source).digest("hex").slice(0, 12)}`;
48
- for (const { item, sourcePath } of await readToolModuleItems(path.join(externalRoot, "tools"))) {
49
- const toolId = typeof item.id === "string" ? item.id : undefined;
50
- if (!toolId) {
51
- continue;
52
- }
53
- const namespacedId = `${sourcePrefix}.${toolId}`;
54
- const parsed = parseToolObject({
55
- id: namespacedId,
56
- kind: "tool",
57
- sourcePath,
58
- value: {
59
- ...item,
60
- id: namespacedId,
61
- },
62
- });
63
- tools.set(parsed.id, parsed);
64
- discoveredToolRefs.push(`tool/${parsed.id}`);
65
- }
66
- const sourceTools = await listResourceToolsForSource(source, workspaceRoot);
67
- const bundleRefs = [...sourceTools.map((tool) => tool.toolPath), ...discoveredToolRefs];
68
- if (bundleRefs.length > 0) {
69
- tools.set(source, {
70
- id: source,
71
- type: "bundle",
72
- name: source,
73
- description: `External tool resource loaded from ${source}`,
74
- bundleRefs,
75
- sourcePath: source,
76
- });
77
- }
78
- }
79
- }
80
- for (const resourceTool of await listResourceTools(toolSourceRefs, workspaceRoot)) {
81
- const existing = tools.get(resourceTool.toolPath);
82
- tools.set(resourceTool.toolPath, {
83
- id: resourceTool.toolPath,
84
- type: existing?.type ?? "backend",
85
- name: existing?.name || resourceTool.name,
86
- description: existing?.description || resourceTool.description,
87
- config: existing?.config,
88
- backendOperation: existing?.backendOperation ?? resourceTool.backendOperation,
89
- bundleRefs: existing?.bundleRefs ?? [],
90
- hitl: existing?.hitl ?? resourceTool.hitl,
91
- sourcePath: existing?.sourcePath ?? resourceTool.toolPath,
92
- });
93
- }
94
- }
95
- function toMcpServerConfig(server) {
96
- return {
97
- transport: server.transport,
98
- command: server.command,
99
- args: server.args,
100
- env: server.env,
101
- cwd: server.cwd,
102
- url: server.url,
103
- token: server.token,
104
- headers: server.headers,
105
- };
106
- }
107
- function readStringArray(value) {
108
- return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
109
- }
110
- function compileRegexList(value, label) {
111
- return readStringArray(value).map((pattern) => {
112
- try {
113
- return new RegExp(pattern);
114
- }
115
- catch (error) {
116
- throw new Error(`${label} contains invalid regex ${JSON.stringify(pattern)}: ${error instanceof Error ? error.message : String(error)}`);
117
- }
118
- });
119
- }
120
- function compileMcpToolFilter(serverItem) {
121
- return {
122
- includeNames: new Set(readStringArray(serverItem.tools)),
123
- excludeNames: new Set(readStringArray(serverItem.excludeTools)),
124
- includePatterns: compileRegexList(serverItem.toolFilters ?? serverItem.toolFilter, "toolFilter"),
125
- excludePatterns: compileRegexList(serverItem.excludeToolFilters ?? serverItem.excludeToolFilter, "excludeToolFilter"),
126
- };
127
- }
128
- function shouldIncludeRemoteMcpTool(filter, toolName) {
129
- const { includeNames, excludeNames, includePatterns, excludePatterns } = filter;
130
- const includedByName = includeNames.size === 0 || includeNames.has(toolName);
131
- const includedByPattern = includePatterns.length === 0 || includePatterns.some((pattern) => pattern.test(toolName));
132
- if (!includedByName || !includedByPattern) {
133
- return false;
134
- }
135
- if (excludeNames.has(toolName)) {
136
- return false;
137
- }
138
- if (excludePatterns.some((pattern) => pattern.test(toolName))) {
139
- return false;
140
- }
141
- return true;
142
- }
143
- async function hydrateAgentMcpTools(agents, mcpServers, tools) {
144
- for (const agent of agents) {
145
- const discoveredRefs = new Set(agent.toolRefs);
146
- for (const item of agent.mcpServers ?? []) {
147
- const name = typeof item.name === "string" && item.name.trim()
148
- ? item.name.trim()
149
- : typeof item.id === "string" && item.id.trim()
150
- ? item.id.trim()
151
- : "";
152
- if (!name) {
153
- throw new Error(`Agent ${agent.id} has an MCP server entry without a name`);
154
- }
155
- const serverId = `${agent.id}.${name}`;
156
- const parsedServer = parseMcpServerObject({
157
- id: serverId,
158
- kind: "mcp",
159
- sourcePath: agent.sourcePath,
160
- value: item,
161
- });
162
- const filter = compileMcpToolFilter(item);
163
- mcpServers.set(serverId, parsedServer);
164
- const remoteTools = await listRemoteMcpTools(toMcpServerConfig(parsedServer));
165
- for (const remoteTool of remoteTools) {
166
- if (!shouldIncludeRemoteMcpTool(filter, remoteTool.name)) {
167
- continue;
168
- }
169
- const toolId = `mcp.${agent.id}.${name}.${remoteTool.name}`;
170
- tools.set(toolId, {
171
- id: toolId,
172
- type: "mcp",
173
- name: remoteTool.name,
174
- description: remoteTool.description ?? remoteTool.name,
175
- config: {
176
- mcp: {
177
- serverRef: `mcp/${serverId}`,
178
- },
179
- },
180
- mcpRef: remoteTool.name,
181
- bundleRefs: [],
182
- sourcePath: agent.sourcePath,
183
- });
184
- discoveredRefs.add(`tool/${toolId}`);
185
- }
186
- }
187
- agent.toolRefs = Array.from(discoveredRefs);
188
- }
189
- }
190
39
  function validateWorkspaceResources(embeddings, mcpServers, models, vectorStores, tools, agents) {
191
40
  embeddings.forEach((embedding) => validateEmbeddingModelObject(embedding));
192
41
  mcpServers.forEach((server) => validateMcpServerObject(server));
@@ -7,18 +7,7 @@ import { resolveIsolatedResourceModulePath } from "../resource/isolation.js";
7
7
  import { 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
- const ROOT_AGENT_FILENAMES = [
11
- "agent.yaml",
12
- "agent.yml",
13
- "orchestra.yaml",
14
- "orchestra.yml",
15
- "direct.yaml",
16
- "direct.yml",
17
- "research.yaml",
18
- "research.yml",
19
- ];
20
10
  const MODEL_FILENAMES = ["models.yaml", "models.yml"];
21
- const LEGACY_GLOBAL_AGENT_FILENAMES = ["agent.yaml", "agent.yml"];
22
11
  const CONVENTIONAL_OBJECT_DIRECTORIES = ["tools"];
23
12
  function conventionalConfigRoot(root) {
24
13
  if (path.basename(root) === "config" && existsSync(root) && statSync(root).isDirectory()) {
@@ -220,6 +209,27 @@ function readObjectArray(items) {
220
209
  .map((item) => ({ ...item }));
221
210
  return records.length > 0 ? records : undefined;
222
211
  }
212
+ function readSharedAgentConfig(item) {
213
+ const middleware = readMiddlewareArray(item.middleware);
214
+ return {
215
+ ...(typeof item.systemPrompt === "string" ? { systemPrompt: item.systemPrompt } : {}),
216
+ ...(typeof item.checkpointer === "object" && item.checkpointer ? { checkpointer: item.checkpointer } : {}),
217
+ ...(typeof item.interruptOn === "object" && item.interruptOn ? { interruptOn: item.interruptOn } : {}),
218
+ ...(item.responseFormat !== undefined ? { responseFormat: item.responseFormat } : {}),
219
+ ...(item.contextSchema !== undefined ? { contextSchema: item.contextSchema } : {}),
220
+ ...(middleware ? { middleware } : {}),
221
+ };
222
+ }
223
+ function readLangchainAgentConfig(item) {
224
+ return readSharedAgentConfig(item);
225
+ }
226
+ function readDeepAgentConfig(item) {
227
+ return {
228
+ ...readSharedAgentConfig(item),
229
+ ...(typeof item.backend === "object" && item.backend ? { backend: item.backend } : {}),
230
+ ...(typeof item.store === "object" && item.store ? { store: item.store } : {}),
231
+ };
232
+ }
223
233
  export function parseAgentItem(item, sourcePath) {
224
234
  const subagentRefs = readRefArray(item.subagents);
225
235
  const subagentPathRefs = readPathArray(item.subagents);
@@ -239,44 +249,8 @@ export function parseAgentItem(item, sourcePath) {
239
249
  memorySources: readPathArray(item.memory),
240
250
  subagentRefs,
241
251
  subagentPathRefs,
242
- langchainAgentConfig: {
243
- ...(typeof item.systemPrompt === "string" ||
244
- typeof item.interruptOn === "object" ||
245
- typeof item.checkpointer === "object" ||
246
- item.responseFormat !== undefined ||
247
- item.contextSchema !== undefined ||
248
- item.middleware !== undefined
249
- ? {
250
- ...(typeof item.systemPrompt === "string" ? { systemPrompt: item.systemPrompt } : {}),
251
- ...(typeof item.interruptOn === "object" && item.interruptOn ? { interruptOn: item.interruptOn } : {}),
252
- ...(typeof item.checkpointer === "object" && item.checkpointer ? { checkpointer: item.checkpointer } : {}),
253
- ...(item.responseFormat !== undefined ? { responseFormat: item.responseFormat } : {}),
254
- ...(item.contextSchema !== undefined ? { contextSchema: item.contextSchema } : {}),
255
- ...(readMiddlewareArray(item.middleware) ? { middleware: readMiddlewareArray(item.middleware) } : {}),
256
- }
257
- : {}),
258
- },
259
- deepAgentConfig: {
260
- ...(typeof item.systemPrompt === "string" ||
261
- typeof item.backend === "object" ||
262
- typeof item.store === "object" ||
263
- typeof item.checkpointer === "object" ||
264
- typeof item.interruptOn === "object" ||
265
- item.responseFormat !== undefined ||
266
- item.contextSchema !== undefined ||
267
- item.middleware !== undefined
268
- ? {
269
- ...(typeof item.systemPrompt === "string" ? { systemPrompt: item.systemPrompt } : {}),
270
- ...(typeof item.backend === "object" && item.backend ? { backend: item.backend } : {}),
271
- ...(typeof item.store === "object" && item.store ? { store: item.store } : {}),
272
- ...(typeof item.checkpointer === "object" && item.checkpointer ? { checkpointer: item.checkpointer } : {}),
273
- ...(typeof item.interruptOn === "object" && item.interruptOn ? { interruptOn: item.interruptOn } : {}),
274
- ...(item.responseFormat !== undefined ? { responseFormat: item.responseFormat } : {}),
275
- ...(item.contextSchema !== undefined ? { contextSchema: item.contextSchema } : {}),
276
- ...(readMiddlewareArray(item.middleware) ? { middleware: readMiddlewareArray(item.middleware) } : {}),
277
- }
278
- : {}),
279
- },
252
+ langchainAgentConfig: readLangchainAgentConfig(item),
253
+ deepAgentConfig: readDeepAgentConfig(item),
280
254
  sourcePath,
281
255
  };
282
256
  }
@@ -346,6 +320,89 @@ function mergeValues(base, override) {
346
320
  }
347
321
  return override;
348
322
  }
323
+ function mergeRawItemRecord(records, key, item, sourcePath) {
324
+ const current = records.get(key);
325
+ const mergedRecord = {
326
+ item: current ? mergeValues(current.item, item) : item,
327
+ sourcePath,
328
+ };
329
+ records.set(key, mergedRecord);
330
+ return mergedRecord;
331
+ }
332
+ function mergeAgentRecord(records, item, sourcePath) {
333
+ const id = typeof item.id === "string" ? item.id : undefined;
334
+ if (!id) {
335
+ return null;
336
+ }
337
+ return mergeRawItemRecord(records, id, item, sourcePath);
338
+ }
339
+ function mergeWorkspaceObjectRecord(records, workspaceObject, item, sourcePath) {
340
+ mergeRawItemRecord(records, `${workspaceObject.kind}/${workspaceObject.id}`, item, sourcePath);
341
+ }
342
+ async function loadNamedModelsForRoot(configRoot, mergedObjects) {
343
+ for (const { item, sourcePath } of await readNamedModelItems(configRoot)) {
344
+ const workspaceObject = parseWorkspaceObject(item, sourcePath);
345
+ if (!workspaceObject || workspaceObject.kind !== "model") {
346
+ continue;
347
+ }
348
+ mergeWorkspaceObjectRecord(mergedObjects, workspaceObject, item, sourcePath);
349
+ }
350
+ }
351
+ async function loadConfigAgentsForRoot(configRoot, mergedAgents) {
352
+ for (const { item, sourcePath } of await readConfigAgentItems(configRoot)) {
353
+ mergeAgentRecord(mergedAgents, item, sourcePath);
354
+ }
355
+ }
356
+ async function loadConventionalObjectsForRoot(root, mergedObjects) {
357
+ for (const directory of CONVENTIONAL_OBJECT_DIRECTORIES) {
358
+ for (const objectRoot of conventionalDirectoryRoots(root, directory)) {
359
+ for (const { item, sourcePath } of await readYamlItems(objectRoot, undefined, { recursive: true })) {
360
+ const workspaceObject = parseWorkspaceObject(item, sourcePath);
361
+ if (!workspaceObject || workspaceObject.kind === "tool") {
362
+ continue;
363
+ }
364
+ mergeWorkspaceObjectRecord(mergedObjects, workspaceObject, item, sourcePath);
365
+ }
366
+ for (const { item, sourcePath } of await readToolModuleItems(objectRoot)) {
367
+ const workspaceObject = parseWorkspaceObject(item, sourcePath);
368
+ if (!workspaceObject) {
369
+ continue;
370
+ }
371
+ mergeWorkspaceObjectRecord(mergedObjects, workspaceObject, item, sourcePath);
372
+ }
373
+ }
374
+ }
375
+ }
376
+ async function loadConfigObjectsForRoot(root, configRoot, mergedObjects) {
377
+ if (!conventionalConfigRoot(root)) {
378
+ return;
379
+ }
380
+ for (const { item, sourcePath } of await readYamlItems(configRoot, undefined, { recursive: true })) {
381
+ const workspaceObject = parseWorkspaceObject(item, sourcePath);
382
+ if (!workspaceObject) {
383
+ continue;
384
+ }
385
+ if (isAgentKind(workspaceObject.kind) || workspaceObject.kind === "model") {
386
+ continue;
387
+ }
388
+ mergeWorkspaceObjectRecord(mergedObjects, workspaceObject, item, sourcePath);
389
+ }
390
+ }
391
+ async function loadRootObjects(root, mergedObjects) {
392
+ for (const { item, sourcePath } of (await readYamlItems(root)).filter(({ sourcePath: fullPath }) => !fullPath.includes(`${path.sep}config${path.sep}`) &&
393
+ !fullPath.includes(`${path.sep}resources${path.sep}`) &&
394
+ !fullPath.includes(`${path.sep}agents${path.sep}`) &&
395
+ !CONVENTIONAL_OBJECT_DIRECTORIES.some((directory) => fullPath.includes(`${path.sep}${directory}${path.sep}`)))) {
396
+ const workspaceObject = parseWorkspaceObject(item, sourcePath);
397
+ if (!workspaceObject) {
398
+ continue;
399
+ }
400
+ if (workspaceObject.kind === "tool" || workspaceObject.kind === "mcp" || workspaceObject.kind === "model") {
401
+ continue;
402
+ }
403
+ mergeWorkspaceObjectRecord(mergedObjects, workspaceObject, item, sourcePath);
404
+ }
405
+ }
349
406
  export async function readYamlItems(root, relativeDir, options = {}) {
350
407
  const targetRoot = relativeDir ? path.join(root, relativeDir) : root;
351
408
  if (!(await fileExists(targetRoot))) {
@@ -405,15 +462,13 @@ function isAgentKind(kind) {
405
462
  return kind === "deepagent" || kind === "langchain-agent";
406
463
  }
407
464
  async function readConfigAgentItems(configRoot) {
408
- const records = await readYamlItems(configRoot, undefined, { recursive: true });
465
+ const records = await readYamlItems(configRoot, "agents", { recursive: true });
409
466
  return records.filter(({ item, sourcePath }) => {
410
467
  const kind = typeof item.kind === "string" ? item.kind : undefined;
411
468
  if (!isAgentKind(kind)) {
412
469
  return false;
413
470
  }
414
- const parentDir = path.dirname(sourcePath);
415
- const filename = path.basename(sourcePath).toLowerCase();
416
- return !(parentDir === configRoot && ROOT_AGENT_FILENAMES.includes(filename));
471
+ return sourcePath.includes(`${path.sep}agents${path.sep}`);
417
472
  });
418
473
  }
419
474
  export async function readToolModuleItems(root) {
@@ -451,10 +506,6 @@ export async function readToolModuleItems(root) {
451
506
  }
452
507
  return records;
453
508
  }
454
- function isPrimaryAgentFile(sourcePath) {
455
- const base = path.basename(sourcePath).toLowerCase();
456
- return ROOT_AGENT_FILENAMES.includes(base);
457
- }
458
509
  function inferExecutionMode(item, current) {
459
510
  const kind = typeof item.kind === "string" ? item.kind : typeof current?.kind === "string" ? current.kind : undefined;
460
511
  if (kind === "langchain-agent") {
@@ -465,160 +516,18 @@ function inferExecutionMode(item, current) {
465
516
  }
466
517
  return undefined;
467
518
  }
468
- function extractSharedAgentDefaults(item) {
469
- const defaults = {};
470
- for (const key of ["modelRef", "runRoot", "checkpointer", "interruptOn"]) {
471
- if (key in item) {
472
- defaults[key] = item[key];
473
- }
474
- }
475
- return defaults;
476
- }
477
- function applySharedAgentDefaults(globalDefaults, defaultsByMode, item, current) {
478
- const executionMode = inferExecutionMode(item, current);
479
- const defaults = {
480
- ...globalDefaults,
481
- ...(executionMode ? defaultsByMode[executionMode] : {}),
482
- };
483
- const merged = { ...item };
484
- for (const [key, value] of Object.entries(defaults)) {
485
- if (key in merged) {
486
- continue;
487
- }
488
- if (current && key in current) {
489
- continue;
490
- }
491
- merged[key] = value;
492
- }
493
- return merged;
494
- }
495
519
  export async function loadWorkspaceObjects(workspaceRoot, options = {}) {
496
520
  const refs = new Map();
497
521
  const mergedAgents = new Map();
498
522
  const mergedObjects = new Map();
499
523
  const roots = [frameworkWorkspaceRoot(), ...(options.overlayRoots ?? []), workspaceRoot];
500
- let sharedAgentDefaults = {};
501
- let sharedAgentDefaultsByMode = {};
502
524
  for (const root of roots) {
503
525
  const configRoot = conventionalConfigRoot(root) ?? root;
504
- const namedAgentRoots = Array.from(new Set([root, configRoot]));
505
- for (const namedAgentRoot of namedAgentRoots) {
506
- for (const { item, sourcePath } of await readNamedYamlItems(namedAgentRoot, [...ROOT_AGENT_FILENAMES])) {
507
- const id = typeof item.id === "string" ? item.id : undefined;
508
- if (!id) {
509
- continue;
510
- }
511
- const current = mergedAgents.get(id);
512
- mergedAgents.set(id, {
513
- item: current ? mergeValues(current.item, item) : item,
514
- sourcePath,
515
- });
516
- const filename = path.basename(sourcePath).toLowerCase();
517
- if (LEGACY_GLOBAL_AGENT_FILENAMES.includes(filename)) {
518
- sharedAgentDefaults = mergeValues(sharedAgentDefaults, extractSharedAgentDefaults(item));
519
- }
520
- else {
521
- const executionMode = inferExecutionMode(item, current?.item);
522
- if (executionMode) {
523
- sharedAgentDefaultsByMode = {
524
- ...sharedAgentDefaultsByMode,
525
- [executionMode]: mergeValues(sharedAgentDefaultsByMode[executionMode] ?? {}, extractSharedAgentDefaults(item)),
526
- };
527
- }
528
- }
529
- }
530
- }
531
- for (const modelRoot of Array.from(new Set([configRoot]))) {
532
- for (const { item, sourcePath } of await readNamedModelItems(modelRoot)) {
533
- const workspaceObject = parseWorkspaceObject(item, sourcePath);
534
- if (!workspaceObject || workspaceObject.kind !== "model") {
535
- continue;
536
- }
537
- const ref = `${workspaceObject.kind}/${workspaceObject.id}`;
538
- const current = mergedObjects.get(ref);
539
- mergedObjects.set(ref, {
540
- item: current ? mergeValues(current.item, item) : item,
541
- sourcePath,
542
- });
543
- }
544
- }
545
- for (const { item, sourcePath } of await readConfigAgentItems(configRoot)) {
546
- const id = typeof item.id === "string" ? item.id : undefined;
547
- if (!id) {
548
- continue;
549
- }
550
- const current = mergedAgents.get(id);
551
- const itemWithDefaults = applySharedAgentDefaults(sharedAgentDefaults, sharedAgentDefaultsByMode, item, current?.item);
552
- mergedAgents.set(id, {
553
- item: current ? mergeValues(current.item, itemWithDefaults) : itemWithDefaults,
554
- sourcePath,
555
- });
556
- }
557
- for (const directory of CONVENTIONAL_OBJECT_DIRECTORIES) {
558
- for (const objectRoot of conventionalDirectoryRoots(root, directory)) {
559
- for (const { item, sourcePath } of await readYamlItems(objectRoot, undefined, { recursive: true })) {
560
- const workspaceObject = parseWorkspaceObject(item, sourcePath);
561
- if (!workspaceObject || workspaceObject.kind === "tool") {
562
- continue;
563
- }
564
- const ref = `${workspaceObject.kind}/${workspaceObject.id}`;
565
- const current = mergedObjects.get(ref);
566
- mergedObjects.set(ref, {
567
- item: current ? mergeValues(current.item, item) : item,
568
- sourcePath,
569
- });
570
- }
571
- for (const { item, sourcePath } of await readToolModuleItems(objectRoot)) {
572
- const workspaceObject = parseWorkspaceObject(item, sourcePath);
573
- if (!workspaceObject) {
574
- continue;
575
- }
576
- const ref = `${workspaceObject.kind}/${workspaceObject.id}`;
577
- const current = mergedObjects.get(ref);
578
- mergedObjects.set(ref, {
579
- item: current ? mergeValues(current.item, item) : item,
580
- sourcePath,
581
- });
582
- }
583
- }
584
- }
585
- if (conventionalConfigRoot(root)) {
586
- for (const { item, sourcePath } of await readYamlItems(configRoot, undefined, { recursive: true })) {
587
- const workspaceObject = parseWorkspaceObject(item, sourcePath);
588
- if (!workspaceObject) {
589
- continue;
590
- }
591
- if (isAgentKind(workspaceObject.kind) ||
592
- workspaceObject.kind === "model") {
593
- continue;
594
- }
595
- const ref = `${workspaceObject.kind}/${workspaceObject.id}`;
596
- const current = mergedObjects.get(ref);
597
- mergedObjects.set(ref, {
598
- item: current ? mergeValues(current.item, item) : item,
599
- sourcePath,
600
- });
601
- }
602
- }
603
- for (const { item, sourcePath } of (await readYamlItems(root)).filter(({ sourcePath: fullPath }) => !fullPath.includes(`${path.sep}config${path.sep}`) &&
604
- !fullPath.includes(`${path.sep}resources${path.sep}`) &&
605
- !fullPath.includes(`${path.sep}agents${path.sep}`) &&
606
- !CONVENTIONAL_OBJECT_DIRECTORIES.some((directory) => fullPath.includes(`${path.sep}${directory}${path.sep}`)) &&
607
- !isPrimaryAgentFile(fullPath))) {
608
- const workspaceObject = parseWorkspaceObject(item, sourcePath);
609
- if (!workspaceObject) {
610
- continue;
611
- }
612
- if (workspaceObject.kind === "tool" || workspaceObject.kind === "mcp" || workspaceObject.kind === "model") {
613
- continue;
614
- }
615
- const ref = `${workspaceObject.kind}/${workspaceObject.id}`;
616
- const current = mergedObjects.get(ref);
617
- mergedObjects.set(ref, {
618
- item: current ? mergeValues(current.item, item) : item,
619
- sourcePath,
620
- });
621
- }
526
+ await loadNamedModelsForRoot(configRoot, mergedObjects);
527
+ await loadConfigAgentsForRoot(configRoot, mergedAgents);
528
+ await loadConventionalObjectsForRoot(root, mergedObjects);
529
+ await loadConfigObjectsForRoot(root, configRoot, mergedObjects);
530
+ await loadRootObjects(root, mergedObjects);
622
531
  }
623
532
  const agents = Array.from(mergedAgents.values()).map(({ item, sourcePath }) => parseAgentItem(item, sourcePath));
624
533
  for (const [ref, { item, sourcePath }] of mergedObjects) {
@@ -0,0 +1,3 @@
1
+ import type { ParsedAgentObject, ParsedMcpServerObject, ParsedToolObject } from "../contracts/types.js";
2
+ export declare function hydrateResourceAndExternalTools(tools: Map<string, ParsedToolObject>, toolSourceRefs: string[], workspaceRoot: string): Promise<void>;
3
+ export declare function hydrateAgentMcpTools(agents: ParsedAgentObject[], mcpServers: Map<string, ParsedMcpServerObject>, tools: Map<string, ParsedToolObject>): Promise<void>;
@@ -0,0 +1,158 @@
1
+ import path from "node:path";
2
+ import { createHash } from "node:crypto";
3
+ import { listRemoteMcpTools } from "../resource/resource-impl.js";
4
+ import { ensureExternalResourceSource, isExternalSourceLocator } from "../resource/sources.js";
5
+ import { listResourceTools, listResourceToolsForSource } from "../resource/resource.js";
6
+ import { readToolModuleItems } from "./object-loader.js";
7
+ import { parseMcpServerObject, parseToolObject } from "./resource-compilers.js";
8
+ function toMcpServerConfig(server) {
9
+ return {
10
+ transport: server.transport,
11
+ command: server.command,
12
+ args: server.args,
13
+ env: server.env,
14
+ cwd: server.cwd,
15
+ url: server.url,
16
+ token: server.token,
17
+ headers: server.headers,
18
+ };
19
+ }
20
+ function readStringArray(value) {
21
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
22
+ }
23
+ function compileRegexList(value, label) {
24
+ return readStringArray(value).map((pattern) => {
25
+ try {
26
+ return new RegExp(pattern);
27
+ }
28
+ catch (error) {
29
+ throw new Error(`${label} contains invalid regex ${JSON.stringify(pattern)}: ${error instanceof Error ? error.message : String(error)}`);
30
+ }
31
+ });
32
+ }
33
+ function compileMcpToolFilter(serverItem) {
34
+ return {
35
+ includeNames: new Set(readStringArray(serverItem.tools)),
36
+ excludeNames: new Set(readStringArray(serverItem.excludeTools)),
37
+ includePatterns: compileRegexList(serverItem.toolFilters ?? serverItem.toolFilter, "toolFilter"),
38
+ excludePatterns: compileRegexList(serverItem.excludeToolFilters ?? serverItem.excludeToolFilter, "excludeToolFilter"),
39
+ };
40
+ }
41
+ function shouldIncludeRemoteMcpTool(filter, toolName) {
42
+ const { includeNames, excludeNames, includePatterns, excludePatterns } = filter;
43
+ const includedByName = includeNames.size === 0 || includeNames.has(toolName);
44
+ const includedByPattern = includePatterns.length === 0 || includePatterns.some((pattern) => pattern.test(toolName));
45
+ if (!includedByName || !includedByPattern) {
46
+ return false;
47
+ }
48
+ if (excludeNames.has(toolName)) {
49
+ return false;
50
+ }
51
+ if (excludePatterns.some((pattern) => pattern.test(toolName))) {
52
+ return false;
53
+ }
54
+ return true;
55
+ }
56
+ async function hydrateExternalToolSource(tools, source, workspaceRoot) {
57
+ const externalRoot = await ensureExternalResourceSource(source, workspaceRoot);
58
+ const discoveredToolRefs = [];
59
+ const sourcePrefix = `external.${createHash("sha256").update(source).digest("hex").slice(0, 12)}`;
60
+ for (const { item, sourcePath } of await readToolModuleItems(path.join(externalRoot, "tools"))) {
61
+ const toolId = typeof item.id === "string" ? item.id : undefined;
62
+ if (!toolId) {
63
+ continue;
64
+ }
65
+ const namespacedId = `${sourcePrefix}.${toolId}`;
66
+ const parsed = parseToolObject({
67
+ id: namespacedId,
68
+ kind: "tool",
69
+ sourcePath,
70
+ value: {
71
+ ...item,
72
+ id: namespacedId,
73
+ },
74
+ });
75
+ tools.set(parsed.id, parsed);
76
+ discoveredToolRefs.push(`tool/${parsed.id}`);
77
+ }
78
+ const sourceTools = await listResourceToolsForSource(source, workspaceRoot);
79
+ const bundleRefs = [...sourceTools.map((tool) => tool.toolPath), ...discoveredToolRefs];
80
+ if (bundleRefs.length > 0) {
81
+ tools.set(source, {
82
+ id: source,
83
+ type: "bundle",
84
+ name: source,
85
+ description: `External tool resource loaded from ${source}`,
86
+ bundleRefs,
87
+ sourcePath: source,
88
+ });
89
+ }
90
+ }
91
+ export async function hydrateResourceAndExternalTools(tools, toolSourceRefs, workspaceRoot) {
92
+ for (const source of toolSourceRefs) {
93
+ if (isExternalSourceLocator(source)) {
94
+ await hydrateExternalToolSource(tools, source, workspaceRoot);
95
+ }
96
+ }
97
+ for (const resourceTool of await listResourceTools(toolSourceRefs, workspaceRoot)) {
98
+ const existing = tools.get(resourceTool.toolPath);
99
+ tools.set(resourceTool.toolPath, {
100
+ id: resourceTool.toolPath,
101
+ type: existing?.type ?? "backend",
102
+ name: existing?.name || resourceTool.name,
103
+ description: existing?.description || resourceTool.description,
104
+ config: existing?.config,
105
+ backendOperation: existing?.backendOperation ?? resourceTool.backendOperation,
106
+ bundleRefs: existing?.bundleRefs ?? [],
107
+ hitl: existing?.hitl ?? resourceTool.hitl,
108
+ sourcePath: existing?.sourcePath ?? resourceTool.toolPath,
109
+ });
110
+ }
111
+ }
112
+ export async function hydrateAgentMcpTools(agents, mcpServers, tools) {
113
+ for (const agent of agents) {
114
+ const discoveredRefs = new Set(agent.toolRefs);
115
+ for (const item of agent.mcpServers ?? []) {
116
+ const name = typeof item.name === "string" && item.name.trim()
117
+ ? item.name.trim()
118
+ : typeof item.id === "string" && item.id.trim()
119
+ ? item.id.trim()
120
+ : "";
121
+ if (!name) {
122
+ throw new Error(`Agent ${agent.id} has an MCP server entry without a name`);
123
+ }
124
+ const serverId = `${agent.id}.${name}`;
125
+ const parsedServer = parseMcpServerObject({
126
+ id: serverId,
127
+ kind: "mcp",
128
+ sourcePath: agent.sourcePath,
129
+ value: item,
130
+ });
131
+ const filter = compileMcpToolFilter(item);
132
+ mcpServers.set(serverId, parsedServer);
133
+ const remoteTools = await listRemoteMcpTools(toMcpServerConfig(parsedServer));
134
+ for (const remoteTool of remoteTools) {
135
+ if (!shouldIncludeRemoteMcpTool(filter, remoteTool.name)) {
136
+ continue;
137
+ }
138
+ const toolId = `mcp.${agent.id}.${name}.${remoteTool.name}`;
139
+ tools.set(toolId, {
140
+ id: toolId,
141
+ type: "mcp",
142
+ name: remoteTool.name,
143
+ description: remoteTool.description ?? remoteTool.name,
144
+ config: {
145
+ mcp: {
146
+ serverRef: `mcp/${serverId}`,
147
+ },
148
+ },
149
+ mcpRef: remoteTool.name,
150
+ bundleRefs: [],
151
+ sourcePath: agent.sourcePath,
152
+ });
153
+ discoveredRefs.add(`tool/${toolId}`);
154
+ }
155
+ }
156
+ agent.toolRefs = Array.from(discoveredRefs);
157
+ }
158
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.22",
3
+ "version": "0.0.23",
4
4
  "description": "Agent Harness framework package",
5
5
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",
@@ -1,44 +0,0 @@
1
- # agent-harness feature: schema version for this declarative config object.
2
- apiVersion: agent-harness/v1alpha1
3
- # agent-harness feature: object type for a named model preset.
4
- # Available options today: `Model`.
5
- # The harness resolves this object into LangChain model construction parameters.
6
- kind: Model
7
- metadata:
8
- # agent-harness feature: stable model object id used by refs such as `model/default`.
9
- name: default
10
- spec:
11
- # ====================
12
- # LangChain v1 Features
13
- # ====================
14
- # LangChain aligned feature: provider family or integration namespace.
15
- # Common options in this harness today include:
16
- # - `ollama`
17
- # - `openai`
18
- # - `openai-compatible`
19
- # - `anthropic`
20
- # - `google` / `google-genai` / `gemini`
21
- # The runtime adapter uses this to select the concrete LangChain chat model implementation.
22
- provider: ollama
23
- # LangChain aligned feature: concrete model identifier passed to the selected provider integration.
24
- # Example values depend on `provider`, such as `gpt-oss:latest` for `ollama`.
25
- model: gpt-oss:latest
26
- init:
27
- # LangChain aligned feature: provider-specific initialization options.
28
- # Available keys are provider-specific; common examples include `baseUrl`, `temperature`, and auth/client settings.
29
- # `baseUrl` configures the Ollama-compatible endpoint used by the model client.
30
- # For `openai-compatible`, `baseUrl` is normalized into the ChatOpenAI `configuration.baseURL` field.
31
- baseUrl: https://ollama-rtx-4070.easynet.world/
32
- # LangChain aligned feature: provider/model initialization option controlling sampling temperature.
33
- temperature: 0.2
34
- # ===================
35
- # DeepAgents Features
36
- # ===================
37
- # DeepAgents uses the same model object shape indirectly through `createDeepAgent({ model })`.
38
- # There is no separate DeepAgents-only field here; DeepAgent bindings consume the same compiled model.
39
-
40
- # ======================
41
- # agent-harness Features
42
- # ======================
43
- # This object is packaged and referenced through `modelRef` fields in harness config, but the actual
44
- # model arguments above map directly onto the upstream LangChain model layer.
File without changes