@salesforce/sfdx-agent-sdk 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,32 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@salesforce/sfdx-agent-sdk` are documented in this file.
4
+ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
+
6
+ ## [0.18.0] - 2026-06-09
7
+
8
+ ### Tests
9
+ - **agent-sdk**: parallel consumer-tool batch e2e (#559) ([#574](https://github.com/forcedotcom/agentic-dx/pull/574))
10
+
11
+ ### Chores
12
+ - **deps**: bump dependencies across packages @W-22695116@ ([#577](https://github.com/forcedotcom/agentic-dx/pull/577))
13
+ - resolve unrelated it.todo placeholders in agent.test.ts and client.test.ts ([#573](https://github.com/forcedotcom/agentic-dx/pull/573))
14
+ - **deps-dev**: bump @types/node from 22.19.19 to 22.19.20 in the dev-dependencies group ([#568](https://github.com/forcedotcom/agentic-dx/pull/568))
15
+
16
+ ## [0.17.0] - 2026-06-09
17
+
18
+ ### Features
19
+ - **release**: auto-generate per-package CHANGELOG.md on publish ([#567](https://github.com/forcedotcom/agentic-dx/pull/567))
20
+ - unify tool-exposure policy under toolSearch.alwaysActive ([#566](https://github.com/forcedotcom/agentic-dx/pull/566))
21
+ - add ClaudeAgentConfig.skillSearch + decouple Mastra skillSearch from toolSearch ([#563](https://github.com/forcedotcom/agentic-dx/pull/563))
22
+ - **agent-sdk**: preserve MCP clients across updateAgentConfig ([#560](https://github.com/forcedotcom/agentic-dx/pull/560))
23
+ - **agent-sdk,harness-claude,harness-mastra**: first-class tool-result redaction ([#546](https://github.com/forcedotcom/agentic-dx/pull/546))
24
+
25
+ ### Fixes
26
+ - **harness-mastra**: honor MCPServerConfig.alwaysLoad when toolSearch is set ([#558](https://github.com/forcedotcom/agentic-dx/pull/558))
27
+
28
+ ### Chores
29
+ - **deps-dev**: bump the eslint group across 1 directory with 2 updates ([#553](https://github.com/forcedotcom/agentic-dx/pull/553))
30
+ - **deps-dev**: bump the dev-dependencies group across 1 directory with 4 updates ([#536](https://github.com/forcedotcom/agentic-dx/pull/536))
31
+ - **deps-dev**: bump the vitest group across 1 directory with 3 updates ([#552](https://github.com/forcedotcom/agentic-dx/pull/552))
32
+
package/README.md CHANGED
@@ -59,14 +59,19 @@ await manager.shutdown();
59
59
 
60
60
  ## API Reference
61
61
 
62
- ### `createAgentManager<F>(storageRootFolder, harnessFactory, connectivityResolver?): Promise<AgentManager<H>>`
62
+ ### `createAgentManager<F>(storageRootFolder, harnessFactory, options?): Promise<AgentManager<H>>`
63
63
 
64
64
  Factory function that creates an `AgentManager` backed by the provided `HarnessFactory`. The `storageRootFolder` must be
65
65
  an existing directory and is used for persistent state (the harness's runtime data plus the SDK's per-agent identity
66
66
  files at `${storageRootFolder}/agents/<id>.json`). The SDK verifies that the constructed harness uses a supported
67
- protocol version, replays any persisted agents the harness can still serve, and returns the manager. The optional
68
- `connectivityResolver` overrides the default sf-CLI-based org resolution — used by e2e tests and custom-auth
69
- deployments; production callers leave it unset.
67
+ protocol version, replays any persisted agents the harness can still serve, and returns the manager.
68
+
69
+ The third-positional `options` bag carries per-manager opt-ins. Production callers typically leave it unset:
70
+
71
+ | Option | Type | Purpose |
72
+ | ---------------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
73
+ | `connectivityResolver` | `AgentConnectivityResolver` | Overrides the default sf-CLI-based org resolution — used by e2e tests and custom-auth deployments. |
74
+ | `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. |
70
75
 
71
76
  The harness type `H` is **inferred from the factory's `create()` return type**, so consumers don't pass an explicit type
72
77
  argument:
@@ -240,7 +245,6 @@ type MCPStdioServerConfig = {
240
245
  env?: Record<string, string>;
241
246
  enabled?: boolean;
242
247
  timeout?: number;
243
- alwaysLoad?: boolean;
244
248
  };
245
249
 
246
250
  // Remote server (HTTP/SSE)
@@ -256,16 +260,13 @@ type MCPRemoteServerConfig = {
256
260
  maxReconnectionDelay?: number;
257
261
  reconnectionDelayGrowFactor?: number;
258
262
  };
259
- alwaysLoad?: boolean;
260
263
  };
