@salesforce/sfdx-agent-sdk 0.15.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 +312 -99
- 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/chat-session.d.ts +43 -26
- package/dist/chat-session.js +34 -23
- package/dist/harness/agent-harness.d.ts +114 -17
- package/dist/harness/always-active.d.ts +60 -0
- package/dist/harness/always-active.js +58 -0
- package/dist/harness/gen-sink.d.ts +41 -0
- package/dist/harness/gen-sink.js +88 -0
- package/dist/harness/index.d.ts +1 -0
- package/dist/harness/index.js +1 -0
- package/dist/harness/public.d.ts +52 -0
- package/dist/harness/public.js +12 -0
- package/dist/index.d.ts +2 -4
- package/dist/index.js +1 -4
- 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 +18 -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
|
@@ -47,7 +47,8 @@ for await (const event of eventStream) {
|
|
|
47
47
|
process.stdout.write(event.text);
|
|
48
48
|
} else if (event.type === 'error') {
|
|
49
49
|
console.error(event.error.message);
|
|
50
|
-
break
|
|
50
|
+
// Don't break — the SDK invariant guarantees a synthetic
|
|
51
|
+
// `FinishEvent('error')` follows; the loop exits naturally next tick.
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -58,14 +59,19 @@ await manager.shutdown();
|
|
|
58
59
|
|
|
59
60
|
## API Reference
|
|
60
61
|
|
|
61
|
-
### `createAgentManager<F>(storageRootFolder, harnessFactory,
|
|
62
|
+
### `createAgentManager<F>(storageRootFolder, harnessFactory, options?): Promise<AgentManager<H>>`
|
|
62
63
|
|
|
63
64
|
Factory function that creates an `AgentManager` backed by the provided `HarnessFactory`. The `storageRootFolder` must be
|
|
64
65
|
an existing directory and is used for persistent state (the harness's runtime data plus the SDK's per-agent identity
|
|
65
66
|
files at `${storageRootFolder}/agents/<id>.json`). The SDK verifies that the constructed harness uses a supported
|
|
66
|
-
protocol version, replays any persisted agents the harness can still serve, and returns the manager.
|
|
67
|
-
|
|
68
|
-
|
|
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. |
|
|
69
75
|
|
|
70
76
|
The harness type `H` is **inferred from the factory's `create()` return type**, so consumers don't pass an explicit type
|
|
71
77
|
argument:
|
|
@@ -152,31 +158,33 @@ keeps unparameterized call sites working.
|
|
|
152
158
|
|
|
153
159
|
A single conversation thread.
|
|
154
160
|
|
|
155
|
-
| Method | Signature
|
|
156
|
-
| ------------------- |
|
|
157
|
-
| `getId` | `() => string`
|
|
158
|
-
| `chat` | `(message: string, options?: ChatOptions) => Promise<ChatStreamResult>`
|
|
159
|
-
| `submitToolResult` | `(toolResult: ToolResultInfo) => Promise<
|
|
160
|
-
| `approveToolCall` | `(toolCallId: string, options?: { remember?: boolean }) => Promise<
|
|
161
|
-
| `declineToolCall` | `(toolCallId: string) => Promise<
|
|
162
|
-
| `getMessageHistory` | `() => Promise<Message[]>`
|
|
163
|
-
| `clearHistory` | `() => Promise<void>`
|
|
164
|
-
| `getContextUsage` | `() => ContextUsage`
|
|
165
|
-
| `addContext` | `(message: string \| Message[]) => Promise<void>`
|
|
166
|
-
| `subscribe` | `(callback: (event: ChatEvent) => void) => void`
|
|
167
|
-
| `unsubscribe` | `(callback: (event: ChatEvent) => void) => void`
|
|
168
|
-
| `onTelemetry` | `(callback: TelemetryEventCallback) => Unsubscribe`
|
|
169
|
-
| `onLog` | `(callback: (record: LogRecord) => void) => Unsubscribe`
|
|
170
|
-
| `dispose` | `() => void`
|
|
161
|
+
| Method | Signature | Description |
|
|
162
|
+
| ------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
163
|
+
| `getId` | `() => string` | Session/thread identifier. |
|
|
164
|
+
| `chat` | `(message: string, options?: ChatOptions) => Promise<ChatStreamResult>` | Send a message and stream the response. The returned `eventStream` is the single iterator for the entire chat turn. |
|
|
165
|
+
| `submitToolResult` | `(toolResult: ToolResultInfo) => Promise<void>` | Return a consumer-executed tool result. Control message on the existing turn — post-resume events flow on the same stream. |
|
|
166
|
+
| `approveToolCall` | `(toolCallId: string, options?: { remember?: boolean }) => Promise<void>` | Approve a pending tool call. Control message on the existing turn — post-resume events flow on the same stream. |
|
|
167
|
+
| `declineToolCall` | `(toolCallId: string) => Promise<void>` | Decline a pending tool call. Control message on the existing turn — post-resume events flow on the same stream. |
|
|
168
|
+
| `getMessageHistory` | `() => Promise<Message[]>` | Retrieve all messages in chronological order. |
|
|
169
|
+
| `clearHistory` | `() => Promise<void>` | Delete all messages. |
|
|
170
|
+
| `getContextUsage` | `() => ContextUsage` | Snapshot of how much of the model's context window the most recent turn used. |
|
|
171
|
+
| `addContext` | `(message: string \| Message[]) => Promise<void>` | Inject context without triggering an LLM response. |
|
|
172
|
+
| `subscribe` | `(callback: (event: ChatEvent) => void) => void` | Register a real-time event listener. |
|
|
173
|
+
| `unsubscribe` | `(callback: (event: ChatEvent) => void) => void` | Remove a listener. |
|
|
174
|
+
| `onTelemetry` | `(callback: TelemetryEventCallback) => Unsubscribe` | Subscribe to telemetry scoped to this session. |
|
|
175
|
+
| `onLog` | `(callback: (record: LogRecord) => void) => Unsubscribe` | Subscribe to logs scoped to this session. |
|
|
176
|
+
| `dispose` | `() => void` | Release session-level event resources. Idempotent. |
|
|
171
177
|
|
|
172
178
|
### `ChatStreamResult`
|
|
173
179
|
|
|
174
|
-
Returned by `chat()
|
|
180
|
+
Returned by `chat()`. The single `eventStream` covers the entire chat turn — including post-resume events from
|
|
181
|
+
`submitToolResult` / `approveToolCall` / `declineToolCall`. Settle methods return `Promise<void>`; the consumer keeps
|
|
182
|
+
iterating the same `eventStream` until it sees a terminal `finish` event.
|
|
175
183
|
|
|
176
|
-
| Property | Type | Description
|
|
177
|
-
| ------------- | --------------------------- |
|
|
178
|
-
| `eventStream` | `AsyncGenerator<ChatEvent>` | Full lifecycle event stream.
|
|
179
|
-
| `textStream` | `AsyncGenerator<string>` | Convenience stream of text-only tokens
|
|
184
|
+
| Property | Type | Description |
|
|
185
|
+
| ------------- | --------------------------- | ---------------------------------------------------------------------------- |
|
|
186
|
+
| `eventStream` | `AsyncGenerator<ChatEvent>` | Full lifecycle event stream for the turn (one stream per `chat()` call). |
|
|
187
|
+
| `textStream` | `AsyncGenerator<string>` | Convenience stream of text-only tokens, derived from the same `eventStream`. |
|
|
180
188
|
|
|
181
189
|
### `ChatEvent`
|
|
182
190
|
|
|
@@ -237,7 +245,6 @@ type MCPStdioServerConfig = {
|
|
|
237
245
|
env?: Record<string, string>;
|
|
238
246
|
enabled?: boolean;
|
|
239
247
|
timeout?: number;
|
|
240
|
-
alwaysLoad?: boolean;
|
|
241
248
|
};
|
|
242
249
|
|
|
243
250
|
// Remote server (HTTP/SSE)
|
|
@@ -253,16 +260,13 @@ type MCPRemoteServerConfig = {
|
|
|
253
260
|
maxReconnectionDelay?: number;
|
|
254
261
|
reconnectionDelayGrowFactor?: number;
|
|
255
262
|
};
|
|
256
|
-
alwaysLoad?: boolean;
|
|
257
263
|
};
|
|
258
264
|
```
|
|
259
265
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
`_meta['anthropic/alwaysLoad'] = true` on each forwarded tool (equivalent to `defer_loading: false` on the Claude API).
|
|
265
|
-
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".
|
|
266
270
|
|
|
267
271
|
**`reconnectionOptions`** tunes the HTTP MCP transport's retry / backoff behavior. Forwarded to the underlying SDK
|
|
268
272
|
transport on both harnesses (Claude's `@modelcontextprotocol/sdk` `StreamableHTTPClientTransport` and Mastra's
|
|
@@ -560,17 +564,17 @@ try {
|
|
|
560
564
|
|
|
561
565
|
#### Streaming method failures
|
|
562
566
|
|
|
563
|
-
|
|
564
|
-
|
|
567
|
+
`chat()` opens a turn; `submitToolResult()` / `approveToolCall()` / `declineToolCall()` are control messages on the
|
|
568
|
+
existing turn. All four share a single failure contract. Subscribers registered via `subscribe()` are guaranteed to
|
|
565
569
|
receive a terminal event for every turn:
|
|
566
570
|
|
|
567
|
-
- **Pre-stream failure** (the harness rejects before producing a stream
|
|
568
|
-
by a `FinishEvent(finishReason: 'error')`, and the returned
|
|
569
|
-
call in `try/catch` if you need to handle this in the caller.
|
|
570
|
-
- **In-stream failure** (the
|
|
571
|
-
yields the `ErrorEvent` (synthesizing one from a thrown exception if needed) and, if no `FinishEvent` was
|
|
572
|
-
emitted, appends a synthetic `FinishEvent(finishReason: 'error')` at the end. The
|
|
573
|
-
iterate the stream to observe the failure.
|
|
571
|
+
- **Pre-stream failure** (the harness rejects before producing a stream, or a settle call rejects before it routes the
|
|
572
|
+
decision): subscribers receive an `ErrorEvent` followed by a `FinishEvent(finishReason: 'error')`, and the returned
|
|
573
|
+
promise then rejects with the original error. Wrap the call in `try/catch` if you need to handle this in the caller.
|
|
574
|
+
- **In-stream failure** (the active `eventStream` yields an `error` event or the underlying generator throws): the
|
|
575
|
+
stream yields the `ErrorEvent` (synthesizing one from a thrown exception if needed) and, if no `FinishEvent` was
|
|
576
|
+
already emitted, appends a synthetic `FinishEvent(finishReason: 'error')` at the end. The originating `chat()` promise
|
|
577
|
+
resolves normally; iterate the stream to observe the failure.
|
|
574
578
|
- **Calling a disposed session** throws `AgentSDKError('DISPOSED')` synchronously without notifying subscribers.
|
|
575
579
|
|
|
576
580
|
### MCP Server Configuration
|
|
@@ -613,99 +617,286 @@ reparsing namespaced tool names. `annotations` is `undefined` when the source di
|
|
|
613
617
|
`requireToolApproval` accepts a `boolean` (`true` is shorthand for `'serial'`) or one of the mode strings exposed via
|
|
614
618
|
the `ToolApprovalMode` type alias (`'serial' | 'batch'`):
|
|
615
619
|
|
|
616
|
-
- **`'serial'`** (the safe default) —
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
approvals before settling.
|
|
620
|
+
- **`'serial'`** (the safe default) — `tool-approval-request` events surface one at a time. The next request (if any)
|
|
621
|
+
arrives later on the same `eventStream` after the consumer settles the current one.
|
|
622
|
+
- **`'batch'`** — when the model emits parallel `tool_use` blocks, all approval-requests for that batch surface on the
|
|
623
|
+
same `eventStream` together, so the consumer can render a batch-approval card. The consumer must gather all approvals
|
|
624
|
+
before settling — a `break`-on-first-approval loop will hang the turn because Mastra/Claude won't continue emitting
|
|
625
|
+
tool-results until the consumer has settled the entire batch.
|
|
623
626
|
|
|
624
627
|
The SDK also exports `resolveToolApprovalMode(value)` — the canonical
|
|
625
628
|
`boolean | ToolApprovalMode | undefined → ToolApprovalMode | undefined` normalizer harness implementations should use to
|
|
626
629
|
dispatch (`undefined` / `false` → `undefined`, `true` → `'serial'`, strings pass through). Reject unknown strings via
|
|
627
630
|
`throw` instead of reimplementing the boolean-vs-string branch — the helper's defensive throw catches `as any` consumers
|
|
628
|
-
passing invalid values that would otherwise silently degrade to "approval gating on but no
|
|
631
|
+
passing invalid values that would otherwise silently degrade to "approval gating on but no coordinator allocated."
|
|
632
|
+
|
|
633
|
+
#### Single stream per turn
|
|
634
|
+
|
|
635
|
+
Issue [#529](https://github.com/forcedotcom/agentic-dx/issues/529) settled the harness contract: **one `eventStream` per
|
|
636
|
+
chat turn.** Settle calls (`approveToolCall` / `declineToolCall` / `submitToolResult`) return `Promise<void>` and behave
|
|
637
|
+
as control messages on the existing turn; the post-resume events (`tool-result`, follow-up `text-delta`, `step-finish`,
|
|
638
|
+
terminal `finish`) flow through the **same** `eventStream` the `chat()` call returned. The consumer should iterate that
|
|
639
|
+
one stream until it sees a terminal `finish` event, calling `approveToolCall` / `declineToolCall` / `submitToolResult`
|
|
640
|
+
in-line as approval-requests / consumer-tool-calls arrive.
|
|
629
641
|
|
|
630
|
-
|
|
631
|
-
> call `approveToolCall`/`declineToolCall`). _Pattern B_ = "return-on-first-approval" (settle the first approval
|
|
632
|
-
> mid-iteration, then re-iterate the continuation stream). `'serial'` works with either; `'batch'` requires Pattern A.
|
|
642
|
+
#### Pattern: settle in-line, continue iterating
|
|
633
643
|
|
|
634
|
-
|
|
644
|
+
The same shape works for `'serial'` and `'batch'` mode:
|
|
635
645
|
|
|
636
646
|
```typescript
|
|
637
647
|
const { eventStream } = await session.chat('Run the deployment', {
|
|
638
|
-
requireToolApproval: true, // or 'serial'
|
|
648
|
+
requireToolApproval: true, // or 'serial' / 'batch'
|
|
639
649
|
});
|
|
640
650
|
|
|
641
651
|
for await (const event of eventStream) {
|
|
642
652
|
if (event.type === 'tool-approval-request') {
|
|
643
653
|
const approved = await promptUser(event);
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
// process continuation
|
|
654
|
+
if (approved) {
|
|
655
|
+
await session.approveToolCall(event.toolCall.toolCallId);
|
|
656
|
+
} else {
|
|
657
|
+
await session.declineToolCall(event.toolCall.toolCallId);
|
|
649
658
|
}
|
|
650
|
-
break
|
|
659
|
+
// Do NOT break — keep iterating; tool-result and follow-up text arrive on the same stream.
|
|
660
|
+
} else if (event.type === 'tool-call' && event.serverName === undefined && isConsumerTool(event.toolName)) {
|
|
661
|
+
// Consumer-executed tool: run locally, return the result via submitToolResult.
|
|
662
|
+
const result = await runLocally(event.args);
|
|
663
|
+
await session.submitToolResult({
|
|
664
|
+
toolCallId: event.toolCallId,
|
|
665
|
+
toolName: event.toolName,
|
|
666
|
+
result,
|
|
667
|
+
});
|
|
668
|
+
} else if (event.type === 'text-delta') {
|
|
669
|
+
process.stdout.write(event.text);
|
|
670
|
+
} else if (event.type === 'finish') {
|
|
671
|
+
// Don't break on `'error'` directly — the SDK invariant guarantees an
|
|
672
|
+
// `error` event is followed by a synthetic `FinishEvent('error')` so
|
|
673
|
+
// the loop exits naturally on the next iteration. Breaking on `error`
|
|
674
|
+
// would finalize the iterator before the synthetic `finish` lands and
|
|
675
|
+
// bypass any per-turn cleanup the harness wires onto the source's
|
|
676
|
+
// close/return path.
|
|
677
|
+
break;
|
|
651
678
|
}
|
|
652
679
|
}
|
|
653
680
|
```
|
|
654
681
|
|
|
655
|
-
#### Pattern
|
|
682
|
+
#### Pattern variant: batch-collect approvals before settling
|
|
683
|
+
|
|
684
|
+
Same single-stream loop, just gather the batch before deciding (useful for "approve these N tools?" UI cards under
|
|
685
|
+
`requireToolApproval: 'batch'`):
|
|
656
686
|
|
|
657
687
|
```typescript
|
|
658
688
|
const { eventStream } = await session.chat('Run the deployment', {
|
|
659
689
|
requireToolApproval: 'batch',
|
|
660
690
|
});
|
|
661
691
|
|
|
662
|
-
const
|
|
692
|
+
const pendingBatch: ToolApprovalRequestEvent[] = [];
|
|
693
|
+
|
|
663
694
|
for await (const event of eventStream) {
|
|
664
695
|
if (event.type === 'tool-approval-request') {
|
|
665
|
-
|
|
696
|
+
pendingBatch.push(event);
|
|
697
|
+
// Heuristic: settle the batch on a quiet window OR after the expected count.
|
|
698
|
+
// Don't break — the harness needs us to settle so it can continue.
|
|
699
|
+
continue;
|
|
666
700
|
}
|
|
667
|
-
if (event.type === '
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
const decisions = await promptUserForBatch(
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
701
|
+
if (event.type === 'tool-call') {
|
|
702
|
+
// The harness emits `tool-approval-request` BEFORE `tool-call` in batch mode.
|
|
703
|
+
// Once we see a tool-call, the batch for this assistant message is complete.
|
|
704
|
+
if (pendingBatch.length > 0) {
|
|
705
|
+
const decisions = await promptUserForBatch(pendingBatch);
|
|
706
|
+
await Promise.all(
|
|
707
|
+
pendingBatch.map((req) =>
|
|
708
|
+
decisions.get(req.toolCall.toolCallId)
|
|
709
|
+
? session.approveToolCall(req.toolCall.toolCallId)
|
|
710
|
+
: session.declineToolCall(req.toolCall.toolCallId),
|
|
711
|
+
),
|
|
712
|
+
);
|
|
713
|
+
pendingBatch.length = 0;
|
|
714
|
+
}
|
|
715
|
+
} else if (event.type === 'text-delta') {
|
|
716
|
+
process.stdout.write(event.text);
|
|
717
|
+
} else if (event.type === 'finish') {
|
|
718
|
+
// Don't break on `'error'` directly — the SDK invariant guarantees an
|
|
719
|
+
// `error` event is followed by a synthetic `FinishEvent('error')` so
|
|
720
|
+
// the loop exits naturally on the next iteration. Breaking on `error`
|
|
721
|
+
// would finalize the iterator before the synthetic `finish` lands and
|
|
722
|
+
// bypass any per-turn cleanup the harness wires onto the source's
|
|
723
|
+
// close/return path.
|
|
724
|
+
break;
|
|
681
725
|
}
|
|
682
726
|
}
|
|
683
727
|
```
|
|
684
728
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
`
|
|
688
|
-
|
|
689
|
-
|
|
729
|
+
> **AsyncIterator gotcha:** the `for await` loop is the supported way to drain `eventStream`. If you instead drive the
|
|
730
|
+
> iterator manually (`iter.next()`), call **at most one** `next()` at a time and `await` the result before the next
|
|
731
|
+
> call. Calling `iter.next()` twice without awaiting the first orphans the first request — JS settles `.next()` calls
|
|
732
|
+
> FIFO, so the next yielded value satisfies the orphaned request and is invisible to the caller. Stick to `for await`
|
|
733
|
+
> unless you have a specific reason not to.
|
|
690
734
|
|
|
691
735
|
### Consumer-Executed Tools
|
|
692
736
|
|
|
737
|
+
Tools declared via `AgentConfig.tools` (no `execute` function) run on the consumer side: the model emits a `tool-call`
|
|
738
|
+
event, the consumer runs the tool locally, then calls `session.submitToolResult(...)` to feed the result back.
|
|
739
|
+
`submitToolResult` returns `Promise<void>`; the model's follow-up turn (and any subsequent `tool-call`s) flows through
|
|
740
|
+
the same `eventStream`:
|
|
741
|
+
|
|
693
742
|
```typescript
|
|
743
|
+
const { eventStream } = await session.chat('Look up the customer record for tenant T-42.');
|
|
744
|
+
|
|
694
745
|
for await (const event of eventStream) {
|
|
695
|
-
if (event.type === 'tool-call' && event.toolName === '
|
|
746
|
+
if (event.type === 'tool-call' && event.toolName === 'lookup_customer') {
|
|
696
747
|
const result = await runLocally(event.args);
|
|
697
|
-
|
|
748
|
+
await session.submitToolResult({
|
|
698
749
|
toolCallId: event.toolCallId,
|
|
699
750
|
toolName: event.toolName,
|
|
700
751
|
result,
|
|
701
752
|
});
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
753
|
+
// Do NOT break — the model's follow-up turn (text, more tool-calls, finish) arrives on the same stream.
|
|
754
|
+
} else if (event.type === 'text-delta') {
|
|
755
|
+
process.stdout.write(event.text);
|
|
756
|
+
} else if (event.type === 'finish') {
|
|
757
|
+
// Don't break on `'error'` directly — the SDK invariant guarantees an
|
|
758
|
+
// `error` event is followed by a synthetic `FinishEvent('error')` so
|
|
759
|
+
// the loop exits naturally on the next iteration. Breaking on `error`
|
|
760
|
+
// would finalize the iterator before the synthetic `finish` lands and
|
|
761
|
+
// bypass any per-turn cleanup the harness wires onto the source's
|
|
762
|
+
// close/return path.
|
|
763
|
+
break;
|
|
705
764
|
}
|
|
706
765
|
}
|
|
707
766
|
```
|
|
708
767
|
|
|
768
|
+
When `requireToolApproval: true` is also set, consumer-executed tools bypass the approval gate by construction (per
|
|
769
|
+
`StreamOptions.requireToolApproval` JSDoc). They surface as a normal `tool-call` event without a preceding
|
|
770
|
+
`tool-approval-request`. Built-in / MCP tools still gate normally.
|
|
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
|
+
|
|
709
900
|
### Connectivity Resolution
|
|
710
901
|
|
|
711
902
|
#### `ResolvedConnectivity`
|
|
@@ -843,15 +1034,37 @@ The SDK ships no harness implementation. Consumers pick one by passing a `Harnes
|
|
|
843
1034
|
[`@salesforce/sfdx-agent-harness-mastra`](../sfdx-agent-harness-mastra); additional harnesses can ship as independent
|
|
844
1035
|
npm packages that depend on this SDK as a `peerDependency`.
|
|
845
1036
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
1037
|
+
### Two import surfaces
|
|
1038
|
+
|
|
1039
|
+
This package publishes two ESM entry points:
|
|
1040
|
+
|
|
1041
|
+
- **`@salesforce/sfdx-agent-sdk`** — consumer surface. Everything an application calling `createAgentManager` needs:
|
|
1042
|
+
`Agent`, `ChatSession`, `ChatEvent`, `MessagePart`, `AgentConfig`, `AgentSDKError`, etc. The harness contract
|
|
1043
|
+
interfaces (`AgentHarness`, `HarnessFactory`, `WithAgentConfig`, `ConfigOf`) also appear here as **type-only**
|
|
1044
|
+
references so consumer code can write `AgentManager<H extends AgentHarness>` without reaching into the
|
|
1045
|
+
harness-implementation surface.
|
|
1046
|
+
- **`@salesforce/sfdx-agent-sdk/harness`** — harness-implementation surface. Harness authors import the contract types
|
|
1047
|
+
and the helpers from here. Consumer applications cannot reach these symbols from the bare specifier — the bundler / TS
|
|
1048
|
+
resolver errors at the import site, which is the right outcome for any consumer who reaches for a primitive they don't
|
|
1049
|
+
need.
|
|
1050
|
+
|
|
1051
|
+
> **Build-tool requirement:** the `/harness` subpath is resolved via the `package.json` `exports` field, which requires
|
|
1052
|
+
> `moduleResolution: "node16" | "nodenext" | "bundler"` in `tsconfig.json`. Legacy `"node"` resolution silently won't
|
|
1053
|
+
> see the subpath. Modern bundlers (Vite, esbuild, Webpack 5+, tsup, Rollup with `@rollup/plugin-node-resolve` v15+)
|
|
1054
|
+
> resolve it natively. This is a harness-author concern only; consumer applications never touch the subpath.
|
|
1055
|
+
|
|
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()`. |
|
|
855
1068
|
|
|
856
1069
|
Minimal skeleton:
|
|
857
1070
|
|
|
@@ -861,7 +1074,7 @@ import {
|
|
|
861
1074
|
type HarnessFactory,
|
|
862
1075
|
HarnessBusOwner,
|
|
863
1076
|
SUPPORTED_PROTOCOL_VERSIONS,
|
|
864
|
-
} from '@salesforce/sfdx-agent-sdk';
|
|
1077
|
+
} from '@salesforce/sfdx-agent-sdk/harness';
|
|
865
1078
|
|
|
866
1079
|
class MyHarness implements AgentHarness {
|
|
867
1080
|
readonly harnessId = 'my-harness';
|