@salesforce/sfdx-agent-harness-mastra 0.65.0 → 0.67.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.67.0] - 2026-09-02
|
|
7
|
+
|
|
8
|
+
### Fixes
|
|
9
|
+
- **harness-mastra**: wall-clock timeout on outbound resume RPC @W-24026787@ ([#790](https://github.com/forcedotcom/agentic-dx/pull/790))
|
|
10
|
+
|
|
11
|
+
## [0.66.0] - 2026-09-02
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
- **agent-sdk,harness-mastra,harness-claude,harness-openai**: classify context-window overflow as a typed CONTEXT_LENGTH_EXCEEDED error @W-24074717@ ([#788](https://github.com/forcedotcom/agentic-dx/pull/788))
|
|
15
|
+
|
|
6
16
|
## [0.65.0] - 2026-09-02
|
|
7
17
|
|
|
8
18
|
_No changes — released alongside dependent packages._
|
|
@@ -35,6 +35,22 @@ export interface MastraApprovalCoordinatorOptions {
|
|
|
35
35
|
};
|
|
36
36
|
requestContext: RequestContext;
|
|
37
37
|
toolApprovalTimeoutMs: number;
|
|
38
|
+
/**
|
|
39
|
+
* Wall-clock bound on the outbound resume RPC — `agent.approveToolCall(...)`
|
|
40
|
+
* / `agent.declineToolCall(...)` — NOT on the resumed turn's work (6c, spike
|
|
41
|
+
* agent-offline-failure-modes, W-24026787). Those calls return promptly with
|
|
42
|
+
* a fresh `fullStream` handle; the model's actual generation (which can be a
|
|
43
|
+
* long deployment or a slow "thinking" turn) streams through that handle,
|
|
44
|
+
* pumped separately with no timeout — so a healthy resume is never capped.
|
|
45
|
+
* This bound only fires when the establishment round-trip itself hangs and
|
|
46
|
+
* never hands back a stream: after a human decision the "waiting for a human"
|
|
47
|
+
* timer is already cancelled and the session is `busy`, so on plain-REST no
|
|
48
|
+
* idle timer or auto-decline sweep is left to recover it. On timeout the
|
|
49
|
+
* coordinator routes through the shared `teardown('timeout', …)` path so the
|
|
50
|
+
* sink still gets a terminal error+finish and the session is released rather
|
|
51
|
+
* than wedged. Defaults to {@link DEFAULT_RESUME_RPC_TIMEOUT_MS}.
|
|
52
|
+
*/
|
|
53
|
+
resumeRpcTimeoutMs?: number;
|
|
38
54
|
/**
|
|
39
55
|
* Per-tool approval-decision callback (Phase 2a). When defined, gating is
|
|
40
56
|
* **active**: the harness passed `requireToolApproval: true` to Mastra so
|
|
@@ -159,6 +175,7 @@ export declare class MastraApprovalCoordinator {
|
|
|
159
175
|
private readonly memory;
|
|
160
176
|
private readonly requestContext;
|
|
161
177
|
private readonly toolApprovalTimeoutMs;
|
|
178
|
+
private readonly resumeRpcTimeoutMs;
|
|
162
179
|
private readonly lookupPolicy?;
|
|
163
180
|
private readonly batchApprovals;
|
|
164
181
|
private readonly consumerToolNames;
|
|
@@ -23,6 +23,10 @@ function pickIdWithDecision(pendingIds, decisions) {
|
|
|
23
23
|
return undefined;
|
|
24
24
|
}
|
|
25
25
|
const POLICY_DENIED_RESULT_TEXT = 'Denied by tool policy.';
|
|
26
|
+
/** Default {@link MastraApprovalCoordinatorOptions.resumeRpcTimeoutMs} — 5 minutes. */
|
|
27
|
+
const DEFAULT_RESUME_RPC_TIMEOUT_MS = 300_000;
|
|
28
|
+
/** Race sentinel: the resume RPC establishment exceeded {@link DEFAULT_RESUME_RPC_TIMEOUT_MS}. */
|
|
29
|
+
const RESUME_TIMEOUT = 'resume-timeout';
|
|
26
30
|
/**
|
|
27
31
|
* Per-`(agentId, threadId)` coordinator that subsumes the pre-#529
|
|
28
32
|
* `MastraStreamBroker` + `activeRuns` pair into one object owning a single
|
|
@@ -65,6 +69,7 @@ export class MastraApprovalCoordinator {
|
|
|
65
69
|
memory;
|
|
66
70
|
requestContext;
|
|
67
71
|
toolApprovalTimeoutMs;
|
|
72
|
+
resumeRpcTimeoutMs;
|
|
68
73
|
lookupPolicy;
|
|
69
74
|
batchApprovals;
|
|
70
75
|
consumerToolNames;
|
|
@@ -170,6 +175,7 @@ export class MastraApprovalCoordinator {
|
|
|
170
175
|
this.memory = opts.memory;
|
|
171
176
|
this.requestContext = opts.requestContext;
|
|
172
177
|
this.toolApprovalTimeoutMs = opts.toolApprovalTimeoutMs;
|
|
178
|
+
this.resumeRpcTimeoutMs = opts.resumeRpcTimeoutMs ?? DEFAULT_RESUME_RPC_TIMEOUT_MS;
|
|
173
179
|
this.lookupPolicy = opts.lookupPolicy;
|
|
174
180
|
this.batchApprovals = opts.batchApprovals ?? false;
|
|
175
181
|
this.consumerToolNames = opts.consumerToolNames;
|
|
@@ -684,42 +690,95 @@ export class MastraApprovalCoordinator {
|
|
|
684
690
|
* pushed) or the new `fullStream` to pump next.
|
|
685
691
|
*/
|
|
686
692
|
async driveResumeReturning(toolCallId, decision) {
|
|
693
|
+
// Kick off the resume RPC. See `resumeRpcTimeoutMs` for why the bound
|
|
694
|
+
// below is on this handshake only, never the streamed work.
|
|
695
|
+
const resume = decision.kind === 'approve'
|
|
696
|
+
? this.agent.approveToolCall({
|
|
697
|
+
runId: this.currentRunId,
|
|
698
|
+
toolCallId,
|
|
699
|
+
memory: this.memory,
|
|
700
|
+
requestContext: this.requestContext,
|
|
701
|
+
})
|
|
702
|
+
: this.agent.declineToolCall({
|
|
703
|
+
runId: this.currentRunId,
|
|
704
|
+
toolCallId,
|
|
705
|
+
memory: this.memory,
|
|
706
|
+
requestContext: this.requestContext,
|
|
707
|
+
...(decision.modelMessage ? { reason: decision.modelMessage } : {}),
|
|
708
|
+
});
|
|
709
|
+
// 6c (W-24026787): bound the handshake with a wall-clock timer. See
|
|
710
|
+
// `resumeRpcTimeoutMs` for why this is the only recovery backstop left
|
|
711
|
+
// at this point on plain-REST. Race the RPC against the timer; whichever
|
|
712
|
+
// settles first wins and the timer is always cleared so the win path
|
|
713
|
+
// never leaves a stray timer to double-signal.
|
|
714
|
+
let timer;
|
|
715
|
+
const timeout = new Promise((resolve) => {
|
|
716
|
+
timer = setTimeout(() => resolve(RESUME_TIMEOUT), this.resumeRpcTimeoutMs);
|
|
717
|
+
});
|
|
718
|
+
let raced;
|
|
687
719
|
try {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
//
|
|
703
|
-
//
|
|
704
|
-
|
|
720
|
+
raced = await Promise.race([resume, timeout]);
|
|
721
|
+
}
|
|
722
|
+
catch (err) {
|
|
723
|
+
// The resume RPC rejected outright. Route through teardown so
|
|
724
|
+
// siblings in `pendingSuspendIds` settle as auto-resolved rather
|
|
725
|
+
// than silently orphaning.
|
|
726
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
727
|
+
this.teardown('pump-error', error, 'pump-error');
|
|
728
|
+
return 'done';
|
|
729
|
+
}
|
|
730
|
+
finally {
|
|
731
|
+
clearTimeout(timer);
|
|
732
|
+
}
|
|
733
|
+
if (raced === RESUME_TIMEOUT) {
|
|
734
|
+
// Release a belatedly-established run on EVERY timeout path — this
|
|
735
|
+
// must sit ABOVE the aborted/disposed/timedOut guard below. This
|
|
736
|
+
// resume timer is a local, not in `approvalTimers`, so `teardown`'s
|
|
737
|
+
// `cancelAllTimers` can't cancel it: a `POST /cancel` during a hung
|
|
738
|
+
// handshake ends the sink but does NOT short-circuit this timer, so
|
|
739
|
+
// it still fires ~resumeRpcTimeoutMs later, hits the guard, and
|
|
740
|
+
// returns — and if the hung RPC then resolves, nothing else would
|
|
741
|
+
// release its run. Attaching here keeps the release on that
|
|
742
|
+
// abort/dispose-then-late-resolve edge (mirrors the abort/dispose
|
|
743
|
+
// post-await guard below). The RPC is never used to drive another
|
|
744
|
+
// stream — the turn is over; a late rejection is a no-op (also
|
|
745
|
+
// already consumed by the race).
|
|
746
|
+
void resume.then((out) => {
|
|
705
747
|
try {
|
|
706
|
-
this.agent.abortRunStream(
|
|
748
|
+
this.agent.abortRunStream(out.runId);
|
|
707
749
|
}
|
|
708
750
|
catch {
|
|
709
751
|
/* best-effort */
|
|
710
752
|
}
|
|
753
|
+
}, () => {
|
|
754
|
+
/* late rejection: already consumed by the race */
|
|
755
|
+
});
|
|
756
|
+
// abort/dispose/another timeout may have already won and ended the
|
|
757
|
+
// sink — if so, nothing more to do (the release above still ran).
|
|
758
|
+
if (this.aborted || this.disposed || this.timedOut)
|
|
711
759
|
return 'done';
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
760
|
+
this.timedOut = true;
|
|
761
|
+
// Route through the SAME teardown path abort/dispose/approval-timeout
|
|
762
|
+
// use, so the single-terminal invariant holds and the session is
|
|
763
|
+
// released. `teardown`'s sink pushes no-op once the sink has ended,
|
|
764
|
+
// so this can't double-signal a turn that already terminated.
|
|
765
|
+
this.teardown('timeout', new Error(`Tool resume RPC timed out after ${this.resumeRpcTimeoutMs}ms for tool call "${toolCallId}".`), RESUME_TIMEOUT);
|
|
766
|
+
return 'done';
|
|
715
767
|
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
768
|
+
const output = raced;
|
|
769
|
+
// Post-await guard: abort/dispose may have fired during the round-trip.
|
|
770
|
+
// Release the new run if so.
|
|
771
|
+
if (this.aborted || this.disposed) {
|
|
772
|
+
try {
|
|
773
|
+
this.agent.abortRunStream(output.runId);
|
|
774
|
+
}
|
|
775
|
+
catch {
|
|
776
|
+
/* best-effort */
|
|
777
|
+
}
|
|
721
778
|
return 'done';
|
|
722
779
|
}
|
|
780
|
+
this.currentRunId = output.runId;
|
|
781
|
+
return output.fullStream;
|
|
723
782
|
}
|
|
724
783
|
awaitContinuation() {
|
|
725
784
|
return new Promise((resolve) => {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Copyright 2026, Salesforce, Inc. All rights reserved.
|
|
3
3
|
* See LICENSE.txt for license terms.
|
|
4
4
|
*/
|
|
5
|
+
import { CONTEXT_LENGTH_EXCEEDED_CODE, createContextLengthExceededError } from '@salesforce/sfdx-agent-sdk';
|
|
5
6
|
import { isClassifiedStreamError } from './stream-errors.js';
|
|
6
7
|
/**
|
|
7
8
|
* Adapts Mastra `ChunkType` stream events to our {@link ChatEvent} discriminated union.
|
|
@@ -153,10 +154,19 @@ export class MastraEventAdapter {
|
|
|
153
154
|
}
|
|
154
155
|
case 'error': {
|
|
155
156
|
const rawError = chunk.payload.error;
|
|
157
|
+
const code = isClassifiedStreamError(rawError) ? rawError.code : undefined;
|
|
158
|
+
const baseError = rawError instanceof Error ? rawError : new Error(String(rawError));
|
|
159
|
+
// Context-window overflow is the one classified error we upgrade to
|
|
160
|
+
// the SDK's typed `AgentSDKError(CONTEXT_LENGTH_EXCEEDED)` so the
|
|
161
|
+
// in-stream signal matches the pre-flight throw and carries the
|
|
162
|
+
// actionable message; the raw ValidationStreamError rides on `.cause`.
|
|
163
|
+
if (code === CONTEXT_LENGTH_EXCEEDED_CODE) {
|
|
164
|
+
return { type: 'error', error: createContextLengthExceededError(baseError), code };
|
|
165
|
+
}
|
|
156
166
|
return {
|
|
157
167
|
type: 'error',
|
|
158
|
-
error:
|
|
159
|
-
...(
|
|
168
|
+
error: baseError,
|
|
169
|
+
...(code !== undefined ? { code } : {}),
|
|
160
170
|
};
|
|
161
171
|
}
|
|
162
172
|
case 'tripwire':
|
package/dist/stream-errors.d.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
export declare const NETWORK_ERROR_CODE: 'network-error';
|
|
2
2
|
export declare const EMPTY_STREAM_ERROR_CODE: 'empty-stream';
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* The overflow `ErrorEvent.code`. Aliased to the SDK-owned
|
|
5
|
+
* {@link CONTEXT_LENGTH_EXCEEDED_CODE} so this harness and the cross-harness
|
|
6
|
+
* contract can never drift on the wire value; the adapter upgrades a
|
|
7
|
+
* ValidationStreamError carrying this code to the typed
|
|
8
|
+
* `AgentSDKError(CONTEXT_LENGTH_EXCEEDED)` before it reaches the consumer.
|
|
9
|
+
*/
|
|
10
|
+
export declare const CONTEXT_OVERFLOW_ERROR_CODE: "context-window-exceeded";
|
|
4
11
|
export declare const VALIDATION_ERROR_CODE: 'validation-error';
|
|
5
12
|
export declare const GATEWAY_RESPONSE_ERROR_CODE: 'gateway-error';
|
|
6
13
|
/**
|
package/dist/stream-errors.js
CHANGED
|
@@ -2,9 +2,17 @@
|
|
|
2
2
|
* Copyright 2026, Salesforce, Inc. All rights reserved.
|
|
3
3
|
* See LICENSE.txt for license terms.
|
|
4
4
|
*/
|
|
5
|
+
import { CONTEXT_LENGTH_EXCEEDED_CODE, isContextWindowOverflowMessage } from '@salesforce/sfdx-agent-sdk';
|
|
5
6
|
export const NETWORK_ERROR_CODE = 'network-error';
|
|
6
7
|
export const EMPTY_STREAM_ERROR_CODE = 'empty-stream';
|
|
7
|
-
|
|
8
|
+
/**
|
|
9
|
+
* The overflow `ErrorEvent.code`. Aliased to the SDK-owned
|
|
10
|
+
* {@link CONTEXT_LENGTH_EXCEEDED_CODE} so this harness and the cross-harness
|
|
11
|
+
* contract can never drift on the wire value; the adapter upgrades a
|
|
12
|
+
* ValidationStreamError carrying this code to the typed
|
|
13
|
+
* `AgentSDKError(CONTEXT_LENGTH_EXCEEDED)` before it reaches the consumer.
|
|
14
|
+
*/
|
|
15
|
+
export const CONTEXT_OVERFLOW_ERROR_CODE = CONTEXT_LENGTH_EXCEEDED_CODE;
|
|
8
16
|
export const VALIDATION_ERROR_CODE = 'validation-error';
|
|
9
17
|
export const GATEWAY_RESPONSE_ERROR_CODE = 'gateway-error';
|
|
10
18
|
/**
|
|
@@ -109,12 +117,6 @@ export function isStreamTransportError(error) {
|
|
|
109
117
|
}
|
|
110
118
|
return false;
|
|
111
119
|
}
|
|
112
|
-
/**
|
|
113
|
-
* Messages Bedrock / Anthropic use for a context-window overflow — the subset of
|
|
114
|
-
* `ValidationException` worth surfacing with the more specific
|
|
115
|
-
* `context-window-exceeded` code (ties into spike item 04).
|
|
116
|
-
*/
|
|
117
|
-
const CONTEXT_OVERFLOW_PATTERNS = /input is too long|prompt is too long|too many (input )?tokens|maximum context length|context (window|length)|exceed(s|ed)? (the )?(maximum )?context/i;
|
|
118
120
|
/**
|
|
119
121
|
* Recognizes the Bedrock `ValidationException` shape (8d), walking the `cause`
|
|
120
122
|
* chain and duck-typing the several ways `@anthropic-ai/bedrock-sdk` /
|
|
@@ -131,7 +133,7 @@ export function recognizeBedrockValidationError(error) {
|
|
|
131
133
|
seen.add(current);
|
|
132
134
|
const message = extractValidationExceptionMessage(current);
|
|
133
135
|
if (message !== undefined) {
|
|
134
|
-
const code =
|
|
136
|
+
const code = isContextWindowOverflowMessage(message) ? CONTEXT_OVERFLOW_ERROR_CODE : VALIDATION_ERROR_CODE;
|
|
135
137
|
return new ValidationStreamError(message, code);
|
|
136
138
|
}
|
|
137
139
|
current = current.cause;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/sfdx-agent-harness-mastra",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.67.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.68.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.68.0",
|
|
51
51
|
"@types/node": "^22.20.1",
|
|
52
52
|
"@vitest/coverage-istanbul": "^4.1.10",
|
|
53
53
|
"@vitest/eslint-plugin": "^1.6.27",
|