@salesforce/sfdx-agent-sdk 0.11.0 → 0.13.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
@@ -181,19 +181,19 @@ Returned by `chat()`, `submitToolResult()`, `approveToolCall()`, and `declineToo
181
181
 
182
182
  Discriminated union (`event.type`) of streaming events:
183
183
 
184
- | Type | Key Fields | Description |
185
- | ----------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
186
- | `start` | — | Stream has begun. |
187
- | `text-delta` | `text` | Incremental response text. |
188
- | `reasoning-delta` | `text` | Chain-of-thought fragment. |
189
- | `tool-call` | `toolCallId`, `toolName`, `args`, `annotations?`, `serverName?` | Tool invocation. `annotations` is the MCP-spec hints (`readOnlyHint`, `destructiveHint`, …) when the source declared them; `serverName` is set when the tool came from an MCP server. |
190
- | `tool-approval-request` | `toolCall: ToolCallInfo`, `annotations?`, `serverName?` | Engine requests approval before executing a tool. Same `annotations` / `serverName` semantics as `tool-call`. |
191
- | `tool-result` | `toolCallId`, `toolName`, `result`, `isError?`, `annotations?`, `serverName?` | Tool execution completed. Same `annotations` / `serverName` semantics as `tool-call`. |
192
- | `step-start` | `stepIndex` | New LLM invocation step began. |
193
- | `step-finish` | `stepIndex`, `finishReason`, `usage?` | Step completed with per-step token usage. |
194
- | `error` | `error`, `code?` | Mid-stream error (yielded, not thrown). |
195
- | `finish` | `finishReason`, `usage?` | Stream completed with aggregate token usage. |
196
- | `unmapped-chunk` | `chunkType`, `rawChunk` | Unrecognized harness event, preserved for observability. |
184
+ | Type | Key Fields | Description |
185
+ | ----------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
186
+ | `start` | — | Stream has begun. |
187
+ | `text-delta` | `text` | Incremental response text. |
188
+ | `reasoning-delta` | `text` | Chain-of-thought fragment. |
189
+ | `tool-call` | `toolCallId`, `toolName`, `args`, `annotations?`, `serverName?` | Tool invocation. `annotations` is the MCP-spec hints (`readOnlyHint`, `destructiveHint`, …) when the source declared them; `serverName` is set when the tool came from an MCP server. |
190
+ | `tool-approval-request` | `toolCall: ToolCallInfo`, `annotations?`, `serverName?` | Engine requests approval before executing a tool. Same `annotations` / `serverName` semantics as `tool-call`. |
191
+ | `tool-result` | `toolCallId`, `toolName`, `result`, `isError?`, `error?`, `annotations?`, `serverName?` | Tool execution completed. `error` is present when `isError` is true (best-effort: harnesses may synthesize an `Error` from a string payload, so `error.stack` is not guaranteed to point at the tool's throw site; the field may be absent on empty error payloads). Same `annotations` / `serverName` semantics as `tool-call`. |
192
+ | `step-start` | `stepIndex` | New LLM invocation step began. |
193
+ | `step-finish` | `stepIndex`, `finishReason`, `usage?` | Step completed with per-step token usage. |
194
+ | `error` | `error`, `code?` | Mid-stream error (yielded, not thrown). |
195
+ | `finish` | `finishReason`, `usage?` | Stream completed with aggregate token usage. |
196
+ | `unmapped-chunk` | `chunkType`, `rawChunk` | Unrecognized harness event, preserved for observability. |
197
197
 
198
198
  ### Configuration Types
199
199
 
@@ -213,10 +213,10 @@ Discriminated union (`event.type`) of streaming events:
213
213
 
214
214
  #### `StreamOptions`
215
215
 
216
- | Field | Type | Description |
217
- | ---------------------- | ------------- | ------------------------------------------------------------------------ |
218
- | `abortSignal?` | `AbortSignal` | Abort the streaming operation. |
219
- | `requireToolApproval?` | `boolean` | When `true`, emits `tool-approval-request` before native tool execution. |
216
+ | Field | Type | Description |
217
+ | ---------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
218
+ | `abortSignal?` | `AbortSignal` | Abort the streaming operation. |
219
+ | `requireToolApproval?` | `boolean \| 'serial' \| 'batch'` | Gates native tool execution behind a `tool-approval-request` event. `true` / `'serial'` (the default) emits one approval per stream — safe for any iterator pattern. `'batch'` opts into parallel-approval UX: when the model emits parallel `tool_use` blocks, all approvals surface on the same stream so the consumer can render a batch approval card. **`'batch'` requires Pattern A iterators** (collect-all-approvals-then-settle); a `break`-on-first-approval loop will hang. See "Tool Approval Flow" below. |
220
220
 
221
221
  #### `MCPConfiguration`
222
222
 
@@ -231,6 +231,7 @@ type MCPStdioServerConfig = {
231
231
  env?: Record<string, string>;
232
232
  enabled?: boolean;
233
233
  timeout?: number;
234
+ alwaysLoad?: boolean;
234
235
  };
235
236
 
236
237
  // Remote server (HTTP/SSE)
@@ -240,9 +241,17 @@ type MCPRemoteServerConfig = {
240
241
  headers?: Record<string, string>;
241
242
  enabled?: boolean;
242
243
  timeout?: number;
244
+ alwaysLoad?: boolean;
243
245
  };
244
246
  ```
245
247
 
248
+ **`alwaysLoad`** opts a server's tool surface out of the active runtime's tool-search deferral. Default (`undefined` /
249
+ `false`) lets the runtime defer the server's tools behind a tool-search round-trip when the global tool surface is
250
+ large; `true` registers every tool from this server with the model up-front. Useful for small, discovery-critical
251
+ surfaces (≤ a few tools the model needs to find without prompting). The Claude harness honors the flag by stamping
252
+ `_meta['anthropic/alwaysLoad'] = true` on each forwarded tool (equivalent to `defer_loading: false` on the Claude API).
253
+ The Mastra harness eager-loads all MCP tools regardless, so the flag is a no-op there.
254
+
246
255
  #### `McpServerInfo`
247
256
 
248
257
  | Field | Type | Description |
@@ -269,16 +278,25 @@ event so subscribers can route on it without pattern-matching `error.message`.
269
278
 
270
279
  #### `McpToolInfo`
271
280
 
