@salesforce/sfdx-agent-sdk 0.12.0 → 0.14.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 +207 -60
- package/dist/chat-session.d.ts +10 -6
- package/dist/chat-session.js +17 -3
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +2 -0
- package/dist/harness/agent-harness.d.ts +5 -3
- package/dist/harness/harness-config.d.ts +49 -5
- package/dist/harness/harness-config.js +25 -0
- package/dist/harness/stream-input.d.ts +33 -0
- package/dist/harness/stream-input.js +54 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -1
- package/dist/mcp-config.d.ts +53 -1
- package/dist/types/messages.d.ts +35 -2
- package/dist/types/telemetry-events.d.ts +8 -4
- package/dist/types/tools.d.ts +2 -0
- package/dist/types/usage.d.ts +2 -0
- package/package.json +4 -4
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
|
|
185
|
-
| ----------------------- |
|
|
186
|
-
| `start` | —
|
|
187
|
-
| `text-delta` | `text`
|
|
188
|
-
| `reasoning-delta` | `text`
|
|
189
|
-
| `tool-call` | `toolCallId`, `toolName`, `args`, `annotations?`, `serverName?`
|
|
190
|
-
| `tool-approval-request` | `toolCall: ToolCallInfo`, `annotations?`, `serverName?`
|
|
191
|
-
| `tool-result` | `toolCallId`, `toolName`, `result`, `isError?`, `annotations?`, `serverName?` | Tool execution completed. Same `annotations` / `serverName` semantics as `tool-call`.
|
|
192
|
-
| `step-start` | `stepIndex`
|
|
193
|
-
| `step-finish` | `stepIndex`, `finishReason`, `usage?`
|
|
194
|
-
| `error` | `error`, `code?`
|
|
195
|
-
| `finish` | `finishReason`, `usage?`
|
|
196
|
-
| `unmapped-chunk` | `chunkType`, `rawChunk`
|
|
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
|
|
217
|
-
| ---------------------- |
|
|
218
|
-
| `abortSignal?` | `AbortSignal`
|
|
219
|
-
| `requireToolApproval?` | `boolean`
|
|
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.
|
|
273
|
-
|
|
274
|
-
treat every field
|
|
275
|
-
|
|
276
|
-
| Field | Type | Description
|
|
277
|
-
| -------------- | ------------------------------------------- |
|
|
278
|
-
| `name` | `string` | Tool name as exposed to the LLM, including any harness-applied namespacing.
|
|
279
|
-
| `
|
|
280
|
-
| `
|
|
281
|
-
| `
|
|
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
|
|
374
|
-
|
|
|
375
|
-
| `AGENT_NOT_FOUND`
|
|
376
|
-
| `CHAT_SESSION_NOT_FOUND`
|
|
377
|
-
| `COMPACTION_FAILED`
|
|
378
|
-
| `DISPOSED`
|
|
379
|
-
| `INCOMPATIBLE_HARNESS`
|
|
380
|
-
| `
|
|
381
|
-
| `
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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` |
|
package/dist/chat-session.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
227
|
-
*
|
|
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
|
package/dist/chat-session.js
CHANGED
|
@@ -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
|
|
122
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
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 ────────────────────────────────────────────────────────
|
package/dist/mcp-config.d.ts
CHANGED
|
@@ -34,6 +34,25 @@ export type MCPStdioServerConfig = {
|
|
|
34
34
|
enabled?: boolean;
|
|
35
35
|
/** Timeout in milliseconds for individual requests to the server. */
|
|
36
36
|
timeout?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Opt the server's tool surface out of the active runtime's tool-search
|
|
39
|
+
* deferral. When `true`, every tool advertised by this server is
|
|
40
|
+
* registered with the model up-front instead of sitting behind a
|
|
41
|
+
* search/load round-trip. Useful for small, discovery-critical surfaces
|
|
42
|
+
* (e.g. ≤ 10 tools the model needs to find without prompting). Default
|
|
43
|
+
* (`undefined` / `false`): tools may be deferred when the active runtime
|
|
44
|
+
* enables tool search.
|
|
45
|
+
*
|
|
46
|
+
* **Harness behavior:**
|
|
47
|
+
* - **Claude harness** — sets `_meta['anthropic/alwaysLoad'] = true` on
|
|
48
|
+
* each tool the bridge forwards, equivalent to
|
|
49
|
+
* `defer_loading: false` on the API. Skill-bridge and consumer-tool
|
|
50
|
+
* tools are always-load regardless of this flag (see
|
|
51
|
+
* `@salesforce/sfdx-agent-harness-claude` ARCHITECTURE.md).
|
|
52
|
+
* - **Mastra harness** — no-op; Mastra eager-loads MCP tools at every
|
|
53
|
+
* turn already, so there's no deferral to opt out of.
|
|
54
|
+
*/
|
|
55
|
+
alwaysLoad?: boolean;
|
|
37
56
|
};
|
|
38
57
|
/** MCP server accessible over HTTP/SSE at a remote URL. */
|
|
39
58
|
export type MCPRemoteServerConfig = {
|
|
@@ -46,6 +65,11 @@ export type MCPRemoteServerConfig = {
|
|
|
46
65
|
enabled?: boolean;
|
|
47
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
|
-
/**
|
|
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
|
/**
|
package/dist/types/messages.d.ts
CHANGED
|
@@ -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. */
|
|
@@ -120,10 +122,12 @@ export type McpServerDiscoveryFailedEvent = Base<'mcp-server-discovery-failed'>
|
|
|
120
122
|
* then on success `Reconnecting → Connected`, or on failure
|
|
121
123
|
* `Reconnecting → Error`.
|
|
122
124
|
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* `
|
|
125
|
+
* Both the Mastra and Claude harnesses emit this on every per-server
|
|
126
|
+
* `McpServerStatus` transition they observe (initial discovery success /
|
|
127
|
+
* failure, reconnect entry, reconnect outcome). Mid-session transport drops
|
|
128
|
+
* on a `Connected` server are not observable on either harness's underlying
|
|
129
|
+
* SDK between turns; the next operation that touches the server discovers
|
|
130
|
+
* the drop and emits the transition then.
|
|
127
131
|
*/
|
|
128
132
|
export type McpServerStatusChangedEvent = Base<'mcp-server-status-changed'> & {
|
|
129
133
|
agentId: string;
|
package/dist/types/tools.d.ts
CHANGED
package/dist/types/usage.d.ts
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.14.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.
|
|
39
|
+
"@salesforce/llm-gateway-sdk": "0.10.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@eslint/js": "^10.0.1",
|
|
43
|
-
"@salesforce/sfdx-agent-harness-claude": "0.
|
|
44
|
-
"@salesforce/sfdx-agent-harness-mastra": "0.
|
|
43
|
+
"@salesforce/sfdx-agent-harness-claude": "0.10.0",
|
|
44
|
+
"@salesforce/sfdx-agent-harness-mastra": "0.13.0",
|
|
45
45
|
"@types/node": "^22.19.17",
|
|
46
46
|
"@vitest/coverage-istanbul": "^4.1.7",
|
|
47
47
|
"@vitest/eslint-plugin": "^1.6.17",
|