@salesforce/sfdx-agent-harness-mastra 0.62.0 → 0.64.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,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.64.0] - 2026-09-02
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
- **harness-mastra**: synthesize a terminal error when a Mastra stream closes without a finish @W-24026817@ ([#784](https://github.com/forcedotcom/agentic-dx/pull/784))
|
|
10
|
+
|
|
11
|
+
## [0.63.0] - 2026-09-02
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
- **agent-sdk,harness-mastra,harness-claude,harness-openai**: reject concurrent turns with a typed SESSION_BUSY error @W-24044572@ ([#783](https://github.com/forcedotcom/agentic-dx/pull/783))
|
|
15
|
+
|
|
6
16
|
## [0.62.0] - 2026-09-02
|
|
7
17
|
|
|
8
18
|
### Fixes
|
|
@@ -261,6 +261,18 @@ export declare class MastraApprovalCoordinator {
|
|
|
261
261
|
private timedOut;
|
|
262
262
|
constructor(opts: MastraApprovalCoordinatorOptions);
|
|
263
263
|
initialSink(): GenSink<ChatEvent>;
|
|
264
|
+
/**
|
|
265
|
+
* True once this turn has reached a terminal event — a natural `finish`
|
|
266
|
+
* pushed by the pump, or the synthetic `error` + `finish('error')` pair a
|
|
267
|
+
* `teardown` (abort / dispose / timeout / pump-error) emits. Both end the
|
|
268
|
+
* single per-turn sink, so `initial.isEnded()` is the authoritative "this
|
|
269
|
+
* turn is over" signal. A coordinator still pumping — including one PARKED on
|
|
270
|
+
* a `tool-approval-request` or a consumer-tool `submitToolResult` — has not
|
|
271
|
+
* ended its sink and is therefore NOT settled. `MastraHarness.stream()` reads
|
|
272
|
+
* this to reject a concurrent turn with `SESSION_BUSY` (a still-active
|
|
273
|
+
* coordinator) versus safely replace a settled one.
|
|
274
|
+
*/
|
|
275
|
+
isSettled(): boolean;
|
|
264
276
|
/**
|
|
265
277
|
* Mastra's current run id. The harness's MCP-progress dispatcher uses
|
|
266
278
|
* this to filter incoming `notifications/progress` to the coordinator
|
|
@@ -395,6 +407,40 @@ export declare class MastraApprovalCoordinator {
|
|
|
395
407
|
*/
|
|
396
408
|
private driveResumeReturning;
|
|
397
409
|
private awaitContinuation;
|
|
410
|
+
/**
|
|
411
|
+
* Terminal-event guard (spike agent-offline-failure-modes #07, W-24026817).
|
|
412
|
+
*
|
|
413
|
+
* Called at the pump's silent-close sites — where Mastra's `fullStream`
|
|
414
|
+
* exhausted with neither a `finish`/`error` chunk nor a recognized
|
|
415
|
+
* suspension, so the turn produced no terminal event of its own. A healthy
|
|
416
|
+
* turn never lands here: a normal turn ends on a `finish` chunk, and an
|
|
417
|
+
* approval turn's FINAL resume produces the real continuation `finish` (see
|
|
418
|
+
* ARCHITECTURE.md → "Pump loop", step 5). The realistic trigger is a
|
|
419
|
+
* COORDINATOR-level terminating chunk the adapter intentionally skips —
|
|
420
|
+
* today `tool-call-suspended` (a tool using Mastra's native `suspend()`,
|
|
421
|
+
* which the event adapter maps to `undefined`) — and the guard also closes
|
|
422
|
+
* the class for any future Mastra chunk that ends the stream without a
|
|
423
|
+
* paired finish.
|
|
424
|
+
*
|
|
425
|
+
* Without this, the pump would `initial.end()` the sink silently and the
|
|
426
|
+
* SDK's `DefaultChatSession.wrapEventStream` — which synthesizes its own
|
|
427
|
+
* terminal `finish` ONLY when an error landed — would record the turn as a
|
|
428
|
+
* false-success `chat-stream-completed` with NO terminal `FinishEvent`,
|
|
429
|
+
* breaking the SDK invariant "every chat turn ends with exactly one terminal
|
|
430
|
+
* FinishEvent". Routing through the single `teardown` path pushes the typed
|
|
431
|
+
* `error('incomplete-stream')` + `finish('error')` pair the SDK invariant
|
|
432
|
+
* requires, so the turn fails loudly and recoverably instead. This is the
|
|
433
|
+
* harness-side complement to the SDK-side invariant item #10 pinned.
|
|
434
|
+
*
|
|
435
|
+
* `teardown` short-circuits its sink pushes once the sink is ended
|
|
436
|
+
* (`GenSink.push` no-ops after `end()`), and every terminal path either
|
|
437
|
+
* ended the sink already or set `aborted`/`disposed`/`timedOut` (which the
|
|
438
|
+
* callers check first), so this can never double-signal a turn that already
|
|
439
|
+
* reached a terminal — including 8a's pass-through `empty-stream` error,
|
|
440
|
+
* which arrives as an `error` chunk that ends the sink before the stream
|
|
441
|
+
* closes.
|
|
442
|
+
*/
|
|
443
|
+
private guardTerminalOnSilentClose;
|
|
398
444
|
/**
|
|
399
445
|
* Push an event onto the sink with MCP-server enrichment applied (when
|
|
400
446
|
* the toolName resolves to a known MCP catalog entry). Mirrors the
|
|
@@ -184,6 +184,20 @@ export class MastraApprovalCoordinator {
|
|
|
184
184
|
initialSink() {
|
|
185
185
|
return this.initial;
|
|
186
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* True once this turn has reached a terminal event — a natural `finish`
|
|
189
|
+
* pushed by the pump, or the synthetic `error` + `finish('error')` pair a
|
|
190
|
+
* `teardown` (abort / dispose / timeout / pump-error) emits. Both end the
|
|
191
|
+
* single per-turn sink, so `initial.isEnded()` is the authoritative "this
|
|
192
|
+
* turn is over" signal. A coordinator still pumping — including one PARKED on
|
|
193
|
+
* a `tool-approval-request` or a consumer-tool `submitToolResult` — has not
|
|
194
|
+
* ended its sink and is therefore NOT settled. `MastraHarness.stream()` reads
|
|
195
|
+
* this to reject a concurrent turn with `SESSION_BUSY` (a still-active
|
|
196
|
+
* coordinator) versus safely replace a settled one.
|
|
197
|
+
*/
|
|
198
|
+
isSettled() {
|
|
199
|
+
return this.initial.isEnded();
|
|
200
|
+
}
|
|
187
201
|
/**
|
|
188
202
|
* Mastra's current run id. The harness's MCP-progress dispatcher uses
|
|
189
203
|
* this to filter incoming `notifications/progress` to the coordinator
|
|
@@ -484,8 +498,8 @@ export class MastraApprovalCoordinator {
|
|
|
484
498
|
// returns a stream that closes silently while still
|
|
485
499
|
// suspended — the pump re-enters here to drive the next id.
|
|
486
500
|
if (this.pendingSuspendIds.size === 0) {
|
|
487
|
-
|
|
488
|
-
|
|
501
|
+
// Terminal-event guard — see guardTerminalOnSilentClose.
|
|
502
|
+
this.guardTerminalOnSilentClose();
|
|
489
503
|
return;
|
|
490
504
|
}
|
|
491
505
|
let nextId = pickIdWithDecision(this.pendingSuspendIds, this.decisions);
|
|
@@ -658,9 +672,9 @@ export class MastraApprovalCoordinator {
|
|
|
658
672
|
if (sawSuspension || this.pendingSuspendIds.size > 0) {
|
|
659
673
|
return 'suspended';
|
|
660
674
|
}
|
|
661
|
-
if (!sawTerminal
|
|
662
|
-
|
|
663
|
-
this.
|
|
675
|
+
if (!sawTerminal) {
|
|
676
|
+
// Terminal-event guard — see guardTerminalOnSilentClose.
|
|
677
|
+
this.guardTerminalOnSilentClose();
|
|
664
678
|
}
|
|
665
679
|
return 'terminal';
|
|
666
680
|
}
|
|
@@ -712,6 +726,44 @@ export class MastraApprovalCoordinator {
|
|
|
712
726
|
this.continuationResolver = resolve;
|
|
713
727
|
});
|
|
714
728
|
}
|
|
729
|
+
/**
|
|
730
|
+
* Terminal-event guard (spike agent-offline-failure-modes #07, W-24026817).
|
|
731
|
+
*
|
|
732
|
+
* Called at the pump's silent-close sites — where Mastra's `fullStream`
|
|
733
|
+
* exhausted with neither a `finish`/`error` chunk nor a recognized
|
|
734
|
+
* suspension, so the turn produced no terminal event of its own. A healthy
|
|
735
|
+
* turn never lands here: a normal turn ends on a `finish` chunk, and an
|
|
736
|
+
* approval turn's FINAL resume produces the real continuation `finish` (see
|
|
737
|
+
* ARCHITECTURE.md → "Pump loop", step 5). The realistic trigger is a
|
|
738
|
+
* COORDINATOR-level terminating chunk the adapter intentionally skips —
|
|
739
|
+
* today `tool-call-suspended` (a tool using Mastra's native `suspend()`,
|
|
740
|
+
* which the event adapter maps to `undefined`) — and the guard also closes
|
|
741
|
+
* the class for any future Mastra chunk that ends the stream without a
|
|
742
|
+
* paired finish.
|
|
743
|
+
*
|
|
744
|
+
* Without this, the pump would `initial.end()` the sink silently and the
|
|
745
|
+
* SDK's `DefaultChatSession.wrapEventStream` — which synthesizes its own
|
|
746
|
+
* terminal `finish` ONLY when an error landed — would record the turn as a
|
|
747
|
+
* false-success `chat-stream-completed` with NO terminal `FinishEvent`,
|
|
748
|
+
* breaking the SDK invariant "every chat turn ends with exactly one terminal
|
|
749
|
+
* FinishEvent". Routing through the single `teardown` path pushes the typed
|
|
750
|
+
* `error('incomplete-stream')` + `finish('error')` pair the SDK invariant
|
|
751
|
+
* requires, so the turn fails loudly and recoverably instead. This is the
|
|
752
|
+
* harness-side complement to the SDK-side invariant item #10 pinned.
|
|
753
|
+
*
|
|
754
|
+
* `teardown` short-circuits its sink pushes once the sink is ended
|
|
755
|
+
* (`GenSink.push` no-ops after `end()`), and every terminal path either
|
|
756
|
+
* ended the sink already or set `aborted`/`disposed`/`timedOut` (which the
|
|
757
|
+
* callers check first), so this can never double-signal a turn that already
|
|
758
|
+
* reached a terminal — including 8a's pass-through `empty-stream` error,
|
|
759
|
+
* which arrives as an `error` chunk that ends the sink before the stream
|
|
760
|
+
* closes.
|
|
761
|
+
*/
|
|
762
|
+
guardTerminalOnSilentClose() {
|
|
763
|
+
if (this.initial.isEnded())
|
|
764
|
+
return;
|
|
765
|
+
this.teardown('incomplete-stream', new Error('The model stream closed without a terminal finish or error event.'), 'incomplete-stream');
|
|
766
|
+
}
|
|
715
767
|
// ───────────────────────── routing helpers ──────────────────────────────
|
|
716
768
|
/**
|
|
717
769
|
* Push an event onto the sink with MCP-server enrichment applied (when
|
package/dist/mastra-harness.js
CHANGED
|
@@ -1618,12 +1618,20 @@ export class MastraHarness {
|
|
|
1618
1618
|
// round-trip per tool call.
|
|
1619
1619
|
const { lookupPolicy, batchApprovals } = this.buildPolicyGate(agentState, options);
|
|
1620
1620
|
const gatingActive = lookupPolicy !== undefined;
|
|
1621
|
-
//
|
|
1622
|
-
//
|
|
1623
|
-
//
|
|
1624
|
-
//
|
|
1625
|
-
// turn
|
|
1621
|
+
// Concurrency guard (spike agent-offline-failure-modes #10, W-24044572):
|
|
1622
|
+
// reject a second turn on a thread whose prior turn is still ACTIVE with a
|
|
1623
|
+
// typed `SESSION_BUSY` error, instead of silently disposing the first
|
|
1624
|
+
// (which surfaced to the first consumer as a spurious `DISPOSED` — a
|
|
1625
|
+
// "silent lost turn"). A coordinator is active until its sink ends (natural
|
|
1626
|
+
// finish or teardown), so one still pumping — including one parked on an
|
|
1627
|
+
// approval / consumer-tool result — blocks a new turn. An already-settled
|
|
1628
|
+
// coordinator (finished or errored) is safe to dispose-and-replace, so the
|
|
1629
|
+
// idempotent settling of an already-terminal turn is preserved.
|
|
1626
1630
|
const priorCoordinator = agentState.coordinators.get(threadId);
|
|
1631
|
+
if (priorCoordinator && !priorCoordinator.isSettled()) {
|
|
1632
|
+
throw new AgentSDKError(`A turn is already in progress for thread "${threadId}" on agent "${agentId}". ` +
|
|
1633
|
+
'Only one turn may run at a time per thread; await the in-flight turn before starting another.', AgentSDKErrorType.SESSION_BUSY);
|
|
1634
|
+
}
|
|
1627
1635
|
if (priorCoordinator) {
|
|
1628
1636
|
priorCoordinator.dispose();
|
|
1629
1637
|
agentState.coordinators.delete(threadId);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/sfdx-agent-harness-mastra",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.64.0",
|
|
4
4
|
"description": "Mastra-backed AgentHarness implementation for @salesforce/sfdx-agent-sdk",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -42,12 +42,12 @@
|
|
|
42
42
|
"openai": "^7.0.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"@salesforce/sfdx-agent-sdk": "0.
|
|
45
|
+
"@salesforce/sfdx-agent-sdk": "0.65.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@eslint/js": "^10.0.1",
|
|
49
49
|
"@salesforce/harness-conformance": "0.0.1",
|
|
50
|
-
"@salesforce/sfdx-agent-sdk": "0.
|
|
50
|
+
"@salesforce/sfdx-agent-sdk": "0.65.0",
|
|
51
51
|
"@types/node": "^22.20.1",
|
|
52
52
|
"@vitest/coverage-istanbul": "^4.1.10",
|
|
53
53
|
"@vitest/eslint-plugin": "^1.6.27",
|