npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@salesforce/sfdx-agent-sdk

v0.25.0

Published

Harness-agnostic agentic infrastructure for Salesforce developer experience tooling

Downloads

4,630

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: 'llmgateway__OpenAIGPT5',
  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. Control message on the existing turn — post-resume events flow on the same stream. | | declineToolCall | (toolCallId: string) => Promise<void> | Decline a pending tool call. Control message on the existing turn — post-resume events flow on the same stream. | | 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. | | addContext | (message: string \| Message[]) => Promise<void> | Inject context without triggering an LLM response. | | 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? | 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. | | 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? | Engine requests approval before executing a tool. Same annotations / serverName semantics as tool-call. | | tool-result | toolCallId, toolName, result, isError?, error?, annotations?, serverName? | 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 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 ChatEvent union 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 via LogBus.debug with chunkType and rawChunk in the record's context. Subscribe via manager.onLog (or agent.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);
}

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. 'llmgateway__OpenAIGPT5'), 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. | | 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. |

StreamOptions

| Field | Type | Description | | ---------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | abortSignal? | AbortSignal | Abort the streaming operation. | | requireToolApproval? | boolean \| ToolApprovalMode | Gates native tool execution behind a tool-approval-request event. true / 'serial' (the default) emits one approval per stream — safe for any iterator pattern. 'batch' opts into parallel-approval UX: when the model emits parallel tool_use blocks, all approvals surface on the same stream so the consumer can render a batch approval card. 'batch' requires Pattern A iterators (collect-all-approvals-then-settle); a break-on-first-approval loop will hang. See "Tool Approval Flow" below. | | 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. |

ToolApprovalMode is the exported type alias 'serial' | 'batch' — useful when typing a settings struct that drives requireToolApproval. Pair with resolveToolApprovalMode(boolean | ToolApprovalMode | undefined) (also exported) to normalize consumer input the same way the SDK does internally (undefined / falseundefined, true'serial', strings pass through, unknown strings throw).

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 doesn't zero out the others. No-op for stdio servers — only MCPRemoteServerConfig carries it.

McpServerInfo

| Field | Type | Description | | -------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | name | string | Server identifier. | | status | McpServerStatus | Connection state — exported enum with the values 'connected' \| 'connecting' \| 'disabled' \| 'error' \| 'reconnecting'. 'reconnecting' is reported during an Agent.reconnectMcpServer(name) call against a previously-'connected' server. | | tools | McpToolInfo[] | Discovered tools (name + metadata). | | error? | string | Sanitized human-readable error message when status is 'error'. Stack frames and file paths are stripped at the harness boundary. | | errorDetail? | McpServerErrorDetail | Structured failure projection for programmatic routing (category / code / retriable). Populated when status is 'error'. |

McpServerErrorDetail

Structured projection of an MCP server failure. Mirror is also attached to the mcp-server-discovery-failed telemetry event so subscribers can route on it without pattern-matching error.message.

| Field | Type | Description | | ----------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | category | McpServerErrorCategory | Stable category for routing logic. Set is additive across minor versions — values are added but never renamed or removed. | | code? | number | JSON-RPC error code from the underlying McpError, when the failure originated as a JSON-RPC error. Undefined for transport-level failures and for harnesses whose underlying SDK does not surface the code (e.g. Claude). | | retriable | boolean | Whether the SDK considers the failure transient (worth Agent.reconnectMcpServer / Agent.refreshMcpAuth) versus fatal. |

McpServerErrorCategory values: 'connect-timeout', 'http-401', 'http-403', 'http-4xx', 'http-5xx', 'transport-eof', 'protocol-error', 'config-error', 'aborted', 'unknown'.

McpToolInfo

Runtime metadata for a single MCP-discovered tool. The required fields (name, serverName, toolName) are populated by every harness; the optional fields are filled when the underlying harness can supply them from its MCP client and left undefined otherwise. Consumers must treat every optional field as undefined-tolerant.

| Field | Type | Description | | -------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | name | string | Tool name as exposed to the LLM, including any harness-applied namespacing. Format is harness-specific — Mastra: ${server}_${tool}; Claude: mcp__${server}__${tool}. See note below. | | serverName | string | Logical MCP server name as configured in AgentConfig.mcpServers. Use together with toolName for harness-agnostic tool lookup. | | toolName | string | Bare tool name as declared by the upstream MCP server's tools/list response (the un-namespaced form). Identical across harnesses for the same server. | | description? | string | Human-readable description of what the tool does. | | inputSchema? | Record<string, unknown> | Tool input parameters as a JSON Schema object (the MCP wire format). | | annotations? | McpToolAnnotations | Behavioral / UI-presentation hints declared by the MCP server. |