261
264
  ```
262
265
 
263
- **`alwaysLoad`** opts a server's tool surface out of the active runtime's tool-search deferral. Default (`undefined` /
264
- `false`) lets the runtime defer the server's tools behind a tool-search round-trip when the global tool surface is
265
- large; `true` registers every tool from this server with the model up-front. Useful for small, discovery-critical
266
- surfaces (≤ a few tools the model needs to find without prompting). The Claude harness honors the flag by stamping
267
- `_meta['anthropic/alwaysLoad'] = true` on each forwarded tool (equivalent to `defer_loading: false` on the Claude API).
268
- The Mastra harness eager-loads all MCP tools regardless, so the flag is a no-op there.
266
+ **Tool-exposure policy** (which tools bypass the active runtime's tool-search deferral) is configured per-agent on the
267
+ harness extension surface, not per-server here. See `MastraAgentConfig.toolSearch.alwaysActive` and
268
+ `ClaudeAgentConfig.toolSearch.alwaysActive` for the entry shape that covers "all tools from server X", "tool Y on server
269
+ X", and "tool Y from any source".
269
270
 
270
271
  **`reconnectionOptions`** tunes the HTTP MCP transport's retry / backoff behavior. Forwarded to the underlying SDK
271
272
  transport on both harnesses (Claude's `@modelcontextprotocol/sdk` `StreamableHTTPClientTransport` and Mastra's
@@ -768,6 +769,134 @@ When `requireToolApproval: true` is also set, consumer-executed tools bypass the
768
769
  `StreamOptions.requireToolApproval` JSDoc). They surface as a normal `tool-call` event without a preceding
769
770
  `tool-approval-request`. Built-in / MCP tools still gate normally.
770
771
 
772
+ ### Tool-Result Redaction
773
+
774
+ Secrets in tool output (a live `accessToken` from `sf org list --json`, an API key in `Bash` stdout, a JWT in an MCP
775
+ response) must be scrubbed **before** the result enters the model's context. Once the model has seen a value it can echo
776
+ it in a reply, route it into a later tool call (`Bash` arg, file write), or send it to provider logs — nothing
777
+ downstream (UI scrubbing, transcript redaction) can undo that.
778
+
779
+ The SDK exposes a harness-agnostic redactor type and a per-agent hooks bag (`AgentHooks`). Pass a `hooksForAgent`
780
+ callback to `createAgentManager`; the SDK invokes it once per agent install (`createAgent`, boot-time restore,
781
+ `Agent.updateAgentConfig`), threads the resolved bag through to whichever harness you're using, and the harness wires
782
+ `onToolResult` to its native seam (Claude Agent SDK `PostToolUse` hook, Mastra `processInputStep`). The same redactor
783
+ function works on both harnesses.
784
+
785
+ ```ts
786
+ import {
787
+ createAgentManager,
788
+ type AgentHooks,
789
+ type HooksForAgent,
790
+ type ToolResultRedactor,
791
+ } from '@salesforce/sfdx-agent-sdk';
792
+
793
+ // Optional: wrap your redactor so an exception substitutes a safe stub
794
+ // instead of propagating. The SDK does NOT do this for you — see
795
+ // "Throw policy" below.
796
+ const REDACTION_FAILURE_STUB = '[redaction failed — original withheld]';
797
+
798
+ const failClosed =
799
+ (inner: ToolResultRedactor): ToolResultRedactor =>
800
+ async (input) => {
801
+ try {
802
+ return await inner(input);
803
+ } catch (err) {
804
+ auditLog.warn('redaction-failed', { toolName: input.toolName, toolCallId: input.toolCallId, err });
805
+ return { output: REDACTION_FAILURE_STUB };
806
+ }
807
+ };
808
+
809
+ const baseRedactor: ToolResultRedactor = ({ toolName, output }) => {
810
+ // Bash needs its native shape preserved (see "Bash gotcha" below).
811
+ if (toolName === 'Bash') {
812
+ const bash = output as { stdout: string; stderr: string; interrupted: boolean };
813
+ return { output: { ...bash, stdout: scrub(bash.stdout), stderr: scrub(bash.stderr) } };
814
+ }
815
+ // Pass-through for non-secret-bearing tools — return undefined.
816
+ if (!mayContainSecrets(toolName)) return;
817
+ return { output: scrubDeep(output) };
818
+ };
819
+
820
+ const hooksForAgent: HooksForAgent = (agentId, config): AgentHooks => ({
821
+ onToolResult: failClosed(baseRedactor),
822
+ });
823
+
824
+ const manager = await createAgentManager(storage, factory, { hooksForAgent });
825
+ ```
826
+
827
+ #### `ToolResultRedactor`
828
+
829
+ Sync-or-async callback invoked once per tool result before the model sees it. Returning `{ output }` replaces the value;
830
+ returning `undefined` passes through unchanged.
831
+
832
+ ```ts
833
+ type ToolResultRedactor = (
834
+ input: ToolResultRedactionInput,
835
+ ) => ToolResultRedactionResult | Promise<ToolResultRedactionResult>;
836
+
837
+ type ToolResultRedactionInput = {
838
+ agentId: string;
839
+ threadId: string;
840
+ toolCallId: string;
841
+ toolName: string;
842
+ serverName?: string; // Originating MCP server when applicable.
843
+ output: unknown; // Raw upstream output, unmodified.
844
+ isError: boolean;
845
+ };
846
+
847
+ type ToolResultRedactionResult = { output: unknown } | undefined;
848
+ ```
849
+
850
+ The redactor fires for every tool result type:
851
+
852
+ - **Built-in tools** (`Bash`, `Read`, `Edit`, `Glob`, `Grep`, …) on Claude.
853
+ - **MCP tools** on either harness (with `serverName` populated from the agent's MCP catalog).
854
+ - **Consumer-executed tools** declared via `AgentConfig.tools` on either harness.
855
+
856
+ #### `AgentHooks` and `HooksForAgent`
857
+
858
+ `AgentHooks` is a forward-compatible bag — today it carries `onToolResult`; future hooks (`onToolCall`, `onStep`, …)
859
+ will land on the same shape without churning factory configs. The SDK and harnesses treat unknown fields as opaque, so
860
+ adding a new hook is non-breaking.
861
+
862
+ ```ts
863
+ type AgentHooks = { onToolResult?: ToolResultRedactor };
864
+ type HooksForAgent = (agentId: string, config: AgentConfig) => AgentHooks;
865
+ ```
866
+
867
+ `hooksForAgent` is sync — the SDK does not await it. Consumers needing async setup (e.g. a remote feature flag
868
+ controlling who gets redaction) pre-resolve before calling `createAgentManager`. The callback receives the agent's id
869
+ and the persisted `AgentConfig`, so per-agent variation can branch on either.
870
+
871
+ #### Audit / preserving the original
872
+
873
+ The redactor receives the unmodified `output` at the call site. Consumers needing an audit trail of the original value
874
+ log it themselves before returning the redaction. The SDK does not put the original on its telemetry bus or persist it
875
+ anywhere — that would defeat the point.
876
+
877
+ #### Throw policy
878
+
879
+ The SDK does NOT own fail-closed semantics. If your redactor throws, the harness propagates: Claude routes through the
880
+ Claude Agent SDK's `PostToolUse` hook-error path (which synthesizes `tool_result(is_error=true)` so the failure is
881
+ observable); Mastra propagates from `processInputStep` and surfaces as an `error` ChatEvent on the consumer's
882
+ eventStream. Either way, the original output never reaches the model. Wrap your redactor in `try`/`catch` (see the
883
+ `failClosed` helper above) when you want a richer fail-closed substitute.
884
+
885
+ #### Bash gotcha (Claude only)
886
+
887
+ Claude's built-in `Bash` tool expects responses to keep the `{ stdout, stderr, interrupted }` shape. A bare-string
888
+ return is rejected by the Claude Agent SDK and the original value leaks. The harness does NOT validate this — the
889
+ redactor knows what tool it is redacting.
890
+
891
+ For MCP tools, the replacement must be a valid `CallToolResult` shape (`{ content: [...], isError? }`). For
892
+ consumer-executed tools, any shape the consumer accepts is fine.
893
+
894
+ #### Composition with consumer-supplied hooks (Claude only)
895
+
896
+ If you also register a `PostToolUse` hook via `ClaudeQueryDefaults.hooks.PostToolUse`, both your hook and the harness's
897
+ redaction hook fire — the harness hook is appended **last** in `options.hooks.PostToolUse`, so its `updatedToolOutput`
898
+ is what actually replaces the value the model sees.
899
+
771
900
  ### Connectivity Resolution
772
901
 
773
902
  #### `ResolvedConnectivity`
@@ -924,14 +1053,18 @@ This package publishes two ESM entry points:
924
1053
  > see the subpath. Modern bundlers (Vite, esbuild, Webpack 5+, tsup, Rollup with `@rollup/plugin-node-resolve` v15+)
925
1054
  > resolve it natively. This is a harness-author concern only; consumer applications never touch the subpath.
926
1055
 
927
- | Export | Surface | Role |
928
- | ----------------------------- | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
929
- | `HarnessFactory<H>` | Type only on bare; value+type on `/harness` | Construct a harness of type `H` bound to a storage root. Declares `harnessId` and `protocolVersion`. Default `H = AgentHarness`. |
930
- | `AgentHarness` | Type only on bare; type on `/harness` | Runtime contract: agent / thread / stream / tool / message lifecycle. Declares its own `harnessId` and `protocolVersion`. |
931
- | `SUPPORTED_PROTOCOL_VERSIONS` | `/harness` only | Readonly list of harness protocol versions this SDK accepts. `createAgentManager` checks both the factory and the constructed harness. |
932
- | `HarnessBusOwner` | `/harness` only | Composition helper owning telemetry + log buses with `dispose()` semantics. Reuse it instead of reimplementing bus plumbing. |
933
- | `lowerStreamInput` | `/harness` only | Validates a `MessagePart[]` and lowers each input part to your runtime's content-block shape. Use it in `stream()` so multimodal caps and `MULTIMODAL_NOT_SUPPORTED` / `INVALID_MESSAGE_CONTENT` semantics match every other harness. |
934
- | `GenSink<T>` | `/harness` only | Buffered async-generator wrapper for routing `ChatEvent`s to a consumer's `ChatStreamResult.eventStream`. Single-iteration: calling `generator()` twice throws — sinks have one waiter slot and one buffer, two iterators race on both. |
1056
+ | Export | Surface | Role |
1057
+ | ----------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1058
+ | `HarnessFactory<H>` | Type only on bare; value+type on `/harness` | Construct a harness of type `H` bound to a storage root. Declares `harnessId` and `protocolVersion`. Default `H = AgentHarness`. |
1059
+ | `AgentHarness` | Type only on bare; type on `/harness` | Runtime contract: agent / thread / stream / tool / message lifecycle. Declares its own `harnessId` and `protocolVersion`. |
1060
+ | `SUPPORTED_PROTOCOL_VERSIONS` | `/harness` only | Readonly list of harness protocol versions this SDK accepts. `createAgentManager` checks both the factory and the constructed harness. |
1061
+ | `HarnessBusOwner` | `/harness` only | Composition helper owning telemetry + log buses with `dispose()` semantics. Reuse it instead of reimplementing bus plumbing. |
1062
+ | `lowerStreamInput` | `/harness` only | Validates a `MessagePart[]` and lowers each input part to your runtime's content-block shape. Use it in `stream()` so multimodal caps and `MULTIMODAL_NOT_SUPPORTED` / `INVALID_MESSAGE_CONTENT` semantics match every other harness. |
1063
+ | `GenSink<T>` | `/harness` only | Buffered async-generator wrapper for routing `ChatEvent`s to a consumer's `ChatStreamResult.eventStream`. Single-iteration: calling `generator()` twice throws — sinks have one waiter slot and one buffer, two iterators race on both. |
1064
+ | `mcpServerConfigEqual` | Bare specifier and `/harness` | Structural deep-equality predicate over `MCPServerConfig`. Use inside `updateAgent` to decide which servers to preserve vs. cycle. Treats `enabled: undefined` and `enabled: true` as equal; compares URLs via `String(url)` (so `URL` instances and strings round-trip); `headers` and `env` are key-order-insensitive; `reconnectionOptions` compares field-wise. |
1065
+ | `AlwaysActiveEntry` | `/harness` only | Entry shape consumed by per-harness `toolSearch.alwaysActive` extension fields. Three matching patterns: `{ serverName }` (server-wide), `{ serverName, toolName }` (precise), `{ toolName }` (cross-source). At least one of `serverName` / `toolName` must be present. |
1066
+ | `matchesAlwaysActive` | `/harness` only | Predicate `(entries, serverName, toolName) → boolean` consulted per-tool when stamping always-load metadata or partitioning a tool-search pool. Use this instead of pattern-matching entries by hand so harness behavior stays uniform. |
1067
+ | `validateAlwaysActiveEntry` | `/harness` only | Throws on a malformed entry (`{}`, both fields empty). Call once per entry at the harness boundary so a typo fails loud at config time rather than silently dropping the entry on every `stream()`. |
935
1068
 
936
1069
  Minimal skeleton:
937
1070
 
@@ -3,6 +3,7 @@ import { type AgentHarness, type ConfigOf } from './harness/agent-harness.js';
3
3
  import type { HarnessFactory } from './harness/harness-factory.js';
4
4
  import { type AgentConfig } from './harness/harness-config.js';
5
5
  import { type Agent } from './agent.js';
6
+ import type { HooksForAgent } from './types/redaction.js';
6
7
  import type { TelemetryEventCallback } from './types/telemetry-events.js';
7
8
  import { type AgentConnectivityResolver } from './agent-connectivity-resolver.js';
8
9
  /**
@@ -124,6 +125,7 @@ export declare class DefaultAgentManager<H extends AgentHarness = AgentHarness>
124
125
  private readonly harness;
125
126
  private readonly agentIdGenerator;
126
127
  private readonly agentConnectivityResolver;
128
+ private readonly hooksForAgent;
127
129
  private readonly clock;
128
130
  private readonly identityStore;
129
131
  private readonly agents;
@@ -144,7 +146,7 @@ export declare class DefaultAgentManager<H extends AgentHarness = AgentHarness>
144
146
  * is private, so this is the only way to obtain an instance, but
145
147
  * consumers should always go through {@link createAgentManager}.
146
148
  */
147
- static __build<H extends AgentHarness>(harness: H, agentConnectivityResolver: AgentConnectivityResolver, storageRootFolder: string, agentIdGenerator: UniqueIDGenerator, clock: Clock, logBus: LogBus): Promise<DefaultAgentManager<H>>;
149
+ static __build<H extends AgentHarness>(harness: H, agentConnectivityResolver: AgentConnectivityResolver, hooksForAgent: HooksForAgent | undefined, storageRootFolder: string, agentIdGenerator: UniqueIDGenerator, clock: Clock, logBus: LogBus): Promise<DefaultAgentManager<H>>;
148
150
  private init;
149
151
  shutdown(): Promise<void>;
150
152
  createAgent(projectRoot: string, config?: ConfigOf<H> & {
@@ -189,14 +191,25 @@ export declare class DefaultAgentManager<H extends AgentHarness = AgentHarness>
189
191
  * function returns; failures are queryable via
190
192
  * {@link AgentManager.getRestoreFailures}.
191
193
  *
192
- * The optional `connectivityResolver` overrides the default
193
- * `DefaultAgentConnectivityResolver` used by e2e tests and custom-auth
194
- * deployments where the SDK should not run sf-CLI-based org resolution.
195
- * Production callers leave it unset.
194
+ * The optional third-positional `options` bag carries the SDK's per-manager
195
+ * opt-ins. Production callers typically leave it unset:
196
+ *
197
+ * - `connectivityResolver` overrides the default
198
+ * `DefaultAgentConnectivityResolver`; used by e2e tests and custom-auth
199
+ * deployments where the SDK should not run sf-CLI-based org resolution.
200
+ * - `hooksForAgent` — sync callback resolving a per-agent
201
+ * {@link AgentHooks} bag (today carries `onToolResult`); invoked once per
202
+ * `createAgent`, boot-time restore, and `Agent.updateAgentConfig`. The
203
+ * resolved bag threads through to `AgentHarness.createAgent`'s
204
+ * `options.hooks` and reaches the harness's native seam (Claude
205
+ * `PostToolUse`, Mastra `processInputStep`).
196
206
  *
197
207
  * @throws {AgentSDKError} `INCOMPATIBLE_HARNESS` when either the factory or
198
208
  * the constructed harness reports a `protocolVersion` outside
199
209
  * {@link SUPPORTED_PROTOCOL_VERSIONS}, or when the harness's reported
200
210
  * version disagrees with the factory's.
201
211
  */
202
- export declare function createAgentManager<H extends AgentHarness = AgentHarness>(storageRootFolder: string, harnessFactory: HarnessFactory<H>, connectivityResolver?: AgentConnectivityResolver): Promise<AgentManager<H>>;
212
+ export declare function createAgentManager<H extends AgentHarness = AgentHarness>(storageRootFolder: string, harnessFactory: HarnessFactory<H>, options?: {
213
+ connectivityResolver?: AgentConnectivityResolver;
214
+ hooksForAgent?: HooksForAgent;
215
+ }): Promise<AgentManager<H>>;
@@ -25,6 +25,7 @@ export class DefaultAgentManager {
25
25
  harness;
26
26
  agentIdGenerator;
27
27
  agentConnectivityResolver;
28
+ hooksForAgent;
28
29
  clock;
29
30
  identityStore;
30
31
  agents = new Map();
@@ -34,9 +35,10 @@ export class DefaultAgentManager {
34
35
  router;
35
36
  unroutedUnsubs;
36
37
  disposed = false;
37
- constructor(harness, agentConnectivityResolver, identityStore, agentIdGenerator, clock, logBus) {
38
+ constructor(harness, agentConnectivityResolver, hooksForAgent, identityStore, agentIdGenerator, clock, logBus) {
38
39
  this.harness = harness;
39
40
  this.agentConnectivityResolver = agentConnectivityResolver;
41
+ this.hooksForAgent = hooksForAgent;
40
42
  this.identityStore = identityStore;
41
43
  this.agentIdGenerator = agentIdGenerator;
42
44
  this.clock = clock;
@@ -57,9 +59,9 @@ export class DefaultAgentManager {
57
59
  * is private, so this is the only way to obtain an instance, but
58
60
  * consumers should always go through {@link createAgentManager}.
59
61
  */
60
- static async __build(harness, agentConnectivityResolver, storageRootFolder, agentIdGenerator, clock, logBus) {
62
+ static async __build(harness, agentConnectivityResolver, hooksForAgent, storageRootFolder, agentIdGenerator, clock, logBus) {
61
63
  const identityStore = new AgentIdentityStore(storageRootFolder, harness.harnessId, logBus);
62
- const manager = new DefaultAgentManager(harness, agentConnectivityResolver, identityStore, agentIdGenerator, clock, logBus);
64
+ const manager = new DefaultAgentManager(harness, agentConnectivityResolver, hooksForAgent, identityStore, agentIdGenerator, clock, logBus);
63
65
  await manager.init();
64
66
  return manager;
65
67
  }
@@ -155,9 +157,10 @@ export class DefaultAgentManager {
155
157
  throw new Error(`projectRoot is not a directory: "${projectRoot}"`);
156
158
  }
157
159
  const runtime = await this.agentConnectivityResolver.resolve(projectRoot, config);
158
- await this.harness.createAgent(agentId, projectRoot, runtime.llmGatewayClient, toHarnessConfig(config, runtime.orgJwt), options.abortSignal !== undefined ? { abortSignal: options.abortSignal } : undefined);
160
+ const hooks = this.hooksForAgent?.(agentId, config) ?? {};
161
+ await this.harness.createAgent(agentId, projectRoot, runtime.llmGatewayClient, toHarnessConfig(config, runtime.orgJwt), { ...(options.abortSignal !== undefined ? { abortSignal: options.abortSignal } : {}), hooks });
159
162
  const agentSlice = this.router.registerAgent(agentId);
160
- const agent = new DefaultAgent(this.harness, agentId, projectRoot, config, runtime.llmGatewayClient, runtime.orgConnection, runtime.orgJwt, this.agentConnectivityResolver, this.identityStore, this.router, agentSlice, { telemetry: this.telemetryBus, log: this.logBus }, this.clock, this.agentIdGenerator);
163
+ const agent = new DefaultAgent(this.harness, agentId, projectRoot, config, runtime.llmGatewayClient, runtime.orgConnection, runtime.orgJwt, this.agentConnectivityResolver, this.hooksForAgent, this.identityStore, this.router, agentSlice, { telemetry: this.telemetryBus, log: this.logBus }, this.clock, this.agentIdGenerator);
161
164
  this.agents.set(agentId, agent);
162
165
  this.telemetryBus.emit({
163
166
  type: 'agent-created',
@@ -262,17 +265,25 @@ export class DefaultAgentManager {
262
265
  * function returns; failures are queryable via
263
266
  * {@link AgentManager.getRestoreFailures}.
264
267
  *
265
- * The optional `connectivityResolver` overrides the default
266
- * `DefaultAgentConnectivityResolver` used by e2e tests and custom-auth
267
- * deployments where the SDK should not run sf-CLI-based org resolution.
268
- * Production callers leave it unset.
268
+ * The optional third-positional `options` bag carries the SDK's per-manager
269
+ * opt-ins. Production callers typically leave it unset:
270
+ *
271
+ * - `connectivityResolver` overrides the default
272
+ * `DefaultAgentConnectivityResolver`; used by e2e tests and custom-auth
273
+ * deployments where the SDK should not run sf-CLI-based org resolution.
274
+ * - `hooksForAgent` — sync callback resolving a per-agent
275
+ * {@link AgentHooks} bag (today carries `onToolResult`); invoked once per
276
+ * `createAgent`, boot-time restore, and `Agent.updateAgentConfig`. The
277
+ * resolved bag threads through to `AgentHarness.createAgent`'s
278
+ * `options.hooks` and reaches the harness's native seam (Claude
279
+ * `PostToolUse`, Mastra `processInputStep`).
269
280
  *
270
281
  * @throws {AgentSDKError} `INCOMPATIBLE_HARNESS` when either the factory or
271
282
  * the constructed harness reports a `protocolVersion` outside
272
283
  * {@link SUPPORTED_PROTOCOL_VERSIONS}, or when the harness's reported
273
284
  * version disagrees with the factory's.
274
285
  */
275
- export async function createAgentManager(storageRootFolder, harnessFactory, connectivityResolver) {
286
+ export async function createAgentManager(storageRootFolder, harnessFactory, options) {
276
287
  let stats;
277
288
  try {
278
289
  stats = await stat(storageRootFolder);
@@ -306,8 +317,8 @@ export async function createAgentManager(storageRootFolder, harnessFactory, conn
306
317
  `advertised version ${factoryVersion} (SDK supports: ${SUPPORTED_PROTOCOL_VERSIONS.join(', ')}). ` +
307
318
  `Update the SDK or harness package.`, AgentSDKErrorType.INCOMPATIBLE_HARNESS);
308
319
  }
309
- const agentConnectivityResolver = connectivityResolver ?? new DefaultAgentConnectivityResolver();
310
- return DefaultAgentManager.__build(harness, agentConnectivityResolver, storageRootFolder, new UUIDGenerator(), new RealClock(), new LogBus());
320
+ const agentConnectivityResolver = options?.connectivityResolver ?? new DefaultAgentConnectivityResolver();
321
+ return DefaultAgentManager.__build(harness, agentConnectivityResolver, options?.hooksForAgent, storageRootFolder, new UUIDGenerator(), new RealClock(), new LogBus());
311
322
  }
312
323
  function isSupportedProtocolVersion(version) {
313
324
  return (typeof version === 'number' &&
package/dist/agent.d.ts CHANGED
@@ -7,6 +7,7 @@ import { type JSONWebToken, type LLMGatewayClient } from '@salesforce/llm-gatewa
7
7
  import { type AgentConnectivityResolver } from './agent-connectivity-resolver.js';
8
8
  import type { AgentIdentityStore } from './internal/agent-identity-store.js';
9
9
  import type { TelemetryRouter, TelemetrySlice } from './internal/telemetry-router.js';
10
+ import type { HooksForAgent } from './types/redaction.js';
10
11
  import type { TelemetryBus, TelemetryEventCallback } from './types/telemetry-events.js';
11
12
  /**
12
13
  * Parent bus pair wired at construction time so an agent's events bubble upward into the manager's buses.
@@ -44,8 +45,13 @@ export interface Agent {
44
45
  /**
45
46
  * Request a reconnect of one MCP server on this agent without recycling
46
47
  * any other server, custom tool, instruction, or skill. Useful for
47
- * recovering a single failed MCP server without paying the full
48
- * `updateAgentConfig` destroy/recreate cost.
48
+ * recovering a single failed MCP server after a transport-level error
49
+ * (e.g. JWT-rotation timing on stdio servers, transient EOF on remote
50
+ * transports). For the diff-driven case — `Agent.updateAgentConfig`
51
+ * applying a new `MCPConfiguration` — the harness already preserves any
52
+ * server whose config is structurally unchanged and cycles only the
53
+ * changed/added/removed servers; an explicit `reconnectMcpServer` call
54
+ * is **not** required there.
49
55
  *
50
56
  * Throws if `serverName` is not configured on this agent or if the named
51
57
  * server is disabled (`enabled: false`).
@@ -132,6 +138,7 @@ export declare class DefaultAgent implements Agent {
132
138
  private orgConnection;
133
139
  private orgJwt;
134
140
  private readonly agentConnectivityResolver;
141
+ private readonly hooksForAgent;
135
142
  private readonly identityStore;
136
143
  private readonly sessions;
137
144
  private readonly sessionSliceUnregisters;
@@ -152,13 +159,17 @@ export declare class DefaultAgent implements Agent {
152
159
  * @param orgConnection - Authenticated org connection carrying identity and env inference.
153
160
  * @param orgJwt - Self-refreshing JWT for the resolved org (used for MCP auth injection).
154
161
  * @param agentConnectivityResolver - Used to re-resolve org connectivity when the org or model changes.
162
+ * @param hooksForAgent - Per-agent hooks resolver supplied by the SDK consumer at `createAgentManager` time. The
163
+ * agent re-invokes it on every `updateAgentConfig` (with `nextConfig`, and again with `previousConfig` on the
164
+ * rollback path) so the bag the harness sees always reflects the current persisted config. `undefined` when
165
+ * the consumer didn't pass a `hooksForAgent`.
155
166
  * @param identityStore - SDK-owned persistence for the `{ agentId, projectRoot, AgentConfig }` triple. The agent
156
167
  * calls `write()` on a successful `updateAgentConfig` so disk state and in-memory state stay in lockstep.
157
168
  * @param router - Telemetry router used to obtain session slices when sessions are created.
158
169
  * @param inbound - Router slice delivering harness events routed to this agent (non-session-scoped).
159
170
  * @param parent - Manager's bus pair; this agent forwards its events upward into them.
160
171
  */
161
- constructor(harness: AgentHarness, agentId: string, projectRoot: string, config: AgentConfig, llmGatewayClient: LLMGatewayClient, orgConnection: OrgConnection, orgJwt: JSONWebToken, agentConnectivityResolver: AgentConnectivityResolver, identityStore: AgentIdentityStore, router: TelemetryRouter, inbound: TelemetrySlice, parent: AgentParentBuses, clock?: Clock, idGenerator?: UniqueIDGenerator);
172
+ constructor(harness: AgentHarness, agentId: string, projectRoot: string, config: AgentConfig, llmGatewayClient: LLMGatewayClient, orgConnection: OrgConnection, orgJwt: JSONWebToken, agentConnectivityResolver: AgentConnectivityResolver, hooksForAgent: HooksForAgent | undefined, identityStore: AgentIdentityStore, router: TelemetryRouter, inbound: TelemetrySlice, parent: AgentParentBuses, clock?: Clock, idGenerator?: UniqueIDGenerator);
162
173
  /**
163
174
  * @requirements
164
175
  * - MUST return the agent's ID.
@@ -178,11 +189,17 @@ export declare class DefaultAgent implements Agent {
178
189
  * @requirements
179
190
  * - MUST merge the provided `config` with the internal `config` object.
180
191
  * - MUST guarantee that the `agentId` remains unchanged during the merge.
181
- * - MUST destroy the existing agent in the harness by delegating to `this.harness.destroyAgent(this.getId())`.
182
- * - MUST recreate the agent in the harness with the newly merged configuration by delegating to `this.harness.createAgent(...)`.
183
- * - MUST persist the merged config via `this.identityStore.write(...)` after the harness recreate succeeds and
184
- * before the in-memory swaps, so a write failure rolls back through the same catch path as a recreate failure.
185
- * - MUST preserve the previous in-memory config state if recreation or persistence fails.
192
+ * - MUST apply the merged config to the harness via `this.harness.updateAgent(...)` — a single primitive
193
+ * that preserves any MCP client whose `MCPServerConfig` is structurally equal to the currently-applied one.
194
+ * The destroy+recreate shape this method used pre-#541 closed every MCP client on a model-only or
195
+ * instructions-only or org-connect-only change; the new shape preserves them and only cycles servers
196
+ * that actually changed.
197
+ * - MUST persist the merged config via `this.identityStore.write(...)` after `harness.updateAgent` succeeds
198
+ * and before the in-memory swaps, so a write failure rolls back through the same catch path as an
199
+ * `updateAgent` failure.
200
+ * - MUST preserve the previous in-memory config state if `updateAgent` or persistence fails. Rollback
201
+ * uses the same `harness.updateAgent` primitive against the previous config — the harness re-diffs
202
+ * against its current (possibly partially-updated) state and reverts only the actual deltas.
186
203
  */
187
204
  updateAgentConfig(config?: AgentConfig, options?: {
188
205
  abortSignal?: AbortSignal;
package/dist/agent.js CHANGED
@@ -21,6 +21,7 @@ export class DefaultAgent {
21
21
  orgConnection;
22
22
  orgJwt;
23
23
  agentConnectivityResolver;
24
+ hooksForAgent;
24
25
  identityStore;
25
26
  sessions = new Map();
26
27
  sessionSliceUnregisters = new Map();
@@ -41,13 +42,17 @@ export class DefaultAgent {
41
42
  * @param orgConnection - Authenticated org connection carrying identity and env inference.
42
43
  * @param orgJwt - Self-refreshing JWT for the resolved org (used for MCP auth injection).
43
44
  * @param agentConnectivityResolver - Used to re-resolve org connectivity when the org or model changes.
45
+ * @param hooksForAgent - Per-agent hooks resolver supplied by the SDK consumer at `createAgentManager` time. The
46
+ * agent re-invokes it on every `updateAgentConfig` (with `nextConfig`, and again with `previousConfig` on the
47
+ * rollback path) so the bag the harness sees always reflects the current persisted config. `undefined` when
48
+ * the consumer didn't pass a `hooksForAgent`.
44
49
  * @param identityStore - SDK-owned persistence for the `{ agentId, projectRoot, AgentConfig }` triple. The agent
45
50
  * calls `write()` on a successful `updateAgentConfig` so disk state and in-memory state stay in lockstep.
46
51
  * @param router - Telemetry router used to obtain session slices when sessions are created.
47
52
  * @param inbound - Router slice delivering harness events routed to this agent (non-session-scoped).
48
53
  * @param parent - Manager's bus pair; this agent forwards its events upward into them.
49
54
  */
50
- constructor(harness, agentId, projectRoot, config, llmGatewayClient, orgConnection, orgJwt, agentConnectivityResolver, identityStore, router, inbound, parent, clock = new RealClock(), idGenerator = new UUIDGenerator()) {
55
+ constructor(harness, agentId, projectRoot, config, llmGatewayClient, orgConnection, orgJwt, agentConnectivityResolver, hooksForAgent, identityStore, router, inbound, parent, clock = new RealClock(), idGenerator = new UUIDGenerator()) {
51
56
  this.harness = harness;
52
57
  this.agentId = agentId;
53
58
  this.projectRoot = projectRoot;
@@ -56,6 +61,7 @@ export class DefaultAgent {
56
61
  this.orgConnection = orgConnection;
57
62
  this.orgJwt = orgJwt;
58
63
  this.agentConnectivityResolver = agentConnectivityResolver;
64
+ this.hooksForAgent = hooksForAgent;
59
65
  this.identityStore = identityStore;
60
66
  this.router = router;
61
67
  this.clock = clock;
@@ -100,11 +106,17 @@ export class DefaultAgent {
100
106
  * @requirements
101
107
  * - MUST merge the provided `config` with the internal `config` object.
102
108
  * - MUST guarantee that the `agentId` remains unchanged during the merge.
103
- * - MUST destroy the existing agent in the harness by delegating to `this.harness.destroyAgent(this.getId())`.
104
- * - MUST recreate the agent in the harness with the newly merged configuration by delegating to `this.harness.createAgent(...)`.
105
- * - MUST persist the merged config via `this.identityStore.write(...)` after the harness recreate succeeds and
106
- * before the in-memory swaps, so a write failure rolls back through the same catch path as a recreate failure.
107
- * - MUST preserve the previous in-memory config state if recreation or persistence fails.
109
+ * - MUST apply the merged config to the harness via `this.harness.updateAgent(...)` — a single primitive
110
+ * that preserves any MCP client whose `MCPServerConfig` is structurally equal to the currently-applied one.
111
+ * The destroy+recreate shape this method used pre-#541 closed every MCP client on a model-only or
112
+ * instructions-only or org-connect-only change; the new shape preserves them and only cycles servers
113
+ * that actually changed.
114
+ * - MUST persist the merged config via `this.identityStore.write(...)` after `harness.updateAgent` succeeds
115
+ * and before the in-memory swaps, so a write failure rolls back through the same catch path as an
116
+ * `updateAgent` failure.
117
+ * - MUST preserve the previous in-memory config state if `updateAgent` or persistence fails. Rollback
118
+ * uses the same `harness.updateAgent` primitive against the previous config — the harness re-diffs
119
+ * against its current (possibly partially-updated) state and reverts only the actual deltas.
108
120
  */
109
121
  async updateAgentConfig(config = {}, options) {
110
122
  this.assertNotDisposed();
@@ -129,13 +141,14 @@ export class DefaultAgent {
129
141
  // (If modelId is omitted, the resolver pinned the default at creation time.)
130
142
  nextClient.setModel(nextModel);
131
143
  }
132
- await this.harness.destroyAgent(this.agentId);
133
- let nextConfigRegistered = false;
134
144
  try {
135
- await this.harness.createAgent(this.agentId, this.projectRoot, nextClient, toHarnessConfig(nextConfig, nextOrgJwt), options);
136
- nextConfigRegistered = true;
145
+ const nextHooks = this.hooksForAgent?.(this.agentId, nextConfig) ?? {};
146
+ await this.harness.updateAgent(this.agentId, nextClient, toHarnessConfig(nextConfig, nextOrgJwt), {
147
+ ...(options?.abortSignal !== undefined ? { abortSignal: options.abortSignal } : {}),
148
+ hooks: nextHooks,
149
+ });
137
150
  // Persist before the in-memory swaps so a write failure flows through the same
138
- // catch block as a recreate failure: the rollback restores the harness with
151
+ // catch block as an updateAgent failure: the rollback re-runs updateAgent against
139
152
  // previousConfig and disk state remains the pre-update record.
140
153
  await this.identityStore.write(this.agentId, this.projectRoot, nextConfig);
141
154
  this.config = nextConfig;
@@ -158,15 +171,11 @@ export class DefaultAgent {
158
171
  if (nextClient === previousClient) {
159
172
  previousClient.setModel(previousModel);
160
173
  }
161
- // Clear nextConfig registration only when the harness recreate
162
- // actually succeeded (identityStore.write-failure path) — the
163
- // harness throws on unknown id, so calling destroyAgent on the
164
- // harness-recreate-failure path would short-circuit the rollback
165
- // createAgent below.
166
- if (nextConfigRegistered) {
167
- await this.harness.destroyAgent(this.agentId);
168
- }
169
- await this.harness.createAgent(this.agentId, this.projectRoot, previousClient, toHarnessConfig(previousConfig, previousOrgJwt));
174
+ // Re-apply the previous config through the same primitive. The harness re-diffs
175
+ // against its current state — if updateAgent partially applied (e.g. some MCP
176
+ // servers were already cycled), reverting via updateAgent restores them too.
177
+ const previousHooks = this.hooksForAgent?.(this.agentId, previousConfig) ?? {};
178
+ await this.harness.updateAgent(this.agentId, previousClient, toHarnessConfig(previousConfig, previousOrgJwt), { hooks: previousHooks });
170
179
  }
171
180
  catch {
172
181
  // Ignore restoration errors; rethrow the original failure.