@salesforce/sfdx-agent-sdk 0.31.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,26 @@
3
3
  All notable changes to `@salesforce/sfdx-agent-sdk` are documented in this file.
4
4
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
+ ## [0.33.0] - 2026-07-14
7
+
8
+ ### Features
9
+ - **agent-sdk,harness-mastra,harness-claude**: cross-harness conformance test suite @W-23372341@ ([#663](https://github.com/forcedotcom/agentic-dx/pull/663))
10
+ - **sdk,harness-mastra,harness-claude**: delete deprecated tool-approval surfaces @W-23090034 ([#637](https://github.com/forcedotcom/agentic-dx/pull/637))
11
+
12
+ ### Fixes
13
+ - **harness-mastra**: thread model maxOutputTokens instead of flooring max_tokens at 4096 @W-23413250@ ([#656](https://github.com/forcedotcom/agentic-dx/pull/656))
14
+
15
+ ### Chores
16
+ - **deps-dev**: bump typescript-eslint from 8.62.1 to 8.63.0 in the dev-dependencies group across 1 directory ([#665](https://github.com/forcedotcom/agentic-dx/pull/665))
17
+ - **deps-dev**: bump the eslint group across 1 directory with 2 updates ([#659](https://github.com/forcedotcom/agentic-dx/pull/659))
18
+ - **deps-dev**: bump @vitest/eslint-plugin from 1.6.22 to 1.6.23 in the vitest group ([#660](https://github.com/forcedotcom/agentic-dx/pull/660))
19
+ - **deps-dev**: bump the vitest group across 1 directory with 3 updates ([#655](https://github.com/forcedotcom/agentic-dx/pull/655))
20
+
21
+ ## [0.32.0] - 2026-07-08
22
+
23
+ ### Fixes
24
+ - **agent-sdk,harness-mastra,harness-claude**: canonical role:tool placement for completed tool calls @W-23351939 ([#649](https://github.com/forcedotcom/agentic-dx/pull/649))
25
+
6
26
  ## [0.31.0] - 2026-07-07
7
27
 
8
28
  ### Features
package/README.md CHANGED
@@ -191,20 +191,20 @@ iterating the same `eventStream` until it sees a terminal `finish` event.
191
191
 
192
192
  Discriminated union (`event.type`) of streaming events:
193
193
 
194
- | Type | Key Fields | Description |
195
- | ----------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
196
- | `start` | — | Stream has begun. |
197
- | `text-delta` | `text` | Incremental response text. |
198
- | `reasoning-delta` | `text` | Chain-of-thought fragment. |
199
- | `tool-call` | `toolCallId`, `toolName`, `args`, `annotations?`, `serverName?`, `bareToolName?` | 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. `toolName` is the harness-namespaced display name; `bareToolName` is the un-namespaced leaf paired with `serverName` (set for MCP tools only) — use the `(serverName, bareToolName)` pair, never string-split `toolName`, for cross-harness identity. |
200
- | `tool-call-delta` | `toolCallId`, `toolName?`, `argsTextDelta` | Incremental fragment of a tool call's args JSON, emitted while the model composes the call. Concatenate successive deltas for the same `toolCallId` to build the args text; the parsed result matches the terminal `tool-call.args`. Useful for live-typing tool inputs UI; consumers that don't need streaming-args can ignore this event and continue reading the parsed `args` on the terminal `tool-call`. `toolName` is optional (Claude's signal does not carry it on the wire). |
201
- | `tool-approval-request` | `toolCall: ToolCallInfo`, `annotations?`, `serverName?`, `bareToolName?` | Engine requests approval before executing a tool. Same `annotations` / `serverName` / `bareToolName` semantics as `tool-call`. `bareToolName` is the field the SDK reads to build the `remember` policy matcher (see [Tool Approval](#tool-approval)). |
194
+ | Type | Key Fields | Description |
195
+ | ----------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
196
+ | `start` | — | Stream has begun. |
197
+ | `text-delta` | `text` | Incremental response text. |
198
+ | `reasoning-delta` | `text` | Chain-of-thought fragment. |
199
+ | `tool-call` | `toolCallId`, `toolName`, `args`, `annotations?`, `serverName?`, `bareToolName?` | 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. `toolName` is the harness-namespaced display name; `bareToolName` is the un-namespaced leaf paired with `serverName` (set for MCP tools only) — use the `(serverName, bareToolName)` pair, never string-split `toolName`, for cross-harness identity. |
200
+ | `tool-call-delta` | `toolCallId`, `toolName?`, `argsTextDelta` | Incremental fragment of a tool call's args JSON, emitted while the model composes the call. Concatenate successive deltas for the same `toolCallId` to build the args text; the parsed result matches the terminal `tool-call.args`. Useful for live-typing tool inputs UI; consumers that don't need streaming-args can ignore this event and continue reading the parsed `args` on the terminal `tool-call`. `toolName` is optional (Claude's signal does not carry it on the wire). |
201
+ | `tool-approval-request` | `toolCall: ToolCallInfo`, `annotations?`, `serverName?`, `bareToolName?` | Engine requests approval before executing a tool. Same `annotations` / `serverName` / `bareToolName` semantics as `tool-call`. `bareToolName` is the field the SDK reads to build the `remember` policy matcher (see [Tool Approval](#tool-approval)). |
202
202
  | `tool-result` | `toolCallId`, `toolName`, `result`, `isError?`, `error?`, `annotations?`, `serverName?`, `bareToolName?` | 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` / `bareToolName` semantics as `tool-call`. |
203
- | `tool-progress` | `toolCallId`, `toolName`, `output?`, `parentToolCallId?` | Incremental progress signal from a long-running tool call. Distinct from `tool-result`: zero or more `tool-progress` events may be emitted before exactly one terminal `tool-result`. `output` and `parentToolCallId` are best-effort enrichment that depends on the tool — the event itself is the load-bearing "tool is still working" signal; consumers SHOULD NOT branch on which optional fields are present. Useful for "tool is working" UI on long-running tools (build, test, deploy, large search, sub-agent tasks). |
204
- | `step-start` | `stepIndex` | New LLM invocation step began. |
205
- | `step-finish` | `stepIndex`, `finishReason`, `usage?` | Step completed with per-step token usage. |
206
- | `error` | `error`, `code?` | Mid-stream error (yielded, not thrown). |
207
- | `finish` | `finishReason`, `usage?` | Stream completed with aggregate token usage. |
203
+ | `tool-progress` | `toolCallId`, `toolName`, `output?`, `parentToolCallId?` | Incremental progress signal from a long-running tool call. Distinct from `tool-result`: zero or more `tool-progress` events may be emitted before exactly one terminal `tool-result`. `output` and `parentToolCallId` are best-effort enrichment that depends on the tool — the event itself is the load-bearing "tool is still working" signal; consumers SHOULD NOT branch on which optional fields are present. Useful for "tool is working" UI on long-running tools (build, test, deploy, large search, sub-agent tasks). |
204
+ | `step-start` | `stepIndex` | New LLM invocation step began. |
205
+ | `step-finish` | `stepIndex`, `finishReason`, `usage?` | Step completed with per-step token usage. |
206
+ | `error` | `error`, `code?` | Mid-stream error (yielded, not thrown). |
207
+ | `finish` | `finishReason`, `usage?` | Stream completed with aggregate token usage. |
208
208
 
209
209
  > **Diagnostic logging.** The `ChatEvent` union is the harness-agnostic public stream — it never carries
210
210
  > harness-internal chunk shapes. When a harness encounters a chunk type its adapter does not recognize (typically after
@@ -232,33 +232,27 @@ function onApprovalRequest(event: ToolApprovalRequestEvent): Promise<boolean> {
232
232
 
233
233
  #### `AgentConfig`
234
234
 
235
- | Field | Type | Description |
236
- | ---------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
237
- | `orgAlias?` | `string` | Salesforce org alias or username. Falls back to project/default org. |
238
- | `modelId?` | `ModelName \| Model` | LLM model selector. Pass a `ModelName` enum value for an in-tree model (e.g. `'llmgateway__OpenAIGPT5'`), or a pre-built `Model` instance to opt into a Bedrock-Anthropic Claude variant the SDK has not yet released — see `createClaudeModel(gatewayId, overrides)` exported from this package. |
239
- | `name?` | `string` | Human-readable agent name. |
240
- | `description?` | `string` | Agent purpose description. |
241
- | `instructions?` | `string` | System instructions for the agent. |
242
- | `tools?` | `ToolDefinition[]` | Consumer-executed tool schemas. |
243
- | `mcpServers?` | `MCPConfiguration` | MCP server connections. |
244
- | `skills?` | `string[]` | Each entry is either an individual skill folder (containing `SKILL.md`) or a parent folder containing skill subfolders. Relative and absolute paths supported; forms can be mixed in the same array. |
245
- | `rules?` | `string[]` | Each entry is either an individual `.md` rule file or a directory of `.md` rule files (scanned one level deep, alphabetical, non-`.md` skipped). Bodies are composed verbatim into the agent's effective system prompt; YAML frontmatter is optional and stripped if present. Matches Claude Code's `.claude/rules/*.md` convention. |
246
- | `toolPolicies?` | `ToolPolicyRule[]` | Ordered per-tool approval rules resolved by `resolveToolApprovalPolicy` (cross-tier deny-wins / within-tier last-wins). Author directly or via `definePolicy(...)`. See "Tool Approval Policy" below. Has no effect until a harness wires the resolver (Phase 2); until then gating uses the deprecated `StreamOptions.requireToolApproval`. |
247
- | `defaultToolDecision?` | `Decision` | Fallback decision when no rule matches. Defaults to `'allow'` (no policy ⇒ no gating). Set to `'require-approval'` for a fail-closed posture (recommended for catalogs with un-annotated MCP servers). |
235
+ | Field | Type | Description |
236
+ | ---------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
237
+ | `orgAlias?` | `string` | Salesforce org alias or username. Falls back to project/default org. |
238
+ | `modelId?` | `ModelName \| Model` | LLM model selector. Pass a `ModelName` enum value for an in-tree model (e.g. `'llmgateway__OpenAIGPT5'`), or a pre-built `Model` instance to opt into a Bedrock-Anthropic Claude variant the SDK has not yet released — see `createClaudeModel(gatewayId, overrides)` exported from this package. |
239
+ | `name?` | `string` | Human-readable agent name. |
240
+ | `description?` | `string` | Agent purpose description. |
241
+ | `instructions?` | `string` | System instructions for the agent. |
242
+ | `tools?` | `ToolDefinition[]` | Consumer-executed tool schemas. |
243
+ | `mcpServers?` | `MCPConfiguration` | MCP server connections. |
244
+ | `skills?` | `string[]` | Each entry is either an individual skill folder (containing `SKILL.md`) or a parent folder containing skill subfolders. Relative and absolute paths supported; forms can be mixed in the same array. |
245
+ | `rules?` | `string[]` | Each entry is either an individual `.md` rule file or a directory of `.md` rule files (scanned one level deep, alphabetical, non-`.md` skipped). Bodies are composed verbatim into the agent's effective system prompt; YAML frontmatter is optional and stripped if present. Matches Claude Code's `.claude/rules/*.md` convention. |
246
+ | `toolPolicies?` | `ToolPolicyRule[]` | Ordered per-tool approval rules resolved by `resolveToolApprovalPolicy` (cross-tier deny-wins / within-tier last-wins). Author directly or via `definePolicy(...)`. See "Tool Approval Policy" below. |
247
+ | `defaultToolDecision?` | `Decision` | Fallback decision when no rule matches. Defaults to `'allow'` (no policy ⇒ no gating). Set to `'require-approval'` for a fail-closed posture (recommended for catalogs with un-annotated MCP servers). |
248
248
 
249
249
  #### `StreamOptions`
250
250
 
251
- | Field | Type | Description |
252
- | ---------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
253
- | `abortSignal?` | `AbortSignal` | Abort the streaming operation. |
254
- | `requireToolApproval?` | `boolean \| ToolApprovalMode` | **Deprecated.** Per-call all-or-nothing gating. Superseded by per-tool policy on `AgentConfig.toolPolicies`; the serial-vs-batch UX axis moved to `batchApprovals`. Kept functional for one release so harness PRs migrate independently. `true` / `'serial'` emits one approval per stream; `'batch'` surfaces parallel approvals on one stream (requires Pattern A iterators). See "Tool Approval Flow" below. |
255
- | `batchApprovals?` | `boolean` | When `true`, parallel approval-requests within a turn surface on the same stream so the consumer can render a batch approval card (requires Pattern A iterators collect-all-then-settle). Defaults to `false` (serial). The self-documenting replacement for the UX half of the deprecated `requireToolApproval` enum; the gating half moves to `AgentConfig.toolPolicies`. No effect when no tool in the turn resolves to `'require-approval'`. |
256
- | `maxSteps?` | `number` | Maximum number of LLM call steps the agent may take per `stream()` invocation. Each step is one LLM call (which may produce text, tool calls, or both). Must be `>= 1`. Defaults to `DEFAULT_MAX_STEPS` (1024) — high enough to be effectively unlimited for real tasks; the practical ceiling is the context window and cost. The constant is exported so consumers and harness authors share one source of truth. |
257
-
258
- `ToolApprovalMode` is the exported (deprecated) type alias `'serial' | 'batch'`. Pair with the (deprecated)
259
- `resolveToolApprovalMode(boolean | ToolApprovalMode | undefined)` to normalize consumer input the same way the SDK does
260
- internally (`undefined` / `false` → `undefined`, `true` → `'serial'`, strings pass through, unknown strings throw). New
261
- code should configure `AgentConfig.toolPolicies` and set `batchApprovals` for the UX axis instead.
251
+ | Field | Type | Description |
252
+ | ----------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
253
+ | `abortSignal?` | `AbortSignal` | Abort the streaming operation. |
254
+ | `batchApprovals?` | `boolean` | When `true`, parallel approval-requests within a turn surface on the same stream so the consumer can render a batch approval card (requires Pattern A iterators — collect-all-then-settle). Defaults to `false` (serial). Controls only the serial-vs-batch emission UX; the gating on/off decision lives on `AgentConfig.toolPolicies` / `AgentConfig.defaultToolDecision`. No effect when no tool in the turn resolves to `'require-approval'`. |
255
+ | `maxSteps?` | `number` | Maximum number of LLM call steps the agent may take per `stream()` invocation. Each step is one LLM call (which may produce text, tool calls, or both). Must be `>= 1`. Defaults to `DEFAULT_MAX_STEPS` (1024) high enough to be effectively unlimited for real tasks; the practical ceiling is the context window and cost. The constant is exported so consumers and harness authors share one source of truth. |
262
256
 
263
257
  #### Tool Approval Policy
264
258
 
@@ -722,21 +716,16 @@ consumers can branch on tool hints (e.g. auto-approve `readOnlyHint`) or group b
722
716
  reparsing namespaced tool names. `annotations` is `undefined` when the source did not declare them (per the MCP spec);
723
717
  `serverName` is `undefined` when the tool is not from an MCP server.
724
718
 
725
- `requireToolApproval` accepts a `boolean` (`true` is shorthand for `'serial'`) or one of the mode strings exposed via
726
- the `ToolApprovalMode` type alias (`'serial' | 'batch'`):
727
-
728
- - **`'serial'`** (the safe default) — `tool-approval-request` events surface one at a time. The next request (if any)
729
- arrives later on the same `eventStream` after the consumer settles the current one.
730
- - **`'batch'`** — when the model emits parallel `tool_use` blocks, all approval-requests for that batch surface on the
731
- same `eventStream` together, so the consumer can render a batch-approval card. The consumer must gather all approvals
732
- before settling — a `break`-on-first-approval loop will hang the turn because Mastra/Claude won't continue emitting
733
- tool-results until the consumer has settled the entire batch.
719
+ Gating is configured on the agent via `AgentConfig.toolPolicies` / `AgentConfig.defaultToolDecision` (see "Tool Approval
720
+ Policy" above). The per-call `StreamOptions.batchApprovals` flag controls only how the resulting `tool-approval-request`
721
+ events are emitted:
734
722
 
735
- The SDK also exports `resolveToolApprovalMode(value)`the canonical
736
- `boolean | ToolApprovalMode | undefined ToolApprovalMode | undefined` normalizer harness implementations should use to
737
- dispatch (`undefined` / `false` `undefined`, `true` `'serial'`, strings pass through). Reject unknown strings via
738
- `throw` instead of reimplementing the boolean-vs-string branch the helper's defensive throw catches `as any` consumers
739
- passing invalid values that would otherwise silently degrade to "approval gating on but no coordinator allocated."
723
+ - **`batchApprovals: false`** (the safe default) — `tool-approval-request` events surface one at a time. The next
724
+ request (if any) arrives later on the same `eventStream` after the consumer settles the current one.
725
+ - **`batchApprovals: true`** when the model emits parallel `tool_use` blocks, all approval-requests for that batch
726
+ surface on the same `eventStream` together, so the consumer can render a batch-approval card. The consumer must gather
727
+ all approvals before settling a `break`-on-first-approval loop will hang the turn because Mastra/Claude won't
728
+ continue emitting tool-results until the consumer has settled the entire batch.
740
729
 
741
730
  #### Single stream per turn
742
731
 
@@ -749,12 +738,12 @@ in-line as approval-requests / consumer-tool-calls arrive.
749
738
 
750
739
  #### Pattern: settle in-line, continue iterating
751
740
 
752
- The same shape works for `'serial'` and `'batch'` mode:
741
+ The same shape works for serial and batch emission. Configure gating on the agent
742
+ (`defaultToolDecision: 'require-approval'`, or per-tool `toolPolicies`); the `chat()` call needs no approval flag:
753
743
 
754
744
  ```typescript
755
- const { eventStream } = await session.chat('Run the deployment', {
756
- requireToolApproval: true, // or 'serial' / 'batch'
757
- });
745
+ // Agent configured with e.g. { defaultToolDecision: 'require-approval' }
746
+ const { eventStream } = await session.chat('Run the deployment');
758
747
 
759
748
  for await (const event of eventStream) {
760
749
  if (event.type === 'tool-approval-request') {
@@ -789,12 +778,13 @@ for await (const event of eventStream) {
789
778
 
790
779
  #### Pattern variant: batch-collect approvals before settling
791
780
 
792
- Same single-stream loop, just gather the batch before deciding (useful for "approve these N tools?" UI cards under
793
- `requireToolApproval: 'batch'`):
781
+ Same single-stream loop, just gather the batch before deciding (useful for "approve these N tools?" UI cards). Set
782
+ `batchApprovals: true` on the `chat()` call; gating itself stays on the agent's `toolPolicies` / `defaultToolDecision`:
794
783
 
795
784
  ```typescript
785
+ // Agent configured with e.g. { defaultToolDecision: 'require-approval' }
796
786
  const { eventStream } = await session.chat('Run the deployment', {
797
- requireToolApproval: 'batch',
787
+ batchApprovals: true,
798
788
  });
799
789
 
800
790
  const pendingBatch: ToolApprovalRequestEvent[] = [];
@@ -873,8 +863,8 @@ for await (const event of eventStream) {
873
863
  }
874
864
  ```
875
865
 
876
- When `requireToolApproval: true` is also set, consumer-executed tools bypass the approval gate by construction (per
877
- `StreamOptions.requireToolApproval` JSDoc). They surface as a normal `tool-call` event without a preceding
866
+ When approval gating is active (via `AgentConfig.toolPolicies` / `defaultToolDecision`), consumer-executed tools bypass
867
+ the approval gate by construction. They surface as a normal `tool-call` event without a preceding
878
868
  `tool-approval-request`. Built-in / MCP tools still gate normally.
879
869
 
880
870
  ### Tool-Result Redaction
@@ -1389,18 +1379,20 @@ This package publishes two ESM entry points:
1389
1379
  > see the subpath. Modern bundlers (Vite, esbuild, Webpack 5+, tsup, Rollup with `@rollup/plugin-node-resolve` v15+)
1390
1380
  > resolve it natively. This is a harness-author concern only; consumer applications never touch the subpath.
1391
1381
 
1392
- | Export | Surface | Role |
1393
- | ----------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1394
- | `HarnessFactory<H>` | Type only on bare; value+type on `/harness` | Construct a harness of type `H` bound to a storage root. Declares `harnessId` and `protocolVersion`. Default `H = AgentHarness`. |
1395
- | `AgentHarness` | Type only on bare; type on `/harness` | Runtime contract: agent / thread / stream / tool / message lifecycle. Declares its own `harnessId` and `protocolVersion`. |
1396
- | `SUPPORTED_PROTOCOL_VERSIONS` | `/harness` only | Readonly list of harness protocol versions this SDK accepts. `createAgentManager` checks both the factory and the constructed harness. |
1397
- | `HarnessBusOwner` | `/harness` only | Composition helper owning telemetry + log buses with `dispose()` semantics. Reuse it instead of reimplementing bus plumbing. |
1398
- | `lowerStreamInput` | `/harness` only | Validates a `MessagePart[]` and lowers each input part to your runtime's content-block shape. Use it in `stream()` so multimodal caps and `MULTIMODAL_NOT_SUPPORTED` / `INVALID_MESSAGE_CONTENT` semantics match every other harness. |
1399
- | `GenSink<T>` | `/harness` only | Buffered async-generator wrapper for routing `ChatEvent`s to a consumer's `ChatStreamResult.eventStream`. Single-iteration: calling `generator()` twice throws — sinks have one waiter slot and one buffer, two iterators race on both. |
1400
- | `mcpServerConfigEqual` | Bare specifier and `/harness` | Structural deep-equality predicate over `MCPServerConfig`. Use inside `updateAgent` to decide which servers to preserve vs. cycle. Treats `enabled: undefined` and `enabled: true` as equal; compares URLs via `String(url)` (so `URL` instances and strings round-trip); `headers` and `env` are key-order-insensitive; `reconnectionOptions` compares field-wise. |
1401
- | `AlwaysActiveEntry` | `/harness` only | Entry shape consumed by per-harness `toolSearch.alwaysActive` extension fields. Three matching patterns: `{ serverName }` (server-wide), `{ serverName, toolName }` (precise), `{ toolName }` (cross-source). At least one of `serverName` / `toolName` must be present. |
1402
- | `matchesAlwaysActive` | `/harness` only | Predicate `(entries, serverName, toolName) → boolean` consulted per-tool when stamping always-load metadata or partitioning a tool-search pool. Use this instead of pattern-matching entries by hand so harness behavior stays uniform. |
1403
- | `validateAlwaysActiveEntry` | `/harness` only | Throws on a malformed entry (`{}`, both fields empty). Call once per entry at the harness boundary so a typo fails loud at config time rather than silently dropping the entry on every `stream()`. |
1382
+ | Export | Surface | Role |
1383
+ | ---------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1384
+ | `HarnessFactory<H>` | Type only on bare; value+type on `/harness` | Construct a harness of type `H` bound to a storage root. Declares `harnessId` and `protocolVersion`. Default `H = AgentHarness`. |
1385
+ | `AgentHarness` | Type only on bare; type on `/harness` | Runtime contract: agent / thread / stream / tool / message lifecycle. Declares its own `harnessId` and `protocolVersion`. |
1386
+ | `SUPPORTED_PROTOCOL_VERSIONS` | `/harness` only | Readonly list of harness protocol versions this SDK accepts. `createAgentManager` checks both the factory and the constructed harness. |
1387
+ | `HarnessBusOwner` | `/harness` only | Composition helper owning telemetry + log buses with `dispose()` semantics. Reuse it instead of reimplementing bus plumbing. |
1388
+ | `lowerStreamInput` | `/harness` only | Validates a `MessagePart[]` and lowers each input part to your runtime's content-block shape. Use it in `stream()` so multimodal caps and `MULTIMODAL_NOT_SUPPORTED` / `INVALID_MESSAGE_CONTENT` semantics match every other harness. |
1389
+ | `GenSink<T>` | `/harness` only | Buffered async-generator wrapper for routing `ChatEvent`s to a consumer's `ChatStreamResult.eventStream`. Single-iteration: calling `generator()` twice throws — sinks have one waiter slot and one buffer, two iterators race on both. |
1390
+ | `mcpServerConfigEqual` | Bare specifier and `/harness` | Structural deep-equality predicate over `MCPServerConfig`. Use inside `updateAgent` to decide which servers to preserve vs. cycle. Treats `enabled: undefined` and `enabled: true` as equal; compares URLs via `String(url)` (so `URL` instances and strings round-trip); `headers` and `env` are key-order-insensitive; `reconnectionOptions` compares field-wise. |
1391
+ | `AlwaysActiveEntry` | `/harness` only | Entry shape consumed by per-harness `toolSearch.alwaysActive` extension fields. Three matching patterns: `{ serverName }` (server-wide), `{ serverName, toolName }` (precise), `{ toolName }` (cross-source). At least one of `serverName` / `toolName` must be present. |
1392
+ | `matchesAlwaysActive` | `/harness` only | Predicate `(entries, serverName, toolName) → boolean` consulted per-tool when stamping always-load metadata or partitioning a tool-search pool. Use this instead of pattern-matching entries by hand so harness behavior stays uniform. |
1393
+ | `validateAlwaysActiveEntry` | `/harness` only | Throws on a malformed entry (`{}`, both fields empty). Call once per entry at the harness boundary so a typo fails loud at config time rather than silently dropping the entry on every `stream()`. |
1394
+ | `splitToolResultsIntoToolMessages` | `/harness` only | Read-side normalizer `(Message[]) → Message[]`. Hoists every completed tool call's `tool-result` part onto its own `role: 'tool'` message (leaving the `tool-call` on the assistant message), so `getMessages()` returns the canonical cross-harness layout. Idempotent; backfills a blank result `toolName` from the matching call; preserves `isError` and (in-memory) `error`. Call it at the end of `getMessages()`. (Whether `error` survives to the returned history is a harness-persistence concern — see `AgentHarness.getMessages`; `isError` always survives.) |
1395
+ | `mergeToolResultsIntoAssistant` | `/harness` only | Write-side inverse `(Message[]) → Message[]`. Folds each `role: 'tool'` message's result back adjacent to its `tool-call` in the preceding assistant message, so a runtime that stores a completed call as one merged object round-trips losslessly. Call it at the start of `addContext()` before persisting. |
1404
1396
 
1405
1397
  Minimal skeleton:
1406
1398
 
@@ -406,16 +406,37 @@ export interface AgentHarness {
406
406
  * MUST populate `Message.createdAt` on every returned message. MUST return
407
407
  * messages sorted ascending by `createdAt`.
408
408
  *
409
- * **Tool calls MUST round-trip both halves.** For every completed tool
410
- * invocation the returned history MUST contain BOTH a `tool-call`
411
- * {@link MessagePart} (carrying the model's `toolName` + `args`) AND a
412
- * `tool-result` part (carrying the outcome). The two share a `toolCallId`.
413
- * A harness that collapses a completed call to only its `tool-result` makes
414
- * history harness-dependent — the model's original call (name + args) is
415
- * lost on read which breaks any consumer that renders, replays, or audits
416
- * tool activity from history (e.g. an AG-UI `MESSAGES_SNAPSHOT`). This is a
417
- * cross-harness contract; the Claude harness, which stores the call and
418
- * result as distinct blocks, is the reference behavior (#647).
409
+ * **A completed tool call MUST produce exactly two messages, in a canonical
410
+ * layout (#647).** For every completed tool invocation the returned history
411
+ * MUST contain:
412
+ *
413
+ * 1. an `assistant` message carrying the `tool-call` {@link MessagePart}
414
+ * (the model's `toolName` + `args`), immediately followed by
415
+ * 2. a separate `role: 'tool'` message carrying the paired `tool-result`
416
+ * part (the outcome). The two share a `toolCallId`.
417
+ *
418
+ * The `tool-result` part appears **only** on the `role: 'tool'` message —
419
+ * never also on the `assistant` message or a `user` message. The
420
+ * `tool-result` part's `isError` flag MUST survive to history, since
421
+ * consumers render failed tool calls differently. The `error` `Error` object
422
+ * is best-effort: it is preserved in-memory but does NOT survive a harness
423
+ * persistence round-trip (neither harness persists it), so consumers keying
424
+ * off a failed result MUST branch on `isError`, not on the presence of
425
+ * `error`.
426
+ *
427
+ * This is the shape the SDK's `MessageRole` documents as canonical (`'tool'`
428
+ * = "Results from tool executions"), aligned with AI SDK `ModelMessage` and
429
+ * the AG-UI `ToolMessage` model, so a downstream translator is a pure
430
+ * per-message 1:1 map with no cross-message state or per-harness placement
431
+ * knowledge. **Placement, not just presence, is the contract** — collapsing a
432
+ * call to only its `tool-result`, or leaving the result on the assistant /
433
+ * user message, makes history harness-dependent and breaks any consumer that
434
+ * renders, replays, or audits tool activity from history (e.g. an AG-UI
435
+ * `MESSAGES_SNAPSHOT`). Both production harnesses converge on this layout via
436
+ * the shared `splitToolResultsIntoToolMessages` normalizer
437
+ * (`@salesforce/sfdx-agent-sdk/harness`); the inverse
438
+ * `mergeToolResultsIntoAssistant` keeps the `getMessages()` → `addContext()`
439
+ * round-trip lossless.
419
440
  *
420
441
  * @param agentId - ID of the agent.
421
442
  * @param threadId - ID of the conversation thread.
@@ -75,17 +75,12 @@ export type AgentConfig = {
75
75
  * the common cases. Per-MCP-server policy is expressed here via
76
76
  * `{ matcher: { type: 'mcp', serverName } }` rules — `MCPServerConfig`
77
77
  * carries no policy fields.
78
- *
79
- * Has no effect until a harness wires the resolver into its gate site
80
- * (Phase 2). Until then, harnesses gate via the deprecated
81
- * {@link StreamOptions.requireToolApproval}.
82
78
  */
83
79
  toolPolicies?: ToolPolicyRule[];
84
80
  /**
85
81
  * Fallback decision for tool invocations that match no rule in
86
82
  * {@link toolPolicies} (or any built-in / harness tier). Defaults to
87
- * `'allow'` for back-compat ("no policy ⇒ no gating," matching today's
88
- * behavior when `requireToolApproval` is unset).
83
+ * `'allow'` ("no policy ⇒ no gating").
89
84
  *
90
85
  * Set to `'require-approval'` for a fail-closed posture — recommended for
91
86
  * tenants whose MCP catalog includes un-annotated servers, since
@@ -126,84 +121,12 @@ export type HarnessAgentConfig = Omit<AgentConfig, 'orgAlias'> & {
126
121
  * `test/harness/harness-config.test.ts` that asserts unknown fields survive.
127
122
  */
128
123
  export declare function toHarnessConfig(config: AgentConfig, orgJwt?: JSONWebToken): HarnessAgentConfig;
129
- /**
130
- * Approval-mode selector for `StreamOptions.requireToolApproval`.
131
- *
132
- * Distinguishes the legacy "serial" UX (one approval per stream;
133
- * consumer settles before the next is surfaced) from the parallel
134
- * "batch" UX (all approval-requests for a parallel `tool_use` batch
135
- * surface on the same stream so the consumer can render them as a
136
- * batch approval card). See `requireToolApproval` for the safety
137
- * note on choosing `batch`.
138
- *
139
- * @deprecated Tool-approval gating is moving to per-tool policy on
140
- * {@link AgentConfig.toolPolicies}, resolved by `resolveToolApprovalPolicy`.
141
- * The serial-vs-batch UX axis survives as {@link StreamOptions.batchApprovals}.
142
- * Removed in a future major once harnesses and consumers have migrated.
143
- */
144
- export type ToolApprovalMode = 'serial' | 'batch';
145
- /**
146
- * Resolves `StreamOptions.requireToolApproval` to its canonical mode:
147
- * `undefined` (gating off), `'serial'`, or `'batch'`. Centralizes the
148
- * boolean-vs-string normalization so harnesses don't duplicate the
149
- * resolution logic.
150
- *
151
- * Semantics:
152
- * - `undefined` / `false` → `undefined` (no gating).
153
- * - `true` → `'serial'` (back-compat shorthand for the original `boolean` shape).
154
- * - `'serial'` → `'serial'` (explicit, equivalent to `true`).
155
- * - `'batch'` → `'batch'`.
156
- *
157
- * @deprecated Superseded by per-tool policy on {@link AgentConfig.toolPolicies}
158
- * (resolved by `resolveToolApprovalPolicy`) plus {@link StreamOptions.batchApprovals}
159
- * for the UX axis. Kept functional for one release so harness PRs can migrate
160
- * independently; removed in a future major.
161
- */
162
- export declare function resolveToolApprovalMode(requireToolApproval: boolean | ToolApprovalMode | undefined): ToolApprovalMode | undefined;
163
124
  /**
164
125
  * Per-call options controlling streaming behavior.
165
126
  */
166
127
  export type StreamOptions = {
167
128
  /** Signal to abort the streaming operation. */
168
129
  abortSignal?: AbortSignal;
169
- /**
170
- * When set, the harness requires human approval before executing any
171
- * native tool (e.g., MCP tools). The stream emits a `tool-approval-request`
172
- * event and suspends until the consumer calls `approveToolCall()` or
173
- * `declineToolCall()`.
174
- *
175
- * Accepts a `boolean` (back-compatible shorthand) or one of the
176
- * approval-mode strings:
177
- *
178
- * - **`true` or `'serial'`** (the safe default): each chat-stream
179
- * surfaces ONE `tool-approval-request` at a time. The consumer
180
- * settles the approval; the next `tool-approval-request` (if any)
181
- * appears on the continuation stream. Identical to the SDK's
182
- * behavior before parallel-approval UX (#447) — safe for consumers
183
- * whose iterator returns on the first approval-request and
184
- * re-iterates the continuation (Pattern B).
185
- *
186
- * - **`'batch'`**: when the model emits parallel `tool_use` blocks, the
187
- * broker surfaces ALL approval-requests on the same stream so the
188
- * consumer can render a batch approval UI ("Approve these N tools?").
189
- * Consumers MUST iterate to natural park collecting approvals
190
- * (Pattern A); a `break`-on-first-approval loop will miss the
191
- * subsequent approvals on the same stream and the chat will hang.
192
- * Only opt into `'batch'` after the consumer's iterator collects all
193
- * approvals before settling.
194
- *
195
- * Does not affect consumer-executed tools (those defined via
196
- * `AgentConfig.tools` without an execute handler) — the consumer
197
- * already controls execution for those via `submitToolResult()`.
198
- *
199
- * @deprecated Per-call all-or-nothing gating is superseded by per-tool
200
- * policy on {@link AgentConfig.toolPolicies}. The serial-vs-batch UX axis
201
- * moved to {@link batchApprovals}. Kept functional for one release so
202
- * harness PRs can migrate independently; removed in a future major. New
203
- * code should configure `AgentConfig.toolPolicies` and, if a batch UX is
204
- * wanted, set `batchApprovals: true`.
205
- */
206
- requireToolApproval?: boolean | ToolApprovalMode;
207
130
  /**
208
131
  * When `true`, parallel approval-requests within a turn (the model emits
209
132
  * multiple `tool_use` blocks in one batch, more than one of which resolves
@@ -212,14 +135,12 @@ export type StreamOptions = {
212
135
  * natural park before settling.
213
136
  *
214
137
  * Defaults to `false` (serial: one approval-request at a time; the consumer
215
- * settles before the next surfaces). Identical to pre-#447 behavior under
216
- * `requireToolApproval: true`.
138
+ * settles before the next surfaces).
217
139
  *
218
140
  * Has no effect when no tool in the turn resolves to `'require-approval'` —
219
- * there is nothing to batch. This is the self-documenting replacement for
220
- * the UX half of the deprecated {@link requireToolApproval} enum; the
221
- * "gating on/off" half moves to per-tool policy on
222
- * {@link AgentConfig.toolPolicies}.
141
+ * there is nothing to batch. The "gating on/off" decision lives on per-tool
142
+ * policy in {@link AgentConfig.toolPolicies} / {@link AgentConfig.defaultToolDecision};
143
+ * this flag controls only the serial-vs-batch emission UX.
223
144
  */
224
145
  batchApprovals?: boolean;
225
146
  /**
@@ -24,36 +24,6 @@ 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
- * @deprecated Superseded by per-tool policy on {@link AgentConfig.toolPolicies}
40
- * (resolved by `resolveToolApprovalPolicy`) plus {@link StreamOptions.batchApprovals}
41
- * for the UX axis. Kept functional for one release so harness PRs can migrate
42
- * independently; removed in a future major.
43
- */
44
- export function resolveToolApprovalMode(requireToolApproval) {
45
- if (requireToolApproval === undefined || requireToolApproval === false)
46
- return undefined;
47
- if (requireToolApproval === true)
48
- return 'serial';
49
- if (requireToolApproval === 'serial' || requireToolApproval === 'batch')
50
- return requireToolApproval;
51
- // Defensive: an `as any` consumer could pass an unknown string. Without
52
- // this guard the value flows through to harness checks, which then
53
- // silently degrade to "approval gating on but no broker allocated" and
54
- // the chat hangs without an error a consumer can debug.
55
- throw new Error(`Invalid requireToolApproval value: ${JSON.stringify(requireToolApproval)}. Expected boolean, 'serial', or 'batch'.`);
56
- }
57
27
  /**
58
28
  * Default maximum steps for a single agent stream invocation.
59
29
  *
@@ -37,10 +37,9 @@
37
37
  * counterparts live only here.
38
38
  *
39
39
  * Symbols that consumers and harnesses both need (`AgentConfig`,
40
- * `StreamOptions`, `DEFAULT_MAX_STEPS`, `resolveToolApprovalMode`,
41
- * `ChatEvent` types, `Message` types, `MCPConfiguration`, etc.) stay on
42
- * the bare specifier. The split is "harness-only" vs. "consumer-AND-harness",
43
- * not "harness vs. consumer."
40
+ * `StreamOptions`, `DEFAULT_MAX_STEPS`, `ChatEvent` types, `Message`
41
+ * types, `MCPConfiguration`, etc.) stay on the bare specifier. The split
42
+ * is "harness-only" vs. "consumer-AND-harness", not "harness vs. consumer."
44
43
  */
45
44
  export type { AgentHarness, HarnessFactory, WithAgentConfig, ConfigOf } from './index.js';
46
45
  export type { AgentHooks } from '../types/redaction.js';
@@ -50,3 +49,4 @@ export { HarnessBusOwner } from './harness-bus-owner.js';
50
49
  export { lowerStreamInput, type InputMessagePart } from './stream-input.js';
51
50
  export { GenSink } from './gen-sink.js';
52
51
  export { matchesAlwaysActive, validateAlwaysActiveEntry, type AlwaysActiveEntry } from './always-active.js';
52
+ export { splitToolResultsIntoToolMessages, mergeToolResultsIntoAssistant } from './tool-message-normalizer.js';
@@ -9,4 +9,5 @@ export { HarnessBusOwner } from './harness-bus-owner.js';
9
9
  export { lowerStreamInput } from './stream-input.js';
10
10
  export { GenSink } from './gen-sink.js';
11
11
  export { matchesAlwaysActive, validateAlwaysActiveEntry } from './always-active.js';
12
+ export { splitToolResultsIntoToolMessages, mergeToolResultsIntoAssistant } from './tool-message-normalizer.js';
12
13
  //# sourceMappingURL=public.js.map
@@ -0,0 +1,44 @@
1
+ import type { Message } from '../types/messages.js';
2
+ /**
3
+ * READ side. Hoists every `tool-result` part that rides on a non-`tool` message
4
+ * (assistant on Mastra, user on Claude) onto its own trailing `role: 'tool'`
5
+ * message, producing the canonical layout described above.
6
+ *
7
+ * Behavior:
8
+ * - **Idempotent.** A `tool-result` already on a `role: 'tool'` message is left
9
+ * untouched, so applying this over already-canonical history (e.g. Claude's
10
+ * `addContext` mirror, or a second pass) is a no-op.
11
+ * - **Order-preserving.** Each hoisted `tool-result` becomes a `role: 'tool'`
12
+ * message inserted immediately after the message it was hoisted from, so a
13
+ * result stays adjacent to its originating `tool-call`.
14
+ * - **toolName backfill.** When a hoisted `tool-result`'s `toolName` is blank
15
+ * (Claude persists `tool_result` blocks without a name), it is backfilled from
16
+ * the matching `tool-call` seen earlier in the same input.
17
+ * - **isError / error survive.** Both flow through to the hoisted part verbatim.
18
+ * - **Empty-message drop.** A source message left with no parts after hoisting
19
+ * (e.g. an assistant message whose only content was a `tool-result`) is
20
+ * dropped rather than emitted as an empty message.
21
+ * - **createdAt.** Each emitted `role: 'tool'` message inherits its source
22
+ * message's `createdAt`, so the harness's ascending-by-`createdAt` sort keeps
23
+ * the tool message at or after its originating message. String-content
24
+ * messages (no parts) pass through untouched.
25
+ */
26
+ export declare function splitToolResultsIntoToolMessages(messages: Message[]): Message[];
27
+ /**
28
+ * WRITE side (inverse of {@link splitToolResultsIntoToolMessages}). Folds each
29
+ * `role: 'tool'` message's `tool-result` part back into the content array of the
30
+ * assistant message that carries the matching `tool-call`, inserting it
31
+ * immediately after that `tool-call` part.
32
+ *
33
+ * The adjacency is load-bearing: Mastra's `mapMessageToMastra` coalesces an
34
+ * adjacent `tool-call` + `tool-result` pair (same `toolCallId`) back into one
35
+ * `tool-invocation(result)`, so folding the result in beside its call keeps the
36
+ * `getMessages()` → `addContext()` round-trip lossless and never double-stores
37
+ * the call.
38
+ *
39
+ * A `role: 'tool'` message whose result has no matching `tool-call` in a prior
40
+ * assistant message is preserved as-is (defensive — a caller-constructed history
41
+ * the SDK didn't produce). History with no `role: 'tool'` messages is returned
42
+ * unchanged.
43
+ */
44
+ export declare function mergeToolResultsIntoAssistant(messages: Message[]): Message[];
@@ -0,0 +1,175 @@
1
+ /*
2
+ * Copyright 2026, Salesforce, Inc. All rights reserved.
3
+ * See LICENSE.txt for license terms.
4
+ */
5
+ /**
6
+ * Canonical cross-harness layout for a completed tool call (#647).
7
+ *
8
+ * A completed tool call MUST surface as TWO messages in `getMessages()`
9
+ * history: an `assistant` message carrying the `tool-call` part (the model's
10
+ * `toolName` + `args`), immediately followed by a separate `role: 'tool'`
11
+ * message carrying the paired `tool-result` part (same `toolCallId`). The
12
+ * `tool-result` appears ONLY on the `role: 'tool'` message — never also on the
13
+ * assistant or a user message.
14
+ *
15
+ * This matches the shape the SDK's `Message` / `MessageRole` types document as
16
+ * canonical ("Aligned with AI SDK `ModelMessage` roles"; `'tool'` = "Results
17
+ * from tool executions") and the AG-UI `ToolMessage` model, so a downstream
18
+ * translator is a pure per-message 1:1 map with no cross-message state.
19
+ *
20
+ * The two production harnesses natively store completed calls differently —
21
+ * Mastra merges call + result into one object on the assistant message; Claude
22
+ * puts the result on the following `user` message (Anthropic wire convention).
23
+ * Both call {@link splitToolResultsIntoToolMessages} at the end of
24
+ * `getMessages()` so consumers see one shape regardless of harness. The write
25
+ * path ({@link mergeToolResultsIntoAssistant}) is the exact inverse, keeping
26
+ * `getMessages()` → `addContext()` round-trips lossless.
27
+ *
28
+ * Both helpers live on `@salesforce/sfdx-agent-sdk/harness` so the two harnesses
29
+ * cannot drift on the normalization — the same precedent as `mcpServerConfigEqual`,
30
+ * `lowerStreamInput`, and `backfillCreatedAt`.
31
+ */
32
+ function isToolCallPart(part) {
33
+ return part.type === 'tool-call';
34
+ }
35
+ function isToolResultPart(part) {
36
+ return part.type === 'tool-result';
37
+ }
38
+ /**
39
+ * READ side. Hoists every `tool-result` part that rides on a non-`tool` message
40
+ * (assistant on Mastra, user on Claude) onto its own trailing `role: 'tool'`
41
+ * message, producing the canonical layout described above.
42
+ *
43
+ * Behavior:
44
+ * - **Idempotent.** A `tool-result` already on a `role: 'tool'` message is left
45
+ * untouched, so applying this over already-canonical history (e.g. Claude's
46
+ * `addContext` mirror, or a second pass) is a no-op.
47
+ * - **Order-preserving.** Each hoisted `tool-result` becomes a `role: 'tool'`
48
+ * message inserted immediately after the message it was hoisted from, so a
49
+ * result stays adjacent to its originating `tool-call`.
50
+ * - **toolName backfill.** When a hoisted `tool-result`'s `toolName` is blank
51
+ * (Claude persists `tool_result` blocks without a name), it is backfilled from
52
+ * the matching `tool-call` seen earlier in the same input.
53
+ * - **isError / error survive.** Both flow through to the hoisted part verbatim.
54
+ * - **Empty-message drop.** A source message left with no parts after hoisting
55
+ * (e.g. an assistant message whose only content was a `tool-result`) is
56
+ * dropped rather than emitted as an empty message.
57
+ * - **createdAt.** Each emitted `role: 'tool'` message inherits its source
58
+ * message's `createdAt`, so the harness's ascending-by-`createdAt` sort keeps
59
+ * the tool message at or after its originating message. String-content
60
+ * messages (no parts) pass through untouched.
61
+ */
62
+ export function splitToolResultsIntoToolMessages(messages) {
63
+ // toolCallId → toolName, harvested from tool-call parts as we walk forward,
64
+ // so a later tool-result with a blank name can be backfilled.
65
+ const toolNameByCallId = new Map();
66
+ for (const msg of messages) {
67
+ if (typeof msg.content === 'string')
68
+ continue;
69
+ for (const part of msg.content) {
70
+ if (isToolCallPart(part) && part.toolName)
71
+ toolNameByCallId.set(part.toolCallId, part.toolName);
72
+ }
73
+ }
74
+ const out = [];
75
+ for (const msg of messages) {
76
+ // String content or a message that is already a tool message passes
77
+ // through unchanged (idempotency + nothing to hoist).
78
+ if (typeof msg.content === 'string' || msg.role === 'tool') {
79
+ out.push(msg);
80
+ continue;
81
+ }
82
+ const kept = [];
83
+ const hoisted = [];
84
+ for (const part of msg.content) {
85
+ if (isToolResultPart(part)) {
86
+ const toolName = part.toolName || toolNameByCallId.get(part.toolCallId) || '';
87
+ hoisted.push({ ...part, toolName });
88
+ }
89
+ else {
90
+ kept.push(part);
91
+ }
92
+ }
93
+ if (hoisted.length === 0) {
94
+ out.push(msg);
95
+ continue;
96
+ }
97
+ // Emit the source message first (minus its tool-results) only if it
98
+ // still carries content; a result-only message is dropped.
99
+ if (kept.length > 0) {
100
+ out.push({ ...msg, content: kept });
101
+ }
102
+ // One role:'tool' message per hoisted result — keeps the downstream
103
+ // AG-UI map a clean 1:1 (one tool-result part → one ToolMessage).
104
+ for (const result of hoisted) {
105
+ out.push({
106
+ id: `${msg.id}:tool:${result.toolCallId}`,
107
+ role: 'tool',
108
+ content: [result],
109
+ createdAt: msg.createdAt,
110
+ });
111
+ }
112
+ }
113
+ return out;
114
+ }
115
+ /**
116
+ * WRITE side (inverse of {@link splitToolResultsIntoToolMessages}). Folds each
117
+ * `role: 'tool'` message's `tool-result` part back into the content array of the
118
+ * assistant message that carries the matching `tool-call`, inserting it
119
+ * immediately after that `tool-call` part.
120
+ *
121
+ * The adjacency is load-bearing: Mastra's `mapMessageToMastra` coalesces an
122
+ * adjacent `tool-call` + `tool-result` pair (same `toolCallId`) back into one
123
+ * `tool-invocation(result)`, so folding the result in beside its call keeps the
124
+ * `getMessages()` → `addContext()` round-trip lossless and never double-stores
125
+ * the call.
126
+ *
127
+ * A `role: 'tool'` message whose result has no matching `tool-call` in a prior
128
+ * assistant message is preserved as-is (defensive — a caller-constructed history
129
+ * the SDK didn't produce). History with no `role: 'tool'` messages is returned
130
+ * unchanged.
131
+ */
132
+ export function mergeToolResultsIntoAssistant(messages) {
133
+ const out = [];
134
+ for (const msg of messages) {
135
+ if (msg.role !== 'tool' || typeof msg.content === 'string') {
136
+ out.push(msg);
137
+ continue;
138
+ }
139
+ const results = msg.content.filter(isToolResultPart);
140
+ const nonResults = msg.content.filter((p) => !isToolResultPart(p));
141
+ const unmerged = [];
142
+ for (const result of results) {
143
+ if (!foldResultIntoAssistant(out, result))
144
+ unmerged.push(result);
145
+ }
146
+ // Anything we couldn't place (no matching prior tool-call) or any
147
+ // non-result parts on the tool message stay on a preserved tool message
148
+ // so we never silently drop content.
149
+ const leftover = [...nonResults, ...unmerged];
150
+ if (leftover.length > 0) {
151
+ out.push({ ...msg, content: leftover });
152
+ }
153
+ }
154
+ return out;
155
+ }
156
+ /**
157
+ * Inserts `result` immediately after the matching `tool-call` in the most recent
158
+ * assistant message in `out` that carries it. Returns `true` when placed.
159
+ */
160
+ function foldResultIntoAssistant(out, result) {
161
+ for (let i = out.length - 1; i >= 0; i--) {
162
+ const candidate = out[i];
163
+ if (candidate.role !== 'assistant' || typeof candidate.content === 'string')
164
+ continue;
165
+ const callIndex = candidate.content.findIndex((p) => isToolCallPart(p) && p.toolCallId === result.toolCallId);
166
+ if (callIndex === -1)
167
+ continue;
168
+ const merged = [...candidate.content];
169
+ merged.splice(callIndex + 1, 0, result);
170
+ out[i] = { ...candidate, content: merged };
171
+ return true;
172
+ }
173
+ return false;
174
+ }
175
+ //# sourceMappingURL=tool-message-normalizer.js.map
package/dist/index.d.ts CHANGED
@@ -5,8 +5,8 @@ export { BUILT_IN_TOOL_POLICIES, SKILL_BRIDGE_SERVER_ID, definePolicy, matcherMa
5
5
  export type { ResolverResult, ResolverTiers, ToolInvocation } from './policy-resolver.js';
6
6
  export type { ContextUsage, FinishReason, UsageMetadata } from './types/usage.js';
7
7
  export type { AgentHooks, HooksForAgent, ToolResultRedactor, ToolResultRedactionInput, ToolResultRedactionResult, } from './types/redaction.js';
8
- export type { AgentConfig, HarnessAgentConfig, StreamOptions, ToolApprovalMode } from './harness/harness-config.js';
9
- export { DEFAULT_MAX_STEPS, resolveToolApprovalMode } from './harness/harness-config.js';
8
+ export type { AgentConfig, HarnessAgentConfig, StreamOptions } from './harness/harness-config.js';
9
+ export { DEFAULT_MAX_STEPS } from './harness/harness-config.js';
10
10
  export type { MCPConfiguration, MCPServerConfig, MCPStdioServerConfig, MCPRemoteServerConfig, McpServerInfo, McpServerErrorCategory, McpServerErrorDetail, McpToolInfo, McpToolAnnotations, } from './mcp-config.js';
11
11
  export { McpServerStatus, mcpServerConfigEqual } from './mcp-config.js';
12
12
  export { Model, ModelName, createClaudeModel, Models, validateMultimodalFiles } from './models/index.js';
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@
8
8
  // `resolveToolApprovalPolicy` into their gate sites (Phase 2); consumers author
9
9
  // `AgentConfig.toolPolicies` (directly or via `definePolicy`).
10
10
  export { BUILT_IN_TOOL_POLICIES, SKILL_BRIDGE_SERVER_ID, definePolicy, matcherMatches, resolveToolApprovalPolicy, } from './policy-resolver.js';
11
- export { DEFAULT_MAX_STEPS, resolveToolApprovalMode } from './harness/harness-config.js';
11
+ export { DEFAULT_MAX_STEPS } from './harness/harness-config.js';
12
12
  export { McpServerStatus, mcpServerConfigEqual } from './mcp-config.js';
13
13
  export { Model, ModelName, createClaudeModel, Models, validateMultimodalFiles } from './models/index.js';
14
14
  export { MimeType } from './models/index.js';
@@ -36,8 +36,10 @@ export type ResolverTiers = {
36
36
  /** Harness-shipped built-in rules (e.g. `MASTRA_BUILT_IN_TOOL_POLICIES`). */
37
37
  harness?: ReadonlyArray<ToolPolicyRule>;
38
38
  /**
39
- * Synthetic rules from harness factory inputs during a deprecation window.
40
- * Today: Claude's `bypassApprovalTools` translation. Empty after Phase 4.
39
+ * Reserved slot for synthetic rules a harness factory injects between the
40
+ * harness built-ins and the consumer's `toolPolicies`. No harness populates
41
+ * it today; it is kept so a future factory-injected tier can slot in
42
+ * without reshaping the resolver signature or the concatenation order.
41
43
  */
42
44
  factory?: ReadonlyArray<ToolPolicyRule>;
43
45
  };
@@ -21,8 +21,8 @@ import type { McpToolAnnotations } from '../mcp-config.js';
21
21
  * For harness-executed (native) tools such as MCP tools, use
22
22
  * `AgentConfig.mcpServers` — those run in the harness and never need
23
23
  * `submitToolResult`. To gate harness-executed tool calls with human approval,
24
- * use `StreamOptions.requireToolApproval` plus `approveToolCall` /
25
- * `declineToolCall`.
24
+ * configure `AgentConfig.toolPolicies` / `AgentConfig.defaultToolDecision`
25
+ * plus `approveToolCall` / `declineToolCall`.
26
26
  *
27
27
  * Structurally compatible with AI SDK `LanguageModelV4FunctionTool`.
28
28
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/sfdx-agent-sdk",
3
- "version": "0.31.0",
3
+ "version": "0.33.0",
4
4
  "description": "Harness-agnostic agentic infrastructure for Salesforce developer experience tooling",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -46,23 +46,23 @@
46
46
  },
47
47
  "devDependencies": {
48
48
  "@eslint/js": "^10.0.1",
49
- "@salesforce/sfdx-agent-harness-claude": "0.27.0",
50
- "@salesforce/sfdx-agent-harness-mastra": "0.30.0",
49
+ "@salesforce/sfdx-agent-harness-claude": "0.29.0",
50
+ "@salesforce/sfdx-agent-harness-mastra": "0.32.0",
51
51
  "@types/node": "^22.20.0",
52
- "@vitest/coverage-istanbul": "^4.1.8",
53
- "@vitest/eslint-plugin": "^1.6.20",
54
- "eslint": "^10.6.0",
52
+ "@vitest/coverage-istanbul": "^4.1.10",
53
+ "@vitest/eslint-plugin": "^1.6.23",
54
+ "eslint": "^10.7.0",
55
55
  "eslint-config-prettier": "^10.1.8",
56
56
  "eslint-import-resolver-typescript": "^4.4.5",
57
57
  "eslint-plugin-import": "^2.32.0",
58
- "eslint-plugin-n": "^18.2.1",
58
+ "eslint-plugin-n": "^18.2.2",
59
59
  "globals": "^17.6.0",
60
60
  "lint-staged": "^17.0.7",
61
61
  "prettier": "^3.9.4",
62
62
  "rimraf": "^6.1.3",
63
63
  "tsx": "^4.23.0",
64
64
  "typescript": "^6.0.3",
65
- "typescript-eslint": "^8.62.1",
65
+ "typescript-eslint": "^8.63.0",
66
66
  "vitest": "^4.1.8"
67
67
  },
68
68
  "engines": {