@salesforce/sfdx-agent-harness-mastra 0.20.0 → 0.22.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,22 @@
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.22.0] - 2026-06-22
7
+
8
+ ### Features
9
+ - **agentic-common,harnesses**: route HTTPS_PROXY via injectable undici Dispatcher @W-23121596 ([#610](https://github.com/forcedotcom/agentic-dx/pull/610))
10
+
11
+ ## [0.21.0] - 2026-06-19
12
+
13
+ ### Features
14
+ - **BREAKING** Major refactor to Unify Connectivity via ModelConnectivityInfo @W-22782317 ([#607](https://github.com/forcedotcom/agentic-dx/pull/607))
15
+
16
+ ### Chores
17
+ - **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))
18
+ - **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))
19
+ - **deps**: bump the mastra group across 1 directory with 4 updates ([#602](https://github.com/forcedotcom/agentic-dx/pull/602))
20
+ - **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))
21
+
6
22
  ## [0.20.0] - 2026-06-15
7
23
 
8
24
  ### Fixes
package/README.md CHANGED
@@ -44,15 +44,26 @@ 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
+
57
+ #### HTTPS proxy routing
58
+
59
+ The harness honors `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` (either casing) via
60
+ `@salesforce/agentic-common`'s `resolveProxyDispatcher()`. When either proxy var is set at factory `create()` time, the
61
+ harness wraps every in-process `fetch` call site (LLM gateway language-model builders + MCP remote transports) with an
62
+ undici-backed routing function — no `globalThis` mutation. When no proxy env is set, `fetch` runs unwrapped (zero
63
+ overhead).
64
+
65
+ **Snapshot timing.** `HTTPS_PROXY` / `HTTP_PROXY` are captured at `create()` time and pinned for the harness's lifetime;
66
+ `NO_PROXY` is re-evaluated per request. Set the proxy env vars before constructing the factory's first harness.
56
67
 
57
68
  ### Extension surface
58
69
 
@@ -236,19 +247,20 @@ contract.
236
247
 
237
248
  ### Using LLM Gateway Express
238
249
 
250
+ LLMG Express, Anthropic-direct, OpenAI-direct, and any other api-key endpoint flow through the SDK's
251
+ `ApiKeyConnectivityResolver` (no harness-specific factory needed):
252
+
239
253
  ```ts
240
- import { createAgentManager } from '@salesforce/sfdx-agent-sdk';
241
- import { MastraHarnessFactory, LLMGExpressMastraModelConfigFactory } from '@salesforce/sfdx-agent-harness-mastra';
254
+ import { ApiKeyConnectivityResolver, createAgentManager } from '@salesforce/sfdx-agent-sdk';
255
+ import { MastraHarnessFactory } from '@salesforce/sfdx-agent-harness-mastra';
242
256
 
243
- const harnessFactory = new MastraHarnessFactory({
244
- modelConfigFactory: new LLMGExpressMastraModelConfigFactory({
245
- apiKey: process.env.LLMG_EXPRESS_API_KEY!,
257
+ const manager = await createAgentManager('/path/to/storage', new MastraHarnessFactory(), {
258
+ connectivityResolver: new ApiKeyConnectivityResolver({
259
+ getApiKey: () => process.env.LLMG_EXPRESS_API_KEY!,
246
260
  baseUrl: process.env.LLMG_EXPRESS_BASE_URL!,
247
- modelId: 'claude-sonnet-4-20250514',
261
+ providerHint: 'openai-compatible',
248
262
  }),
249
263
  });
250
-
251
- const manager = await createAgentManager('/path/to/storage', harnessFactory);
252
264
  ```
253
265
 
254
266
  ## 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
  }
@@ -2,6 +2,7 @@
2
2
  * Copyright 2026, Salesforce, Inc. All rights reserved.
3
3
  * See LICENSE.txt for license terms.
4
4
  */
5
+ import { createProxyAwareFetch, resolveProxyDispatcher } from '@salesforce/agentic-common';
5
6
  import { MastraHarness } from './mastra-harness.js';
