@salesforce/sfdx-agent-harness-mastra 0.4.0 → 0.6.0

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
@@ -3,7 +3,8 @@
3
3
  Mastra-backed `AgentHarness` implementation for [`@salesforce/sfdx-agent-sdk`](../sfdx-agent-sdk). Provides
4
4
  `MastraHarnessFactory`, the entry point consumers pass to `createAgentManager`.
5
5
 
6
- > **Closed source.** This package is published to npm under the [Salesforce Public Code License](../../LICENSE.txt) and is for use by Salesforce only.
6
+ > **Closed source.** This package is published to npm under the [Salesforce Public Code License](../../LICENSE.txt) and
7
+ > is for use by Salesforce only.
7
8
 
8
9
  ## Install
9
10
 
@@ -25,6 +26,8 @@ internal architecture, see [ARCHITECTURE.md](ARCHITECTURE.md).
25
26
 
26
27
  ## Public API
27
28
 
29
+ ### Core
30
+
28
31
  | Export | Kind | Description |
29
32
  | ------------------------------------- | --------- | ----------------------------------------------------------------------------------------- |
30
33
  | `MastraHarnessFactory` | class | `HarnessFactory` that produces a Mastra-backed `AgentHarness` bound to a storage root. |
@@ -34,6 +37,78 @@ internal architecture, see [ARCHITECTURE.md](ARCHITECTURE.md).
34
37
  | `LLMGExpressMastraModelConfigFactory` | class | Factory for LLM Gateway Express (API-key-authenticated, OpenAI-compatible endpoint). |
35
38
  | `LLMGExpressConfig` | type | Configuration for `LLMGExpressMastraModelConfigFactory` (`apiKey`, `baseUrl`, `modelId`). |
36
39
 
