@salesforce/sfdx-agent-harness-mastra 0.5.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 +76 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/mastra-harness-factory.d.ts +4 -3
- package/dist/mastra-harness.d.ts +56 -2
- package/dist/mastra-harness.js +163 -2
- package/dist/mastra-message-mapper.d.ts +3 -0
- package/dist/mastra-message-mapper.js +6 -0
- package/dist/mastra-tool-mapper.js +15 -1
- package/dist/salesforce-llm-gateway.d.ts +3 -6
- package/dist/salesforce-llm-gateway.js +81 -44
- package/dist/tool-search.d.ts +148 -0
- package/dist/tool-search.js +60 -0
- package/package.json +18 -17
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
|
|
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 {
|
|
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<
|
|
10
|
+
create(storageRootFolder: string): Promise<MastraAgentHarness>;
|
|
10
11
|
}
|
package/dist/mastra-harness.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
package/dist/mastra-harness.js
CHANGED
|
@@ -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
|
-
|
|
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({
|
|
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 {
|
|
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
|
|
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():
|
|
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
|
|
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: '
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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": "^
|
|
31
|
-
"@mastra/core": "^1.
|
|
32
|
-
"@mastra/libsql": "^1.
|
|
33
|
-
"@mastra/mcp": "^1.
|
|
34
|
-
"@mastra/memory": "^1.
|
|
35
|
-
"@
|
|
36
|
-
"@salesforce/
|
|
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.
|
|
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.
|
|
44
|
+
"@salesforce/sfdx-agent-sdk": "0.6.0",
|
|
44
45
|
"@types/node": "^22.19.17",
|
|
45
|
-
"@vitest/coverage-istanbul": "^4.1.
|
|
46
|
-
"@vitest/eslint-plugin": "^1.6.
|
|
47
|
-
"eslint": "^10.
|
|
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.
|
|
54
|
+
"lint-staged": "^17.0.5",
|
|
54
55
|
"prettier": "^3.8.3",
|
|
55
56
|
"rimraf": "^6.1.3",
|
|
56
|
-
"tsx": "^4.
|
|
57
|
+
"tsx": "^4.22.3",
|
|
57
58
|
"typescript": "^6.0.3",
|
|
58
|
-
"typescript-eslint": "^8.59.
|
|
59
|
-
"vitest": "^4.1.
|
|
59
|
+
"typescript-eslint": "^8.59.4",
|
|
60
|
+
"vitest": "^4.1.7"
|
|
60
61
|
},
|
|
61
62
|
"engines": {
|
|
62
63
|
"node": ">=22.19.0"
|