@salesforce/sfdx-agent-harness-mastra 0.59.0 → 0.61.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,20 @@
3
3
  All notable changes to `@salesforce/sfdx-agent-harness-mastra` are documented in this file.
4
4
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
+ ## [0.61.0] - 2026-09-01
7
+
8
+ ### Fixes
9
+ - **harness-mastra**: forward abortSignal into provider gateway fetch @W-23988781@ ([#779](https://github.com/forcedotcom/agentic-dx/pull/779))
10
+
11
+ ## [0.60.0] - 2026-09-01
12
+
13
+ ### Features
14
+ - **agent-sdk**: add policy-aware tool declines @W-24048333@ ([#776](https://github.com/forcedotcom/agentic-dx/pull/776))
15
+
16
+ ### Chores
17
+ - **deps-dev**: bump typescript-eslint from 8.67.0 to 8.68.0 in the dev-dependencies group across 1 directory ([#777](https://github.com/forcedotcom/agentic-dx/pull/777))
18
+ - **deps-dev**: bump eslint from 10.8.1 to 10.9.1 in the eslint group ([#778](https://github.com/forcedotcom/agentic-dx/pull/778))
19
+
6
20
  ## [0.59.0] - 2026-08-28
7
21
 
8
22
  ### Fixes
package/README.md CHANGED
@@ -249,6 +249,11 @@ Consumer-executed tools (`AgentConfig.tools`) always bypass the gate — their e
249
249
  via `submitToolResult`. Set `StreamOptions.batchApprovals: true` to surface parallel `require-approval` requests on one
250
250
  stream for a batch-approval UI.
251
251
 
252
+ With `@mastra/core` 1.62.0, an organization-policy decline is passed through `agent.declineToolCall({ reason })`. Mastra
253
+ emits one native `tool-output-denied` chunk; the adapter maps it to one `tool-result(isError=true)` carrying the same
254
+ exact message the model sees. Automatic policy denies use `Denied by tool policy.`; reasonless consumer declines retain
255
+ Mastra's `Tool call was not approved by the user` default.
256
+
252
257
  ### Tool approval timeout
253
258
 
254
259
  When the approval gate is engaged and a tool resolves to `require-approval`, the harness suspends the workflow on each
@@ -1,7 +1,7 @@
1
1
  import type { Agent } from '@mastra/core/agent';
2
2
  import type { RequestContext } from '@mastra/core/request-context';
3
3
  import type { ChunkType } from '@mastra/core/stream';
4
- import { type ChatEvent, type Decision as PolicyDecision, type McpToolAnnotations, type ToolInvocation, type WireCommunicationEvent } from '@salesforce/sfdx-agent-sdk';
4
+ import { type ChatEvent, type Decision as PolicyDecision, type McpToolAnnotations, type ToolDeclineReason, type ToolInvocation, type WireCommunicationEvent } from '@salesforce/sfdx-agent-sdk';
5
5
  import { GenSink } from '@salesforce/sfdx-agent-sdk/harness';
6
6
  import type { Clock, LogBus } from '@salesforce/agentic-common';
7
7
  /**
@@ -125,8 +125,8 @@ export interface MastraApprovalCoordinatorOptions {
125
125
  * Responsibilities:
126
126
  * - Policy-driven approval gating (Phase 2a): for each non-consumer tool,
127
127
  * consult `lookupPolicy` — `allow` silently approves, `deny` silently
128
- * declines (the coordinator synthesizes a `tool-result(isError=true)` and
129
- * drives `agent.declineToolCall` so the model sees the failed call),
128
+ * declines through `agent.declineToolCall` so Mastra emits one native
129
+ * `tool-output-denied` result for the model and event adapter,
130
130
  * `require-approval` emits a `tool-approval-request` and drives
131
131
  * `agent.approveToolCall(...)` / `declineToolCall(...)` when the consumer
132
132
  * settles. `batchApprovals` controls whether parallel `require-approval`
@@ -337,7 +337,7 @@ export declare class MastraApprovalCoordinator {
337
337
  */
338
338
  private teardown;
339
339
  approve(toolCallId: string): void;
340
- decline(toolCallId: string): void;
340
+ decline(toolCallId: string, reason?: ToolDeclineReason): void;
341
341
  private recordDecision;
342
342
  /**
343
343
  * Parker callback the consumer-tool bridge `execute` handler invokes
@@ -3,7 +3,7 @@
3
3
  * See LICENSE.txt for license terms.
4
4
  */
5
5
  import { AgentSDKError, AgentSDKErrorType, } from '@salesforce/sfdx-agent-sdk';
6
- import { GenSink } from '@salesforce/sfdx-agent-sdk/harness';
6
+ import { GenSink, resolveToolDeclineModelMessage } from '@salesforce/sfdx-agent-sdk/harness';
7
7
  import { MastraEventAdapter } from './mastra-event-adapter.js';
8
8
  import { SKILL_BRIDGE_SERVER_ID } from './skill-bridge.js';
9
9
  /**
@@ -22,13 +22,7 @@ function pickIdWithDecision(pendingIds, decisions) {
22
22
  }
23
23
  return undefined;
24
24
  }
25
- /**
26
- * Result text the coordinator stamps on the synthesized `tool-result` for a
27
- * policy-denied tool call. Matches the text Mastra folds into the model's
28
- * context on decline ("Tool call was not approved by the user") so the
29
- * consumer-facing event and the model's transcript stay consistent.
30
- */
31
- const POLICY_DENIED_RESULT_TEXT = 'Tool call was not approved by the user';
25
+ const POLICY_DENIED_RESULT_TEXT = 'Denied by tool policy.';
32
26
  /**
33
27
  * Per-`(agentId, threadId)` coordinator that subsumes the pre-#529
34
28
  * `MastraStreamBroker` + `activeRuns` pair into one object owning a single
@@ -37,8 +31,8 @@ const POLICY_DENIED_RESULT_TEXT = 'Tool call was not approved by the user';
37
31
  * Responsibilities:
38
32
  * - Policy-driven approval gating (Phase 2a): for each non-consumer tool,
39
33
  * consult `lookupPolicy` — `allow` silently approves, `deny` silently
40
- * declines (the coordinator synthesizes a `tool-result(isError=true)` and
41
- * drives `agent.declineToolCall` so the model sees the failed call),
34
+ * declines through `agent.declineToolCall` so Mastra emits one native
35
+ * `tool-output-denied` result for the model and event adapter,
42
36
  * `require-approval` emits a `tool-approval-request` and drives
43
37
  * `agent.approveToolCall(...)` / `declineToolCall(...)` when the consumer
44
38
  * settles. `batchApprovals` controls whether parallel `require-approval`
@@ -353,8 +347,11 @@ export class MastraApprovalCoordinator {
353
347
  approve(toolCallId) {
354
348
  this.recordDecision('approve', toolCallId, { kind: 'approve' });
355
349
  }
356
- decline(toolCallId) {
357
- this.recordDecision('decline', toolCallId, { kind: 'decline' });
350
+ decline(toolCallId, reason) {
351
+ this.recordDecision('decline', toolCallId, {
352
+ kind: 'decline',
353
+ ...(reason ? { modelMessage: resolveToolDeclineModelMessage(reason) } : {}),
354
+ });
358
355
  }
359
356
  recordDecision(method, toolCallId, decision) {
360
357
  // Issue #589: idempotent settle. A tool call the coordinator
@@ -618,36 +615,12 @@ export class MastraApprovalCoordinator {
618
615
  this.decisions.set(tc.toolCallId, { kind: 'approve' });
619
616
  }
620
617
  else if (decision === 'deny') {
621
- // Silent decline — no `tool-approval-request` surfaces.
622
- // The pump drives `agent.declineToolCall` so Mastra folds
623
- // the denial into the model's context (persisted as the
624
- // `role: 'tool'` message "Tool call was not approved by the
625
- // user"), letting the model recover on the resume stream.
626
- //
627
- // We synthesize the consumer-facing `tool-result(isError=true)`
628
- // ourselves rather than relying on Mastra to emit one:
629
- // @mastra/core 1.49 no longer emits a standalone
630
- // `tool-result` chunk on the decline-resume `fullStream`
631
- // (the denial text lives only inside the persisted
632
- // `messages.all` payload embedded in `step-finish`), so the
633
- // old "re-stamp Mastra's native result" path never fired and
634
- // the SDK `Decision` contract "a denied tool surfaces as
635
- // `tool-result(isError=true)`" was silently broken. Routing
636
- // it here — immediately after the already-routed `tool-call`
637
- // and before the decline resume — keeps the natural
638
- // tool-call → tool-result ordering the SDK telemetry deriver
639
- // matches on. The un-emitted approval is marked
640
- // coordinator-settled so a (spurious) consumer
641
- // `approveToolCall` for this id is a no-op rather than a
642
- // TOOL_CALL_NOT_FOUND throw.
618
+ // Mastra emits the native `tool-output-denied` chunk
619
+ // carrying this same reason on the resumed stream.
643
620
  this.settledByCoordinator.add(tc.toolCallId);
644
- this.decisions.set(tc.toolCallId, { kind: 'decline' });
645
- this.routeEnriched({
646
- type: 'tool-result',
647
- toolCallId: tc.toolCallId,
648
- toolName: tc.toolName,
649
- result: POLICY_DENIED_RESULT_TEXT,
650
- isError: true,
621
+ this.decisions.set(tc.toolCallId, {
622
+ kind: 'decline',
623
+ modelMessage: POLICY_DENIED_RESULT_TEXT,
651
624
  });
652
625
  }
653
626
  else if (!this.batchApprovals && !this.emittedApprovals.has(tc.toolCallId)) {
@@ -710,6 +683,7 @@ export class MastraApprovalCoordinator {
710
683
  toolCallId,
711
684
  memory: this.memory,
712
685
  requestContext: this.requestContext,
686
+ ...(decision.modelMessage ? { reason: decision.modelMessage } : {}),
713
687
  });
714
688
  // Post-await guard: abort/dispose may have fired during the
715
689
  // round-trip. Release the new run if so.
@@ -33,6 +33,7 @@ import type { ChatEvent } from '@salesforce/sfdx-agent-sdk';
33
33
  * | `tool-call-approval` | `tool-approval-request` | `{ toolCall: { toolCallId, toolName, args } }` — stream suspends |
34
34
  * | `tool-result` | `tool-result` | `{ toolCallId, toolName, result, isError: false }` |
35
35
  * | `tool-error` | `tool-result` | `{ toolCallId, toolName, result: error, isError: true}`|
36
+ * | `tool-output-denied` | `tool-result` | `{ toolCallId, toolName, result: reason, isError: true}`|
36
37
  * | `tool-output` | `tool-progress` | `{ toolCallId, toolName, output }` |
37
38
  * | `tool-call-delta` | `tool-call-delta` | `{ toolCallId, toolName?, argsTextDelta }` |
38
39
  * | `step-start` | `step-start` | `{ stepIndex }` from step metadata |
@@ -35,6 +35,7 @@ import { isNetworkStreamError } from './stream-errors.js';
35
35
  * | `tool-call-approval` | `tool-approval-request` | `{ toolCall: { toolCallId, toolName, args } }` — stream suspends |
36
36
  * | `tool-result` | `tool-result` | `{ toolCallId, toolName, result, isError: false }` |
37
37
  * | `tool-error` | `tool-result` | `{ toolCallId, toolName, result: error, isError: true}`|
38
+ * | `tool-output-denied` | `tool-result` | `{ toolCallId, toolName, result: reason, isError: true}`|
38
39
  * | `tool-output` | `tool-progress` | `{ toolCallId, toolName, output }` |
39
40
  * | `tool-call-delta` | `tool-call-delta` | `{ toolCallId, toolName?, argsTextDelta }` |
40
41
  * | `step-start` | `step-start` | `{ stepIndex }` from step metadata |
@@ -117,6 +118,14 @@ export class MastraEventAdapter {
117
118
  error,
118
119
  };
119
120
  }
121
+ case 'tool-output-denied':
122
+ return {
123
+ type: 'tool-result',
124
+ toolCallId: chunk.payload.toolCallId,
125
+ toolName: chunk.payload.toolName,
126
+ result: chunk.payload.approval.reason ?? 'Tool call was not approved by the user',
127
+ isError: true,
128
+ };
120
129
  case 'step-start':
121
130
  return { type: 'step-start', stepIndex: this.stepIndex++ };
122
131
  case 'step-finish':
@@ -1,7 +1,7 @@
1
1
  import { Agent } from '@mastra/core/agent';
2
2
  import { SkillSearchProcessor, ToolSearchProcessor } from '@mastra/core/processors';
3
3
  import { MCPClient } from '@mastra/mcp';
4
- import { McpServerStatus, type AgentHarness, type WithAgentConfig, type ChatStreamResult, type HarnessAgentConfig, type MCPConfiguration, type McpServerErrorDetail, type McpServerInfo, type Message, type MessagePart, type McpAuthProviders, type ModelConnectivityInfo, type SessionContext, type StreamOptions, type TelemetryEventCallback, type ToolResultInfo, type WireCommunicationEventCallback } from '@salesforce/sfdx-agent-sdk';
4
+ import { McpServerStatus, type AgentHarness, type WithAgentConfig, type ChatStreamResult, type HarnessAgentConfig, type MCPConfiguration, type McpServerErrorDetail, type McpServerInfo, type Message, type MessagePart, type McpAuthProviders, type ModelConnectivityInfo, type SessionContext, type StreamOptions, type TelemetryEventCallback, type ToolDeclineReason, type ToolResultInfo, type WireCommunicationEventCallback } from '@salesforce/sfdx-agent-sdk';
5
5
  import { type AgentHooks } from '@salesforce/sfdx-agent-sdk/harness';
6
6
  import type { MastraLanguageModelBuilderContext } from './index.js';
7
7
  import { MastraApprovalCoordinator } from './mastra-approval-coordinator.js';
@@ -563,7 +563,7 @@ export declare class MastraHarness implements MastraAgentHarness {
563
563
  * except the coordinator drives `agent.declineToolCall()` instead of
564
564
  * `agent.approveToolCall()`.
565
565
  */
566
- declineToolCall(agentId: string, threadId: string, toolCallId: string): Promise<void>;
566
+ declineToolCall(agentId: string, threadId: string, toolCallId: string, reason?: ToolDeclineReason): Promise<void>;
567
567
  private getCoordinatorOrThrow;
568
568
  /**
569
569
  * @requirements
@@ -1751,9 +1751,12 @@ export class MastraHarness {
1751
1751
  * except the coordinator drives `agent.declineToolCall()` instead of
1752
1752
  * `agent.approveToolCall()`.
1753
1753
  */
1754
- async declineToolCall(agentId, threadId, toolCallId) {
1754
+ async declineToolCall(agentId, threadId, toolCallId, reason) {
1755
1755
  const coordinator = this.getCoordinatorOrThrow(agentId, threadId, 'declineToolCall');
1756
- coordinator.decline(toolCallId);
1756
+ if (reason)
1757
+ coordinator.decline(toolCallId, reason);
1758
+ else
1759
+ coordinator.decline(toolCallId);
1757
1760
  }
1758
1761
  getCoordinatorOrThrow(agentId, threadId, method) {
1759
1762
  const agentState = this.getAgentStateOrThrow(agentId);
@@ -170,7 +170,10 @@ async function buildAnthropicRequest(ctx, options) {
170
170
  }
171
171
  async function doGenerate(ctx, options) {
172
172
  const { client, commonParams } = await buildAnthropicRequest(ctx, options);
173
- const message = await client.messages.create({ ...commonParams, stream: false });
173
+ // Forward the turn's AbortSignal into the SDK's RequestOptions so firing the
174
+ // controller (POST /cancel, idle timeout, shutdown abortAll) actually aborts
175
+ // the outbound gateway fetch, not just harness bookkeeping (W-23988781).
176
+ const message = await client.messages.create({ ...commonParams, stream: false }, { signal: options.abortSignal });
174
177
  const content = [];
175
178
  let sawToolCalls = false;
176
179
  for (const block of message.content) {
@@ -201,7 +204,9 @@ async function doGenerate(ctx, options) {
201
204
  }
202
205
  async function doStream(ctx, options) {
203
206
  const { client, commonParams } = await buildAnthropicRequest(ctx, options);
204
- const sdkStream = await client.messages.create({ ...commonParams, stream: true });
207
+ // See doGenerate: thread the AbortSignal into the SDK so a mid-stream abort
208
+ // tears down the live gateway socket (W-23988781).
209
+ const sdkStream = await client.messages.create({ ...commonParams, stream: true }, { signal: options.abortSignal });
205
210
  const stream = new ReadableStream({
206
211
  start: async (controller) => {
207
212
  const textSpan = new TextSpanLifecycle();
@@ -78,7 +78,10 @@ async function buildRequest(ctx, options) {
78
78
  async function doGenerate(ctx, options) {
79
79
  const { client, commonParams } = await buildRequest(ctx, options);
80
80
  const params = { ...commonParams };
81
- const response = await client.chat.completions.create(params);
81
+ // Forward the turn's AbortSignal into the SDK's RequestOptions so firing the
82
+ // controller (POST /cancel, idle timeout, shutdown abortAll) actually aborts
83
+ // the outbound gateway fetch, not just harness bookkeeping (W-23988781).
84
+ const response = await client.chat.completions.create(params, { signal: options.abortSignal });
82
85
  const content = [];
83
86
  const choice = response.choices[0];
84
87
  let sawToolCalls = false;
@@ -120,7 +123,9 @@ async function doStream(ctx, options) {
120
123
  // to `undefined` `LanguageModelV3Usage` per the missing-usage contract.
121
124
  stream_options: { include_usage: true },
122
125
  };
123
- const sdkStream = await client.chat.completions.create(params);
126
+ // See doGenerate: thread the AbortSignal into the SDK so a mid-stream abort
127
+ // tears down the live gateway socket (W-23988781).
128
+ const sdkStream = await client.chat.completions.create(params, { signal: options.abortSignal });
124
129
  const stream = new ReadableStream({
125
130
  start: async (controller) => {
126
131
  const textSpan = new TextSpanLifecycle();
@@ -76,7 +76,10 @@ async function buildOpenAIRequest(ctx, options) {
76
76
  async function doGenerate(ctx, options) {
77
77
  const { client, commonParams } = await buildOpenAIRequest(ctx, options);
78
78
  const params = { ...commonParams };
79
- const response = await client.responses.create(params);
79
+ // Forward the turn's AbortSignal into the SDK's RequestOptions so firing the
80
+ // controller (POST /cancel, idle timeout, shutdown abortAll) actually aborts
81
+ // the outbound gateway fetch, not just harness bookkeeping (W-23988781).
82
+ const response = await client.responses.create(params, { signal: options.abortSignal });
80
83
  const content = [];
81
84
  let sawToolCalls = false;
82
85
  if (response.output_text && response.output_text.length > 0) {
@@ -103,7 +106,9 @@ async function doGenerate(ctx, options) {
103
106
  async function doStream(ctx, options) {
104
107
  const { client, commonParams } = await buildOpenAIRequest(ctx, options);
105
108
  const params = { ...commonParams, stream: true };
106
- const sdkStream = await client.responses.create(params);
109
+ // See doGenerate: thread the AbortSignal into the SDK so a mid-stream abort
110
+ // tears down the live gateway socket (W-23988781).
111
+ const sdkStream = await client.responses.create(params, { signal: options.abortSignal });
107
112
  const stream = new ReadableStream({
108
113
  start: async (controller) => {
109
114
  const textSpan = new TextSpanLifecycle();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/sfdx-agent-harness-mastra",
3
- "version": "0.59.0",
3
+ "version": "0.61.0",
4
4
  "description": "Mastra-backed AgentHarness implementation for @salesforce/sfdx-agent-sdk",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -42,16 +42,16 @@
42
42
  "openai": "^7.0.0"
43
43
  },
44
44
  "peerDependencies": {
45
- "@salesforce/sfdx-agent-sdk": "0.60.0"
45
+ "@salesforce/sfdx-agent-sdk": "0.62.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@eslint/js": "^10.0.1",
49
49
  "@salesforce/harness-conformance": "0.0.1",
50
- "@salesforce/sfdx-agent-sdk": "0.60.0",
50
+ "@salesforce/sfdx-agent-sdk": "0.62.0",
51
51
  "@types/node": "^22.20.1",
52
52
  "@vitest/coverage-istanbul": "^4.1.10",
53
53
  "@vitest/eslint-plugin": "^1.6.27",
54
- "eslint": "^10.8.1",
54
+ "eslint": "^10.9.1",
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",
@@ -62,7 +62,7 @@
62
62
  "rimraf": "^6.1.3",
63
63
  "tsx": "^4.23.12",
64
64
  "typescript": "^7.0.2",
65
- "typescript-eslint": "^8.67.0",
65
+ "typescript-eslint": "^8.68.0",
66
66
  "vitest": "^4.1.8"
67
67
  },
68
68
  "engines": {