40
+ ### Extension surface
41
+
42
+ These types let consumers reach Mastra-specific features through the SDK's typed `extensions` slot. The SDK infers
43
+ `MastraAgentHarness` from `MastraHarnessFactory.create()`, so consumers usually don't reference these types explicitly —
44
+ they show up via inference at the call site.
45
+
46
+ | Export | Kind | Description |
47
+ | ------------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
48
+ | `MastraAgentHarness` | type | Branded `AgentHarness` subtype; carries the Mastra-specific `extensions` namespace and the inferred config type. |
49
+ | `MastraAgentConfig` | type | `AgentConfig` extended with Mastra-only fields (currently `toolSearch`). Inferred at `manager.createAgent` automatically — annotation rarely needed. |
50
+ | `MastraToolSearchConfig` | type | Shape of `MastraAgentConfig.toolSearch` (`pool`, `alwaysActive`, `topK`, `ttl`). |
51
+ | `formatMcpToolName` | function | `(serverName: string, toolName: string) => string` — builds the namespaced MCP tool name (`${serverName}_${toolName}`) used in `toolSearch.alwaysActive`. |
52
+ | `ToolSearchHandle` | type | Stable interface for the tool-search runtime: `id`, `getStateStats`, `clearState`, `clearAllState`, `cleanupNow`. Wraps Mastra's internal processor. |
53
+ | `SkillSearchHandle` | type | Stable interface for the skill-search runtime: `id` (Mastra's processor has no other public methods today; the harness owns lifecycle). |
54
+
55
+ #### Tool search
56
+
57
+ Hide a large tool catalog behind Mastra's `search_tools` / `load_tool` meta-tools:
58
+
59
+ ```ts
60
+ import { createAgentManager } from '@salesforce/sfdx-agent-sdk';
61
+ import { MastraHarnessFactory, formatMcpToolName } from '@salesforce/sfdx-agent-harness-mastra';
62
+
63
+ const manager = await createAgentManager(storageRoot, new MastraHarnessFactory());
64
+
65
+ const agent = await manager.createAgent(projectRoot, {
66
+ instructions: '...',
67
+ tools: [...customTools],
68
+ mcpServers: { sfdx: { type: 'stdio', command: 'npx', args: ['@salesforce/mcp'] } },
69
+ toolSearch: {
70
+ pool: '*', // or an explicit name list
71
+ alwaysActive: ['lookup_account', formatMcpToolName('sfdx', 'deploy_metadata')],
72
+ topK: 5, // advisory; defaults to Mastra's 5
73
+ ttl: 3_600_000, // ms; advisory; defaults to Mastra's 1h
74
+ },
75
+ });
76
+ ```
77
+
78
+ When `toolSearch` is set, `createAgent` blocks on MCP discovery so MCP tools enter the searchable pool. Without
79
+ `toolSearch`, MCP discovery stays non-blocking (existing behavior). `alwaysActive` tools are visible directly to the
80
+ model; everything else lives in the searchable pool. Unknown `alwaysActive` names are kept (for late MCP discovery);
81
+ unknown `pool` names are dropped.
82
+
83
+ `MastraToolSearchConfig.topK` and `ttl` are advisory — Mastra applies its own defaults if omitted.
84
+
85
+ #### Skill search
86
+
87
+ Skills (`AgentConfig.skills`) load eagerly by default. When `toolSearch` is also set, skills load on demand via
88
+ harness-injected `search_skills` / `load_skill` meta-tools (Mastra's `SkillSearchProcessor`). The gating is deliberate —
89
+ consumers who opt into `toolSearch` get the same on-demand pattern across tools and skills.
90
+
91
+ #### Operational handles
92
+
93
+ Reach the tool-search and skill-search runtime state for an agent. The accessors return stable `ToolSearchHandle` /
94
+ `SkillSearchHandle` interfaces — not Mastra's internal processor types — so consumer code keeps compiling across Mastra
95
+ version bumps.
96
+
97
+ ```ts
98
+ const tsHandle = manager.extensions.mastra.getToolSearchProcessor(agent.getId());
99
+ // ^? ToolSearchHandle | undefined
100
+ const stats = tsHandle?.getStateStats();
101
+ // { threadCount, oldestAccessTime }
102
+
103
+ const ssHandle = manager.extensions.mastra.getSkillSearchProcessor(agent.getId());
104
+ // ^? SkillSearchHandle | undefined
105
+ ```
106
+
107
+ Both return `undefined` when the agent doesn't have the corresponding feature configured (or doesn't exist). The
108
+ Mastra-internal pool / index / loaded-tools state is intentionally not on the handle interface; the documented methods
109
+ are `getStateStats()`, `clearState(threadId?)`, `clearAllState()`, and `cleanupNow()` on `ToolSearchHandle`, plus `id`
110
+ on both handles.
111
+
37
112
  ### Using LLM Gateway Express
38
113
 
39
114
  ```ts
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export { MastraHarnessFactory } from './mastra-harness-factory.js';
2
+ export type { MastraAgentHarness } from './mastra-harness.js';
3
+ export { type MastraAgentConfig, type MastraToolSearchConfig, type SkillSearchHandle, type ToolSearchHandle, formatMcpToolName, } from './tool-search.js';
2
4
  export type { MastraModelConfigFactory, ResolvedModelConfig } from './model-config-factory.js';
3
5
  export { DefaultMastraModelConfigFactory } from './default-model-config-factory.js';
4
6
  export { LLMGExpressMastraModelConfigFactory, type LLMGExpressConfig } from './llmg-express-model-config-factory.js';
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  * See LICENSE.txt for license terms.
4
4
  */
5
5
  export { MastraHarnessFactory } from './mastra-harness-factory.js';
6
+ export { formatMcpToolName, } from './tool-search.js';
6
7
  export { DefaultMastraModelConfigFactory } from './default-model-config-factory.js';
7
8
  export { LLMGExpressMastraModelConfigFactory } from './llmg-express-model-config-factory.js';
8
9
  //# sourceMappingURL=index.js.map
@@ -1,10 +1,11 @@
1
- import type { AgentHarness, HarnessFactory } from '@salesforce/sfdx-agent-sdk';
1
+ import type { HarnessFactory } from '@salesforce/sfdx-agent-sdk';
2
+ import { type MastraAgentHarness } from './mastra-harness.js';
2
3
  import type { MastraModelConfigFactory } from './model-config-factory.js';
3
4
  /** Mastra-backed factory for creating AgentHarness instances. */
4
- export declare class MastraHarnessFactory implements HarnessFactory {
5
+ export declare class MastraHarnessFactory implements HarnessFactory<MastraAgentHarness> {
5
6
  private readonly modelConfigFactory?;
6
7
  readonly harnessId = "mastra";
7
8
  readonly protocolVersion = 1;
8
9
  constructor(modelConfigFactory?: MastraModelConfigFactory | undefined);
9
- create(storageRootFolder: string): Promise<AgentHarness>;
10
+ create(storageRootFolder: string): Promise<MastraAgentHarness>;
10
11
  }
@@ -1,8 +1,10 @@
1
1
  import { Agent } from '@mastra/core/agent';
2
+ import { SkillSearchProcessor, ToolSearchProcessor } from '@mastra/core/processors';
2
3
  import type { ChunkType } from '@mastra/core/stream';
3
4
  import { MCPClient } from '@mastra/mcp';
4
- import { type AgentHarness, type ChatStreamResult, type HarnessAgentConfig, type MCPConfiguration, type McpServerInfo, type Message, type StreamOptions, type TelemetryEventCallback, type ToolResultInfo } from '@salesforce/sfdx-agent-sdk';
5
+ import { type AgentHarness, type WithAgentConfig, type ChatStreamResult, type HarnessAgentConfig, type MCPConfiguration, type McpServerInfo, type Message, type StreamOptions, type TelemetryEventCallback, type ToolResultInfo } from '@salesforce/sfdx-agent-sdk';
5
6
  import type { MastraModelConfigFactory } from './model-config-factory.js';
7
+ import { type MastraAgentConfig, type SkillSearchHandle, type ToolSearchHandle } from './tool-search.js';
6
8
  import type { LLMGatewayClient } from '@salesforce/llm-gateway-sdk';
7
9
  import { Clock, type LogRecord, type UniqueIDGenerator, type Unsubscribe } from '@salesforce/agentic-common';
8
10
  import { type ToolsInput } from './utils.js';
@@ -49,8 +51,55 @@ interface MastraAgentState {
49
51
  mastraWorkspace?: Workspace;
50
52
  /** Tracks the active workflow run per thread for tool approval. */
51
53
  activeRuns: Map<string, ActiveRun>;
54
+ /** Tool-search processor instance, when `toolSearch` is configured on the factory. */
55
+ toolSearchProcessor?: ToolSearchProcessor;
56
+ /** Skill-search processor instance, when both `skills` and `toolSearch` are configured. */
57
+ skillSearchProcessor?: SkillSearchProcessor;
52
58
  }
53
- export declare class MastraHarness implements AgentHarness {
59
+ /**
60
+ * Branded subtype consumers parameterize over to reach Mastra-specific
61
+ * runtime objects through the typed `extensions` namespace.
62
+ *
63
+ * Wrapped with `WithAgentConfig` so `manager.createAgent` autocompletes
64
+ * `MastraAgentConfig` fields (e.g. `toolSearch`) without an explicit
65
+ * type argument. The wrapper is type-only; it never exists at runtime.
66
+ *
67
+ * Usage:
68
+ * ```ts
69
+ * const manager = await createAgentManager(
70
+ * storageRootFolder,
71
+ * new MastraHarnessFactory(),
72
+ * );
73
+ * // manager.createAgent autocompletes `toolSearch` via WithAgentConfig:
74
+ * const agent = await manager.createAgent(projectRoot, {
75
+ * instructions: '...',
76
+ * toolSearch: { pool: '*', alwaysActive: ['lookup_account'] },
77
+ * });
78
+ * const processor = manager.extensions.mastra.getToolSearchProcessor(agent.getId());
79
+ * ```
80
+ */
81
+ export type MastraAgentHarness = WithAgentConfig<AgentHarness & {
82
+ readonly harnessId: 'mastra';
83
+ readonly extensions: {
84
+ mastra: {
85
+ /**
86
+ * Returns a {@link ToolSearchHandle} for an agent, or `undefined`
87
+ * if the agent has no tool-search configured or doesn't exist.
88
+ * The handle wraps Mastra's internal `ToolSearchProcessor` with
89
+ * a stable surface (state stats + thread-state management).
90
+ */
91
+ getToolSearchProcessor(agentId: string): ToolSearchHandle | undefined;
92
+ /**
93
+ * Returns a {@link SkillSearchHandle} for an agent, or `undefined`
94
+ * if the agent doesn't have both `skills` and `toolSearch` configured
95
+ * (skill search is only enabled when both are set; without `toolSearch`,
96
+ * Mastra falls back to eager skill loading).
97
+ */
98
+ getSkillSearchProcessor(agentId: string): SkillSearchHandle | undefined;
99
+ };
100
+ };
101
+ }, MastraAgentConfig>;
102
+ export declare class MastraHarness implements MastraAgentHarness {
54
103
  private readonly storageRootFolder;
55
104
  private readonly modelConfigFactory;
56
105
  private readonly clock;
@@ -67,6 +116,11 @@ export declare class MastraHarness implements AgentHarness {
67
116
  private readonly memory;
68
117
  private readonly mastra;
69
118
  private readonly buses;
119
+ /**
120
+ * Mastra-specific extension surface, typed off `MastraAgentHarness`. The
121
+ * SDK never reads this; consumers reach through `manager.extensions.mastra`.
122
+ */
123
+ readonly extensions: MastraAgentHarness['extensions'];
70
124
  constructor(storageRootFolder: string, modelConfigFactory?: MastraModelConfigFactory, clock?: Clock, idGenerator?: UniqueIDGenerator);
71
125
  /**
72
126
  * @requirements
@@ -7,14 +7,17 @@ import path from 'node:path';
7
7
  import { Agent } from '@mastra/core/agent';
8
8
  import { Mastra } from '@mastra/core/mastra';
9
9
  import { Memory } from '@mastra/memory';
10
+ import { SkillSearchProcessor, ToolSearchProcessor } from '@mastra/core/processors';
10
11
  import { LibSQLStore } from '@mastra/libsql';
11
12
  import { MCPClient } from '@mastra/mcp';
12
13
  import { AgentSDKError, AgentSDKErrorType, DEFAULT_MAX_STEPS, HarnessBusOwner, McpServerStatus, } from '@salesforce/sfdx-agent-sdk';
13
14
  import { MastraEventAdapter } from './mastra-event-adapter.js';
14
15
  import { mapMastraToMessage, mapMessageToMastra, partToTranscriptText } from './mastra-message-mapper.js';
16
+ import { isStandardSchemaWithJSON, standardSchemaToJSONSchema } from '@mastra/schema-compat';
15
17
  import { mapToolDefinitionsToMastraTools } from './mastra-tool-mapper.js';
16
18
  import { hasEnabledServers, mapToMastraMCPServers } from './mcp-config-mapper.js';
17
19
  import { DefaultMastraModelConfigFactory } from './default-model-config-factory.js';
20
+ import { partitionPool, } from './tool-search.js';
18
21
  import { Clock, getErrorMessage, RealClock, splitFrontmatterAndBody, UUIDGenerator, } from '@salesforce/agentic-common';
19
22
  import { makeAbortable } from './utils.js';
20
23
  import { Workspace, LocalFilesystem, LocalSandbox, WORKSPACE_TOOLS, WORKSPACE_TOOLS_PREFIX, } from '@mastra/core/workspace';
@@ -165,6 +168,78 @@ async function listMarkdownFilesInDirectory(dir) {
165
168
  }
166
169
  return names.sort().map((name) => path.join(dir, name));
167
170
  }
171
+ /**
172
+ * Projects the namespaced Mastra `ToolAction` map for one MCP server into the
173
+ * SDK's harness-agnostic `McpToolInfo[]` shape.
174
+ *
175
+ * Mastra's `MCPClient` pipes each protocol `Tool` from `tools/list` through
176
+ * `convertInputSchema` (which yields plain JSON Schema) and then through
177
+ * `createTool()`, which wraps the result in a Standard Schema envelope —
178
+ * `tool.inputSchema` ends up as `{ '~standard': { version, vendor, validate,
179
+ * jsonSchema } }`, **not** the raw JSON Schema the SDK contract promises.
180
+ * The harness undoes that wrap here via `standardSchemaToJSONSchema(...,
181
+ * { io: 'input' })` so consumers reading `McpToolInfo.inputSchema` get the
182
+ * JSON Schema they expect. Conversion adds JSON Schema annotations the
183
+ * source server may not have declared (`$schema`, `additionalProperties:
184
+ * false`); both are valid JSON Schema and we forward them untouched.
185
+ *
186
+ * Field-by-field forwarding:
187
+ *
188
+ * - `name`: the namespaced map key (e.g. `sf_query`).
189
+ * - `description`: collapsed to `undefined` when Mastra's `tool.description ||
190
+ * ""` fallback fired and the original was missing — empty strings are not
191
+ * useful UX.
192
+ * - `inputSchema`: unwrapped from the Standard Schema envelope to JSON
193
+ * Schema. If the value is already plain JSON Schema (e.g. from a unit-test
194
+ * stub that bypasses `createTool`), it's forwarded as-is. The unwrap
195
+ * invokes the provider's lazy JSON-Schema serializer (e.g. Zod's
196
+ * `toJSONSchema`), which can throw for unrepresentable types or circular
197
+ * refs; on any failure we degrade `inputSchema` to `undefined` (the SDK
198
+ * contract types it optional) and emit a `warn` log so the failure is
199
+ * observable. A single hostile tool must not erase the full status snapshot.
200
+ * - `annotations`: passed through structurally; Mastra's `ToolAnnotations`
201
+ * interface mirrors the MCP spec field-for-field, matching `McpToolAnnotations`.
202
+ *
203
+ * `outputSchema` is not forwarded — `@mastra/mcp` strips it from each wrapped
204
+ * tool deliberately (see the MCP-spec rationale documented on
205
+ * {@link McpToolInfo}), so the SDK contract omits the field entirely.
206
+ */
207
+ function mapMastraToolsToMcpToolInfo(tools, logBus) {
208
+ return Object.entries(tools).map(([name, tool]) => {
209
+ const info = { name };
210
+ const description = tool.description;
211
+ if (description !== undefined && description !== '') {
212
+ info.description = description;
213
+ }
214
+ const inputSchema = tool.inputSchema;
215
+ if (isStandardSchemaWithJSON(inputSchema)) {
216
+ // Real `createTool`-wrapped path. `io: 'input'` selects the input shape
217
+ // for tool parameters per `@mastra/schema-compat`'s documented contract.
218
+ // The unwrap calls the provider's lazy JSON-Schema serializer, which
219
+ // can throw — degrade to `undefined` rather than erase the snapshot.
220
+ try {
221
+ info.inputSchema = standardSchemaToJSONSchema(inputSchema, { io: 'input' });
222
+ }
223
+ catch (err) {
224
+ logBus?.warn('Failed to unwrap MCP tool inputSchema; omitting from McpToolInfo', {
225
+ toolName: name,
226
+ error: getErrorMessage(err),
227
+ });
228
+ }
229
+ }
230
+ else if (typeof inputSchema === 'object' && inputSchema !== null) {
231
+ // Plain JSON Schema (no `~standard` envelope) — forward as-is. Reached
232
+ // by unit-test fixtures that build the tools map directly without going
233
+ // through `@mastra/mcp`'s wrapping.
234
+ info.inputSchema = inputSchema;
235
+ }
236
+ const annotations = tool.mcp?.annotations;
237
+ if (annotations !== undefined) {
238
+ info.annotations = annotations;
239
+ }
240
+ return info;
241
+ });
242
+ }
168
243
  export class MastraHarness {
169
244
  storageRootFolder;
170
245
  modelConfigFactory;
@@ -182,6 +257,24 @@ export class MastraHarness {
182
257
  memory;
183
258
  mastra;
184
259
  buses = new HarnessBusOwner();
260
+ /**
261
+ * Mastra-specific extension surface, typed off `MastraAgentHarness`. The
262
+ * SDK never reads this; consumers reach through `manager.extensions.mastra`.
263
+ */
264
+ extensions = {
265
+ mastra: {
266
+ // Return type narrows the Mastra-internal processor to the
267
+ // stable `ToolSearchHandle` surface. The processor *is* a
268
+ // ToolSearchHandle structurally (it has all the listed methods),
269
+ // so no runtime wrapping is needed.
270
+ getToolSearchProcessor: (agentId) => {
271
+ return this.activeAgents.get(agentId)?.toolSearchProcessor;
272
+ },
273
+ getSkillSearchProcessor: (agentId) => {
274
+ return this.activeAgents.get(agentId)?.skillSearchProcessor;
275
+ },
276
+ },
277
+ };
185
278
  constructor(storageRootFolder, modelConfigFactory = new DefaultMastraModelConfigFactory(), clock = new RealClock(), idGenerator = new UUIDGenerator()) {
186
279
  this.storageRootFolder = storageRootFolder;
187
280
  this.modelConfigFactory = modelConfigFactory;
@@ -219,6 +312,7 @@ export class MastraHarness {
219
312
  await s.ready.catch(() => undefined);
220
313
  await s.client.disconnect();
221
314
  }));
315
+ state.skillSearchProcessor?.dispose();
222
316
  }));
223
317
  this.activeAgents.clear();
224
318
  await this.mastra?.shutdown();
@@ -346,6 +440,13 @@ export class MastraHarness {
346
440
  let mastraWorkspace;
347
441
  let agent;
348
442
  let mastraAgentAdded = false;
443
+ let toolSearchProcessor;
444
+ let toolSearchAlwaysActive;
445
+ let skillSearchProcessor;
446
+ // `toolSearch` rides through on the consumer's `MastraAgentConfig`
447
+ // even though `HarnessAgentConfig` itself doesn't declare it —
448
+ // `toHarnessConfig` preserves extra fields via spread.
449
+ const toolSearchConfig = config.toolSearch;
349
450
  try {
350
451
  const allowedPaths = (config.skills ?? []).filter((s) => path.isAbsolute(s));
351
452
  const localFs = new LocalFilesystem({ basePath: projectRoot, allowedPaths });
@@ -362,6 +463,38 @@ export class MastraHarness {
362
463
  ])),
363
464
  });
364
465
  await mastraWorkspace.init();
466
+ // On-demand skill search: only when both `skills` and `toolSearch`
467
+ // are configured. Without `toolSearch`, Mastra's built-in eager
468
+ // skill loading is used (existing behavior).
469
+ if (mastraWorkspace.skills && toolSearchConfig) {
470
+ skillSearchProcessor = new SkillSearchProcessor({ workspace: mastraWorkspace });
471
+ }
472
+ // When toolSearch is configured, await MCP discovery so MCP tools
473
+ // can enter the searchable pool. Without it, MCP discovery stays
474
+ // non-blocking (existing behavior).
475
+ if (toolSearchConfig) {
476
+ if (mcpServerStates.size > 0) {
477
+ await Promise.allSettled([...mcpServerStates.values()].map((s) => s.ready));
478
+ }
479
+ const mcpTools = [...mcpServerStates.values()].reduce((acc, s) => ({ ...acc, ...s.tools }), {});
480
+ const allTools = { ...customTools, ...mcpTools };
481
+ const knownNames = new Set(Object.keys(allTools));
482
+ const partition = partitionPool(toolSearchConfig.pool, toolSearchConfig.alwaysActive, knownNames);
483
+ toolSearchAlwaysActive = new Set(partition.alwaysActive);
484
+ const poolTools = {};
485
+ for (const name of partition.pool) {
486
+ if (allTools[name]) {
487
+ poolTools[name] = allTools[name];
488
+ }
489
+ }
490
+ toolSearchProcessor = new ToolSearchProcessor({
491
+ tools: poolTools,
492
+ search: toolSearchConfig.topK !== undefined ? { topK: toolSearchConfig.topK } : undefined,
493
+ ttl: toolSearchConfig.ttl,
494
+ });
495
+ }
496
+ const inputProcessorsList = [skillSearchProcessor, toolSearchProcessor].filter((p) => p !== undefined);
497
+ const inputProcessors = inputProcessorsList.length > 0 ? inputProcessorsList : undefined;
365
498
  agent = new Agent({
366
499
  id: agentId,
367
500
  name: config.name || agentId,
@@ -370,9 +503,21 @@ export class MastraHarness {
370
503
  memory: this.memory,
371
504
  tools: () => {
372
505
  const mcpTools = [...mcpServerStates.values()].reduce((acc, s) => ({ ...acc, ...s.tools }), {});
373
- return { ...customTools, ...mcpTools };
506
+ const merged = { ...customTools, ...mcpTools };
507
+ if (!toolSearchAlwaysActive) {
508
+ return merged;
509
+ }
510
+ // Only expose alwaysActive tools; the rest are behind search/load.
511
+ const active = {};
512
+ for (const [name, tool] of Object.entries(merged)) {
513
+ if (toolSearchAlwaysActive.has(name)) {
514
+ active[name] = tool;
515
+ }
516
+ }
517
+ return active;
374
518
  },
375
519
  workspace: mastraWorkspace,
520
+ ...(inputProcessors ? { inputProcessors } : {}),
376
521
  });
377
522
  this.mastra.addAgent(agent);
378
523
  mastraAgentAdded = true;
@@ -382,12 +527,17 @@ export class MastraHarness {
382
527
  mcpConfig: config.mcpServers,
383
528
  mastraWorkspace,
384
529
  activeRuns: new Map(),
530
+ toolSearchProcessor,
531
+ skillSearchProcessor,
385
532
  });
386
533
  }
387
534
  catch (error) {
388
535
  if (mastraAgentAdded) {
389
536
  this.mastra.removeAgent(agentId);
390
537
  }
538
+ // SkillSearchProcessor schedules a TTL cleanup interval; dispose
539
+ // on failure so we don't leak a timer for a never-registered agent.
540
+ skillSearchProcessor?.dispose();
391
541
  await mastraWorkspace?.destroy().catch(() => undefined);
392
542
  await Promise.allSettled(Array.from(mcpServerStates.values()).map((s) => s.client.disconnect().catch(() => undefined)));
393
543
  throw error;
@@ -405,6 +555,7 @@ export class MastraHarness {
405
555
  await s.ready.catch(() => undefined);
406
556
  await s.client.disconnect();
407
557
  }));
558
+ state.skillSearchProcessor?.dispose();
408
559
  await state.mastraWorkspace?.destroy();
409
560
  this.mastra.removeAgent(agentId);
410
561
  return this.activeAgents.delete(agentId);
@@ -427,7 +578,11 @@ export class MastraHarness {
427
578
  results.push({ error: serverState.error, name, status: McpServerStatus.Error, tools: [] });
428
579
  }
429
580
  else {
430
- results.push({ name, status: McpServerStatus.Connected, tools: Object.keys(serverState.tools) });
581
+ results.push({
582
+ name,
583
+ status: McpServerStatus.Connected,
584
+ tools: mapMastraToolsToMcpToolInfo(serverState.tools, this.buses.getLogBus()),
585
+ });
431
586
  }
432
587
  }
433
588
  if (state.mcpConfig) {
@@ -697,7 +852,13 @@ export class MastraHarness {
697
852
  perPage: false,
698
853
  });
699
854
  const logBus = this.buses.getLogBus();
855
+ // Drop Mastra's experimental `'signal'` role (system reminders the agent synthesizes via `createSignal()`,
856
+ // see `@mastra/core/dist/agent/signals.d.ts` — all `@experimental`). The SDK's `MessageRole` exposes only
857
+ // `'system' | 'user' | 'assistant' | 'tool'` and surfacing experimental upstream concepts would leak the
858
+ // harness internals up to consumers. If Mastra promotes signals to stable and we want them, the SDK
859
+ // contract needs to add the role first.
700
860
  return messages
861
+ .filter((m) => m.role !== 'signal')
701
862
  .map((m) => mapMastraToMessage(m, logBus))
702
863
  .sort((a, b) => (a.createdAt?.getTime() ?? 0) - (b.createdAt?.getTime() ?? 0));
703
864
  }
@@ -4,6 +4,9 @@ import { Clock, type LogBus } from '@salesforce/agentic-common';
4
4
  /**
5
5
  * Converts a Mastra persisted message into the SDK's {@link Message} type.
6
6
  *
7
+ * Callers must filter out Mastra's experimental `'signal'` role before invoking;
8
+ * those messages have no SDK equivalent and must not surface in `Message[]`.
9
+ *
7
10
  * Parts that have no SDK equivalent (source, file, step-start, and any
8
11
  * unrecognized upstream types including AI SDK v5 `data-*` metadata parts)
9
12
  * are silently dropped. Unrecognized types emit a structured `warn` log when a
@@ -6,6 +6,9 @@ import { Clock, RealClock } from '@salesforce/agentic-common';
6
6
  /**
7
7
  * Converts a Mastra persisted message into the SDK's {@link Message} type.
8
8
  *
9
+ * Callers must filter out Mastra's experimental `'signal'` role before invoking;
10
+ * those messages have no SDK equivalent and must not surface in `Message[]`.
11
+ *
9
12
  * Parts that have no SDK equivalent (source, file, step-start, and any
10
13
  * unrecognized upstream types including AI SDK v5 `data-*` metadata parts)
11
14
  * are silently dropped. Unrecognized types emit a structured `warn` log when a
@@ -13,6 +16,9 @@ import { Clock, RealClock } from '@salesforce/agentic-common';
13
16
  * message history retrieval.
14
17
  */
15
18
  export function mapMastraToMessage(msg, logBus) {
19
+ if (msg.role === 'signal') {
20
+ throw new Error("mapMastraToMessage: 'signal' role messages must be filtered out before mapping");
21
+ }
16
22
  const parts = [];
17
23
  for (const part of msg.content.parts) {
18
24
  const mapped = mapMastraPartToMessagePart(part, logBus);
@@ -34,7 +34,7 @@ export function mapToolDefinitionsToMastraTools(tools) {
34
34
  const mastraTool = {
35
35
  id: tool.name,
36
36
  description: tool.description ?? '',
37
- inputSchema: tool.inputSchema,
37
+ inputSchema: ensureJsonSchemaMarker(tool.inputSchema),
38
38
  requireApproval: true,
39
39
  execute: async (_inputData, context) => {
40
40
  const resume = context.resumeData;
@@ -52,4 +52,18 @@ export function mapToolDefinitionsToMastraTools(tools) {
52
52
  }
53
53
  return result;
54
54
  }
55
+ const JSON_SCHEMA_DRAFT = 'http://json-schema.org/draft-07/schema#';
56
+ /**
57
+ * Stamp a Mastra-acceptable `$schema` marker onto an `inputSchema`.
58
+ * Mastra's `prepareToolsAndToolChoice` requires `$schema` to start with
59
+ * `http://json-schema.org/` (literal `http://`); anything else crashes.
60
+ * Existing valid markers are preserved.
61
+ */
62
+ function ensureJsonSchemaMarker(schema) {
63
+ const existing = schema['$schema'];
64
+ if (typeof existing === 'string' && existing.startsWith('http://json-schema.org/')) {
65
+ return schema;
66
+ }
67
+ return { ...schema, $schema: JSON_SCHEMA_DRAFT };
68
+ }
55
69
  //# sourceMappingURL=mastra-tool-mapper.js.map
@@ -1,10 +1,10 @@
1
1
  import { MastraModelGateway, type ProviderConfig } from '@mastra/core/llm';
2
- import type { LanguageModelV2 } from '@ai-sdk/provider';
2
+ import type { LanguageModelV3 } from '@ai-sdk/provider';
3
3
  import type { LLMGatewayClient } from '@salesforce/llm-gateway-sdk';
4
4
  /**
5
5
  * Bridges the Salesforce LLM Gateway SDK to Mastra's MastraModelGateway interface.
6
6
  *
7
- * Translates AI SDK LanguageModelV2 calls into GatewayClient API calls,
7
+ * Translates AI SDK LanguageModelV3 calls into GatewayClient API calls,
8
8
  * mapping prompts, tools, and responses between the two type systems.
9
9
  * Uses chat() for non-streaming generation and chatStream() for streaming.
10
10
  */
@@ -12,14 +12,11 @@ export declare class SalesforceMastraModelGateway extends MastraModelGateway {
12
12
  private readonly gatewayClient;
13
13
  readonly id = "sfdc";
14
14
  readonly name = "Salesforce LLM Gateway";
15
- private lastResponseTextPromise;
16
- private resolveLastResponseText?;
17
15
  constructor(gatewayClient: LLMGatewayClient);
18
16
  getApiKey(_modelId: string): Promise<string>;
19
17
  buildUrl(_modelId: string, _envVars: Record<string, string>): string;
20
18
  fetchProviders(): Promise<Record<string, ProviderConfig>>;
21
- resolveLanguageModel(): LanguageModelV2;
22
- takeLastResponseText(): Promise<string | undefined>;
19
+ resolveLanguageModel(): LanguageModelV3;
23
20
  private doGenerate;
24
21
  private doStream;
25
22
  private buildChatRequest;
@@ -7,7 +7,7 @@ import { ModelName, Models } from '@salesforce/llm-gateway-sdk';
7
7
  /**
8
8
  * Bridges the Salesforce LLM Gateway SDK to Mastra's MastraModelGateway interface.
9
9
  *
10
- * Translates AI SDK LanguageModelV2 calls into GatewayClient API calls,
10
+ * Translates AI SDK LanguageModelV3 calls into GatewayClient API calls,
11
11
  * mapping prompts, tools, and responses between the two type systems.
12
12
  * Uses chat() for non-streaming generation and chatStream() for streaming.
13
13
  */
@@ -15,8 +15,6 @@ export class SalesforceMastraModelGateway extends MastraModelGateway {
15
15
  gatewayClient;
16
16
  id = 'sfdc';
17
17
  name = 'Salesforce LLM Gateway';
18
- lastResponseTextPromise = Promise.resolve(undefined);
19
- resolveLastResponseText;
20
18
  constructor(gatewayClient) {
21
19
  super();
22
20
  this.gatewayClient = gatewayClient;
@@ -43,7 +41,7 @@ export class SalesforceMastraModelGateway extends MastraModelGateway {
43
41
  resolveLanguageModel() {
44
42
  const model = this.gatewayClient.getModel();
45
43
  return {
46
- specificationVersion: 'v2',
44
+ specificationVersion: 'v3',
47
45
  provider: 'salesforce-llm-gateway',
48
46
  modelId: model.name,
49
47
  supportedUrls: {},
@@ -51,16 +49,10 @@ export class SalesforceMastraModelGateway extends MastraModelGateway {
51
49
  doStream: (options) => this.doStream(options),
52
50
  };
53
51
  }
54
- async takeLastResponseText() {
55
- const text = await this.lastResponseTextPromise;
56
- this.lastResponseTextPromise = Promise.resolve(undefined);
57
- return text;
58
- }
59
52
  async doGenerate(options) {
60
53
  const chatRequest = this.buildChatRequest(options);
61
54
  const response = await this.gatewayClient.chat(chatRequest);
62
55
  const { generatedText, toolInvocations, usage: responseUsage, finishReason: rawFinishReason } = response.data;
63
- this.lastResponseTextPromise = Promise.resolve(generatedText || undefined);
64
56
  const content = [];
65
57
  if (generatedText) {
66
58
  content.push({ type: 'text', text: generatedText });
@@ -74,39 +66,20 @@ export class SalesforceMastraModelGateway extends MastraModelGateway {
74
66
  });
75
67
  }
76
68
  const sawToolCalls = (toolInvocations ?? []).length > 0;
77
- const finishReason = sawToolCalls
78
- ? 'tool-calls'
79
- : rawFinishReason === 'length'
80
- ? 'length'
81
- : 'stop';
82
- const usage = responseUsage
83
- ? {
84
- inputTokens: responseUsage.inputTokens,
85
- outputTokens: responseUsage.outputTokens,
86
- totalTokens: responseUsage.totalTokens,
87
- reasoningTokens: responseUsage.reasoningTokens,
88
- }
89
- : { inputTokens: undefined, outputTokens: undefined, totalTokens: undefined };
69
+ const finishReason = mapFinishReason(rawFinishReason, sawToolCalls);
70
+ const usage = mapUsage(responseUsage);
90
71
  return { content, finishReason, usage, warnings: [] };
91
72
  }
92
73
  async doStream(options) {
93
74
  const chatRequest = this.buildChatRequest(options);
94
75
  const response = await this.gatewayClient.chatStream(chatRequest);
95
76
  let sawToolCalls = false;
96
- let aggregatedText = '';
97
77
  let textSpanOpen = false;
98
78
  let textSpanId = 0;
99
- this.lastResponseTextPromise = new Promise((resolve) => {
100
- this.resolveLastResponseText = resolve;
101
- });
102
79
  const stream = new ReadableStream({
103
80
  start: async (controller) => {
104
81
  try {
105
- let lastUsage = {
106
- inputTokens: undefined,
107
- outputTokens: undefined,
108
- totalTokens: undefined,
109
- };
82
+ let lastUsage = mapUsage(undefined);
110
83
  for await (const chunk of response.data) {
111
84
  if (chunk.generatedText && !chunk.done) {
112
85
  if (!textSpanOpen) {
@@ -118,7 +91,6 @@ export class SalesforceMastraModelGateway extends MastraModelGateway {
118
91
  delta: chunk.generatedText,
119
92
  id: `text-${textSpanId}`,
120
93
  });
121
- aggregatedText += chunk.generatedText;
122
94
  }
123
95
  if (chunk.toolInvocations?.length) {
124
96
  // Close any open text span before emitting tool calls so that
@@ -139,18 +111,13 @@ export class SalesforceMastraModelGateway extends MastraModelGateway {
139
111
  }
140
112
  }
141
113
  if (chunk.usage) {
142
- lastUsage = {
143
- inputTokens: chunk.usage.inputTokens,
144
- outputTokens: chunk.usage.outputTokens,
145
- totalTokens: chunk.usage.totalTokens,
146
- reasoningTokens: chunk.usage.reasoningTokens,
147
- };
114
+ lastUsage = mapUsage(chunk.usage);
148
115
  }
149
116
  if (chunk.done) {
150
117
  if (textSpanOpen) {
151
118
  controller.enqueue({ type: 'text-end', id: `text-${textSpanId}` });
152
119
  }
153
- const finishReason = sawToolCalls ? 'tool-calls' : 'stop';
120
+ const finishReason = mapFinishReason(chunk.finishReason, sawToolCalls);
154
121
  controller.enqueue({
155
122
  type: 'finish',
156
123
  finishReason,
@@ -166,9 +133,6 @@ export class SalesforceMastraModelGateway extends MastraModelGateway {
166
133
  controller.enqueue({ type: 'error', error });
167
134
  controller.close();
168
135
  }
169
- finally {
170
- this.resolveLastResponseText?.(aggregatedText || undefined);
171
- }
172
136
  },
173
137
  });
174
138
  return { stream };
@@ -224,7 +188,13 @@ export class SalesforceMastraModelGateway extends MastraModelGateway {
224
188
  return msg;
225
189
  }
226
190
  case 'tool':
227
- return message.content.map((part) => ({
191
+ // V3's `tool` role content can include `tool-approval-response` parts alongside
192
+ // `tool-result` parts. We do not surface tool-approval-request to consumers, so we
193
+ // never receive approval-response parts in practice — but filter defensively so
194
+ // the gateway is forward-compatible with future approval flows.
195
+ return message.content
196
+ .filter((part) => part.type === 'tool-result')
197
+ .map((part) => ({
228
198
  role: 'tool',
229
199
  content: this.toolResultOutputToString(part.output),
230
200
  tool_call_id: part.toolCallId,
@@ -246,6 +216,8 @@ export class SalesforceMastraModelGateway extends MastraModelGateway {
246
216
  .filter((v) => v.type === 'text')
247
217
  .map((v) => v.text)
248
218
  .join('');
219
+ case 'execution-denied':
220
+ return output.reason ?? 'tool execution denied';
249
221
  }
250
222
  }
251
223
  translateTools(options) {
@@ -263,4 +235,69 @@ export class SalesforceMastraModelGateway extends MastraModelGateway {
263
235
  }));
264
236
  }
265
237
  }
238
+ /**
239
+ * Maps a Salesforce LLM Gateway raw finish-reason string to the AI SDK V3
240
+ * `LanguageModelV3FinishReason` object shape. V3 finish reasons are an object
241
+ * with a `unified` enum field (the cross-provider canonical reason) and a `raw`
242
+ * field (the original provider string, preserved for debugging).
243
+ *
244
+ * The gateway's `FinishReason` union spans GPT-5 (`stop` / `length` / `tool_calls`)
245
+ * and Claude (`end_turn` / `max_tokens` / `tool_use`); we collapse both vocabularies
246
+ * onto V3's unified set.
247
+ *
248
+ * If `sawToolCalls` is true, `unified` is forced to `'tool-calls'` regardless of
249
+ * what the gateway returned — the gateway can report a non-tool reason even when
250
+ * tool invocations are present, and Mastra's downstream loop relies on
251
+ * `'tool-calls'` to schedule the next step.
252
+ */
253
+ function mapFinishReason(rawFinishReason, sawToolCalls) {
254
+ const unified = sawToolCalls
255
+ ? 'tool-calls'
256
+ : rawFinishReason === 'length' || rawFinishReason === 'max_tokens'
257
+ ? 'length'
258
+ : rawFinishReason === 'tool_calls' || rawFinishReason === 'tool_use'
259
+ ? 'tool-calls'
260
+ : 'stop';
261
+ return { unified, raw: rawFinishReason };
262
+ }
263
+ /**
264
+ * Maps a Salesforce LLM Gateway `TokenUsage` (flat shape) to the AI SDK V3
265
+ * `LanguageModelV3Usage` shape (nested input/output groups). V3 dropped the
266
+ * top-level `totalTokens` field; downstream consumers (e.g. `MastraEventAdapter`)
267
+ * read a flat `totalTokens` from Mastra's `LanguageModelUsage` extension, which
268
+ * Mastra recomputes as `inputTokens.total + outputTokens.total`.
269
+ *
270
+ * Two adaptations matter for token-count fidelity:
271
+ *
272
+ * 1. **Reasoning tokens roll up into `outputTokens.total`** to match V3's spec
273
+ * that `outputTokens.total = outputTokens.text + outputTokens.reasoning`.
274
+ * Without this, GPT-5 responses with reasoning would lose those tokens from
275
+ * the recomputed flat `totalTokens` (the V2 path preserved the gateway's
276
+ * `totalTokens` directly, which already included reasoning).
277
+ * 2. **Cache tokens populate `inputTokens.cacheRead` / `cacheWrite`** for Claude
278
+ * prompt caching. We do not roll these into `inputTokens.total` because
279
+ * Anthropic's accounting treats cache reads as billable input replacement
280
+ * (already counted in the gateway's `inputTokens`) and cache writes as a
281
+ * separate billable line (not part of `inputTokens`).
282
+ */
283
+ function mapUsage(responseUsage) {
284
+ const outputTokens = responseUsage?.outputTokens;
285
+ const reasoningTokens = responseUsage?.reasoningTokens;
286
+ const outputTotal = outputTokens === undefined && reasoningTokens === undefined
287
+ ? undefined
288
+ : (outputTokens ?? 0) + (reasoningTokens ?? 0);
289
+ return {
290
+ inputTokens: {
291
+ total: responseUsage?.inputTokens,
292
+ noCache: undefined,
293
+ cacheRead: responseUsage?.cacheReadTokens,
294
+ cacheWrite: responseUsage?.cacheWriteTokens,
295
+ },
296
+ outputTokens: {
297
+ total: outputTotal,
298
+ text: outputTokens,
299
+ reasoning: reasoningTokens,
300
+ },
301
+ };
302
+ }
266
303
  //# sourceMappingURL=salesforce-llm-gateway.js.map
@@ -0,0 +1,148 @@
1
+ import type { AgentConfig } from '@salesforce/sfdx-agent-sdk';
2
+ /**
3
+ * Configuration for hiding a large tool catalog behind Mastra's
4
+ * search/load workflow. When present on `MastraAgentConfig.toolSearch`,
5
+ * the harness exposes only `alwaysActive` tools by default and injects
6
+ * Mastra's `search_tools` / `load_tool` meta-tools for the model to
7
+ * discover the rest on demand.
8
+ */
9
+ export type MastraToolSearchConfig = {
10
+ /**
11
+ * Tools eligible for search/load. `'*'` means every searchable tool;
12
+ * an explicit array limits the pool to the listed names.
13
+ */
14
+ pool: string[] | '*';
15
+ /**
16
+ * Tools that bypass search and are always visible to the model.
17
+ * Overrides `pool` on overlap. MCP tool names use the harness namespace
18
+ * prefix, e.g. `sfdx_deploy_metadata` for the `deploy_metadata` tool on
19
+ * the `sfdx` server.
20
+ */
21
+ alwaysActive?: string[];
22
+ /** Max tools returned per search (advisory). */
23
+ topK?: number;
24
+ /** Time-to-live in ms for a granted tool in the active set (advisory). */
25
+ ttl?: number;
26
+ };
27
+ /**
28
+ * Mastra-flavored extension of {@link AgentConfig}, adding the Mastra-only
29
+ * `toolSearch` field. Consumers do not import or pass this type explicitly —
30
+ * `MastraAgentHarness` brands itself with `MastraAgentConfig` via
31
+ * `WithAgentConfig`, so `manager.createAgent`'s config parameter resolves to
32
+ * this shape automatically and `toolSearch` autocompletes on the object
33
+ * literal.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * const agent = await manager.createAgent(projectRoot, {
38
+ * instructions: '...',
39
+ * tools,
40
+ * toolSearch: { pool: '*', alwaysActive: ['lookup_account'] },
41
+ * });
42
+ * ```
43
+ *
44
+ * Extra fields ride through `toHarnessConfig`'s `...rest` spread to the
45
+ * harness, which reads `toolSearch` at its boundary via runtime narrowing.
46
+ */
47
+ export type MastraAgentConfig = AgentConfig & {
48
+ /** Hide tools behind Mastra's search/load workflow. */
49
+ toolSearch?: MastraToolSearchConfig;
50
+ };
51
+ /**
52
+ * Opaque handle to the tool-search runtime state, exposed via
53
+ * `manager.extensions.mastra.getToolSearchProcessor(agentId)`. Lets consumers
54
+ * monitor and manage the per-thread loaded-tools state without reaching
55
+ * into Mastra's internal processor type.
56
+ *
57
+ * The underlying object *is* a Mastra `ToolSearchProcessor`; this interface
58
+ * narrows the public surface to a stable set of methods that won't churn
59
+ * if Mastra's internal shape changes. Consumers who need the full processor
60
+ * can still cast (`as unknown as ToolSearchProcessor`), but doing so
61
+ * accepts coupling to Mastra's version.
62
+ */
63
+ export interface ToolSearchHandle {
64
+ /** Stable identifier for the processor (`'tool-search'`). */
65
+ readonly id: string;
66
+ /** Snapshot of per-thread state for monitoring. */
67
+ getStateStats(): {
68
+ threadCount: number;
69
+ oldestAccessTime: number | null;
70
+ };
71
+ /** Clear loaded tools for a thread (or `'default'` if omitted). */
72
+ clearState(threadId?: string): void;
73
+ /** Clear loaded tools across every thread on this processor. */
74
+ clearAllState(): void;
75
+ /**
76
+ * Trigger TTL cleanup eagerly. The processor schedules its own interval;
77
+ * this is for tests and explicit eviction.
78
+ * @returns Number of threads cleaned up.
79
+ */
80
+ cleanupNow(): number;
81
+ }
82
+ /**
83
+ * Opaque handle to the skill-search runtime, exposed via
84
+ * `manager.extensions.mastra.getSkillSearchProcessor(agentId)`.
85
+ *
86
+ * The Mastra `SkillSearchProcessor`'s public surface today is just
87
+ * `dispose()` (and the `id` / `name` / `description` metadata). The
88
+ * harness owns the lifecycle, so consumers shouldn't call `dispose()`
89
+ * directly — the handle exposes only the identification fields.
90
+ *
91
+ * A future Mastra release that adds public methods (analogous to
92
+ * `ToolSearchProcessor.getStateStats()`) can extend this interface
93
+ * without breaking consumers.
94
+ */
95
+ export interface SkillSearchHandle {
96
+ /** Stable identifier for the processor (`'skill-search'`). */
97
+ readonly id: string;
98
+ }
99
+ /**
100
+ * Build the namespaced tool name Mastra uses for an MCP-discovered tool.
101
+ *
102
+ * The Mastra harness namespaces every MCP tool as `${serverName}_${toolName}`
103
+ * so two MCP servers exposing tools with the same short name (e.g. `query` on
104
+ * both `sfdx` and `postgres`) don't collide, and so MCP tools never collide
105
+ * with custom tools defined inline on `AgentConfig.tools`. Only MCP tool
106
+ * names are namespaced — custom tools keep their declared name unchanged.
107
+ *
108
+ * Reach for this when constructing `toolSearch.alwaysActive` entries that
109
+ * reference MCP tools at agent-creation time, *before* MCP discovery has
110
+ * settled. Without this helper you'd have to either hard-code the namespaced
111
+ * string (brittle if the server name changes) or wait for discovery (which
112
+ * defeats the point of `alwaysActive`).
113
+ *
114
+ * Custom tools listed in `alwaysActive` should use their declared name
115
+ * directly — do **not** wrap them with this helper.
116
+ *
117
+ * @param serverName - The MCP server name as it appears in
118
+ * `AgentConfig.mcpServers` (the key, not the URL/command).
119
+ * @param toolName - The tool's name as exposed by the MCP server (the
120
+ * un-namespaced name the server itself returns).
121
+ * @returns The namespaced name the harness will register the tool under.
122
+ *
123
+ * @example
124
+ * ```ts
125
+ * await manager.createAgent(projectRoot, {
126
+ * instructions: '...',
127
+ * tools: [{ name: 'lookup_account', ... }], // custom tool
128
+ * mcpServers: { sfdx: { command: 'sf-mcp' } },
129
+ * toolSearch: {
130
+ * pool: '*',
131
+ * alwaysActive: [
132
+ * 'lookup_account', // custom — bare name
133
+ * formatMcpToolName('sfdx', 'deploy_metadata'), // MCP — namespaced
134
+ * ],
135
+ * },
136
+ * });
137
+ * ```
138
+ */
139
+ export declare function formatMcpToolName(serverName: string, toolName: string): string;
140
+ /**
141
+ * Resolve `pool` / `alwaysActive` against known tools. `'*'` expands to all
142
+ * known names; unknown pool names are dropped; unknown alwaysActive names are
143
+ * kept for late MCP discovery; alwaysActive overrides pool on overlap.
144
+ */
145
+ export declare function partitionPool(poolSpec: string[] | '*', alwaysActive: string[] | undefined, allKnown: ReadonlySet<string>): {
146
+ pool: string[];
147
+ alwaysActive: string[];
148
+ };
@@ -0,0 +1,60 @@
1
+ /*
2
+ * Copyright 2026, Salesforce, Inc. All rights reserved.
3
+ * See LICENSE.txt for license terms.
4
+ */
5
+ /**
6
+ * Build the namespaced tool name Mastra uses for an MCP-discovered tool.
7
+ *
8
+ * The Mastra harness namespaces every MCP tool as `${serverName}_${toolName}`
9
+ * so two MCP servers exposing tools with the same short name (e.g. `query` on
10
+ * both `sfdx` and `postgres`) don't collide, and so MCP tools never collide
11
+ * with custom tools defined inline on `AgentConfig.tools`. Only MCP tool
12
+ * names are namespaced — custom tools keep their declared name unchanged.
13
+ *
14
+ * Reach for this when constructing `toolSearch.alwaysActive` entries that
15
+ * reference MCP tools at agent-creation time, *before* MCP discovery has
16
+ * settled. Without this helper you'd have to either hard-code the namespaced
17
+ * string (brittle if the server name changes) or wait for discovery (which
18
+ * defeats the point of `alwaysActive`).
19
+ *
20
+ * Custom tools listed in `alwaysActive` should use their declared name
21
+ * directly — do **not** wrap them with this helper.
22
+ *
23
+ * @param serverName - The MCP server name as it appears in
24
+ * `AgentConfig.mcpServers` (the key, not the URL/command).
25
+ * @param toolName - The tool's name as exposed by the MCP server (the
26
+ * un-namespaced name the server itself returns).
27
+ * @returns The namespaced name the harness will register the tool under.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * await manager.createAgent(projectRoot, {
32
+ * instructions: '...',
33
+ * tools: [{ name: 'lookup_account', ... }], // custom tool
34
+ * mcpServers: { sfdx: { command: 'sf-mcp' } },
35
+ * toolSearch: {
36
+ * pool: '*',
37
+ * alwaysActive: [
38
+ * 'lookup_account', // custom — bare name
39
+ * formatMcpToolName('sfdx', 'deploy_metadata'), // MCP — namespaced
40
+ * ],
41
+ * },
42
+ * });
43
+ * ```
44
+ */
45
+ export function formatMcpToolName(serverName, toolName) {
46
+ return `${serverName}_${toolName}`;
47
+ }
48
+ /**
49
+ * Resolve `pool` / `alwaysActive` against known tools. `'*'` expands to all
50
+ * known names; unknown pool names are dropped; unknown alwaysActive names are
51
+ * kept for late MCP discovery; alwaysActive overrides pool on overlap.
52
+ */
53
+ export function partitionPool(poolSpec, alwaysActive, allKnown) {
54
+ const resolvedActive = alwaysActive ?? [];
55
+ const activeSet = new Set(resolvedActive);
56
+ const resolvedPoolBase = poolSpec === '*' ? Array.from(allKnown) : poolSpec.filter((n) => allKnown.has(n));
57
+ const pool = resolvedPoolBase.filter((n) => !activeSet.has(n));
58
+ return { pool, alwaysActive: resolvedActive };
59
+ }
60
+ //# sourceMappingURL=tool-search.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/sfdx-agent-harness-mastra",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Mastra-backed AgentHarness implementation for @salesforce/sfdx-agent-sdk",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -27,36 +27,37 @@
27
27
  "LICENSE.txt"
28
28
  ],
29
29
  "dependencies": {
30
- "@ai-sdk/provider": "^2.0.1",
31
- "@mastra/core": "^1.31.0",
32
- "@mastra/libsql": "^1.9.1",
33
- "@mastra/mcp": "^1.6.0",
34
- "@mastra/memory": "^1.17.4",
35
- "@salesforce/agentic-common": "0.3.0",
36
- "@salesforce/llm-gateway-sdk": "0.3.0"
30
+ "@ai-sdk/provider": "^3.0.10",
31
+ "@mastra/core": "^1.36.0",
32
+ "@mastra/libsql": "^1.11.1",
33
+ "@mastra/mcp": "^1.8.0",
34
+ "@mastra/memory": "^1.19.0",
35
+ "@mastra/schema-compat": "^1.2.10",
36
+ "@salesforce/agentic-common": "0.4.0",
37
+ "@salesforce/llm-gateway-sdk": "0.4.0"
37
38
  },
38
39
  "peerDependencies": {
39
- "@salesforce/sfdx-agent-sdk": "0.4.0"
40
+ "@salesforce/sfdx-agent-sdk": "0.6.0"
40
41
  },
41
42
  "devDependencies": {
42
43
  "@eslint/js": "^10.0.1",
43
- "@salesforce/sfdx-agent-sdk": "0.4.0",
44
+ "@salesforce/sfdx-agent-sdk": "0.6.0",
44
45
  "@types/node": "^22.19.17",
45
- "@vitest/coverage-istanbul": "^4.1.5",
46
- "@vitest/eslint-plugin": "^1.6.16",
47
- "eslint": "^10.2.1",
46
+ "@vitest/coverage-istanbul": "^4.1.7",
47
+ "@vitest/eslint-plugin": "^1.6.17",
48
+ "eslint": "^10.4.0",
48
49
  "eslint-config-prettier": "^10.1.8",
49
50
  "eslint-import-resolver-typescript": "^4.4.4",
50
51
  "eslint-plugin-import": "^2.32.0",
51
52
  "eslint-plugin-n": "^18.0.1",
52
53
  "globals": "^17.6.0",
53
- "lint-staged": "^17.0.4",
54
+ "lint-staged": "^17.0.5",
54
55
  "prettier": "^3.8.3",
55
56
  "rimraf": "^6.1.3",
56
- "tsx": "^4.21.0",
57
+ "tsx": "^4.22.3",
57
58
  "typescript": "^6.0.3",
58
- "typescript-eslint": "^8.59.1",
59
- "vitest": "^4.1.5"
59
+ "typescript-eslint": "^8.59.4",
60
+ "vitest": "^4.1.7"
60
61
  },
61
62
  "engines": {
62
63
  "node": ">=22.19.0"