@salesforce/sfdx-agent-harness-mastra 0.13.0 → 0.14.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.
@@ -1,3 +1,4 @@
1
+ import type { LogBus } from '@salesforce/agentic-common';
1
2
  import { type LLMGatewayClient } from '@salesforce/llm-gateway-sdk';
2
3
  import type { MastraModelConfigFactory, ResolvedModelConfig } from './model-config-factory.js';
3
4
  /**
@@ -6,7 +7,10 @@ import type { MastraModelConfigFactory, ResolvedModelConfig } from './model-conf
6
7
  * Wraps the LLMGatewayClient in a SalesforceMastraModelGateway and calls
7
8
  * resolveLanguageModel() to produce the LanguageModelV2 that Mastra consumes.
8
9
  * Captures the gateway model at creation time to provide multimodal validation.
10
+ * Forwards the harness's `logBus` into the bridge so streaming anomalies
11
+ * (e.g. a Bedrock-Anthropic stream that ends without a usage chunk) surface as
12
+ * structured logs.
9
13
  */
10
14
  export declare class DefaultMastraModelConfigFactory implements MastraModelConfigFactory {
11
- create(llmGatewayClient: LLMGatewayClient): ResolvedModelConfig;
15
+ create(llmGatewayClient: LLMGatewayClient, logBus?: LogBus): ResolvedModelConfig;
12
16
  }
@@ -10,10 +10,13 @@ import { SalesforceMastraModelGateway } from './salesforce-llm-gateway.js';
10
10
  * Wraps the LLMGatewayClient in a SalesforceMastraModelGateway and calls
11
11
  * resolveLanguageModel() to produce the LanguageModelV2 that Mastra consumes.
12
12
  * Captures the gateway model at creation time to provide multimodal validation.
13
+ * Forwards the harness's `logBus` into the bridge so streaming anomalies
14
+ * (e.g. a Bedrock-Anthropic stream that ends without a usage chunk) surface as
15
+ * structured logs.
13
16
  */
