@salesforce/sfdx-agent-harness-mastra 0.15.0 → 0.16.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/CHANGELOG.md +25 -0
- package/README.md +53 -18
- package/dist/index.d.ts +1 -1
- package/dist/mastra-harness.d.ts +49 -3
- package/dist/mastra-harness.js +291 -16
- package/dist/mastra-tool-result-redaction-processor.d.ts +54 -0
- package/dist/mastra-tool-result-redaction-processor.js +136 -0
- package/dist/mcp-error-classifier.js +2 -2
- package/dist/tool-search.d.ts +87 -16
- package/dist/tool-search.js +58 -7
- package/package.json +17 -16
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@salesforce/sfdx-agent-harness-mastra` are documented in this file.
|
|
4
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
|
+
|
|
6
|
+
## [0.16.0] - 2026-06-09
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
- **release**: auto-generate per-package CHANGELOG.md on publish ([#567](https://github.com/forcedotcom/agentic-dx/pull/567))
|
|
10
|
+
- unify tool-exposure policy under toolSearch.alwaysActive ([#566](https://github.com/forcedotcom/agentic-dx/pull/566))
|
|
11
|
+
- add ClaudeAgentConfig.skillSearch + decouple Mastra skillSearch from toolSearch ([#563](https://github.com/forcedotcom/agentic-dx/pull/563))
|
|
12
|
+
- **agent-sdk**: preserve MCP clients across updateAgentConfig ([#560](https://github.com/forcedotcom/agentic-dx/pull/560))
|
|
13
|
+
- **agent-sdk,harness-claude,harness-mastra**: first-class tool-result redaction ([#546](https://github.com/forcedotcom/agentic-dx/pull/546))
|
|
14
|
+
|
|
15
|
+
### Fixes
|
|
16
|
+
- **harness-claude,harness-mastra**: align event/log payload shapes ([#562](https://github.com/forcedotcom/agentic-dx/pull/562))
|
|
17
|
+
- **harness-mastra**: honor MCPServerConfig.alwaysLoad when toolSearch is set ([#558](https://github.com/forcedotcom/agentic-dx/pull/558))
|
|
18
|
+
|
|
19
|
+
### Chores
|
|
20
|
+
- **harness-claude,harness-mastra**: re-enable monorepoLintPack at four offender sites ([#561](https://github.com/forcedotcom/agentic-dx/pull/561))
|
|
21
|
+
- **deps-dev**: bump the eslint group across 1 directory with 2 updates ([#553](https://github.com/forcedotcom/agentic-dx/pull/553))
|
|
22
|
+
- **deps**: bump the mastra group across 1 directory with 3 updates ([#539](https://github.com/forcedotcom/agentic-dx/pull/539))
|
|
23
|
+
- **deps-dev**: bump the dev-dependencies group across 1 directory with 4 updates ([#536](https://github.com/forcedotcom/agentic-dx/pull/536))
|
|
24
|
+
- **deps-dev**: bump the vitest group across 1 directory with 3 updates ([#552](https://github.com/forcedotcom/agentic-dx/pull/552))
|
|
25
|
+
|
package/README.md
CHANGED
|
@@ -62,13 +62,14 @@ These types let consumers reach Mastra-specific features through the SDK's typed
|
|
|
62
62
|
`MastraAgentHarness` from `MastraHarnessFactory.create()`, so consumers usually don't reference these types explicitly —
|
|
63
63
|
they show up via inference at the call site.
|
|
64
64
|
|
|
65
|
-
| Export
|
|
66
|
-
|
|
|
67
|
-
| `MastraAgentHarness`
|
|
68
|
-
| `MastraAgentConfig`
|
|
69
|
-
| `MastraToolSearchConfig`
|
|
70
|
-
| `
|
|
71
|
-
| `
|
|
65
|
+
| Export | Kind | Description |
|
|
66
|
+
| ------------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
67
|
+
| `MastraAgentHarness` | type | Branded `AgentHarness` subtype; carries the Mastra-specific `extensions` namespace and the inferred config type. |
|
|
68
|
+
| `MastraAgentConfig` | type | `AgentConfig` extended with Mastra-only fields (`toolSearch`, `skillSearch`). Inferred at `manager.createAgent` automatically — annotation rarely needed. |
|
|
69
|
+
| `MastraToolSearchConfig` | type | Shape of `MastraAgentConfig.toolSearch` (`pool`, `alwaysActive`, `topK`, `ttl`). |
|
|
70
|
+
| `MastraSkillSearchConfig` | type | Shape of `MastraAgentConfig.skillSearch` (`topK`). Opts into Mastra's `SkillSearchProcessor`. |
|
|
71
|
+
| `ToolSearchHandle` | type | Stable interface for the tool-search runtime: `id`, `getStateStats`, `clearState`, `clearAllState`, `cleanupNow`. Wraps Mastra's internal processor. |
|
|
72
|
+
| `SkillSearchHandle` | type | Stable interface for the skill-search runtime: `id` (Mastra's processor has no other public methods today; the harness owns lifecycle). |
|
|
72
73
|
|
|
73
74
|
The Mastra-specific accessors live on `manager.extensions.mastra` (`getToolSearchProcessor(agentId)`,
|
|
74
75
|
`getSkillSearchProcessor(agentId)`, `formatMcpToolName(serverName, toolName)`) and require no separate import.
|
|
@@ -90,12 +91,13 @@ const agent = await manager.createAgent(projectRoot, {
|
|
|
90
91
|
toolSearch: {
|
|
91
92
|
pool: '*', // or an explicit name list
|
|
92
93
|
alwaysActive: [
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
//
|
|
98
|
-
|
|
94
|
+
// Every tool from a server (post-discovery expansion).
|
|
95
|
+
{ serverName: 'sfdx' },
|
|
96
|
+
// Exactly one tool on a specific server.
|
|
97
|
+
{ serverName: 'other', toolName: 'lookup_account' },
|
|
98
|
+
// Any tool by this name, regardless of source — built-ins, workspace
|
|
99
|
+
// tools, consumer-declared tools, AND any MCP server's tool surface.
|
|
100
|
+
{ toolName: 'resume_tool_operation' },
|
|
99
101
|
],
|
|
100
102
|
topK: 5, // advisory; defaults to Mastra's 5
|
|
101
103
|
ttl: 3_600_000, // ms; advisory; defaults to Mastra's 1h
|
|
@@ -105,16 +107,35 @@ const agent = await manager.createAgent(projectRoot, {
|
|
|
105
107
|
|
|
106
108
|
When `toolSearch` is set, `createAgent` blocks on MCP discovery so MCP tools enter the searchable pool. Without
|
|
107
109
|
`toolSearch`, MCP discovery stays non-blocking (existing behavior). `alwaysActive` tools are visible directly to the
|
|
108
|
-
model; everything else lives in the searchable pool.
|
|
109
|
-
|
|
110
|
+
model; everything else lives in the searchable pool.
|
|
111
|
+
|
|
112
|
+
Each `alwaysActive` entry is an `AlwaysActiveEntry` (re-exported from `@salesforce/sfdx-agent-sdk/harness` and shared
|
|
113
|
+
with `ClaudeAgentConfig.toolSearch.alwaysActive`). At least one of `serverName` or `toolName` must be present — `{}`
|
|
114
|
+
rejects at config time. Unknown `pool` names are dropped; unmatched `{ toolName }`-only entries are kept for late MCP
|
|
115
|
+
discovery.
|
|
110
116
|
|
|
111
117
|
`MastraToolSearchConfig.topK` and `ttl` are advisory — Mastra applies its own defaults if omitted.
|
|
112
118
|
|
|
113
119
|
#### Skill search
|
|
114
120
|
|
|
115
|
-
Skills (`AgentConfig.skills`) load eagerly by default.
|
|
116
|
-
harness-injected `search_skills` / `load_skill` meta-tools (Mastra's `SkillSearchProcessor`)
|
|
117
|
-
|
|
121
|
+
Skills (`AgentConfig.skills`) load eagerly by default. Set `MastraAgentConfig.skillSearch` to switch to on-demand
|
|
122
|
+
loading via harness-injected `search_skills` / `load_skill` meta-tools (Mastra's `SkillSearchProcessor`):
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
const agent = await manager.createAgent(projectRoot, {
|
|
126
|
+
instructions: '...',
|
|
127
|
+
skills: ['/path/to/skills'],
|
|
128
|
+
skillSearch: { topK: 5 }, // ranked, on-demand discovery; eager catalog stays out of the prompt
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`skillSearch.topK` is advisory; Mastra defaults to 5 if omitted. The same `skillSearch` field exists on
|
|
133
|
+
`ClaudeAgentConfig`, so cross-harness consumers can opt into on-demand skill discovery on either harness with the same
|
|
134
|
+
shape.
|
|
135
|
+
|
|
136
|
+
> **Breaking change (#468).** Pre-#468, on-demand skill search was implicitly gated on `skills && toolSearch`. That
|
|
137
|
+
> coupling has been removed: `skillSearch` is now the explicit opt-in, independent of `toolSearch`. Consumers who relied
|
|
138
|
+
> on the coupling must add `skillSearch: {}` to their config to keep the previous behavior.
|
|
118
139
|
|
|
119
140
|
#### Operational handles
|
|
120
141
|
|
|
@@ -198,6 +219,20 @@ for await (const event of result.eventStream) {
|
|
|
198
219
|
}
|
|
199
220
|
```
|
|
200
221
|
|
|
222
|
+
### Tool-result redaction
|
|
223
|
+
|
|
224
|
+
Tool-result redaction is configured at the manager layer, not the factory. Pass a `hooksForAgent` callback to
|
|
225
|
+
`createAgentManager`; the SDK threads the resolved `AgentHooks` bag through `createAgent`'s `options.hooks`, and the
|
|
226
|
+
Mastra harness registers a `ToolResultRedactionProcessor` on the agent's `inputProcessors` chain whose
|
|
227
|
+
`processInputStep` walks new tool-result rows on the MessageList and replaces their content in place. The same
|
|
228
|
+
`ToolResultRedactor` works on the Claude harness. See the SDK README → "Tool-Result Redaction" for the full pattern
|
|
229
|
+
(including a `failClosed(...)` wrapper consumers can use to substitute a safe stub on throw).
|
|
230
|
+
|
|
231
|
+
The processor composes with existing harness-internal processors (`ToolSearchProcessor`, `SkillSearchProcessor`); no
|
|
232
|
+
consumer-supplied processor passthrough is needed. Throws propagate from `processInputStep` and the harness's approval
|
|
233
|
+
coordinator surfaces them as an `error` ChatEvent on the consumer's eventStream — see ARCHITECTURE.md for the full
|
|
234
|
+
contract.
|
|
235
|
+
|
|
201
236
|
### Using LLM Gateway Express
|
|
202
237
|
|
|
203
238
|
```ts
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { MastraHarnessFactory, type MastraHarnessFactoryConfig } from './mastra-harness-factory.js';
|
|
2
2
|
export type { MastraAgentHarness } from './mastra-harness.js';
|
|
3
|
-
export { type MastraAgentConfig, type MastraToolSearchConfig, type SkillSearchHandle, type ToolSearchHandle, } from './tool-search.js';
|
|
3
|
+
export { type MastraAgentConfig, type MastraSkillSearchConfig, type MastraToolSearchConfig, type SkillSearchHandle, type ToolSearchHandle, } from './tool-search.js';
|
|
4
4
|
export type { MastraModelConfigFactory, ResolvedModelConfig } from './model-config-factory.js';
|
|
5
5
|
export { DefaultMastraModelConfigFactory } from './default-model-config-factory.js';
|
|
6
6
|
export { LLMGExpressMastraModelConfigFactory, type LLMGExpressConfig } from './llmg-express-model-config-factory.js';
|
package/dist/mastra-harness.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Agent } from '@mastra/core/agent';
|
|
|
2
2
|
import { SkillSearchProcessor, ToolSearchProcessor } from '@mastra/core/processors';
|
|
3
3
|
import { MCPClient } from '@mastra/mcp';
|
|
4
4
|
import { McpServerStatus, type AgentHarness, type WithAgentConfig, type ChatStreamResult, type HarnessAgentConfig, type MCPConfiguration, type McpServerErrorDetail, type McpServerInfo, type Message, type MessagePart, type StreamOptions, type TelemetryEventCallback, type ToolResultInfo } from '@salesforce/sfdx-agent-sdk';
|
|
5
|
+
import { type AgentHooks } from '@salesforce/sfdx-agent-sdk/harness';
|
|
5
6
|
import { MastraApprovalCoordinator } from './mastra-approval-coordinator.js';
|
|
6
7
|
import type { MastraModelConfigFactory } from './model-config-factory.js';
|
|
7
8
|
import { type MastraAgentConfig, type SkillSearchHandle, type ToolSearchHandle } from './tool-search.js';
|
|
@@ -44,6 +45,12 @@ interface McpServerState {
|
|
|
44
45
|
errorDetail?: McpServerErrorDetail;
|
|
45
46
|
ready: Promise<void>;
|
|
46
47
|
settled: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Discovered tools for this server, keyed by the harness's namespaced form
|
|
50
|
+
* `${serverName}_${toolName}`. The namespacing is applied at discovery time
|
|
51
|
+
* by `runMcpDiscovery` and is the contract every reader of this map relies
|
|
52
|
+
* on (tool merging, alwaysActive expansion, MCP-tool metadata lookup).
|
|
53
|
+
*/
|
|
47
54
|
tools: ToolsInput;
|
|
48
55
|
}
|
|
49
56
|
/**
|
|
@@ -51,6 +58,13 @@ interface McpServerState {
|
|
|
51
58
|
* generic API never sees them.
|
|
52
59
|
*/
|
|
53
60
|
interface MastraAgentState {
|
|
61
|
+
/**
|
|
62
|
+
* Project folder this agent operates within. Captured at `createAgent`
|
|
63
|
+
* time and reused by `updateAgent` to rebuild the `Workspace`
|
|
64
|
+
* (`projectRoot` is immutable post-create — `Agent.updateAgentConfig`
|
|
65
|
+
* does not allow changing it).
|
|
66
|
+
*/
|
|
67
|
+
projectRoot: string;
|
|
54
68
|
/** Mastra Agent instance. */
|
|
55
69
|
agent: Agent;
|
|
56
70
|
/**
|
|
@@ -99,6 +113,15 @@ interface MastraAgentState {
|
|
|
99
113
|
toolSearchProcessor?: ToolSearchProcessor;
|
|
100
114
|
/** Skill-search processor instance, when both `skills` and `toolSearch` are configured. */
|
|
101
115
|
skillSearchProcessor?: SkillSearchProcessor;
|
|
116
|
+
/**
|
|
117
|
+
* Per-agent hook bag the SDK passed at `createAgent` time. The SDK
|
|
118
|
+
* resolves the bag once per install / restore / `updateAgentConfig` via
|
|
119
|
+
* `createAgentManager`'s `hooksForAgent` callback. The harness reads
|
|
120
|
+
* `hooks.onToolResult` at processor construction time and ignores
|
|
121
|
+
* unknown hook fields (forward-compat). Defaults to `{}` when the
|
|
122
|
+
* consumer didn't pass a `hooksForAgent`.
|
|
123
|
+
*/
|
|
124
|
+
hooks: AgentHooks;
|
|
102
125
|
}
|
|
103
126
|
/**
|
|
104
127
|
* Branded subtype consumers parameterize over to reach Mastra-specific
|
|
@@ -135,9 +158,9 @@ export type MastraAgentHarness = WithAgentConfig<AgentHarness & {
|
|
|
135
158
|
getToolSearchProcessor(agentId: string): ToolSearchHandle | undefined;
|
|
136
159
|
/**
|
|
137
160
|
* Returns a {@link SkillSearchHandle} for an agent, or `undefined`
|
|
138
|
-
* if the agent doesn't have both `skills` and `
|
|
139
|
-
* (skill search is only enabled when both are set; without `
|
|
140
|
-
* Mastra falls back to eager skill loading).
|
|
161
|
+
* if the agent doesn't have both `skills` and `skillSearch` configured
|
|
162
|
+
* (skill search is only enabled when both are set; without `skillSearch`,
|
|
163
|
+
* Mastra falls back to eager skill loading via `SkillsProcessor`).
|
|
141
164
|
*/
|
|
142
165
|
getSkillSearchProcessor(agentId: string): SkillSearchHandle | undefined;
|
|
143
166
|
/**
|
|
@@ -238,6 +261,7 @@ export declare class MastraHarness implements MastraAgentHarness {
|
|
|
238
261
|
*/
|
|
239
262
|
createAgent(agentId: string, projectRoot: string, llmGatewayClient: LLMGatewayClient, config?: HarnessAgentConfig, options?: {
|
|
240
263
|
abortSignal?: AbortSignal;
|
|
264
|
+
hooks?: AgentHooks;
|
|
241
265
|
}): Promise<void>;
|
|
242
266
|
/**
|
|
243
267
|
* @requirements
|
|
@@ -264,6 +288,28 @@ export declare class MastraHarness implements MastraAgentHarness {
|
|
|
264
288
|
* telemetry pair fires; the returned promise still resolves.
|
|
265
289
|
*/
|
|
266
290
|
reconnectMcpServer(agentId: string, serverName: string): Promise<void>;
|
|
291
|
+
/**
|
|
292
|
+
* Implementation of {@link AgentHarness.updateAgent}. Diffs the next config
|
|
293
|
+
* against the agent's currently-applied state, preserves any MCP client
|
|
294
|
+
* whose `MCPServerConfig` is structurally equal to the previously-applied
|
|
295
|
+
* one, and rebuilds the Mastra `Agent` so non-MCP changes (instructions,
|
|
296
|
+
* model, tools, skills, rules, toolSearch) take effect.
|
|
297
|
+
*
|
|
298
|
+
* The Mastra `Agent`'s `tools: () => …` closure captures `state.mcpServers`
|
|
299
|
+
* by reference, so the new `Agent` constructed here observes the same Map
|
|
300
|
+
* the diff just mutated — preserved clients keep their discovered tool
|
|
301
|
+
* catalogs without a re-listing.
|
|
302
|
+
*
|
|
303
|
+
* State writes are per-server-incremental: each disconnect/connect mutates
|
|
304
|
+
* `state.mcpServers` synchronously after the corresponding harness work
|
|
305
|
+
* finishes. A re-entrant `updateAgent` call (e.g. SDK rollback against
|
|
306
|
+
* `previousConfig`) sees the harness's current truth and reverts only the
|
|
307
|
+
* actual deltas.
|
|
308
|
+
*/
|
|
309
|
+
updateAgent(agentId: string, llmGatewayClient: LLMGatewayClient, config?: HarnessAgentConfig, options?: {
|
|
310
|
+
abortSignal?: AbortSignal;
|
|
311
|
+
hooks?: AgentHooks;
|
|
312
|
+
}): Promise<void>;
|
|
267
313
|
/**
|
|
268
314
|
* @requirements
|
|
269
315
|
* - MUST throw an error if the specified `agentId` is not found in `activeAgents`.
|
package/dist/mastra-harness.js
CHANGED
|
@@ -13,7 +13,7 @@ import { LibSQLStore } from '@mastra/libsql';
|
|
|
13
13
|
import { createClient } from '@libsql/client';
|
|
14
14
|
import { MCPClient } from '@mastra/mcp';
|
|
15
15
|
import { AgentSDKError, AgentSDKErrorType, DEFAULT_MAX_STEPS, McpServerStatus, resolveToolApprovalMode, } from '@salesforce/sfdx-agent-sdk';
|
|
16
|
-
import { HarnessBusOwner, lowerStreamInput } from '@salesforce/sfdx-agent-sdk/harness';
|
|
16
|
+
import { HarnessBusOwner, lowerStreamInput, mcpServerConfigEqual, } from '@salesforce/sfdx-agent-sdk/harness';
|
|
17
17
|
import { MastraApprovalCoordinator } from './mastra-approval-coordinator.js';
|
|
18
18
|
import { mapMastraToMessage, mapMessageToMastra, partToTranscriptText } from './mastra-message-mapper.js';
|
|
19
19
|
import { isStandardSchemaWithJSON, standardSchemaToJSONSchema } from '@mastra/schema-compat';
|
|
@@ -21,8 +21,9 @@ import { mapToolDefinitionsToMastraTools } from './mastra-tool-mapper.js';
|
|
|
21
21
|
import { hasEnabledServers, mapToMastraMCPServers } from './mcp-config-mapper.js';
|
|
22
22
|
import { buildMcpErrorDetail, sanitizeMcpErrorMessage } from './mcp-error-classifier.js';
|
|
23
23
|
import { DefaultMastraModelConfigFactory } from './default-model-config-factory.js';
|
|
24
|
-
import { partitionPool, } from './tool-search.js';
|
|
24
|
+
import { partitionPool, resolveAlwaysActive, } from './tool-search.js';
|
|
25
25
|
import {} from '@salesforce/llm-gateway-sdk';
|
|
26
|
+
import { ToolResultRedactionProcessor } from './mastra-tool-result-redaction-processor.js';
|
|
26
27
|
import { backfillCreatedAt, buildSummaryPrompt, Clock, getErrorMessage, RealClock, splitFrontmatterAndBody, UUIDGenerator, } from '@salesforce/agentic-common';
|
|
27
28
|
import { makeAbortable } from './utils.js';
|
|
28
29
|
import { Workspace, LocalFilesystem, LocalSandbox, WORKSPACE_TOOLS, WORKSPACE_TOOLS_PREFIX, } from '@mastra/core/workspace';
|
|
@@ -126,6 +127,33 @@ const MAX_TRANSCRIPT_CHARS = 400_000;
|
|
|
126
127
|
* like deploys. Per-server `timeout` in MCPServerConfig still overrides this.
|
|
127
128
|
*/
|
|
128
129
|
const DEFAULT_MCP_TIMEOUT_MS = 600_000;
|
|
130
|
+
/**
|
|
131
|
+
* Build the `Map<namespacedName, ToolSource>` index `resolveAlwaysActive`
|
|
132
|
+
* needs to match `{ serverName }` / `{ serverName, toolName }` / `{ toolName }`
|
|
133
|
+
* entries against the merged custom-tools + MCP catalog.
|
|
134
|
+
*
|
|
135
|
+
* Custom tools key on their declared name with `serverName: undefined`
|
|
136
|
+
* (matching the cross-harness `tool-call` ChatEvent shape for non-MCP tools).
|
|
137
|
+
* MCP tools key on `${serverName}_${toolName}` with the un-namespaced
|
|
138
|
+
* `(serverName, toolName)` pair as the source — recovered by stripping the
|
|
139
|
+
* `${serverName}_` prefix from the namespaced key, the same way every reader
|
|
140
|
+
* of `MastraAgentState.mcpServers` handles namespacing.
|
|
141
|
+
*/
|
|
142
|
+
function buildToolSources(customTools, mcpServers) {
|
|
143
|
+
const sources = new Map();
|
|
144
|
+
for (const toolName of Object.keys(customTools)) {
|
|
145
|
+
sources.set(toolName, { serverName: undefined, toolName });
|
|
146
|
+
}
|
|
147
|
+
for (const [serverName, serverState] of mcpServers) {
|
|
148
|
+
const prefix = `${serverName}_`;
|
|
149
|
+
for (const namespaced of Object.keys(serverState.tools)) {
|
|
150
|
+
if (!namespaced.startsWith(prefix))
|
|
151
|
+
continue;
|
|
152
|
+
sources.set(namespaced, { serverName, toolName: namespaced.slice(prefix.length) });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return sources;
|
|
156
|
+
}
|
|
129
157
|
/** Loads `config.rules` into a single composed `instructions` string. See ARCHITECTURE.md → "Rule Composition". */
|
|
130
158
|
async function composeInstructionsWithRules(baseInstructions, rulePaths) {
|
|
131
159
|
const base = baseInstructions ?? '';
|
|
@@ -206,9 +234,9 @@ async function listMarkdownFilesInDirectory(dir) {
|
|
|
206
234
|
* `Map<namespacedToolName, { serverName, McpToolInfo }>` index keyed off
|
|
207
235
|
* discovery completion.
|
|
208
236
|
*/
|
|
209
|
-
function lookupMcpToolMetadata(
|
|
237
|
+
function lookupMcpToolMetadata(mcpServers, namespacedToolName) {
|
|
210
238
|
let best;
|
|
211
|
-
for (const [serverName, serverState] of
|
|
239
|
+
for (const [serverName, serverState] of mcpServers) {
|
|
212
240
|
if (!namespacedToolName.startsWith(`${serverName}_`))
|
|
213
241
|
continue;
|
|
214
242
|
if (!(namespacedToolName in serverState.tools))
|
|
@@ -519,10 +547,11 @@ export class MastraHarness {
|
|
|
519
547
|
let toolSearchProcessor;
|
|
520
548
|
let toolSearchAlwaysActive;
|
|
521
549
|
let skillSearchProcessor;
|
|
522
|
-
// `toolSearch`
|
|
523
|
-
// even though `HarnessAgentConfig` itself doesn't
|
|
524
|
-
// `toHarnessConfig` preserves extra fields via spread.
|
|
550
|
+
// `toolSearch` and `skillSearch` ride through on the consumer's
|
|
551
|
+
// `MastraAgentConfig` even though `HarnessAgentConfig` itself doesn't
|
|
552
|
+
// declare them — `toHarnessConfig` preserves extra fields via spread.
|
|
525
553
|
const toolSearchConfig = config.toolSearch;
|
|
554
|
+
const skillSearchConfig = config.skillSearch;
|
|
526
555
|
try {
|
|
527
556
|
const allowedPaths = (config.skills ?? []).filter((s) => path.isAbsolute(s));
|
|
528
557
|
const localFs = new LocalFilesystem({ basePath: projectRoot, allowedPaths });
|
|
@@ -546,11 +575,11 @@ export class MastraHarness {
|
|
|
546
575
|
])),
|
|
547
576
|
});
|
|
548
577
|
await mastraWorkspace.init();
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
578
|
+
if (mastraWorkspace.skills && skillSearchConfig) {
|
|
579
|
+
skillSearchProcessor = new SkillSearchProcessor({
|
|
580
|
+
workspace: mastraWorkspace,
|
|
581
|
+
...(skillSearchConfig.topK !== undefined && { search: { topK: skillSearchConfig.topK } }),
|
|
582
|
+
});
|
|
554
583
|
}
|
|
555
584
|
// When toolSearch is configured, await MCP discovery so MCP tools
|
|
556
585
|
// can enter the searchable pool. Without it, MCP discovery stays
|
|
@@ -562,7 +591,16 @@ export class MastraHarness {
|
|
|
562
591
|
const mcpTools = [...mcpServerStates.values()].reduce((acc, s) => ({ ...acc, ...s.tools }), {});
|
|
563
592
|
const allTools = { ...customTools, ...mcpTools };
|
|
564
593
|
const knownNames = new Set(Object.keys(allTools));
|
|
565
|
-
|
|
594
|
+
// Resolve `toolSearch.alwaysActive` entries against the
|
|
595
|
+
// merged custom-tools + MCP catalog. Each entry covers
|
|
596
|
+
// `{ serverName }` (server-wide), `{ serverName, toolName }`
|
|
597
|
+
// (precise), or `{ toolName }` (cross-source) — see
|
|
598
|
+
// `tool-search.ts` resolveAlwaysActive for the full rules.
|
|
599
|
+
// Unresolved `{ toolName }`-only entries flow through to
|
|
600
|
+
// `partitionPool`'s late-discovery hatch.
|
|
601
|
+
const sources = buildToolSources(customTools, mcpServerStates);
|
|
602
|
+
const { resolved, unresolvedToolNames } = resolveAlwaysActive(toolSearchConfig.alwaysActive, sources);
|
|
603
|
+
const partition = partitionPool(toolSearchConfig.pool, resolved, unresolvedToolNames, knownNames);
|
|
566
604
|
toolSearchAlwaysActive = new Set(partition.alwaysActive);
|
|
567
605
|
const poolTools = {};
|
|
568
606
|
for (const name of partition.pool) {
|
|
@@ -576,7 +614,24 @@ export class MastraHarness {
|
|
|
576
614
|
ttl: toolSearchConfig.ttl,
|
|
577
615
|
});
|
|
578
616
|
}
|
|
579
|
-
|
|
617
|
+
// Tool-result redaction processor — registered LAST in the
|
|
618
|
+
// inputProcessors chain so it sees the final messageList state
|
|
619
|
+
// after any earlier processor mutations. Reads the redactor
|
|
620
|
+
// from the per-agent hooks bag the SDK passed at createAgent
|
|
621
|
+
// time (resolved by `createAgentManager`'s `hooksForAgent`).
|
|
622
|
+
// Closes over the live mcpServerStates map (same reference the
|
|
623
|
+
// agent state will hold) so MCP `serverName` enrichment on
|
|
624
|
+
// `ToolResultRedactionInput` matches the cross-harness
|
|
625
|
+
// `tool-result` ChatEvent shape.
|
|
626
|
+
const onToolResult = options?.hooks?.onToolResult;
|
|
627
|
+
const toolResultRedactionProcessor = onToolResult
|
|
628
|
+
? new ToolResultRedactionProcessor(agentId, onToolResult, (namespacedToolName) => lookupMcpToolMetadata(mcpServerStates, namespacedToolName))
|
|
629
|
+
: undefined;
|
|
630
|
+
const inputProcessorsList = [
|
|
631
|
+
skillSearchProcessor,
|
|
632
|
+
toolSearchProcessor,
|
|
633
|
+
toolResultRedactionProcessor,
|
|
634
|
+
].filter((p) => p !== undefined);
|
|
580
635
|
const inputProcessors = inputProcessorsList.length > 0 ? inputProcessorsList : undefined;
|
|
581
636
|
agent = new Agent({
|
|
582
637
|
id: agentId,
|
|
@@ -605,6 +660,7 @@ export class MastraHarness {
|
|
|
605
660
|
this.mastra.addAgent(agent);
|
|
606
661
|
mastraAgentAdded = true;
|
|
607
662
|
this.activeAgents.set(agentId, {
|
|
663
|
+
projectRoot,
|
|
608
664
|
agent,
|
|
609
665
|
composedInstructions,
|
|
610
666
|
validateFiles,
|
|
@@ -615,6 +671,7 @@ export class MastraHarness {
|
|
|
615
671
|
consumerToolNames,
|
|
616
672
|
toolSearchProcessor,
|
|
617
673
|
skillSearchProcessor,
|
|
674
|
+
hooks: options?.hooks ?? {},
|
|
618
675
|
});
|
|
619
676
|
}
|
|
620
677
|
catch (error) {
|
|
@@ -769,6 +826,7 @@ export class MastraHarness {
|
|
|
769
826
|
serverState.settled = true;
|
|
770
827
|
const emitStatusChange = this.prepareMcpStatusChange(agentId, serverName, serverState, McpServerStatus.Error, message);
|
|
771
828
|
this.buses.getLogBus()?.warn('MCP server reconnect failed', {
|
|
829
|
+
agentId,
|
|
772
830
|
serverName,
|
|
773
831
|
error: message,
|
|
774
832
|
});
|
|
@@ -797,6 +855,222 @@ export class MastraHarness {
|
|
|
797
855
|
serverState.ready = this.runMcpDiscovery(agentId, serverName, serverState.client, state.mcpServers, epoch, undefined, startTime);
|
|
798
856
|
await serverState.ready;
|
|
799
857
|
}
|
|
858
|
+
/**
|
|
859
|
+
* Implementation of {@link AgentHarness.updateAgent}. Diffs the next config
|
|
860
|
+
* against the agent's currently-applied state, preserves any MCP client
|
|
861
|
+
* whose `MCPServerConfig` is structurally equal to the previously-applied
|
|
862
|
+
* one, and rebuilds the Mastra `Agent` so non-MCP changes (instructions,
|
|
863
|
+
* model, tools, skills, rules, toolSearch) take effect.
|
|
864
|
+
*
|
|
865
|
+
* The Mastra `Agent`'s `tools: () => …` closure captures `state.mcpServers`
|
|
866
|
+
* by reference, so the new `Agent` constructed here observes the same Map
|
|
867
|
+
* the diff just mutated — preserved clients keep their discovered tool
|
|
868
|
+
* catalogs without a re-listing.
|
|
869
|
+
*
|
|
870
|
+
* State writes are per-server-incremental: each disconnect/connect mutates
|
|
871
|
+
* `state.mcpServers` synchronously after the corresponding harness work
|
|
872
|
+
* finishes. A re-entrant `updateAgent` call (e.g. SDK rollback against
|
|
873
|
+
* `previousConfig`) sees the harness's current truth and reverts only the
|
|
874
|
+
* actual deltas.
|
|
875
|
+
*/
|
|
876
|
+
async updateAgent(agentId, llmGatewayClient, config = {}, options) {
|
|
877
|
+
const state = this.getAgentStateOrThrow(agentId);
|
|
878
|
+
options?.abortSignal?.throwIfAborted();
|
|
879
|
+
// ── Phase 0: validate non-MCP inputs before any MCP client is touched.
|
|
880
|
+
// A bad rule path or skill path rejects updateAgent here so
|
|
881
|
+
// half-applied state from non-MCP failures is impossible.
|
|
882
|
+
// Mirrors Claude's posture; closes a Mastra atomicity gap
|
|
883
|
+
// where Phase 1 ran first and a Phase-2 throw left MCP
|
|
884
|
+
// servers already half-cycled.
|
|
885
|
+
const composedInstructions = await composeInstructionsWithRules(config.instructions, config.rules);
|
|
886
|
+
const allowedPaths = (config.skills ?? []).filter((s) => path.isAbsolute(s));
|
|
887
|
+
// projectRoot is immutable post-create — captured on agent state at
|
|
888
|
+
// createAgent time. Reuse it rather than threading it through every
|
|
889
|
+
// updateAgent call.
|
|
890
|
+
const projectRoot = state.projectRoot;
|
|
891
|
+
const localFs = new LocalFilesystem({ basePath: projectRoot, allowedPaths });
|
|
892
|
+
const sandbox = new LocalSandbox({
|
|
893
|
+
workingDirectory: projectRoot,
|
|
894
|
+
env: process.env,
|
|
895
|
+
});
|
|
896
|
+
const mastraWorkspace = new Workspace({
|
|
897
|
+
filesystem: localFs,
|
|
898
|
+
skills: config.skills,
|
|
899
|
+
sandbox,
|
|
900
|
+
tools: Object.fromEntries(ALL_MASTRA_WORKSPACE_TOOL_NAMES.map((nativeName) => [
|
|
901
|
+
nativeName,
|
|
902
|
+
{ name: stripMastraWorkspacePrefix(nativeName) },
|
|
903
|
+
])),
|
|
904
|
+
});
|
|
905
|
+
try {
|
|
906
|
+
await mastraWorkspace.init();
|
|
907
|
+
}
|
|
908
|
+
catch (error) {
|
|
909
|
+
await mastraWorkspace.destroy().catch(() => undefined);
|
|
910
|
+
throw error;
|
|
911
|
+
}
|
|
912
|
+
// ── Phase 1: MCP diff against state.mcpConfig ─────────────────────
|
|
913
|
+
const prevConfig = state.mcpConfig ?? {};
|
|
914
|
+
const nextConfig = config.mcpServers ?? {};
|
|
915
|
+
const allNames = new Set([...Object.keys(prevConfig), ...Object.keys(nextConfig)]);
|
|
916
|
+
const nextEnabledServers = hasEnabledServers(nextConfig)
|
|
917
|
+
? mapToMastraMCPServers(nextConfig, config.orgJwt)
|
|
918
|
+
: {};
|
|
919
|
+
for (const name of allNames) {
|
|
920
|
+
const prev = prevConfig[name];
|
|
921
|
+
const next = nextConfig[name];
|
|
922
|
+
if (mcpServerConfigEqual(prev, next))
|
|
923
|
+
continue;
|
|
924
|
+
// Tear down whatever was previously attached for this name. Both
|
|
925
|
+
// "removed" and "changed" go through this branch; "added" hits no
|
|
926
|
+
// previous entry so the disconnect is a no-op.
|
|
927
|
+
const prevEntry = state.mcpServers.get(name);
|
|
928
|
+
if (prevEntry) {
|
|
929
|
+
state.mcpServers.delete(name);
|
|
930
|
+
await prevEntry.ready.catch(() => undefined);
|
|
931
|
+
await prevEntry.client.disconnect().catch(() => undefined);
|
|
932
|
+
}
|
|
933
|
+
// Connect the new one if the next config has it enabled.
|
|
934
|
+
if (next && next.enabled !== false) {
|
|
935
|
+
const serverDef = nextEnabledServers[name];
|
|
936
|
+
if (serverDef) {
|
|
937
|
+
const client = new MCPClient({
|
|
938
|
+
id: `${agentId}:${name}`,
|
|
939
|
+
servers: { [name]: serverDef },
|
|
940
|
+
timeout: DEFAULT_MCP_TIMEOUT_MS,
|
|
941
|
+
});
|
|
942
|
+
const entry = {
|
|
943
|
+
client,
|
|
944
|
+
currentStatus: McpServerStatus.Connecting,
|
|
945
|
+
epoch: 0,
|
|
946
|
+
ready: Promise.resolve(),
|
|
947
|
+
settled: false,
|
|
948
|
+
tools: {},
|
|
949
|
+
};
|
|
950
|
+
state.mcpServers.set(name, entry);
|
|
951
|
+
entry.ready = this.runMcpDiscovery(agentId, name, client, state.mcpServers, 0, options?.abortSignal);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
// ── Phase 2: non-MCP rebuild ──────────────────────────────────────
|
|
956
|
+
const consumerToolNames = new Set((config.tools ?? []).map((t) => t.name));
|
|
957
|
+
const parkerLookup = (threadId) => this.activeAgents.get(agentId)?.coordinators.get(threadId)?.parkConsumerToolCall;
|
|
958
|
+
const customTools = config.tools?.length ? mapToolDefinitionsToMastraTools(config.tools, parkerLookup) : {};
|
|
959
|
+
const { model: resolvedModel, validateFiles } = this.modelConfigFactory.create(llmGatewayClient, this.buses.getLogBus());
|
|
960
|
+
// Capture the live mcpServers map by reference — the new Agent's
|
|
961
|
+
// tools() closure will read it on every step, so preserved clients'
|
|
962
|
+
// discovered tools stay visible without a re-listing.
|
|
963
|
+
const mcpServers = state.mcpServers;
|
|
964
|
+
const toolSearchConfig = config.toolSearch;
|
|
965
|
+
const skillSearchConfig = config.skillSearch;
|
|
966
|
+
let toolSearchProcessor;
|
|
967
|
+
let toolSearchAlwaysActive;
|
|
968
|
+
let skillSearchProcessor;
|
|
969
|
+
try {
|
|
970
|
+
if (mastraWorkspace.skills && skillSearchConfig) {
|
|
971
|
+
skillSearchProcessor = new SkillSearchProcessor({
|
|
972
|
+
workspace: mastraWorkspace,
|
|
973
|
+
...(skillSearchConfig.topK !== undefined && { search: { topK: skillSearchConfig.topK } }),
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
if (toolSearchConfig) {
|
|
977
|
+
if (mcpServers.size > 0) {
|
|
978
|
+
await Promise.allSettled([...mcpServers.values()].map((s) => s.ready));
|
|
979
|
+
}
|
|
980
|
+
const mcpTools = [...mcpServers.values()].reduce((acc, s) => ({ ...acc, ...s.tools }), {});
|
|
981
|
+
const allTools = { ...customTools, ...mcpTools };
|
|
982
|
+
const knownNames = new Set(Object.keys(allTools));
|
|
983
|
+
const sources = buildToolSources(customTools, mcpServers);
|
|
984
|
+
const { resolved, unresolvedToolNames } = resolveAlwaysActive(toolSearchConfig.alwaysActive, sources);
|
|
985
|
+
const partition = partitionPool(toolSearchConfig.pool, resolved, unresolvedToolNames, knownNames);
|
|
986
|
+
toolSearchAlwaysActive = new Set(partition.alwaysActive);
|
|
987
|
+
const poolTools = {};
|
|
988
|
+
for (const name of partition.pool) {
|
|
989
|
+
if (allTools[name]) {
|
|
990
|
+
poolTools[name] = allTools[name];
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
toolSearchProcessor = new ToolSearchProcessor({
|
|
994
|
+
tools: poolTools,
|
|
995
|
+
search: toolSearchConfig.topK !== undefined ? { topK: toolSearchConfig.topK } : undefined,
|
|
996
|
+
ttl: toolSearchConfig.ttl,
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
// Tool-result redaction processor — built off the per-agent hook
|
|
1000
|
+
// bag the SDK passed at updateAgent time. Same wiring as createAgent
|
|
1001
|
+
// (registered LAST in the chain so it sees the post-mutation
|
|
1002
|
+
// messageList state); closes over the live state.mcpServers map so
|
|
1003
|
+
// MCP `serverName` enrichment matches the cross-harness shape.
|
|
1004
|
+
const onToolResult = options?.hooks?.onToolResult;
|
|
1005
|
+
const toolResultRedactionProcessor = onToolResult
|
|
1006
|
+
? new ToolResultRedactionProcessor(agentId, onToolResult, (namespacedToolName) => lookupMcpToolMetadata(mcpServers, namespacedToolName))
|
|
1007
|
+
: undefined;
|
|
1008
|
+
const inputProcessorsList = [
|
|
1009
|
+
skillSearchProcessor,
|
|
1010
|
+
toolSearchProcessor,
|
|
1011
|
+
toolResultRedactionProcessor,
|
|
1012
|
+
].filter((p) => p !== undefined);
|
|
1013
|
+
const inputProcessors = inputProcessorsList.length > 0 ? inputProcessorsList : undefined;
|
|
1014
|
+
const newAgent = new Agent({
|
|
1015
|
+
id: agentId,
|
|
1016
|
+
name: config.name || agentId,
|
|
1017
|
+
instructions: composedInstructions,
|
|
1018
|
+
model: resolvedModel,
|
|
1019
|
+
memory: this.memory,
|
|
1020
|
+
tools: () => {
|
|
1021
|
+
const mcpTools = [...mcpServers.values()].reduce((acc, s) => ({ ...acc, ...s.tools }), {});
|
|
1022
|
+
const merged = { ...customTools, ...mcpTools };
|
|
1023
|
+
if (!toolSearchAlwaysActive) {
|
|
1024
|
+
return merged;
|
|
1025
|
+
}
|
|
1026
|
+
const active = {};
|
|
1027
|
+
for (const [name, tool] of Object.entries(merged)) {
|
|
1028
|
+
if (toolSearchAlwaysActive.has(name)) {
|
|
1029
|
+
active[name] = tool;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
return active;
|
|
1033
|
+
},
|
|
1034
|
+
workspace: mastraWorkspace,
|
|
1035
|
+
...(inputProcessors ? { inputProcessors } : {}),
|
|
1036
|
+
});
|
|
1037
|
+
// Mastra's addAgent silently no-ops on a duplicate key, so we must
|
|
1038
|
+
// remove first. The previous Agent instance is still referenced by
|
|
1039
|
+
// any in-flight stream's coordinator (captured at stream-start) and
|
|
1040
|
+
// continues to run against its bound model. The next stream() will
|
|
1041
|
+
// resolve newAgent from activeAgents.
|
|
1042
|
+
this.mastra.removeAgent(agentId);
|
|
1043
|
+
this.mastra.addAgent(newAgent);
|
|
1044
|
+
// Dispose the previous workspace and skill-search processor only
|
|
1045
|
+
// after the new ones are wired in, so any synchronous reader sees
|
|
1046
|
+
// a coherent state. Coordinators are intentionally NOT disposed —
|
|
1047
|
+
// they hold references to the old Agent and finish their turns
|
|
1048
|
+
// against it.
|
|
1049
|
+
const previousWorkspace = state.mastraWorkspace;
|
|
1050
|
+
const previousSkillSearchProcessor = state.skillSearchProcessor;
|
|
1051
|
+
state.agent = newAgent;
|
|
1052
|
+
state.composedInstructions = composedInstructions;
|
|
1053
|
+
state.validateFiles = validateFiles;
|
|
1054
|
+
state.mcpConfig = config.mcpServers;
|
|
1055
|
+
state.mastraWorkspace = mastraWorkspace;
|
|
1056
|
+
state.consumerToolNames = consumerToolNames;
|
|
1057
|
+
state.toolSearchProcessor = toolSearchProcessor;
|
|
1058
|
+
state.skillSearchProcessor = skillSearchProcessor;
|
|
1059
|
+
state.hooks = options?.hooks ?? {};
|
|
1060
|
+
previousSkillSearchProcessor?.dispose();
|
|
1061
|
+
await previousWorkspace?.destroy().catch(() => undefined);
|
|
1062
|
+
}
|
|
1063
|
+
catch (error) {
|
|
1064
|
+
// Phase 2 failed after Phase 1 mutated state.mcpServers. We don't
|
|
1065
|
+
// try to revert Phase 1 here — the SDK's rollback path will call
|
|
1066
|
+
// updateAgent(previousConfig) which re-diffs against the now-
|
|
1067
|
+
// current state and reverts the actual deltas. Phase 2 cleanup is
|
|
1068
|
+
// local: dispose anything we constructed.
|
|
1069
|
+
skillSearchProcessor?.dispose();
|
|
1070
|
+
await mastraWorkspace.destroy().catch(() => undefined);
|
|
1071
|
+
throw error;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
800
1074
|
// ── Thread (Chat Session) Lifecycle ──────────────────────────────
|
|
801
1075
|
/**
|
|
802
1076
|
* @requirements
|
|
@@ -915,7 +1189,7 @@ export class MastraHarness {
|
|
|
915
1189
|
]);
|
|
916
1190
|
}
|
|
917
1191
|
catch (err) {
|
|
918
|
-
throw new AgentSDKError(`Failed to compact thread "${threadId}" for agent "${agentId}": ${
|
|
1192
|
+
throw new AgentSDKError(`Failed to compact thread "${threadId}" for agent "${agentId}": ${getErrorMessage(err)}`, AgentSDKErrorType.COMPACTION_FAILED, { cause: err });
|
|
919
1193
|
}
|
|
920
1194
|
// Source-thread cleanup is best-effort. The user's data is already
|
|
921
1195
|
// safely seeded into newThreadId; a failed delete leaves an orphan
|
|
@@ -1039,7 +1313,7 @@ export class MastraHarness {
|
|
|
1039
1313
|
toolApprovalTimeoutMs: this.toolApprovalTimeoutMs,
|
|
1040
1314
|
mode: approvalMode,
|
|
1041
1315
|
consumerToolNames: agentState.consumerToolNames,
|
|
1042
|
-
lookupMcpToolMetadata: (name) => lookupMcpToolMetadata(agentState, name),
|
|
1316
|
+
lookupMcpToolMetadata: (name) => lookupMcpToolMetadata(agentState.mcpServers, name),
|
|
1043
1317
|
logBus: this.buses.getLogBus(),
|
|
1044
1318
|
});
|
|
1045
1319
|
agentState.coordinators.set(threadId, coordinator);
|
|
@@ -1413,6 +1687,7 @@ export class MastraHarness {
|
|
|
1413
1687
|
s.settled = true;
|
|
1414
1688
|
const emitStatusChange = this.prepareMcpStatusChange(agentId, serverName, s, McpServerStatus.Error, errorMessage);
|
|
1415
1689
|
this.buses.getLogBus()?.warn('MCP server discovery failed', {
|
|
1690
|
+
agentId,
|
|
1416
1691
|
serverName,
|
|
1417
1692
|
error: errorMessage,
|
|
1418
1693
|
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ProcessInputStepArgs, Processor } from '@mastra/core/processors';
|
|
2
|
+
import { type McpToolAnnotations, type ToolResultRedactor } from '@salesforce/sfdx-agent-sdk';
|
|
3
|
+
/**
|
|
4
|
+
* Internal MCP-catalog lookup the harness passes the processor at construction
|
|
5
|
+
* time, closed over the agent's `mcpServers` map. Returns the originating
|
|
6
|
+
* server name (and optionally annotations) for a Mastra-namespaced tool name
|
|
7
|
+
* (`<serverName>_<toolName>`); `undefined` for consumer-declared / workspace
|
|
8
|
+
* / built-in tools. Mirrors `MastraHarness#lookupMcpToolMetadata` so the
|
|
9
|
+
* `ToolResultRedactionInput.serverName` enrichment matches the cross-harness
|
|
10
|
+
* shape on the `tool-result` ChatEvent.
|
|
11
|
+
*/
|
|
12
|
+
export type McpServerLookup = (namespacedToolName: string) => {
|
|
13
|
+
serverName: string;
|
|
14
|
+
annotations?: McpToolAnnotations;
|
|
15
|
+
} | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Mastra `Processor` that runs a consumer-supplied {@link ToolResultRedactor}
|
|
18
|
+
* over every new tool-result message before the next LLM call sees it.
|
|
19
|
+
*
|
|
20
|
+
* ### Why `processInputStep` and not `processInput`
|
|
21
|
+
*
|
|
22
|
+
* `processInput` fires once at the start of a stream. Tool results arrive
|
|
23
|
+
* mid-stream (the model emits a tool-call, the tool runs, the result row is
|
|
24
|
+
* appended to the MessageList, then the loop pumps another LLM call). Only
|
|
25
|
+
* `processInputStep` fires before EVERY step's LLM call — so it sees newly
|
|
26
|
+
* appended tool-result rows on the second and subsequent steps and gets a
|
|
27
|
+
* chance to redact them before the model is invoked.
|
|
28
|
+
*
|
|
29
|
+
* ### Per-call dedup
|
|
30
|
+
*
|
|
31
|
+
* The same MessageList survives across steps within a single
|
|
32
|
+
* `agent.stream()` invocation. To avoid re-redacting on every step (which
|
|
33
|
+
* would be quadratic *and* would mean the redactor sees its own previous
|
|
34
|
+
* replacements), the processor records every `toolCallId` it has redacted
|
|
35
|
+
* in `args.state`, scoped per-stream by Mastra's processor-state contract.
|
|
36
|
+
*
|
|
37
|
+
* ### Throw policy
|
|
38
|
+
*
|
|
39
|
+
* The processor does NOT catch redactor throws. An exception propagates
|
|
40
|
+
* through `processInputStep`, which the Mastra approval coordinator's pump
|
|
41
|
+
* loop treats as `pump-error`: the consumer sees a synthetic `error` +
|
|
42
|
+
* `finish('error')` ChatEvent pair on the eventStream and the run aborts.
|
|
43
|
+
* The original tool output never reaches the model. Consumers requiring a
|
|
44
|
+
* richer fail-closed substitute wrap their redactor's body in `try`/`catch`
|
|
45
|
+
* themselves — see SDK README → "Tool-Result Redaction".
|
|
46
|
+
*/
|
|
47
|
+
export declare class ToolResultRedactionProcessor implements Processor<'sfdx-tool-result-redaction'> {
|
|
48
|
+
private readonly agentId;
|
|
49
|
+
private readonly redactor;
|
|
50
|
+
private readonly lookupMcpToolMetadata;
|
|
51
|
+
readonly id: "sfdx-tool-result-redaction";
|
|
52
|
+
constructor(agentId: string, redactor: ToolResultRedactor, lookupMcpToolMetadata: McpServerLookup);
|
|
53
|
+
processInputStep(args: ProcessInputStepArgs): Promise<void>;
|
|
54
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026, Salesforce, Inc. All rights reserved.
|
|
3
|
+
* See LICENSE.txt for license terms.
|
|
4
|
+
*/
|
|
5
|
+
import { MASTRA_THREAD_ID_KEY } from '@mastra/core/request-context';
|
|
6
|
+
import {} from '@salesforce/sfdx-agent-sdk';
|
|
7
|
+
/**
|
|
8
|
+
* Mastra `Processor` that runs a consumer-supplied {@link ToolResultRedactor}
|
|
9
|
+
* over every new tool-result message before the next LLM call sees it.
|
|
10
|
+
*
|
|
11
|
+
* ### Why `processInputStep` and not `processInput`
|
|
12
|
+
*
|
|
13
|
+
* `processInput` fires once at the start of a stream. Tool results arrive
|
|
14
|
+
* mid-stream (the model emits a tool-call, the tool runs, the result row is
|
|
15
|
+
* appended to the MessageList, then the loop pumps another LLM call). Only
|
|
16
|
+
* `processInputStep` fires before EVERY step's LLM call — so it sees newly
|
|
17
|
+
* appended tool-result rows on the second and subsequent steps and gets a
|
|
18
|
+
* chance to redact them before the model is invoked.
|
|
19
|
+
*
|
|
20
|
+
* ### Per-call dedup
|
|
21
|
+
*
|
|
22
|
+
* The same MessageList survives across steps within a single
|
|
23
|
+
* `agent.stream()` invocation. To avoid re-redacting on every step (which
|
|
24
|
+
* would be quadratic *and* would mean the redactor sees its own previous
|
|
25
|
+
* replacements), the processor records every `toolCallId` it has redacted
|
|
26
|
+
* in `args.state`, scoped per-stream by Mastra's processor-state contract.
|
|
27
|
+
*
|
|
28
|
+
* ### Throw policy
|
|
29
|
+
*
|
|
30
|
+
* The processor does NOT catch redactor throws. An exception propagates
|
|
31
|
+
* through `processInputStep`, which the Mastra approval coordinator's pump
|
|
32
|
+
* loop treats as `pump-error`: the consumer sees a synthetic `error` +
|
|
33
|
+
* `finish('error')` ChatEvent pair on the eventStream and the run aborts.
|
|
34
|
+
* The original tool output never reaches the model. Consumers requiring a
|
|
35
|
+
* richer fail-closed substitute wrap their redactor's body in `try`/`catch`
|
|
36
|
+
* themselves — see SDK README → "Tool-Result Redaction".
|
|
37
|
+
*/
|
|
38
|
+
export class ToolResultRedactionProcessor {
|
|
39
|
+
agentId;
|
|
40
|
+
redactor;
|
|
41
|
+
lookupMcpToolMetadata;
|
|
42
|
+
id = 'sfdx-tool-result-redaction';
|
|
43
|
+
constructor(agentId, redactor, lookupMcpToolMetadata) {
|
|
44
|
+
this.agentId = agentId;
|
|
45
|
+
this.redactor = redactor;
|
|
46
|
+
this.lookupMcpToolMetadata = lookupMcpToolMetadata;
|
|
47
|
+
}
|
|
48
|
+
async processInputStep(args) {
|
|
49
|
+
const seen = readSeenSet(args.state);
|
|
50
|
+
// The harness sets MASTRA_THREAD_ID_KEY on every thread-bound entry
|
|
51
|
+
// point's RequestContext. Thread-less paths (e.g. compactThread's
|
|
52
|
+
// `summarize()` helper) fall back to '' — the redactor uses threadId
|
|
53
|
+
// for identification only, not routing.
|
|
54
|
+
const ctx = args.requestContext;
|
|
55
|
+
const threadIdValue = ctx?.get?.(MASTRA_THREAD_ID_KEY);
|
|
56
|
+
const threadId = typeof threadIdValue === 'string' ? threadIdValue : '';
|
|
57
|
+
for (const message of args.messages) {
|
|
58
|
+
await redactMessageInPlace({
|
|
59
|
+
message,
|
|
60
|
+
seen,
|
|
61
|
+
redactor: this.redactor,
|
|
62
|
+
lookup: this.lookupMcpToolMetadata,
|
|
63
|
+
agentId: this.agentId,
|
|
64
|
+
threadId,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const SEEN_KEY = '__sfdx-tool-result-redaction:seen-toolCallIds';
|
|
70
|
+
function readSeenSet(state) {
|
|
71
|
+
const existing = state[SEEN_KEY];
|
|
72
|
+
if (existing instanceof Set)
|
|
73
|
+
return existing;
|
|
74
|
+
const fresh = new Set();
|
|
75
|
+
state[SEEN_KEY] = fresh;
|
|
76
|
+
return fresh;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Walk one DB-shaped message's parts in place. `MastraToolInvocationPart`s
|
|
80
|
+
* carry the tool result on `toolInvocation.result` (success) or surface
|
|
81
|
+
* errors via `state === 'output-error'` with `errorText`. Both paths are
|
|
82
|
+
* funneled through the redactor so consumers don't have to special-case the
|
|
83
|
+
* error variant.
|
|
84
|
+
*/
|
|
85
|
+
async function redactMessageInPlace(opts) {
|
|
86
|
+
const { message, seen, redactor, lookup, agentId, threadId } = opts;
|
|
87
|
+
const parts = message.content.parts;
|
|
88
|
+
if (!Array.isArray(parts))
|
|
89
|
+
return;
|
|
90
|
+
for (const rawPart of parts) {
|
|
91
|
+
if (!isToolInvocationResultPart(rawPart))
|
|
92
|
+
continue;
|
|
93
|
+
const part = rawPart;
|
|
94
|
+
const inv = part.toolInvocation;
|
|
95
|
+
const toolCallId = inv.toolCallId;
|
|
96
|
+
if (seen.has(toolCallId))
|
|
97
|
+
continue;
|
|
98
|
+
seen.add(toolCallId);
|
|
99
|
+
const isError = inv.state === 'output-error' || inv.state === 'output-denied';
|
|
100
|
+
const originalOutput = isError && inv.errorText !== undefined ? inv.errorText : inv.result;
|
|
101
|
+
const metadata = lookup(inv.toolName);
|
|
102
|
+
const result = await redactor({
|
|
103
|
+
agentId,
|
|
104
|
+
threadId,
|
|
105
|
+
toolCallId,
|
|
106
|
+
toolName: inv.toolName,
|
|
107
|
+
...(metadata?.serverName ? { serverName: metadata.serverName } : {}),
|
|
108
|
+
output: originalOutput,
|
|
109
|
+
isError,
|
|
110
|
+
});
|
|
111
|
+
if (!result || typeof result !== 'object' || !('output' in result))
|
|
112
|
+
continue;
|
|
113
|
+
// Both success and error paths replace `result` so the model sees
|
|
114
|
+
// the redacted shape. `errorText` is left intact for non-error
|
|
115
|
+
// success paths (untouched) and overwritten with the redaction
|
|
116
|
+
// string when the original was an error so downstream UI keeps a
|
|
117
|
+
// consistent text projection.
|
|
118
|
+
inv.result = result.output;
|
|
119
|
+
if (isError && typeof result.output === 'string') {
|
|
120
|
+
inv.errorText = result.output;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function isToolInvocationResultPart(part) {
|
|
125
|
+
if (part.type !== 'tool-invocation')
|
|
126
|
+
return false;
|
|
127
|
+
const inv = part.toolInvocation;
|
|
128
|
+
if (!inv || typeof inv.toolCallId !== 'string' || typeof inv.toolName !== 'string')
|
|
129
|
+
return false;
|
|
130
|
+
// Only redact result-bearing states. `partial-call` / `call` /
|
|
131
|
+
// `approval-requested` / `approval-responded` haven't produced an output
|
|
132
|
+
// yet; redacting them would either no-op (result is undefined) or get
|
|
133
|
+
// overwritten on the next state transition.
|
|
134
|
+
return inv.state === 'result' || inv.state === 'output-error' || inv.state === 'output-denied';
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=mastra-tool-result-redaction-processor.js.map
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Copyright 2026, Salesforce, Inc. All rights reserved.
|
|
3
3
|
* See LICENSE.txt for license terms.
|
|
4
4
|
*/
|
|
5
|
-
import { isAbortError } from '@salesforce/agentic-common';
|
|
5
|
+
import { getErrorMessage, isAbortError } from '@salesforce/agentic-common';
|
|
6
6
|
/**
|
|
7
7
|
* Structural duck-type for MCP-shaped errors. Matches the upstream
|
|
8
8
|
* `@modelcontextprotocol/sdk` `McpError` class (`extends Error`, numeric
|
|
@@ -35,7 +35,7 @@ function isMcpErrorLike(err) {
|
|
|
35
35
|
* through `String(...)`.
|
|
36
36
|
*/
|
|
37
37
|
export function sanitizeMcpErrorMessage(err) {
|
|
38
|
-
const raw =
|
|
38
|
+
const raw = getErrorMessage(err);
|
|
39
39
|
const lines = raw.split(/\r?\n/);
|
|
40
40
|
// Keep the first line that is NOT a V8 stack frame. V8 frames start with
|
|
41
41
|
// arbitrary leading whitespace followed by "at " (matched at line start).
|
package/dist/tool-search.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentConfig } from '@salesforce/sfdx-agent-sdk';
|
|
2
|
+
import { type AlwaysActiveEntry } from '@salesforce/sfdx-agent-sdk/harness';
|
|
2
3
|
/**
|
|
3
4
|
* Configuration for hiding a large tool catalog behind Mastra's
|
|
4
5
|
* search/load workflow. When present on `MastraAgentConfig.toolSearch`,
|
|
@@ -9,44 +10,78 @@ import type { AgentConfig } from '@salesforce/sfdx-agent-sdk';
|
|
|
9
10
|
export type MastraToolSearchConfig = {
|
|
10
11
|
/**
|
|
11
12
|
* Tools eligible for search/load. `'*'` means every searchable tool;
|
|
12
|
-
* an explicit array limits the pool to the listed names.
|
|
13
|
+
* an explicit array limits the pool to the listed names. Pool entries
|
|
14
|
+
* are bare tool names (un-namespaced) — the harness resolves them
|
|
15
|
+
* against the merged custom-tools + MCP catalog.
|
|
13
16
|
*/
|
|
14
17
|
pool: string[] | '*';
|
|
15
18
|
/**
|
|
16
19
|
* Tools that bypass search and are always visible to the model.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
+
*
|
|
21
|
+
* Each entry covers one of three patterns:
|
|
22
|
+
* - `{ serverName: 'X' }` — every tool from server `X`.
|
|
23
|
+
* - `{ serverName: 'X', toolName: 'Y' }` — exactly tool `Y` on server `X`.
|
|
24
|
+
* - `{ toolName: 'Y' }` — any tool named `Y`, regardless of source.
|
|
25
|
+
*
|
|
26
|
+
* Overrides `pool` on overlap.
|
|
27
|
+
*
|
|
28
|
+
* Mirrors `ClaudeAgentConfig.toolSearch.alwaysActive`. See
|
|
29
|
+
* {@link AlwaysActiveEntry} on `@salesforce/sfdx-agent-sdk/harness`
|
|
30
|
+
* for the full matching semantics.
|
|
20
31
|
*/
|
|
21
|
-
alwaysActive?:
|
|
32
|
+
alwaysActive?: AlwaysActiveEntry[];
|
|
22
33
|
/** Max tools returned per search (advisory). */
|
|
23
34
|
topK?: number;
|
|
24
35
|
/** Time-to-live in ms for a granted tool in the active set (advisory). */
|
|
25
36
|
ttl?: number;
|
|
26
37
|
};
|
|
38
|
+
/**
|
|
39
|
+
* Configuration for hiding a large skill catalog behind Mastra's
|
|
40
|
+
* on-demand `search_skills` / `load_skill` meta-tools. When present
|
|
41
|
+
* on `MastraAgentConfig.skillSearch`, the harness wires
|
|
42
|
+
* `SkillSearchProcessor` so the model discovers skills via search
|
|
43
|
+
* rather than seeing the full catalog injected into the system
|
|
44
|
+
* prompt. When absent, the default `SkillsProcessor` (eager catalog)
|
|
45
|
+
* is used.
|
|
46
|
+
*
|
|
47
|
+
* Mirrors the cross-harness shape — Claude's `ClaudeAgentConfig.skillSearch`
|
|
48
|
+
* has the same field.
|
|
49
|
+
*/
|
|
50
|
+
export type MastraSkillSearchConfig = {
|
|
51
|
+
/**
|
|
52
|
+
* Max number of results returned per `search_skills` call (advisory).
|
|
53
|
+
* Defaults to 5 — same as Mastra's `SkillSearchProcessor` default and
|
|
54
|
+
* Claude's `ClaudeSkillSearchConfig.topK` default.
|
|
55
|
+
*/
|
|
56
|
+
topK?: number;
|
|
57
|
+
};
|
|
27
58
|
/**
|
|
28
59
|
* Mastra-flavored extension of {@link AgentConfig}, adding the Mastra-only
|
|
29
|
-
* `toolSearch`
|
|
30
|
-
* `MastraAgentHarness` brands itself with
|
|
31
|
-
* `WithAgentConfig`, so `manager.createAgent`'s
|
|
32
|
-
* this shape automatically and
|
|
33
|
-
* literal.
|
|
60
|
+
* `toolSearch` and `skillSearch` fields. Consumers do not import or pass
|
|
61
|
+
* this type explicitly — `MastraAgentHarness` brands itself with
|
|
62
|
+
* `MastraAgentConfig` via `WithAgentConfig`, so `manager.createAgent`'s
|
|
63
|
+
* config parameter resolves to this shape automatically and the extra
|
|
64
|
+
* fields autocomplete on the object literal.
|
|
34
65
|
*
|
|
35
66
|
* @example
|
|
36
67
|
* ```ts
|
|
37
68
|
* const agent = await manager.createAgent(projectRoot, {
|
|
38
69
|
* instructions: '...',
|
|
39
70
|
* tools,
|
|
40
|
-
* toolSearch: { pool: '*', alwaysActive: ['lookup_account'] },
|
|
71
|
+
* toolSearch: { pool: '*', alwaysActive: [{ toolName: 'lookup_account' }] },
|
|
72
|
+
* skills: ['/path/to/skills'],
|
|
73
|
+
* skillSearch: { topK: 5 },
|
|
41
74
|
* });
|
|
42
75
|
* ```
|
|
43
76
|
*
|
|
44
77
|
* Extra fields ride through `toHarnessConfig`'s `...rest` spread to the
|
|
45
|
-
* harness, which reads
|
|
78
|
+
* harness, which reads them at its boundary via runtime narrowing.
|
|
46
79
|
*/
|
|
47
80
|
export type MastraAgentConfig = AgentConfig & {
|
|
48
81
|
/** Hide tools behind Mastra's search/load workflow. */
|
|
49
82
|
toolSearch?: MastraToolSearchConfig;
|
|
83
|
+
/** Hide skills behind Mastra's `search_skills` / `load_skill` meta-tools. */
|
|
84
|
+
skillSearch?: MastraSkillSearchConfig;
|
|
50
85
|
};
|
|
51
86
|
/**
|
|
52
87
|
* Opaque handle to the tool-search runtime state, exposed via
|
|
@@ -97,11 +132,47 @@ export interface SkillSearchHandle {
|
|
|
97
132
|
readonly id: string;
|
|
98
133
|
}
|
|
99
134
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
135
|
+
* Tool sourcing context fed to {@link resolveAlwaysActive}. The harness has
|
|
136
|
+
* already merged consumer custom tools with discovered MCP tools by the
|
|
137
|
+
* time it asks tool-search to partition; this map keeps each tool's origin
|
|
138
|
+
* recoverable so `{ serverName }` and `{ toolName }` entries match correctly.
|
|
139
|
+
*/
|
|
140
|
+
export type ToolSource = {
|
|
141
|
+
/** Logical MCP server name (the key in `AgentConfig.mcpServers`), or `undefined` for non-MCP tools (custom tools, workspace tools, built-ins). */
|
|
142
|
+
serverName: string | undefined;
|
|
143
|
+
/** Bare tool name from the upstream MCP server's `tools/list`, or the consumer-declared name for non-MCP tools. */
|
|
144
|
+
toolName: string;
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* Resolve {@link MastraToolSearchConfig.alwaysActive} entries to a set of
|
|
148
|
+
* Mastra-namespaced tool names that should bypass tool-search deferral.
|
|
149
|
+
*
|
|
150
|
+
* Each entry is matched against `knownToolSources` (a map keyed by the
|
|
151
|
+
* Mastra-namespaced name `{customName}` for custom tools or
|
|
152
|
+
* `${serverName}_${toolName}` for MCP tools, with the un-namespaced
|
|
153
|
+
* `(serverName, toolName)` pair as the value) using the cross-harness
|
|
154
|
+
* {@link matchesAlwaysActive} predicate. Unmatched entries are kept in the
|
|
155
|
+
* returned `unresolved` list so the harness can fold them into the
|
|
156
|
+
* tool-search pool's "kept for late MCP discovery" hatch — same posture
|
|
157
|
+
* as the previous `partitionPool`'s string-based behavior.
|
|
158
|
+
*
|
|
159
|
+
* Pure / deterministic.
|
|
160
|
+
*/
|
|
161
|
+
export declare function resolveAlwaysActive(entries: AlwaysActiveEntry[] | undefined, knownToolSources: ReadonlyMap<string, ToolSource>): {
|
|
162
|
+
resolved: Set<string>;
|
|
163
|
+
unresolvedToolNames: string[];
|
|
164
|
+
};
|
|
165
|
+
/**
|
|
166
|
+
* Resolve `pool` / `alwaysActive` against known tools.
|
|
167
|
+
*
|
|
168
|
+
* - `pool: '*'` expands to all known namespaced names; unknown pool names
|
|
169
|
+
* in an explicit array are dropped.
|
|
170
|
+
* - `alwaysActive` is the resolved-and-namespaced set from
|
|
171
|
+
* {@link resolveAlwaysActive}, plus any unresolved `{ toolName }`-only
|
|
172
|
+
* entries kept for late MCP discovery.
|
|
173
|
+
* - alwaysActive overrides pool on overlap.
|
|
103
174
|
*/
|
|
104
|
-
export declare function partitionPool(poolSpec: string[] | '*',
|
|
175
|
+
export declare function partitionPool(poolSpec: string[] | '*', alwaysActiveResolved: Set<string>, unresolvedToolNames: readonly string[], allKnown: ReadonlySet<string>): {
|
|
105
176
|
pool: string[];
|
|
106
177
|
alwaysActive: string[];
|
|
107
178
|
};
|
package/dist/tool-search.js
CHANGED
|
@@ -2,16 +2,67 @@
|
|
|
2
2
|
* Copyright 2026, Salesforce, Inc. All rights reserved.
|
|
3
3
|
* See LICENSE.txt for license terms.
|
|
4
4
|
*/
|
|
5
|
+
import { matchesAlwaysActive, validateAlwaysActiveEntry, } from '@salesforce/sfdx-agent-sdk/harness';
|
|
5
6
|
/**
|
|
6
|
-
* Resolve
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Resolve {@link MastraToolSearchConfig.alwaysActive} entries to a set of
|
|
8
|
+
* Mastra-namespaced tool names that should bypass tool-search deferral.
|
|
9
|
+
*
|
|
10
|
+
* Each entry is matched against `knownToolSources` (a map keyed by the
|
|
11
|
+
* Mastra-namespaced name `{customName}` for custom tools or
|
|
12
|
+
* `${serverName}_${toolName}` for MCP tools, with the un-namespaced
|
|
13
|
+
* `(serverName, toolName)` pair as the value) using the cross-harness
|
|
14
|
+
* {@link matchesAlwaysActive} predicate. Unmatched entries are kept in the
|
|
15
|
+
* returned `unresolved` list so the harness can fold them into the
|
|
16
|
+
* tool-search pool's "kept for late MCP discovery" hatch — same posture
|
|
17
|
+
* as the previous `partitionPool`'s string-based behavior.
|
|
18
|
+
*
|
|
19
|
+
* Pure / deterministic.
|
|
9
20
|
*/
|
|
10
|
-
export function
|
|
11
|
-
const
|
|
12
|
-
const
|
|
21
|
+
export function resolveAlwaysActive(entries, knownToolSources) {
|
|
22
|
+
const resolved = new Set();
|
|
23
|
+
const unresolvedToolNames = [];
|
|
24
|
+
if (!entries || entries.length === 0)
|
|
25
|
+
return { resolved, unresolvedToolNames };
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
validateAlwaysActiveEntry(entry);
|
|
28
|
+
let matched = false;
|
|
29
|
+
for (const [namespaced, source] of knownToolSources) {
|
|
30
|
+
if (matchesAlwaysActive([entry], source.serverName, source.toolName)) {
|
|
31
|
+
resolved.add(namespaced);
|
|
32
|
+
matched = true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (!matched && entry.toolName !== undefined && entry.serverName === undefined) {
|
|
36
|
+
// Late-discovery hatch: a `{ toolName }`-only entry that doesn't
|
|
37
|
+
// resolve today might match a yet-to-be-discovered MCP tool by the
|
|
38
|
+
// same name. Carry it forward so the partition function can keep
|
|
39
|
+
// it visible. Server-scoped misses are NOT carried — those are
|
|
40
|
+
// typos (configured server name we know of, no matching tool) and
|
|
41
|
+
// surfacing them would mask the typo.
|
|
42
|
+
unresolvedToolNames.push(entry.toolName);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { resolved, unresolvedToolNames };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Resolve `pool` / `alwaysActive` against known tools.
|
|
49
|
+
*
|
|
50
|
+
* - `pool: '*'` expands to all known namespaced names; unknown pool names
|
|
51
|
+
* in an explicit array are dropped.
|
|
52
|
+
* - `alwaysActive` is the resolved-and-namespaced set from
|
|
53
|
+
* {@link resolveAlwaysActive}, plus any unresolved `{ toolName }`-only
|
|
54
|
+
* entries kept for late MCP discovery.
|
|
55
|
+
* - alwaysActive overrides pool on overlap.
|
|
56
|
+
*/
|
|
57
|
+
export function partitionPool(poolSpec, alwaysActiveResolved, unresolvedToolNames, allKnown) {
|
|
58
|
+
// Late-discovery names appended verbatim — the partition's consumer (the
|
|
59
|
+
// ToolSearchProcessor) treats `alwaysActive` as a name-list it filters
|
|
60
|
+
// against the live tool catalog, so unmatched names are harmless until
|
|
61
|
+
// they resolve.
|
|
62
|
+
const merged = [...alwaysActiveResolved, ...unresolvedToolNames];
|
|
63
|
+
const activeSet = new Set(merged);
|
|
13
64
|
const resolvedPoolBase = poolSpec === '*' ? Array.from(allKnown) : poolSpec.filter((n) => allKnown.has(n));
|
|
14
65
|
const pool = resolvedPoolBase.filter((n) => !activeSet.has(n));
|
|
15
|
-
return { pool, alwaysActive:
|
|
66
|
+
return { pool, alwaysActive: merged };
|
|
16
67
|
}
|
|
17
68
|
//# 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.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Mastra-backed AgentHarness implementation for @salesforce/sfdx-agent-sdk",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -24,40 +24,41 @@
|
|
|
24
24
|
"dist",
|
|
25
25
|
"!dist/**/*.map",
|
|
26
26
|
"!dist/test",
|
|
27
|
+
"CHANGELOG.md",
|
|
27
28
|
"LICENSE.txt"
|
|
28
29
|
],
|
|
29
30
|
"dependencies": {
|
|
30
31
|
"@ai-sdk/provider": "^3.0.10",
|
|
31
32
|
"@libsql/client": "^0.15.15",
|
|
32
|
-
"@mastra/core": "^1.
|
|
33
|
+
"@mastra/core": "^1.37.1",
|
|
33
34
|
"@mastra/libsql": "^1.11.1",
|
|
34
|
-
"@mastra/mcp": "^1.8.
|
|
35
|
-
"@mastra/memory": "^1.
|
|
35
|
+
"@mastra/mcp": "^1.8.1",
|
|
36
|
+
"@mastra/memory": "^1.20.0",
|
|
36
37
|
"@mastra/schema-compat": "^1.2.10",
|
|
37
|
-
"@salesforce/agentic-common": "0.
|
|
38
|
-
"@salesforce/llm-gateway-sdk": "0.
|
|
38
|
+
"@salesforce/agentic-common": "0.9.0",
|
|
39
|
+
"@salesforce/llm-gateway-sdk": "0.13.0"
|
|
39
40
|
},
|
|
40
41
|
"peerDependencies": {
|
|
41
|
-
"@salesforce/sfdx-agent-sdk": "0.
|
|
42
|
+
"@salesforce/sfdx-agent-sdk": "0.17.0"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"@eslint/js": "^10.0.1",
|
|
45
|
-
"@salesforce/sfdx-agent-sdk": "0.
|
|
46
|
-
"@types/node": "^22.19.
|
|
47
|
-
"@vitest/coverage-istanbul": "^4.1.
|
|
48
|
-
"@vitest/eslint-plugin": "^1.6.
|
|
49
|
-
"eslint": "^10.4.
|
|
46
|
+
"@salesforce/sfdx-agent-sdk": "0.17.0",
|
|
47
|
+
"@types/node": "^22.19.19",
|
|
48
|
+
"@vitest/coverage-istanbul": "^4.1.8",
|
|
49
|
+
"@vitest/eslint-plugin": "^1.6.19",
|
|
50
|
+
"eslint": "^10.4.1",
|
|
50
51
|
"eslint-config-prettier": "^10.1.8",
|
|
51
|
-
"eslint-import-resolver-typescript": "^4.4.
|
|
52
|
+
"eslint-import-resolver-typescript": "^4.4.5",
|
|
52
53
|
"eslint-plugin-import": "^2.32.0",
|
|
53
54
|
"eslint-plugin-n": "^18.0.1",
|
|
54
55
|
"globals": "^17.6.0",
|
|
55
|
-
"lint-staged": "^17.0.
|
|
56
|
+
"lint-staged": "^17.0.7",
|
|
56
57
|
"prettier": "^3.8.3",
|
|
57
58
|
"rimraf": "^6.1.3",
|
|
58
|
-
"tsx": "^4.22.
|
|
59
|
+
"tsx": "^4.22.4",
|
|
59
60
|
"typescript": "^6.0.3",
|
|
60
|
-
"typescript-eslint": "^8.
|
|
61
|
+
"typescript-eslint": "^8.60.1",
|
|
61
62
|
"vitest": "^4.1.7"
|
|
62
63
|
},
|
|
63
64
|
"engines": {
|