@salesforce/sfdx-agent-sdk 0.14.0 → 0.16.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/README.md +255 -100
- package/dist/agent-connectivity-resolver.d.ts +16 -1
- package/dist/agent-connectivity-resolver.js +54 -3
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +30 -14
- package/dist/chat-session.d.ts +109 -27
- package/dist/chat-session.js +120 -26
- package/dist/harness/agent-harness.d.ts +59 -22
- package/dist/harness/gen-sink.d.ts +41 -0
- package/dist/harness/gen-sink.js +88 -0
- package/dist/harness/harness-config.d.ts +10 -3
- package/dist/harness/index.d.ts +1 -0
- package/dist/harness/index.js +1 -0
- package/dist/harness/public.d.ts +49 -0
- package/dist/harness/public.js +10 -0
- package/dist/index.d.ts +3 -5
- package/dist/index.js +1 -4
- package/dist/mcp-config.d.ts +26 -0
- package/dist/types/events.d.ts +1 -14
- package/dist/types/messages.d.ts +12 -1
- package/dist/types/usage.d.ts +65 -0
- package/package.json +9 -5
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
|
|
|
@@ -152,30 +153,33 @@ keeps unparameterized call sites working.
|
|
|
152
153
|
|
|
153
154
|
A single conversation thread.
|
|
154
155
|
|
|
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
|
-
| `
|
|
165
|
-
| `
|
|
166
|
-
| `
|
|
167
|
-
| `
|
|
168
|
-
| `
|
|
169
|
-
| `
|
|
156
|
+
| Method | Signature | Description |
|
|
157
|
+
| ------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
158
|
+
| `getId` | `() => string` | Session/thread identifier. |
|
|
159
|
+
| `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. |
|
|
160
|
+
| `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. |
|
|
161
|
+
| `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. |
|
|
162
|
+
| `declineToolCall` | `(toolCallId: string) => Promise<void>` | Decline a pending tool call. Control message on the existing turn — post-resume events flow on the same stream. |
|
|
163
|
+
| `getMessageHistory` | `() => Promise<Message[]>` | Retrieve all messages in chronological order. |
|
|
164
|
+
| `clearHistory` | `() => Promise<void>` | Delete all messages. |
|
|
165
|
+
| `getContextUsage` | `() => ContextUsage` | Snapshot of how much of the model's context window the most recent turn used. |
|
|
166
|
+
| `addContext` | `(message: string \| Message[]) => Promise<void>` | Inject context without triggering an LLM response. |
|
|
167
|
+
| `subscribe` | `(callback: (event: ChatEvent) => void) => void` | Register a real-time event listener. |
|
|
168
|
+
| `unsubscribe` | `(callback: (event: ChatEvent) => void) => void` | Remove a listener. |
|
|
169
|
+
| `onTelemetry` | `(callback: TelemetryEventCallback) => Unsubscribe` | Subscribe to telemetry scoped to this session. |
|
|
170
|
+
| `onLog` | `(callback: (record: LogRecord) => void) => Unsubscribe` | Subscribe to logs scoped to this session. |
|
|
171
|
+
| `dispose` | `() => void` | Release session-level event resources. Idempotent. |
|
|
170
172
|
|
|
171
173
|
### `ChatStreamResult`
|
|
172
174
|
|
|
173
|
-
Returned by `chat()
|
|
175
|
+
Returned by `chat()`. The single `eventStream` covers the entire chat turn — including post-resume events from
|
|
176
|
+
`submitToolResult` / `approveToolCall` / `declineToolCall`. Settle methods return `Promise<void>`; the consumer keeps
|
|
177
|
+
iterating the same `eventStream` until it sees a terminal `finish` event.
|
|
174
178
|
|
|
175
|
-
| Property | Type | Description
|
|
176
|
-
| ------------- | --------------------------- |
|
|
177
|
-
| `eventStream` | `AsyncGenerator<ChatEvent>` | Full lifecycle event stream.
|
|
178
|
-
| `textStream` | `AsyncGenerator<string>` | Convenience stream of text-only tokens
|
|
179
|
+
| Property | Type | Description |
|
|
180
|
+
| ------------- | --------------------------- | ---------------------------------------------------------------------------- |
|
|
181
|
+
| `eventStream` | `AsyncGenerator<ChatEvent>` | Full lifecycle event stream for the turn (one stream per `chat()` call). |
|
|
182
|
+
| `textStream` | `AsyncGenerator<string>` | Convenience stream of text-only tokens, derived from the same `eventStream`. |
|
|
179
183
|
|
|
180
184
|
### `ChatEvent`
|
|
181
185
|
|
|
@@ -193,23 +197,28 @@ Discriminated union (`event.type`) of streaming events:
|
|
|
193
197
|
| `step-finish` | `stepIndex`, `finishReason`, `usage?` | Step completed with per-step token usage. |
|
|
194
198
|
| `error` | `error`, `code?` | Mid-stream error (yielded, not thrown). |
|
|
195
199
|
| `finish` | `finishReason`, `usage?` | Stream completed with aggregate token usage. |
|
|
196
|
-
|
|
200
|
+
|
|
201
|
+
> **Diagnostic logging.** The `ChatEvent` union is the harness-agnostic public stream — it never carries
|
|
202
|
+
> harness-internal chunk shapes. When a harness encounters a chunk type its adapter does not recognize (typically after
|
|
203
|
+
> an upstream Mastra / Claude SDK upgrade), the chunk is skipped on the public stream and surfaced via `LogBus.debug`
|
|
204
|
+
> with `chunkType` and `rawChunk` in the record's `context`. Subscribe via `manager.onLog` (or `agent.onLog` /
|
|
205
|
+
> `session.onLog`) at debug level to observe these. Production consumers do not need to filter for unrecognized chunks.
|
|
197
206
|
|
|
198
207
|
### Configuration Types
|
|
199
208
|
|
|
200
209
|
#### `AgentConfig`
|
|
201
210
|
|
|
202
|
-
| Field | Type
|
|
203
|
-
| --------------- |
|
|
204
|
-
| `orgAlias?` | `string`
|
|
205
|
-
| `modelId?` | `ModelName`
|
|
206
|
-
| `name?` | `string`
|
|
207
|
-
| `description?` | `string`
|
|
208
|
-
| `instructions?` | `string`
|
|
209
|
-
| `tools?` | `ToolDefinition[]`
|
|
210
|
-
| `mcpServers?` | `MCPConfiguration`
|
|
211
|
-
| `skills?` | `string[]`
|
|
212
|
-
| `rules?` | `string[]`
|
|
211
|
+
| Field | Type | Description |
|
|
212
|
+
| --------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
213
|
+
| `orgAlias?` | `string` | Salesforce org alias or username. Falls back to project/default org. |
|
|
214
|
+
| `modelId?` | `ModelName \| Model` | LLM model selector. Pass a `ModelName` enum value for an in-tree model (e.g. `'llmgateway__OpenAIGPT5'`), or a pre-built `Model` instance to opt into a Bedrock-Anthropic Claude variant the SDK has not yet released — see `createClaudeModel(gatewayId, overrides)` in `@salesforce/llm-gateway-sdk`. |
|
|
215
|
+
| `name?` | `string` | Human-readable agent name. |
|
|
216
|
+
| `description?` | `string` | Agent purpose description. |
|
|
217
|
+
| `instructions?` | `string` | System instructions for the agent. |
|
|
218
|
+
| `tools?` | `ToolDefinition[]` | Consumer-executed tool schemas. |
|
|
219
|
+
| `mcpServers?` | `MCPConfiguration` | MCP server connections. |
|
|
220
|
+
| `skills?` | `string[]` | Each entry is either an individual skill folder (containing `SKILL.md`) or a parent folder containing skill subfolders. Relative and absolute paths supported; forms can be mixed in the same array. |
|
|
221
|
+
| `rules?` | `string[]` | Each entry is either an individual `.md` rule file or a directory of `.md` rule files (scanned one level deep, alphabetical, non-`.md` skipped). Bodies are composed verbatim into the agent's effective system prompt; YAML frontmatter is optional and stripped if present. Matches Claude Code's `.claude/rules/*.md` convention. |
|
|
213
222
|
|
|
214
223
|
#### `StreamOptions`
|
|
215
224
|
|
|
@@ -241,6 +250,12 @@ type MCPRemoteServerConfig = {
|
|
|
241
250
|
headers?: Record<string, string>;
|
|
242
251
|
enabled?: boolean;
|
|
243
252
|
timeout?: number;
|
|
253
|
+
reconnectionOptions?: {
|
|
254
|
+
maxRetries?: number;
|
|
255
|
+
initialReconnectionDelay?: number;
|
|
256
|
+
maxReconnectionDelay?: number;
|
|
257
|
+
reconnectionDelayGrowFactor?: number;
|
|
258
|
+
};
|
|
244
259
|
alwaysLoad?: boolean;
|
|
245
260
|
};
|
|
246
261
|
```
|
|
@@ -252,6 +267,14 @@ surfaces (≤ a few tools the model needs to find without prompting). The Claude
|
|
|
252
267
|
`_meta['anthropic/alwaysLoad'] = true` on each forwarded tool (equivalent to `defer_loading: false` on the Claude API).
|
|
253
268
|
The Mastra harness eager-loads all MCP tools regardless, so the flag is a no-op there.
|
|
254
269
|
|
|
270
|
+
**`reconnectionOptions`** tunes the HTTP MCP transport's retry / backoff behavior. Forwarded to the underlying SDK
|
|
271
|
+
transport on both harnesses (Claude's `@modelcontextprotocol/sdk` `StreamableHTTPClientTransport` and Mastra's
|
|
272
|
+
`@mastra/mcp` `HttpServerDefinition`, which is itself typed off the same MCP SDK shape). Each field is optional;
|
|
273
|
+
unspecified fields fall back to the MCP SDK's built-in defaults — `maxRetries: 2`, `initialReconnectionDelay: 1000` ms,
|
|
274
|
+
`maxReconnectionDelay: 30000` ms, `reconnectionDelayGrowFactor: 1.5`. Partial overrides are merged with those defaults
|
|
275
|
+
at the harness boundary so a consumer setting only `maxRetries` doesn't zero out the others. No-op for stdio servers —
|
|
276
|
+
only `MCPRemoteServerConfig` carries it.
|
|
277
|
+
|
|
255
278
|
#### `McpServerInfo`
|
|
256
279
|
|
|
257
280
|
| Field | Type | Description |
|
|
@@ -375,6 +398,13 @@ type ImagePart = { type: 'image'; mimeType: 'image/png' | 'image/jpeg'; data: st
|
|
|
375
398
|
type FilePart = { type: 'file'; mimeType: 'application/pdf'; data: string; fileName?: string };
|
|
376
399
|
```
|
|
377
400
|
|
|
401
|
+
`createdAt` is **required-on-read, optional-on-write**:
|
|
402
|
+
|
|
403
|
+
- Messages returned from `ChatSession.getMessageHistory()` always have `createdAt` populated, and the array is sorted
|
|
404
|
+
ascending by `createdAt`. Consumer code can read `msg.createdAt` directly.
|
|
405
|
+
- Consumers constructing `Message` literals for `ChatSession.addContext()` may omit `createdAt`; the SDK backfills the
|
|
406
|
+
current time before forwarding to the harness. Pass an explicit value to override.
|
|
407
|
+
|
|
378
408
|
#### Multimodal input
|
|
379
409
|
|
|
380
410
|
`ChatSession.chat()` (and the harness `stream()` it delegates to) accept either a plain string or a `MessagePart[]`. Use
|
|
@@ -409,7 +439,8 @@ await session.chat([
|
|
|
409
439
|
},
|
|
410
440
|
]);
|
|
411
441
|
|
|
412
|
-
// Inject multimodal context before a chat turn
|
|
442
|
+
// Inject multimodal context before a chat turn. `createdAt` is omitted —
|
|
443
|
+
// the SDK backfills it before forwarding to the harness.
|
|
413
444
|
await session.addContext([
|
|
414
445
|
{
|
|
415
446
|
id: 'ctx-screenshot',
|
|
@@ -455,9 +486,52 @@ type UsageMetadata = {
|
|
|
455
486
|
cacheWriteInputTokens?: number;
|
|
456
487
|
};
|
|
457
488
|
|
|
489
|
+
type ContextUsage = {
|
|
490
|
+
/**
|
|
491
|
+
* Last per-step usage reading observed on this session. Pre-first-turn and
|
|
492
|
+
* immediately after `clearHistory()` this is `{}` (every token field undefined).
|
|
493
|
+
*/
|
|
494
|
+
usage: UsageMetadata;
|
|
495
|
+
/** The model's total context-window size in tokens. Always populated. */
|
|
496
|
+
contextWindow: number;
|
|
497
|
+
/**
|
|
498
|
+
* `(usage.inputTokens + usage.cachedInputTokens + usage.cacheWriteInputTokens) / contextWindow`,
|
|
499
|
+
* clamped to [0, 1]. Cached prompt tokens are summed in because they occupy the
|
|
500
|
+
* model's context window — on Bedrock-Claude, the bulk of the prompt is reported
|
|
501
|
+
* via `cachedInputTokens` / `cacheWriteInputTokens`, not `inputTokens`. `undefined`
|
|
502
|
+
* when ALL three input-bearing fields are missing.
|
|
503
|
+
*/
|
|
504
|
+
usedFraction: number | undefined;
|
|
505
|
+
};
|
|
506
|
+
|
|
458
507
|
type FinishReason = 'stop' | 'length' | 'tool-calls' | 'content-filter' | 'error' | 'other';
|
|
459
508
|
```
|
|
460
509
|
|
|
510
|
+
**Tracking context-window utilization.** `ChatSession.getContextUsage()` always returns a populated `ContextUsage` —
|
|
511
|
+
even pre-first-turn, where `usage` is `{}` and `usedFraction` is `undefined`, but `contextWindow` is always available.
|
|
512
|
+
Use it to decide when to compact a thread:
|
|
513
|
+
|
|
514
|
+
```typescript
|
|
515
|
+
const ctx = session.getContextUsage();
|
|
516
|
+
if (ctx.usedFraction !== undefined && ctx.usedFraction > 0.8) {
|
|
517
|
+
await agent.compactChatSession(session.getId());
|
|
518
|
+
}
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
Render a context-usage indicator that distinguishes "no reading yet" from a real measurement:
|
|
522
|
+
|
|
523
|
+
```typescript
|
|
524
|
+
const ctx = session.getContextUsage();
|
|
525
|
+
const limit = ctx.contextWindow.toLocaleString(); // always available
|
|
526
|
+
const used = ctx.usage.inputTokens?.toLocaleString() ?? '—';
|
|
527
|
+
const pct = ctx.usedFraction !== undefined ? `${Math.round(ctx.usedFraction * 100)}%` : '—';
|
|
528
|
+
return `${used} / ${limit} tokens (${pct})`;
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
The snapshot uses **last-step** semantics, not the per-turn billing aggregate — `finish.usage` sums all steps in a turn
|
|
532
|
+
and double-counts persistent context, which is the wrong denominator for "how full is my context." For per-turn billing
|
|
533
|
+
totals, subscribe to `chat-stream-completed` telemetry instead.
|
|
534
|
+
|
|
461
535
|
### Error Handling
|
|
462
536
|
|
|
463
537
|
The SDK throws `AgentSDKError` for predictable not-found and compatibility conditions. Each error has a `type` property
|
|
@@ -489,17 +563,17 @@ try {
|
|
|
489
563
|
|
|
490
564
|
#### Streaming method failures
|
|
491
565
|
|
|
492
|
-
|
|
493
|
-
|
|
566
|
+
`chat()` opens a turn; `submitToolResult()` / `approveToolCall()` / `declineToolCall()` are control messages on the
|
|
567
|
+
existing turn. All four share a single failure contract. Subscribers registered via `subscribe()` are guaranteed to
|
|
494
568
|
receive a terminal event for every turn:
|
|
495
569
|
|
|
496
|
-
- **Pre-stream failure** (the harness rejects before producing a stream
|
|
497
|
-
by a `FinishEvent(finishReason: 'error')`, and the returned
|
|
498
|
-
call in `try/catch` if you need to handle this in the caller.
|
|
499
|
-
- **In-stream failure** (the
|
|
500
|
-
yields the `ErrorEvent` (synthesizing one from a thrown exception if needed) and, if no `FinishEvent` was
|
|
501
|
-
emitted, appends a synthetic `FinishEvent(finishReason: 'error')` at the end. The
|
|
502
|
-
iterate the stream to observe the failure.
|
|
570
|
+
- **Pre-stream failure** (the harness rejects before producing a stream, or a settle call rejects before it routes the
|
|
571
|
+
decision): subscribers receive an `ErrorEvent` followed by a `FinishEvent(finishReason: 'error')`, and the returned
|
|
572
|
+
promise then rejects with the original error. Wrap the call in `try/catch` if you need to handle this in the caller.
|
|
573
|
+
- **In-stream failure** (the active `eventStream` yields an `error` event or the underlying generator throws): the
|
|
574
|
+
stream yields the `ErrorEvent` (synthesizing one from a thrown exception if needed) and, if no `FinishEvent` was
|
|
575
|
+
already emitted, appends a synthetic `FinishEvent(finishReason: 'error')` at the end. The originating `chat()` promise
|
|
576
|
+
resolves normally; iterate the stream to observe the failure.
|
|
503
577
|
- **Calling a disposed session** throws `AgentSDKError('DISPOSED')` synchronously without notifying subscribers.
|
|
504
578
|
|
|
505
579
|
### MCP Server Configuration
|
|
@@ -542,99 +616,158 @@ reparsing namespaced tool names. `annotations` is `undefined` when the source di
|
|
|
542
616
|
`requireToolApproval` accepts a `boolean` (`true` is shorthand for `'serial'`) or one of the mode strings exposed via
|
|
543
617
|
the `ToolApprovalMode` type alias (`'serial' | 'batch'`):
|
|
544
618
|
|
|
545
|
-
- **`'serial'`** (the safe default) —
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
approvals before settling.
|
|
619
|
+
- **`'serial'`** (the safe default) — `tool-approval-request` events surface one at a time. The next request (if any)
|
|
620
|
+
arrives later on the same `eventStream` after the consumer settles the current one.
|
|
621
|
+
- **`'batch'`** — when the model emits parallel `tool_use` blocks, all approval-requests for that batch surface on the
|
|
622
|
+
same `eventStream` together, so the consumer can render a batch-approval card. The consumer must gather all approvals
|
|
623
|
+
before settling — a `break`-on-first-approval loop will hang the turn because Mastra/Claude won't continue emitting
|
|
624
|
+
tool-results until the consumer has settled the entire batch.
|
|
552
625
|
|
|
553
626
|
The SDK also exports `resolveToolApprovalMode(value)` — the canonical
|
|
554
627
|
`boolean | ToolApprovalMode | undefined → ToolApprovalMode | undefined` normalizer harness implementations should use to
|
|
555
628
|
dispatch (`undefined` / `false` → `undefined`, `true` → `'serial'`, strings pass through). Reject unknown strings via
|
|
556
629
|
`throw` instead of reimplementing the boolean-vs-string branch — the helper's defensive throw catches `as any` consumers
|
|
557
|
-
passing invalid values that would otherwise silently degrade to "approval gating on but no
|
|
630
|
+
passing invalid values that would otherwise silently degrade to "approval gating on but no coordinator allocated."
|
|
631
|
+
|
|
632
|
+
#### Single stream per turn
|
|
633
|
+
|
|
634
|
+
Issue [#529](https://github.com/forcedotcom/agentic-dx/issues/529) settled the harness contract: **one `eventStream` per
|
|
635
|
+
chat turn.** Settle calls (`approveToolCall` / `declineToolCall` / `submitToolResult`) return `Promise<void>` and behave
|
|
636
|
+
as control messages on the existing turn; the post-resume events (`tool-result`, follow-up `text-delta`, `step-finish`,
|
|
637
|
+
terminal `finish`) flow through the **same** `eventStream` the `chat()` call returned. The consumer should iterate that
|
|
638
|
+
one stream until it sees a terminal `finish` event, calling `approveToolCall` / `declineToolCall` / `submitToolResult`
|
|
639
|
+
in-line as approval-requests / consumer-tool-calls arrive.
|
|
558
640
|
|
|
559
|
-
|
|
560
|
-
> call `approveToolCall`/`declineToolCall`). _Pattern B_ = "return-on-first-approval" (settle the first approval
|
|
561
|
-
> mid-iteration, then re-iterate the continuation stream). `'serial'` works with either; `'batch'` requires Pattern A.
|
|
641
|
+
#### Pattern: settle in-line, continue iterating
|
|
562
642
|
|
|
563
|
-
|
|
643
|
+
The same shape works for `'serial'` and `'batch'` mode:
|
|
564
644
|
|
|
565
645
|
```typescript
|
|
566
646
|
const { eventStream } = await session.chat('Run the deployment', {
|
|
567
|
-
requireToolApproval: true, // or 'serial'
|
|
647
|
+
requireToolApproval: true, // or 'serial' / 'batch'
|
|
568
648
|
});
|
|
569
649
|
|
|
570
650
|
for await (const event of eventStream) {
|
|
571
651
|
if (event.type === 'tool-approval-request') {
|
|
572
652
|
const approved = await promptUser(event);
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
// process continuation
|
|
653
|
+
if (approved) {
|
|
654
|
+
await session.approveToolCall(event.toolCall.toolCallId);
|
|
655
|
+
} else {
|
|
656
|
+
await session.declineToolCall(event.toolCall.toolCallId);
|
|
578
657
|
}
|
|
579
|
-
break
|
|
658
|
+
// Do NOT break — keep iterating; tool-result and follow-up text arrive on the same stream.
|
|
659
|
+
} else if (event.type === 'tool-call' && event.serverName === undefined && isConsumerTool(event.toolName)) {
|
|
660
|
+
// Consumer-executed tool: run locally, return the result via submitToolResult.
|
|
661
|
+
const result = await runLocally(event.args);
|
|
662
|
+
await session.submitToolResult({
|
|
663
|
+
toolCallId: event.toolCallId,
|
|
664
|
+
toolName: event.toolName,
|
|
665
|
+
result,
|
|
666
|
+
});
|
|
667
|
+
} else if (event.type === 'text-delta') {
|
|
668
|
+
process.stdout.write(event.text);
|
|
669
|
+
} else if (event.type === 'finish') {
|
|
670
|
+
// Don't break on `'error'` directly — the SDK invariant guarantees an
|
|
671
|
+
// `error` event is followed by a synthetic `FinishEvent('error')` so
|
|
672
|
+
// the loop exits naturally on the next iteration. Breaking on `error`
|
|
673
|
+
// would finalize the iterator before the synthetic `finish` lands and
|
|
674
|
+
// bypass any per-turn cleanup the harness wires onto the source's
|
|
675
|
+
// close/return path.
|
|
676
|
+
break;
|
|
580
677
|
}
|
|
581
678
|
}
|
|
582
679
|
```
|
|
583
680
|
|
|
584
|
-
#### Pattern
|
|
681
|
+
#### Pattern variant: batch-collect approvals before settling
|
|
682
|
+
|
|
683
|
+
Same single-stream loop, just gather the batch before deciding (useful for "approve these N tools?" UI cards under
|
|
684
|
+
`requireToolApproval: 'batch'`):
|
|
585
685
|
|
|
586
686
|
```typescript
|
|
587
687
|
const { eventStream } = await session.chat('Run the deployment', {
|
|
588
688
|
requireToolApproval: 'batch',
|
|
589
689
|
});
|
|
590
690
|
|
|
591
|
-
const
|
|
691
|
+
const pendingBatch: ToolApprovalRequestEvent[] = [];
|
|
692
|
+
|
|
592
693
|
for await (const event of eventStream) {
|
|
593
694
|
if (event.type === 'tool-approval-request') {
|
|
594
|
-
|
|
695
|
+
pendingBatch.push(event);
|
|
696
|
+
// Heuristic: settle the batch on a quiet window OR after the expected count.
|
|
697
|
+
// Don't break — the harness needs us to settle so it can continue.
|
|
698
|
+
continue;
|
|
595
699
|
}
|
|
596
|
-
if (event.type === '
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
const decisions = await promptUserForBatch(
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
700
|
+
if (event.type === 'tool-call') {
|
|
701
|
+
// The harness emits `tool-approval-request` BEFORE `tool-call` in batch mode.
|
|
702
|
+
// Once we see a tool-call, the batch for this assistant message is complete.
|
|
703
|
+
if (pendingBatch.length > 0) {
|
|
704
|
+
const decisions = await promptUserForBatch(pendingBatch);
|
|
705
|
+
await Promise.all(
|
|
706
|
+
pendingBatch.map((req) =>
|
|
707
|
+
decisions.get(req.toolCall.toolCallId)
|
|
708
|
+
? session.approveToolCall(req.toolCall.toolCallId)
|
|
709
|
+
: session.declineToolCall(req.toolCall.toolCallId),
|
|
710
|
+
),
|
|
711
|
+
);
|
|
712
|
+
pendingBatch.length = 0;
|
|
713
|
+
}
|
|
714
|
+
} else if (event.type === 'text-delta') {
|
|
715
|
+
process.stdout.write(event.text);
|
|
716
|
+
} else if (event.type === 'finish') {
|
|
717
|
+
// Don't break on `'error'` directly — the SDK invariant guarantees an
|
|
718
|
+
// `error` event is followed by a synthetic `FinishEvent('error')` so
|
|
719
|
+
// the loop exits naturally on the next iteration. Breaking on `error`
|
|
720
|
+
// would finalize the iterator before the synthetic `finish` lands and
|
|
721
|
+
// bypass any per-turn cleanup the harness wires onto the source's
|
|
722
|
+
// close/return path.
|
|
723
|
+
break;
|
|
610
724
|
}
|
|
611
725
|
}
|
|
612
726
|
```
|
|
613
727
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
`
|
|
617
|
-
|
|
618
|
-
|
|
728
|
+
> **AsyncIterator gotcha:** the `for await` loop is the supported way to drain `eventStream`. If you instead drive the
|
|
729
|
+
> iterator manually (`iter.next()`), call **at most one** `next()` at a time and `await` the result before the next
|
|
730
|
+
> call. Calling `iter.next()` twice without awaiting the first orphans the first request — JS settles `.next()` calls
|
|
731
|
+
> FIFO, so the next yielded value satisfies the orphaned request and is invisible to the caller. Stick to `for await`
|
|
732
|
+
> unless you have a specific reason not to.
|
|
619
733
|
|
|
620
734
|
### Consumer-Executed Tools
|
|
621
735
|
|
|
736
|
+
Tools declared via `AgentConfig.tools` (no `execute` function) run on the consumer side: the model emits a `tool-call`
|
|
737
|
+
event, the consumer runs the tool locally, then calls `session.submitToolResult(...)` to feed the result back.
|
|
738
|
+
`submitToolResult` returns `Promise<void>`; the model's follow-up turn (and any subsequent `tool-call`s) flows through
|
|
739
|
+
the same `eventStream`:
|
|
740
|
+
|
|
622
741
|
```typescript
|
|
742
|
+
const { eventStream } = await session.chat('Look up the customer record for tenant T-42.');
|
|
743
|
+
|
|
623
744
|
for await (const event of eventStream) {
|
|
624
|
-
if (event.type === 'tool-call' && event.toolName === '
|
|
745
|
+
if (event.type === 'tool-call' && event.toolName === 'lookup_customer') {
|
|
625
746
|
const result = await runLocally(event.args);
|
|
626
|
-
|
|
747
|
+
await session.submitToolResult({
|
|
627
748
|
toolCallId: event.toolCallId,
|
|
628
749
|
toolName: event.toolName,
|
|
629
750
|
result,
|
|
630
751
|
});
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
752
|
+
// Do NOT break — the model's follow-up turn (text, more tool-calls, finish) arrives on the same stream.
|
|
753
|
+
} else if (event.type === 'text-delta') {
|
|
754
|
+
process.stdout.write(event.text);
|
|
755
|
+
} else if (event.type === 'finish') {
|
|
756
|
+
// Don't break on `'error'` directly — the SDK invariant guarantees an
|
|
757
|
+
// `error` event is followed by a synthetic `FinishEvent('error')` so
|
|
758
|
+
// the loop exits naturally on the next iteration. Breaking on `error`
|
|
759
|
+
// would finalize the iterator before the synthetic `finish` lands and
|
|
760
|
+
// bypass any per-turn cleanup the harness wires onto the source's
|
|
761
|
+
// close/return path.
|
|
762
|
+
break;
|
|
634
763
|
}
|
|
635
764
|
}
|
|
636
765
|
```
|
|
637
766
|
|
|
767
|
+
When `requireToolApproval: true` is also set, consumer-executed tools bypass the approval gate by construction (per
|
|
768
|
+
`StreamOptions.requireToolApproval` JSDoc). They surface as a normal `tool-call` event without a preceding
|
|
769
|
+
`tool-approval-request`. Built-in / MCP tools still gate normally.
|
|
770
|
+
|
|
638
771
|
### Connectivity Resolution
|
|
639
772
|
|
|
640
773
|
#### `ResolvedConnectivity`
|
|
@@ -669,7 +802,11 @@ Returns `true` if the URL matches a Salesforce Hosted MCP Server endpoint (prod,
|
|
|
669
802
|
|
|
670
803
|
### Re-exported from `@salesforce/llm-gateway-sdk`
|
|
671
804
|
|
|
672
|
-
- `
|
|
805
|
+
- `Model` — abstract base class. Returned by `Models.getByName(...)` and accepted as an `AgentConfig.modelId` value.
|
|
806
|
+
- `ModelName` — enum of in-tree model identifiers.
|
|
807
|
+
- `createClaudeModel(gatewayId, overrides?)` — escape-hatch factory for opting into a Bedrock-Anthropic Claude variant
|
|
808
|
+
the SDK has not released yet (`AgentConfig.modelId` accepts the returned instance directly).
|
|
809
|
+
- `ClaudeModelOverrides` — optional caps for `createClaudeModel`.
|
|
673
810
|
- `SfApiEnv` — Salesforce API environment enum (`dev`, `perf`, `prod`, `stage`, `test`)
|
|
674
811
|
- `inferSfApiEnv(instanceUrl, options?)` — maps an instance URL to a `SfApiEnv`. Re-exported from
|
|
675
812
|
`@salesforce/agentic-common` for consumers that need the mapping without an `OrgConnection` (e.g. building a
|
|
@@ -768,15 +905,33 @@ The SDK ships no harness implementation. Consumers pick one by passing a `Harnes
|
|
|
768
905
|
[`@salesforce/sfdx-agent-harness-mastra`](../sfdx-agent-harness-mastra); additional harnesses can ship as independent
|
|
769
906
|
npm packages that depend on this SDK as a `peerDependency`.
|
|
770
907
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
908
|
+
### Two import surfaces
|
|
909
|
+
|
|
910
|
+
This package publishes two ESM entry points:
|
|
911
|
+
|
|
912
|
+
- **`@salesforce/sfdx-agent-sdk`** — consumer surface. Everything an application calling `createAgentManager` needs:
|
|
913
|
+
`Agent`, `ChatSession`, `ChatEvent`, `MessagePart`, `AgentConfig`, `AgentSDKError`, etc. The harness contract
|
|
914
|
+
interfaces (`AgentHarness`, `HarnessFactory`, `WithAgentConfig`, `ConfigOf`) also appear here as **type-only**
|
|
915
|
+
references so consumer code can write `AgentManager<H extends AgentHarness>` without reaching into the
|
|
916
|
+
harness-implementation surface.
|
|
917
|
+
- **`@salesforce/sfdx-agent-sdk/harness`** — harness-implementation surface. Harness authors import the contract types
|
|
918
|
+
and the helpers from here. Consumer applications cannot reach these symbols from the bare specifier — the bundler / TS
|
|
919
|
+
resolver errors at the import site, which is the right outcome for any consumer who reaches for a primitive they don't
|
|
920
|
+
need.
|
|
921
|
+
|
|
922
|
+
> **Build-tool requirement:** the `/harness` subpath is resolved via the `package.json` `exports` field, which requires
|
|
923
|
+
> `moduleResolution: "node16" | "nodenext" | "bundler"` in `tsconfig.json`. Legacy `"node"` resolution silently won't
|
|
924
|
+
> see the subpath. Modern bundlers (Vite, esbuild, Webpack 5+, tsup, Rollup with `@rollup/plugin-node-resolve` v15+)
|
|
925
|
+
> resolve it natively. This is a harness-author concern only; consumer applications never touch the subpath.
|
|
926
|
+
|
|
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. |
|
|
780
935
|
|
|
781
936
|
Minimal skeleton:
|
|
782
937
|
|
|
@@ -786,7 +941,7 @@ import {
|
|
|
786
941
|
type HarnessFactory,
|
|
787
942
|
HarnessBusOwner,
|
|
788
943
|
SUPPORTED_PROTOCOL_VERSIONS,
|
|
789
|
-
} from '@salesforce/sfdx-agent-sdk';
|
|
944
|
+
} from '@salesforce/sfdx-agent-sdk/harness';
|
|
790
945
|
|
|
791
946
|
class MyHarness implements AgentHarness {
|
|
792
947
|
readonly harnessId = 'my-harness';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type JSONWebToken, type LLMGatewayClient, type LLMGatewayClientFactory } from '@salesforce/llm-gateway-sdk';
|
|
1
|
+
import { Model, type JSONWebToken, type LLMGatewayClient, type LLMGatewayClientFactory } from '@salesforce/llm-gateway-sdk';
|
|
2
2
|
import { type OrgConnection, type OrgConnectionFactory } from '@salesforce/agentic-common';
|
|
3
3
|
import type { AgentConfig } from './harness/harness-config.js';
|
|
4
4
|
/**
|
|
@@ -60,3 +60,18 @@ export declare class DefaultAgentConnectivityResolver implements AgentConnectivi
|
|
|
60
60
|
*/
|
|
61
61
|
resolve(projectRoot: string, config: AgentConfig): Promise<ResolvedConnectivity>;
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Resolves an `AgentConfig.modelId` value (which may be a {@link ModelName} enum value, a
|
|
65
|
+
* pre-built {@link Model} instance, or `undefined`) to a concrete {@link Model}.
|
|
66
|
+
*
|
|
67
|
+
* The enum branch goes through the strict {@link Models.getByName} registry; the live
|
|
68
|
+
* instance branch passes the consumer-built model through unchanged. A persisted-and-restored
|
|
69
|
+
* `Model` instance arrives here as a plain object (the JSON round-trip drops its prototype),
|
|
70
|
+
* and is rehydrated via {@link createClaudeModel} for Bedrock-Anthropic Claude variants — the
|
|
71
|
+
* single use case the consumer-built escape hatch was added for. Any other persisted shape is
|
|
72
|
+
* a programming error and throws.
|
|
73
|
+
*
|
|
74
|
+
* Exported for use by `Agent.updateAgentConfig`, which performs the same resolution when
|
|
75
|
+
* comparing previous and next models without re-running the full connectivity resolver.
|
|
76
|
+
*/
|
|
77
|
+
export declare function resolveAgentConfigModel(modelId: AgentConfig['modelId']): Model;
|