6
7
  const DEFAULT_TOOL_APPROVAL_TIMEOUT_MS = 600_000;
7
8
  /** Mastra-backed factory for creating AgentHarness instances. */
@@ -9,13 +10,21 @@ export class MastraHarnessFactory {
9
10
  config;
10
11
  harnessId = 'mastra';
11
12
  protocolVersion = 1;
13
+ supportedProviderHints = [
14
+ 'anthropic',
15
+ 'bedrock-anthropic',
16
+ 'openai',
17
+ 'openai-responses',
18
+ 'openai-compatible',
19
+ ];
12
20
  constructor(config = {}) {
13
21
  this.config = config;
14
22
  }
15
23
  async create(storageRootFolder) {
24
+ const innerFetch = createProxyAwareFetch(resolveProxyDispatcher());
16
25
  return new MastraHarness(storageRootFolder, {
17
- modelConfigFactory: this.config.modelConfigFactory,
18
26
  toolApprovalTimeoutMs: this.config.toolApprovalTimeoutMs ?? DEFAULT_TOOL_APPROVAL_TIMEOUT_MS,
27
+ innerFetch,
19
28
  });
20
29
  }
21
30
  }
@@ -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,10 +194,18 @@ 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;
200
+ /**
201
+ * Inner fetch the harness threads into every in-process HTTP call site
202
+ * (LLM gateway language-model builders + MCP remote transports). Defaults
203
+ * to `globalThis.fetch` when omitted. The harness factory builds a
204
+ * proxy-aware fetch via `createProxyAwareFetch(dispatcher)` once per
205
+ * `create()` and passes it here, so `HTTPS_PROXY` routing happens without
206
+ * any `globalThis` mutation.
207
+ */
208
+ innerFetch?: typeof fetch;
199
209
  }
