@salesforce/sfdx-agent-sdk 0.16.0 → 0.17.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 +22 -0
- package/README.md +153 -20
- package/dist/agent-manager.d.ts +19 -6
- package/dist/agent-manager.js +23 -12
- package/dist/agent.d.ts +25 -8
- package/dist/agent.js +29 -20
- package/dist/harness/agent-harness.d.ts +91 -1
- package/dist/harness/always-active.d.ts +60 -0
- package/dist/harness/always-active.js +58 -0
- package/dist/harness/public.d.ts +3 -0
- package/dist/harness/public.js +2 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/mcp-config.d.ts +30 -24
- package/dist/mcp-config.js +98 -0
- package/dist/types/redaction.d.ts +171 -0
- package/dist/types/redaction.js +6 -0
- package/package.json +14 -13
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
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.17.0] - 2026-06-09
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
- **release**: auto-generate per-package CHANGELOG.md on publish ([#567](https://github.com/forcedotcom/agentic-dx/pull/567))
|
|
10
|
+
- unify tool-exposure policy under toolSearch.alwaysActive ([#566](https://github.com/forcedotcom/agentic-dx/pull/566))
|
|
11
|
+
- add ClaudeAgentConfig.skillSearch + decouple Mastra skillSearch from toolSearch ([#563](https://github.com/forcedotcom/agentic-dx/pull/563))
|
|
12
|
+
- **agent-sdk**: preserve MCP clients across updateAgentConfig ([#560](https://github.com/forcedotcom/agentic-dx/pull/560))
|
|
13
|
+
- **agent-sdk,harness-claude,harness-mastra**: first-class tool-result redaction ([#546](https://github.com/forcedotcom/agentic-dx/pull/546))
|
|
14
|
+
|
|
15
|
+
### Fixes
|
|
16
|
+
- **harness-mastra**: honor MCPServerConfig.alwaysLoad when toolSearch is set ([#558](https://github.com/forcedotcom/agentic-dx/pull/558))
|
|
17
|
+
|
|
18
|
+
### Chores
|
|
19
|
+
- **deps-dev**: bump the eslint group across 1 directory with 2 updates ([#553](https://github.com/forcedotcom/agentic-dx/pull/553))
|
|
20
|
+
- **deps-dev**: bump the dev-dependencies group across 1 directory with 4 updates ([#536](https://github.com/forcedotcom/agentic-dx/pull/536))
|
|
21
|
+
- **deps-dev**: bump the vitest group across 1 directory with 3 updates ([#552](https://github.com/forcedotcom/agentic-dx/pull/552))
|
|
22
|
+
|
package/README.md
CHANGED
|
@@ -59,14 +59,19 @@ await manager.shutdown();
|
|
|
59
59
|
|
|
60
60
|
## API Reference
|
|
61
61
|
|
|
62
|
-
### `createAgentManager<F>(storageRootFolder, harnessFactory,
|
|
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.
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
|
package/dist/agent-manager.d.ts
CHANGED
|
@@ -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 `
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
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>,
|
|
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>>;
|
package/dist/agent-manager.js
CHANGED
|
@@ -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
|
-
|
|
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 `
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
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,
|
|
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
|
|
48
|
-
*
|
|
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
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
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
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
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
|
-
|
|
136
|
-
|
|
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
|
|
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
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
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.
|
|
@@ -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
|
|
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
|
package/dist/harness/public.d.ts
CHANGED
|
@@ -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';
|
package/dist/harness/public.js
CHANGED
|
@@ -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 ─────────────────────────────────────────────────────
|
package/dist/mcp-config.d.ts
CHANGED
|
@@ -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;
|
package/dist/mcp-config.js
CHANGED
|
@@ -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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/sfdx-agent-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Harness-agnostic agentic infrastructure for Salesforce developer experience tooling",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -36,31 +36,32 @@
|
|
|
36
36
|
"dist",
|
|
37
37
|
"!dist/**/*.map",
|
|
38
38
|
"!dist/test",
|
|
39
|
+
"CHANGELOG.md",
|
|
39
40
|
"LICENSE.txt"
|
|
40
41
|
],
|
|
41
42
|
"dependencies": {
|
|
42
|
-
"@salesforce/agentic-common": "0.
|
|
43
|
-
"@salesforce/llm-gateway-sdk": "0.
|
|
43
|
+
"@salesforce/agentic-common": "0.9.0",
|
|
44
|
+
"@salesforce/llm-gateway-sdk": "0.13.0"
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|
|
46
47
|
"@eslint/js": "^10.0.1",
|
|
47
|
-
"@salesforce/sfdx-agent-harness-claude": "0.
|
|
48
|
-
"@salesforce/sfdx-agent-harness-mastra": "0.
|
|
49
|
-
"@types/node": "^22.19.
|
|
50
|
-
"@vitest/coverage-istanbul": "^4.1.
|
|
51
|
-
"@vitest/eslint-plugin": "^1.6.
|
|
52
|
-
"eslint": "^10.4.
|
|
48
|
+
"@salesforce/sfdx-agent-harness-claude": "0.13.0",
|
|
49
|
+
"@salesforce/sfdx-agent-harness-mastra": "0.16.0",
|
|
50
|
+
"@types/node": "^22.19.19",
|
|
51
|
+
"@vitest/coverage-istanbul": "^4.1.8",
|
|
52
|
+
"@vitest/eslint-plugin": "^1.6.19",
|
|
53
|
+
"eslint": "^10.4.1",
|
|
53
54
|
"eslint-config-prettier": "^10.1.8",
|
|
54
|
-
"eslint-import-resolver-typescript": "^4.4.
|
|
55
|
+
"eslint-import-resolver-typescript": "^4.4.5",
|
|
55
56
|
"eslint-plugin-import": "^2.32.0",
|
|
56
57
|
"eslint-plugin-n": "^18.0.1",
|
|
57
58
|
"globals": "^17.6.0",
|
|
58
|
-
"lint-staged": "^17.0.
|
|
59
|
+
"lint-staged": "^17.0.7",
|
|
59
60
|
"prettier": "^3.8.3",
|
|
60
61
|
"rimraf": "^6.1.3",
|
|
61
|
-
"tsx": "^4.22.
|
|
62
|
+
"tsx": "^4.22.4",
|
|
62
63
|
"typescript": "^6.0.3",
|
|
63
|
-
"typescript-eslint": "^8.
|
|
64
|
+
"typescript-eslint": "^8.60.1",
|
|
64
65
|
"vitest": "^4.1.7"
|
|
65
66
|
},
|
|
66
67
|
"engines": {
|