@salesforce/sfdx-agent-sdk
v0.51.0
Published
Harness-agnostic agentic infrastructure for Salesforce developer experience tooling
Downloads
5,614
Maintainers
Keywords
Readme
@salesforce/sfdx-agent-sdk
Harness-agnostic SDK for creating and managing AI agents within the Salesforce developer experience. It provides a stable API for agent lifecycle, chat sessions, streaming responses, MCP tool integration, and tool approval flows — without coupling consumer code to a specific AI framework.
Quick Start
Closed source. This package is published to npm under the Salesforce Public Code License and is for use by Salesforce only.
import { createAgentManager } from '@salesforce/sfdx-agent-sdk';
import { MastraHarnessFactory } from '@salesforce/sfdx-agent-harness-mastra';
// 1. Create the manager. This validates the storage folder, gates the harness's
// protocol version, and replays any agents the SDK persisted on a prior run
// (one JSON file per agent under `${storageRootFolder}/agents/`).
const manager = await createAgentManager('/path/to/storage', new MastraHarnessFactory());
// Bridge SDK logs into your host logger so you observe restore failures + warnings.
manager.onLog((record) => {
console[record.level](record.message, record.context);
});
// Boot-time restore failures are queryable as instance state — use them to seed
// per-agent UI state, not for logging (the SDK already emitted each via onLog).
for (const failure of manager.getRestoreFailures()) {
// mark `failure.agentId` as `error` in your application state
}
// 2. Create an agent. The identity triple `{ agentId, projectRoot, config }` is
// persisted to disk; the next call to `createAgentManager` over the same
// storage folder will replay this agent automatically.
const agent = await manager.createAgent('/path/to/project', {
agentId: 'developer-assistant',
modelId: 'sfdc_ai__DefaultGPT5',
instructions: 'You are a helpful Salesforce developer assistant.',
});
// 3. Open a chat session and stream a response
const session = await agent.createChatSession();
const { eventStream } = await session.chat('Explain Lightning Web Components.');
for await (const event of eventStream) {
if (event.type === 'text-delta') {
process.stdout.write(event.text);
} else if (event.type === 'error') {
console.error(event.error.message);
// Don't break — the SDK invariant guarantees a synthetic
// `FinishEvent('error')` follows; the loop exits naturally next tick.
}
}
// 4. Shut down. The harness is torn down; persisted identity files are NOT
// removed, so a subsequent `createAgentManager` call restores them.
await manager.shutdown();API Reference
createAgentManager<F>(storageRootFolder, harnessFactory, options?): Promise<AgentManager<H>>
Factory function that creates an AgentManager backed by the provided HarnessFactory. The storageRootFolder must be
an existing directory and is used for persistent state (the harness's runtime data plus the SDK's per-agent identity
files at ${storageRootFolder}/agents/<id>.json). The SDK verifies that the constructed harness uses a supported
protocol version, replays any persisted agents the harness can still serve, and returns the manager.
The third-positional options bag carries per-manager opt-ins. Production callers typically leave it unset:
| Option | Type | Purpose |
| ---------------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| connectivityResolver | AgentConnectivityResolver | Overrides the default sf-CLI-based org resolution — used by e2e tests and custom-auth deployments. |
| hooksForAgent | HooksForAgent | Sync callback resolving a per-agent AgentHooks bag (today carries onToolResult). Invoked once per createAgent, boot-time restore, and Agent.updateAgentConfig. See "Tool-Result Redaction" below. |
The harness type H is inferred from the factory's create() return type, so consumers don't pass an explicit type
argument:
import { MastraHarnessFactory } from '@salesforce/sfdx-agent-harness-mastra';
const manager = await createAgentManager(storageRoot, new MastraHarnessFactory());
// ^? AgentManager<MastraAgentHarness>When the factory's create() is typed as Promise<AgentHarness> (the default), the manager is
AgentManager<AgentHarness> and behaves exactly as before. Harness packages that ship a branded subtype (e.g.
MastraAgentHarness) lift consumers into that subtype automatically — see "Harness Extensibility" below.
Restore failures (a persisted record the SDK could not bring back online — e.g. missing project directory, harness
rejection, thread rehydration failure) are queryable on the returned manager via getRestoreFailures(). Soft skips
inside the persistence directory (corrupt JSON, harness-id mismatch) are silently dropped from the restore pass and emit
a warn on the SDK's log bus.
AgentManager<H extends AgentHarness = AgentHarness>
Top-level orchestrator that owns the harness and manages agent lifecycle. AgentManager is an interface; the concrete
implementation is internal — createAgentManager is the only public entry point.
The optional H type parameter (default AgentHarness) lets harness-aware consumers reach harness-specific features
through a typed manager.extensions slot. createAgentManager infers H from the factory; you usually don't write it
explicitly. The createAgent config parameter narrows automatically when the harness brands itself with
WithAgentConfig — see "Harness Extensibility" below.
| Property / Method | Signature | Description |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| extensions | H['extensions'] | Harness-specific extensions namespace (read-only). Re-exposes the harness's extensions slot typed off H. Per-agent accessors take the agent id as their first argument. The SDK never reads or interprets this — see "Harness Extensibility" below. |
| createAgent | (projectRoot: string, config?: ConfigOf<H> & { agentId?: string }, options?: { abortSignal?: AbortSignal }) => Promise<Agent<H>> | Create and register a new agent and persist its identity triple. projectRoot must be an existing directory. If agentId is omitted a UUID is generated. The config type is inferred from the harness — see ConfigOf<H>. |
| getAgent | (agentId: string) => Agent<H> | Retrieve a live agent by ID. Throws AgentSDKError (AGENT_NOT_FOUND) for unknown ids and for ids that are only present in getRestoreFailures(). |
| getAgentIds | () => string[] | List all live agent IDs (successful + successfully restored). Failed-restore agents are not included — query getRestoreFailures() separately. |
| destroyAgent | (agentId: string) => Promise<void> | Destroy an agent, remove its identity record from disk, and clear any matching getRestoreFailures() entry. Failed-restore-only ids are accepted (no harness call made). |
| shutdown | () => Promise<void> | Destroy all live agents and shut down the harness. Identity files survive (that's the whole point) — restart createAgentManager over the same root to bring them back. |
| onTelemetry | (callback: TelemetryEventCallback) => Unsubscribe | Subscribe to telemetry across all managed agents. |
| onLog | (callback: (record: LogRecord) => void) => Unsubscribe | Subscribe to structured logs across all managed agents. Bridge this into your host logger to observe restore-failure events + soft-skip warnings. |
| onWireCommunication | (callback: WireCommunicationEventCallback) => Unsubscribe | Subscribe to wire-level communication events from the harness. Opt-in diagnostic channel that surfaces outbound LLM requests, responses, and harness-specific monitoring metadata. Subscriber-gated end-to-end — harnesses pay no cost when nobody listens. See "Wire-Communication Events" below for the event-shape catalog and the harness-asymmetric coverage (Mastra emits per-call request/response pairs; Claude emits a per-stream pointer to a debug log file). |
| getRestoreFailures | () => RestoreFailure[] | Snapshot of agents the SDK could not restore on this boot. Each entry carries the persisted { agentId, projectRoot, config } plus the underlying error. |
RestoreFailure
type RestoreFailure = {
agentId: string;
projectRoot: string;
config: AgentConfig;
error: unknown;
};Returned by AgentManager.getRestoreFailures(). Use it to seed error-state placeholders in your application; do
not iterate it for logging — the SDK already emitted each failure via onLog at error level during the restore
pass, before this function returned.
Agent<H extends AgentHarness = AgentHarness>
A configured AI agent. Factory for chat sessions. The optional H type parameter is currently informational on Agent
— harness-specific features are reached through manager.extensions, not agent.extensions. The default AgentHarness
keeps unparameterized call sites working.
| Method | Signature | Description |
| -------------------- | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| getId | () => string | Agent identifier. |
| getProjectRoot | () => string | Absolute project path. |
| getOrgConnection | () => OrgConnection \| undefined | The Salesforce OrgConnection resolved for this agent (or undefined if the connectivity resolver omitted it — non-Salesforce hosts on the BYOK / api-key path return undefined). |
| getAgentConfig | () => AgentConfig | Current configuration (shallow copy). |
| getMcpServerInfo | () => McpServerInfo[] | MCP server status and discovered tools. |
| reconnectMcpServer | (serverName: string) => Promise<void> | Recover one MCP server without recycling the agent. Semantics vary by harness — observe via getMcpServerInfo() and discovery telemetry. |
| updateAgentConfig | (config?: AgentConfig, options?: { abortSignal?: AbortSignal; forceResolve?: boolean }) => Promise<void> | Merge new config into the live agent. Pass forceResolve: true to re-run the connectivity resolver even when the partial config doesn't include orgAlias or modelId — for consumer-side state changes (BYOK toggle, feature-id flip, rate-limit gate) the resolver reads but the SDK can't observe directly. |
| createChatSession | () => Promise<ChatSession> | Open a new conversation thread. |
| getChatSession | (sessionId: string) => ChatSession | Retrieve a session. Throws AgentSDKError (CHAT_SESSION_NOT_FOUND). |
| getChatSessionIds | () => string[] | List active session IDs. |
| destroyChatSession | (sessionId: string) => Promise<void> | Destroy a session and its history. |
| cloneChatSession | (sourceSessionId: string) => Promise<ChatSession> | Clone a session with its message history. |
| compactChatSession | (sessionId: string) => Promise<ChatSession> | Compact a session into a summarized new session. |
| destroy | () => Promise<void> | Destroy the agent and all its sessions. |
| onTelemetry | (callback: TelemetryEventCallback) => Unsubscribe | Subscribe to telemetry scoped to this agent (and its sessions). |
| onLog | (callback: (record: LogRecord) => void) => Unsubscribe | Subscribe to logs scoped to this agent (and its sessions). |
ChatSession
A single conversation thread.
| Method | Signature | Description |
| ------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| getId | () => string | Session/thread identifier. |
| chat | (message: string, options?: ChatOptions) => Promise<ChatStreamResult> | Send a message and stream the response. The returned eventStream is the single iterator for the entire chat turn. |
| submitToolResult | (toolResult: ToolResultInfo) => Promise<void> | Return a consumer-executed tool result. Control message on the existing turn — post-resume events flow on the same stream. |
| approveToolCall | (toolCallId: string, options?: { remember?: boolean }) => Promise<void> | Approve a pending tool call. { remember: true } ("Allow always") appends an allow rule to AgentConfig.toolPolicies and persists it before settling. Control message on the existing turn. |
| declineToolCall | (toolCallId: string, options?: { remember?: boolean }) => Promise<void> | Decline a pending tool call. { remember: true } ("Deny always") appends a deny rule and persists it before settling. Control message on the existing turn. |
| getMessageHistory | () => Promise<Message[]> | Retrieve all messages in chronological order. |
| clearHistory | () => Promise<void> | Delete all messages. |
| getContextUsage | () => ContextUsage | Snapshot of how much of the model's context window the most recent turn used. |
| addMessages | (message: string \| Message[]) => Promise<void> | Append real transcript messages (user / assistant / tool) to the thread without requesting an agent response — the write-only half of a turn. The messages persist, appear in getMessageHistory(), and replay to the model as prior conversation on the next chat(). Use it to seed earlier turns (e.g. file contents as a user message) before the first live prompt; the SDK equivalent of the service's POST /messages with noReply=true. Not setSessionContext: this writes transcript history (visible in getMessageHistory, additive); setSessionContext writes an out-of-history overlay object (never in history, whole-object replace). 'system' is not a valid role here — system-level state rides setSessionContext / AgentConfig.instructions. |
| addContext | (message: string \| Message[]) => Promise<void> | Deprecated — renamed to addMessages (identical signature/behavior); delegates to it. The old name read as a sibling of setSessionContext, but the two are distinct channels. Will be removed in a future release; migrate to addMessages. |
| setSessionContext | (content: SessionContext) => Promise<void> | Replace this session's session-context object in full (whole-object set, not a merge). Persisted per-thread and durable across restart; kept out of message history, so it never appears in getMessageHistory(). Currently persistence-only — the stored object is not yet rendered into the model's system-level context; per-harness rendering on subsequent turns lands in a follow-up. Delegates to AgentHarness.setSessionContext — see that method's JSDoc for the full delivery/durability/isolation contract. |
| getSessionContext | () => Promise<SessionContext> | Read this session's current session-context object. Returns {} (an empty object) — never null or undefined — when nothing has been set on this thread yet, so callers never need a null-check. Unrelated to getContextUsage(), which reports context-window token occupancy, not the seeded context object. |
| subscribe | (callback: (event: ChatEvent) => void) => void | Register a real-time event listener. |
| unsubscribe | (callback: (event: ChatEvent) => void) => void | Remove a listener. |
| onTelemetry | (callback: TelemetryEventCallback) => Unsubscribe | Subscribe to telemetry scoped to this session. |
| onLog | (callback: (record: LogRecord) => void) => Unsubscribe | Subscribe to logs scoped to this session. |
| dispose | () => void | Release session-level event resources. Idempotent. |
ChatStreamResult
Returned by chat(). The single eventStream covers the entire chat turn — including post-resume events from
submitToolResult / approveToolCall / declineToolCall. Settle methods return Promise<void>; the consumer keeps
iterating the same eventStream until it sees a terminal finish event.
| Property | Type | Description |
| ------------- | --------------------------- | ---------------------------------------------------------------------------- |
| eventStream | AsyncGenerator<ChatEvent> | Full lifecycle event stream for the turn (one stream per chat() call). |
| textStream | AsyncGenerator<string> | Convenience stream of text-only tokens, derived from the same eventStream. |
ChatEvent
Discriminated union (event.type) of streaming events:
| Type | Key Fields | Description |
| ----------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| start | — | Stream has begun. |
| text-delta | text | Incremental response text. |
| reasoning-delta | text | Chain-of-thought fragment. |
| tool-call | toolCallId, toolName, args, annotations?, serverName?, bareToolName? | Tool invocation. annotations is the MCP-spec hints (readOnlyHint, destructiveHint, …) when the source declared them; serverName is set when the tool came from an MCP server. toolName is the harness-namespaced display name; bareToolName is the un-namespaced leaf paired with serverName (set for MCP tools only) — use the (serverName, bareToolName) pair, never string-split toolName, for cross-harness identity. |
| tool-call-delta | toolCallId, toolName?, argsTextDelta | Incremental fragment of a tool call's args JSON, emitted while the model composes the call. Concatenate successive deltas for the same toolCallId to build the args text; the parsed result matches the terminal tool-call.args. Useful for live-typing tool inputs UI; consumers that don't need streaming-args can ignore this event and continue reading the parsed args on the terminal tool-call. toolName is optional (Claude's signal does not carry it on the wire). |
| tool-approval-request | toolCall: ToolCallInfo, annotations?, serverName?, bareToolName? | Engine requests approval before executing a tool. Same annotations / serverName / bareToolName semantics as tool-call. bareToolName is the field the SDK reads to build the remember policy matcher (see Tool Approval). |
| tool-result | toolCallId, toolName, result, isError?, error?, annotations?, serverName?, bareToolName? | Tool execution completed. error is present when isError is true (best-effort: harnesses may synthesize an Error from a string payload, so error.stack is not guaranteed to point at the tool's throw site; the field may be absent on empty error payloads). Same annotations / serverName / bareToolName semantics as tool-call. |
| tool-progress | toolCallId, toolName, output?, parentToolCallId? | Incremental progress signal from a long-running tool call. Distinct from tool-result: zero or more tool-progress events may be emitted before exactly one terminal tool-result. output and parentToolCallId are best-effort enrichment that depends on the tool — the event itself is the load-bearing "tool is still working" signal; consumers SHOULD NOT branch on which optional fields are present. Useful for "tool is working" UI on long-running tools (build, test, deploy, large search, sub-agent tasks). |
| step-start | stepIndex | New LLM invocation step began. |
| step-finish | stepIndex, finishReason, usage? | Step completed with per-step token usage. |
| error | error, code? | Mid-stream error (yielded, not thrown). |
| finish | finishReason, usage? | Stream completed with aggregate token usage. |
Diagnostic logging. The
ChatEventunion is the harness-agnostic public stream — it never carries harness-internal chunk shapes. When a harness encounters a chunk type its adapter does not recognize (typically after an upstream Mastra / Claude SDK upgrade), the chunk is skipped on the public stream and surfaced viaLogBus.debugwithchunkTypeandrawChunkin the record'scontext. Subscribe viamanager.onLog(oragent.onLog/session.onLog) at debug level to observe these. Production consumers do not need to filter for unrecognized chunks.
Per-variant event types
Every ChatEvent variant is exported as a named type so consumers can write narrowed callbacks without re-declaring the
shape: StartEvent, TextDeltaEvent, ReasoningDeltaEvent, ToolCallEvent, ToolCallDeltaEvent,
ToolApprovalRequestEvent, ToolResultEvent, ToolProgressEvent, StepStartEvent, StepFinishEvent, ErrorEvent,
FinishEvent. Useful when factoring per-event handlers out of a for await loop:
import type { ToolApprovalRequestEvent } from '@salesforce/sfdx-agent-sdk';
function onApprovalRequest(event: ToolApprovalRequestEvent): Promise<boolean> {
// typed access to event.toolCall, event.annotations, event.serverName
return promptUser(event);
}Environment Context
Every TelemetryEvent and LogRecord the SDK emits carries optional, flat identity fields — stamped once, at the
event's construction site, by the origin bus (not appended per-subscriber). Subscribe via onTelemetry / onLog at any
scope and read the fields directly (e.g. event.orgId); because they're flat top-level fields (not nested), log
pipelines like Pino → Splunk can filter on them without dotted-path escaping.
type EnvironmentFields = {
instanceType?: string; // INSTANCE_TYPE — what kind of running instance this is
instanceId?: string; // INSTANCE_ID — this specific running instance
orgId?: string; // ORG_ID
userId?: string; // USER_ID
featureId?: string; // FEATURE_ID, falling back to legacy LLMG_FEATURE_ID
};The fields are populated from these generic environment variables, read once when the buses are constructed. Each
field is optional — present only when its variable holds a non-empty value, so in a plain local process the fields are
absent entirely. A deployment maps its own concepts onto the generic vars at the environment boundary (e.g. a DX
Workspace container exports INSTANCE_TYPE=$DXW_TEMPLATE_NAME / INSTANCE_ID=$SFDX_INSTANCE_ID), so the SDK never
learns deployment-specific names. These fields are a convenience for single-tenant deployments and are subject to
change; the type and readers are re-exported from @salesforce/agentic-common:
readEnvironmentContext(env?)— maps the generic env vars onto anEnvironmentFieldsbag.resolveFeatureId(env?)—FEATURE_ID→ legacyLLMG_FEATURE_ID→undefined.
The bus hierarchy that stamps these (TimestampedEventBus / EnvironmentAwareEventBus / the reusable
BaseTelemetryEventBus) lives in @salesforce/agentic-common — see that package's README to build env-stamped
telemetry for a service of your own.
Configuration Types
AgentConfig
| Field | Type | Description |
| ---------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| orgAlias? | string | Salesforce org alias or username. Falls back to project/default org. |
| modelId? | ModelName \| Model | LLM model selector. Pass a ModelName enum value for an in-tree model (e.g. 'sfdc_ai__DefaultGPT5'), or a pre-built Model instance to opt into a Bedrock-Anthropic Claude variant the SDK has not yet released — see createClaudeModel(gatewayId, overrides) exported from this package. Legacy pinned llmgateway__* ids still resolve via a back-compat alias map. |
| name? | string | Human-readable agent name. |
| description? | string | Agent purpose description. |
| instructions? | string | System instructions for the agent. |
| tools? | ToolDefinition[] | Consumer-executed tool schemas. |
| mcpServers? | MCPConfiguration | MCP server connections. |
| skills? | string[] | Each entry is either an individual skill folder (containing SKILL.md) or a parent folder containing skill subfolders. Relative and absolute paths supported; forms can be mixed in the same array. |
| rules? | string[] | Each entry is either an individual .md rule file or a directory of .md rule files (scanned one level deep, alphabetical, non-.md skipped). Bodies are composed verbatim into the agent's effective system prompt; YAML frontmatter is optional and stripped if present. Matches Claude Code's .claude/rules/*.md convention. |
| toolPolicies? | ToolPolicyRule[] | Ordered per-tool approval rules resolved by resolveToolApprovalPolicy (cross-tier deny-wins / within-tier last-wins). Author directly or via definePolicy(...). See "Tool Approval Policy" below. |
| defaultToolDecision? | Decision | Fallback decision when no rule matches. Defaults to 'allow' (no policy ⇒ no gating). Set to 'require-approval' for a fail-closed posture (recommended for catalogs with un-annotated MCP servers). |
StreamOptions
| Field | Type | Description |
| ----------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| abortSignal? | AbortSignal | Abort the streaming operation. |
| batchApprovals? | boolean | When true, parallel approval-requests within a turn surface on the same stream so the consumer can render a batch approval card (requires Pattern A iterators — collect-all-then-settle). Defaults to false (serial). Controls only the serial-vs-batch emission UX; the gating on/off decision lives on AgentConfig.toolPolicies / AgentConfig.defaultToolDecision. No effect when no tool in the turn resolves to 'require-approval'. |
| maxSteps? | number | Maximum number of LLM call steps the agent may take per stream() invocation. Each step is one LLM call (which may produce text, tool calls, or both). Must be >= 1. Defaults to DEFAULT_MAX_STEPS (1024) — high enough to be effectively unlimited for real tasks; the practical ceiling is the context window and cost. The constant is exported so consumers and harness authors share one source of truth. |
Tool Approval Policy
Per-tool approval is configured on the agent, not per chat() call. AgentConfig.toolPolicies is an ordered list of
ToolPolicyRules; for each harness-executed tool call the harness consults resolveToolApprovalPolicy(...), a pure
function exported from this package.
export type Decision = 'allow' | 'deny' | 'require-approval';
export type ToolMatcher =
| { type: 'builtin'; name: string } // harness built-in (e.g. Claude `Bash`)
| { type: 'mcp'; serverName?: string; toolName?: string } // MCP tool(s); omitted fields are wildcards
| { type: 'mcp-annotation'; readOnlyHint?: boolean; destructiveHint?: boolean }; // by discovered annotation
export type ToolPolicyRule = {
matcher: ToolMatcher;
decision: Decision;
source?: 'built-in' | 'agent-config' | 'remember'; // advisory provenance; ignored by the resolver
};Resolution — cross-tier deny-wins / within-tier last-wins. The resolver concatenates four tiers in precedence order
— [...BUILT_IN_TOOL_POLICIES, ...harness, ...factory, ...AgentConfig.toolPolicies] — then: any matching 'deny' in
any tier wins; otherwise the last matching non-deny rule decides; if nothing matched, the result is
AgentConfig.defaultToolDecision ?? 'allow'. So a later rule overrides an earlier one of the same matcher (a consumer
rule beats a built-in), but no consumer rule can override a deny.
Built-in rules. BUILT_IN_TOOL_POLICIES (exported, frozen) ships cross-harness rules only: MCP-annotation defaults
(destructiveHint ⇒ require-approval, readOnlyHint ⇒ allow) and the skill_bridge capability-discovery meta-tools
(anchored on the exported SKILL_BRIDGE_SERVER_ID). Harness-specific built-ins (Claude's Bash, Mastra's
updateWorkingMemory) live in the harness packages' <HARNESS>_BUILT_IN_TOOL_POLICIES arrays, not here.
definePolicy helper. Compresses the common cases into a ToolPolicyRule[] (source: 'agent-config'):
import { definePolicy } from '@salesforce/sfdx-agent-sdk';
const config: AgentConfig = {
defaultToolDecision: 'require-approval', // fail-closed
toolPolicies: definePolicy({
Bash: 'deny', // builtin matcher
'mcp:sfdx': 'require-approval', // every tool from the 'sfdx' MCP server
'mcp:sfdx/list_orgs': 'allow', // one tool from the 'sfdx' server
}),
};definePolicy covers builtin and mcp:server[/tool] keys only — author the structured ToolPolicyRule form for
mcp-annotation matchers, the bare { type: 'mcp' } wildcard, or tool names containing /.
AG-UI button mapping. The settle methods grow a symmetric, honored remember flag:
| AG-UI button | SDK call | Effect |
| ------------ | ----------------------------------------- | -------------------------------------------------------- |
| Allow | approveToolCall(id) | Settle only. |
| Allow always | approveToolCall(id, { remember: true }) | Append an allow remember rule, persist, then settle. |
| Deny | declineToolCall(id) | Settle only. |
| Deny always | declineToolCall(id, { remember: true }) | Append a deny remember rule, persist, then settle. |
With { remember: true } the SDK derives the matcher from the pending tool-approval-request's
(serverName?, bareToolName ?? toolName) — bareToolName is the harness-supplied un-namespaced leaf for MCP tools
(building the matcher from the namespaced display toolName would never match on replay); built-in tools have no
bareToolName and their toolName is already bare. It appends a source: 'remember' rule to
AgentConfig.toolPolicies and persists it via updateAgentConfig before settling — so the decision survives a
restart and a persistence failure surfaces as the settle's rejection. A remember settle for a toolCallId with no
pending approval throws TOOL_CALL_NOT_FOUND.
MCPConfiguration
type MCPConfiguration = Record<string, MCPServerConfig>;
// Stdio server (local subprocess)
type MCPStdioServerConfig = {
type: 'stdio';
command: string;
args?: string[];
env?: Record<string, string>;
enabled?: boolean;
timeout?: number;
};
// Remote server (HTTP/SSE)
type MCPRemoteServerConfig = {
type: 'remote';
url: string | URL;
headers?: Record<string, string>;
enabled?: boolean;
timeout?: number;
reconnectionOptions?: {
maxRetries?: number;
initialReconnectionDelay?: number;
maxReconnectionDelay?: number;
reconnectionDelayGrowFactor?: number;
};
};Tool-exposure policy (which tools bypass the active runtime's tool-search deferral) is configured per-agent on the
harness extension surface, not per-server here. See MastraAgentConfig.toolSearch.alwaysActive and
ClaudeAgentConfig.toolSearch.alwaysActive for the entry shape that covers "all tools from server X", "tool Y on server
X", and "tool Y from any source".
reconnectionOptions tunes the HTTP MCP transport's retry / backoff behavior. Forwarded to the underlying SDK
transport on both harnesses (Claude's @modelcontextprotocol/sdk StreamableHTTPClientTransport and Mastra's
@mastra/mcp HttpServerDefinition, which is itself typed off the same MCP SDK shape). Each field is optional;
unspecified fields fall back to the MCP SDK's built-in defaults — maxRetries: 2, initialReconnectionDelay: 1000 ms,
maxReconnectionDelay: 30000 ms, reconnectionDelayGrowFactor: 1.5. Partial overrides are merged with those defaults
at the harness boundary so a consumer setting only maxRetries
