@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 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 | Description |
156
- | ------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
157
- | `getId` | `() => string` | Session/thread identifier. |
158
- | `chat` | `(message: string, options?: ChatOptions) => Promise<ChatStreamResult>` | Send a message and stream the response. |
159
- | `submitToolResult` | `(toolResult: ToolResultInfo) => Promise<ChatStreamResult>` | Return a consumer-executed tool result and resume the stream. |
160
- | `approveToolCall` | `(toolCallId: string, options?: { remember?: boolean }) => Promise<ChatStreamResult>` | Approve a pending tool call. |
161
- | `declineToolCall` | `(toolCallId: string) => Promise<ChatStreamResult>` | Decline a pending tool call. |
162
- | `getMessageHistory` | `() => Promise<Message[]>` | Retrieve all messages in chronological order. |
163
- | `clearHistory` | `() => Promise<void>` | Delete all messages. |
164
- | `addContext` | `(message: string \| Message[]) => Promise<void>` | Inject context without triggering an LLM response. |
165
- | `subscribe` | `(callback: (event: ChatEvent) => void) => void` | Register a real-time event listener. |
166
- | `unsubscribe` | `(callback: (event: ChatEvent) => void) => void` | Remove a listener. |
167
- | `onTelemetry` | `(callback: TelemetryEventCallback) => Unsubscribe` | Subscribe to telemetry scoped to this session. |
168
- | `onLog` | `(callback: (record: LogRecord) => void) => Unsubscribe` | Subscribe to logs scoped to this session. |
169
- | `dispose` | `() => void` | Release session-level event resources. Idempotent. |
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()`, `submitToolResult()`, `approveToolCall()`, and `declineToolCall()`.
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
- | `unmapped-chunk` | `chunkType`, `rawChunk` | Unrecognized harness event, preserved for observability. |
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 | Description |
203
- | --------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
204
- | `orgAlias?` | `string` | Salesforce org alias or username. Falls back to project/default org. |
205
- | `modelId?` | `ModelName` | LLM model identifier (e.g. `'llmgateway__OpenAIGPT5'`). |
206
- | `name?` | `string` | Human-readable agent name. |
207
- | `description?` | `string` | Agent purpose description. |
208
- | `instructions?` | `string` | System instructions for the agent. |
209
- | `tools?` | `ToolDefinition[]` | Consumer-executed tool schemas. |
210
- | `mcpServers?` | `MCPConfiguration` | MCP server connections. |
211
- | `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. |
212
- | `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. |
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
- The four streaming methods on `ChatSession` `chat()`, `submitToolResult()`, `approveToolCall()`, and
493
- `declineToolCall()` share a single failure contract. Subscribers registered via `subscribe()` are guaranteed to
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): subscribers receive an `ErrorEvent` followed
497
- by a `FinishEvent(finishReason: 'error')`, and the returned promise then rejects with the original error. Wrap the
498
- call in `try/catch` if you need to handle this in the caller.
499
- - **In-stream failure** (the stream yields an `error` event or the underlying generator throws): the `eventStream`
500
- yields the `ErrorEvent` (synthesizing one from a thrown exception if needed) and, if no `FinishEvent` was already
501
- emitted, appends a synthetic `FinishEvent(finishReason: 'error')` at the end. The returned promise resolves normally;
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) — each stream surfaces one `tool-approval-request` at a time. The next request, if
546
- any, appears on the continuation stream returned by `approveToolCall` / `declineToolCall`. Works with both consumer
547
- iterator patterns below.
548
- - **`'batch'`** when the model emits parallel `tool_use` blocks, all approval-requests surface on the same stream so
549
- the consumer can render a batch-approval card. **Pattern A iterators only** — a `break`-on-first-approval loop will
550
- miss the subsequent approvals and hang the chat. Opt into `'batch'` only after the consumer iterator collects all
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 broker allocated."
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
- > **Glossary:** _Pattern A_ = "collect-all-then-settle" (iterate until natural park, gather all approval-requests, then
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
- #### Pattern B — return on first approval (works with `'serial'`)
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
- const continuation = approved
574
- ? await session.approveToolCall(event.toolCall.toolCallId)
575
- : await session.declineToolCall(event.toolCall.toolCallId);
576
- for await (const e of continuation.eventStream) {
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; // safe: serial mode surfaces at most one approval per stream
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 A collect all approvals, then settle (required for `'batch'`)
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 requests: ToolApprovalRequestEvent[] = [];
691
+ const pendingBatch: ToolApprovalRequestEvent[] = [];
692
+
592
693
  for await (const event of eventStream) {
593
694
  if (event.type === 'tool-approval-request') {
594
- requests.push(event); // do NOT break — collect the whole parallel batch
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 === 'finish' || event.type === 'error') break;
597
- }
598
-
599
- // Render a batch-approval card; settle each decision.
600
- const decisions = await promptUserForBatch(requests);
601
- let continuation: ChatStreamResult | undefined;
602
- for (const event of requests) {
603
- continuation = decisions.get(event.toolCall.toolCallId)
604
- ? await session.approveToolCall(event.toolCall.toolCallId)
605
- : await session.declineToolCall(event.toolCall.toolCallId);
606
- }
607
- if (continuation) {
608
- for await (const e of continuation.eventStream) {
609
- // process the model's follow-up turn
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
- #### `textStream` and `'batch'` mode
615
-
616
- `ChatStreamResult.textStream` is empty on the initial stream when `requireToolApproval: 'batch'` surfaces approval
617
- requests the model emitted `tool_use` blocks, not text. Read text deltas from the continuation stream returned by
618
- `approveToolCall` / `declineToolCall` (the model's follow-up turn after tool execution).
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 === 'my-tool') {
745
+ if (event.type === 'tool-call' && event.toolName === 'lookup_customer') {
625
746
  const result = await runLocally(event.args);
626
- const continuation = await session.submitToolResult({
747
+ await session.submitToolResult({
627
748
  toolCallId: event.toolCallId,
628
749
  toolName: event.toolName,
629
750
  result,
630
751
  });
631
- for await (const e of continuation.eventStream) {
632
- // process continuation
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
- - `ModelName` — enum of supported model identifiers
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
- Harness authors implement two interfaces and can compose one helper class, all exported from this package:
772
-
773
- | Export | Role |
774
- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
775
- | `HarnessFactory<H>` | Construct a harness of type `H` bound to a storage root. Declares `harnessId` and `protocolVersion`. Default `H = AgentHarness`. |
776
- | `AgentHarness` | Runtime contract: agent / thread / stream / tool / message lifecycle. Declares its own `harnessId` and `protocolVersion`. |
777
- | `SUPPORTED_PROTOCOL_VERSIONS` | Readonly list of harness protocol versions this SDK accepts. `createAgentManager` checks both the factory and the constructed harness. |
778
- | `HarnessBusOwner` | Composition helper owning telemetry + log buses with `dispose()` semantics. Reuse it instead of reimplementing bus plumbing. |
779
- | `lowerStreamInput` | 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. |
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;