14
17
  export class DefaultMastraModelConfigFactory {
15
- create(llmGatewayClient) {
16
- const agentGateway = new SalesforceMastraModelGateway(llmGatewayClient);
18
+ create(llmGatewayClient, logBus) {
19
+ const agentGateway = new SalesforceMastraModelGateway(llmGatewayClient, undefined, logBus);
17
20
  const model = agentGateway.resolveLanguageModel();
18
21
  const gatewayModel = llmGatewayClient.getModel();
19
22
  return {
@@ -1,3 +1,4 @@
1
+ import type { LogBus } from '@salesforce/agentic-common';
1
2
  import { type LLMGatewayClient } from '@salesforce/llm-gateway-sdk';
2
3
  import type { MastraModelConfigFactory, ResolvedModelConfig } from './model-config-factory.js';
3
4
  export interface LLMGExpressConfig {
@@ -21,5 +22,5 @@ export interface LLMGExpressConfig {
21
22
  export declare class LLMGExpressMastraModelConfigFactory implements MastraModelConfigFactory {
22
23
  private readonly config;
23
24
  constructor(config: LLMGExpressConfig);
24
- create(llmGatewayClient: LLMGatewayClient): ResolvedModelConfig;
25
+ create(llmGatewayClient: LLMGatewayClient, _logBus?: LogBus): ResolvedModelConfig;
25
26
  }
@@ -18,7 +18,9 @@ export class LLMGExpressMastraModelConfigFactory {
18
18
  constructor(config) {
19
19
  this.config = config;
20
20
  }
21
- create(llmGatewayClient) {
21
+ // _logBus is unused: the express path resolves through Mastra's built-in OpenAI-compatible
22
+ // provider, not SalesforceMastraModelGateway, so the missing-usage warn from W-22692131 doesn't apply.
23
+ create(llmGatewayClient, _logBus) {
22
24
  const model = {
23
25
  providerId: 'llmg-express',
24
26
  modelId: this.config.modelId,
@@ -47,7 +47,7 @@ import type { ChatEvent } from '@salesforce/sfdx-agent-sdk';
47
47
  * | `tool-call-delta`, | | |
48
48
  * | `tool-call-suspended`, | | |
49
49
  * | `object`, `tool-output` | | |
50
- * | *(all others)* | `unmapped-chunk` | `{ chunkType, rawChunk }` for observability |
50
+ * | *(all others)* | *(skipped)* | Logged via `LogBus.debug` with `{ chunkType, rawChunk }` |
51
51
  *
52
52
  * Stateful per stream — a new instance must be created for each `MastraHarness.stream()` call.
53
53
  */
@@ -56,18 +56,20 @@ export declare class MastraEventAdapter {
56
56
  private stepIndex;
57
57
  /**
58
58
  * @param logBus - Optional structured-log sink. When supplied, the adapter emits a `debug`
59
- * log for each chunk it intentionally drops (informational lifecycle events the SDK
60
- * doesn't model, and known-but-unsupported chunk types) so consumers can observe what's
61
- * being filtered without it leaking into the public `ChatEvent` stream.
59
+ * log for every chunk it intentionally drops — informational lifecycle events, known-but-
60
+ * unsupported chunk types, and any unrecognized chunk type the adapter has not seen — so
61
+ * harness developers can observe forward-compat gaps without harness internals leaking
62
+ * onto the public `ChatEvent` stream.
62
63
  */
63
64
  constructor(logBus?: LogBus | undefined);
64
65
  /**
65
66
  * Convert a Mastra stream chunk to a ChatEvent.
66
67
  *
67
68
  * @param chunk - A Mastra `ChunkType` from `agent.stream().fullStream`.
68
- * @returns The corresponding {@link ChatEvent}, `undefined` for
69
- * informational chunks that should be silently skipped, or an
70
- * `UnmappedChunkEvent` for truly unrecognized chunk types.
69
+ * @returns The corresponding {@link ChatEvent}, or `undefined` for chunks the
70
+ * adapter intentionally skips (informational lifecycle events, known-but-
71
+ * unsupported chunk types, and unrecognized chunk types). Skipped chunks are
72
+ * logged via the optional `LogBus` so harness developers can still observe them.
71
73
  */
72
74
  toChatEvent(chunk: ChunkType): ChatEvent | undefined;
73
75
  }
@@ -48,7 +48,7 @@
48
48
  * | `tool-call-delta`, | | |
49
49
  * | `tool-call-suspended`, | | |
50
50
  * | `object`, `tool-output` | | |
51
- * | *(all others)* | `unmapped-chunk` | `{ chunkType, rawChunk }` for observability |
51
+ * | *(all others)* | *(skipped)* | Logged via `LogBus.debug` with `{ chunkType, rawChunk }` |
52
52
  *
53
53
  * Stateful per stream — a new instance must be created for each `MastraHarness.stream()` call.
54
54
  */
@@ -57,9 +57,10 @@ export class MastraEventAdapter {
57
57
  stepIndex = 0;
58
58
  /**
59
59
  * @param logBus - Optional structured-log sink. When supplied, the adapter emits a `debug`
60
- * log for each chunk it intentionally drops (informational lifecycle events the SDK
61
- * doesn't model, and known-but-unsupported chunk types) so consumers can observe what's
62
- * being filtered without it leaking into the public `ChatEvent` stream.
60
+ * log for every chunk it intentionally drops — informational lifecycle events, known-but-
61
+ * unsupported chunk types, and any unrecognized chunk type the adapter has not seen — so
62
+ * harness developers can observe forward-compat gaps without harness internals leaking
63
+ * onto the public `ChatEvent` stream.
63
64
  */
64
65
  constructor(logBus) {
65
66
  this.logBus = logBus;
@@ -68,9 +69,10 @@ export class MastraEventAdapter {
68
69
  * Convert a Mastra stream chunk to a ChatEvent.
69
70
  *
70
71
  * @param chunk - A Mastra `ChunkType` from `agent.stream().fullStream`.
71
- * @returns The corresponding {@link ChatEvent}, `undefined` for
72
- * informational chunks that should be silently skipped, or an
73
- * `UnmappedChunkEvent` for truly unrecognized chunk types.
72
+ * @returns The corresponding {@link ChatEvent}, or `undefined` for chunks the
73
+ * adapter intentionally skips (informational lifecycle events, known-but-
74
+ * unsupported chunk types, and unrecognized chunk types). Skipped chunks are
75
+ * logged via the optional `LogBus` so harness developers can still observe them.
74
76
  */
75
77
  toChatEvent(chunk) {
76
78
  switch (chunk.type) {
@@ -168,8 +170,8 @@ export class MastraEventAdapter {
168
170
  this.logBus?.debug('Skipping informational Mastra chunk', { chunkType: chunk.type });
169
171
  return undefined;
170
172
  // Known harness chunk types not yet supported in the SDK's ChatEvent
171
- // model. Silently dropped to avoid leaking harness internals via
172
- // unmapped-chunk. Candidates for future ChatEvent variants when the
173
+ // model. Silently dropped to avoid leaking harness internals onto the
174
+ // public stream. Candidates for future ChatEvent variants when the
173
175
  // corresponding features (citations, file generation, tool arg
174
176
  // streaming, tool suspension, structured output) are supported.
175
177
  case 'source':
@@ -183,7 +185,15 @@ export class MastraEventAdapter {
183
185
  this.logBus?.debug('Skipping unsupported Mastra chunk type', { chunkType: chunk.type });
184
186
  return undefined;
185
187
  default:
186
- return { type: 'unmapped-chunk', chunkType: chunk.type, rawChunk: chunk };
188
+ // Forward-compat: a newer Mastra version can emit a chunk type the
189
+ // adapter has not seen. Surface it on `LogBus.debug` for harness
190
+ // developers and skip it on the public stream so harness internals
191
+ // never reach consumers.
192
+ this.logBus?.debug('Skipping unrecognized Mastra chunk type', {
193
+ chunkType: chunk.type,
194
+ rawChunk: chunk,
195
+ });
196
+ return undefined;
187
197
  }
188
198
  }
189
199
  }
@@ -195,13 +205,21 @@ function mapFinishReason(reason) {
195
205
  function mapUsage(usage) {
196
206
  if (!usage)
197
207
  return undefined;
198
- return {
199
- inputTokens: usage.inputTokens ?? undefined,
200
- outputTokens: usage.outputTokens ?? undefined,
201
- totalTokens: usage.totalTokens ?? undefined,
202
- reasoningTokens: usage.reasoningTokens,
203
- cachedInputTokens: usage.cachedInputTokens,
204
- cacheWriteInputTokens: usage.cacheCreationInputTokens ? usage.cacheCreationInputTokens : undefined,
205
- };
208
+ const inputTokens = usage.inputTokens ?? undefined;
209
+ const outputTokens = usage.outputTokens ?? undefined;
210
+ const totalTokens = usage.totalTokens ?? undefined;
211
+ const reasoningTokens = usage.reasoningTokens;
212
+ const cachedInputTokens = usage.cachedInputTokens;
213
+ const cacheWriteInputTokens = usage.cacheCreationInputTokens ? usage.cacheCreationInputTokens : undefined;
214
+ // Collapse all-falsy usage to undefined so consumers see "no usage" instead of a populated-but-empty object (W-22692131).
215
+ if (!inputTokens &&
216
+ !outputTokens &&
217
+ !totalTokens &&
218
+ !reasoningTokens &&
219
+ !cachedInputTokens &&
220
+ !cacheWriteInputTokens) {
221
+ return undefined;
222
+ }
223
+ return { inputTokens, outputTokens, totalTokens, reasoningTokens, cachedInputTokens, cacheWriteInputTokens };
206
224
  }
207
225
  //# sourceMappingURL=mastra-event-adapter.js.map
@@ -332,7 +332,7 @@ export declare class MastraHarness implements MastraAgentHarness {
332
332
  * - MUST throw an error if the specified `agentId` is not found in `activeAgents`.
333
333
  * - MUST clone the source thread's messages into a new thread.
334
334
  */
335
- cloneThread(agentId: string, sourceThreadId: string, targetThreadId?: string): Promise<string>;
335
+ cloneThread(agentId: string, sourceThreadId: string): Promise<string>;
336
336
  /**
337
337
  * @requirements
338
338
  * - MUST throw an error if the specified `agentId` is not found in `activeAgents`.
@@ -23,7 +23,7 @@ import { buildMcpErrorDetail, sanitizeMcpErrorMessage } from './mcp-error-classi
23
23
  import { DefaultMastraModelConfigFactory } from './default-model-config-factory.js';
24
24
  import { partitionPool, } from './tool-search.js';
25
25
  import {} from '@salesforce/llm-gateway-sdk';
26
- import { buildSummaryPrompt, Clock, getErrorMessage, RealClock, splitFrontmatterAndBody, UUIDGenerator, } from '@salesforce/agentic-common';
26
+ import { backfillCreatedAt, buildSummaryPrompt, Clock, getErrorMessage, RealClock, splitFrontmatterAndBody, UUIDGenerator, } from '@salesforce/agentic-common';
27
27
  import { makeAbortable } from './utils.js';
28
28
  import { Workspace, LocalFilesystem, LocalSandbox, WORKSPACE_TOOLS, WORKSPACE_TOOLS_PREFIX, } from '@mastra/core/workspace';
29
29
  /**
@@ -529,7 +529,7 @@ export class MastraHarness {
529
529
  }
530
530
  }
531
531
  const customTools = config.tools?.length ? mapToolDefinitionsToMastraTools(config.tools) : {};
532
- const { model: resolvedModel, validateFiles } = this.modelConfigFactory.create(llmGatewayClient);
532
+ const { model: resolvedModel, validateFiles } = this.modelConfigFactory.create(llmGatewayClient, this.buses.getLogBus());
533
533
  let mastraWorkspace;
534
534
  let agent;
535
535
  let mastraAgentAdded = false;
@@ -870,12 +870,9 @@ export class MastraHarness {
870
870
  * - MUST throw an error if the specified `agentId` is not found in `activeAgents`.
871
871
  * - MUST clone the source thread's messages into a new thread.
872
872
  */
873
- async cloneThread(agentId, sourceThreadId, targetThreadId) {
873
+ async cloneThread(agentId, sourceThreadId) {
874
874
  const memory = await this.getMemoryOrThrow(agentId);
875
- const { thread } = await memory.cloneThread({
876
- sourceThreadId,
877
- newThreadId: targetThreadId,
878
- });
875
+ const { thread } = await memory.cloneThread({ sourceThreadId });
879
876
  return thread.id;
880
877
  }
881
878
  /**
@@ -1265,10 +1262,26 @@ export class MastraHarness {
1265
1262
  // `'system' | 'user' | 'assistant' | 'tool'` and surfacing experimental upstream concepts would leak the
1266
1263
  // harness internals up to consumers. If Mastra promotes signals to stable and we want them, the SDK
1267
1264
  // contract needs to add the role first.
1265
+ // Per-position fallback for any persisted row whose `createdAt` is
1266
+ // null/undefined (legacy rows from before the optional-on-write
1267
+ // contract, partial-write recovery, or a libsql schema change).
1268
+ // Mirrors the Claude harness's clock.nextAfter pattern so the
1269
+ // read-side contract — every returned Message.createdAt populated;
1270
+ // sort ascending — holds even when the persisted layer fails to
1271
+ // produce a timestamp. Without this, the prior `?? 0` form would
1272
+ // silently bucket null-timestamp rows at epoch and propagate
1273
+ // `undefined` to consumers.
1274
+ let lastFallback = this.clock.now();
1268
1275
  return messages
1269
1276
  .filter((m) => m.role !== 'signal')
1270
1277
  .map((m) => mapMastraToMessage(m, logBus))
1271
- .sort((a, b) => (a.createdAt?.getTime() ?? 0) - (b.createdAt?.getTime() ?? 0));
1278
+ .map((m) => {
1279
+ if (m.createdAt)
1280
+ return m;
1281
+ lastFallback = this.clock.nextAfter(lastFallback);
1282
+ return { ...m, createdAt: lastFallback };
1283
+ })
1284
+ .sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
1272
1285
  }
1273
1286
  /**
1274
1287
  * @requirements
@@ -1294,7 +1307,15 @@ export class MastraHarness {
1294
1307
  async addContext(agentId, threadId, messages) {
1295
1308
  const memory = await this.getMemoryOrThrow(agentId);
1296
1309
  const resourceId = this.agentResourceId(agentId);
1297
- const mapped = messages.map((m) => mapMessageToMastra(m, threadId, resourceId, this.clock));
1310
+ // Backfill `createdAt` on any caller-supplied message that omits it.
1311
+ // The SDK's `ChatSession.addContext` already does this for SDK-routed
1312
+ // calls; the harness backfills defensively so a direct
1313
+ // `harness.addContext` call still produces a sortable persisted
1314
+ // record (Mastra's libsql column is non-nullable). Routes through the
1315
+ // shared `backfillCreatedAt` helper from `agentic-common` so the SDK
1316
+ // and both harnesses can't drift on the per-position step policy.
1317
+ const filled = backfillCreatedAt(messages, this.clock);
1318
+ const mapped = filled.map((m) => mapMessageToMastra(m, threadId, resourceId));
1298
1319
  await memory.saveMessages({ messages: mapped });
1299
1320
  }
1300
1321
  // ── Private Helpers ──────────────────────────────────────────────
@@ -1372,7 +1393,7 @@ export class MastraHarness {
1372
1393
  getAgentStateOrThrow(agentId) {
1373
1394
  const state = this.activeAgents.get(agentId);
1374
1395
  if (!state) {
1375
- throw new Error(`Agent with id "${agentId}" is not registered`);
1396
+ throw new AgentSDKError(`No Agent found with id: "${agentId}"`, AgentSDKErrorType.AGENT_NOT_FOUND);
1376
1397
  }
1377
1398
  return state;
1378
1399
  }
@@ -1,6 +1,6 @@
1
1
  import type { MastraDBMessage, MastraMessagePart } from '@mastra/core/agent';
2
2
  import type { Message, MessagePart } from '@salesforce/sfdx-agent-sdk';
3
- import { Clock, type LogBus } from '@salesforce/agentic-common';
3
+ import type { LogBus } from '@salesforce/agentic-common';
4
4
  /**
5
5
  * Converts a Mastra persisted message into the SDK's {@link Message} type.
6
6
  *
@@ -26,12 +26,8 @@ export declare function mapMastraPartToMessagePart(part: MastraMessagePart, logB
26
26
  * Converts an SDK {@link Message} into the Mastra persisted message format.
27
27
  * The `role` field is normalised: `'tool'` role (which has no Mastra equivalent)
28
28
  * is coerced to `'assistant'`.
29
- *
30
- * @param clock - Optional clock used for the `createdAt` fallback when the
31
- * source message has no timestamp. Defaults to `RealClock`. Tests inject
32
- * a stub to make assertions deterministic.
33
29
  */
34
- export declare function mapMessageToMastra(msg: Message, threadId: string, resourceId: string, clock?: Clock): MastraDBMessage;
30
+ export declare function mapMessageToMastra(msg: Message, threadId: string, resourceId: string): MastraDBMessage;
35
31
  /**
36
32
  * Converts a single SDK {@link MessagePart} into the Mastra message part format.
37
33
  */
@@ -2,7 +2,6 @@
2
2
  * Copyright 2026, Salesforce, Inc. All rights reserved.
3
3
  * See LICENSE.txt for license terms.
4
4
  */
5
- import { Clock, RealClock } from '@salesforce/agentic-common';
6
5
  /**
7
6
  * Reads the SDK `fileName` stashed under `providerMetadata.salesforce.fileName` when a file part
8
7
  * was persisted via {@link mapMessagePartToMastra}. Returns `undefined` when absent or not a string.
@@ -155,12 +154,8 @@ export function mapMastraPartToMessagePart(part, logBus) {
155
154
  * Converts an SDK {@link Message} into the Mastra persisted message format.
156
155
  * The `role` field is normalised: `'tool'` role (which has no Mastra equivalent)
157
156
  * is coerced to `'assistant'`.
158
- *
159
- * @param clock - Optional clock used for the `createdAt` fallback when the
160
- * source message has no timestamp. Defaults to `RealClock`. Tests inject
161
- * a stub to make assertions deterministic.
162
157
  */
163
- export function mapMessageToMastra(msg, threadId, resourceId, clock = new RealClock()) {
158
+ export function mapMessageToMastra(msg, threadId, resourceId) {
164
159
  const parts = [];
165
160
  if (typeof msg.content === 'string') {
166
161
  parts.push({ type: 'text', text: msg.content });
@@ -171,10 +166,20 @@ export function mapMessageToMastra(msg, threadId, resourceId, clock = new RealCl
171
166
  }
172
167
  }
173
168
  const role = msg.role === 'tool' ? 'assistant' : msg.role;
169
+ if (!msg.createdAt) {
170
+ // Internal contract: callers (the harness's `addContext` and
171
+ // `compactThread`) backfill `createdAt` before reaching this
172
+ // mapper, so Mastra's non-nullable persisted column always has a
173
+ // value. Throwing here surfaces the bug at its real call site
174
+ // instead of producing a silently-wrong row downstream. Include
175
+ // role + threadId so the failure self-locates if a future caller
176
+ // forgets to backfill.
177
+ throw new Error(`mapMessageToMastra: msg.createdAt is required (id="${msg.id}", role="${msg.role}", threadId="${threadId}")`);
178
+ }
174
179
  return {
175
180
  id: msg.id,
176
181
  role,
177
- createdAt: msg.createdAt ?? clock.now(),
182
+ createdAt: msg.createdAt,
178
183
  threadId,
179
184
  resourceId,
180
185
  content: {
@@ -17,6 +17,10 @@ export declare function hasEnabledServers(config: MCPConfiguration | undefined):
17
17
  * Mastra's `timeout` (post-connect request lifetime) and `connectTimeout` (initial transport
18
18
  * handshake) so the configured value covers both phases — Mastra's hardcoded 3s
19
19
  * `connectTimeout` default would otherwise abort cold-start handshakes before `timeout` applied.
20
+ * - Per-server `reconnectionOptions` is forwarded to Mastra's `HttpServerDefinition.reconnectionOptions`
21
+ * (typed as `StreamableHTTPClientTransportOptions['reconnectionOptions']`). Unspecified fields
22
+ * are filled from {@link DEFAULT_RECONNECTION_OPTIONS} so partial overrides leave the rest at
23
+ * the MCP SDK's built-in defaults.
20
24
  * - When `orgJwt` is provided, remote servers get a `fetch` that delegates to
21
25
  * {@link resolveMcpServerHeaders} per-request to inject auth for Salesforce Platform URLs.
22
26
  */
@@ -12,6 +12,20 @@ export function hasEnabledServers(config) {
12
12
  return false;
13
13
  return Object.values(config).some((s) => s.enabled !== false);
14
14
  }
15
+ /**
16
+ * MCP SDK's built-in `StreamableHTTPReconnectionOptions` defaults. Mirrored
17
+ * here so partial `MCPRemoteServerConfig.reconnectionOptions` inputs merge
18
+ * field-wise with the underlying defaults rather than replacing them — the
19
+ * underlying transport replaces the entire defaults object when
20
+ * `reconnectionOptions` is set, so a partial override would otherwise zero
21
+ * out the unspecified fields.
22
+ */
23
+ const DEFAULT_RECONNECTION_OPTIONS = {
24
+ maxRetries: 2,
25
+ initialReconnectionDelay: 1000,
26
+ maxReconnectionDelay: 30000,
27
+ reconnectionDelayGrowFactor: 1.5,
28
+ };
15
29
  /**
16
30
  * Converts our {@link MCPConfiguration} into the `servers` map expected by Mastra's `MCPClient`.
17
31
  *
@@ -23,6 +37,10 @@ export function hasEnabledServers(config) {
23
37
  * Mastra's `timeout` (post-connect request lifetime) and `connectTimeout` (initial transport
24
38
  * handshake) so the configured value covers both phases — Mastra's hardcoded 3s
25
39
  * `connectTimeout` default would otherwise abort cold-start handshakes before `timeout` applied.
40
+ * - Per-server `reconnectionOptions` is forwarded to Mastra's `HttpServerDefinition.reconnectionOptions`
41
+ * (typed as `StreamableHTTPClientTransportOptions['reconnectionOptions']`). Unspecified fields
42
+ * are filled from {@link DEFAULT_RECONNECTION_OPTIONS} so partial overrides leave the rest at
43
+ * the MCP SDK's built-in defaults.
26
44
  * - When `orgJwt` is provided, remote servers get a `fetch` that delegates to
27
45
  * {@link resolveMcpServerHeaders} per-request to inject auth for Salesforce Platform URLs.
28
46
  */
@@ -53,6 +71,12 @@ export function mapToMastraMCPServers(config, orgJwt) {
53
71
  timeout: serverConfig.timeout,
54
72
  connectTimeout: serverConfig.timeout,
55
73
  }),
74
+ ...(serverConfig.reconnectionOptions !== undefined && {
75
+ reconnectionOptions: {
76
+ ...DEFAULT_RECONNECTION_OPTIONS,
77
+ ...serverConfig.reconnectionOptions,
78
+ },
79
+ }),
56
80
  };
57
81
  result[name] = def;
58
82
  }
@@ -1,4 +1,5 @@
1
1
  import type { MastraModelConfig } from '@mastra/core/llm';
2
+ import type { LogBus } from '@salesforce/agentic-common';
2
3
  import type { LLMGatewayClient, MultimodalFile } from '@salesforce/llm-gateway-sdk';
3
4
  export interface ResolvedModelConfig {
4
5
  model: MastraModelConfig;
@@ -21,8 +22,12 @@ export interface ResolvedModelConfig {
21
22
  * Implementations decide how to build a language model from the available
22
23
  * context. The LLMGatewayClient is provided for implementations that need
23
24
  * Salesforce org-authenticated model access; implementations that use
24
- * alternative backends (e.g., direct API key auth) may ignore it.
25
+ * alternative backends (e.g., direct API key auth) may ignore it. The
26
+ * optional `logBus` is the harness's structured-log sink — implementations
27
+ * that wrap a streaming gateway (e.g. `SalesforceMastraModelGateway`) use it
28
+ * to surface non-fatal anomalies (such as missing usage chunks) without
29
+ * polluting the public `ChatEvent` stream.
25
30
  */
26
31
  export interface MastraModelConfigFactory {
27
- create(llmGatewayClient: LLMGatewayClient): ResolvedModelConfig;
32
+ create(llmGatewayClient: LLMGatewayClient, logBus?: LogBus): ResolvedModelConfig;
28
33
  }
@@ -1,7 +1,7 @@
1
1
  import { MastraModelGateway, type ProviderConfig } from '@mastra/core/llm';
2
2
  import type { LanguageModelV3 } from '@ai-sdk/provider';
3
3
  import type { LLMGatewayClient } from '@salesforce/llm-gateway-sdk';
4
- import { type UniqueIDGenerator } from '@salesforce/agentic-common';
4
+ import { type LogBus, type UniqueIDGenerator } from '@salesforce/agentic-common';
5
5
  /**
6
6
  * Bridges the Salesforce LLM Gateway SDK to Mastra's MastraModelGateway interface.
7
7
  *
@@ -12,14 +12,18 @@ import { type UniqueIDGenerator } from '@salesforce/agentic-common';
12
12
  export declare class SalesforceMastraModelGateway extends MastraModelGateway {
13
13
  private readonly gatewayClient;
14
14
  private readonly idGenerator;
15
+ private readonly logBus?;
15
16
  readonly id = "sfdc";
16
17
  readonly name = "Salesforce LLM Gateway";
17
18
  /**
18
19
  * @param gatewayClient - the LLM Gateway client this bridge delegates generation to.
19
20
  * @param idGenerator - source of the `fileId` stamped on each translated multimodal file.
20
21
  * Defaults to `UUIDGenerator`; tests inject a stub for deterministic ids.
22
+ * @param logBus - optional structured-log sink. When supplied, `doStream` emits a `warn`
23
+ * log on streams that end without a usage chunk (W-22692131) so operators can correlate
24
+ * the upstream condition with downstream missing-usage observations.
21
25
  */
22
- constructor(gatewayClient: LLMGatewayClient, idGenerator?: UniqueIDGenerator);
26
+ constructor(gatewayClient: LLMGatewayClient, idGenerator?: UniqueIDGenerator, logBus?: LogBus | undefined);
23
27
  getApiKey(_modelId: string): Promise<string>;
24
28
  buildUrl(_modelId: string, _envVars: Record<string, string>): string;
25
29
  fetchProviders(): Promise<Record<string, ProviderConfig>>;
@@ -15,17 +15,22 @@ import { UUIDGenerator } from '@salesforce/agentic-common';
15
15
  export class SalesforceMastraModelGateway extends MastraModelGateway {
16
16
  gatewayClient;
17
17
  idGenerator;
18
+ logBus;
18
19
  id = 'sfdc';
19
20
  name = 'Salesforce LLM Gateway';
20
21
  /**
21
22
  * @param gatewayClient - the LLM Gateway client this bridge delegates generation to.
22
23
  * @param idGenerator - source of the `fileId` stamped on each translated multimodal file.
23
24
  * Defaults to `UUIDGenerator`; tests inject a stub for deterministic ids.
25
+ * @param logBus - optional structured-log sink. When supplied, `doStream` emits a `warn`
26
+ * log on streams that end without a usage chunk (W-22692131) so operators can correlate
27
+ * the upstream condition with downstream missing-usage observations.
24
28
  */
25
- constructor(gatewayClient, idGenerator = new UUIDGenerator()) {
29
+ constructor(gatewayClient, idGenerator = new UUIDGenerator(), logBus) {
26
30
  super();
27
31
  this.gatewayClient = gatewayClient;
28
32
  this.idGenerator = idGenerator;
33
+ this.logBus = logBus;
29
34
  }
30
35
  async getApiKey(_modelId) {
31
36
  return '';
@@ -87,7 +92,9 @@ export class SalesforceMastraModelGateway extends MastraModelGateway {
87
92
  const stream = new ReadableStream({
88
93
  start: async (controller) => {
89
94
  try {
90
- let lastUsage = mapUsage(undefined);
95
+ // V3 requires a `usage` field on `finish`; leave undefined here so we can
96
+ // detect a stream that ended without a usage chunk (W-22692131).
97
+ let lastUsage;
91
98
  for await (const chunk of response.data) {
92
99
  if (chunk.generatedText && !chunk.done) {
93
100
  if (!textSpanOpen) {
@@ -126,10 +133,16 @@ export class SalesforceMastraModelGateway extends MastraModelGateway {
126
133
  controller.enqueue({ type: 'text-end', id: `text-${textSpanId}` });
127
134
  }
128
135
  const finishReason = mapFinishReason(chunk.finishReason, sawToolCalls);
136
+ if (lastUsage === undefined) {
137
+ this.logBus?.warn('LLM Gateway stream ended without a usage chunk', {
138
+ modelId: this.gatewayClient.getModel().name,
139
+ finishReason: chunk.finishReason,
140
+ });
141
+ }
129
142
  controller.enqueue({
130
143
  type: 'finish',
131
144
  finishReason,
132
- usage: lastUsage,
145
+ usage: lastUsage ?? mapUsage(undefined),
133
146
  });
134
147
  controller.close();
135
148
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/sfdx-agent-harness-mastra",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Mastra-backed AgentHarness implementation for @salesforce/sfdx-agent-sdk",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -34,15 +34,15 @@
34
34
  "@mastra/mcp": "^1.8.0",
35
35
  "@mastra/memory": "^1.19.0",
36
36
  "@mastra/schema-compat": "^1.2.10",
37
- "@salesforce/agentic-common": "0.6.0",
38
- "@salesforce/llm-gateway-sdk": "0.10.0"
37
+ "@salesforce/agentic-common": "0.7.0",
38
+ "@salesforce/llm-gateway-sdk": "0.11.0"
39
39
  },
40
40
  "peerDependencies": {
41
- "@salesforce/sfdx-agent-sdk": "0.14.0"
41
+ "@salesforce/sfdx-agent-sdk": "0.15.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@eslint/js": "^10.0.1",
45
- "@salesforce/sfdx-agent-sdk": "0.14.0",
45
+ "@salesforce/sfdx-agent-sdk": "0.15.0",
46
46
  "@types/node": "^22.19.17",
47
47
  "@vitest/coverage-istanbul": "^4.1.7",
48
48
  "@vitest/eslint-plugin": "^1.6.17",