@salesforce/sfdx-agent-harness-mastra 0.17.0 → 0.19.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 +14 -0
- package/dist/mastra-approval-coordinator.d.ts +54 -5
- package/dist/mastra-approval-coordinator.js +123 -63
- package/dist/mastra-event-adapter.d.ts +3 -2
- package/dist/mastra-event-adapter.js +23 -6
- package/dist/mastra-harness.d.ts +16 -0
- package/dist/mastra-harness.js +51 -0
- package/dist/mcp-config-mapper.js +9 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
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.19.0] - 2026-06-12
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
- **agent-sdk,harness-mastra,harness-claude**: add tool-call-delta ChatEvent for streaming tool-call args @W-22965697@ ([#597](https://github.com/forcedotcom/agentic-dx/pull/597))
|
|
10
|
+
- **agent-sdk,harness-mastra,harness-claude**: add tool-progress ChatEvent variant @W-22951127@ ([#594](https://github.com/forcedotcom/agentic-dx/pull/594))
|
|
11
|
+
|
|
12
|
+
### Fixes
|
|
13
|
+
- **harness-claude**: map SDKResultSuccess.is_error to ChatEvent.error ([#593](https://github.com/forcedotcom/agentic-dx/pull/593))
|
|
14
|
+
|
|
15
|
+
## [0.18.0] - 2026-06-11
|
|
16
|
+
|
|
17
|
+
### Fixes
|
|
18
|
+
- **harness-mastra**: drive one resume per pending approval for parallel batches ([#587](https://github.com/forcedotcom/agentic-dx/pull/587))
|
|
19
|
+
|
|
6
20
|
## [0.17.0] - 2026-06-09
|
|
7
21
|
|
|
8
22
|
### Fixes
|
|
@@ -143,12 +143,34 @@ export declare class MastraApprovalCoordinator {
|
|
|
143
143
|
*/
|
|
144
144
|
private currentRunId;
|
|
145
145
|
/**
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
146
|
+
* `toolCallId` of the MCP tool call currently in-flight on this turn,
|
|
147
|
+
* or `null` when no MCP tool is awaiting a result. Tracked at
|
|
148
|
+
* `routeEnriched` time: set when a MCP-namespaced `tool-call` arrives,
|
|
149
|
+
* cleared on the matching `tool-result`.
|
|
150
|
+
*
|
|
151
|
+
* Used by {@link emitMcpProgress} to attribute incoming
|
|
152
|
+
* `notifications/progress` (which only carry a `progressToken`) to the
|
|
153
|
+
* right `toolCallId` for the `tool-progress` ChatEvent.
|
|
154
|
+
*
|
|
155
|
+
* Mastra serializes MCP tool execution per workflow step, so at most
|
|
156
|
+
* one MCP tool call is in flight per coordinator at any time —
|
|
157
|
+
* matching the "currentMcpToolCallId" field's scalar shape.
|
|
158
|
+
*/
|
|
159
|
+
private currentMcpToolCallId;
|
|
160
|
+
/**
|
|
161
|
+
* Captures the (Mastra-namespaced) tool name for each `toolCallId`
|
|
162
|
+
* routed through `routeEnriched`. Used by {@link emitMcpProgress} to
|
|
163
|
+
* stamp `toolName` on the `tool-progress` ChatEvent so AG-UI Activity
|
|
164
|
+
* translators don't have to consult the originating `tool-call` event
|
|
165
|
+
* to route progress chunks.
|
|
166
|
+
*/
|
|
167
|
+
private readonly toolNameForCallId;
|
|
168
|
+
/**
|
|
169
|
+
* ToolCallIds Mastra is currently suspended on. Mastra retains a separate
|
|
170
|
+
* workflow snapshot step per id, so the pump must drive one resume per
|
|
171
|
+
* pending id with `{ toolCallId }` to disambiguate (#586).
|
|
150
172
|
*/
|
|
151
|
-
private
|
|
173
|
+
private readonly pendingSuspendIds;
|
|
152
174
|
/** Consumer-tool: bound parkers, keyed by `toolCallId`. `submitToolResult` resolves these. */
|
|
153
175
|
private readonly pendingByToolCallId;
|
|
154
176
|
/**
|
|
@@ -176,6 +198,33 @@ export declare class MastraApprovalCoordinator {
|
|
|
176
198
|
private timedOut;
|
|
177
199
|
constructor(opts: MastraApprovalCoordinatorOptions);
|
|
178
200
|
initialSink(): GenSink<ChatEvent>;
|
|
201
|
+
/**
|
|
202
|
+
* Mastra's current run id. The harness's MCP-progress dispatcher uses
|
|
203
|
+
* this to filter incoming `notifications/progress` to the coordinator
|
|
204
|
+
* whose run produced the originating tool call (the upstream server
|
|
205
|
+
* received the progressToken Mastra injected from `context.runId`).
|
|
206
|
+
*/
|
|
207
|
+
getCurrentRunId(): string;
|
|
208
|
+
/**
|
|
209
|
+
* Issue #591: dispatch an MCP `notifications/progress` notification
|
|
210
|
+
* received via `mcpClient.progress.onUpdate(...)` to the consumer's
|
|
211
|
+
* eventStream as a `tool-progress` ChatEvent.
|
|
212
|
+
*
|
|
213
|
+
* Caller (the harness MCP-progress listener) is responsible for
|
|
214
|
+
* filtering by `progressToken === coordinator.getCurrentRunId()` so this
|
|
215
|
+
* method only sees notifications belonging to its own turn.
|
|
216
|
+
*
|
|
217
|
+
* Drops the notification (no event surfaced) when no MCP tool call is
|
|
218
|
+
* currently in flight — the only path that produces `currentMcpToolCallId
|
|
219
|
+
* === null` after a turn started is "progress arrived after the matching
|
|
220
|
+
* tool-result." Losing a stragglier is preferable to surfacing a
|
|
221
|
+
* progress event without a meaningful toolCallId.
|
|
222
|
+
*/
|
|
223
|
+
emitMcpProgress(params: {
|
|
224
|
+
progress: number;
|
|
225
|
+
total?: number;
|
|
226
|
+
message?: string;
|
|
227
|
+
}): void;
|
|
179
228
|
onTeardown(callback: () => void): void;
|
|
180
229
|
private runTeardownCallbacks;
|
|
181
230
|
/**
|
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
import { AgentSDKError, AgentSDKErrorType, } from '@salesforce/sfdx-agent-sdk';
|
|
6
6
|
import { GenSink } from '@salesforce/sfdx-agent-sdk/harness';
|
|
7
7
|
import { MastraEventAdapter } from './mastra-event-adapter.js';
|
|
8
|
+
function pickIdWithDecision(pendingIds, decisions) {
|
|
9
|
+
for (const id of pendingIds) {
|
|
10
|
+
if (decisions.has(id))
|
|
11
|
+
return id;
|
|
12
|
+
}
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
8
15
|
/**
|
|
9
16
|
* Per-`(agentId, threadId)` coordinator that subsumes the pre-#529
|
|
10
17
|
* `MastraStreamBroker` + `activeRuns` pair into one object owning a single
|
|
@@ -72,12 +79,34 @@ export class MastraApprovalCoordinator {
|
|
|
72
79
|
*/
|
|
73
80
|
currentRunId = '';
|
|
74
81
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
82
|
+
* `toolCallId` of the MCP tool call currently in-flight on this turn,
|
|
83
|
+
* or `null` when no MCP tool is awaiting a result. Tracked at
|
|
84
|
+
* `routeEnriched` time: set when a MCP-namespaced `tool-call` arrives,
|
|
85
|
+
* cleared on the matching `tool-result`.
|
|
86
|
+
*
|
|
87
|
+
* Used by {@link emitMcpProgress} to attribute incoming
|
|
88
|
+
* `notifications/progress` (which only carry a `progressToken`) to the
|
|
89
|
+
* right `toolCallId` for the `tool-progress` ChatEvent.
|
|
90
|
+
*
|
|
91
|
+
* Mastra serializes MCP tool execution per workflow step, so at most
|
|
92
|
+
* one MCP tool call is in flight per coordinator at any time —
|
|
93
|
+
* matching the "currentMcpToolCallId" field's scalar shape.
|
|
94
|
+
*/
|
|
95
|
+
currentMcpToolCallId = null;
|
|
96
|
+
/**
|
|
97
|
+
* Captures the (Mastra-namespaced) tool name for each `toolCallId`
|
|
98
|
+
* routed through `routeEnriched`. Used by {@link emitMcpProgress} to
|
|
99
|
+
* stamp `toolName` on the `tool-progress` ChatEvent so AG-UI Activity
|
|
100
|
+
* translators don't have to consult the originating `tool-call` event
|
|
101
|
+
* to route progress chunks.
|
|
102
|
+
*/
|
|
103
|
+
toolNameForCallId = new Map();
|
|
104
|
+
/**
|
|
105
|
+
* ToolCallIds Mastra is currently suspended on. Mastra retains a separate
|
|
106
|
+
* workflow snapshot step per id, so the pump must drive one resume per
|
|
107
|
+
* pending id with `{ toolCallId }` to disambiguate (#586).
|
|
79
108
|
*/
|
|
80
|
-
|
|
109
|
+
pendingSuspendIds = new Set();
|
|
81
110
|
/** Consumer-tool: bound parkers, keyed by `toolCallId`. `submitToolResult` resolves these. */
|
|
82
111
|
pendingByToolCallId = new Map();
|
|
83
112
|
/**
|
|
@@ -117,6 +146,43 @@ export class MastraApprovalCoordinator {
|
|
|
117
146
|
initialSink() {
|
|
118
147
|
return this.initial;
|
|
119
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Mastra's current run id. The harness's MCP-progress dispatcher uses
|
|
151
|
+
* this to filter incoming `notifications/progress` to the coordinator
|
|
152
|
+
* whose run produced the originating tool call (the upstream server
|
|
153
|
+
* received the progressToken Mastra injected from `context.runId`).
|
|
154
|
+
*/
|
|
155
|
+
getCurrentRunId() {
|
|
156
|
+
return this.currentRunId;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Issue #591: dispatch an MCP `notifications/progress` notification
|
|
160
|
+
* received via `mcpClient.progress.onUpdate(...)` to the consumer's
|
|
161
|
+
* eventStream as a `tool-progress` ChatEvent.
|
|
162
|
+
*
|
|
163
|
+
* Caller (the harness MCP-progress listener) is responsible for
|
|
164
|
+
* filtering by `progressToken === coordinator.getCurrentRunId()` so this
|
|
165
|
+
* method only sees notifications belonging to its own turn.
|
|
166
|
+
*
|
|
167
|
+
* Drops the notification (no event surfaced) when no MCP tool call is
|
|
168
|
+
* currently in flight — the only path that produces `currentMcpToolCallId
|
|
169
|
+
* === null` after a turn started is "progress arrived after the matching
|
|
170
|
+
* tool-result." Losing a stragglier is preferable to surfacing a
|
|
171
|
+
* progress event without a meaningful toolCallId.
|
|
172
|
+
*/
|
|
173
|
+
emitMcpProgress(params) {
|
|
174
|
+
if (this.aborted || this.disposed)
|
|
175
|
+
return;
|
|
176
|
+
const toolCallId = this.currentMcpToolCallId;
|
|
177
|
+
if (!toolCallId)
|
|
178
|
+
return;
|
|
179
|
+
this.initial.push({
|
|
180
|
+
type: 'tool-progress',
|
|
181
|
+
toolCallId,
|
|
182
|
+
toolName: this.toolNameForCallId.get(toolCallId) ?? 'unknown',
|
|
183
|
+
output: { ...params },
|
|
184
|
+
});
|
|
185
|
+
}
|
|
120
186
|
onTeardown(callback) {
|
|
121
187
|
if (this.aborted || this.disposed) {
|
|
122
188
|
queueMicrotask(callback);
|
|
@@ -210,7 +276,7 @@ export class MastraApprovalCoordinator {
|
|
|
210
276
|
this.initial.push({ type: 'finish', finishReason: 'error' });
|
|
211
277
|
this.initial.end();
|
|
212
278
|
}
|
|
213
|
-
this.
|
|
279
|
+
this.pendingSuspendIds.clear();
|
|
214
280
|
this.runTeardownCallbacks();
|
|
215
281
|
}
|
|
216
282
|
// ───────────────────────── settle methods ───────────────────────────────
|
|
@@ -233,14 +299,9 @@ export class MastraApprovalCoordinator {
|
|
|
233
299
|
this.consumerSettled.add(toolCallId);
|
|
234
300
|
this.cancelTimer(toolCallId);
|
|
235
301
|
this.decisions.set(toolCallId, decision);
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
// when it sees the matching suspension and drives the resume. If
|
|
240
|
-
// the pump is currently parked on `awaitContinuation` (saw the
|
|
241
|
-
// suspension first, decision arrived second), wake it so it
|
|
242
|
-
// re-checks `decisions`.
|
|
243
|
-
if (this.continuationResolver && this.currentSuspendId === toolCallId) {
|
|
302
|
+
// Wake the pump if it's parked on `awaitContinuation` waiting for
|
|
303
|
+
// a decision on a pending id.
|
|
304
|
+
if (this.continuationResolver && this.pendingSuspendIds.has(toolCallId)) {
|
|
244
305
|
const resolve = this.continuationResolver;
|
|
245
306
|
this.continuationResolver = undefined;
|
|
246
307
|
resolve('continued');
|
|
@@ -330,33 +391,29 @@ export class MastraApprovalCoordinator {
|
|
|
330
391
|
}
|
|
331
392
|
if (this.aborted || this.disposed || this.timedOut)
|
|
332
393
|
return;
|
|
333
|
-
// outcome === 'suspended':
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
const suspendId = this.currentSuspendId;
|
|
339
|
-
if (suspendId === null) {
|
|
340
|
-
// Defensive: stream ended without a suspension. Treat
|
|
341
|
-
// as terminal.
|
|
394
|
+
// outcome === 'suspended': drive one resume per outer
|
|
395
|
+
// iteration. For parallel batches, all but the last resume
|
|
396
|
+
// returns a stream that closes silently while still
|
|
397
|
+
// suspended — the pump re-enters here to drive the next id.
|
|
398
|
+
if (this.pendingSuspendIds.size === 0) {
|
|
342
399
|
if (!this.initial.isEnded())
|
|
343
400
|
this.initial.end();
|
|
344
401
|
return;
|
|
345
402
|
}
|
|
346
|
-
let
|
|
347
|
-
if (
|
|
348
|
-
// Park for a decision (or teardown).
|
|
403
|
+
let nextId = pickIdWithDecision(this.pendingSuspendIds, this.decisions);
|
|
404
|
+
if (nextId === undefined) {
|
|
349
405
|
const cont = await this.awaitContinuation();
|
|
350
406
|
if (cont === 'done')
|
|
351
407
|
return;
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
buffered = this.decisions.get(suspendId);
|
|
355
|
-
if (buffered === undefined)
|
|
408
|
+
nextId = pickIdWithDecision(this.pendingSuspendIds, this.decisions);
|
|
409
|
+
if (nextId === undefined)
|
|
356
410
|
return;
|
|
357
411
|
}
|
|
358
|
-
|
|
359
|
-
|
|
412
|
+
const decision = this.decisions.get(nextId);
|
|
413
|
+
if (decision === undefined)
|
|
414
|
+
return;
|
|
415
|
+
this.pendingSuspendIds.delete(nextId);
|
|
416
|
+
const next = await this.driveResumeReturning(nextId, decision);
|
|
360
417
|
if (next === 'done')
|
|
361
418
|
return;
|
|
362
419
|
currentStream = next;
|
|
@@ -434,22 +491,21 @@ export class MastraApprovalCoordinator {
|
|
|
434
491
|
continue;
|
|
435
492
|
}
|
|
436
493
|
if (event.type === 'tool-approval-request') {
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
//
|
|
440
|
-
// auto-approve silently.
|
|
494
|
+
// Parallel batches emit one chunk per suspension before
|
|
495
|
+
// the stream closes; accumulate every id and let the
|
|
496
|
+
// read loop continue to the natural close.
|
|
441
497
|
const tc = event.toolCall;
|
|
442
498
|
this.toolCallArgs.set(tc.toolCallId, tc.args);
|
|
443
|
-
this.
|
|
499
|
+
this.pendingSuspendIds.add(tc.toolCallId);
|
|
444
500
|
sawSuspension = true;
|
|
445
501
|
if (this.consumerToolNames.has(tc.toolName)) {
|
|
446
502
|
// Consumer tools bypass the gate per the
|
|
447
503
|
// StreamOptions.requireToolApproval JSDoc. Auto-
|
|
448
504
|
// approve silently — no ChatEvent surfaces, and the
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
//
|
|
452
|
-
//
|
|
505
|
+
// pump loop drives `agent.approveToolCall` when it
|
|
506
|
+
// picks up the buffered decision. After Mastra
|
|
507
|
+
// resumes, the bridge `execute` parker runs and
|
|
508
|
+
// `submitToolResult` settles it — same shape as
|
|
453
509
|
// non-approval-mode consumer-tool flow.
|
|
454
510
|
this.settledByCoordinator.add(tc.toolCallId);
|
|
455
511
|
this.decisions.set(tc.toolCallId, { kind: 'approve' });
|
|
@@ -459,14 +515,7 @@ export class MastraApprovalCoordinator {
|
|
|
459
515
|
// pre-emit in tool-call routing).
|
|
460
516
|
this.emitApprovalRequest(tc.toolCallId, tc.toolName, tc.args);
|
|
461
517
|
}
|
|
462
|
-
|
|
463
|
-
// snapshot persists before we attempt a resume.
|
|
464
|
-
while (true) {
|
|
465
|
-
const { done: d } = await reader.read();
|
|
466
|
-
if (d)
|
|
467
|
-
break;
|
|
468
|
-
}
|
|
469
|
-
return 'suspended';
|
|
518
|
+
continue;
|
|
470
519
|
}
|
|
471
520
|
this.routeEnriched(event);
|
|
472
521
|
}
|
|
@@ -490,17 +539,13 @@ export class MastraApprovalCoordinator {
|
|
|
490
539
|
/* best-effort: lock may already be released after a thrown read */
|
|
491
540
|
}
|
|
492
541
|
}
|
|
493
|
-
// Stream closed
|
|
494
|
-
//
|
|
495
|
-
//
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
// have arrived in the loop above. So (b) shouldn't reach here.
|
|
501
|
-
// Treat as terminal: sink is either ended already (teardown) or
|
|
502
|
-
// we must end it now.
|
|
503
|
-
if (!sawTerminal && !sawSuspension && !this.initial.isEnded()) {
|
|
542
|
+
// Stream closed without `finish`/`error`. If any suspension is
|
|
543
|
+
// pending (just observed, or carried over from a prior non-final
|
|
544
|
+
// resume that closed silently), defer to the pump loop's drive.
|
|
545
|
+
if (sawSuspension || this.pendingSuspendIds.size > 0) {
|
|
546
|
+
return 'suspended';
|
|
547
|
+
}
|
|
548
|
+
if (!sawTerminal && !this.initial.isEnded()) {
|
|
504
549
|
this.initial.end();
|
|
505
550
|
}
|
|
506
551
|
return 'terminal';
|
|
@@ -540,10 +585,10 @@ export class MastraApprovalCoordinator {
|
|
|
540
585
|
return output.fullStream;
|
|
541
586
|
}
|
|
542
587
|
catch (err) {
|
|
588
|
+
// Route through teardown so siblings in `pendingSuspendIds`
|
|
589
|
+
// settle as auto-resolved rather than silently orphaning.
|
|
543
590
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
544
|
-
this.
|
|
545
|
-
this.initial.push({ type: 'finish', finishReason: 'error' });
|
|
546
|
-
this.initial.end();
|
|
591
|
+
this.teardown('pump-error', error, 'pump-error');
|
|
547
592
|
return 'done';
|
|
548
593
|
}
|
|
549
594
|
}
|
|
@@ -563,6 +608,21 @@ export class MastraApprovalCoordinator {
|
|
|
563
608
|
if (this.aborted || this.disposed)
|
|
564
609
|
return;
|
|
565
610
|
const enriched = this.maybeEnrich(event);
|
|
611
|
+
// Issue #591: track in-flight MCP tool call so progress notifications
|
|
612
|
+
// arriving via `progress.onUpdate(...)` can be attributed to the
|
|
613
|
+
// right `toolCallId`. Set on enriched (i.e. MCP-namespaced) tool-call;
|
|
614
|
+
// cleared on the matching tool-result. Non-MCP tools don't carry
|
|
615
|
+
// `serverName` (the lookup returns undefined) so this never fires
|
|
616
|
+
// for built-ins, consumer-declared tools, or workspace tools.
|
|
617
|
+
if (enriched.type === 'tool-call' && enriched.serverName !== undefined) {
|
|
618
|
+
this.currentMcpToolCallId = enriched.toolCallId;
|
|
619
|
+
this.toolNameForCallId.set(enriched.toolCallId, enriched.toolName);
|
|
620
|
+
}
|
|
621
|
+
else if (enriched.type === 'tool-result' &&
|
|
622
|
+
enriched.serverName !== undefined &&
|
|
623
|
+
this.currentMcpToolCallId === enriched.toolCallId) {
|
|
624
|
+
this.currentMcpToolCallId = null;
|
|
625
|
+
}
|
|
566
626
|
this.initial.push(enriched);
|
|
567
627
|
}
|
|
568
628
|
maybeEnrich(event) {
|
|
@@ -33,6 +33,8 @@ import type { ChatEvent } from '@salesforce/sfdx-agent-sdk';
|
|
|
33
33
|
* | `tool-call-approval` | `tool-approval-request` | `{ toolCall: { toolCallId, toolName, args } }` — stream suspends |
|
|
34
34
|
* | `tool-result` | `tool-result` | `{ toolCallId, toolName, result, isError: false }` |
|
|
35
35
|
* | `tool-error` | `tool-result` | `{ toolCallId, toolName, result: error, isError: true}`|
|
|
36
|
+
* | `tool-output` | `tool-progress` | `{ toolCallId, toolName, output }` |
|
|
37
|
+
* | `tool-call-delta` | `tool-call-delta` | `{ toolCallId, toolName?, argsTextDelta }` |
|
|
36
38
|
* | `step-start` | `step-start` | `{ stepIndex }` from step metadata |
|
|
37
39
|
* | `step-finish` | `step-finish` | `{ stepIndex, finishReason, usage }` from `stepResult` |
|
|
38
40
|
* | `finish` | `finish` | `{ finishReason, usage }` from `output` |
|
|
@@ -44,9 +46,8 @@ import type { ChatEvent } from '@salesforce/sfdx-agent-sdk';
|
|
|
44
46
|
* | `reasoning-start`, | | |
|
|
45
47
|
* | `reasoning-end` | | |
|
|
46
48
|
* | `source`, `file`, | *(skipped)* | Known but unsupported — future ChatEvent candidates |
|
|
47
|
-
* | `tool-call-delta`, | | |
|
|
48
49
|
* | `tool-call-suspended`, | | |
|
|
49
|
-
* | `object
|
|
50
|
+
* | `object` | | |
|
|
50
51
|
* | *(all others)* | *(skipped)* | Logged via `LogBus.debug` with `{ chunkType, rawChunk }` |
|
|
51
52
|
*
|
|
52
53
|
* Stateful per stream — a new instance must be created for each `MastraHarness.stream()` call.
|
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
* | `tool-call-approval` | `tool-approval-request` | `{ toolCall: { toolCallId, toolName, args } }` — stream suspends |
|
|
35
35
|
* | `tool-result` | `tool-result` | `{ toolCallId, toolName, result, isError: false }` |
|
|
36
36
|
* | `tool-error` | `tool-result` | `{ toolCallId, toolName, result: error, isError: true}`|
|
|
37
|
+
* | `tool-output` | `tool-progress` | `{ toolCallId, toolName, output }` |
|
|
38
|
+
* | `tool-call-delta` | `tool-call-delta` | `{ toolCallId, toolName?, argsTextDelta }` |
|
|
37
39
|
* | `step-start` | `step-start` | `{ stepIndex }` from step metadata |
|
|
38
40
|
* | `step-finish` | `step-finish` | `{ stepIndex, finishReason, usage }` from `stepResult` |
|
|
39
41
|
* | `finish` | `finish` | `{ finishReason, usage }` from `output` |
|
|
@@ -45,9 +47,8 @@
|
|
|
45
47
|
* | `reasoning-start`, | | |
|
|
46
48
|
* | `reasoning-end` | | |
|
|
47
49
|
* | `source`, `file`, | *(skipped)* | Known but unsupported — future ChatEvent candidates |
|
|
48
|
-
* | `tool-call-delta`, | | |
|
|
49
50
|
* | `tool-call-suspended`, | | |
|
|
50
|
-
* | `object
|
|
51
|
+
* | `object` | | |
|
|
51
52
|
* | *(all others)* | *(skipped)* | Logged via `LogBus.debug` with `{ chunkType, rawChunk }` |
|
|
52
53
|
*
|
|
53
54
|
* Stateful per stream — a new instance must be created for each `MastraHarness.stream()` call.
|
|
@@ -169,19 +170,35 @@ export class MastraEventAdapter {
|
|
|
169
170
|
case 'reasoning-end':
|
|
170
171
|
this.logBus?.debug('Skipping informational Mastra chunk', { chunkType: chunk.type });
|
|
171
172
|
return undefined;
|
|
173
|
+
case 'tool-output':
|
|
174
|
+
return {
|
|
175
|
+
type: 'tool-progress',
|
|
176
|
+
toolCallId: chunk.payload.toolCallId,
|
|
177
|
+
toolName: chunk.payload.toolName ?? 'unknown',
|
|
178
|
+
output: chunk.payload.output,
|
|
179
|
+
};
|
|
180
|
+
case 'tool-call-delta':
|
|
181
|
+
return {
|
|
182
|
+
type: 'tool-call-delta',
|
|
183
|
+
toolCallId: chunk.payload.toolCallId,
|
|
184
|
+
// Strict `!== undefined` (not truthy) so an empty-string
|
|
185
|
+
// toolName from upstream — unlikely but possible — is
|
|
186
|
+
// forwarded honestly instead of silently swallowed.
|
|
187
|
+
...(chunk.payload.toolName !== undefined ? { toolName: chunk.payload.toolName } : {}),
|
|
188
|
+
argsTextDelta: chunk.payload.argsTextDelta,
|
|
189
|
+
};
|
|
172
190
|
// Known harness chunk types not yet supported in the SDK's ChatEvent
|
|
173
191
|
// model. Silently dropped to avoid leaking harness internals onto the
|
|
174
192
|
// public stream. Candidates for future ChatEvent variants when the
|
|
175
|
-
// corresponding features (citations, file generation, tool
|
|
176
|
-
//
|
|
193
|
+
// corresponding features (citations, file generation, tool input
|
|
194
|
+
// lifecycle bookends, tool suspension, structured output) are
|
|
195
|
+
// supported.
|
|
177
196
|
case 'source':
|
|
178
197
|
case 'file':
|
|
179
|
-
case 'tool-call-delta':
|
|
180
198
|
case 'tool-call-input-streaming-start':
|
|
181
199
|
case 'tool-call-input-streaming-end':
|
|
182
200
|
case 'tool-call-suspended':
|
|
183
201
|
case 'object':
|
|
184
|
-
case 'tool-output':
|
|
185
202
|
this.logBus?.debug('Skipping unsupported Mastra chunk type', { chunkType: chunk.type });
|
|
186
203
|
return undefined;
|
|
187
204
|
default:
|
package/dist/mastra-harness.d.ts
CHANGED
|
@@ -478,6 +478,22 @@ export declare class MastraHarness implements MastraAgentHarness {
|
|
|
478
478
|
* after `shutdown()` has disposed the buses.
|
|
479
479
|
*/
|
|
480
480
|
private prepareMcpStatusChange;
|
|
481
|
+
/**
|
|
482
|
+
* Issue #591: install a `progress.onUpdate(serverName, ...)` listener
|
|
483
|
+
* on the MCP client. Incoming `notifications/progress` are dispatched
|
|
484
|
+
* to the active coordinator on this agent whose `currentRunId` matches
|
|
485
|
+
* the notification's `progressToken` — `enableProgressTracking: true`
|
|
486
|
+
* makes `@mastra/mcp` inject `_meta.progressToken = context.runId` into
|
|
487
|
+
* every tool call, so the token uniquely identifies the originating
|
|
488
|
+
* turn even across concurrent threads on the same agent.
|
|
489
|
+
*
|
|
490
|
+
* `progress.onUpdate` is async (it lazily connects the underlying
|
|
491
|
+
* internal client). We swallow rejections inside a fire-and-forget
|
|
492
|
+
* wrapper rather than blocking `createAgent`/`updateAgent` — discovery
|
|
493
|
+
* and progress tracking are independent: a server that fails to wire
|
|
494
|
+
* progress still works for tool calls, just without progress events.
|
|
495
|
+
*/
|
|
496
|
+
private registerMcpProgressListener;
|
|
481
497
|
/**
|
|
482
498
|
* Runs MCP discovery for one server: emits the
|
|
483
499
|
* `mcp-server-discovery-started` / `…-completed` / `…-failed` telemetry
|
package/dist/mastra-harness.js
CHANGED
|
@@ -473,6 +473,9 @@ export class MastraHarness {
|
|
|
473
473
|
};
|
|
474
474
|
mcpServerStates.set(serverName, entry);
|
|
475
475
|
entry.ready = this.runMcpDiscovery(agentId, serverName, client, mcpServerStates, 0, options?.abortSignal);
|
|
476
|
+
// Issue #591: bridge MCP `notifications/progress` onto the
|
|
477
|
+
// coordinator's per-turn sink as `tool-progress` ChatEvents.
|
|
478
|
+
this.registerMcpProgressListener(agentId, serverName, client);
|
|
476
479
|
}
|
|
477
480
|
}
|
|
478
481
|
// Issue #529 step 2c: the bridge `execute` handler resolves the
|
|
@@ -893,6 +896,8 @@ export class MastraHarness {
|
|
|
893
896
|
};
|
|
894
897
|
state.mcpServers.set(name, entry);
|
|
895
898
|
entry.ready = this.runMcpDiscovery(agentId, name, client, state.mcpServers, 0, options?.abortSignal);
|
|
899
|
+
// Issue #591: same MCP-progress wiring as createAgent path.
|
|
900
|
+
this.registerMcpProgressListener(agentId, name, client);
|
|
896
901
|
}
|
|
897
902
|
}
|
|
898
903
|
}
|
|
@@ -1528,6 +1533,52 @@ export class MastraHarness {
|
|
|
1528
1533
|
}
|
|
1529
1534
|
};
|
|
1530
1535
|
}
|
|
1536
|
+
/**
|
|
1537
|
+
* Issue #591: install a `progress.onUpdate(serverName, ...)` listener
|
|
1538
|
+
* on the MCP client. Incoming `notifications/progress` are dispatched
|
|
1539
|
+
* to the active coordinator on this agent whose `currentRunId` matches
|
|
1540
|
+
* the notification's `progressToken` — `enableProgressTracking: true`
|
|
1541
|
+
* makes `@mastra/mcp` inject `_meta.progressToken = context.runId` into
|
|
1542
|
+
* every tool call, so the token uniquely identifies the originating
|
|
1543
|
+
* turn even across concurrent threads on the same agent.
|
|
1544
|
+
*
|
|
1545
|
+
* `progress.onUpdate` is async (it lazily connects the underlying
|
|
1546
|
+
* internal client). We swallow rejections inside a fire-and-forget
|
|
1547
|
+
* wrapper rather than blocking `createAgent`/`updateAgent` — discovery
|
|
1548
|
+
* and progress tracking are independent: a server that fails to wire
|
|
1549
|
+
* progress still works for tool calls, just without progress events.
|
|
1550
|
+
*/
|
|
1551
|
+
registerMcpProgressListener(agentId, serverName, client) {
|
|
1552
|
+
client.progress
|
|
1553
|
+
.onUpdate(serverName, (params) => {
|
|
1554
|
+
const agentState = this.activeAgents.get(agentId);
|
|
1555
|
+
if (!agentState)
|
|
1556
|
+
return;
|
|
1557
|
+
// Walk per-thread coordinators to find the one whose current
|
|
1558
|
+
// run produced this progressToken. Mastra's
|
|
1559
|
+
// `enableProgressTracking` injects `progressToken =
|
|
1560
|
+
// context.runId`, so `progressToken === coordinator.runId`
|
|
1561
|
+
// is an exact, single-match filter.
|
|
1562
|
+
const targetToken = String(params.progressToken);
|
|
1563
|
+
for (const coordinator of agentState.coordinators.values()) {
|
|
1564
|
+
if (coordinator.getCurrentRunId() === targetToken) {
|
|
1565
|
+
coordinator.emitMcpProgress({
|
|
1566
|
+
progress: params.progress,
|
|
1567
|
+
...(params.total !== undefined && { total: params.total }),
|
|
1568
|
+
...(params.message !== undefined && { message: params.message }),
|
|
1569
|
+
});
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
})
|
|
1574
|
+
.catch((err) => {
|
|
1575
|
+
this.buses.getLogBus()?.warn('Failed to register MCP progress listener', {
|
|
1576
|
+
agentId,
|
|
1577
|
+
serverName,
|
|
1578
|
+
error: getErrorMessage(err),
|
|
1579
|
+
});
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1531
1582
|
/**
|
|
1532
1583
|
* Runs MCP discovery for one server: emits the
|
|
1533
1584
|
* `mcp-server-discovery-started` / `…-completed` / `…-failed` telemetry
|
|
@@ -56,6 +56,13 @@ export function mapToMastraMCPServers(config, orgJwt) {
|
|
|
56
56
|
...(serverConfig.args !== undefined && { args: serverConfig.args }),
|
|
57
57
|
...(serverConfig.env !== undefined && { env: serverConfig.env }),
|
|
58
58
|
...(serverConfig.timeout !== undefined && { timeout: serverConfig.timeout }),
|
|
59
|
+
// Issue #591: opt every server in to MCP progress notifications
|
|
60
|
+
// so the upstream server knows it can send `notifications/progress`
|
|
61
|
+
// (the harness bridges those to `tool-progress` ChatEvents via
|
|
62
|
+
// `mcpClient.progress.onUpdate(...)`). Default in `@mastra/mcp` is
|
|
63
|
+
// off, so without this the spec-compliant progressToken is never
|
|
64
|
+
// injected into `_meta` and servers send no progress.
|
|
65
|
+
enableProgressTracking: true,
|
|
59
66
|
};
|
|
60
67
|
result[name] = def;
|
|
61
68
|
}
|
|
@@ -77,6 +84,8 @@ export function mapToMastraMCPServers(config, orgJwt) {
|
|
|
77
84
|
...serverConfig.reconnectionOptions,
|
|
78
85
|
},
|
|
79
86
|
}),
|
|
87
|
+
// Issue #591: same rationale as stdio.
|
|
88
|
+
enableProgressTracking: true,
|
|
80
89
|
};
|
|
81
90
|
result[name] = def;
|
|
82
91
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/sfdx-agent-harness-mastra",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Mastra-backed AgentHarness implementation for @salesforce/sfdx-agent-sdk",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -38,11 +38,11 @@
|
|
|
38
38
|
"@salesforce/llm-gateway-sdk": "0.14.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@salesforce/sfdx-agent-sdk": "0.
|
|
41
|
+
"@salesforce/sfdx-agent-sdk": "0.20.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.20.0",
|
|
46
46
|
"@types/node": "^22.19.20",
|
|
47
47
|
"@vitest/coverage-istanbul": "^4.1.8",
|
|
48
48
|
"@vitest/eslint-plugin": "^1.6.19",
|