@salesforce/sfdx-agent-harness-mastra
v0.33.0
Published
Mastra-backed AgentHarness implementation for @salesforce/sfdx-agent-sdk
Maintainers
Keywords
Readme
@salesforce/sfdx-agent-harness-mastra
Mastra-backed AgentHarness implementation for @salesforce/sfdx-agent-sdk. Provides
MastraHarnessFactory, the entry point consumers pass to createAgentManager.
Closed source. This package is published to npm under the Salesforce Public Code License and is for use by Salesforce only.
Install
npm install @salesforce/sfdx-agent-sdk @salesforce/sfdx-agent-harness-mastraQuick start
import { createAgentManager } from '@salesforce/sfdx-agent-sdk';
import { MastraHarnessFactory } from '@salesforce/sfdx-agent-harness-mastra';
const manager = await createAgentManager('/path/to/storage', new MastraHarnessFactory());See the @salesforce/sfdx-agent-sdk README for the full consumer-facing API. For
internal architecture, see ARCHITECTURE.md.
Removing the storage directory on Windows
manager.shutdown() checkpoints the WAL and closes the libsql client (forcedotcom/agentic-dx#429,
mastra-ai/mastra#17285), but the underlying SQLite handle release on Windows is asynchronous at the OS level. After
shutdown() returns, Windows can briefly hold the file lock — so a naïve fs.rm(storageRoot) immediately afterward can
fail with EBUSY. Pass maxRetries and retryDelay so Node retries until the OS lets go:
import { rm } from 'node:fs/promises';
await manager.shutdown();
await rm(storageRoot, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });Linux and macOS release the handle eagerly; the retry options are no-ops on those platforms.
Public API
Core
| Export | Kind | Description |
| ------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| MastraHarnessFactory | class | HarnessFactory that produces a Mastra-backed AgentHarness bound to a storage root. |
| MastraHarnessFactoryConfig | interface | Configuration for MastraHarnessFactory (toolApprovalTimeoutMs?). |
| MASTRA_BUILT_IN_TOOL_POLICIES | const | Frozen ReadonlyArray<ToolPolicyRule> the harness feeds the SDK's resolveToolApprovalPolicy as the tiers.harness slice — allow rules for Mastra's built-in updateWorkingMemory / skill / skill_search / skill_read tools. |
| MASTRA_BUILT_IN_TOOL_NAMES | const | The four built-in tool leaf names MASTRA_BUILT_IN_TOOL_POLICIES covers. |
Connectivity (model + provider auth) is supplied by the SDK's AgentConnectivityResolver. Use the bundled
DefaultAgentConnectivityResolver for the standard Salesforce LLM Gateway path, or ApiKeyConnectivityResolver for
direct-provider / LLMG Express api-key endpoints — both ship from @salesforce/sfdx-agent-sdk. The harness dispatches
on ModelConnectivityInfo.providerHint internally to pick the right pass-through builder for each provider.
HTTPS proxy routing
The harness honors HTTPS_PROXY / HTTP_PROXY / NO_PROXY (either casing) via @salesforce/agentic-common's
resolveProxyDispatcher(). When either proxy var is set at factory create() time, the harness wraps every in-process
fetch call site (LLM gateway language-model builders + MCP remote transports) with an undici-backed routing function —
no globalThis mutation. When no proxy env is set, fetch runs unwrapped (zero overhead).
Snapshot timing. HTTPS_PROXY / HTTP_PROXY are captured at create() time and pinned for the harness's lifetime;
NO_PROXY is re-evaluated per request. Set the proxy env vars before constructing the factory's first harness.
Extension surface
These types let consumers reach Mastra-specific features through the SDK's typed extensions slot. The SDK infers
MastraAgentHarness from MastraHarnessFactory.create(), so consumers usually don't reference these types explicitly —
they show up via inference at the call site.
| Export | Kind | Description |
| ------------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| MastraAgentHarness | type | Branded AgentHarness subtype; carries the Mastra-specific extensions namespace and the inferred config type. |
| MastraAgentConfig | type | AgentConfig extended with Mastra-only fields (toolSearch, skillSearch). Inferred at manager.createAgent automatically — annotation rarely needed. |
| MastraToolSearchConfig | type | Shape of MastraAgentConfig.toolSearch (pool, alwaysActive, topK, ttl). |
| MastraSkillSearchConfig | type | Shape of MastraAgentConfig.skillSearch (topK). Opts into Mastra's SkillSearchProcessor. |
| ToolSearchHandle | type | Stable interface for the tool-search runtime: id, getStateStats, clearState, clearAllState, cleanupNow. Wraps Mastra's internal processor. |
| SkillSearchHandle | type | Stable interface for the skill-search runtime: id (Mastra's processor has no other public methods today; the harness owns lifecycle). |
The Mastra-specific accessors live on manager.extensions.mastra (getToolSearchProcessor(agentId),
getSkillSearchProcessor(agentId), formatMcpToolName(serverName, toolName)) and require no separate import.
Tool search
Hide a large tool catalog behind Mastra's search_tools / load_tool meta-tools:
import { createAgentManager } from '@salesforce/sfdx-agent-sdk';
import { MastraHarnessFactory } from '@salesforce/sfdx-agent-harness-mastra';
const manager = await createAgentManager(storageRoot, new MastraHarnessFactory());
const agent = await manager.createAgent(projectRoot, {
instructions: '...',
tools: [...customTools],
mcpServers: { sfdx: { type: 'stdio', command: 'npx', args: ['@salesforce/mcp'] } },
toolSearch: {
pool: '*', // or an explicit name list
alwaysActive: [
// Every tool from a server (post-discovery expansion).
{ serverName: 'sfdx' },
// Exactly one tool on a specific server.
{ serverName: 'other', toolName: 'lookup_account' },
// Any tool by this name, regardless of source — built-ins, workspace
// tools, consumer-declared tools, AND any MCP server's tool surface.
{ toolName: 'resume_tool_operation' },
],
topK: 5, // advisory; defaults to Mastra's 5
ttl: 3_600_000, // ms; advisory; defaults to Mastra's 1h
},
});When toolSearch is set, createAgent blocks on MCP discovery so MCP tools enter the searchable pool. Without
toolSearch, MCP discovery stays non-blocking (existing behavior). alwaysActive tools are visible directly to the
model; everything else lives in the searchable pool.
Each alwaysActive entry is an AlwaysActiveEntry (re-exported from @salesforce/sfdx-agent-sdk/harness and shared
with ClaudeAgentConfig.toolSearch.alwaysActive). At least one of serverName or toolName must be present — {}
rejects at config time. Unknown pool names are dropped; unmatched { toolName }-only entries are kept for late MCP
discovery.
MastraToolSearchConfig.topK and ttl are advisory — Mastra applies its own defaults if omitted.
Skill search
Skills (AgentConfig.skills) load eagerly by default. Set MastraAgentConfig.skillSearch to switch to on-demand
loading via harness-injected search_skills / load_skill meta-tools (Mastra's SkillSearchProcessor):
const agent = await manager.createAgent(projectRoot, {
instructions: '...',
skills: ['/path/to/skills'],
skillSearch: { topK: 5 }, // ranked, on-demand discovery; eager catalog stays out of the prompt
});skillSearch.topK is advisory; Mastra defaults to 5 if omitted. The same skillSearch field exists on
ClaudeAgentConfig, so cross-harness consumers can opt into on-demand skill discovery on either harness with the same
shape.
Breaking change (#468). Pre-#468, on-demand skill search was implicitly gated on
skills && toolSearch. That coupling has been removed:skillSearchis now the explicit opt-in, independent oftoolSearch. Consumers who relied on the coupling must addskillSearch: {}to their config to keep the previous behavior.
Operational handles
Reach the tool-search and skill-search runtime state for an agent. The accessors return stable ToolSearchHandle /
SkillSearchHandle interfaces — not Mastra's internal processor types — so consumer code keeps compiling across Mastra
version bumps.
const tsHandle = manager.extensions.mastra.getToolSearchProcessor(agent.getId());
// ^? ToolSearchHandle | undefined
const stats = tsHandle?.getStateStats();
// { threadCount, oldestAccessTime }
const ssHandle = manager.extensions.mastra.getSkillSearchProcessor(agent.getId());
// ^? SkillSearchHandle | undefinedBoth return undefined when the agent doesn't have the corresponding feature configured (or doesn't exist). The
Mastra-internal pool / index / loaded-tools state is intentionally not on the handle interface; the documented methods
are getStateStats(), clearState(threadId?), clearAllState(), and cleanupNow() on ToolSearchHandle, plus id
on both handles.
MCP tool-event enrichment
The harness enriches every tool-call, tool-result, and tool-approval-request event with the originating MCP server
name and any annotations the server declared, so consumers can branch on tool hints or group by server without parsing
the namespaced tool name themselves. serverName is undefined for non-MCP tools (workspace tools, consumer-declared
tools); annotations is undefined when the server omitted the field (per the MCP spec).
for await (const event of result.eventStream) {
if (event.type === 'tool-approval-request') {
if (event.annotations?.readOnlyHint) {
// Auto-approve safe reads.
await session.approveToolCall(event.toolCall.toolCallId);
} else {
// Group/label by server when present, e.g. "sfdx wants to: deploy_metadata".
const approved = await promptUser(event.serverName, event.toolCall);
approved
? await session.approveToolCall(event.toolCall.toolCallId)
: await session.declineToolCall(event.toolCall.toolCallId);
}
}
}Tool-approval policy
The harness gates harness-executed tools through the SDK's resolveToolApprovalPolicy (configure rules via
AgentConfig.toolPolicies / AgentConfig.defaultToolDecision — see the SDK README). For each non-consumer tool the
harness resolves a decision and acts on it:
allow— the tool runs without surfacing atool-approval-request.deny— the tool is refused and the model receives atool-result(isError=true)so it can recover; notool-approval-requestsurfaces.require-approval— atool-approval-requestis emitted and the workflow suspends until the consumer settles.
Gating is opt-in: it engages only when the agent's config sets toolPolicies or defaultToolDecision. With neither
configured, tools run without gating (no suspension), matching the SDK's "no policy ⇒ no gating" default. The harness
adds MASTRA_BUILT_IN_TOOL_POLICIES (Mastra's updateWorkingMemory / skill / skill_search / skill_read
built-ins) and the SDK's skill_bridge meta-tool rules as allow defaults, so capability-discovery never prompts.
Consumer-executed tools (AgentConfig.tools) always bypass the gate — their execution is the consumer's responsibility
via submitToolResult. Set StreamOptions.batchApprovals: true to surface parallel require-approval requests on one
stream for a batch-approval UI.
Tool approval timeout
When the approval gate is engaged and a tool resolves to require-approval, the harness suspends the workflow on each
tool-approval-request and waits for the consumer to call approveToolCall(...) / declineToolCall(...) /
submitToolResult(...). To bound how long a pending approval can wait — for example, when a UI process crashes or a
user walks away — the factory accepts a toolApprovalTimeoutMs option. It defaults to 600_000 (10 minutes) and
matches the ClaudeHarnessFactoryConfig knob of the same name.
const harnessFactory = new MastraHarnessFactory({
toolApprovalTimeoutMs: 60_000, // auto-deny pending approvals after 60s
});When the timer fires, the harness aborts the suspended Mastra run via the per-(agentId, threadId)
MastraApprovalCoordinator, marks every emitted approval as auto-resolved, and yields a synthetic terminal pair on the
active eventStream:
{ type: 'error', error: Error('Tool approval timed out after 60000ms.'), code: 'tool-approval-timeout' }
{ type: 'finish', finishReason: 'error' }Subsequent approveToolCall / declineToolCall / submitToolResult calls for the timed-out tool are silent no-ops
(issue #589 idempotent settle) — the coordinator already auto-resolved them, so the consumer's intended outcome is
already true and there's nothing to fail. Only a toolCallId the coordinator never saw at all (typo, wrong session,
called before tool-approval-request ever fired) throws, and that throw is an AgentSDKError whose
type === AgentSDKErrorType.TOOL_CALL_NOT_FOUND. The granularity is per toolCallId — each emitted approval arms its
own timer; the first one to fire tears down the whole turn (since Mastra can't continue the suspended run after timeout
anyway). To observe the timeout, keep iterating the eventStream past the tool-approval-request event so the
synthetic terminal pair lands in your loop:
for await (const event of result.eventStream) {
if (event.type === 'error' && event.code === 'tool-approval-timeout') {
// Pending approval was never answered.
}
}Tool-result redaction
Tool-result redaction is configured at the manager layer, not the factory. Pass a hooksForAgent callback to
createAgentManager; the SDK threads the resolved AgentHooks bag through createAgent's options.hooks, and the
Mastra harness registers a ToolResultRedactionProcessor on the agent's inputProcessors chain whose
processInputStep walks new tool-result rows on the MessageList and replaces their content in place. The same
ToolResultRedactor works on the Claude harness. See the SDK README → "Tool-Result Redaction" for the full pattern
(including a failClosed(...) wrapper consumers can use to substitute a safe stub on throw).
The processor composes with existing harness-internal processors (ToolSearchProcessor, SkillSearchProcessor); no
consumer-supplied processor passthrough is needed. Throws propagate from processInputStep and the harness's approval
coordinator surfaces them as an error ChatEvent on the consumer's eventStream — see ARCHITECTURE.md for the full
contract.
Using LLM Gateway Express
LLMG Express, Anthropic-direct, OpenAI-direct, and any other api-key endpoint flow through the SDK's
ApiKeyConnectivityResolver (no harness-specific factory needed):
import { ApiKeyConnectivityResolver, createAgentManager } from '@salesforce/sfdx-agent-sdk';
import { MastraHarnessFactory } from '@salesforce/sfdx-agent-harness-mastra';
const manager = await createAgentManager('/path/to/storage', new MastraHarnessFactory(), {
connectivityResolver: new ApiKeyConnectivityResolver({
getApiKey: () => process.env.LLMG_EXPRESS_API_KEY!,
baseUrl: process.env.LLMG_EXPRESS_BASE_URL!,
providerHint: 'openai-compatible',
}),
});Development
npm run build # TypeScript compilation
npm run test # Unit tests with coverage
npm run lint # ESLint
npm run lint:fix # Auto-fix ESLint issues
npm run format # Prettier
npm run clean # Remove build artifactsFrom the repo root:
npm run build -w @salesforce/sfdx-agent-harness-mastranpm run test -w @salesforce/sfdx-agent-harness-mastra
