@salesforce/sfdx-agent-harness-mastra 0.77.0 → 0.79.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,16 @@
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.79.0] - 2026-09-14
7
+
8
+ ### Fixes
9
+ - **harness-mastra**: stamp Anthropic cache_control in the Mastra pass-through @W-24130072@ ([#811](https://github.com/forcedotcom/agentic-dx/pull/811))
10
+
11
+ ## [0.78.0] - 2026-09-14
12
+
13
+ ### Features
14
+ - add ChatSession.cancelTurn() with abort-persistence barrier @W-23559264@ ([#810](https://github.com/forcedotcom/agentic-dx/pull/810))
15
+
6
16
  ## [0.77.0] - 2026-09-09
7
17
 
8
18
  _No changes — released alongside dependent packages._
@@ -275,7 +275,9 @@ export class MastraApprovalCoordinator {
275
275
  this.aborted = true;
276
276
  const abortError = new Error('The stream was aborted.');
277
277
  abortError.name = 'AbortError';
278
- this.teardown('abort', abortError, 'aborted');
278
+ // Canonical cancellation code across harnesses (was harness-specific
279
+ // `'aborted'`): the abort teardown ends the turn with `finish('cancelled')`.
280
+ this.teardown('abort', abortError, 'cancelled');
279
281
  }
280
282
  dispose() {
281
283
  if (this.disposed)
@@ -357,8 +359,12 @@ export class MastraApprovalCoordinator {
357
359
  const resolve = this.continuationResolver;
358
360
  this.continuationResolver = undefined;
359
361
  resolve?.('done');
362
+ // A cancellation (abort) ends the turn with `finish('cancelled')`, never
363
+ // `error` — the terminal reason a cancelled turn surfaces per the SDK
364
+ // contract. Every other teardown reason stays `error`.
365
+ const finishReason = reason === 'abort' ? 'cancelled' : 'error';
360
366
  this.initial.push({ type: 'error', error, code });
361
- this.initial.push({ type: 'finish', finishReason: 'error' });
367
+ this.initial.push({ type: 'finish', finishReason });
362
368
  this.initial.end();
363
369
  this.pendingSuspendIds.clear();
364
370
  this.runTeardownCallbacks();
@@ -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 ToolDeclineReason, type ToolResultInfo, type WireCommunicationEventCallback } from '@salesforce/sfdx-agent-sdk';
4
+ import { McpServerStatus, type AgentHarness, type WithAgentConfig, type HarnessStreamResult, 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';
@@ -520,7 +520,8 @@ export declare class MastraHarness implements MastraAgentHarness {
520
520
  * - MUST throw an error if the specified `agentId` is not found in `activeAgents`.
521
521
  * - MUST invoke `agent.stream()` with memory context for the thread.
522
522
  * - MUST capture the `runId` for tool approval flows.
523
- * - MUST return a `ChatStreamResult` with `textStream` and `eventStream`.
523
+ * - MUST return a `HarnessStreamResult` with `textStream`, `eventStream`, and the `settled`
524
+ * barrier (coordinator sink ended + Mastra transcript save flushed) for `cancelTurn()`.
524
525
  * - On pre-stream rejection from `agent.stream()`, MUST re-throw without emitting telemetry —
525
526
  * the SDK's chat-session boundary catches the rejection and emits `chat-stream-error`.
526
527
  * - MUST install a per-run abort listener on `options.abortSignal` so that aborting
@@ -571,7 +572,7 @@ export declare class MastraHarness implements MastraAgentHarness {
571
572
  * harness; this method only supplies the Mastra-specific lowering of each part.
572
573
  */
573
574
  private toMastraStreamInput;
574
- stream(agentId: string, threadId: string, message: string | MessagePart[], options?: StreamOptions): Promise<ChatStreamResult>;
575
+ stream(agentId: string, threadId: string, message: string | MessagePart[], options?: StreamOptions): Promise<HarnessStreamResult>;
575
576
  /**
576
577
  * @requirements
577
578
  * - MUST throw an error if the specified `agentId` is not found in `activeAgents`.
@@ -1648,7 +1648,8 @@ export class MastraHarness {
1648
1648
  * - MUST throw an error if the specified `agentId` is not found in `activeAgents`.
1649
1649
  * - MUST invoke `agent.stream()` with memory context for the thread.
1650
1650
  * - MUST capture the `runId` for tool approval flows.
1651
- * - MUST return a `ChatStreamResult` with `textStream` and `eventStream`.
1651
+ * - MUST return a `HarnessStreamResult` with `textStream`, `eventStream`, and the `settled`
1652
+ * barrier (coordinator sink ended + Mastra transcript save flushed) for `cancelTurn()`.
1652
1653
  * - On pre-stream rejection from `agent.stream()`, MUST re-throw without emitting telemetry —
1653
1654
  * the SDK's chat-session boundary catches the rejection and emits `chat-stream-error`.
1654
1655
  * - MUST install a per-run abort listener on `options.abortSignal` so that aborting
@@ -1845,9 +1846,21 @@ export class MastraHarness {
1845
1846
  }
1846
1847
  }
1847
1848
  coordinator.start({ runId: output.runId, fullStream: output.fullStream });
1849
+ // Settle barrier for `ChatSession.cancelTurn()`: resolves only once the
1850
+ // coordinator's per-turn sink has ended (natural finish or abort teardown)
1851
+ // AND Mastra's own stream completion has flushed — its `onFinish` / LibSQL
1852
+ // transcript save runs on natural finish and, since `@mastra/core >= 1.62`,
1853
+ // on abort too (W-24001006). Awaiting `output.finishReason` waits out that
1854
+ // save; it never rejects the barrier (an aborted turn is over either way).
1855
+ const coordinatorSettled = new Promise((resolve) => coordinator.onTeardown(resolve));
1856
+ // `Promise.resolve(...)` tolerates a test-stub output with no `finishReason`
1857
+ // (production `MastraModelOutput` always exposes the promise).
1858
+ const persistenceFlushed = Promise.resolve(output.finishReason).then(() => undefined, () => undefined);
1859
+ const settled = Promise.all([coordinatorSettled, persistenceFlushed]).then(() => undefined);
1848
1860
  return {
1849
1861
  eventStream: coordinator.initialSink().generator(),
1850
1862
  textStream: this.readableStreamToAsyncGenerator(output.textStream),
1863
+ settled,
1851
1864
  };
1852
1865
  }
1853
1866
  // ── Tool Execution ───────────────────────────────────────────────
@@ -105,6 +105,19 @@ export declare function buildAnthropicDirectLanguageModel(opts: {
105
105
  /** Per-read idle timeout (ms) for the gateway response body; see `createGatewayFetch`. */
106
106
  idleReadTimeoutMs?: number;
107
107
  }): LanguageModelV3;
108
+ /**
109
+ * Stamps `cache_control: {type:'ephemeral'}` on up to three of Anthropic's four
110
+ * allowed breakpoints: the last tool (caches the constant tools block), the
111
+ * system prompt (sent as a `text` block carrying the breakpoint), and a
112
+ * rolling breakpoint on the last cache-eligible block of the last message —
113
+ * the growing history is the majority of per-step input on a multi-step turn,
114
+ * so this breakpoint is load-bearing, not incidental: dropping it would leave
115
+ * only the turn-constant tools/system cached and forfeit most of the win.
116
+ * Mutates only `commonParams`'s own arrays — which
117
+ * `translate*ToAnthropic` builds fresh per request — never the caller's
118
+ * original `LanguageModelV3` prompt/tool objects.
119
+ */
120
+ export declare function applyCacheBreakpoints(commonParams: Omit<Anthropic.Messages.MessageCreateParamsStreaming, 'stream'>): void;
108
121
  /**
109
122
  * Translates a V3 `LanguageModelV3Prompt` into Anthropic's Messages API shape
110
123
  * — top-level `system` string + `MessageParam[]` of role-tagged content blocks.
@@ -177,8 +177,76 @@ async function buildAnthropicRequest(ctx, options) {
177
177
  }
178
178
  if (options.stopSequences !== undefined)
179
179
  commonParams.stop_sequences = [...options.stopSequences];
180
+ // Anthropic caching is opt-in per request (unlike OpenAI's automatic
181
+ // prefix cache) — nothing caches without an explicit `cache_control`
182
+ // breakpoint. `Model.supportsPromptCache` is the gate (`true` for the
183
+ // Claude family, `false` for GPT); see `applyCacheBreakpoints`.
184
+ if (info.model.supportsPromptCache && !isPromptCacheKillSwitchEngaged())
185
+ applyCacheBreakpoints(commonParams);
180
186
  return { client, commonParams, toolSchemas };
181
187
  }
188
+ /**
189
+ * Internal, undocumented escape hatch — NOT a consumer-facing `AgentConfig`
190
+ * field or a documented environment variable. Exists solely so an operator can
191
+ * disable cache-control stamping process-wide if a provider-side caching bug
192
+ * surfaces in production, without a code rollback. Read fresh on every request
193
+ * (not cached) so it can be flipped at runtime.
194
+ */
195
+ function isPromptCacheKillSwitchEngaged() {
196
+ return process.env['SF_MASTRA_DISABLE_PROMPT_CACHE'] === 'true';
197
+ }
198
+ /** Content-block types that accept `cache_control` on the Anthropic Messages
199
+ * API. Deliberately excludes `thinking` / `redacted_thinking`, which reject it. */
200
+ const CACHE_ELIGIBLE_BLOCK_TYPES = new Set([
201
+ 'text',
202
+ 'tool_use',
203
+ 'tool_result',
204
+ 'image',
205
+ 'document',
206
+ ]);
207
+ /** Shared marker object stamped onto up to 3 locations per request, reused
208
+ * across every request and every agent for the process lifetime — never
209
+ * mutate this in place; construct a new object if a call site ever needs a
210
+ * different breakpoint shape (e.g. a `ttl`). */
211
+ const CACHE_CONTROL_EPHEMERAL = Object.freeze({ type: 'ephemeral' });
212
+ /**
213
+ * Stamps `cache_control: {type:'ephemeral'}` on up to three of Anthropic's four
214
+ * allowed breakpoints: the last tool (caches the constant tools block), the
215
+ * system prompt (sent as a `text` block carrying the breakpoint), and a
216
+ * rolling breakpoint on the last cache-eligible block of the last message —
217
+ * the growing history is the majority of per-step input on a multi-step turn,
218
+ * so this breakpoint is load-bearing, not incidental: dropping it would leave
219
+ * only the turn-constant tools/system cached and forfeit most of the win.
220
+ * Mutates only `commonParams`'s own arrays — which
221
+ * `translate*ToAnthropic` builds fresh per request — never the caller's
222
+ * original `LanguageModelV3` prompt/tool objects.
223
+ */
224
+ export function applyCacheBreakpoints(commonParams) {
225
+ const tools = commonParams.tools;
226
+ if (tools !== undefined && tools.length > 0) {
227
+ const lastTool = tools[tools.length - 1];
228
+ if (lastTool !== undefined)
229
+ lastTool.cache_control = CACHE_CONTROL_EPHEMERAL;
230
+ }
231
+ if (typeof commonParams.system === 'string') {
232
+ commonParams.system = [{ type: 'text', text: commonParams.system, cache_control: CACHE_CONTROL_EPHEMERAL }];
233
+ }
234
+ else if (Array.isArray(commonParams.system) && commonParams.system.length > 0) {
235
+ commonParams.system[commonParams.system.length - 1].cache_control = CACHE_CONTROL_EPHEMERAL;
236
+ }
237
+ const lastMessage = commonParams.messages[commonParams.messages.length - 1];
238
+ const content = lastMessage?.content;
239
+ if (Array.isArray(content)) {
240
+ for (let i = content.length - 1; i >= 0; i--) {
241
+ const block = content[i];
242
+ if (block !== undefined && CACHE_ELIGIBLE_BLOCK_TYPES.has(block.type)) {
243
+ block.cache_control =
244
+ CACHE_CONTROL_EPHEMERAL;
245
+ break;
246
+ }
247
+ }
248
+ }
249
+ }
182
250
  async function doGenerate(ctx, options) {
183
251
  const { client, commonParams, toolSchemas } = await buildAnthropicRequest(ctx, options);
184
252
  // Forward the turn's AbortSignal into the SDK's RequestOptions so firing the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/sfdx-agent-harness-mastra",
3
- "version": "0.77.0",
3
+ "version": "0.79.0",
4
4
  "description": "Mastra-backed AgentHarness implementation for @salesforce/sfdx-agent-sdk",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -42,12 +42,12 @@
42
42
  "openai": "^7.0.0"
43
43
  },
44
44
  "peerDependencies": {
45
- "@salesforce/sfdx-agent-sdk": "0.78.0"
45
+ "@salesforce/sfdx-agent-sdk": "0.80.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.78.0",
50
+ "@salesforce/sfdx-agent-sdk": "0.80.0",
51
51
  "@types/node": "^22.20.1",
52
52
  "@vitest/coverage-istanbul": "^4.1.11",
53
53
  "@vitest/eslint-plugin": "^1.6.27",