name is harness-specific; use (serverName, toolName) for cross-harness lookups. name round-trips against tool-call / tool-result / tool-approval-request events on the same harness, so consumers wiring UI to a single harness can match on it. Code that needs to identify a tool across both Mastra and Claude — or against getMcpServerInfo() regardless of which harness was constructed — must match on the (serverName, toolName) pair. Don't regex name to recover the components, and don't try to construct it portably (no helper produces the right format on every harness).

inputSchema is a JSON Schema object, not a Zod schema. It is typed as Record<string, unknown> so this package incurs no zod or @types/json-schema dependency. If you need a Zod schema at runtime, convert with a library such as json-schema-to-zod; for runtime validation, feed the schema to a JSON Schema validator such as AJV.

The exact set of keys present on inputSchema depends on the harness's normalization step. The Mastra harness, for example, reaches consumers after passing through @mastra/schema-compat's converter, which adds a $schema annotation (typically http://json-schema.org/draft-07/schema#) and additionalProperties: false even when the source MCP server did not declare them. Both are valid JSON Schema annotations and are forwarded untouched.

Why no outputSchema field? The MCP protocol carries an optional outputSchema per tool, but neither shipped harness can supply it: Mastra's @mastra/mcp strips it from each wrapped tool before the harness sees it (to keep CallToolResult validation correct), and the Claude Agent SDK's MCP status surface omits the field entirely. We deliberately keep it off the SDK contract rather than ship an always-undefined field consumers would have to ignore; adding it later if a harness gains the data is non-breaking.

McpToolAnnotations

Mirrors the MCP protocol's Tool.annotations shape. Each field is optional because MCP servers populate annotations à la carte; absence means "the server did not declare this hint," not "false."

| Field | Type | Description | | ------------------ | --------- | ------------------------------------------------------------------------------ | | title? | string | Human-readable label suitable for UI display (vs. the machine name). | | readOnlyHint? | boolean | When true, the tool only reads data and has no side effects. | | destructiveHint? | boolean | When true, the tool may perform destructive updates to its environment. | | idempotentHint? | boolean | When true, repeated calls with the same arguments have no additional effect. | | openWorldHint? | boolean | When true, the tool may interact with an open world of external entities. |

Tool Types

type ToolDefinition = {
  name: string;
  description?: string;
  inputSchema: Record<string, unknown>;
  // Optional MCP-spec UI/behavioral hints. Consumer-declared tools can carry
  // the same hints as MCP-discovered tools and receive matching UI treatment.
  // Example: `{ name: 'read_doc', annotations: { readOnlyHint: true } }`.
  annotations?: McpToolAnnotations;
};

type ToolCallInfo = {
  toolCallId: string;
  toolName: string;
  args: Record<string, unknown>;
};

type ToolResultInfo = {
  toolCallId: string;
  toolName: string;
  result: unknown;
  isError?: boolean;
  /** Present when isError is true. Best-effort: error.stack is not guaranteed to point at the tool's throw site. */
  error?: Error;
};

Message Types

type Message = {
  id: string;
  role: 'system' | 'user' | 'assistant' | 'tool';
  content: string | MessagePart[];
  createdAt?: Date;
};

type MessagePart = TextPart | ReasoningPart | ToolCallPart | ToolResultPart | ImagePart | FilePart;

// Plain text and reasoning segments (model chain-of-thought).
type TextPart = { type: 'text'; text: string };
type ReasoningPart = { type: 'reasoning'; text: string };

// Tool invocation / result segments persisted on `Message.content`. Extend the
// `ToolCallInfo` / `ToolResultInfo` shapes with a discriminator. They appear in
// message history; they are NOT valid input on `chat()` / `addContext()`.
type ToolCallPart = ToolCallInfo & { type: 'tool-call' };
type ToolResultPart = ToolResultInfo & { type: 'tool-result' };

// Multimodal input parts. `data` is base64-encoded bytes with no `data:` URI prefix.
type ImagePart = { type: 'image'; mimeType: 'image/png' | 'image/jpeg'; data: string; fileName?: string };
type FilePart = { type: 'file'; mimeType: 'application/pdf'; data: string; fileName?: string };

createdAt is required-on-read, optional-on-write:

  • Messages returned from ChatSession.getMessageHistory() always have createdAt populated, and the array is sorted ascending by createdAt. Consumer code can read msg.createdAt directly.
  • Consumers constructing Message literals for ChatSession.addContext() may omit createdAt; the SDK backfills the current time before forwarding to the harness. Pass an explicit value to override.

Multimodal input

ChatSession.chat() (and the harness stream() it delegates to) accept either a plain string or a MessagePart[]. Use the array form to send images or PDFs alongside text:

import { readFileSync } from 'node:fs';
import { AgentSDKError, AgentSDKErrorType } from '@salesforce/sfdx-agent-sdk';

// Attach a PNG image alongside a text prompt
const { eventStream } = await session.chat([
  { type: 'text', text: 'What does this screenshot show?' },
  {
    type: 'image',
    mimeType: 'image/png',
    data: readFileSync('screenshot.png').toString('base64'),
    fileName: 'screenshot.png',
  },
]);
for await (const event of eventStream) {
  if (event.type === 'text-delta') process.stdout.write(event.text);
}

// Attach a PDF
await session.chat([
  { type: 'text', text: 'Summarise the key findings in this report.' },
  {
    type: 'file',
    mimeType: 'application/pdf',
    data: readFileSync('report.pdf').toString('base64'),
    fileName: 'q1-report.pdf',
  },
]);

// Inject multimodal context before a chat turn. `createdAt` is omitted —
// the SDK backfills it before forwarding to the harness.
await session.addContext([
  {
    id: 'ctx-screenshot',
    role: 'user',
    content: [
      { type: 'text', text: 'Reference screenshot from the failing test run:' },
      { type: 'image', mimeType: 'image/png', data: readFileSync('failure.png').toString('base64') },
    ],
  },
]);
await session.chat('What component is throwing the null pointer?');

// Handle pre-stream validation errors
try {
  await session.chat([{ type: 'image', mimeType: 'image/png', data: base64Png }]);
} catch (err) {
  if (err instanceof AgentSDKError) {
    if (err.type === AgentSDKErrorType.MULTIMODAL_NOT_SUPPORTED) {
      // Model does not support file attachments, or the file violates a per-model cap
      // (unsupported format, too large, too many files).
    }
    if (err.type === AgentSDKErrorType.INVALID_MESSAGE_CONTENT) {
      // A part has an invalid type for user input (e.g. a bare tool-result part).
    }
  }
}

Only input parts (text, image, file) are valid — passing tool-call / tool-result parts is a programmer error. Validation runs before the stream is opened: callers never receive a partial stream followed by an error. The per-model formats and caps come from Model.supportedFormats, so a file is accepted or rejected identically across harnesses.

Usage & Finish Types

type UsageMetadata = {
  inputTokens?: number;
  outputTokens?: number;
  totalTokens?: number;
  reasoningTokens?: number;
  cachedInputTokens?: number;
  /** Input tokens written to the provider cache. */
  cacheWriteInputTokens?: number;
};

type ContextUsage = {
  /**
   * Last per-step usage reading observed on this session. Pre-first-turn and
   * immediately after `clearHistory()` this is `{}` (every token field undefined).
   */
  usage: UsageMetadata;
  /** The model's total context-window size in tokens. Always populated. */
  contextWindow: number;
  /**
   * `(usage.inputTokens + usage.cachedInputTokens + usage.cacheWriteInputTokens) / contextWindow`,
   * clamped to [0, 1]. Cached prompt tokens are summed in because they occupy the
   * model's context window — on Bedrock-Claude, the bulk of the prompt is reported
   * via `cachedInputTokens` / `cacheWriteInputTokens`, not `inputTokens`. `undefined`
   * when ALL three input-bearing fields are missing.
   */
  usedFraction: number | undefined;
};

type FinishReason = 'stop' | 'length' | 'tool-calls' | 'content-filter' | 'error' | 'other';

Tracking context-window utilization. ChatSession.getContextUsage() always returns a populated ContextUsage — even pre-first-turn, where usage is {} and usedFraction is undefined, but contextWindow is always available. Use it to decide when to compact a thread:

const ctx = session.getContextUsage();
if (ctx.usedFraction !== undefined && ctx.usedFraction > 0.8) {
  await agent.compactChatSession(session.getId());
}

Render a context-usage indicator that distinguishes "no reading yet" from a real measurement:

const ctx = session.getContextUsage();
const limit = ctx.contextWindow.toLocaleString(); // always available
const used = ctx.usage.inputTokens?.toLocaleString() ?? '—';
const pct = ctx.usedFraction !== undefined ? `${Math.round(ctx.usedFraction * 100)}%` : '—';
return `${used} / ${limit} tokens (${pct})`;

The snapshot uses last-step semantics, not the per-turn billing aggregate — finish.usage sums all steps in a turn and double-counts persistent context, which is the wrong denominator for "how full is my context." For per-turn billing totals, subscribe to chat-stream-completed telemetry instead.

Error Handling

The SDK throws AgentSDKError for predictable not-found and compatibility conditions. Each error has a type property from AgentSDKErrorType:

| Type | Thrown By | | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | AGENT_NOT_FOUND | AgentManager.getAgent(), AgentManager.destroyAgent() | | CHAT_SESSION_NOT_FOUND | Agent.getChatSession(), Agent.destroyChatSession(), Agent.cloneChatSession(), Agent.compactChatSession() | | COMPACTION_FAILED | Agent.compactChatSession() when the harness's underlying summarization call rejects. The original error is attached as cause; the source session is left intact. | | DISPOSED | Agent and ChatSession methods called after the owner has been destroyed | | INCOMPATIBLE_HARNESS | createAgentManager() when the factory advertises an unsupported protocolVersion, or the constructed harness reports a protocolVersion that differs from the factory's