@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.
- package/dist/default-model-config-factory.d.ts +5 -1
- package/dist/default-model-config-factory.js +5 -2
- package/dist/llmg-express-model-config-factory.d.ts +2 -1
- package/dist/llmg-express-model-config-factory.js +3 -1
- package/dist/mastra-event-adapter.d.ts +9 -7
- package/dist/mastra-event-adapter.js +36 -18
- package/dist/mastra-harness.d.ts +1 -1
- package/dist/mastra-harness.js +31 -10
- package/dist/mastra-message-mapper.d.ts +2 -6
- package/dist/mastra-message-mapper.js +12 -7
- package/dist/mcp-config-mapper.d.ts +4 -0
- package/dist/mcp-config-mapper.js +24 -0
- package/dist/model-config-factory.d.ts +7 -2
- package/dist/salesforce-llm-gateway.d.ts +6 -2
- package/dist/salesforce-llm-gateway.js +16 -3
- package/package.json +5 -5
|
@@ -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
|
-
|
|
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)* | `
|
|
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
|
|
60
|
-
*
|
|
61
|
-
*
|
|
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
|
-
*
|
|
70
|
-
*
|
|
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)* | `
|
|
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
|
|
61
|
-
*
|
|
62
|
-
*
|
|
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
|
-
*
|
|
73
|
-
*
|
|
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
|
|
172
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
package/dist/mastra-harness.d.ts
CHANGED
|
@@ -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
|
|
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`.
|
package/dist/mastra-harness.js
CHANGED
|
@@ -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
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
38
|
-
"@salesforce/llm-gateway-sdk": "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.
|
|
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.
|
|
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",
|