272
- Runtime metadata for a single MCP-discovered tool. Optional fields are populated when the underlying harness can supply
273
- them from its MCP client; harnesses whose runtime does not expose a given field leave it `undefined`. Consumers must
274
- treat every field except `name` as optional.
275
-
276
- | Field | Type | Description |
277
- | -------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
278
- | `name` | `string` | Tool name as exposed to the LLM, including any harness-applied namespacing. |
279
- | `description?` | `string` | Human-readable description of what the tool does. |
280
- | `inputSchema?` | `Record<string, unknown>` | Tool input parameters as a [**JSON Schema**](https://json-schema.org/) object (the MCP wire format). |
281
- | `annotations?` | [`McpToolAnnotations`](#mcptoolannotations) | Behavioral / UI-presentation hints declared by the MCP server. |
281
+ Runtime metadata for a single MCP-discovered tool. The required fields (`name`, `serverName`, `toolName`) are populated
282
+ by every harness; the optional fields are filled when the underlying harness can supply them from its MCP client and
283
+ left `undefined` otherwise. Consumers must treat every optional field as `undefined`-tolerant.
284
+
285
+ | Field | Type | Description |
286
+ | -------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
287
+ | `name` | `string` | Tool name as exposed to the LLM, including any harness-applied namespacing. **Format is harness-specific** — Mastra: `${server}_${tool}`; Claude: `mcp__${server}__${tool}`. See note below. |
288
+ | `serverName` | `string` | Logical MCP server name as configured in `AgentConfig.mcpServers`. Use together with `toolName` for harness-agnostic tool lookup. |
289
+ | `toolName` | `string` | Bare tool name as declared by the upstream MCP server's `tools/list` response (the un-namespaced form). Identical across harnesses for the same server. |
290
+ | `description?` | `string` | Human-readable description of what the tool does. |
291
+ | `inputSchema?` | `Record<string, unknown>` | Tool input parameters as a [**JSON Schema**](https://json-schema.org/) object (the MCP wire format). |
292
+ | `annotations?` | [`McpToolAnnotations`](#mcptoolannotations) | Behavioral / UI-presentation hints declared by the MCP server. |
293
+
294
+ **`name` is harness-specific; use `(serverName, toolName)` for cross-harness lookups.** `name` round-trips against
295
+ `tool-call` / `tool-result` / `tool-approval-request` events on the same harness, so consumers wiring UI to a single
296
+ harness can match on it. Code that needs to identify a tool across both Mastra and Claude — or against
297
+ `getMcpServerInfo()` regardless of which harness was constructed — must match on the `(serverName, toolName)` pair.
298
+ Don't regex `name` to recover the components, and don't try to construct it portably (no helper produces the right
299
+ format on every harness).
282
300
 
283
301
  **`inputSchema` is a JSON Schema object, not a Zod schema.** It is typed as `Record<string, unknown>` so this package
284
302
  incurs no `zod` or `@types/json-schema` dependency. If you need a Zod schema at runtime, convert with a library such as
@@ -335,6 +353,8 @@ type ToolResultInfo = {
335
353
  toolName: string;
336
354
  result: unknown;
337
355
  isError?: boolean;
356
+ /** Present when isError is true. Best-effort: error.stack is not guaranteed to point at the tool's throw site. */
357
+ error?: Error;
338
358
  };
339
359
  ```
340
360
 
@@ -348,9 +368,80 @@ type Message = {
348
368
  createdAt?: Date;
349
369
  };
350
370
 
351
- type MessagePart = TextPart | ReasoningPart | ToolCallPart | ToolResultPart;
371
+ type MessagePart = TextPart | ReasoningPart | ToolCallPart | ToolResultPart | ImagePart | FilePart;
372
+
373
+ // Multimodal input parts. `data` is base64-encoded bytes with no `data:` URI prefix.
374
+ type ImagePart = { type: 'image'; mimeType: 'image/png' | 'image/jpeg'; data: string; fileName?: string };
375
+ type FilePart = { type: 'file'; mimeType: 'application/pdf'; data: string; fileName?: string };
376
+ ```
377
+
378
+ #### Multimodal input
379
+
380
+ `ChatSession.chat()` (and the harness `stream()` it delegates to) accept either a plain string or a `MessagePart[]`. Use
381
+ the array form to send images or PDFs alongside text:
382
+
383
+ ```typescript
384
+ import { readFileSync } from 'node:fs';
385
+ import { AgentSDKError, AgentSDKErrorType } from '@salesforce/sfdx-agent-sdk';
386
+
387
+ // Attach a PNG image alongside a text prompt
388
+ const { eventStream } = await session.chat([
389
+ { type: 'text', text: 'What does this screenshot show?' },
390
+ {
391
+ type: 'image',
392
+ mimeType: 'image/png',
393
+ data: readFileSync('screenshot.png').toString('base64'),
394
+ fileName: 'screenshot.png',
395
+ },
396
+ ]);
397
+ for await (const event of eventStream) {
398
+ if (event.type === 'text-delta') process.stdout.write(event.text);
399
+ }
400
+
401
+ // Attach a PDF
402
+ await session.chat([
403
+ { type: 'text', text: 'Summarise the key findings in this report.' },
404
+ {
405
+ type: 'file',
406
+ mimeType: 'application/pdf',
407
+ data: readFileSync('report.pdf').toString('base64'),
408
+ fileName: 'q1-report.pdf',
409
+ },
410
+ ]);
411
+
412
+ // Inject multimodal context before a chat turn
413
+ await session.addContext([
414
+ {
415
+ id: 'ctx-screenshot',
416
+ role: 'user',
417
+ content: [
418
+ { type: 'text', text: 'Reference screenshot from the failing test run:' },
419
+ { type: 'image', mimeType: 'image/png', data: readFileSync('failure.png').toString('base64') },
420
+ ],
421
+ },
422
+ ]);
423
+ await session.chat('What component is throwing the null pointer?');
424
+
425
+ // Handle pre-stream validation errors
426
+ try {
427
+ await session.chat([{ type: 'image', mimeType: 'image/png', data: base64Png }]);
428
+ } catch (err) {
429
+ if (err instanceof AgentSDKError) {
430
+ if (err.type === AgentSDKErrorType.MULTIMODAL_NOT_SUPPORTED) {
431
+ // Model does not support file attachments, or the file violates a per-model cap
432
+ // (unsupported format, too large, too many files).
433
+ }
434
+ if (err.type === AgentSDKErrorType.INVALID_MESSAGE_CONTENT) {
435
+ // A part has an invalid type for user input (e.g. a bare tool-result part).
436
+ }
437
+ }
438
+ }
352
439
  ```
353
440
 
441
+ Only input parts (`text`, `image`, `file`) are valid — passing `tool-call` / `tool-result` parts is a programmer error.
442
+ Validation runs before the stream is opened: callers never receive a partial stream followed by an error. The per-model
443
+ formats and caps come from `Model.supportedFormats`, so a file is accepted or rejected identically across harnesses.
444
+
354
445
  ### Usage & Finish Types
355
446
 
356
447
  ```typescript
@@ -360,6 +451,8 @@ type UsageMetadata = {
360
451
  totalTokens?: number;
361
452
  reasoningTokens?: number;
362
453
  cachedInputTokens?: number;
454
+ /** Input tokens written to the provider cache. */
455
+ cacheWriteInputTokens?: number;
363
456
  };
364
457
 
365
458
  type FinishReason = 'stop' | 'length' | 'tool-calls' | 'content-filter' | 'error' | 'other';
@@ -370,15 +463,17 @@ type FinishReason = 'stop' | 'length' | 'tool-calls' | 'content-filter' | 'error
370
463
  The SDK throws `AgentSDKError` for predictable not-found and compatibility conditions. Each error has a `type` property
371
464
  from `AgentSDKErrorType`:
372
465
 
373
- | Type | Thrown By |
374
- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
375
- | `AGENT_NOT_FOUND` | `AgentManager.getAgent()`, `AgentManager.destroyAgent()` |
376
- | `CHAT_SESSION_NOT_FOUND` | `Agent.getChatSession()`, `Agent.destroyChatSession()`, `Agent.cloneChatSession()`, `Agent.compactChatSession()` |
377
- | `COMPACTION_FAILED` | `Agent.compactChatSession()` when the harness's underlying summarization call rejects. The original error is attached as `cause`; the source session is left intact. |
378
- | `DISPOSED` | `Agent` and `ChatSession` methods called after the owner has been destroyed |
379
- | `INCOMPATIBLE_HARNESS` | `createAgentManager()` when the factory advertises an unsupported `protocolVersion`, or the constructed harness reports a `protocolVersion` that differs from the factory's |
380
- | `MCP_SERVER_DISABLED` | `Agent.reconnectMcpServer()` when the named server is configured with `enabled: false` |
381
- | `MCP_SERVER_NOT_FOUND` | `Agent.reconnectMcpServer()` when the server name is not in the agent's `mcpServers` config |
466
+ | Type | Thrown By |
467
+ | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
468
+ | `AGENT_NOT_FOUND` | `AgentManager.getAgent()`, `AgentManager.destroyAgent()` |
469
+ | `CHAT_SESSION_NOT_FOUND` | `Agent.getChatSession()`, `Agent.destroyChatSession()`, `Agent.cloneChatSession()`, `Agent.compactChatSession()` |
470
+ | `COMPACTION_FAILED` | `Agent.compactChatSession()` when the harness's underlying summarization call rejects. The original error is attached as `cause`; the source session is left intact. |
471
+ | `DISPOSED` | `Agent` and `ChatSession` methods called after the owner has been destroyed |
472
+ | `INCOMPATIBLE_HARNESS` | `createAgentManager()` when the factory advertises an unsupported `protocolVersion`, or the constructed harness reports a `protocolVersion` that differs from the factory's |
473
+ | `INVALID_MESSAGE_CONTENT` | `ChatSession.chat()` / harness `stream()` when a message part is not valid as input (a `tool-call`/`tool-result` part, or non-base64-string file data) |
474
+ | `MCP_SERVER_DISABLED` | `Agent.reconnectMcpServer()` when the named server is configured with `enabled: false` |
475
+ | `MCP_SERVER_NOT_FOUND` | `Agent.reconnectMcpServer()` when the server name is not in the agent's `mcpServers` config |
476
+ | `MULTIMODAL_NOT_SUPPORTED` | `ChatSession.chat()` / harness `stream()` when a file fails pre-stream capability validation (unsupported format, too large, or too many files) |
382
477
 
383
478
  ```typescript
384
479
  import { AgentSDKError, AgentSDKErrorType } from '@salesforce/sfdx-agent-sdk';
@@ -444,33 +539,84 @@ consumers can branch on tool hints (e.g. auto-approve `readOnlyHint`) or group b
444
539
  reparsing namespaced tool names. `annotations` is `undefined` when the source did not declare them (per the MCP spec);
445
540
  `serverName` is `undefined` when the tool is not from an MCP server.
446
541
 
542
+ `requireToolApproval` accepts a `boolean` (`true` is shorthand for `'serial'`) or one of the mode strings exposed via
543
+ the `ToolApprovalMode` type alias (`'serial' | 'batch'`):
544
+
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.
552
+
553
+ The SDK also exports `resolveToolApprovalMode(value)` — the canonical
554
+ `boolean | ToolApprovalMode | undefined → ToolApprovalMode | undefined` normalizer harness implementations should use to
555
+ dispatch (`undefined` / `false` → `undefined`, `true` → `'serial'`, strings pass through). Reject unknown strings via
556
+ `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."
558
+
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.
562
+
563
+ #### Pattern B — return on first approval (works with `'serial'`)
564
+
447
565
  ```typescript
448
566
  const { eventStream } = await session.chat('Run the deployment', {
449
- requireToolApproval: true,
567
+ requireToolApproval: true, // or 'serial'
450
568
  });
451
569
 
452
570
  for await (const event of eventStream) {
453
571
  if (event.type === 'tool-approval-request') {
454
- if (event.annotations?.readOnlyHint) {
455
- // Safe read — auto-approve.
456
- const continuation = await session.approveToolCall(event.toolCall.toolCallId);
457
- for await (const e of continuation.eventStream) {
458
- // process continuation
459
- }
460
- } else {
461
- // Route to the user; group/label by event.serverName when set.
462
- const approved = await promptUser(event);
463
- const continuation = approved
464
- ? await session.approveToolCall(event.toolCall.toolCallId)
465
- : await session.declineToolCall(event.toolCall.toolCallId);
466
- for await (const e of continuation.eventStream) {
467
- // process continuation
468
- }
572
+ 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
469
578
  }
579
+ break; // safe: serial mode surfaces at most one approval per stream
580
+ }
581
+ }
582
+ ```
583
+
584
+ #### Pattern A — collect all approvals, then settle (required for `'batch'`)
585
+
586
+ ```typescript
587
+ const { eventStream } = await session.chat('Run the deployment', {
588
+ requireToolApproval: 'batch',
589
+ });
590
+
591
+ const requests: ToolApprovalRequestEvent[] = [];
592
+ for await (const event of eventStream) {
593
+ if (event.type === 'tool-approval-request') {
594
+ requests.push(event); // do NOT break — collect the whole parallel batch
595
+ }
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
470
610
  }
471
611
  }
472
612
  ```
473
613
 
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).
619
+
474
620
  ### Consumer-Executed Tools
475
621
 
476
622
  ```typescript
@@ -624,12 +770,13 @@ npm packages that depend on this SDK as a `peerDependency`.
624
770
 
625
771
  Harness authors implement two interfaces and can compose one helper class, all exported from this package:
626
772
 
627
- | Export | Role |
628
- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
629
- | `HarnessFactory<H>` | Construct a harness of type `H` bound to a storage root. Declares `harnessId` and `protocolVersion`. Default `H = AgentHarness`. |
630
- | `AgentHarness` | Runtime contract: agent / thread / stream / tool / message lifecycle. Declares its own `harnessId` and `protocolVersion`. |
631
- | `SUPPORTED_PROTOCOL_VERSIONS` | Readonly list of harness protocol versions this SDK accepts. `createAgentManager` checks both the factory and the constructed harness. |
632
- | `HarnessBusOwner` | Composition helper owning telemetry + log buses with `dispose()` semantics. Reuse it instead of reimplementing bus plumbing. |
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. |
633
780
 
634
781
  Minimal skeleton:
635
782
 
@@ -716,7 +863,7 @@ All variants share `{ type: <discriminant>, timestamp: Date }` plus the fields b
716
863
  | `chat-stream-completed` | `agentId`, `threadId`, `durationMs`, `usage?` |
717
864
  | `chat-stream-error` | `agentId`, `threadId`, `durationMs`, `error` |
718
865
  | `tool-execution-started` | `agentId`, `threadId`, `toolCallId`, `toolName`, `annotations?`, `serverName?` |
719
- | `tool-execution-completed` | `agentId`, `threadId`, `toolCallId`, `toolName`, `durationMs`, `isError`, `annotations?`, `serverName?` |
866
+ | `tool-execution-completed` | `agentId`, `threadId`, `toolCallId`, `toolName`, `durationMs`, `isError`, `error?`, `annotations?`, `serverName?` |
720
867
  | `tool-approval-requested` | `agentId`, `threadId`, `toolCallId`, `toolName`, `annotations?`, `serverName?` |
721
868
  | `tool-approval-resolved` | `agentId`, `threadId`, `toolCallId`, `approved` |
722
869
  | `mcp-server-discovery-started` | `agentId`, `serverName` |
@@ -3,7 +3,11 @@
3
3
  * See LICENSE.txt for license terms.
4
4
  */
5
5
  import { DefaultLLMGatewayClientFactory, Models, createJWTFromConnection, } from '@salesforce/llm-gateway-sdk';
6
- import { RealOrgConnectionFactory } from '@salesforce/agentic-common';
6
+ import { SfApiEnv, RealOrgConnectionFactory, } from '@salesforce/agentic-common';
7
+ // TODO(@W-22782317): Temporary workaround — only on prod orgs the LLM Gateway must
8
+ // route requests through AgentforceVibes rather than the default VibesService. Remove once a
9
+ // long-term feature ID configuration strategy is in place.
10
+ const PROD_ORG_FEATURE_ID = 'AgentforceVibes';
7
11
  /**
8
12
  * Default implementation of {@link AgentConnectivityResolver}.
9
13
  *
@@ -37,8 +41,11 @@ export class DefaultAgentConnectivityResolver {
37
41
  const orgConnection = config.orgAlias !== undefined
38
42
  ? await this.connectionFactory.createFromOrgAliasOrUsername(config.orgAlias)
39
43
  : await this.connectionFactory.createFromTargetOrg({ projectRoot });
40
- const orgJwt = await createJWTFromConnection(orgConnection);
41
- const llmGatewayClient = this.gatewayClientFactory.create(orgJwt, { env: orgConnection.getInferredSfApiEnv() });
44
+ // TODO(@W-22782317): Temporary workaround
45
+ const env = orgConnection.getInferredSfApiEnv();
46
+ const featureId = env === SfApiEnv.Prod ? PROD_ORG_FEATURE_ID : undefined;
47
+ const orgJwt = await createJWTFromConnection(orgConnection, { featureId });
48
+ const llmGatewayClient = this.gatewayClientFactory.create(orgJwt, { env });
42
49
  const modelName = config.modelId ?? Models.getDefault().name;
43
50
  llmGatewayClient.setModel(Models.getByName(modelName));
44
51
  return { llmGatewayClient, orgConnection, orgJwt };
@@ -3,7 +3,7 @@ import type { AgentHarness } from './harness/agent-harness.js';
3
3
  import type { StreamOptions } from './harness/harness-config.js';
4
4
  import type { TelemetrySlice } from './internal/telemetry-router.js';
5
5
  import type { ChatEvent, ChatStreamResult } from './types/events.js';
6
- import type { Message } from './types/messages.js';
6
+ import type { Message, MessagePart } from './types/messages.js';
7
7
  import type { TelemetryBus, TelemetryEventCallback } from './types/telemetry-events.js';
8
8
  import type { ToolResultInfo } from './types/tools.js';
9
9
  /**
@@ -50,10 +50,11 @@ export interface ChatSession {
50
50
  * On pre-stream failure, subscribers are notified with `ErrorEvent` + `FinishEvent` before
51
51
  * the returned promise rejects. See the interface-level "Failure handling" notes for details.
52
52
  *
53
- * @param message - User message as a plain string.
53
+ * @param message - User message: a plain string, or an array of {@link MessagePart}s for
54
+ * multi-part input (text plus `image` / `file` attachments).
54
55
  * @param options - Per-call options controlling mode, tools, model, etc.
55
56
  */
56
- chat(message: string, options?: ChatOptions): Promise<ChatStreamResult>;
57
+ chat(message: string | MessagePart[], options?: ChatOptions): Promise<ChatStreamResult>;
57
58
  /**
58
59
  * Feed the result of a **consumer-executed (client-side) tool** back into the
59
60
  * conversation and resume stream generation.
@@ -196,7 +197,7 @@ export declare class DefaultChatSession implements ChatSession {
196
197
  * - MUST notify listeners with `ErrorEvent` + `FinishEvent` and re-throw if the harness throws
197
198
  * before returning a stream result.
198
199
  */
199
- chat(message: string, options?: ChatOptions): Promise<ChatStreamResult>;
200
+ chat(message: string | MessagePart[], options?: ChatOptions): Promise<ChatStreamResult>;
200
201
  /**
201
202
  * @requirements
202
203
  * - MUST delegate to `this.harness.submitToolResult()`, passing `this.agentId` and `this.threadId`.
@@ -223,8 +224,11 @@ export declare class DefaultChatSession implements ChatSession {
223
224
  * continuations within the same chat turn — those are continuations of one logical turn
224
225
  * and the tool start timestamp lives on the session, not the stream. The tracking map is
225
226
  * cleared on every terminal `finish` ChatEvent so stale entries from one turn never bleed
226
- * into the next. An unmatched `tool-result` (no recorded `tool-call` in the session) is
227
- * still skipped.
227
+ * into the next, **except** when `finishReason === 'tool-calls'` under the
228
+ * parallel-approval UX (#447) those are per-tool continuation terminators, not turn
229
+ * boundaries; the matching `tool-result` may land on a later continuation stream and
230
+ * clearing here would lose its start timestamp. An unmatched `tool-result` (no recorded
231
+ * `tool-call` in the session) is still skipped.
228
232
  *
229
233
  * `chat-stream-started` is emitted by the entry-point method (chat / submitToolResult /
230
234
  * approveToolCall / declineToolCall) before the harness call so that pre-stream rejections
@@ -118,8 +118,11 @@ export class DefaultChatSession {
118
118
  * continuations within the same chat turn — those are continuations of one logical turn
119
119
  * and the tool start timestamp lives on the session, not the stream. The tracking map is
120
120
  * cleared on every terminal `finish` ChatEvent so stale entries from one turn never bleed
121
- * into the next. An unmatched `tool-result` (no recorded `tool-call` in the session) is
122
- * still skipped.
121
+ * into the next, **except** when `finishReason === 'tool-calls'` under the
122
+ * parallel-approval UX (#447) those are per-tool continuation terminators, not turn
123
+ * boundaries; the matching `tool-result` may land on a later continuation stream and
124
+ * clearing here would lose its start timestamp. An unmatched `tool-result` (no recorded
125
+ * `tool-call` in the session) is still skipped.
123
126
  *
124
127
  * `chat-stream-started` is emitted by the entry-point method (chat / submitToolResult /
125
128
  * approveToolCall / declineToolCall) before the harness call so that pre-stream rejections
@@ -141,7 +144,17 @@ export class DefaultChatSession {
141
144
  // Turn boundary — clear any unmatched in-flight tool starts so a stale
142
145
  // entry from one turn cannot pair with an unrelated tool-result on the
143
146
  // next turn. Matched pairs already removed their entry in `deriveToolTelemetry`.
144
- this.toolStartMs.clear();
147
+ //
148
+ // Skip the clear when `finishReason === 'tool-calls'`. Under the
149
+ // parallel-approval UX (#447), a single chat turn can produce multiple
150
+ // per-tool continuation streams, each terminated with a synthetic
151
+ // `finish('tool-calls')` to honor the "one finish per per-tool stream"
152
+ // invariant. Those mid-turn finishes are NOT turn boundaries — clearing
153
+ // would lose tracking for tool-calls whose tool-result lands on a later
154
+ // continuation stream. Only terminal `FinishReason`s end the turn.
155
+ if (event.finishReason !== 'tool-calls') {
156
+ this.toolStartMs.clear();
157
+ }
145
158
  }
146
159
  if (event.type === 'error')
147
160
  lastError = event.error;
@@ -370,6 +383,7 @@ export class DefaultChatSession {
370
383
  toolName: event.toolName,
371
384
  durationMs: this.clock.now().getTime() - start,
372
385
  isError: event.isError === true,
386
+ ...(event.error ? { error: event.error } : {}),
373
387
  ...(event.annotations ? { annotations: event.annotations } : {}),
374
388
  ...(event.serverName ? { serverName: event.serverName } : {}),
375
389
  });
package/dist/errors.d.ts CHANGED
@@ -4,8 +4,10 @@ export declare const AgentSDKErrorType: {
4
4
  readonly COMPACTION_FAILED: "COMPACTION_FAILED";
5
5
  readonly DISPOSED: "DISPOSED";
6
6
  readonly INCOMPATIBLE_HARNESS: "INCOMPATIBLE_HARNESS";
7
+ readonly INVALID_MESSAGE_CONTENT: "INVALID_MESSAGE_CONTENT";
7
8
  readonly MCP_SERVER_DISABLED: "MCP_SERVER_DISABLED";
8
9
  readonly MCP_SERVER_NOT_FOUND: "MCP_SERVER_NOT_FOUND";
10
+ readonly MULTIMODAL_NOT_SUPPORTED: "MULTIMODAL_NOT_SUPPORTED";
9
11
  readonly NOT_SUPPORTED: "NOT_SUPPORTED";
10
12
  };
11
13
  export type AgentSDKErrorType = (typeof AgentSDKErrorType)[keyof typeof AgentSDKErrorType];
package/dist/errors.js CHANGED
@@ -8,8 +8,10 @@ export const AgentSDKErrorType = {
8
8
  COMPACTION_FAILED: 'COMPACTION_FAILED',
9
9
  DISPOSED: 'DISPOSED',
10
10
  INCOMPATIBLE_HARNESS: 'INCOMPATIBLE_HARNESS',
11
+ INVALID_MESSAGE_CONTENT: 'INVALID_MESSAGE_CONTENT',
11
12
  MCP_SERVER_DISABLED: 'MCP_SERVER_DISABLED',
12
13
  MCP_SERVER_NOT_FOUND: 'MCP_SERVER_NOT_FOUND',
14
+ MULTIMODAL_NOT_SUPPORTED: 'MULTIMODAL_NOT_SUPPORTED',
13
15
  NOT_SUPPORTED: 'NOT_SUPPORTED',
14
16
  };
15
17
  export class AgentSDKError extends Error {
@@ -1,7 +1,7 @@
1
1
  import type { LogRecord, Unsubscribe } from '@salesforce/agentic-common';
2
2
  import type { McpServerInfo } from '../mcp-config.js';
3
3
  import type { ChatStreamResult } from '../types/events.js';
4
- import type { Message } from '../types/messages.js';
4
+ import type { Message, MessagePart } from '../types/messages.js';
5
5
  import type { TelemetryEventCallback } from '../types/telemetry-events.js';
6
6
  import type { ToolResultInfo } from '../types/tools.js';
7
7
  import type { AgentConfig, HarnessAgentConfig, StreamOptions } from './harness-config.js';
@@ -203,10 +203,12 @@ export interface AgentHarness {
203
203
  *
204
204
  * @param agentId - ID of the agent to invoke.
205
205
  * @param threadId - ID of the conversation thread.
206
- * @param message - User message as a plain string.
206
+ * @param message - User message: a plain string, or an array of {@link MessagePart}s for
207
+ * multi-part input (text plus `image` / `file` attachments). Only input parts are valid here —
208
+ * passing `tool-call` / `tool-result` parts is a programmer error and harnesses reject it.
207
209
  * @param options - Per-call streaming options.
208
210
  */
209
- stream(agentId: string, threadId: string, message: string, options?: StreamOptions): Promise<ChatStreamResult>;
211
+ stream(agentId: string, threadId: string, message: string | MessagePart[], options?: StreamOptions): Promise<ChatStreamResult>;
210
212
  /**
211
213
  * Feed the result of a **consumer-executed (client-side) tool** back into the
212
214
  * conversation and resume stream generation. Implements the consumer-facing
@@ -85,6 +85,30 @@ export type HarnessAgentConfig = Omit<AgentConfig, 'orgAlias'> & {
85
85
  * `test/harness/harness-config.test.ts` that asserts unknown fields survive.
86
86
  */
87
87
  export declare function toHarnessConfig(config: AgentConfig, orgJwt?: JSONWebToken): HarnessAgentConfig;
88
+ /**
89
+ * Approval-mode selector for `StreamOptions.requireToolApproval`.
90
+ *
91
+ * Distinguishes the legacy "serial" UX (one approval per stream;
92
+ * consumer settles before the next is surfaced) from the parallel
93
+ * "batch" UX (all approval-requests for a parallel `tool_use` batch
94
+ * surface on the same stream so the consumer can render them as a
95
+ * batch approval card). See `requireToolApproval` for the safety
96
+ * note on choosing `batch`.
97
+ */
98
+ export type ToolApprovalMode = 'serial' | 'batch';
99
+ /**
100
+ * Resolves `StreamOptions.requireToolApproval` to its canonical mode:
101
+ * `undefined` (gating off), `'serial'`, or `'batch'`. Centralizes the
102
+ * boolean-vs-string normalization so harnesses don't duplicate the
103
+ * resolution logic.
104
+ *
105
+ * Semantics:
106
+ * - `undefined` / `false` → `undefined` (no gating).
107
+ * - `true` → `'serial'` (back-compat shorthand for the original `boolean` shape).
108
+ * - `'serial'` → `'serial'` (explicit, equivalent to `true`).
109
+ * - `'batch'` → `'batch'`.
110
+ */
111
+ export declare function resolveToolApprovalMode(requireToolApproval: boolean | ToolApprovalMode | undefined): ToolApprovalMode | undefined;
88
112
  /**
89
113
  * Per-call options controlling streaming behavior.
90
114
  */
@@ -92,16 +116,36 @@ export type StreamOptions = {
92
116
  /** Signal to abort the streaming operation. */
93
117
  abortSignal?: AbortSignal;
94
118
  /**
95
- * When `true`, the harness requires human approval before executing any
119
+ * When set, the harness requires human approval before executing any
96
120
  * native tool (e.g., MCP tools). The stream emits a `tool-approval-request`
97
121
  * event and suspends until the consumer calls `approveToolCall()` or
98
122
  * `declineToolCall()`.
99
123
  *
100
- * Does not affect consumer-executed tools (those defined via `AgentConfig.tools`
101
- * without an execute handler) — the consumer already controls execution for
102
- * those via `submitToolResult()`.
124
+ * Accepts a `boolean` (back-compatible shorthand) or one of the
125
+ * approval-mode strings:
126
+ *
127
+ * - **`true` or `'serial'`** (the safe default): each chat-stream
128
+ * surfaces ONE `tool-approval-request` at a time. The consumer
129
+ * settles the approval; the next `tool-approval-request` (if any)
130
+ * appears on the continuation stream. Identical to the SDK's
131
+ * behavior before parallel-approval UX (#447) — safe for consumers
132
+ * whose iterator returns on the first approval-request and
133
+ * re-iterates the continuation (Pattern B).
134
+ *
135
+ * - **`'batch'`**: when the model emits parallel `tool_use` blocks, the
136
+ * broker surfaces ALL approval-requests on the same stream so the
137
+ * consumer can render a batch approval UI ("Approve these N tools?").
138
+ * Consumers MUST iterate to natural park collecting approvals
139
+ * (Pattern A); a `break`-on-first-approval loop will miss the
140
+ * subsequent approvals on the same stream and the chat will hang.
141
+ * Only opt into `'batch'` after the consumer's iterator collects all
142
+ * approvals before settling.
143
+ *
144
+ * Does not affect consumer-executed tools (those defined via
145
+ * `AgentConfig.tools` without an execute handler) — the consumer
146
+ * already controls execution for those via `submitToolResult()`.
103
147
  */
104
- requireToolApproval?: boolean;
148
+ requireToolApproval?: boolean | ToolApprovalMode;
105
149
  /**
106
150
  * Maximum number of LLM call steps the agent may take per `stream()` invocation.
107
151
  * Each step is one LLM call (which may produce text, tool calls, or both).
@@ -24,6 +24,31 @@ export function toHarnessConfig(config, orgJwt) {
24
24
  const { orgAlias: _, ...rest } = config;
25
25
  return { ...rest, orgJwt };
26
26
  }
27
+ /**
28
+ * Resolves `StreamOptions.requireToolApproval` to its canonical mode:
29
+ * `undefined` (gating off), `'serial'`, or `'batch'`. Centralizes the
30
+ * boolean-vs-string normalization so harnesses don't duplicate the
31
+ * resolution logic.
32
+ *
33
+ * Semantics:
34
+ * - `undefined` / `false` → `undefined` (no gating).
35
+ * - `true` → `'serial'` (back-compat shorthand for the original `boolean` shape).
36
+ * - `'serial'` → `'serial'` (explicit, equivalent to `true`).
37
+ * - `'batch'` → `'batch'`.
38
+ */
39
+ export function resolveToolApprovalMode(requireToolApproval) {
40
+ if (requireToolApproval === undefined || requireToolApproval === false)
41
+ return undefined;
42
+ if (requireToolApproval === true)
43
+ return 'serial';
44
+ if (requireToolApproval === 'serial' || requireToolApproval === 'batch')
45
+ return requireToolApproval;
46
+ // Defensive: an `as any` consumer could pass an unknown string. Without
47
+ // this guard the value flows through to harness checks, which then
48
+ // silently degrade to "approval gating on but no broker allocated" and
49
+ // the chat hangs without an error a consumer can debug.
50
+ throw new Error(`Invalid requireToolApproval value: ${JSON.stringify(requireToolApproval)}. Expected boolean, 'serial', or 'batch'.`);
51
+ }
27
52
  /**
28
53
  * Default maximum steps for a single agent stream invocation.
29
54
  *
@@ -0,0 +1,33 @@
1
+ import { type MultimodalFile } from '@salesforce/llm-gateway-sdk';
2
+ import type { FilePart, ImagePart, MessagePart, TextPart } from '../types/messages.js';
3
+ /**
4
+ * The subset of {@link MessagePart} that is valid as user `stream()` input: plain `text` plus the
5
+ * multimodal `image` / `file` attachment parts. The recorded-turn parts (`reasoning`, `tool-call`,
6
+ * `tool-result`) are output artifacts of a previous turn and are rejected by {@link lowerStreamInput}.
7
+ */
8
+ export type InputMessagePart = TextPart | ImagePart | FilePart;
9
+ /**
10
+ * Shared `MessagePart[]` validation + lowering for `AgentHarness.stream()` implementations. Every
11
+ * harness that accepts multimodal input routes through this so the SDK guarantee — "a file is
12
+ * accepted or rejected identically regardless of harness" — is enforced in one place instead of
13
+ * re-derived (and left to drift) per harness. Only the per-part lowering is harness-specific; the
14
+ * file extraction, capability validation, error mapping, and input-part guard are not.
15
+ *
16
+ * Order of operations:
17
+ * 1. Collect `image` / `file` parts into a {@link MultimodalFile} list and hand them to
18
+ * `validateFiles` (per-model + global gateway caps). An {@link LLMGClientError} is mapped to
19
+ * `AgentSDKError(MULTIMODAL_NOT_SUPPORTED)`; any other throw propagates unchanged.
20
+ * 2. Lower each part via `mapPart`. A `reasoning` / `tool-call` / `tool-result` part is not valid
21
+ * stream input and throws `AgentSDKError(INVALID_MESSAGE_CONTENT)` before `mapPart` sees it.
22
+ *
23
+ * Files are validated before lowering so an over-cap attachment surfaces as `MULTIMODAL_NOT_SUPPORTED`
24
+ * even when the same message also carries a malformed part. Everything runs synchronously, so a
25
+ * failure rejects the harness's `stream()` promise pre-stream rather than mid-iteration.
26
+ *
27
+ * @param parts - the user message parts to validate and lower.
28
+ * @param validateFiles - per-harness file validation (e.g. `validateMultimodalFiles(files, model)`)
29
+ * that throws `LLMGClientError` on a cap/format violation and no-ops for text-only input.
30
+ * @param mapPart - lowers one validated input part into the harness's content-block shape.
31
+ * @returns the lowered blocks, in the original part order.
32
+ */
33
+ export declare function lowerStreamInput<TBlock>(parts: MessagePart[], validateFiles: (files: readonly MultimodalFile[]) => void, mapPart: (part: InputMessagePart) => TBlock): TBlock[];
@@ -0,0 +1,54 @@
1
+ /*
2
+ * Copyright 2026, Salesforce, Inc. All rights reserved.
3
+ * See LICENSE.txt for license terms.
4
+ */
5
+ import { LLMGClientError } from '@salesforce/llm-gateway-sdk';
6
+ import { AgentSDKError, AgentSDKErrorType } from '../errors.js';
7
+ /**
8
+ * Shared `MessagePart[]` validation + lowering for `AgentHarness.stream()` implementations. Every
9
+ * harness that accepts multimodal input routes through this so the SDK guarantee — "a file is
10
+ * accepted or rejected identically regardless of harness" — is enforced in one place instead of
11
+ * re-derived (and left to drift) per harness. Only the per-part lowering is harness-specific; the
12
+ * file extraction, capability validation, error mapping, and input-part guard are not.
13
+ *
14
+ * Order of operations:
15
+ * 1. Collect `image` / `file` parts into a {@link MultimodalFile} list and hand them to
16
+ * `validateFiles` (per-model + global gateway caps). An {@link LLMGClientError} is mapped to
17
+ * `AgentSDKError(MULTIMODAL_NOT_SUPPORTED)`; any other throw propagates unchanged.
18
+ * 2. Lower each part via `mapPart`. A `reasoning` / `tool-call` / `tool-result` part is not valid
19
+ * stream input and throws `AgentSDKError(INVALID_MESSAGE_CONTENT)` before `mapPart` sees it.
20
+ *
21
+ * Files are validated before lowering so an over-cap attachment surfaces as `MULTIMODAL_NOT_SUPPORTED`
22
+ * even when the same message also carries a malformed part. Everything runs synchronously, so a
23
+ * failure rejects the harness's `stream()` promise pre-stream rather than mid-iteration.
24
+ *
25
+ * @param parts - the user message parts to validate and lower.
26
+ * @param validateFiles - per-harness file validation (e.g. `validateMultimodalFiles(files, model)`)
27
+ * that throws `LLMGClientError` on a cap/format violation and no-ops for text-only input.
28
+ * @param mapPart - lowers one validated input part into the harness's content-block shape.
29
+ * @returns the lowered blocks, in the original part order.
30
+ */
31
+ export function lowerStreamInput(parts, validateFiles, mapPart) {
32
+ const files = [];
33
+ for (const part of parts) {
34
+ if (part.type === 'image' || part.type === 'file') {
35
+ files.push({ mimeType: part.mimeType, data: part.data });
36
+ }
37
+ }
38
+ try {
39
+ validateFiles(files);
40
+ }
41
+ catch (err) {
42
+ if (err instanceof LLMGClientError) {
43
+ throw new AgentSDKError(err.message, AgentSDKErrorType.MULTIMODAL_NOT_SUPPORTED, { cause: err });
44
+ }
45
+ throw err;
46
+ }
47
+ return parts.map((part) => {
48
+ if (part.type === 'text' || part.type === 'image' || part.type === 'file') {
49
+ return mapPart(part);
50
+ }
51
+ throw new AgentSDKError(`Message part of type "${part.type}" is not valid stream input; only text, image, and file parts are accepted.`, AgentSDKErrorType.INVALID_MESSAGE_CONTENT);
52
+ });
53
+ }
54
+ //# sourceMappingURL=stream-input.js.map
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- export type { Message, MessagePart } from './types/messages.js';
1
+ export type { Message, MessagePart, ImagePart, FilePart } from './types/messages.js';
2
2
  export type { ChatEvent, StartEvent, TextDeltaEvent, ReasoningDeltaEvent, ToolCallEvent, ToolApprovalRequestEvent, ToolResultEvent, StepStartEvent, StepFinishEvent, ErrorEvent, FinishEvent, ChatStreamResult, } from './types/events.js';
3
3
  export type { ToolDefinition, ToolCallInfo, ToolResultInfo } from './types/tools.js';
4
4
  export type { FinishReason, UsageMetadata } from './types/usage.js';
5
- export type { AgentConfig, HarnessAgentConfig, StreamOptions } from './harness/harness-config.js';
6
- export { DEFAULT_MAX_STEPS } from './harness/harness-config.js';
5
+ export type { AgentConfig, HarnessAgentConfig, StreamOptions, ToolApprovalMode } from './harness/harness-config.js';
6
+ export { DEFAULT_MAX_STEPS, resolveToolApprovalMode } from './harness/harness-config.js';
7
7
  export type { MCPConfiguration, MCPServerConfig, MCPStdioServerConfig, MCPRemoteServerConfig, McpServerInfo, McpServerErrorCategory, McpServerErrorDetail, McpToolInfo, McpToolAnnotations, } from './mcp-config.js';
8
8
  export { McpServerStatus } from './mcp-config.js';
9
9
  export { ModelName } from '@salesforce/llm-gateway-sdk';
@@ -15,6 +15,7 @@ export type { AgentConnectivityResolver, ResolvedConnectivity } from './agent-co
15
15
  export type { AgentHarness, HarnessFactory, WithAgentConfig, ConfigOf } from './harness/index.js';
16
16
  export { SUPPORTED_PROTOCOL_VERSIONS } from './harness/agent-harness.js';
17
17
  export { HarnessBusOwner } from './harness/harness-bus-owner.js';
18
+ export { lowerStreamInput, type InputMessagePart } from './harness/stream-input.js';
18
19
  export { AgentSDKError, AgentSDKErrorType } from './errors.js';
19
20
  export type { AgentCreatedEvent, AgentDestroyedEvent, ChatStreamCompletedEvent, ChatStreamErrorEvent, ChatStreamStartedEvent, ChatStreamTrigger, McpServerDiscoveryCompletedEvent, McpServerDiscoveryFailedEvent, McpServerDiscoveryStartedEvent, McpServerStatusChangedEvent, SessionCreatedEvent, SessionDestroyedEvent, TelemetryEvent, TelemetryEventCallback, ToolApprovalRequestedEvent, ToolApprovalResolvedEvent, ToolExecutionCompletedEvent, ToolExecutionStartedEvent, } from './types/telemetry-events.js';
20
21
  export type { LogLevel, LogRecord, Unsubscribe } from '@salesforce/agentic-common';
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Copyright 2026, Salesforce, Inc. All rights reserved.
3
3
  * See LICENSE.txt for license terms.
4
4
  */
5
- export { DEFAULT_MAX_STEPS } from './harness/harness-config.js';
5
+ export { DEFAULT_MAX_STEPS, resolveToolApprovalMode } from './harness/harness-config.js';
6
6
  export { McpServerStatus } from './mcp-config.js';
7
7
  export { ModelName } from '@salesforce/llm-gateway-sdk';
8
8
  export { inferSfApiEnv, SfApiEnv } from '@salesforce/agentic-common';
@@ -12,6 +12,7 @@ export {} from './agent.js';
12
12
  export {} from './chat-session.js';
13
13
  export { SUPPORTED_PROTOCOL_VERSIONS } from './harness/agent-harness.js';
14
14
  export { HarnessBusOwner } from './harness/harness-bus-owner.js';
15
+ export { lowerStreamInput } from './harness/stream-input.js';
15
16
  // ── Errors ───────────────────────────────────────────────────────────
16
17
  export { AgentSDKError, AgentSDKErrorType } from './errors.js';
17
18
  // ── MCP Auth ────────────────────────────────────────────────────────
@@ -32,8 +32,27 @@ export type MCPStdioServerConfig = {
32
32
  env?: Record<string, string>;
33
33
  /** Whether this server is enabled. Defaults to `true`. */
34
34
  enabled?: boolean;
35
- /** Timeout in milliseconds for connecting to the server. */
35
+ /** Timeout in milliseconds for individual requests to the server. */
36
36
  timeout?: number;
37
+ /**
38
+ * Opt the server's tool surface out of the active runtime's tool-search
39
+ * deferral. When `true`, every tool advertised by this server is
40
+ * registered with the model up-front instead of sitting behind a
41
+ * search/load round-trip. Useful for small, discovery-critical surfaces
42
+ * (e.g. ≤ 10 tools the model needs to find without prompting). Default
43
+ * (`undefined` / `false`): tools may be deferred when the active runtime
44
+ * enables tool search.
45
+ *
46
+ * **Harness behavior:**
47
+ * - **Claude harness** — sets `_meta['anthropic/alwaysLoad'] = true` on
48
+ * each tool the bridge forwards, equivalent to
49
+ * `defer_loading: false` on the API. Skill-bridge and consumer-tool
50
+ * tools are always-load regardless of this flag (see
51
+ * `@salesforce/sfdx-agent-harness-claude` ARCHITECTURE.md).
52
+ * - **Mastra harness** — no-op; Mastra eager-loads MCP tools at every
53
+ * turn already, so there's no deferral to opt out of.
54
+ */
55
+ alwaysLoad?: boolean;
37
56
  };
38
57
  /** MCP server accessible over HTTP/SSE at a remote URL. */
39
58
  export type MCPRemoteServerConfig = {
@@ -44,8 +63,13 @@ export type MCPRemoteServerConfig = {
44
63
  headers?: Record<string, string>;
45
64
  /** Whether this server is enabled. Defaults to `true`. */
46
65
  enabled?: boolean;
47
- /** Timeout in milliseconds for connecting to the server. */
66
+ /** Timeout in milliseconds for individual requests to the server. */
48
67
  timeout?: number;
68
+ /**
69
+ * Opt the server's tool surface out of the active runtime's tool-search
70
+ * deferral. See {@link MCPStdioServerConfig.alwaysLoad}.
71
+ */
72
+ alwaysLoad?: boolean;
49
73
  };
50
74
  /** Connection status of a single MCP server. */
51
75
  export declare enum McpServerStatus {
@@ -107,8 +131,36 @@ export type McpToolAnnotations = {
107
131
  * contract is a non-breaking additive change at that point.
108
132
  */
109
133
  export type McpToolInfo = {
110
- /** Tool name as exposed to the LLM, including any harness-applied namespacing. */
134
+ /**
135
+ * Tool name as exposed to the LLM, including any harness-applied namespacing.
136
+ *
137
+ * The format is **harness-specific**:
138
+ * - Mastra: `${serverName}_${toolName}`
139
+ * - Claude: `mcp__${serverName}__${toolName}`
140
+ *
141
+ * Treat `name` as the LLM-facing display string within a single harness —
142
+ * it round-trips against `tool-call` / `tool-result` /
143
+ * `tool-approval-request` events on the same harness, so consumers wiring
144
+ * UI off a single harness can match against it. Cross-harness consumer
145
+ * code that needs to identify a tool MUST use the {@link serverName} +
146
+ * {@link toolName} pair below; do NOT regex `name` to recover the
147
+ * components, and do NOT construct `name` portably (no helper produces
148
+ * the right format on every harness).
149
+ */
111
150
  name: string;
151
+ /**
152
+ * Logical MCP server name as configured in `AgentConfig.mcpServers` (the
153
+ * map key, not a URL or command). Use together with {@link toolName} for
154
+ * harness-agnostic tool lookups against `getMcpServerInfo()`.
155
+ */
156
+ serverName: string;
157
+ /**
158
+ * Bare tool name as declared by the upstream MCP server's `tools/list`
159
+ * response — the un-namespaced form, identical across harnesses for the
160
+ * same server. Use together with {@link serverName} for harness-agnostic
161
+ * tool lookups.
162
+ */
163
+ toolName: string;
112
164
  /** Human-readable description of what the tool does. */
113
165
  description?: string;
114
166
  /**
@@ -29,9 +29,10 @@ export type Message = {
29
29
  };
30
30
  /**
31
31
  * Discriminated union of message content parts.
32
- * Aligned with AI SDK `TextPart`, `ReasoningPart`, `ToolCallPart`, `ToolResultPart`.
32
+ * Aligned with AI SDK `TextPart`, `ReasoningPart`, `ToolCallPart`, `ToolResultPart`, plus the
33
+ * multimodal `ImagePart` / `FilePart` inputs.
33
34
  */
34
- export type MessagePart = TextPart | ReasoningPart | ToolCallPart | ToolResultPart;
35
+ export type MessagePart = TextPart | ReasoningPart | ToolCallPart | ToolResultPart | ImagePart | FilePart;
35
36
  /** A plain text content segment. */
36
37
  export type TextPart = {
37
38
  type: 'text';
@@ -58,3 +59,35 @@ export type ToolCallPart = ToolCallInfo & {
58
59
  export type ToolResultPart = ToolResultInfo & {
59
60
  type: 'tool-result';
60
61
  };
62
+ /**
63
+ * An image attached to a user message. Field names are camelCase on the SDK's public surface;
64
+ * harnesses translate them to the runtime/wire shape they need (e.g. the Mastra gateway maps
65
+ * `mimeType` → the gateway's `ChatMessageFile.mimeType`, the Claude harness maps it onto an
66
+ * Anthropic `image` content block).
67
+ *
68
+ * `data` is the base64-encoded file bytes with no `data:` URI prefix. v1 supports base64 input only.
69
+ */
70
+ export type ImagePart = {
71
+ type: 'image';
72
+ /** MIME type of the image. */
73
+ mimeType: 'image/png' | 'image/jpeg';
74
+ /** Base64-encoded image bytes (no `data:` URI prefix). */
75
+ data: string;
76
+ /** Optional human-readable filename, surfaced to providers that display it. */
77
+ fileName?: string;
78
+ };
79
+ /**
80
+ * A file (currently PDF only) attached to a user message. See {@link ImagePart} for the
81
+ * camelCase-vs-runtime translation contract.
82
+ *
83
+ * `data` is the base64-encoded file bytes with no `data:` URI prefix. v1 supports base64 input only.
84
+ */
85
+ export type FilePart = {
86
+ type: 'file';
87
+ /** MIME type of the file. */
88
+ mimeType: 'application/pdf';
89
+ /** Base64-encoded file bytes (no `data:` URI prefix). */
90
+ data: string;
91
+ /** Optional human-readable filename, surfaced to providers that display it. */
92
+ fileName?: string;
93
+ };
@@ -63,6 +63,8 @@ export type ToolExecutionCompletedEvent = Base<'tool-execution-completed'> & {
63
63
  toolName: string;
64
64
  durationMs: number;
65
65
  isError: boolean;
66
+ /** The error thrown by the tool execution. Present when `isError` is true. */
67
+ error?: Error;
66
68
  /** Annotations declared for the tool, when available. See {@link ToolApprovalRequestEvent.annotations}. */
67
69
  annotations?: McpToolAnnotations;
68
70
  /** Originating MCP server name, when the tool was discovered through MCP. */
@@ -86,4 +86,6 @@ export type ToolResultInfo = {
86
86
  * the SDK or harness crashing.
87
87
  */
88
88
  isError?: boolean;
89
+ /** The error thrown by the tool execution. Present when `isError` is true. Absent on success paths. */
90
+ error?: Error;
89
91
  };
@@ -13,6 +13,8 @@ export type UsageMetadata = {
13
13
  reasoningTokens?: number;
14
14
  /** Input tokens served from provider cache (reduces cost). */
15
15
  cachedInputTokens?: number;
16
+ /** Input tokens written to the provider cache during this interaction. */
17
+ cacheWriteInputTokens?: number;
16
18
  };
17
19
  /**
18
20
  * Reason the model stopped generating.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/sfdx-agent-sdk",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Harness-agnostic agentic infrastructure for Salesforce developer experience tooling",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -36,12 +36,12 @@
36
36
  ],
37
37
  "dependencies": {
38
38
  "@salesforce/agentic-common": "0.6.0",
39
- "@salesforce/llm-gateway-sdk": "0.8.0"
39
+ "@salesforce/llm-gateway-sdk": "0.9.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@eslint/js": "^10.0.1",
43
- "@salesforce/sfdx-agent-harness-claude": "0.7.0",
44
- "@salesforce/sfdx-agent-harness-mastra": "0.10.0",
43
+ "@salesforce/sfdx-agent-harness-claude": "0.9.0",
44
+ "@salesforce/sfdx-agent-harness-mastra": "0.12.0",
45
45
  "@types/node": "^22.19.17",
46
46
  "@vitest/coverage-istanbul": "^4.1.7",
47
47
  "@vitest/eslint-plugin": "^1.6.17",