@salesforce/sfdx-agent-harness-mastra 0.20.0 → 0.21.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,17 @@
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.21.0] - 2026-06-19
7
+
8
+ ### Features
9
+ - **BREAKING** Major refactor to Unify Connectivity via ModelConnectivityInfo @W-22782317 ([#607](https://github.com/forcedotcom/agentic-dx/pull/607))
10
+
11
+ ### Chores
12
+ - **deps-dev**: bump @vitest/eslint-plugin from 1.6.19 to 1.6.20 in the vitest group across 1 directory ([#601](https://github.com/forcedotcom/agentic-dx/pull/601))
13
+ - **deps-dev**: bump eslint from 10.4.1 to 10.5.0 in the eslint group across 1 directory ([#600](https://github.com/forcedotcom/agentic-dx/pull/600))
14
+ - **deps**: bump the mastra group across 1 directory with 4 updates ([#602](https://github.com/forcedotcom/agentic-dx/pull/602))
15
+ - **deps-dev**: bump @types/node from 22.19.20 to 22.19.21 in the dev-dependencies group ([#599](https://github.com/forcedotcom/agentic-dx/pull/599))
16
+
6
17
  ## [0.20.0] - 2026-06-15
7
18
 
8
19
  ### Fixes
package/README.md CHANGED
@@ -44,15 +44,15 @@ Linux and macOS release the handle eagerly; the retry options are no-ops on thos
44
44
 
45
45
  ### Core
46
46
 
47
- | Export | Kind | Description |
48
- | ------------------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
49
- | `MastraHarnessFactory` | class | `HarnessFactory` that produces a Mastra-backed `AgentHarness` bound to a storage root. |
50
- | `MastraHarnessFactoryConfig` | interface | Configuration for `MastraHarnessFactory` (`modelConfigFactory?`, `toolApprovalTimeoutMs?`). |
51
- | `MastraModelConfigFactory` | interface | Strategy for creating model configurations; injected into `MastraHarnessFactory`. |
52
- | `ResolvedModelConfig` | type | Return type of `MastraModelConfigFactory.create()` — a `MastraModelConfig` plus a `validateFiles(files)` hook the harness calls for pre-flight multimodal (`image`/`file`) validation. |
53
- | `DefaultMastraModelConfigFactory` | class | Default factory using the Salesforce LLM Gateway (`SalesforceMastraModelGateway`). |
54
- | `LLMGExpressMastraModelConfigFactory` | class | Factory for LLM Gateway Express (API-key-authenticated, OpenAI-compatible endpoint). |
55
- | `LLMGExpressConfig` | type | Configuration for `LLMGExpressMastraModelConfigFactory` (`apiKey`, `baseUrl`, `modelId`). |
47
+ | Export | Kind | Description |
48
+ | ---------------------------- | --------- | -------------------------------------------------------------------------------------- |
49
+ | `MastraHarnessFactory` | class | `HarnessFactory` that produces a Mastra-backed `AgentHarness` bound to a storage root. |
50
+ | `MastraHarnessFactoryConfig` | interface | Configuration for `MastraHarnessFactory` (`toolApprovalTimeoutMs?`). |
51
+
52
+ Connectivity (model + provider auth) is supplied by the SDK's `AgentConnectivityResolver`. Use the bundled
53
+ `DefaultAgentConnectivityResolver` for the standard Salesforce LLM Gateway path, or `ApiKeyConnectivityResolver` for
54
+ direct-provider / LLMG Express api-key endpoints — both ship from `@salesforce/sfdx-agent-sdk`. The harness dispatches
55
+ on `ModelConnectivityInfo.providerHint` internally to pick the right pass-through builder for each provider.
56
56
 
57
57
  ### Extension surface
58
58
 
@@ -236,19 +236,20 @@ contract.
236
236
 
237
237
  ### Using LLM Gateway Express
238
238
 
239
+ LLMG Express, Anthropic-direct, OpenAI-direct, and any other api-key endpoint flow through the SDK's
240
+ `ApiKeyConnectivityResolver` (no harness-specific factory needed):
241
+
239
242
  ```ts
240
- import { createAgentManager } from '@salesforce/sfdx-agent-sdk';
241
- import { MastraHarnessFactory, LLMGExpressMastraModelConfigFactory } from '@salesforce/sfdx-agent-harness-mastra';
243
+ import { ApiKeyConnectivityResolver, createAgentManager } from '@salesforce/sfdx-agent-sdk';
244
+ import { MastraHarnessFactory } from '@salesforce/sfdx-agent-harness-mastra';
242
245
 
243
- const harnessFactory = new MastraHarnessFactory({
244
- modelConfigFactory: new LLMGExpressMastraModelConfigFactory({
245
- apiKey: process.env.LLMG_EXPRESS_API_KEY!,
246
+ const manager = await createAgentManager('/path/to/storage', new MastraHarnessFactory(), {
247
+ connectivityResolver: new ApiKeyConnectivityResolver({
248
+ getApiKey: () => process.env.LLMG_EXPRESS_API_KEY!,
246
249
  baseUrl: process.env.LLMG_EXPRESS_BASE_URL!,
247
- modelId: 'claude-sonnet-4-20250514',
250
+ providerHint: 'openai-compatible',
248
251
  }),
249
252
  });
250
-
251
- const manager = await createAgentManager('/path/to/storage', harnessFactory);
252
253
  ```
253
254
 
254
255
  ## Development
package/dist/index.d.ts CHANGED
@@ -1,6 +1,3 @@
1
1
  export { MastraHarnessFactory, type MastraHarnessFactoryConfig } from './mastra-harness-factory.js';
2
2
  export type { MastraAgentHarness } from './mastra-harness.js';
3
3
  export { type MastraAgentConfig, type MastraSkillSearchConfig, type MastraToolSearchConfig, type SkillSearchHandle, type ToolSearchHandle, } from './tool-search.js';
4
- export type { MastraModelConfigFactory, ResolvedModelConfig } from './model-config-factory.js';
5
- export { DefaultMastraModelConfigFactory } from './default-model-config-factory.js';
6
- export { LLMGExpressMastraModelConfigFactory, type LLMGExpressConfig } from './llmg-express-model-config-factory.js';
package/dist/index.js CHANGED
@@ -4,6 +4,11 @@
4
4
  */
5
5
  export { MastraHarnessFactory } from './mastra-harness-factory.js';
6
6
  export {} from './tool-search.js';
7
- export { DefaultMastraModelConfigFactory } from './default-model-config-factory.js';
8
- export { LLMGExpressMastraModelConfigFactory } from './llmg-express-model-config-factory.js';
7
+ // Mastra drives requests through provider-native pass-through endpoints —
8
+ // Bedrock-Anthropic, direct Anthropic Messages API, OpenAI Responses, and
9
+ // OpenAI Chat Completions (LLMG Express / openai-compatible) — dispatched on
10
+ // the connectivity resolver's `ModelConnectivityInfo.providerHint`. There's
11
+ // no consumer-facing knob for switching the wire path; the resolver drives
12
+ // it. For BYOK / direct-Anthropic / LLMG-Express deployments use
13
+ // `ApiKeyConnectivityResolver` from `@salesforce/sfdx-agent-sdk`.
9
14
  //# sourceMappingURL=index.js.map
@@ -1,13 +1,6 @@
1
- import type { HarnessFactory } from '@salesforce/sfdx-agent-sdk';
1
+ import type { HarnessFactory, ProviderHint } from '@salesforce/sfdx-agent-sdk';
2
2
  import { type MastraAgentHarness } from './mastra-harness.js';
3
- import type { MastraModelConfigFactory } from './model-config-factory.js';
4
3
  export interface MastraHarnessFactoryConfig {
5
- /**
6
- * Strategy for resolving the Mastra model configuration. Defaults to
7
- * `DefaultMastraModelConfigFactory`, which targets the standard LLM
8
- * Gateway path.
9
- */
10
- modelConfigFactory?: MastraModelConfigFactory;
11
4
  /**
12
5
  * How long to wait for a `requireToolApproval` consumer to call
13
6
  * `approveToolCall` / `declineToolCall` before auto-denying via
@@ -23,6 +16,7 @@ export declare class MastraHarnessFactory implements HarnessFactory<MastraAgentH
23
16
  private readonly config;
24
17
  readonly harnessId = "mastra";
25
18
  readonly protocolVersion = 1;
19
+ readonly supportedProviderHints: readonly ProviderHint[];
26
20
  constructor(config?: MastraHarnessFactoryConfig);
27
21
  create(storageRootFolder: string): Promise<MastraAgentHarness>;
28
22
  }
@@ -9,12 +9,18 @@ export class MastraHarnessFactory {
9
9
  config;
10
10
  harnessId = 'mastra';
11
11
  protocolVersion = 1;
12
+ supportedProviderHints = [
13
+ 'anthropic',
14
+ 'bedrock-anthropic',
15
+ 'openai',
16
+ 'openai-responses',
17
+ 'openai-compatible',
18
+ ];
12
19
  constructor(config = {}) {
13
20
  this.config = config;
14
21
  }
15
22
  async create(storageRootFolder) {
16
23
  return new MastraHarness(storageRootFolder, {
17
- modelConfigFactory: this.config.modelConfigFactory,
18
24
  toolApprovalTimeoutMs: this.config.toolApprovalTimeoutMs ?? DEFAULT_TOOL_APPROVAL_TIMEOUT_MS,
19
25
  });
20
26
  }
@@ -1,12 +1,11 @@
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 StreamOptions, type TelemetryEventCallback, type ToolResultInfo } 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 ModelConnectivityInfo, type StreamOptions, type TelemetryEventCallback, type ToolResultInfo, type WireCommunicationEventCallback } from '@salesforce/sfdx-agent-sdk';
5
5
  import { type AgentHooks } from '@salesforce/sfdx-agent-sdk/harness';
6
6
  import { MastraApprovalCoordinator } from './mastra-approval-coordinator.js';
7
- import type { MastraModelConfigFactory } from './model-config-factory.js';
8
7
  import { type MastraAgentConfig, type SkillSearchHandle, type ToolSearchHandle } from './tool-search.js';
9
- import { type LLMGatewayClient, type MultimodalFile } from '@salesforce/llm-gateway-sdk';
8
+ import { type MultimodalFile } from '@salesforce/sfdx-agent-sdk';
10
9
  import { Clock, type LogRecord, type UniqueIDGenerator, type Unsubscribe } from '@salesforce/agentic-common';
11
10
  import { type ToolsInput } from './utils.js';
12
11
  import { Workspace } from '@mastra/core/workspace';
@@ -77,9 +76,12 @@ interface MastraAgentState {
77
76
  composedInstructions: string;
78
77
  /**
79
78
  * Pre-flight multimodal validation for this agent's model configuration.
80
- * Captured from {@link ResolvedModelConfig.validateFiles} at `createAgent`
81
- * time. Called by `toMastraStreamInput` before every stream that carries
82
- * `image` or `file` parts. Throws `LLMGClientError` on violation.
79
+ * Wraps `validateMultimodalFiles(files, model)` against the agent's
80
+ * currently-bound `ModelConnectivityInfo.model` at call time, so within-
81
+ * shape model swaps (Claude 4.5 → 4.6) pick up the updated caps without
82
+ * rebuilding the closure. Called by `toMastraStreamInput` before every
83
+ * stream that carries `image` or `file` parts. Throws
84
+ * `AgentSDKError(MULTIMODAL_NOT_SUPPORTED)` on violation.
83
85
  */
84
86
  validateFiles: (files: readonly MultimodalFile[]) => void;
85
87
  /** Per-server MCP state, keyed by server name. Empty map when no MCP servers are configured. */
@@ -192,7 +194,6 @@ export type MastraAgentHarness = WithAgentConfig<AgentHarness & {
192
194
  * directly.
193
195
  */
194
196
  export interface MastraHarnessOptions {
195
- modelConfigFactory?: MastraModelConfigFactory;
196
197
  clock?: Clock;
197
198
  idGenerator?: UniqueIDGenerator;
198
199
  toolApprovalTimeoutMs?: number;
@@ -216,7 +217,15 @@ export declare class MastraHarness implements MastraAgentHarness {
216
217
  * SDK never reads this; consumers reach through `manager.extensions.mastra`.
217
218
  */
218
219
  readonly extensions: MastraAgentHarness['extensions'];
219
- private readonly modelConfigFactory;
220
+ /**
221
+ * Live `ModelConnectivityInfo` per agent. The pass-through builders close
222
+ * over a getter that reads from this map, so within-shape `modelId` swaps,
223
+ * JWT rotations, and BYOK provider swaps land on the next outbound call
224
+ * without rebuilding the Mastra `Agent`. The map is replaced on every
225
+ * `updateAgent`; the language model's fetch wrapper picks up the new bag
226
+ * via the closure.
227
+ */
228
+ private readonly modelConnectivityByAgent;
220
229
  private readonly clock;
221
230
  private readonly idGenerator;
222
231
  private readonly toolApprovalTimeoutMs;
@@ -233,6 +242,7 @@ export declare class MastraHarness implements MastraAgentHarness {
233
242
  shutdown(): Promise<void>;
234
243
  onTelemetry(callback: TelemetryEventCallback): Unsubscribe;
235
244
  onLog(callback: (record: LogRecord) => void): Unsubscribe;
245
+ onWireCommunication(callback: WireCommunicationEventCallback): Unsubscribe;
236
246
  /**
237
247
  * @requirements
238
248
  * - MUST throw an error if the harness has not been initialized.
@@ -251,7 +261,7 @@ export declare class MastraHarness implements MastraAgentHarness {
251
261
  * `mapToolDefinitionsToMastraTools()` and merge with namespaced MCP-discovered tools.
252
262
  * - MUST store the original `MCPConfiguration` so getMcpServerInfo can report disabled servers.
253
263
  */
254
- createAgent(agentId: string, projectRoot: string, llmGatewayClient: LLMGatewayClient, config?: HarnessAgentConfig, options?: {
264
+ createAgent(agentId: string, projectRoot: string, modelConnectivityInfo: ModelConnectivityInfo, config?: HarnessAgentConfig, options?: {
255
265
  abortSignal?: AbortSignal;
256
266
  hooks?: AgentHooks;
257
267
  }): Promise<void>;
@@ -298,7 +308,7 @@ export declare class MastraHarness implements MastraAgentHarness {
298
308
  * `previousConfig`) sees the harness's current truth and reverts only the
299
309
  * actual deltas.
300
310
  */
301
- updateAgent(agentId: string, llmGatewayClient: LLMGatewayClient, config?: HarnessAgentConfig, options?: {
311
+ updateAgent(agentId: string, modelConnectivityInfo: ModelConnectivityInfo, config?: HarnessAgentConfig, options?: {
302
312
  abortSignal?: AbortSignal;
303
313
  hooks?: AgentHooks;
304
314
  }): Promise<void>;
@@ -359,6 +369,18 @@ export declare class MastraHarness implements MastraAgentHarness {
359
369
  * still observable. Used by `compactThread` on both the empty-source and
360
370
  * non-empty paths.
361
371
  */
372
+ /**
373
+ * Dispatches on the bag's `providerHint` and returns a `LanguageModelV3` that
374
+ * speaks the provider-native pass-through endpoint. The builder closes over
375
+ * `getInfo` so within-shape `modelId` swaps, JWT rotations, and
376
+ * premium-rate-limit gate flips land on the next outbound call without
377
+ * rebuilding this language model.
378
+ *
379
+ * The manager validates `providerHint` against `MastraHarnessFactory.supportedProviderHints`
380
+ * via G8 before any harness work runs (`AgentSDKErrorType.MODEL_NOT_SUPPORTED_BY_HARNESS`),
381
+ * so a hint not handled here is a programming error worth surfacing eagerly.
382
+ */
383
+ private buildLanguageModel;
362
384
  private destroyThreadBestEffort;
363
385
  /**
364
386
  * @requirements
@@ -19,9 +19,11 @@ import { isStandardSchemaWithJSON, standardSchemaToJSONSchema } from '@mastra/sc
19
19
  import { mapToolDefinitionsToMastraTools } from './mastra-tool-mapper.js';
20
20
  import { hasEnabledServers, mapToMastraMCPServers } from './mcp-config-mapper.js';
21
21
  import { buildMcpErrorDetail, sanitizeMcpErrorMessage } from './mcp-error-classifier.js';
22
- import { DefaultMastraModelConfigFactory } from './default-model-config-factory.js';
23
22
  import { partitionPool, resolveAlwaysActive, } from './tool-search.js';
24
- import {} from '@salesforce/llm-gateway-sdk';
23
+ import { validateMultimodalFiles } from '@salesforce/sfdx-agent-sdk';
24
+ import { buildAnthropicDirectLanguageModel, buildAnthropicLanguageModel } from './pass-through-anthropic.js';
25
+ import { buildOpenAIChatCompletionsLanguageModel } from './pass-through-openai-chat-completions.js';
26
+ import { buildOpenAILanguageModel } from './pass-through-openai.js';
25
27
  import { ToolResultRedactionProcessor } from './mastra-tool-result-redaction-processor.js';
26
28
  import { backfillCreatedAt, buildSummaryPrompt, Clock, getErrorMessage, RealClock, splitFrontmatterAndBody, UUIDGenerator, } from '@salesforce/agentic-common';
27
29
  import { makeAbortable } from './utils.js';
@@ -362,13 +364,20 @@ export class MastraHarness {
362
364
  formatMcpToolName: (serverName, toolName) => `${serverName}_${toolName}`,
363
365
  },
364
366
  };
365
- modelConfigFactory;
367
+ /**
368
+ * Live `ModelConnectivityInfo` per agent. The pass-through builders close
369
+ * over a getter that reads from this map, so within-shape `modelId` swaps,
370
+ * JWT rotations, and BYOK provider swaps land on the next outbound call
371
+ * without rebuilding the Mastra `Agent`. The map is replaced on every
372
+ * `updateAgent`; the language model's fetch wrapper picks up the new bag
373
+ * via the closure.
374
+ */
375
+ modelConnectivityByAgent = new Map();
366
376
  clock;
367
377
  idGenerator;
368
378
  toolApprovalTimeoutMs;
369
379
  constructor(storageRootFolder, opts = {}) {
370
380
  this.storageRootFolder = storageRootFolder;
371
- this.modelConfigFactory = opts.modelConfigFactory ?? new DefaultMastraModelConfigFactory();
372
381
  this.clock = opts.clock ?? new RealClock();
373
382
  this.idGenerator = opts.idGenerator ?? new UUIDGenerator();
374
383
  this.toolApprovalTimeoutMs = opts.toolApprovalTimeoutMs ?? 600_000;
@@ -414,6 +423,7 @@ export class MastraHarness {
414
423
  state.skillSearchProcessor?.dispose();
415
424
  }));
416
425
  this.activeAgents.clear();
426
+ this.modelConnectivityByAgent.clear();
417
427
  await this.mastra?.shutdown();
418
428
  this.buses.dispose();
419
429
  }
@@ -423,6 +433,9 @@ export class MastraHarness {
423
433
  onLog(callback) {
424
434
  return this.buses.onLog(callback);
425
435
  }
436
+ onWireCommunication(callback) {
437
+ return this.buses.onWireCommunication(callback);
438
+ }
426
439
  // ── Agent Lifecycle ──────────────────────────────────────────────
427
440
  /**
428
441
  * @requirements
@@ -442,10 +455,12 @@ export class MastraHarness {
442
455
  * `mapToolDefinitionsToMastraTools()` and merge with namespaced MCP-discovered tools.
443
456
  * - MUST store the original `MCPConfiguration` so getMcpServerInfo can report disabled servers.
444
457
  */
445
- async createAgent(agentId, projectRoot, llmGatewayClient, config = {}, options) {
458
+ async createAgent(agentId, projectRoot, modelConnectivityInfo, config = {}, options) {
446
459
  if (this.activeAgents.has(agentId)) {
447
460
  throw new Error(`Agent with id "${agentId}" is already registered`);
448
461
  }
462
+ // Store the bag first so the language-model getter has it on construct.
463
+ this.modelConnectivityByAgent.set(agentId, modelConnectivityInfo);
449
464
  // Run rule load before MCP setup so a bad rule path can't leak constructed clients.
450
465
  const composedInstructions = await composeInstructionsWithRules(config.instructions, config.rules);
451
466
  // ── MCP setup (non-blocking, per-server) ──────────────────────────
@@ -487,7 +502,22 @@ export class MastraHarness {
487
502
  const consumerToolNames = new Set((config.tools ?? []).map((t) => t.name));
488
503
  const parkerLookup = (threadId) => this.activeAgents.get(agentId)?.coordinators.get(threadId)?.parkConsumerToolCall;
489
504
  const customTools = config.tools?.length ? mapToolDefinitionsToMastraTools(config.tools, parkerLookup) : {};
490
- const { model: resolvedModel, validateFiles } = this.modelConfigFactory.create(llmGatewayClient, this.buses.getLogBus());
505
+ // Build the language model from the live ModelConnectivityInfo bag. The
506
+ // builder closes over `() => this.modelConnectivityByAgent.get(agentId)!`
507
+ // so within-shape `modelId` swaps and JWT rotations land on the next
508
+ // outbound call without rebuilding the Mastra `Agent` (cross-shape
509
+ // swaps rebuild via `updateAgent` below). `validateFiles` reads `model`
510
+ // from the bag so multimodal pre-flight stays in lockstep with the
511
+ // model the harness is actually about to talk to.
512
+ const getInfoForAgent = () => {
513
+ const info = this.modelConnectivityByAgent.get(agentId);
514
+ if (!info) {
515
+ throw new AgentSDKError(`No connectivity info registered for agentId "${agentId}".`, AgentSDKErrorType.AGENT_NOT_FOUND);
516
+ }
517
+ return info;
518
+ };
519
+ const resolvedModel = this.buildLanguageModel(getInfoForAgent);
520
+ const validateFiles = (files) => validateMultimodalFiles(files, getInfoForAgent().model);
491
521
  let mastraWorkspace;
492
522
  let agent;
493
523
  let mastraAgentAdded = false;
@@ -630,6 +660,11 @@ export class MastraHarness {
630
660
  skillSearchProcessor?.dispose();
631
661
  await mastraWorkspace?.destroy().catch(() => undefined);
632
662
  await Promise.allSettled(Array.from(mcpServerStates.values()).map((s) => s.client.disconnect().catch(() => undefined)));
663
+ // The bag was registered eagerly at the top of createAgent so the
664
+ // language-model getter could read it. On failure we never made
665
+ // it to activeAgents.set, so the registration is orphaned —
666
+ // clear it for symmetry with destroyAgent.
667
+ this.modelConnectivityByAgent.delete(agentId);
633
668
  throw error;
634
669
  }
635
670
  }
@@ -652,6 +687,7 @@ export class MastraHarness {
652
687
  state.skillSearchProcessor?.dispose();
653
688
  await state.mastraWorkspace?.destroy();
654
689
  this.mastra.removeAgent(agentId);
690
+ this.modelConnectivityByAgent.delete(agentId);
655
691
  return this.activeAgents.delete(agentId);
656
692
  }
657
693
  /**
@@ -820,9 +856,21 @@ export class MastraHarness {
820
856
  * `previousConfig`) sees the harness's current truth and reverts only the
821
857
  * actual deltas.
822
858
  */
823
- async updateAgent(agentId, llmGatewayClient, config = {}, options) {
859
+ async updateAgent(agentId, modelConnectivityInfo, config = {}, options) {
824
860
  const state = this.getAgentStateOrThrow(agentId);
825
861
  options?.abortSignal?.throwIfAborted();
862
+ // Swap the connectivity bag in the per-agent map BEFORE any rebuild
863
+ // logic runs. Within-shape model swaps (Claude 4.5 → 4.6) flow through
864
+ // the existing language model's getter on the next outbound call;
865
+ // cross-shape swaps trigger an `Agent` rebuild below. JWT rotation
866
+ // and rate-limit gate flips also land here — no separate swap path.
867
+ // Cross-shape swaps could in principle skip the Agent rebuild below
868
+ // — only a `providerHint` change requires a new `mastra.removeAgent`
869
+ // round trip — but Mastra always rebuilds the `Agent` on
870
+ // `updateAgent` regardless, so that optimization isn't load-bearing
871
+ // today; the previous-hint capture is left out so this site doesn't
872
+ // accumulate dead state.
873
+ this.modelConnectivityByAgent.set(agentId, modelConnectivityInfo);
826
874
  // ── Phase 0: validate non-MCP inputs before any MCP client is touched.
827
875
  // A bad rule path or skill path rejects updateAgent here so
828
876
  // half-applied state from non-MCP failures is impossible.
@@ -905,7 +953,24 @@ export class MastraHarness {
905
953
  const consumerToolNames = new Set((config.tools ?? []).map((t) => t.name));
906
954
  const parkerLookup = (threadId) => this.activeAgents.get(agentId)?.coordinators.get(threadId)?.parkConsumerToolCall;
907
955
  const customTools = config.tools?.length ? mapToolDefinitionsToMastraTools(config.tools, parkerLookup) : {};
908
- const { model: resolvedModel, validateFiles } = this.modelConfigFactory.create(llmGatewayClient, this.buses.getLogBus());
956
+ // Same shape as createAgent's getter — reads the live bag on every call so
957
+ // the next-built `Agent`'s language model picks up future bag swaps.
958
+ const getInfoForAgent = () => {
959
+ const info = this.modelConnectivityByAgent.get(agentId);
960
+ if (!info) {
961
+ throw new AgentSDKError(`No connectivity info registered for agentId "${agentId}".`, AgentSDKErrorType.AGENT_NOT_FOUND);
962
+ }
963
+ return info;
964
+ };
965
+ // Cross-shape swap (e.g. Claude → GPT) requires a different language
966
+ // model class because the provider SDK underneath differs. Within-shape
967
+ // swaps could reuse the existing language model — its getter reads the
968
+ // refreshed bag on the next outbound call — but Mastra reconstructs the
969
+ // `Agent` unconditionally on every `updateAgent` (instructions, tools,
970
+ // processors all flow through), so we always rebuild the language model
971
+ // here too. Allocation cost is negligible compared to the Agent rebuild.
972
+ const resolvedModel = this.buildLanguageModel(getInfoForAgent);
973
+ const validateFiles = (files) => validateMultimodalFiles(files, getInfoForAgent().model);
909
974
  // Capture the live mcpServers map by reference — the new Agent's
910
975
  // tools() closure will read it on every step, so preserved clients'
911
976
  // discovered tools stay visible without a re-listing.
@@ -1153,6 +1218,59 @@ export class MastraHarness {
1153
1218
  * still observable. Used by `compactThread` on both the empty-source and
1154
1219
  * non-empty paths.
1155
1220
  */
1221
+ /**
1222
+ * Dispatches on the bag's `providerHint` and returns a `LanguageModelV3` that
1223
+ * speaks the provider-native pass-through endpoint. The builder closes over
1224
+ * `getInfo` so within-shape `modelId` swaps, JWT rotations, and
1225
+ * premium-rate-limit gate flips land on the next outbound call without
1226
+ * rebuilding this language model.
1227
+ *
1228
+ * The manager validates `providerHint` against `MastraHarnessFactory.supportedProviderHints`
1229
+ * via G8 before any harness work runs (`AgentSDKErrorType.MODEL_NOT_SUPPORTED_BY_HARNESS`),
1230
+ * so a hint not handled here is a programming error worth surfacing eagerly.
1231
+ */
1232
+ buildLanguageModel(getInfo) {
1233
+ const initial = getInfo();
1234
+ const logBus = this.buses.getLogBus();
1235
+ // The fetch wrapper emits one WireCommunicationEvent pair per HTTP call. Forward
1236
+ // each event to the harness bus; if the bus has been disposed (a late
1237
+ // emit during shutdown), swallow the typed DISPOSED error so trace
1238
+ // hooks never fail the underlying fetch.
1239
+ const emitWireCommunication = (event) => {
1240
+ try {
1241
+ this.buses.emitWireCommunication(event);
1242
+ }
1243
+ catch (err) {
1244
+ if (!isDisposedError(err))
1245
+ throw err;
1246
+ }
1247
+ };
1248
+ switch (initial.providerHint) {
1249
+ case 'bedrock-anthropic':
1250
+ // Salesforce LLM Gateway's `/invoke-with-response-stream` (Bedrock pass-through).
1251
+ return buildAnthropicLanguageModel({ getInfo, logBus, emitWireCommunication });
1252
+ case 'anthropic':
1253
+ // Direct Anthropic Messages API (`api.anthropic.com/v1/messages`) —
1254
+ // different wire shape (`x-api-key` auth, body without `anthropic_version`).
1255
+ return buildAnthropicDirectLanguageModel({ getInfo, logBus, emitWireCommunication });
1256
+ case 'openai-responses':
1257
+ case 'openai':
1258
+ // OpenAI Responses API. Salesforce LLM Gateway's `/responses` pass-through
1259
+ // for `'openai-responses'`; `api.openai.com/v1/responses` for `'openai'` direct.
1260
+ // Same wire shape; the only difference is the `baseUrl` the resolver supplies.
1261
+ return buildOpenAILanguageModel({ getInfo, logBus, emitWireCommunication });
1262
+ case 'openai-compatible':
1263
+ // OpenAI-compatible Chat Completions API (LLMG Express, LiteLLM, etc.).
1264
+ // Different wire shape from Responses — `/v1/chat/completions` with
1265
+ // `choices[0].delta` streaming events.
1266
+ return buildOpenAIChatCompletionsLanguageModel({ getInfo, logBus, emitWireCommunication });
1267
+ default:
1268
+ // Defensive — manager-side G8 should have caught this before
1269
+ // we got here. Make the error typed so consumers can branch
1270
+ // on `err.type` instead of regexing the message.
1271
+ throw new AgentSDKError(`Mastra harness has no language-model builder for providerHint "${initial.providerHint}".`, AgentSDKErrorType.MODEL_NOT_SUPPORTED_BY_HARNESS);
1272
+ }
1273
+ }
1156
1274
  async destroyThreadBestEffort(agentId, threadId) {
1157
1275
  try {
1158
1276
  await this.destroyThread(agentId, threadId);
@@ -1,5 +1,5 @@
1
1
  import type { MastraMCPServerDefinition } from '@mastra/mcp';
2
- import type { JSONWebToken } from '@salesforce/llm-gateway-sdk';
2
+ import type { JSONWebToken } from '@salesforce/agentic-common';
3
3
  import { type MCPConfiguration } from '@salesforce/sfdx-agent-sdk';
4
4
  /**
5
5
  * Returns `true` if `config` contains at least one server that is not explicitly disabled.
@@ -0,0 +1,66 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+ import type { LanguageModelV3, LanguageModelV3CallOptions, LanguageModelV3Prompt } from '@ai-sdk/provider';
3
+ import type { LogBus } from '@salesforce/agentic-common';
4
+ import type { ModelConnectivityInfo, WireCommunicationEvent } from '@salesforce/sfdx-agent-sdk';
5
+ /**
6
+ * Builds the in-process `LanguageModelV3` impl for the Salesforce LLM Gateway's
7
+ * `/invoke-with-response-stream` (Bedrock-Anthropic pass-through) endpoint.
8
+ * Drives `@anthropic-ai/bedrock-sdk` with `skipAuth: true` so the connectivity
9
+ * resolver's headers (Salesforce JWT, tenant key, etc.) are the single source
10
+ * of auth.
11
+ *
12
+ * Each `doStream` / `doGenerate` call reads the live `ModelConnectivityInfo`
13
+ * via the supplied `getInfo` getter — allowing the harness to swap the bag
14
+ * (within-shape `modelId` change, JWT rotation, premium-rate-limit gate flip)
15
+ * between turns without rebuilding this language model. The fetch wrapper
16
+ * calls `info.getHeaders()` per HTTP call so credential rotation lands on
17
+ * the next request.
18
+ */
19
+ export declare function buildAnthropicLanguageModel(opts: {
20
+ getInfo: () => ModelConnectivityInfo;
21
+ logBus?: LogBus;
22
+ emitWireCommunication?: (event: WireCommunicationEvent) => void;
23
+ }): LanguageModelV3;
24
+ /**
25
+ * Builds the in-process `LanguageModelV3` impl for direct-Anthropic API access
26
+ * (`api.anthropic.com/v1/messages` or any Anthropic-shaped endpoint). Drives
27
+ * `@anthropic-ai/sdk` with the api-key peeled from the resolver's headers.
28
+ *
29
+ * Authentication: the connectivity resolver emits `Authorization: Bearer <key>`
30
+ * as its generic credential carrier. The Anthropic Messages API requires
31
+ * `x-api-key` instead, so this builder peels the bearer token at construction
32
+ * time and passes it as the SDK's `apiKey` arg. The SDK then writes
33
+ * `x-api-key` and `anthropic-version` itself; our fetch wrapper supplies the
34
+ * resolver's remaining headers (typically `Content-Type` and any
35
+ * `model.customHeaders`) on top, with the bearer header explicitly stripped
36
+ * to avoid sending both auth shapes redundantly.
37
+ *
38
+ * Same per-call freshness contract as the Bedrock builder: every HTTP call
39
+ * re-evaluates `getHeaders()` via the fetch wrapper so a rotating api-key
40
+ * source (BYOK toggle, secret-store fetch) lands without rebuilding the
41
+ * language model.
42
+ */
43
+ export declare function buildAnthropicDirectLanguageModel(opts: {
44
+ getInfo: () => ModelConnectivityInfo;
45
+ logBus?: LogBus;
46
+ emitWireCommunication?: (event: WireCommunicationEvent) => void;
47
+ }): LanguageModelV3;
48
+ /**
49
+ * Translates a V3 `LanguageModelV3Prompt` into Anthropic's Messages API shape
50
+ * — top-level `system` string + `MessageParam[]` of role-tagged content blocks.
51
+ *
52
+ * V3 keeps system / user / assistant / tool messages flat. Anthropic combines
53
+ * tool results onto a synthetic user-role turn carrying `tool_result` blocks,
54
+ * so we collapse consecutive tool-only V3 messages into the surrounding user
55
+ * turn shape.
56
+ */
57
+ export declare function translatePromptToAnthropic(prompt: LanguageModelV3Prompt): {
58
+ system?: string;
59
+ messages: Anthropic.Messages.MessageParam[];
60
+ };
61
+ /**
62
+ * Translates V3 function-tool definitions into Anthropic's `Tool[]` shape.
63
+ * Skips non-function variants (provider-defined / dynamic) — the pass-through
64
+ * path supports only the function-tool subset that Mastra's tool mapper produces.
65
+ */
66
+ export declare function translateToolsToAnthropic(options: LanguageModelV3CallOptions): Anthropic.Messages.Tool[];