@salesforce/sfdx-agent-harness-mastra 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
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.18.0] - 2026-06-11
|
|
7
|
+
|
|
8
|
+
### Fixes
|
|
9
|
+
- **harness-mastra**: drive one resume per pending approval for parallel batches ([#587](https://github.com/forcedotcom/agentic-dx/pull/587))
|
|
10
|
+
|
|
6
11
|
## [0.17.0] - 2026-06-09
|
|
7
12
|
|
|
8
13
|
### Fixes
|
|
@@ -143,12 +143,11 @@ export declare class MastraApprovalCoordinator {
|
|
|
143
143
|
*/
|
|
144
144
|
private currentRunId;
|
|
145
145
|
/**
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* closes to decide whether to drive a resume.
|
|
146
|
+
* ToolCallIds Mastra is currently suspended on. Mastra retains a separate
|
|
147
|
+
* workflow snapshot step per id, so the pump must drive one resume per
|
|
148
|
+
* pending id with `{ toolCallId }` to disambiguate (#586).
|
|
150
149
|
*/
|
|
151
|
-
private
|
|
150
|
+
private readonly pendingSuspendIds;
|
|
152
151
|
/** Consumer-tool: bound parkers, keyed by `toolCallId`. `submitToolResult` resolves these. */
|
|
153
152
|
private readonly pendingByToolCallId;
|
|
154
153
|
/**
|
|
@@ -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,11 @@ export class MastraApprovalCoordinator {
|
|
|
72
79
|
*/
|
|
73
80
|
currentRunId = '';
|
|
74
81
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* closes to decide whether to drive a resume.
|
|
82
|
+
* ToolCallIds Mastra is currently suspended on. Mastra retains a separate
|
|
83
|
+
* workflow snapshot step per id, so the pump must drive one resume per
|
|
84
|
+
* pending id with `{ toolCallId }` to disambiguate (#586).
|
|
79
85
|
*/
|
|
80
|
-
|
|
86
|
+
pendingSuspendIds = new Set();
|
|
81
87
|
/** Consumer-tool: bound parkers, keyed by `toolCallId`. `submitToolResult` resolves these. */
|
|
82
88
|
pendingByToolCallId = new Map();
|
|
83
89
|
/**
|
|
@@ -210,7 +216,7 @@ export class MastraApprovalCoordinator {
|
|
|
210
216
|
this.initial.push({ type: 'finish', finishReason: 'error' });
|
|
211
217
|
this.initial.end();
|
|
212
218
|
}
|
|
213
|
-
this.
|
|
219
|
+
this.pendingSuspendIds.clear();
|
|
214
220
|
this.runTeardownCallbacks();
|
|
215
221
|
}
|
|
216
222
|
// ───────────────────────── settle methods ───────────────────────────────
|
|
@@ -233,14 +239,9 @@ export class MastraApprovalCoordinator {
|
|
|
233
239
|
this.consumerSettled.add(toolCallId);
|
|
234
240
|
this.cancelTimer(toolCallId);
|
|
235
241
|
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) {
|
|
242
|
+
// Wake the pump if it's parked on `awaitContinuation` waiting for
|
|
243
|
+
// a decision on a pending id.
|
|
244
|
+
if (this.continuationResolver && this.pendingSuspendIds.has(toolCallId)) {
|
|
244
245
|
const resolve = this.continuationResolver;
|
|
245
246
|
this.continuationResolver = undefined;
|
|
246
247
|
resolve('continued');
|
|
@@ -330,33 +331,29 @@ export class MastraApprovalCoordinator {
|
|
|
330
331
|
}
|
|
331
332
|
if (this.aborted || this.disposed || this.timedOut)
|
|
332
333
|
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.
|
|
334
|
+
// outcome === 'suspended': drive one resume per outer
|
|
335
|
+
// iteration. For parallel batches, all but the last resume
|
|
336
|
+
// returns a stream that closes silently while still
|
|
337
|
+
// suspended — the pump re-enters here to drive the next id.
|
|
338
|
+
if (this.pendingSuspendIds.size === 0) {
|
|
342
339
|
if (!this.initial.isEnded())
|
|
343
340
|
this.initial.end();
|
|
344
341
|
return;
|
|
345
342
|
}
|
|
346
|
-
let
|
|
347
|
-
if (
|
|
348
|
-
// Park for a decision (or teardown).
|
|
343
|
+
let nextId = pickIdWithDecision(this.pendingSuspendIds, this.decisions);
|
|
344
|
+
if (nextId === undefined) {
|
|
349
345
|
const cont = await this.awaitContinuation();
|
|
350
346
|
if (cont === 'done')
|
|
351
347
|
return;
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
buffered = this.decisions.get(suspendId);
|
|
355
|
-
if (buffered === undefined)
|
|
348
|
+
nextId = pickIdWithDecision(this.pendingSuspendIds, this.decisions);
|
|
349
|
+
if (nextId === undefined)
|
|
356
350
|
return;
|
|
357
351
|
}
|
|
358
|
-
|
|
359
|
-
|
|
352
|
+
const decision = this.decisions.get(nextId);
|
|
353
|
+
if (decision === undefined)
|
|
354
|
+
return;
|
|
355
|
+
this.pendingSuspendIds.delete(nextId);
|
|
356
|
+
const next = await this.driveResumeReturning(nextId, decision);
|
|
360
357
|
if (next === 'done')
|
|
361
358
|
return;
|
|
362
359
|
currentStream = next;
|
|
@@ -434,22 +431,21 @@ export class MastraApprovalCoordinator {
|
|
|
434
431
|
continue;
|
|
435
432
|
}
|
|
436
433
|
if (event.type === 'tool-approval-request') {
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
//
|
|
440
|
-
// auto-approve silently.
|
|
434
|
+
// Parallel batches emit one chunk per suspension before
|
|
435
|
+
// the stream closes; accumulate every id and let the
|
|
436
|
+
// read loop continue to the natural close.
|
|
441
437
|
const tc = event.toolCall;
|
|
442
438
|
this.toolCallArgs.set(tc.toolCallId, tc.args);
|
|
443
|
-
this.
|
|
439
|
+
this.pendingSuspendIds.add(tc.toolCallId);
|
|
444
440
|
sawSuspension = true;
|
|
445
441
|
if (this.consumerToolNames.has(tc.toolName)) {
|
|
446
442
|
// Consumer tools bypass the gate per the
|
|
447
443
|
// StreamOptions.requireToolApproval JSDoc. Auto-
|
|
448
444
|
// approve silently — no ChatEvent surfaces, and the
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
//
|
|
452
|
-
//
|
|
445
|
+
// pump loop drives `agent.approveToolCall` when it
|
|
446
|
+
// picks up the buffered decision. After Mastra
|
|
447
|
+
// resumes, the bridge `execute` parker runs and
|
|
448
|
+
// `submitToolResult` settles it — same shape as
|
|
453
449
|
// non-approval-mode consumer-tool flow.
|
|
454
450
|
this.settledByCoordinator.add(tc.toolCallId);
|
|
455
451
|
this.decisions.set(tc.toolCallId, { kind: 'approve' });
|
|
@@ -459,14 +455,7 @@ export class MastraApprovalCoordinator {
|
|
|
459
455
|
// pre-emit in tool-call routing).
|
|
460
456
|
this.emitApprovalRequest(tc.toolCallId, tc.toolName, tc.args);
|
|
461
457
|
}
|
|
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';
|
|
458
|
+
continue;
|
|
470
459
|
}
|
|
471
460
|
this.routeEnriched(event);
|
|
472
461
|
}
|
|
@@ -490,17 +479,13 @@ export class MastraApprovalCoordinator {
|
|
|
490
479
|
/* best-effort: lock may already be released after a thrown read */
|
|
491
480
|
}
|
|
492
481
|
}
|
|
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()) {
|
|
482
|
+
// Stream closed without `finish`/`error`. If any suspension is
|
|
483
|
+
// pending (just observed, or carried over from a prior non-final
|
|
484
|
+
// resume that closed silently), defer to the pump loop's drive.
|
|
485
|
+
if (sawSuspension || this.pendingSuspendIds.size > 0) {
|
|
486
|
+
return 'suspended';
|
|
487
|
+
}
|
|
488
|
+
if (!sawTerminal && !this.initial.isEnded()) {
|
|
504
489
|
this.initial.end();
|
|
505
490
|
}
|
|
506
491
|
return 'terminal';
|
|
@@ -540,10 +525,10 @@ export class MastraApprovalCoordinator {
|
|
|
540
525
|
return output.fullStream;
|
|
541
526
|
}
|
|
542
527
|
catch (err) {
|
|
528
|
+
// Route through teardown so siblings in `pendingSuspendIds`
|
|
529
|
+
// settle as auto-resolved rather than silently orphaning.
|
|
543
530
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
544
|
-
this.
|
|
545
|
-
this.initial.push({ type: 'finish', finishReason: 'error' });
|
|
546
|
-
this.initial.end();
|
|
531
|
+
this.teardown('pump-error', error, 'pump-error');
|
|
547
532
|
return 'done';
|
|
548
533
|
}
|
|
549
534
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/sfdx-agent-harness-mastra",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.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.19.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.19.0",
|
|
46
46
|
"@types/node": "^22.19.20",
|
|
47
47
|
"@vitest/coverage-istanbul": "^4.1.8",
|
|
48
48
|
"@vitest/eslint-plugin": "^1.6.19",
|