@salesforce/sfdx-agent-harness-mastra 0.38.0 → 0.40.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 +10 -0
- package/README.md +10 -0
- package/dist/mastra-approval-coordinator.d.ts +15 -2
- package/dist/mastra-approval-coordinator.js +51 -0
- package/dist/mastra-harness.d.ts +1 -0
- package/dist/mastra-harness.js +91 -24
- package/dist/pass-through-anthropic.d.ts +3 -0
- package/dist/pass-through-anthropic.js +6 -4
- package/dist/pass-through-openai-chat-completions.d.ts +2 -0
- package/dist/pass-through-openai-chat-completions.js +6 -5
- package/dist/pass-through-openai.d.ts +2 -0
- package/dist/pass-through-openai.js +6 -5
- package/dist/v3-stream-mapping.d.ts +18 -3
- package/dist/v3-stream-mapping.js +174 -12
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
All notable changes to `@salesforce/sfdx-agent-harness-mastra` are documented in this file.
|
|
4
4
|
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
5
|
|
|
6
|
+
## [0.40.0] - 2026-08-03
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
- **agent-sdk,harnesses**: emit Tier-1 logBus siblings for lifecycle telemetry @W-23547150@ ([#716](https://github.com/forcedotcom/agentic-dx/pull/716))
|
|
10
|
+
|
|
11
|
+
## [0.39.0] - 2026-08-03
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
- **agent-sdk,harness-mastra,harness-claude,harness-openai**: observability gaps — mcp-tool-call-completed, llm-retry, responseText/TTFT @W-23528008@ ([#717](https://github.com/forcedotcom/agentic-dx/pull/717))
|
|
15
|
+
|
|
6
16
|
## [0.38.0] - 2026-07-30
|
|
7
17
|
|
|
8
18
|
### Features
|
package/README.md
CHANGED
|
@@ -24,6 +24,16 @@ const manager = await createAgentManager('/path/to/storage', new MastraHarnessFa
|
|
|
24
24
|
See the [`@salesforce/sfdx-agent-sdk` README](../sfdx-agent-sdk/README.md) for the full consumer-facing API. For
|
|
25
25
|
internal architecture, see [ARCHITECTURE.md](ARCHITECTURE.md).
|
|
26
26
|
|
|
27
|
+
### Observability compatibility
|
|
28
|
+
|
|
29
|
+
This in-process harness emits structured `llm-request` / `llm-response` wire events. Matching events carry the
|
|
30
|
+
harness-generated `correlationId`; `llm-response` provides best-effort `responseText` and `timeToFirstTokenMs` after the
|
|
31
|
+
streaming body terminates. It also emits the debug-only `mcp-tool-call-completed` event; its `durationMs` is an
|
|
32
|
+
adapter/coordinator-observed window and can include a model round-trip. It does **not** emit `llm-retry` telemetry
|
|
33
|
+
because its stateless fetch layer has no observable retry attempt counter. See the SDK README's
|
|
34
|
+
[cross-harness coverage table](../sfdx-agent-sdk/README.md#cross-harness-coverage) for the complete contract and
|
|
35
|
+
cross-harness comparison.
|
|
36
|
+
|
|
27
37
|
### Removing the storage directory on Windows
|
|
28
38
|
|
|
29
39
|
`manager.shutdown()` checkpoints the WAL and closes the libsql client (forcedotcom/agentic-dx#429,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { Agent } from '@mastra/core/agent';
|
|
2
2
|
import type { RequestContext } from '@mastra/core/request-context';
|
|
3
3
|
import type { ChunkType } from '@mastra/core/stream';
|
|
4
|
-
import { type ChatEvent, type Decision as PolicyDecision, type McpToolAnnotations, type ToolInvocation } from '@salesforce/sfdx-agent-sdk';
|
|
4
|
+
import { type ChatEvent, type Decision as PolicyDecision, type McpToolAnnotations, type ToolInvocation, type WireCommunicationEvent } from '@salesforce/sfdx-agent-sdk';
|
|
5
5
|
import { GenSink } from '@salesforce/sfdx-agent-sdk/harness';
|
|
6
|
-
import type { LogBus } from '@salesforce/agentic-common';
|
|
6
|
+
import type { Clock, LogBus } from '@salesforce/agentic-common';
|
|
7
7
|
/**
|
|
8
8
|
* Output shape from `agent.stream()` / `agent.approveToolCall()` /
|
|
9
9
|
* `agent.declineToolCall()`. Captured as a structural type so the
|
|
@@ -113,6 +113,9 @@ export interface MastraApprovalCoordinatorOptions {
|
|
|
113
113
|
*/
|
|
114
114
|
skillBridgeToolNames?: ReadonlySet<string>;
|
|
115
115
|
logBus?: LogBus;
|
|
116
|
+
clock: Clock;
|
|
117
|
+
emitWireCommunication: (event: WireCommunicationEvent) => void;
|
|
118
|
+
hasWireCommunicationSubscribers: () => boolean;
|
|
116
119
|
}
|
|
117
120
|
/**
|
|
118
121
|
* Per-`(agentId, threadId)` coordinator that subsumes the pre-#529
|
|
@@ -161,6 +164,9 @@ export declare class MastraApprovalCoordinator {
|
|
|
161
164
|
private readonly consumerToolNames;
|
|
162
165
|
private readonly lookupMcpToolMetadata?;
|
|
163
166
|
private readonly skillBridgeToolNames;
|
|
167
|
+
private readonly clock;
|
|
168
|
+
private readonly emitWireCommunication;
|
|
169
|
+
private readonly hasWireCommunicationSubscribers;
|
|
164
170
|
private readonly adapter;
|
|
165
171
|
/** Single per-turn sink. Issue #529 contract: one stream per turn. */
|
|
166
172
|
private readonly initial;
|
|
@@ -183,6 +189,7 @@ export declare class MastraApprovalCoordinator {
|
|
|
183
189
|
private readonly approvalTimers;
|
|
184
190
|
/** Args from the adapter's `tool-call` ChatEvent keyed by `toolCallId`. */
|
|
185
191
|
private readonly toolCallArgs;
|
|
192
|
+
private readonly pendingMcpToolCallStarts;
|
|
186
193
|
/**
|
|
187
194
|
* Mastra's current `runId` after the latest stream/resume. Updated on
|
|
188
195
|
* each successful pump entry. Needed for the next
|
|
@@ -395,6 +402,12 @@ export declare class MastraApprovalCoordinator {
|
|
|
395
402
|
* `tool-result` / `tool-approval-request`.
|
|
396
403
|
*/
|
|
397
404
|
private routeEnriched;
|
|
405
|
+
/**
|
|
406
|
+
* Emits a coordinator-observed call-to-result window for a real MCP tool.
|
|
407
|
+
* Mastra durationMs includes model round-trip time, so it is not comparable
|
|
408
|
+
* to the Claude bridge's host-to-MCP RPC latency.
|
|
409
|
+
*/
|
|
410
|
+
private emitMcpToolCallCompleted;
|
|
398
411
|
private maybeEnrich;
|
|
399
412
|
/**
|
|
400
413
|
* Build the {@link ToolInvocation} the policy resolver matches against, and
|
|
@@ -76,6 +76,9 @@ export class MastraApprovalCoordinator {
|
|
|
76
76
|
consumerToolNames;
|
|
77
77
|
lookupMcpToolMetadata;
|
|
78
78
|
skillBridgeToolNames;
|
|
79
|
+
clock;
|
|
80
|
+
emitWireCommunication;
|
|
81
|
+
hasWireCommunicationSubscribers;
|
|
79
82
|
adapter;
|
|
80
83
|
/** Single per-turn sink. Issue #529 contract: one stream per turn. */
|
|
81
84
|
initial = new GenSink();
|
|
@@ -98,6 +101,7 @@ export class MastraApprovalCoordinator {
|
|
|
98
101
|
approvalTimers = new Map();
|
|
99
102
|
/** Args from the adapter's `tool-call` ChatEvent keyed by `toolCallId`. */
|
|
100
103
|
toolCallArgs = new Map();
|
|
104
|
+
pendingMcpToolCallStarts = new Map();
|
|
101
105
|
/**
|
|
102
106
|
* Mastra's current `runId` after the latest stream/resume. Updated on
|
|
103
107
|
* each successful pump entry. Needed for the next
|
|
@@ -177,6 +181,9 @@ export class MastraApprovalCoordinator {
|
|
|
177
181
|
this.consumerToolNames = opts.consumerToolNames;
|
|
178
182
|
this.lookupMcpToolMetadata = opts.lookupMcpToolMetadata;
|
|
179
183
|
this.skillBridgeToolNames = opts.skillBridgeToolNames ?? new Set();
|
|
184
|
+
this.clock = opts.clock;
|
|
185
|
+
this.emitWireCommunication = opts.emitWireCommunication;
|
|
186
|
+
this.hasWireCommunicationSubscribers = opts.hasWireCommunicationSubscribers;
|
|
180
187
|
this.adapter = new MastraEventAdapter(opts.logBus);
|
|
181
188
|
}
|
|
182
189
|
// ───────────────────────── lifecycle / sinks ────────────────────────────
|
|
@@ -302,6 +309,7 @@ export class MastraApprovalCoordinator {
|
|
|
302
309
|
*/
|
|
303
310
|
teardown(reason, error, code) {
|
|
304
311
|
this.cancelAllTimers();
|
|
312
|
+
this.pendingMcpToolCallStarts.clear();
|
|
305
313
|
for (const id of this.emittedApprovals)
|
|
306
314
|
this.settledByCoordinator.add(id);
|
|
307
315
|
// Issue #589: drain in-flight consumer-tool ids into the
|
|
@@ -546,6 +554,7 @@ export class MastraApprovalCoordinator {
|
|
|
546
554
|
sawTerminal = true;
|
|
547
555
|
this.cancelAllTimers();
|
|
548
556
|
this.routeEnriched(event);
|
|
557
|
+
this.pendingMcpToolCallStarts.clear();
|
|
549
558
|
this.initial.end();
|
|
550
559
|
return 'terminal';
|
|
551
560
|
}
|
|
@@ -553,6 +562,7 @@ export class MastraApprovalCoordinator {
|
|
|
553
562
|
sawTerminal = true;
|
|
554
563
|
this.cancelAllTimers();
|
|
555
564
|
this.routeEnriched(event);
|
|
565
|
+
this.pendingMcpToolCallStarts.clear();
|
|
556
566
|
// Per SDK contract: 'error' is terminal; emit a synthetic
|
|
557
567
|
// 'finish' so wrapEventStream's invariant holds.
|
|
558
568
|
this.initial.push({ type: 'finish', finishReason: 'error' });
|
|
@@ -676,6 +686,7 @@ export class MastraApprovalCoordinator {
|
|
|
676
686
|
return 'suspended';
|
|
677
687
|
}
|
|
678
688
|
if (!sawTerminal && !this.initial.isEnded()) {
|
|
689
|
+
this.pendingMcpToolCallStarts.clear();
|
|
679
690
|
this.initial.end();
|
|
680
691
|
}
|
|
681
692
|
return 'terminal';
|
|
@@ -753,14 +764,54 @@ export class MastraApprovalCoordinator {
|
|
|
753
764
|
if (enriched.type === 'tool-call' && isRealMcpServer(enriched.serverName)) {
|
|
754
765
|
this.currentMcpToolCallId = enriched.toolCallId;
|
|
755
766
|
this.toolNameForCallId.set(enriched.toolCallId, enriched.toolName);
|
|
767
|
+
if (this.hasWireCommunicationSubscribers()) {
|
|
768
|
+
this.pendingMcpToolCallStarts.set(enriched.toolCallId, this.clock.now());
|
|
769
|
+
}
|
|
756
770
|
}
|
|
757
771
|
else if (enriched.type === 'tool-result' &&
|
|
758
772
|
isRealMcpServer(enriched.serverName) &&
|
|
759
773
|
this.currentMcpToolCallId === enriched.toolCallId) {
|
|
760
774
|
this.currentMcpToolCallId = null;
|
|
761
775
|
}
|
|
776
|
+
if (enriched.type === 'tool-result' && isRealMcpServer(enriched.serverName)) {
|
|
777
|
+
this.emitMcpToolCallCompleted(enriched);
|
|
778
|
+
}
|
|
762
779
|
this.initial.push(enriched);
|
|
763
780
|
}
|
|
781
|
+
/**
|
|
782
|
+
* Emits a coordinator-observed call-to-result window for a real MCP tool.
|
|
783
|
+
* Mastra durationMs includes model round-trip time, so it is not comparable
|
|
784
|
+
* to the Claude bridge's host-to-MCP RPC latency.
|
|
785
|
+
*/
|
|
786
|
+
emitMcpToolCallCompleted(event) {
|
|
787
|
+
const startedAt = this.pendingMcpToolCallStarts.get(event.toolCallId);
|
|
788
|
+
if (startedAt === undefined)
|
|
789
|
+
return;
|
|
790
|
+
this.pendingMcpToolCallStarts.delete(event.toolCallId);
|
|
791
|
+
if (!this.hasWireCommunicationSubscribers())
|
|
792
|
+
return;
|
|
793
|
+
const completedAt = this.clock.now();
|
|
794
|
+
const serverName = event.serverName;
|
|
795
|
+
if (serverName === undefined)
|
|
796
|
+
return;
|
|
797
|
+
try {
|
|
798
|
+
this.emitWireCommunication({
|
|
799
|
+
type: 'mcp-tool-call-completed',
|
|
800
|
+
timestamp: completedAt,
|
|
801
|
+
serverName,
|
|
802
|
+
toolName: event.toolName,
|
|
803
|
+
bareToolName: event.bareToolName ?? event.toolName,
|
|
804
|
+
args: this.toolCallArgs.get(event.toolCallId) ?? {},
|
|
805
|
+
result: event.result,
|
|
806
|
+
durationMs: completedAt.getTime() - startedAt.getTime(),
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
catch (err) {
|
|
810
|
+
if (err instanceof AgentSDKError && err.type === AgentSDKErrorType.DISPOSED)
|
|
811
|
+
return;
|
|
812
|
+
throw err;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
764
815
|
maybeEnrich(event) {
|
|
765
816
|
if (event.type !== 'tool-call' && event.type !== 'tool-result' && event.type !== 'tool-approval-request') {
|
|
766
817
|
return event;
|
package/dist/mastra-harness.d.ts
CHANGED
|
@@ -266,6 +266,7 @@ export declare class MastraHarness implements MastraAgentHarness {
|
|
|
266
266
|
private readonly idGenerator;
|
|
267
267
|
private readonly toolApprovalTimeoutMs;
|
|
268
268
|
private readonly innerFetch;
|
|
269
|
+
private readonly wireObserverAbortController;
|
|
269
270
|
private readonly languageModelBuilders?;
|
|
270
271
|
constructor(storageRootFolder: string, opts?: MastraHarnessOptions);
|
|
271
272
|
/**
|
package/dist/mastra-harness.js
CHANGED
|
@@ -398,6 +398,7 @@ export class MastraHarness {
|
|
|
398
398
|
idGenerator;
|
|
399
399
|
toolApprovalTimeoutMs;
|
|
400
400
|
innerFetch;
|
|
401
|
+
wireObserverAbortController = new AbortController();
|
|
401
402
|
languageModelBuilders;
|
|
402
403
|
constructor(storageRootFolder, opts = {}) {
|
|
403
404
|
this.storageRootFolder = storageRootFolder;
|
|
@@ -436,6 +437,7 @@ export class MastraHarness {
|
|
|
436
437
|
* (forcedotcom/agentic-dx#429, mastra-ai/mastra#17285).
|
|
437
438
|
*/
|
|
438
439
|
async shutdown() {
|
|
440
|
+
this.wireObserverAbortController.abort();
|
|
439
441
|
await Promise.all([...this.activeAgents.values()].map(async (state) => {
|
|
440
442
|
for (const coordinator of state.coordinators.values()) {
|
|
441
443
|
coordinator.dispose();
|
|
@@ -849,21 +851,31 @@ export class MastraHarness {
|
|
|
849
851
|
serverState.errorDetail = errorDetail;
|
|
850
852
|
serverState.settled = true;
|
|
851
853
|
const emitStatusChange = this.prepareMcpStatusChange(agentId, serverName, serverState, McpServerStatus.Error, message);
|
|
852
|
-
this.buses.getLogBus()?.warn('MCP server reconnect failed', {
|
|
853
|
-
agentId,
|
|
854
|
-
serverName,
|
|
855
|
-
error: message,
|
|
856
|
-
});
|
|
857
854
|
try {
|
|
855
|
+
const failedAt = this.clock.now();
|
|
856
|
+
const failedError = effectiveError instanceof Error ? effectiveError : new Error(message);
|
|
858
857
|
this.buses.emitTelemetry({
|
|
859
858
|
type: 'mcp-server-discovery-failed',
|
|
860
|
-
timestamp:
|
|
859
|
+
timestamp: failedAt,
|
|
861
860
|
agentId,
|
|
862
861
|
serverName,
|
|
863
|
-
durationMs:
|
|
864
|
-
error:
|
|
862
|
+
durationMs: failedAt.getTime() - startTime,
|
|
863
|
+
error: failedError,
|
|
865
864
|
errorDetail,
|
|
866
865
|
});
|
|
866
|
+
this.buses.emitLog({
|
|
867
|
+
level: 'error',
|
|
868
|
+
message: 'MCP server discovery failed',
|
|
869
|
+
timestamp: failedAt,
|
|
870
|
+
context: {
|
|
871
|
+
event_type: 'mcp-server-discovery-failed',
|
|
872
|
+
agentId,
|
|
873
|
+
serverName,
|
|
874
|
+
durationMs: failedAt.getTime() - startTime,
|
|
875
|
+
errorDetail,
|
|
876
|
+
},
|
|
877
|
+
error: failedError,
|
|
878
|
+
});
|
|
867
879
|
}
|
|
868
880
|
catch (emitErr) {
|
|
869
881
|
if (!isDisposedError(emitErr))
|
|
@@ -1290,25 +1302,34 @@ export class MastraHarness {
|
|
|
1290
1302
|
}
|
|
1291
1303
|
};
|
|
1292
1304
|
const innerFetch = this.innerFetch;
|
|
1305
|
+
const wireHooks = {
|
|
1306
|
+
emitWireCommunication,
|
|
1307
|
+
getModel: () => getInfo().nativeModelId,
|
|
1308
|
+
getProviderHint: () => getInfo().providerHint,
|
|
1309
|
+
clock: this.clock,
|
|
1310
|
+
idGenerator: this.idGenerator,
|
|
1311
|
+
hasWireCommunicationSubscribers: () => this.buses.hasWireCommunicationSubscribers(),
|
|
1312
|
+
disposeSignal: this.wireObserverAbortController.signal,
|
|
1313
|
+
};
|
|
1293
1314
|
switch (initial.providerHint) {
|
|
1294
1315
|
case 'bedrock-anthropic':
|
|
1295
1316
|
// Salesforce LLM Gateway's `/invoke-with-response-stream` (Bedrock pass-through).
|
|
1296
|
-
return buildAnthropicLanguageModel({ getInfo, logBus,
|
|
1317
|
+
return buildAnthropicLanguageModel({ getInfo, logBus, wireHooks, innerFetch });
|
|
1297
1318
|
case 'anthropic':
|
|
1298
1319
|
// Direct Anthropic Messages API (`api.anthropic.com/v1/messages`) —
|
|
1299
1320
|
// different wire shape (`x-api-key` auth, body without `anthropic_version`).
|
|
1300
|
-
return buildAnthropicDirectLanguageModel({ getInfo, logBus,
|
|
1321
|
+
return buildAnthropicDirectLanguageModel({ getInfo, logBus, wireHooks, innerFetch });
|
|
1301
1322
|
case 'openai-responses':
|
|
1302
1323
|
case 'openai':
|
|
1303
1324
|
// OpenAI Responses API. Salesforce LLM Gateway's `/responses` pass-through
|
|
1304
1325
|
// for `'openai-responses'`; `api.openai.com/v1/responses` for `'openai'` direct.
|
|
1305
1326
|
// Same wire shape; the only difference is the `baseUrl` the resolver supplies.
|
|
1306
|
-
return buildOpenAILanguageModel({ getInfo, logBus,
|
|
1327
|
+
return buildOpenAILanguageModel({ getInfo, logBus, wireHooks, innerFetch });
|
|
1307
1328
|
case 'openai-compatible':
|
|
1308
1329
|
// OpenAI-compatible Chat Completions API (LLMG Express, LiteLLM, etc.).
|
|
1309
1330
|
// Different wire shape from Responses — `/v1/chat/completions` with
|
|
1310
1331
|
// `choices[0].delta` streaming events.
|
|
1311
|
-
return buildOpenAIChatCompletionsLanguageModel({ getInfo, logBus,
|
|
1332
|
+
return buildOpenAIChatCompletionsLanguageModel({ getInfo, logBus, wireHooks, innerFetch });
|
|
1312
1333
|
default: {
|
|
1313
1334
|
// Consult the injected builders map before throwing. This is the
|
|
1314
1335
|
// extension point for consumer-registered wire shapes (e.g. a temporary
|
|
@@ -1486,6 +1507,9 @@ export class MastraHarness {
|
|
|
1486
1507
|
lookupMcpToolMetadata: (name) => lookupMcpToolMetadata(agentState.mcpServers, name),
|
|
1487
1508
|
skillBridgeToolNames: activeSkillBridgeToolNames(agentState),
|
|
1488
1509
|
logBus: this.buses.getLogBus(),
|
|
1510
|
+
clock: this.clock,
|
|
1511
|
+
emitWireCommunication: (event) => this.buses.emitWireCommunication(event),
|
|
1512
|
+
hasWireCommunicationSubscribers: () => this.buses.hasWireCommunicationSubscribers(),
|
|
1489
1513
|
});
|
|
1490
1514
|
agentState.coordinators.set(threadId, coordinator);
|
|
1491
1515
|
const abortSignal = options?.abortSignal;
|
|
@@ -1754,15 +1778,31 @@ export class MastraHarness {
|
|
|
1754
1778
|
serverState.currentStatus = nextStatus;
|
|
1755
1779
|
return () => {
|
|
1756
1780
|
try {
|
|
1781
|
+
const changedAt = this.clock.now();
|
|
1757
1782
|
this.buses.emitTelemetry({
|
|
1758
1783
|
type: 'mcp-server-status-changed',
|
|
1759
|
-
timestamp:
|
|
1784
|
+
timestamp: changedAt,
|
|
1760
1785
|
agentId,
|
|
1761
1786
|
serverName,
|
|
1762
1787
|
previousStatus,
|
|
1763
1788
|
nextStatus,
|
|
1764
1789
|
...(error !== undefined ? { error } : {}),
|
|
1765
1790
|
});
|
|
1791
|
+
// Consider dropping this if it becomes too noisy.
|
|
1792
|
+
const isErrorStatus = nextStatus === McpServerStatus.Error;
|
|
1793
|
+
this.buses.emitLog({
|
|
1794
|
+
level: isErrorStatus ? 'error' : 'info',
|
|
1795
|
+
message: 'MCP server status changed',
|
|
1796
|
+
timestamp: changedAt,
|
|
1797
|
+
context: {
|
|
1798
|
+
event_type: 'mcp-server-status-changed',
|
|
1799
|
+
agentId,
|
|
1800
|
+
serverName,
|
|
1801
|
+
previousStatus,
|
|
1802
|
+
nextStatus,
|
|
1803
|
+
...(error !== undefined ? { error } : {}),
|
|
1804
|
+
},
|
|
1805
|
+
});
|
|
1766
1806
|
}
|
|
1767
1807
|
catch (emitErr) {
|
|
1768
1808
|
if (!isDisposedError(emitErr))
|
|
@@ -1891,13 +1931,29 @@ export class MastraHarness {
|
|
|
1891
1931
|
// after `shutdown()` has disposed the buses. Swallow only `DISPOSED`
|
|
1892
1932
|
// so a real listener exception still surfaces.
|
|
1893
1933
|
try {
|
|
1934
|
+
const completedAt = this.clock.now();
|
|
1935
|
+
const toolCount = Object.keys(namespacedTools).length;
|
|
1936
|
+
const durationMs = completedAt.getTime() - startTime;
|
|
1894
1937
|
this.buses.emitTelemetry({
|
|
1895
1938
|
type: 'mcp-server-discovery-completed',
|
|
1896
|
-
timestamp:
|
|
1939
|
+
timestamp: completedAt,
|
|
1897
1940
|
agentId,
|
|
1898
1941
|
serverName,
|
|
1899
|
-
toolCount
|
|
1900
|
-
durationMs
|
|
1942
|
+
toolCount,
|
|
1943
|
+
durationMs,
|
|
1944
|
+
});
|
|
1945
|
+
// Consider dropping this if it becomes too noisy.
|
|
1946
|
+
this.buses.emitLog({
|
|
1947
|
+
level: 'info',
|
|
1948
|
+
message: 'MCP server discovery completed',
|
|
1949
|
+
timestamp: completedAt,
|
|
1950
|
+
context: {
|
|
1951
|
+
event_type: 'mcp-server-discovery-completed',
|
|
1952
|
+
agentId,
|
|
1953
|
+
serverName,
|
|
1954
|
+
toolCount,
|
|
1955
|
+
durationMs,
|
|
1956
|
+
},
|
|
1901
1957
|
});
|
|
1902
1958
|
}
|
|
1903
1959
|
catch (emitErr) {
|
|
@@ -1937,21 +1993,32 @@ export class MastraHarness {
|
|
|
1937
1993
|
s.errorDetail = errorDetail;
|
|
1938
1994
|
s.settled = true;
|
|
1939
1995
|
const emitStatusChange = this.prepareMcpStatusChange(agentId, serverName, s, McpServerStatus.Error, errorMessage);
|
|
1940
|
-
this.buses.getLogBus()?.warn('MCP server discovery failed', {
|
|
1941
|
-
agentId,
|
|
1942
|
-
serverName,
|
|
1943
|
-
error: errorMessage,
|
|
1944
|
-
});
|
|
1945
1996
|
try {
|
|
1997
|
+
const failedAt = this.clock.now();
|
|
1998
|
+
const failedError = effectiveError instanceof Error ? effectiveError : new Error(String(effectiveError));
|
|
1999
|
+
const durationMs = failedAt.getTime() - startTime;
|
|
1946
2000
|
this.buses.emitTelemetry({
|
|
1947
2001
|
type: 'mcp-server-discovery-failed',
|
|
1948
|
-
timestamp:
|
|
2002
|
+
timestamp: failedAt,
|
|
1949
2003
|
agentId,
|
|
1950
2004
|
serverName,
|
|
1951
|
-
durationMs
|
|
1952
|
-
error:
|
|
2005
|
+
durationMs,
|
|
2006
|
+
error: failedError,
|
|
1953
2007
|
errorDetail,
|
|
1954
2008
|
});
|
|
2009
|
+
this.buses.emitLog({
|
|
2010
|
+
level: 'error',
|
|
2011
|
+
message: 'MCP server discovery failed',
|
|
2012
|
+
timestamp: failedAt,
|
|
2013
|
+
context: {
|
|
2014
|
+
event_type: 'mcp-server-discovery-failed',
|
|
2015
|
+
agentId,
|
|
2016
|
+
serverName,
|
|
2017
|
+
durationMs,
|
|
2018
|
+
errorDetail,
|
|
2019
|
+
},
|
|
2020
|
+
error: failedError,
|
|
2021
|
+
});
|
|
1955
2022
|
}
|
|
1956
2023
|
catch (emitErr) {
|
|
1957
2024
|
if (!isDisposedError(emitErr))
|
|
@@ -2,6 +2,7 @@ import Anthropic from '@anthropic-ai/sdk';
|
|
|
2
2
|
import type { LanguageModelV3, LanguageModelV3CallOptions, LanguageModelV3Prompt } from '@ai-sdk/provider';
|
|
3
3
|
import type { LogBus } from '@salesforce/agentic-common';
|
|
4
4
|
import type { ModelConnectivityInfo, WireCommunicationEvent } from '@salesforce/sfdx-agent-sdk';
|
|
5
|
+
import { type GatewayFetchWireHooks } from './v3-stream-mapping.js';
|
|
5
6
|
/**
|
|
6
7
|
* Builds the in-process `LanguageModelV3` impl for the Salesforce LLM Gateway's
|
|
7
8
|
* `/invoke-with-response-stream` (Bedrock-Anthropic pass-through) endpoint.
|
|
@@ -20,6 +21,7 @@ export declare function buildAnthropicLanguageModel(opts: {
|
|
|
20
21
|
getInfo: () => ModelConnectivityInfo;
|
|
21
22
|
logBus?: LogBus;
|
|
22
23
|
emitWireCommunication?: (event: WireCommunicationEvent) => void;
|
|
24
|
+
wireHooks?: GatewayFetchWireHooks;
|
|
23
25
|
/**
|
|
24
26
|
* Inner fetch routed through the harness's `Dispatcher` (built from
|
|
25
27
|
* `HTTPS_PROXY` / `HTTP_PROXY` or a consumer-supplied one). Defaults to
|
|
@@ -51,6 +53,7 @@ export declare function buildAnthropicDirectLanguageModel(opts: {
|
|
|
51
53
|
getInfo: () => ModelConnectivityInfo;
|
|
52
54
|
logBus?: LogBus;
|
|
53
55
|
emitWireCommunication?: (event: WireCommunicationEvent) => void;
|
|
56
|
+
wireHooks?: GatewayFetchWireHooks;
|
|
54
57
|
/** See {@link buildAnthropicLanguageModel} `innerFetch`. */
|
|
55
58
|
innerFetch?: typeof fetch;
|
|
56
59
|
}): LanguageModelV3;
|
|
@@ -20,19 +20,20 @@ import { TextSpanLifecycle, createGatewayFetch, isEmptyUsage, isParameterExclude
|
|
|
20
20
|
* the next request.
|
|
21
21
|
*/
|
|
22
22
|
export function buildAnthropicLanguageModel(opts) {
|
|
23
|
-
const { getInfo, logBus, emitWireCommunication, innerFetch } = opts;
|
|
23
|
+
const { getInfo, logBus, emitWireCommunication, wireHooks, innerFetch } = opts;
|
|
24
24
|
const initialInfo = getInfo();
|
|
25
25
|
const ctx = {
|
|
26
26
|
getInfo,
|
|
27
27
|
logBus,
|
|
28
28
|
emitWireCommunication,
|
|
29
|
+
wireHooks,
|
|
29
30
|
buildClient: (info) => new AnthropicBedrock({
|
|
30
31
|
baseURL: info.baseUrl,
|
|
31
32
|
skipAuth: true,
|
|
32
33
|
// The fetch wrapper re-evaluates `getHeaders()` on every outbound call —
|
|
33
34
|
// JWT rotation, premium-rate-limit gate flips, BYOK provider rotation
|
|
34
35
|
// all land on the next request without rebuilding the bridge.
|
|
35
|
-
fetch: createGatewayFetch(() => info.getHeaders(), buildWireHooks(ctx), innerFetch),
|
|
36
|
+
fetch: createGatewayFetch(() => info.getHeaders(), ctx.wireHooks ?? buildWireHooks(ctx), innerFetch),
|
|
36
37
|
}),
|
|
37
38
|
};
|
|
38
39
|
return {
|
|
@@ -68,12 +69,13 @@ export function buildAnthropicLanguageModel(opts) {
|
|
|
68
69
|
* language model.
|
|
69
70
|
*/
|
|
70
71
|
export function buildAnthropicDirectLanguageModel(opts) {
|
|
71
|
-
const { getInfo, logBus, emitWireCommunication, innerFetch } = opts;
|
|
72
|
+
const { getInfo, logBus, emitWireCommunication, wireHooks, innerFetch } = opts;
|
|
72
73
|
const initialInfo = getInfo();
|
|
73
74
|
const ctx = {
|
|
74
75
|
getInfo,
|
|
75
76
|
logBus,
|
|
76
77
|
emitWireCommunication,
|
|
78
|
+
wireHooks,
|
|
77
79
|
buildClient: (info) =>
|
|
78
80
|
// Anthropic SDK constructor takes `apiKey` and writes `x-api-key` itself.
|
|
79
81
|
// We use a non-empty placeholder here because the SDK requires `apiKey`
|
|
@@ -85,7 +87,7 @@ export function buildAnthropicDirectLanguageModel(opts) {
|
|
|
85
87
|
new Anthropic({
|
|
86
88
|
apiKey: 'anthropic-direct-resolver-managed',
|
|
87
89
|
baseURL: info.baseUrl,
|
|
88
|
-
fetch: createGatewayFetch(() => translateBearerToXApiKey(info), buildWireHooks(ctx), innerFetch),
|
|
90
|
+
fetch: createGatewayFetch(() => translateBearerToXApiKey(info), ctx.wireHooks ?? buildWireHooks(ctx), innerFetch),
|
|
89
91
|
}),
|
|
90
92
|
};
|
|
91
93
|
return {
|
|
@@ -2,6 +2,7 @@ import type { ChatCompletionMessageParam, ChatCompletionTool } from 'openai/reso
|
|
|
2
2
|
import type { LanguageModelV3, LanguageModelV3CallOptions, LanguageModelV3FinishReason, LanguageModelV3Prompt } from '@ai-sdk/provider';
|
|
3
3
|
import type { LogBus } from '@salesforce/agentic-common';
|
|
4
4
|
import type { ModelConnectivityInfo, WireCommunicationEvent } from '@salesforce/sfdx-agent-sdk';
|
|
5
|
+
import { type GatewayFetchWireHooks } from './v3-stream-mapping.js';
|
|
5
6
|
/**
|
|
6
7
|
* Builds the in-process `LanguageModelV3` impl that drives the `openai` SDK's
|
|
7
8
|
* `chat.completions.create({ stream: true })` against an OpenAI-compatible
|
|
@@ -26,6 +27,7 @@ export declare function buildOpenAIChatCompletionsLanguageModel(opts: {
|
|
|
26
27
|
getInfo: () => ModelConnectivityInfo;
|
|
27
28
|
logBus?: LogBus;
|
|
28
29
|
emitWireCommunication?: (event: WireCommunicationEvent) => void;
|
|
30
|
+
wireHooks?: GatewayFetchWireHooks;
|
|
29
31
|
/** See `buildAnthropicLanguageModel` `innerFetch`. */
|
|
30
32
|
innerFetch?: typeof fetch;
|
|
31
33
|
}): LanguageModelV3;
|
|
@@ -25,9 +25,9 @@ import { TextSpanLifecycle, createGatewayFetch, isEmptyUsage, isParameterExclude
|
|
|
25
25
|
* without rebuilding the language model.
|
|
26
26
|
*/
|
|
27
27
|
export function buildOpenAIChatCompletionsLanguageModel(opts) {
|
|
28
|
-
const { getInfo, logBus, emitWireCommunication, innerFetch } = opts;
|
|
28
|
+
const { getInfo, logBus, emitWireCommunication, wireHooks, innerFetch } = opts;
|
|
29
29
|
const initialInfo = getInfo();
|
|
30
|
-
const ctx = { getInfo, logBus, emitWireCommunication, innerFetch };
|
|
30
|
+
const ctx = { getInfo, logBus, emitWireCommunication, wireHooks, innerFetch };
|
|
31
31
|
return {
|
|
32
32
|
specificationVersion: 'v3',
|
|
33
33
|
provider: 'openai-compatible-chat-completions-pass-through',
|
|
@@ -45,9 +45,10 @@ export function buildOpenAIChatCompletionsLanguageModel(opts) {
|
|
|
45
45
|
*/
|
|
46
46
|
async function buildRequest(ctx, options) {
|
|
47
47
|
const info = ctx.getInfo();
|
|
48
|
-
const wireHooks = ctx.
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
const wireHooks = ctx.wireHooks ??
|
|
49
|
+
(ctx.emitWireCommunication
|
|
50
|
+
? { getModel: () => ctx.getInfo().nativeModelId, emitWireCommunication: ctx.emitWireCommunication }
|
|
51
|
+
: undefined);
|
|
51
52
|
const client = new OpenAI({
|
|
52
53
|
baseURL: info.baseUrl,
|
|
53
54
|
// The custom fetch injects the resolver-built `Authorization` header per
|
|
@@ -2,6 +2,7 @@ import type { ResponseInput, Tool } from 'openai/resources/responses/responses';
|
|
|
2
2
|
import type { LanguageModelV3, LanguageModelV3CallOptions, LanguageModelV3Prompt } from '@ai-sdk/provider';
|
|
3
3
|
import type { LogBus } from '@salesforce/agentic-common';
|
|
4
4
|
import type { ModelConnectivityInfo, WireCommunicationEvent } from '@salesforce/sfdx-agent-sdk';
|
|
5
|
+
import { type GatewayFetchWireHooks } from './v3-stream-mapping.js';
|
|
5
6
|
/**
|
|
6
7
|
* Builds the in-process `LanguageModelV3` impl that drives the `openai` SDK's
|
|
7
8
|
* `responses.create({ stream: true })` against the Salesforce LLM Gateway's
|
|
@@ -18,6 +19,7 @@ export declare function buildOpenAILanguageModel(opts: {
|
|
|
18
19
|
getInfo: () => ModelConnectivityInfo;
|
|
19
20
|
logBus?: LogBus;
|
|
20
21
|
emitWireCommunication?: (event: WireCommunicationEvent) => void;
|
|
22
|
+
wireHooks?: GatewayFetchWireHooks;
|
|
21
23
|
/** See `buildAnthropicLanguageModel` `innerFetch`. */
|
|
22
24
|
innerFetch?: typeof fetch;
|
|
23
25
|
}): LanguageModelV3;
|
|
@@ -17,9 +17,9 @@ import { TextSpanLifecycle, createGatewayFetch, isEmptyUsage, isParameterExclude
|
|
|
17
17
|
* next request.
|
|
18
18
|
*/
|
|
19
19
|
export function buildOpenAILanguageModel(opts) {
|
|
20
|
-
const { getInfo, logBus, emitWireCommunication, innerFetch } = opts;
|
|
20
|
+
const { getInfo, logBus, emitWireCommunication, wireHooks, innerFetch } = opts;
|
|
21
21
|
const initialInfo = getInfo();
|
|
22
|
-
const ctx = { getInfo, logBus, emitWireCommunication, innerFetch };
|
|
22
|
+
const ctx = { getInfo, logBus, emitWireCommunication, wireHooks, innerFetch };
|
|
23
23
|
return {
|
|
24
24
|
specificationVersion: 'v3',
|
|
25
25
|
provider: 'salesforce-llm-gateway-pass-through-openai',
|
|
@@ -41,9 +41,10 @@ export function buildOpenAILanguageModel(opts) {
|
|
|
41
41
|
*/
|
|
42
42
|
async function buildOpenAIRequest(ctx, options) {
|
|
43
43
|
const info = ctx.getInfo();
|
|
44
|
-
const wireHooks = ctx.
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
const wireHooks = ctx.wireHooks ??
|
|
45
|
+
(ctx.emitWireCommunication
|
|
46
|
+
? { getModel: () => ctx.getInfo().nativeModelId, emitWireCommunication: ctx.emitWireCommunication }
|
|
47
|
+
: undefined);
|
|
47
48
|
const client = new OpenAI({
|
|
48
49
|
baseURL: info.baseUrl,
|
|
49
50
|
// The custom fetch injects the resolver-built `Authorization` header per
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LanguageModelV3FinishReason, LanguageModelV3StreamPart, LanguageModelV3ToolResultOutput, LanguageModelV3Usage } from '@ai-sdk/provider';
|
|
2
|
-
import { Clock } from '@salesforce/agentic-common';
|
|
2
|
+
import { type Clock, type UniqueIDGenerator } from '@salesforce/agentic-common';
|
|
3
3
|
import type { Model } from '@salesforce/sfdx-agent-sdk';
|
|
4
4
|
/**
|
|
5
5
|
* Shared V3 helpers for the Mastra harness's `LanguageModelV3` implementations
|
|
@@ -196,6 +196,14 @@ export type GatewayFetchWireHooks = {
|
|
|
196
196
|
emitWireCommunication: (event: WireCommunicationLikeEvent) => void;
|
|
197
197
|
/** Optional clock for test determinism. Defaults to a fresh `RealClock`. */
|
|
198
198
|
clock?: Clock;
|
|
199
|
+
/** ID source for pairing one fetch's request and terminal response events. */
|
|
200
|
+
idGenerator?: UniqueIDGenerator;
|
|
201
|
+
/** Current provider wire shape; response text extraction is deliberately shape-specific. */
|
|
202
|
+
getProviderHint?: () => string;
|
|
203
|
+
/** Whether the harness has subscribers; avoids teeing streams nobody observes. */
|
|
204
|
+
hasWireCommunicationSubscribers?: () => boolean;
|
|
205
|
+
/** Aborted by harness shutdown so detached body observers cannot outlive their owner. */
|
|
206
|
+
disposeSignal?: AbortSignal;
|
|
199
207
|
};
|
|
200
208
|
/**
|
|
201
209
|
* Wraps an `innerFetch` (defaults to `globalThis.fetch`) with a per-request
|
|
@@ -212,8 +220,11 @@ export type GatewayFetchWireHooks = {
|
|
|
212
220
|
*
|
|
213
221
|
* When `wireHooks` is supplied, every outbound call emits one
|
|
214
222
|
* `WireCommunicationEvent` `request` event and one `response` event onto the
|
|
215
|
-
* supplied callback.
|
|
216
|
-
*
|
|
223
|
+
* supplied callback. For streaming responses the response event moves from
|
|
224
|
+
* fetch resolution to HTTP-body terminality: an owned tee branch observes only
|
|
225
|
+
* recognized assistant text deltas without blocking the provider SDK's branch.
|
|
226
|
+
* Wire-communication events are best-effort: a failed body decode does NOT
|
|
227
|
+
* prevent the call from completing. Body capture is
|
|
217
228
|
* intentionally bounded — only requests with a JSON body shorter than 256 KB
|
|
218
229
|
* are decoded; longer bodies are recorded as `{ truncated: true, byteLength:
|
|
219
230
|
* <n> }` so wire-communication captures don't blow the consumer's log file
|
|
@@ -231,15 +242,19 @@ type WireCommunicationLikeEvent = {
|
|
|
231
242
|
url: string;
|
|
232
243
|
method: string;
|
|
233
244
|
model: string;
|
|
245
|
+
correlationId?: string;
|
|
234
246
|
body?: Record<string, unknown>;
|
|
235
247
|
} | {
|
|
236
248
|
type: 'llm-response';
|
|
237
249
|
timestamp: Date;
|
|
238
250
|
model: string;
|
|
239
251
|
status: number;
|
|
252
|
+
correlationId?: string;
|
|
240
253
|
error?: Error;
|
|
241
254
|
totalDurationMs?: number;
|
|
242
255
|
xClientTraceId?: string;
|
|
256
|
+
responseText?: string;
|
|
257
|
+
timeToFirstTokenMs?: number;
|
|
243
258
|
};
|
|
244
259
|
/**
|
|
245
260
|
* Reduces a V3 `LanguageModelV3ToolResultOutput` to the string shape every
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Copyright 2026, Salesforce, Inc. All rights reserved.
|
|
3
3
|
* See LICENSE.txt for license terms.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { RealClock, UUIDGenerator } from '@salesforce/agentic-common';
|
|
6
6
|
/**
|
|
7
7
|
* Shared V3 helpers for the Mastra harness's `LanguageModelV3` implementations
|
|
8
8
|
* — stream-shaping primitives, finish-reason / usage mappers, prompt-translation
|
|
@@ -254,8 +254,11 @@ export function mapOpenAIUsage(usage) {
|
|
|
254
254
|
*
|
|
255
255
|
* When `wireHooks` is supplied, every outbound call emits one
|
|
256
256
|
* `WireCommunicationEvent` `request` event and one `response` event onto the
|
|
257
|
-
* supplied callback.
|
|
258
|
-
*
|
|
257
|
+
* supplied callback. For streaming responses the response event moves from
|
|
258
|
+
* fetch resolution to HTTP-body terminality: an owned tee branch observes only
|
|
259
|
+
* recognized assistant text deltas without blocking the provider SDK's branch.
|
|
260
|
+
* Wire-communication events are best-effort: a failed body decode does NOT
|
|
261
|
+
* prevent the call from completing. Body capture is
|
|
259
262
|
* intentionally bounded — only requests with a JSON body shorter than 256 KB
|
|
260
263
|
* are decoded; longer bodies are recorded as `{ truncated: true, byteLength:
|
|
261
264
|
* <n> }` so wire-communication captures don't blow the consumer's log file
|
|
@@ -283,6 +286,7 @@ export function createGatewayFetch(getHeaders, wireHooks, innerFetch = globalThi
|
|
|
283
286
|
const model = wireHooks.getModel();
|
|
284
287
|
const clock = wireHooks.clock ?? new RealClock();
|
|
285
288
|
const startedAt = clock.now();
|
|
289
|
+
const correlationId = (wireHooks.idGenerator ?? new UUIDGenerator()).getUniqueId();
|
|
286
290
|
const requestStart = performance.now();
|
|
287
291
|
wireHooks.emitWireCommunication({
|
|
288
292
|
type: 'llm-request',
|
|
@@ -290,19 +294,41 @@ export function createGatewayFetch(getHeaders, wireHooks, innerFetch = globalThi
|
|
|
290
294
|
url,
|
|
291
295
|
method,
|
|
292
296
|
model,
|
|
297
|
+
correlationId,
|
|
293
298
|
body: tryDecodeBody(finalInit.body),
|
|
294
299
|
});
|
|
295
300
|
try {
|
|
296
301
|
const response = await innerFetch(input, finalInit);
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
302
|
+
const emitResponse = (result = {}) => {
|
|
303
|
+
wireHooks.emitWireCommunication({
|
|
304
|
+
type: 'llm-response',
|
|
305
|
+
timestamp: clock.now(),
|
|
306
|
+
model,
|
|
307
|
+
status: response.status,
|
|
308
|
+
correlationId,
|
|
309
|
+
totalDurationMs: Math.round(performance.now() - requestStart),
|
|
310
|
+
xClientTraceId: response.headers.get('x-client-trace-id') ?? undefined,
|
|
311
|
+
...(result.responseText !== undefined ? { responseText: result.responseText } : {}),
|
|
312
|
+
...(result.timeToFirstTokenMs !== undefined
|
|
313
|
+
? { timeToFirstTokenMs: result.timeToFirstTokenMs }
|
|
314
|
+
: {}),
|
|
315
|
+
});
|
|
316
|
+
};
|
|
317
|
+
if (response.body === null || !(wireHooks.hasWireCommunicationSubscribers?.() ?? true)) {
|
|
318
|
+
emitResponse();
|
|
319
|
+
return response;
|
|
320
|
+
}
|
|
321
|
+
const [observerBody, sdkBody] = response.body.tee();
|
|
322
|
+
void observeResponseBody({
|
|
323
|
+
body: observerBody,
|
|
324
|
+
providerHint: wireHooks.getProviderHint?.(),
|
|
325
|
+
contentType: response.headers.get('content-type'),
|
|
326
|
+
requestSignal: finalInit.signal ?? undefined,
|
|
327
|
+
disposeSignal: wireHooks.disposeSignal,
|
|
328
|
+
clock,
|
|
329
|
+
onComplete: emitResponse,
|
|
330
|
+
}).catch(() => undefined);
|
|
331
|
+
return new Response(sdkBody, response);
|
|
306
332
|
}
|
|
307
333
|
catch (error) {
|
|
308
334
|
wireHooks.emitWireCommunication({
|
|
@@ -310,6 +336,7 @@ export function createGatewayFetch(getHeaders, wireHooks, innerFetch = globalThi
|
|
|
310
336
|
timestamp: clock.now(),
|
|
311
337
|
model,
|
|
312
338
|
status: 0,
|
|
339
|
+
correlationId,
|
|
313
340
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
314
341
|
totalDurationMs: Math.round(performance.now() - requestStart),
|
|
315
342
|
});
|
|
@@ -318,6 +345,141 @@ export function createGatewayFetch(getHeaders, wireHooks, innerFetch = globalThi
|
|
|
318
345
|
};
|
|
319
346
|
}
|
|
320
347
|
const TRACE_BODY_BYTE_BUDGET = 256 * 1024;
|
|
348
|
+
/**
|
|
349
|
+
* Observes only the owned tee branch. Cancellation intentionally does not await
|
|
350
|
+
* `reader.cancel()`: WHATWG tee cancellation may wait for the provider SDK's
|
|
351
|
+
* unread branch and must never let telemetry hang a fetch.
|
|
352
|
+
*/
|
|
353
|
+
async function observeResponseBody(options) {
|
|
354
|
+
const reader = options.body.getReader();
|
|
355
|
+
const decoder = new TextDecoder();
|
|
356
|
+
const startedAt = options.clock.now();
|
|
357
|
+
const text = [];
|
|
358
|
+
let textBytes = 0;
|
|
359
|
+
let rawBytes = 0;
|
|
360
|
+
let firstTokenAt;
|
|
361
|
+
let sawOutputText = false;
|
|
362
|
+
let sseBuffer = '';
|
|
363
|
+
let cancelled = false;
|
|
364
|
+
let resolveCancelled;
|
|
365
|
+
const cancellation = new Promise((resolve) => {
|
|
366
|
+
resolveCancelled = resolve;
|
|
367
|
+
});
|
|
368
|
+
const cancel = () => {
|
|
369
|
+
if (cancelled)
|
|
370
|
+
return;
|
|
371
|
+
cancelled = true;
|
|
372
|
+
void reader.cancel().catch(() => undefined);
|
|
373
|
+
try {
|
|
374
|
+
reader.releaseLock();
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
String(error);
|
|
378
|
+
}
|
|
379
|
+
resolveCancelled?.();
|
|
380
|
+
};
|
|
381
|
+
const abort = () => cancel();
|
|
382
|
+
options.requestSignal?.addEventListener('abort', abort, { once: true });
|
|
383
|
+
options.disposeSignal?.addEventListener('abort', abort, { once: true });
|
|
384
|
+
if (options.requestSignal?.aborted || options.disposeSignal?.aborted)
|
|
385
|
+
cancel();
|
|
386
|
+
try {
|
|
387
|
+
while (!cancelled) {
|
|
388
|
+
const read = reader.read();
|
|
389
|
+
void read.catch(() => undefined);
|
|
390
|
+
const next = await Promise.race([read, cancellation.then(() => undefined)]);
|
|
391
|
+
if (cancelled || next === undefined || next.done)
|
|
392
|
+
break;
|
|
393
|
+
if (firstTokenAt === undefined)
|
|
394
|
+
firstTokenAt = options.clock.now().getTime() - startedAt.getTime();
|
|
395
|
+
if (rawBytes + next.value.byteLength > TRACE_BODY_BYTE_BUDGET) {
|
|
396
|
+
cancel();
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
rawBytes += next.value.byteLength;
|
|
400
|
+
sseBuffer += decoder.decode(next.value, { stream: true });
|
|
401
|
+
const parsed = takeSseEvents(sseBuffer);
|
|
402
|
+
sseBuffer = parsed.remaining;
|
|
403
|
+
for (const event of parsed.events) {
|
|
404
|
+
const delta = outputTextDelta(options.providerHint, options.contentType, event);
|
|
405
|
+
if (delta === undefined)
|
|
406
|
+
continue;
|
|
407
|
+
const deltaBytes = Buffer.byteLength(delta, 'utf8');
|
|
408
|
+
if (textBytes + deltaBytes > TRACE_BODY_BYTE_BUDGET) {
|
|
409
|
+
cancel();
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
sawOutputText = true;
|
|
413
|
+
text.push(delta);
|
|
414
|
+
textBytes += deltaBytes;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
sawOutputText = false;
|
|
420
|
+
}
|
|
421
|
+
finally {
|
|
422
|
+
options.requestSignal?.removeEventListener('abort', abort);
|
|
423
|
+
options.disposeSignal?.removeEventListener('abort', abort);
|
|
424
|
+
if (!cancelled) {
|
|
425
|
+
try {
|
|
426
|
+
reader.releaseLock();
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
String(error);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
options.onComplete({
|
|
433
|
+
...(sawOutputText ? { responseText: text.join('') } : {}),
|
|
434
|
+
...(firstTokenAt !== undefined ? { timeToFirstTokenMs: firstTokenAt } : {}),
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
function takeSseEvents(buffer) {
|
|
439
|
+
const complete = buffer.lastIndexOf('\n');
|
|
440
|
+
if (complete === -1)
|
|
441
|
+
return { events: [], remaining: buffer };
|
|
442
|
+
const events = [];
|
|
443
|
+
for (const line of buffer.slice(0, complete).split(/\r?\n/)) {
|
|
444
|
+
if (!line.startsWith('data:'))
|
|
445
|
+
continue;
|
|
446
|
+
const data = line.slice('data:'.length).trim();
|
|
447
|
+
if (data.length === 0 || data === '[DONE]')
|
|
448
|
+
continue;
|
|
449
|
+
try {
|
|
450
|
+
events.push(JSON.parse(data));
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
// Unknown framing is deliberately not captured as text.
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return { events, remaining: buffer.slice(complete + 1) };
|
|
457
|
+
}
|
|
458
|
+
/** Extract only the documented assistant output-text delta for each supported wire shape. */
|
|
459
|
+
function outputTextDelta(providerHint, contentType, event) {
|
|
460
|
+
if (!contentType?.toLowerCase().includes('text/event-stream') || typeof event !== 'object' || event === null) {
|
|
461
|
+
return undefined;
|
|
462
|
+
}
|
|
463
|
+
const candidate = event;
|
|
464
|
+
if (providerHint === 'anthropic' || providerHint === 'bedrock-anthropic') {
|
|
465
|
+
const delta = candidate.delta;
|
|
466
|
+
return candidate.type === 'content_block_delta' &&
|
|
467
|
+
delta?.type === 'text_delta' &&
|
|
468
|
+
typeof delta.text === 'string'
|
|
469
|
+
? delta.text
|
|
470
|
+
: undefined;
|
|
471
|
+
}
|
|
472
|
+
if (providerHint === 'openai' || providerHint === 'openai-responses') {
|
|
473
|
+
return candidate.type === 'response.output_text.delta' && typeof candidate.delta === 'string'
|
|
474
|
+
? candidate.delta
|
|
475
|
+
: undefined;
|
|
476
|
+
}
|
|
477
|
+
if (providerHint === 'openai-compatible') {
|
|
478
|
+
const content = candidate.choices?.[0]?.delta?.content;
|
|
479
|
+
return typeof content === 'string' ? content : undefined;
|
|
480
|
+
}
|
|
481
|
+
return undefined;
|
|
482
|
+
}
|
|
321
483
|
function tryDecodeBody(body) {
|
|
322
484
|
if (body === undefined || body === null)
|
|
323
485
|
return undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/sfdx-agent-harness-mastra",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"description": "Mastra-backed AgentHarness implementation for @salesforce/sfdx-agent-sdk",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
"openai": "^6.0.0"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"@salesforce/sfdx-agent-sdk": "0.
|
|
44
|
+
"@salesforce/sfdx-agent-sdk": "0.41.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@eslint/js": "^10.0.1",
|
|
48
48
|
"@salesforce/harness-conformance": "0.0.1",
|
|
49
|
-
"@salesforce/sfdx-agent-sdk": "0.
|
|
49
|
+
"@salesforce/sfdx-agent-sdk": "0.41.0",
|
|
50
50
|
"@types/node": "^22.20.0",
|
|
51
51
|
"@vitest/coverage-istanbul": "^4.1.10",
|
|
52
52
|
"@vitest/eslint-plugin": "^1.6.22",
|