200
210
  export declare class MastraHarness implements MastraAgentHarness {
201
211
  private readonly storageRootFolder;
@@ -216,10 +226,19 @@ export declare class MastraHarness implements MastraAgentHarness {
216
226
  * SDK never reads this; consumers reach through `manager.extensions.mastra`.
217
227
  */
218
228
  readonly extensions: MastraAgentHarness['extensions'];
219
- private readonly modelConfigFactory;
229
+ /**
230
+ * Live `ModelConnectivityInfo` per agent. The pass-through builders close
231
+ * over a getter that reads from this map, so within-shape `modelId` swaps,
232
+ * JWT rotations, and BYOK provider swaps land on the next outbound call
233
+ * without rebuilding the Mastra `Agent`. The map is replaced on every
234
+ * `updateAgent`; the language model's fetch wrapper picks up the new bag
235
+ * via the closure.
236
+ */
237
+ private readonly modelConnectivityByAgent;
220
238
  private readonly clock;
221
239
  private readonly idGenerator;
222
240
  private readonly toolApprovalTimeoutMs;
241
+ private readonly innerFetch;
223
242
  constructor(storageRootFolder: string, opts?: MastraHarnessOptions);
224
243
  /**
225
244
  * @requirements
@@ -233,6 +252,7 @@ export declare class MastraHarness implements MastraAgentHarness {
233
252
  shutdown(): Promise<void>;
234
253
  onTelemetry(callback: TelemetryEventCallback): Unsubscribe;
235
254
  onLog(callback: (record: LogRecord) => void): Unsubscribe;
255
+ onWireCommunication(callback: WireCommunicationEventCallback): Unsubscribe;
236
256
  /**
237
257
  * @requirements
238
258
  * - MUST throw an error if the harness has not been initialized.
@@ -251,7 +271,7 @@ export declare class MastraHarness implements MastraAgentHarness {
251
271
  * `mapToolDefinitionsToMastraTools()` and merge with namespaced MCP-discovered tools.
252
272
  * - MUST store the original `MCPConfiguration` so getMcpServerInfo can report disabled servers.
253
273
  */
254
- createAgent(agentId: string, projectRoot: string, llmGatewayClient: LLMGatewayClient, config?: HarnessAgentConfig, options?: {
274
+ createAgent(agentId: string, projectRoot: string, modelConnectivityInfo: ModelConnectivityInfo, config?: HarnessAgentConfig, options?: {
255
275
  abortSignal?: AbortSignal;
256
276
  hooks?: AgentHooks;
257
277
  }): Promise<void>;
@@ -298,7 +318,7 @@ export declare class MastraHarness implements MastraAgentHarness {
298
318
  * `previousConfig`) sees the harness's current truth and reverts only the
299
319
  * actual deltas.
300
320
  */
301
- updateAgent(agentId: string, llmGatewayClient: LLMGatewayClient, config?: HarnessAgentConfig, options?: {
321
+ updateAgent(agentId: string, modelConnectivityInfo: ModelConnectivityInfo, config?: HarnessAgentConfig, options?: {
302
322
  abortSignal?: AbortSignal;
303
323
  hooks?: AgentHooks;
304
324
  }): Promise<void>;
@@ -359,6 +379,18 @@ export declare class MastraHarness implements MastraAgentHarness {
359
379
  * still observable. Used by `compactThread` on both the empty-source and
360
380
  * non-empty paths.
361
381
  */
382
+ /**
383
+ * Dispatches on the bag's `providerHint` and returns a `LanguageModelV3` that
384
+ * speaks the provider-native pass-through endpoint. The builder closes over
385
+ * `getInfo` so within-shape `modelId` swaps, JWT rotations, and
386
+ * premium-rate-limit gate flips land on the next outbound call without
387
+ * rebuilding this language model.
388
+ *
389
+ * The manager validates `providerHint` against `MastraHarnessFactory.supportedProviderHints`
390
+ * via G8 before any harness work runs (`AgentSDKErrorType.MODEL_NOT_SUPPORTED_BY_HARNESS`),
391
+ * so a hint not handled here is a programming error worth surfacing eagerly.
392
+ */
393
+ private buildLanguageModel;
362
394
  private destroyThreadBestEffort;
363
395
  /**
364
396
  * @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,16 +364,25 @@ 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;
379
+ innerFetch;
369
380
  constructor(storageRootFolder, opts = {}) {
370
381
  this.storageRootFolder = storageRootFolder;
371
- this.modelConfigFactory = opts.modelConfigFactory ?? new DefaultMastraModelConfigFactory();
372
382
  this.clock = opts.clock ?? new RealClock();
373
383
  this.idGenerator = opts.idGenerator ?? new UUIDGenerator();
374
384
  this.toolApprovalTimeoutMs = opts.toolApprovalTimeoutMs ?? 600_000;
385
+ this.innerFetch = opts.innerFetch ?? globalThis.fetch;
375
386
  this.storage = new LibSQLStore({
376
387
  id: 'mastra-storage',
377
388
  url: `file:${this.storageRootFolder}/mastra.db`,
@@ -414,6 +425,7 @@ export class MastraHarness {
414
425
  state.skillSearchProcessor?.dispose();
415
426
  }));
416
427
  this.activeAgents.clear();
428
+ this.modelConnectivityByAgent.clear();
417
429
  await this.mastra?.shutdown();
418
430
  this.buses.dispose();
419
431
  }
@@ -423,6 +435,9 @@ export class MastraHarness {
423
435
  onLog(callback) {
424
436
  return this.buses.onLog(callback);
425
437
  }
438
+ onWireCommunication(callback) {
439
+ return this.buses.onWireCommunication(callback);
440
+ }
426
441
  // ── Agent Lifecycle ──────────────────────────────────────────────
427
442
  /**
428
443
  * @requirements
@@ -442,17 +457,19 @@ export class MastraHarness {
442
457
  * `mapToolDefinitionsToMastraTools()` and merge with namespaced MCP-discovered tools.
443
458
  * - MUST store the original `MCPConfiguration` so getMcpServerInfo can report disabled servers.
444
459
  */
445
- async createAgent(agentId, projectRoot, llmGatewayClient, config = {}, options) {
460
+ async createAgent(agentId, projectRoot, modelConnectivityInfo, config = {}, options) {
446
461
  if (this.activeAgents.has(agentId)) {
447
462
  throw new Error(`Agent with id "${agentId}" is already registered`);
448
463
  }
464
+ // Store the bag first so the language-model getter has it on construct.
465
+ this.modelConnectivityByAgent.set(agentId, modelConnectivityInfo);
449
466
  // Run rule load before MCP setup so a bad rule path can't leak constructed clients.
450
467
  const composedInstructions = await composeInstructionsWithRules(config.instructions, config.rules);
451
468
  // ── MCP setup (non-blocking, per-server) ──────────────────────────
452
469
  const mcpServerStates = new Map();
453
470
  if (hasEnabledServers(config.mcpServers)) {
454
471
  options?.abortSignal?.throwIfAborted();
455
- const enabledServers = mapToMastraMCPServers(config.mcpServers, config.orgJwt);
472
+ const enabledServers = mapToMastraMCPServers(config.mcpServers, config.orgJwt, this.innerFetch);
456
473
  for (const [serverName, serverDef] of Object.entries(enabledServers)) {
457
474
  const client = new MCPClient({
458
475
  id: `${agentId}:${serverName}`,
@@ -487,7 +504,22 @@ export class MastraHarness {
487
504
  const consumerToolNames = new Set((config.tools ?? []).map((t) => t.name));
488
505
  const parkerLookup = (threadId) => this.activeAgents.get(agentId)?.coordinators.get(threadId)?.parkConsumerToolCall;
489
506
  const customTools = config.tools?.length ? mapToolDefinitionsToMastraTools(config.tools, parkerLookup) : {};
490
- const { model: resolvedModel, validateFiles } = this.modelConfigFactory.create(llmGatewayClient, this.buses.getLogBus());
507
+ // Build the language model from the live ModelConnectivityInfo bag. The
508
+ // builder closes over `() => this.modelConnectivityByAgent.get(agentId)!`
509
+ // so within-shape `modelId` swaps and JWT rotations land on the next
510
+ // outbound call without rebuilding the Mastra `Agent` (cross-shape
511
+ // swaps rebuild via `updateAgent` below). `validateFiles` reads `model`
512
+ // from the bag so multimodal pre-flight stays in lockstep with the
513
+ // model the harness is actually about to talk to.
514
+ const getInfoForAgent = () => {
515
+ const info = this.modelConnectivityByAgent.get(agentId);
516
+ if (!info) {
517
+ throw new AgentSDKError(`No connectivity info registered for agentId "${agentId}".`, AgentSDKErrorType.AGENT_NOT_FOUND);
518
+ }
519
+ return info;
520
+ };
521
+ const resolvedModel = this.buildLanguageModel(getInfoForAgent);
522
+ const validateFiles = (files) => validateMultimodalFiles(files, getInfoForAgent().model);
491
523
  let mastraWorkspace;
492
524
  let agent;
493
525
  let mastraAgentAdded = false;
@@ -630,6 +662,11 @@ export class MastraHarness {
630
662
  skillSearchProcessor?.dispose();
631
663
  await mastraWorkspace?.destroy().catch(() => undefined);
632
664
  await Promise.allSettled(Array.from(mcpServerStates.values()).map((s) => s.client.disconnect().catch(() => undefined)));
665
+ // The bag was registered eagerly at the top of createAgent so the
666
+ // language-model getter could read it. On failure we never made
667
+ // it to activeAgents.set, so the registration is orphaned —
668
+ // clear it for symmetry with destroyAgent.
669
+ this.modelConnectivityByAgent.delete(agentId);
633
670
  throw error;
634
671
  }
635
672
  }
@@ -652,6 +689,7 @@ export class MastraHarness {
652
689
  state.skillSearchProcessor?.dispose();
653
690
  await state.mastraWorkspace?.destroy();
654
691
  this.mastra.removeAgent(agentId);
692
+ this.modelConnectivityByAgent.delete(agentId);
655
693
  return this.activeAgents.delete(agentId);
656
694
  }
657
695
  /**
@@ -820,9 +858,21 @@ export class MastraHarness {
820
858
  * `previousConfig`) sees the harness's current truth and reverts only the
821
859
  * actual deltas.
822
860
  */
823
- async updateAgent(agentId, llmGatewayClient, config = {}, options) {
861
+ async updateAgent(agentId, modelConnectivityInfo, config = {}, options) {
824
862
  const state = this.getAgentStateOrThrow(agentId);
825
863
  options?.abortSignal?.throwIfAborted();
864
+ // Swap the connectivity bag in the per-agent map BEFORE any rebuild
865
+ // logic runs. Within-shape model swaps (Claude 4.5 → 4.6) flow through
866
+ // the existing language model's getter on the next outbound call;
867
+ // cross-shape swaps trigger an `Agent` rebuild below. JWT rotation
868
+ // and rate-limit gate flips also land here — no separate swap path.
869
+ // Cross-shape swaps could in principle skip the Agent rebuild below
870
+ // — only a `providerHint` change requires a new `mastra.removeAgent`
871
+ // round trip — but Mastra always rebuilds the `Agent` on
872
+ // `updateAgent` regardless, so that optimization isn't load-bearing
873
+ // today; the previous-hint capture is left out so this site doesn't
874
+ // accumulate dead state.
875
+ this.modelConnectivityByAgent.set(agentId, modelConnectivityInfo);
826
876
  // ── Phase 0: validate non-MCP inputs before any MCP client is touched.
827
877
  // A bad rule path or skill path rejects updateAgent here so
828
878
  // half-applied state from non-MCP failures is impossible.
@@ -861,7 +911,7 @@ export class MastraHarness {
861
911
  const nextConfig = config.mcpServers ?? {};
862
912
  const allNames = new Set([...Object.keys(prevConfig), ...Object.keys(nextConfig)]);
863
913
  const nextEnabledServers = hasEnabledServers(nextConfig)
864
- ? mapToMastraMCPServers(nextConfig, config.orgJwt)
914
+ ? mapToMastraMCPServers(nextConfig, config.orgJwt, this.innerFetch)
865
915
  : {};
866
916
  for (const name of allNames) {
867
917
  const prev = prevConfig[name];
@@ -905,7 +955,24 @@ export class MastraHarness {
905
955
  const consumerToolNames = new Set((config.tools ?? []).map((t) => t.name));
906
956
  const parkerLookup = (threadId) => this.activeAgents.get(agentId)?.coordinators.get(threadId)?.parkConsumerToolCall;
907
957
  const customTools = config.tools?.length ? mapToolDefinitionsToMastraTools(config.tools, parkerLookup) : {};
908
- const { model: resolvedModel, validateFiles } = this.modelConfigFactory.create(llmGatewayClient, this.buses.getLogBus());
958
+ // Same shape as createAgent's getter — reads the live bag on every call so
959
+ // the next-built `Agent`'s language model picks up future bag swaps.
960
+ const getInfoForAgent = () => {
961
+ const info = this.modelConnectivityByAgent.get(agentId);
962
+ if (!info) {
963
+ throw new AgentSDKError(`No connectivity info registered for agentId "${agentId}".`, AgentSDKErrorType.AGENT_NOT_FOUND);
964
+ }
965
+ return info;
966
+ };
967
+ // Cross-shape swap (e.g. Claude → GPT) requires a different language
968
+ // model class because the provider SDK underneath differs. Within-shape
969
+ // swaps could reuse the existing language model — its getter reads the
970
+ // refreshed bag on the next outbound call — but Mastra reconstructs the
971
+ // `Agent` unconditionally on every `updateAgent` (instructions, tools,
972
+ // processors all flow through), so we always rebuild the language model
973
+ // here too. Allocation cost is negligible compared to the Agent rebuild.
974
+ const resolvedModel = this.buildLanguageModel(getInfoForAgent);
975
+ const validateFiles = (files) => validateMultimodalFiles(files, getInfoForAgent().model);
909
976
  // Capture the live mcpServers map by reference — the new Agent's
910
977
  // tools() closure will read it on every step, so preserved clients'
911
978
  // discovered tools stay visible without a re-listing.
@@ -1153,6 +1220,60 @@ export class MastraHarness {
1153
1220
  * still observable. Used by `compactThread` on both the empty-source and
1154
1221
  * non-empty paths.
1155
1222
  */
1223
+ /**
1224
+ * Dispatches on the bag's `providerHint` and returns a `LanguageModelV3` that
1225
+ * speaks the provider-native pass-through endpoint. The builder closes over
1226
+ * `getInfo` so within-shape `modelId` swaps, JWT rotations, and
1227
+ * premium-rate-limit gate flips land on the next outbound call without
1228
+ * rebuilding this language model.
1229
+ *
1230
+ * The manager validates `providerHint` against `MastraHarnessFactory.supportedProviderHints`
1231
+ * via G8 before any harness work runs (`AgentSDKErrorType.MODEL_NOT_SUPPORTED_BY_HARNESS`),
1232
+ * so a hint not handled here is a programming error worth surfacing eagerly.
1233
+ */
1234
+ buildLanguageModel(getInfo) {
1235
+ const initial = getInfo();
1236
+ const logBus = this.buses.getLogBus();
1237
+ // The fetch wrapper emits one WireCommunicationEvent pair per HTTP call. Forward
1238
+ // each event to the harness bus; if the bus has been disposed (a late
1239
+ // emit during shutdown), swallow the typed DISPOSED error so trace
1240
+ // hooks never fail the underlying fetch.
1241
+ const emitWireCommunication = (event) => {
1242
+ try {
1243
+ this.buses.emitWireCommunication(event);
1244
+ }
1245
+ catch (err) {
1246
+ if (!isDisposedError(err))
1247
+ throw err;
1248
+ }
1249
+ };
1250
+ const innerFetch = this.innerFetch;
1251
+ switch (initial.providerHint) {
1252
+ case 'bedrock-anthropic':
1253
+ // Salesforce LLM Gateway's `/invoke-with-response-stream` (Bedrock pass-through).
1254
+ return buildAnthropicLanguageModel({ getInfo, logBus, emitWireCommunication, innerFetch });
1255
+ case 'anthropic':
1256
+ // Direct Anthropic Messages API (`api.anthropic.com/v1/messages`) —
1257
+ // different wire shape (`x-api-key` auth, body without `anthropic_version`).
1258
+ return buildAnthropicDirectLanguageModel({ getInfo, logBus, emitWireCommunication, innerFetch });
1259
+ case 'openai-responses':
1260
+ case 'openai':
1261
+ // OpenAI Responses API. Salesforce LLM Gateway's `/responses` pass-through
1262
+ // for `'openai-responses'`; `api.openai.com/v1/responses` for `'openai'` direct.
1263
+ // Same wire shape; the only difference is the `baseUrl` the resolver supplies.
1264
+ return buildOpenAILanguageModel({ getInfo, logBus, emitWireCommunication, innerFetch });
1265
+ case 'openai-compatible':
1266
+ // OpenAI-compatible Chat Completions API (LLMG Express, LiteLLM, etc.).
1267
+ // Different wire shape from Responses — `/v1/chat/completions` with
1268
+ // `choices[0].delta` streaming events.
1269
+ return buildOpenAIChatCompletionsLanguageModel({ getInfo, logBus, emitWireCommunication, innerFetch });
1270
+ default:
1271
+ // Defensive — manager-side G8 should have caught this before
1272
+ // we got here. Make the error typed so consumers can branch
1273
+ // on `err.type` instead of regexing the message.
1274
+ throw new AgentSDKError(`Mastra harness has no language-model builder for providerHint "${initial.providerHint}".`, AgentSDKErrorType.MODEL_NOT_SUPPORTED_BY_HARNESS);
1275
+ }
1276
+ }
1156
1277
  async destroyThreadBestEffort(agentId, threadId) {
1157
1278
  try {
1158
1279
  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.
@@ -24,4 +24,11 @@ export declare function hasEnabledServers(config: MCPConfiguration | undefined):
24
24
  * - When `orgJwt` is provided, remote servers get a `fetch` that delegates to
25
25
  * {@link resolveMcpServerHeaders} per-request to inject auth for Salesforce Platform URLs.
26
26
  */
27
- export declare function mapToMastraMCPServers(config: MCPConfiguration, orgJwt?: JSONWebToken): Record<string, MastraMCPServerDefinition>;
27
+ export declare function mapToMastraMCPServers(config: MCPConfiguration, orgJwt?: JSONWebToken,
28
+ /**
29
+ * Inner fetch routed through the harness's `Dispatcher`. Defaults to
30
+ * `globalThis.fetch`; the harness factory threads a proxy-aware fetch
31
+ * here so MCP remote transports honor `HTTPS_PROXY` without `globalThis`
32
+ * mutation. Stdio transports ignore it (they're child processes).
33
+ */
34
+ innerFetch?: typeof fetch): Record<string, MastraMCPServerDefinition>;
@@ -44,7 +44,14 @@ const DEFAULT_RECONNECTION_OPTIONS = {
44
44
  * - When `orgJwt` is provided, remote servers get a `fetch` that delegates to
45
45
  * {@link resolveMcpServerHeaders} per-request to inject auth for Salesforce Platform URLs.
46
46
  */
47
- export function mapToMastraMCPServers(config, orgJwt) {
47
+ export function mapToMastraMCPServers(config, orgJwt,
48
+ /**
49
+ * Inner fetch routed through the harness's `Dispatcher`. Defaults to
50
+ * `globalThis.fetch`; the harness factory threads a proxy-aware fetch
51
+ * here so MCP remote transports honor `HTTPS_PROXY` without `globalThis`
52
+ * mutation. Stdio transports ignore it (they're child processes).
53
+ */
54
+ innerFetch = globalThis.fetch) {
48
55
  const result = {};
49
56
  for (const [name, serverConfig] of Object.entries(config)) {
50
57
  if (serverConfig.enabled === false) {
@@ -73,7 +80,7 @@ export function mapToMastraMCPServers(config, orgJwt) {
73
80
  ...(serverConfig.headers !== undefined && {
74
81
  requestInit: { headers: serverConfig.headers },
75
82
  }),
76
- ...(orgJwt && { fetch: createAuthFetch(serverConfig.url, orgJwt, serverConfig.headers) }),
83
+ ...(orgJwt && { fetch: createAuthFetch(serverConfig.url, orgJwt, innerFetch, serverConfig.headers) }),
77
84
  ...(serverConfig.timeout !== undefined && {
78
85
  timeout: serverConfig.timeout,
79
86
  connectTimeout: serverConfig.timeout,
@@ -99,14 +106,14 @@ export function mapToMastraMCPServers(config, orgJwt) {
99
106
  }
100
107
  return result;
101
108
  }
102
- function createAuthFetch(serverUrl, orgJwt, existingHeaders) {
109
+ function createAuthFetch(serverUrl, orgJwt, innerFetch, existingHeaders) {
103
110
  return async (url, init) => {
104
111
  const authHeaders = await resolveMcpServerHeaders(serverUrl, orgJwt, existingHeaders);
105
112
  const merged = new Headers(init?.headers);
106
113
  for (const [key, value] of Object.entries(authHeaders)) {
107
114
  merged.set(key, value);
108
115
  }
109
- return globalThis.fetch(url, { ...init, headers: merged });
116
+ return innerFetch(url, { ...init, headers: merged });
110
117
  };
111
118
  }
112
119
  //# sourceMappingURL=mcp-config-mapper.js.map