@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.
@@ -4,6 +4,7 @@ import type { ChatStreamResult } from '../types/events.js';
4
4
  import type { Message, MessagePart } from '../types/messages.js';
5
5
  import type { TelemetryEventCallback } from '../types/telemetry-events.js';
6
6
  import type { ToolResultInfo } from '../types/tools.js';
7
+ import type { AgentHooks } from '../types/redaction.js';
7
8
  import type { AgentConfig, HarnessAgentConfig, StreamOptions } from './harness-config.js';
8
9
  import type { LLMGatewayClient } from '@salesforce/llm-gateway-sdk';
9
10
  export declare const SUPPORTED_PROTOCOL_VERSIONS: readonly [1];
@@ -105,10 +106,24 @@ export interface AgentHarness {
105
106
  * @param projectRoot - Project folder the agent is allowed to manipulate files from.
106
107
  * @param llmGatewayClient - Pre-configured LLM gateway client for this agent.
107
108
  * @param config - Engine-facing agent configuration (org resolution omitted).
108
- * @param options - Optional execution options, including abort signals.
109
+ * @param options - Optional execution options.
110
+ * - `abortSignal` — caller-side cancellation; harnesses thread it
111
+ * through long-running install work (rules load, MCP discovery, …).
112
+ * - `hooks` — per-agent {@link AgentHooks} bag, resolved by the SDK
113
+ * from `createAgentManager`'s `hooksForAgent` callback. The bag is
114
+ * opaque to the SDK; harnesses MUST store it on per-agent state and
115
+ * route each recognized hook to their native seam (Claude
116
+ * `PostToolUse`, Mastra `processInputStep`). Harnesses MUST IGNORE
117
+ * hook fields they do not recognize (forward-compat). Harnesses
118
+ * MUST NOT swallow hook throws — exceptions MUST propagate on the
119
+ * native error path so the original tool output never leaks to the
120
+ * model. The SDK does not own fail-closed semantics; consumers
121
+ * wrap their hook bodies in `try`/`catch` themselves when they
122
+ * want a richer fail-closed substitute.
109
123
  */
110
124
  createAgent(agentId: string, projectRoot: string, llmGatewayClient: LLMGatewayClient, config?: HarnessAgentConfig, options?: {
111
125
  abortSignal?: AbortSignal;
126
+ hooks?: AgentHooks;
112
127
  }): Promise<void>;
113
128
  /**
114
129
  * Destroy an agent and release its resources (MCP connections, workspace, memory).
@@ -122,6 +137,81 @@ export interface AgentHarness {
122
137
  * @returns `true` after a real removal.
123
138
  */
124
139
  destroyAgent(agentId: string): Promise<boolean>;
140
+ /**
141
+ * Apply a new configuration to a registered agent without recycling MCP
142
+ * clients whose `MCPServerConfig` is structurally equal to the
143
+ * currently-applied one.
144
+ *
145
+ * This is the load-bearing primitive behind `Agent.updateAgentConfig`'s
146
+ * "don't blow up live MCP clients on a model-only / instructions-only /
147
+ * org-connect change" contract. Pre-#541 the SDK called
148
+ * `destroyAgent` + `createAgent` here, which closed every MCP client and
149
+ * forced the model to wait through the discovery wave again. With
150
+ * `updateAgent` the SDK calls one method and the harness preserves
151
+ * unchanged servers in place.
152
+ *
153
+ * Implementors MUST:
154
+ *
155
+ * - throw `AgentSDKError(AGENT_NOT_FOUND)` on unknown `agentId`,
156
+ * matching the rest of the cross-harness contract.
157
+ * - preserve the in-memory MCP client (and its discovered tool catalog)
158
+ * for any server name whose config is `mcpServerConfigEqual` to the
159
+ * currently-applied one. No transport teardown, no `tools/list`
160
+ * re-run, no per-server discovery telemetry.
161
+ * - cycle (disconnect-then-reconnect) any server whose config differs.
162
+ * - disconnect any server present in the currently-applied config but
163
+ * absent from the next config, removing it from
164
+ * `getMcpServerInfo()`'s output.
165
+ * - connect any server present in the next config but absent from the
166
+ * currently-applied one, running discovery the same way `createAgent`
167
+ * would (background, non-blocking; failures land on
168
+ * `McpServerState.error`, not as a thrown rejection).
169
+ * - apply non-MCP changes — `instructions`, `model`, `tools`, `skills`,
170
+ * `rules`, harness-specific extra-config fields surviving via
171
+ * `toHarnessConfig`'s spread — atomically with the MCP diff. After
172
+ * this resolves, the agent's effective config is the one passed in.
173
+ * - be idempotent on a no-op call at the MCP layer: when the next
174
+ * config's `mcpServers` is deep-equal (per `mcpServerConfigEqual`) to
175
+ * the currently-applied one, no server is cycled, no `tools/list` is
176
+ * re-run, and no per-server discovery telemetry fires. Implementors
177
+ * MAY rebuild non-MCP state (e.g. Mastra reconstructs its `Agent`
178
+ * unconditionally) — that work is local and cheap; correct no-op
179
+ * detection across `orgJwt` rotation, hook-bag closures, and
180
+ * harness-specific extra-config fields is not.
181
+ * - write per-server state incrementally so a subsequent `updateAgent`
182
+ * call (e.g. SDK rollback against `previousConfig`) sees the harness's
183
+ * current truth, not a snapshot from the start of the failed update.
184
+ *
185
+ * Implementors MUST NOT:
186
+ *
187
+ * - touch persisted thread / session state. Sessions are config-
188
+ * independent — `Agent.updateAgentConfig` does not invalidate them.
189
+ * - dispose in-flight stream coordinators. In-flight turns continue
190
+ * executing against the agent state captured at stream-start; the
191
+ * next `stream()` after this resolves uses the new state.
192
+ * - mutate `AgentConfig` or persist anything to disk. Persistence is
193
+ * the SDK's responsibility (`AgentIdentityStore.write` is gated by
194
+ * `Agent.updateAgentConfig` after this method resolves).
195
+ *
196
+ * @param agentId - ID of the agent to update.
197
+ * @param llmGatewayClient - LLM gateway client bound to the next config's org / model.
198
+ * @param config - Engine-facing agent configuration to apply.
199
+ * @param options - Optional execution options.
200
+ * - `abortSignal` — caller-side cancellation; harnesses thread it
201
+ * through long-running update work (rules load, MCP discovery, …).
202
+ * - `hooks` — per-agent {@link AgentHooks} bag, resolved by the SDK
203
+ * from `createAgentManager`'s `hooksForAgent` callback against the
204
+ * incoming `nextConfig`. Same semantics as `createAgent.options.hooks`:
205
+ * opaque to the SDK; harnesses store it on per-agent state and
206
+ * re-route each recognized hook to its native seam. The bag is
207
+ * re-resolved on every `updateAgent` so consumers can vary hooks by
208
+ * config (and so a rollback `updateAgent(previousConfig)` restores
209
+ * the prior hook bag too).
210
+ */
211
+ updateAgent(agentId: string, llmGatewayClient: LLMGatewayClient, config?: HarnessAgentConfig, options?: {
212
+ abortSignal?: AbortSignal;
213
+ hooks?: AgentHooks;
214
+ }): Promise<void>;
125
215
  /**
126
216
  * List the IDs of all currently registered agents.
127
217
  */
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Tool-exposure policy entry shape consumed by both
3
+ * {@link MastraAgentConfig.toolSearch.alwaysActive} and
4
+ * {@link ClaudeAgentConfig.toolSearch.alwaysActive}.
5
+ *
6
+ * One entry covers three matching patterns:
7
+ *
8
+ * | Entry shape | Matches |
9
+ * | --- | --- |
10
+ * | `{ serverName: 'X' }` | every tool advertised by server `X` (post-discovery expansion) |
11
+ * | `{ serverName: 'X', toolName: 'Y' }` | exactly tool `Y` on server `X` |
12
+ * | `{ toolName: 'Y' }` | any tool named `Y`, **regardless of source** — built-ins, workspace tools, consumer-declared tools, AND any MCP server's tool surface |
13
+ *
14
+ * The `{ toolName }` pattern is intentionally broad — it's the consumer's
15
+ * escape hatch for "I want this tool always-active and I don't care where it
16
+ * comes from." The `{ serverName, toolName }` form is the precise version for
17
+ * when ambiguity matters.
18
+ *
19
+ * **Validation rule:** at least one of `serverName` or `toolName` must be
20
+ * present. An empty `{}` is rejected at config time via
21
+ * {@link validateAlwaysActiveEntry} — a typo should fail loud, not silently
22
+ * match nothing (and definitely not silently match everything).
23
+ *
24
+ * Lives on the harness public surface (`@salesforce/sfdx-agent-sdk/harness`)
25
+ * because it's harness-implementation shape that both production harnesses
26
+ * share. Ready to graduate to `AgentConfig.toolSearch` on the SDK contract
27
+ * surface once a third harness exercises it — same graduation pattern PR
28
+ * #563 established for `skillSearch`.
29
+ */
30
+ export type AlwaysActiveEntry = {
31
+ serverName: string;
32
+ toolName?: string;
33
+ } | {
34
+ serverName?: undefined;
35
+ toolName: string;
36
+ };
37
+ /**
38
+ * Throws on an entry whose `serverName` AND `toolName` are both absent. Both
39
+ * harness boundaries call this on every entry before reading it so a typo
40
+ * (`{}` / `{ severName: 'X' }` with a misspelled key) fails loud at config
41
+ * time rather than silently dropping the entry — or worse, silently matching
42
+ * nothing while looking like it should match everything.
43
+ */
44
+ export declare function validateAlwaysActiveEntry(entry: AlwaysActiveEntry): void;
45
+ /**
46
+ * Returns `true` when `(serverName, toolName)` matches at least one entry in
47
+ * `entries`. Pure / deterministic; harnesses call it per-tool when building
48
+ * their tool catalogs.
49
+ *
50
+ * - `{ serverName: 'X' }` matches every tool from server X.
51
+ * - `{ serverName: 'X', toolName: 'Y' }` matches only `Y` on `X`.
52
+ * - `{ toolName: 'Y' }` matches `Y` from any source (built-ins, workspace,
53
+ * consumer-declared tools, every connected MCP server).
54
+ *
55
+ * For tools without a server (built-ins, in-process workspace), pass
56
+ * `serverName: undefined`. The `{ toolName }`-only entries match those;
57
+ * `{ serverName: 'X' }` and `{ serverName: 'X', toolName: ... }` entries do
58
+ * not.
59
+ */
60
+ export declare function matchesAlwaysActive(entries: readonly AlwaysActiveEntry[] | undefined, serverName: string | undefined, toolName: string): boolean;
@@ -0,0 +1,58 @@
1
+ /*
2
+ * Copyright 2026, Salesforce, Inc. All rights reserved.
3
+ * See LICENSE.txt for license terms.
4
+ */
5
+ /**
6
+ * Throws on an entry whose `serverName` AND `toolName` are both absent. Both
7
+ * harness boundaries call this on every entry before reading it so a typo
8
+ * (`{}` / `{ severName: 'X' }` with a misspelled key) fails loud at config
9
+ * time rather than silently dropping the entry — or worse, silently matching
10
+ * nothing while looking like it should match everything.
11
+ */
12
+ export function validateAlwaysActiveEntry(entry) {
13
+ const serverName = entry.serverName;
14
+ const toolName = entry.toolName;
15
+ if ((typeof serverName !== 'string' || serverName.length === 0) &&
16
+ (typeof toolName !== 'string' || toolName.length === 0)) {
17
+ throw new Error('AlwaysActiveEntry must declare at least one of `serverName` or `toolName` (received: ' +
18
+ JSON.stringify(entry) +
19
+ ')');
20
+ }
21
+ }
22
+ /**
23
+ * Returns `true` when `(serverName, toolName)` matches at least one entry in
24
+ * `entries`. Pure / deterministic; harnesses call it per-tool when building
25
+ * their tool catalogs.
26
+ *
27
+ * - `{ serverName: 'X' }` matches every tool from server X.
28
+ * - `{ serverName: 'X', toolName: 'Y' }` matches only `Y` on `X`.
29
+ * - `{ toolName: 'Y' }` matches `Y` from any source (built-ins, workspace,
30
+ * consumer-declared tools, every connected MCP server).
31
+ *
32
+ * For tools without a server (built-ins, in-process workspace), pass
33
+ * `serverName: undefined`. The `{ toolName }`-only entries match those;
34
+ * `{ serverName: 'X' }` and `{ serverName: 'X', toolName: ... }` entries do
35
+ * not.
36
+ */
37
+ export function matchesAlwaysActive(entries, serverName, toolName) {
38
+ if (!entries || entries.length === 0)
39
+ return false;
40
+ for (const entry of entries) {
41
+ const entryServer = entry.serverName;
42
+ const entryTool = entry.toolName;
43
+ if (entryServer !== undefined) {
44
+ if (entryServer !== serverName)
45
+ continue;
46
+ if (entryTool === undefined)
47
+ return true; // server-wide
48
+ if (entryTool === toolName)
49
+ return true; // exact server+tool
50
+ continue;
51
+ }
52
+ // entryServer === undefined ⇒ entryTool MUST be defined (validated upstream)
53
+ if (entryTool === toolName)
54
+ return true;
55
+ }
56
+ return false;
57
+ }
58
+ //# sourceMappingURL=always-active.js.map
@@ -43,7 +43,10 @@
43
43
  * not "harness vs. consumer."
44
44
  */
45
45
  export type { AgentHarness, HarnessFactory, WithAgentConfig, ConfigOf } from './index.js';
46
+ export type { AgentHooks } from '../types/redaction.js';
46
47
  export { SUPPORTED_PROTOCOL_VERSIONS } from './agent-harness.js';
48
+ export { mcpServerConfigEqual } from '../mcp-config.js';
47
49
  export { HarnessBusOwner } from './harness-bus-owner.js';
48
50
  export { lowerStreamInput, type InputMessagePart } from './stream-input.js';
49
51
  export { GenSink } from './gen-sink.js';
52
+ export { matchesAlwaysActive, validateAlwaysActiveEntry, type AlwaysActiveEntry } from './always-active.js';
@@ -3,8 +3,10 @@
3
3
  * See LICENSE.txt for license terms.
4
4
  */
5
5
  export { SUPPORTED_PROTOCOL_VERSIONS } from './agent-harness.js';
6
+ export { mcpServerConfigEqual } from '../mcp-config.js';
6
7
  // ── Harness-implementation helpers ──────────────────────────────────
7
8
  export { HarnessBusOwner } from './harness-bus-owner.js';
8
9
  export { lowerStreamInput } from './stream-input.js';
9
10
  export { GenSink } from './gen-sink.js';
11
+ export { matchesAlwaysActive, validateAlwaysActiveEntry } from './always-active.js';
10
12
  //# sourceMappingURL=public.js.map
package/dist/index.d.ts CHANGED
@@ -2,10 +2,11 @@ export type { Message, MessagePart, ImagePart, FilePart } from './types/messages
2
2
  export type { ChatEvent, StartEvent, TextDeltaEvent, ReasoningDeltaEvent, ToolCallEvent, ToolApprovalRequestEvent, ToolResultEvent, StepStartEvent, StepFinishEvent, ErrorEvent, FinishEvent, ChatStreamResult, } from './types/events.js';
3
3
  export type { ToolDefinition, ToolCallInfo, ToolResultInfo } from './types/tools.js';
4
4
  export type { ContextUsage, FinishReason, UsageMetadata } from './types/usage.js';
5
+ export type { AgentHooks, HooksForAgent, ToolResultRedactor, ToolResultRedactionInput, ToolResultRedactionResult, } from './types/redaction.js';
5
6
  export type { AgentConfig, HarnessAgentConfig, StreamOptions, ToolApprovalMode } from './harness/harness-config.js';
6
7
  export { DEFAULT_MAX_STEPS, resolveToolApprovalMode } from './harness/harness-config.js';
7
8
  export type { MCPConfiguration, MCPServerConfig, MCPStdioServerConfig, MCPRemoteServerConfig, McpServerInfo, McpServerErrorCategory, McpServerErrorDetail, McpToolInfo, McpToolAnnotations, } from './mcp-config.js';
8
- export { McpServerStatus } from './mcp-config.js';
9
+ export { McpServerStatus, mcpServerConfigEqual } from './mcp-config.js';
9
10
  export { Model, ModelName, createClaudeModel } from '@salesforce/llm-gateway-sdk';
10
11
  export type { ClaudeModelOverrides } from '@salesforce/llm-gateway-sdk';
11
12
  export { inferSfApiEnv, SfApiEnv } from '@salesforce/agentic-common';
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * See LICENSE.txt for license terms.
4
4
  */
5
5
  export { DEFAULT_MAX_STEPS, resolveToolApprovalMode } from './harness/harness-config.js';
6
- export { McpServerStatus } from './mcp-config.js';
6
+ export { McpServerStatus, mcpServerConfigEqual } from './mcp-config.js';
7
7
  export { Model, ModelName, createClaudeModel } from '@salesforce/llm-gateway-sdk';
8
8
  export { inferSfApiEnv, SfApiEnv } from '@salesforce/agentic-common';
9
9
  // ── Agent Layer ─────────────────────────────────────────────────────
@@ -34,25 +34,6 @@ export type MCPStdioServerConfig = {
34
34
  enabled?: boolean;
35
35
  /** Timeout in milliseconds for individual requests to the server. */
36
36
  timeout?: number;
37
- /**
38
- * Opt the server's tool surface out of the active runtime's tool-search
39
- * deferral. When `true`, every tool advertised by this server is
40
- * registered with the model up-front instead of sitting behind a
41
- * search/load round-trip. Useful for small, discovery-critical surfaces
42
- * (e.g. ≤ 10 tools the model needs to find without prompting). Default
43
- * (`undefined` / `false`): tools may be deferred when the active runtime
44
- * enables tool search.
45
- *
46
- * **Harness behavior:**
47
- * - **Claude harness** — sets `_meta['anthropic/alwaysLoad'] = true` on
48
- * each tool the bridge forwards, equivalent to
49
- * `defer_loading: false` on the API. Skill-bridge and consumer-tool
50
- * tools are always-load regardless of this flag (see
51
- * `@salesforce/sfdx-agent-harness-claude` ARCHITECTURE.md).
52
- * - **Mastra harness** — no-op; Mastra eager-loads MCP tools at every
53
- * turn already, so there's no deferral to opt out of.
54
- */
55
- alwaysLoad?: boolean;
56
37
  };
57
38
  /** MCP server accessible over HTTP/SSE at a remote URL. */
58
39
  export type MCPRemoteServerConfig = {
@@ -91,11 +72,6 @@ export type MCPRemoteServerConfig = {
91
72
  /** Factor by which the reconnection delay grows after each attempt. Default `1.5`. */
92
73
  reconnectionDelayGrowFactor?: number;
93
74
  };
94
- /**
95
- * Opt the server's tool surface out of the active runtime's tool-search
96
- * deferral. See {@link MCPStdioServerConfig.alwaysLoad}.
97
- */
98
- alwaysLoad?: boolean;
99
75
  };
100
76
  /** Connection status of a single MCP server. */
101
77
  export declare enum McpServerStatus {
@@ -260,6 +236,36 @@ export type McpServerErrorDetail = {
260
236
  */
261
237
  retriable: boolean;
262
238
  };
239
+ /**
240
+ * Structural deep-equality predicate over {@link MCPServerConfig}. Returns
241
+ * `true` when two configs would behave identically at the harness layer —
242
+ * meaning a harness handed `b` while currently bound to `a` MUST be free to
243
+ * preserve its existing transport, client instance, and discovered tool
244
+ * catalog without cycling.
245
+ *
246
+ * Used by harnesses inside `AgentHarness.updateAgent` to decide which MCP
247
+ * servers to preserve vs. cycle when an agent's config changes. Exported so
248
+ * both production harnesses use the same equality and a third harness can
249
+ * adopt it without duplicating the rules.
250
+ *
251
+ * **Equality rules:**
252
+ * - Both `undefined` ⇒ `true`. Exactly one `undefined` ⇒ `false`.
253
+ * - `type` mismatch ⇒ `false` (the discriminated union splits stdio vs remote).
254
+ * - `enabled: undefined` and `enabled: true` compare equal — both type docs
255
+ * declare `true` as the default.
256
+ * - Stdio: structural compare on `command`, `args` (order-sensitive),
257
+ * `env` (key-order-insensitive), `timeout`.
258
+ * - Remote: `url` is compared via `String(url)` so `URL` instances and
259
+ * strings round-trip; `headers` (key-order-insensitive); `timeout`,
260
+ * `reconnectionOptions` (field-wise).
261
+ *
262
+ * Two configs that pass this predicate but produce different runtime tools
263
+ * (e.g. an upstream stdio server whose binary was overwritten on disk) are
264
+ * NOT detected here — the predicate compares declared config, not runtime
265
+ * state. Use `Agent.reconnectMcpServer(name)` to force a per-server cycle in
266
+ * that case.
267
+ */
268
+ export declare function mcpServerConfigEqual(a: MCPServerConfig | undefined, b: MCPServerConfig | undefined): boolean;
263
269
  /** Runtime status of a configured MCP server, including its discovered tools. */
264
270
  export type McpServerInfo = {
265
271
  name: string;
@@ -17,4 +17,102 @@ export var McpServerStatus;
17
17
  */
18
18
  McpServerStatus["Reconnecting"] = "reconnecting";
19
19
  })(McpServerStatus || (McpServerStatus = {}));
20
+ /**
21
+ * Structural deep-equality predicate over {@link MCPServerConfig}. Returns
22
+ * `true` when two configs would behave identically at the harness layer —
23
+ * meaning a harness handed `b` while currently bound to `a` MUST be free to
24
+ * preserve its existing transport, client instance, and discovered tool
25
+ * catalog without cycling.
26
+ *
27
+ * Used by harnesses inside `AgentHarness.updateAgent` to decide which MCP
28
+ * servers to preserve vs. cycle when an agent's config changes. Exported so
29
+ * both production harnesses use the same equality and a third harness can
30
+ * adopt it without duplicating the rules.
31
+ *
32
+ * **Equality rules:**
33
+ * - Both `undefined` ⇒ `true`. Exactly one `undefined` ⇒ `false`.
34
+ * - `type` mismatch ⇒ `false` (the discriminated union splits stdio vs remote).
35
+ * - `enabled: undefined` and `enabled: true` compare equal — both type docs
36
+ * declare `true` as the default.
37
+ * - Stdio: structural compare on `command`, `args` (order-sensitive),
38
+ * `env` (key-order-insensitive), `timeout`.
39
+ * - Remote: `url` is compared via `String(url)` so `URL` instances and
40
+ * strings round-trip; `headers` (key-order-insensitive); `timeout`,
41
+ * `reconnectionOptions` (field-wise).
42
+ *
43
+ * Two configs that pass this predicate but produce different runtime tools
44
+ * (e.g. an upstream stdio server whose binary was overwritten on disk) are
45
+ * NOT detected here — the predicate compares declared config, not runtime
46
+ * state. Use `Agent.reconnectMcpServer(name)` to force a per-server cycle in
47
+ * that case.
48
+ */
49
+ export function mcpServerConfigEqual(a, b) {
50
+ if (a === b)
51
+ return true;
52
+ if (!a || !b)
53
+ return false;
54
+ if (a.type !== b.type)
55
+ return false;
56
+ if ((a.enabled ?? true) !== (b.enabled ?? true))
57
+ return false;
58
+ if (a.timeout !== b.timeout)
59
+ return false;
60
+ if (a.type === 'stdio' && b.type === 'stdio') {
61
+ if (a.command !== b.command)
62
+ return false;
63
+ if (!arraysEqual(a.args, b.args))
64
+ return false;
65
+ if (!recordsEqual(a.env, b.env))
66
+ return false;
67
+ return true;
68
+ }
69
+ if (a.type === 'remote' && b.type === 'remote') {
70
+ if (String(a.url) !== String(b.url))
71
+ return false;
72
+ if (!recordsEqual(a.headers, b.headers))
73
+ return false;
74
+ if (!reconnectionOptionsEqual(a.reconnectionOptions, b.reconnectionOptions))
75
+ return false;
76
+ return true;
77
+ }
78
+ return false;
79
+ }
80
+ function arraysEqual(a, b) {
81
+ if (a === b)
82
+ return true;
83
+ if (!a || !b)
84
+ return (a?.length ?? 0) === (b?.length ?? 0);
85
+ if (a.length !== b.length)
86
+ return false;
87
+ for (let i = 0; i < a.length; i++) {
88
+ if (a[i] !== b[i])
89
+ return false;
90
+ }
91
+ return true;
92
+ }
93
+ function recordsEqual(a, b) {
94
+ if (a === b)
95
+ return true;
96
+ const aKeys = a ? Object.keys(a) : [];
97
+ const bKeys = b ? Object.keys(b) : [];
98
+ if (aKeys.length !== bKeys.length)
99
+ return false;
100
+ for (const k of aKeys) {
101
+ if (!Object.prototype.hasOwnProperty.call(b ?? {}, k))
102
+ return false;
103
+ if (a[k] !== b[k])
104
+ return false;
105
+ }
106
+ return true;
107
+ }
108
+ function reconnectionOptionsEqual(a, b) {
109
+ if (a === b)
110
+ return true;
111
+ if (!a || !b)
112
+ return !a && !b;
113
+ return (a.maxRetries === b.maxRetries &&
114
+ a.initialReconnectionDelay === b.initialReconnectionDelay &&
115
+ a.maxReconnectionDelay === b.maxReconnectionDelay &&
116
+ a.reconnectionDelayGrowFactor === b.reconnectionDelayGrowFactor);
117
+ }
20
118
  //# sourceMappingURL=mcp-config.js.map
@@ -0,0 +1,171 @@
1
+ import type { AgentConfig } from '../harness/harness-config.js';
2
+ /**
3
+ * Sync-or-async callback the harness invokes for every tool result before it
4
+ * enters the model's context. The redactor inspects the upstream output and
5
+ * either replaces it (returning `{ output }`) or passes it through unchanged
6
+ * (returning `undefined`).
7
+ *
8
+ * Wired per-agent via {@link HooksForAgent} on `createAgentManager`; the SDK
9
+ * surfaces it inside the harness through {@link AgentHooks.onToolResult} on
10
+ * `AgentHarness.createAgent`'s `options.hooks` bag. A single registration
11
+ * covers built-in tools (`Bash`, `Read`, `Edit`, …), MCP tools, and
12
+ * consumer-executed tools declared via {@link AgentConfig.tools}.
13
+ *
14
+ * ### Why this lives in the harness layer
15
+ *
16
+ * Once a tool result reaches the SDK boundary the model has already seen it —
17
+ * any value can then be echoed in the reply, routed into a later tool call
18
+ * (`Bash` arg, file write), or sent to provider logs. Redaction has to fire
19
+ * INSIDE the engine, before the result is folded into the model's next
20
+ * request. The SDK exposes a harness-agnostic shape; each harness wires its
21
+ * native seam (Claude Agent SDK `PostToolUse` hook,
22
+ * Mastra `processInputStep`).
23
+ *
24
+ * ### Audit / preserving the original
25
+ *
26
+ * The redactor sees the unmodified `output` at the call site. Consumers that
27
+ * need an audit trail of the original value MUST log it themselves before
28
+ * returning the redaction. The SDK does not put the original on its telemetry
29
+ * bus or persist it anywhere — that would defeat the point.
30
+ *
31
+ * ### Throw policy is the consumer's
32
+ *
33
+ * The SDK does not own fail-closed semantics. If the redactor throws, the
34
+ * harness re-throws on its native error path: Claude routes through the
35
+ * Claude Agent SDK's `PostToolUse` hook-error path (which synthesizes a
36
+ * `tool_result(is_error=true)`); Mastra propagates from `processInputStep`
37
+ * and surfaces as an `error` ChatEvent on the consumer's eventStream.
38
+ * Consumers requiring a richer fail-closed substitute wrap their redactor's
39
+ * body in `try`/`catch` themselves — see the SDK README's
40
+ * "Tool-Result Redaction" section for the recommended boilerplate.
41
+ *
42
+ * ### Tool-shape constraints
43
+ *
44
+ * The harness does NOT validate that the replacement `output` has the same
45
+ * shape as the original — the redactor knows what tool it is redacting and
46
+ * is responsible for honoring that tool's expected return shape. Notable
47
+ * cases:
48
+ *
49
+ * - **Claude built-in `Bash`** — the replacement MUST keep the
50
+ * `{ stdout, stderr, interrupted }` shape. A bare-string return is rejected
51
+ * by the Claude Agent SDK and the original leaks.
52
+ * - **MCP tools** — the replacement MUST be a valid MCP `CallToolResult`
53
+ * shape (`{ content: [...], isError? }`).
54
+ * - **Consumer-executed tools** — replacement passes through unchanged to
55
+ * `submitToolResult`, so any shape the consumer accepts is fine.
56
+ *
57
+ * ### Performance
58
+ *
59
+ * Both harnesses skip their per-result hook entirely when
60
+ * `hooks.onToolResult` is undefined, so the no-op overhead is exactly zero.
61
+ * When set, both engines await the redactor (sync redactors collapse to a
62
+ * microtask).
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * const redactor: ToolResultRedactor = ({ toolName, output, isError }) => {
67
+ * // Caller-side audit (consumer's responsibility — SDK does not log originals).
68
+ * auditLog.write({ toolName, originalLength: JSON.stringify(output).length });
69
+ *
70
+ * // Bash needs its native shape preserved.
71
+ * if (toolName === 'Bash') {
72
+ * const bash = output as { stdout: string; stderr: string; interrupted: boolean };
73
+ * return { output: { ...bash, stdout: scrub(bash.stdout), stderr: scrub(bash.stderr) } };
74
+ * }
75
+ *
76
+ * // Other tools: walk the structured output and scrub field-by-field.
77
+ * return { output: scrubDeep(output) };
78
+ * };
79
+ *
80
+ * const manager = await createAgentManager(storage, factory, {
81
+ * hooksForAgent: () => ({ onToolResult: redactor }),
82
+ * });
83
+ * ```
84
+ */
85
+ export type ToolResultRedactor = (input: ToolResultRedactionInput) => ToolResultRedactionResult | Promise<ToolResultRedactionResult>;
86
+ /**
87
+ * Inputs the harness hands the {@link ToolResultRedactor} for each tool
88
+ * result. Carries enough identity for the redactor to decide what (if
89
+ * anything) to redact and to attribute audit log entries.
90
+ */
91
+ export type ToolResultRedactionInput = {
92
+ /** Agent that produced the tool result. */
93
+ agentId: string;
94
+ /** Conversation thread the result belongs to. */
95
+ threadId: string;
96
+ /** Stable id linking this result to the originating `tool-call` event. */
97
+ toolCallId: string;
98
+ /** Tool name the model invoked. Built-in / consumer / namespaced MCP form depends on the harness. */
99
+ toolName: string;
100
+ /**
101
+ * Originating MCP server when the tool came from an MCP catalog.
102
+ * `undefined` for built-ins, consumer-executed tools, and Mastra workspace
103
+ * tools. Mirrors the enrichment on {@link ToolResultEvent.serverName}.
104
+ */
105
+ serverName?: string;
106
+ /**
107
+ * Raw upstream output, exactly as the engine received it. The redactor
108
+ * MUST treat this as input only — mutating it is undefined behavior.
109
+ */
110
+ output: unknown;
111
+ /** `true` when the tool execution failed (engine flagged the result as an error). */
112
+ isError: boolean;
113
+ };
114
+ /**
115
+ * Return value from a {@link ToolResultRedactor} invocation.
116
+ *
117
+ * - `{ output }` — replace the original with this value.
118
+ * - `undefined` — pass the original through unchanged.
119
+ *
120
+ * The function signature already permits "no return" (an arrow body that
121
+ * doesn't `return` resolves to `undefined`), so a separate `void` variant
122
+ * isn't needed in the value-type union.
123
+ *
124
+ * The replacement shape MUST match what the originating tool produces. See
125
+ * the tool-shape notes on {@link ToolResultRedactor} for the harness-specific
126
+ * constraints (notably the Claude `Bash` `{ stdout, stderr, interrupted }`
127
+ * requirement).
128
+ */
129
+ export type ToolResultRedactionResult = {
130
+ output: unknown;
131
+ } | undefined;
132
+ /**
133
+ * Per-agent hook bag the SDK resolves once per agent install / update via
134
+ * {@link HooksForAgent} and threads through to the harness on
135
+ * `AgentHarness.createAgent`'s `options.hooks`. Today the bag carries one
136
+ * field; the shape is open so future hooks (e.g. `onToolCall`, `onStep`)
137
+ * can be added without churning `*HarnessFactoryConfig`s.
138
+ *
139
+ * Harnesses MUST treat this object as opaque: store it on per-agent state,
140
+ * route the hooks they recognize to their native seam, and IGNORE unknown
141
+ * fields (forward-compat). Harnesses MUST NOT swallow hook throws — an
142
+ * exception from a hook MUST propagate on the harness's native error path
143
+ * so the original value never leaks to the model.
144
+ *
145
+ * The SDK never reads, persists, or surfaces this bag on its telemetry bus.
146
+ */
147
+ export type AgentHooks = {
148
+ /**
149
+ * Optional redactor invoked for every tool result before it enters the
150
+ * model's context. See {@link ToolResultRedactor}. Each harness routes
151
+ * this to its native seam (Claude `PostToolUse`, Mastra
152
+ * `processInputStep`); the SDK does not enforce fail-closed semantics.
153
+ */
154
+ onToolResult?: ToolResultRedactor;
155
+ };
156
+ /**
157
+ * Resolves a per-agent {@link AgentHooks} bag from the agent's id and the
158
+ * config the SDK currently has on file for that agent. Invoked by
159
+ * `AgentManager` once per agent install (`createAgent`, boot-time restore,
160
+ * `Agent.updateAgentConfig`); the resolved bag is handed to
161
+ * `AgentHarness.createAgent`'s `options.hooks`.
162
+ *
163
+ * The callback is sync — the SDK does not await. Consumers needing async
164
+ * setup (e.g. remote feature flags) pre-resolve before constructing the
165
+ * manager.
166
+ *
167
+ * Consumers with one global policy ignore both arguments and return the
168
+ * same bag every time; consumers wanting per-agent variation branch on
169
+ * `agentId` or fields of `config`.
170
+ */
171
+ export type HooksForAgent = (agentId: string, config: AgentConfig) => AgentHooks;
@@ -0,0 +1,6 @@
1
+ /*
2
+ * Copyright 2026, Salesforce, Inc. All rights reserved.
3
+ * See LICENSE.txt for license terms.
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=redaction.js.map