@salesforce/sfdx-agent-harness-mastra 0.13.0 → 0.15.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/README.md CHANGED
@@ -175,18 +175,19 @@ const harnessFactory = new MastraHarnessFactory({
175
175
  });
176
176
  ```
177
177
 
178
- When the timer fires, the harness aborts the suspended Mastra run, drops the `activeRuns` entry for the thread, and
179
- yields a synthetic terminal pair on the active `eventStream`:
178
+ When the timer fires, the harness aborts the suspended Mastra run via the per-`(agentId, threadId)`
179
+ `MastraApprovalCoordinator`, marks every emitted approval as auto-resolved, and yields a synthetic terminal pair on the
180
+ active `eventStream`:
180
181
 
181
182
  ```ts
182
183
  { type: 'error', error: Error('Tool approval timed out after 60000ms.'), code: 'tool-approval-timeout' }
183
184
  { type: 'finish', finishReason: 'error' }
184
185
  ```
185
186
 
186
- Subsequent `approveToolCall` / `declineToolCall` / `submitToolResult` calls for the timed-out tool reject with
187
- `no active run found for thread "<id>"`. The granularity is per thread — each suspended `ActiveRun` arms its own timer
188
- because Mastra suspends one tool approval at a time per thread; multi-tool turns get a fresh timer with each
189
- continuation. To observe the timeout, keep iterating the `eventStream` past the `tool-approval-request` event so the
187
+ Subsequent `approveToolCall` / `declineToolCall` / `submitToolResult` calls for the timed-out tool throw because the
188
+ coordinator has already auto-resolved them. The granularity is per `toolCallId` — each emitted approval arms its own
189
+ timer; the first one to fire tears down the whole turn (since Mastra can't continue the suspended run after timeout
190
+ anyway). To observe the timeout, keep iterating the `eventStream` past the `tool-approval-request` event so the
190
191
  synthetic terminal pair lands in your loop:
191
192
 
192
193
  ```ts
@@ -1,3 +1,4 @@
1
+ import type { LogBus } from '@salesforce/agentic-common';
1
2
  import { type LLMGatewayClient } from '@salesforce/llm-gateway-sdk';
2
3
  import type { MastraModelConfigFactory, ResolvedModelConfig } from './model-config-factory.js';
3
4
  /**
@@ -6,7 +7,10 @@ import type { MastraModelConfigFactory, ResolvedModelConfig } from './model-conf
6
7
  * Wraps the LLMGatewayClient in a SalesforceMastraModelGateway and calls
7
8
  * resolveLanguageModel() to produce the LanguageModelV2 that Mastra consumes.
8
9
  * Captures the gateway model at creation time to provide multimodal validation.
10
+ * Forwards the harness's `logBus` into the bridge so streaming anomalies
11
+ * (e.g. a Bedrock-Anthropic stream that ends without a usage chunk) surface as
12
+ * structured logs.
9
13
  */
10
14
  export declare class DefaultMastraModelConfigFactory implements MastraModelConfigFactory {
11
- create(llmGatewayClient: LLMGatewayClient): ResolvedModelConfig;
15
+ create(llmGatewayClient: LLMGatewayClient, logBus?: LogBus): ResolvedModelConfig;
12
16
  }
@@ -10,10 +10,13 @@ import { SalesforceMastraModelGateway } from './salesforce-llm-gateway.js';
10
10
  * Wraps the LLMGatewayClient in a SalesforceMastraModelGateway and calls
11
11
  * resolveLanguageModel() to produce the LanguageModelV2 that Mastra consumes.
12
12
  * Captures the gateway model at creation time to provide multimodal validation.
13
+ * Forwards the harness's `logBus` into the bridge so streaming anomalies
14
+ * (e.g. a Bedrock-Anthropic stream that ends without a usage chunk) surface as
15
+ * structured logs.
13
16
  */
14
17
  export class DefaultMastraModelConfigFactory {
15
- create(llmGatewayClient) {
16
- const agentGateway = new SalesforceMastraModelGateway(llmGatewayClient);
18
+ create(llmGatewayClient, logBus) {
19
+ const agentGateway = new SalesforceMastraModelGateway(llmGatewayClient, undefined, logBus);
17
20
  const model = agentGateway.resolveLanguageModel();
18
21
  const gatewayModel = llmGatewayClient.getModel();
19
22
  return {
@@ -1,23 +1,8 @@
1
1
  /**
2
- * Buffered async generator wrapper used by `MastraStreamBroker` to deliver
3
- * `ChatEvent`s to a consumer's `ChatStreamResult.eventStream`.
4
- *
5
- * The buffer holds events pushed before the consumer starts iterating, so the
6
- * broker can begin populating the sink eagerly without coordinating with the
7
- * consumer's `for await` loop. Events pushed after iteration starts unblock
8
- * the awaiting consumer via an internal Promise waiter.
9
- *
10
- * Internal to `@salesforce/sfdx-agent-harness-mastra`; not part of the public
11
- * harness surface. Mirrors the same primitive in `@salesforce/sfdx-agent-harness-claude`
12
- * to keep each harness's broker self-contained without forcing a shared
13
- * cross-package surface.
2
+ * Re-export of the `GenSink` primitive from the SDK's harness-implementation
3
+ * subpath (`@salesforce/sfdx-agent-sdk/harness`). See that surface for the
4
+ * single-iteration invariant; issue #529 is the rationale for the
5
+ * cross-harness lift and for the subpath split (consumer applications can no
6
+ * longer import `GenSink` from the bare specifier — they have no use for it).
14
7
  */
15
- export declare class GenSink<T> {
16
- private readonly buffered;
17
- private ended;
18
- private waiter;
19
- push(event: T): void;
20
- end(): void;
21
- generator(): AsyncGenerator<T, void>;
22
- private wake;
23
- }
8
+ export { GenSink } from '@salesforce/sfdx-agent-sdk/harness';
package/dist/gen-sink.js CHANGED
@@ -3,54 +3,11 @@
3
3
  * See LICENSE.txt for license terms.
4
4
  */
5
5
  /**
6
- * Buffered async generator wrapper used by `MastraStreamBroker` to deliver
7
- * `ChatEvent`s to a consumer's `ChatStreamResult.eventStream`.
8
- *
9
- * The buffer holds events pushed before the consumer starts iterating, so the
10
- * broker can begin populating the sink eagerly without coordinating with the
11
- * consumer's `for await` loop. Events pushed after iteration starts unblock
12
- * the awaiting consumer via an internal Promise waiter.
13
- *
14
- * Internal to `@salesforce/sfdx-agent-harness-mastra`; not part of the public
15
- * harness surface. Mirrors the same primitive in `@salesforce/sfdx-agent-harness-claude`
16
- * to keep each harness's broker self-contained without forcing a shared
17
- * cross-package surface.
6
+ * Re-export of the `GenSink` primitive from the SDK's harness-implementation
7
+ * subpath (`@salesforce/sfdx-agent-sdk/harness`). See that surface for the
8
+ * single-iteration invariant; issue #529 is the rationale for the
9
+ * cross-harness lift and for the subpath split (consumer applications can no
10
+ * longer import `GenSink` from the bare specifier — they have no use for it).
18
11
  */
19
- export class GenSink {
20
- buffered = [];
21
- ended = false;
22
- waiter = null;
23
- push(event) {
24
- if (this.ended)
25
- return;
26
- this.buffered.push(event);
27
- this.wake();
28
- }
29
- end() {
30
- if (this.ended)
31
- return;
32
- this.ended = true;
33
- this.wake();
34
- }
35
- async *generator() {
36
- while (true) {
37
- if (this.buffered.length > 0) {
38
- yield this.buffered.shift();
39
- continue;
40
- }
41
- if (this.ended)
42
- return;
43
- await new Promise((resolve) => {
44
- this.waiter = { resolve };
45
- });
46
- }
47
- }
48
- wake() {
49
- const w = this.waiter;
50
- if (w) {
51
- this.waiter = null;
52
- w.resolve();
53
- }
54
- }
55
- }
12
+ export { GenSink } from '@salesforce/sfdx-agent-sdk/harness';
56
13
  //# sourceMappingURL=gen-sink.js.map
@@ -1,3 +1,4 @@
1
+ import type { LogBus } from '@salesforce/agentic-common';
1
2
  import { type LLMGatewayClient } from '@salesforce/llm-gateway-sdk';
2
3
  import type { MastraModelConfigFactory, ResolvedModelConfig } from './model-config-factory.js';
3
4
  export interface LLMGExpressConfig {
@@ -21,5 +22,5 @@ export interface LLMGExpressConfig {
21
22
  export declare class LLMGExpressMastraModelConfigFactory implements MastraModelConfigFactory {
22
23
  private readonly config;
23
24
  constructor(config: LLMGExpressConfig);
24
- create(llmGatewayClient: LLMGatewayClient): ResolvedModelConfig;
25
+ create(llmGatewayClient: LLMGatewayClient, _logBus?: LogBus): ResolvedModelConfig;
25
26
  }
@@ -18,7 +18,9 @@ export class LLMGExpressMastraModelConfigFactory {
18
18
  constructor(config) {
19
19
  this.config = config;
20
20
  }
21
- create(llmGatewayClient) {
21
+ // _logBus is unused: the express path resolves through Mastra's built-in OpenAI-compatible
22
+ // provider, not SalesforceMastraModelGateway, so the missing-usage warn from W-22692131 doesn't apply.
23
+ create(llmGatewayClient, _logBus) {
22
24
  const model = {
23
25
  providerId: 'llmg-express',
24
26
  modelId: this.config.modelId,
@@ -0,0 +1,273 @@
1
+ import type { Agent } from '@mastra/core/agent';
2
+ import type { RequestContext } from '@mastra/core/request-context';
3
+ import type { ChunkType } from '@mastra/core/stream';
4
+ import { type ChatEvent, type McpToolAnnotations } from '@salesforce/sfdx-agent-sdk';
5
+ import { GenSink } from '@salesforce/sfdx-agent-sdk/harness';
6
+ import type { LogBus } from '@salesforce/agentic-common';
7
+ /**
8
+ * Output shape from `agent.stream()` / `agent.approveToolCall()` /
9
+ * `agent.declineToolCall()`. Captured as a structural type so the
10
+ * coordinator doesn't depend on Mastra's exact `StreamOutput` generic.
11
+ *
12
+ * Mastra's actual output also carries `textStream`; the coordinator doesn't
13
+ * need it (it derives text deltas from `fullStream` via the event adapter)
14
+ * and structural typing accepts the wider value at the call site.
15
+ */
16
+ export interface MastraStreamOutput {
17
+ runId: string;
18
+ fullStream: ReadableStream<ChunkType>;
19
+ }
20
+ /**
21
+ * Looks up MCP-server enrichment (`serverName` / `annotations`) for a given
22
+ * Mastra-namespaced tool name (`${serverName}_${toolName}`). Returns
23
+ * `undefined` for non-MCP tools (consumer-declared, workspace, built-in).
24
+ */
25
+ export type MastraMcpToolMetadataLookup = (namespacedToolName: string) => {
26
+ serverName: string;
27
+ annotations?: McpToolAnnotations;
28
+ } | undefined;
29
+ export interface MastraApprovalCoordinatorOptions {
30
+ agent: Agent;
31
+ threadId: string;
32
+ memory: {
33
+ thread: string;
34
+ resource: string;
35
+ };
36
+ requestContext: RequestContext;
37
+ toolApprovalTimeoutMs: number;
38
+ /**
39
+ * Approval-emission mode (#447). Defaults to `undefined` (no gating).
40
+ *
41
+ * - `undefined`: stream is run without `requireToolApproval`. Mastra
42
+ * suspends only on tools whose per-tool `requireApproval` is `true`
43
+ * (consumer tools post-2c are `false` so they don't suspend; the
44
+ * bridge's parker `execute` handles their lifecycle natively).
45
+ * - `'serial'`: stream runs with `requireToolApproval: true`. The
46
+ * coordinator emits one `tool-approval-request` per natural
47
+ * `tool-call-approval` chunk — Mastra serializes one approval per
48
+ * suspension.
49
+ * - `'batch'`: same as serial except `tool-approval-request` is
50
+ * front-loaded off the adapter's `tool-call` events so parallel
51
+ * `tool_use` batches all surface their approval-requests on the
52
+ * single per-turn stream.
53
+ *
54
+ * Note: in approval mode, consumer-executed tools still bypass the
55
+ * approval gate (per the `StreamOptions.requireToolApproval` JSDoc) —
56
+ * the coordinator silently calls `agent.approveToolCall(id)` for them
57
+ * without surfacing a `tool-approval-request` ChatEvent.
58
+ */
59
+ mode?: 'serial' | 'batch';
60
+ /**
61
+ * Set of tool names registered via `AgentConfig.tools`. The coordinator
62
+ * uses this to route `tool-call-approval` chunks: if the toolName is in
63
+ * the set, auto-approve silently (consumer tools bypass the gate); else
64
+ * surface a `tool-approval-request` ChatEvent and park.
65
+ *
66
+ * Empty set when no consumer tools are configured — every approval
67
+ * chunk surfaces to the consumer in approval mode.
68
+ */
69
+ consumerToolNames: ReadonlySet<string>;
70
+ /**
71
+ * Resolves MCP enrichment for a Mastra-namespaced tool name. Wired to
72
+ * `lookupMcpToolMetadata` in {@link MastraHarness} so the same lookup
73
+ * the legacy `createEventStream` used post-enriches `tool-call` /
74
+ * `tool-result` / `tool-approval-request` events.
75
+ */
76
+ lookupMcpToolMetadata?: MastraMcpToolMetadataLookup;
77
+ logBus?: LogBus;
78
+ }
79
+ /**
80
+ * Per-`(agentId, threadId)` coordinator that subsumes the pre-#529
81
+ * `MastraStreamBroker` + `activeRuns` pair into one object owning a single
82
+ * sink, single multi-source pump loop, and single set of teardown paths.
83
+ *
84
+ * Responsibilities:
85
+ * - Approval gating in `'serial'` / `'batch'` mode: emit
86
+ * `tool-approval-request` for non-consumer tools, drive
87
+ * `agent.approveToolCall(...)` / `declineToolCall(...)` when the
88
+ * consumer settles.
89
+ * - Consumer-tool plumbing: every `AgentConfig.tools` entry has
90
+ * `requireApproval: false` and a Promise-returning `execute` that parks
91
+ * on the coordinator's `parkConsumerToolCall(...)`. Mastra natively
92
+ * suspends the agentic loop while the Promise pends, so consumer tools
93
+ * don't go through `agent.approveToolCall(...)` in non-approval mode.
94
+ * In approval mode the coordinator auto-approves them silently so the
95
+ * bridge `execute` parker still runs on the resumed stream.
96
+ * - Multi-source pump: Mastra returns a fresh `fullStream` per resume
97
+ * call; the coordinator pumps each one into the same `initial` sink so
98
+ * the consumer iterates one stream per turn.
99
+ * - Snapshot-drain before each resume call (Mastra-specific: workflow
100
+ * snapshots persist asynchronously after `tool-call-approval`).
101
+ * - Approval-timeout / abort / dispose teardown — single helper drains
102
+ * timers, rejects pending parkers, marks emitted approvals as
103
+ * coordinator-settled, and pushes the synthetic terminal pair on the
104
+ * sink.
105
+ *
106
+ * Issue #529 contract: settle calls (`approveToolCall` / `declineToolCall` /
107
+ * `submitToolResult`) are control messages on the chat() turn's existing
108
+ * eventStream. There is exactly one sink (`initial`) per turn; post-resume
109
+ * events flow through the same sink the consumer is iterating from
110
+ * `chat()`. There are no per-tool sinks and no `lastGenSinkStale` flag.
111
+ */
112
+ export declare class MastraApprovalCoordinator {
113
+ private readonly agent;
114
+ private readonly memory;
115
+ private readonly requestContext;
116
+ private readonly toolApprovalTimeoutMs;
117
+ private readonly mode;
118
+ private readonly consumerToolNames;
119
+ private readonly lookupMcpToolMetadata?;
120
+ private readonly adapter;
121
+ /** Single per-turn sink. Issue #529 contract: one stream per turn. */
122
+ private readonly initial;
123
+ /** Buffered consumer decisions keyed by `toolCallId`. */
124
+ private readonly decisions;
125
+ /** ToolCallIds for which a `tool-approval-request` ChatEvent has been emitted. */
126
+ private readonly emittedApprovals;
127
+ /** ToolCallIds the consumer has settled. Rejects double-settle. */
128
+ private readonly consumerSettled;
129
+ /**
130
+ * ToolCallIds the coordinator auto-resolved (timeout / abort / dispose
131
+ * / consumer-tool auto-approve). A subsequent settle for these ids
132
+ * rejects with "already auto-resolved by the coordinator".
133
+ */
134
+ private readonly settledByCoordinator;
135
+ /** Per-toolCallId approval-timeout timers (armed when an approval is emitted). */
136
+ private readonly approvalTimers;
137
+ /** Args from the adapter's `tool-call` ChatEvent keyed by `toolCallId`. */
138
+ private readonly toolCallArgs;
139
+ /**
140
+ * Mastra's current `runId` after the latest stream/resume. Updated on
141
+ * each successful pump entry. Needed for the next
142
+ * `agent.approveToolCall(...)` / `declineToolCall(...)`.
143
+ */
144
+ private currentRunId;
145
+ /**
146
+ * The toolCallId Mastra is currently suspended on (after the natural
147
+ * `tool-call-approval` chunk). `null` when no suspension. The
148
+ * coordinator's pump loop reads this when the current `fullStream`
149
+ * closes to decide whether to drive a resume.
150
+ */
151
+ private currentSuspendId;
152
+ /** Consumer-tool: bound parkers, keyed by `toolCallId`. `submitToolResult` resolves these. */
153
+ private readonly pendingByToolCallId;
154
+ /**
155
+ * Consumer-tool: results submitted before the bridge `execute` handler
156
+ * parked. Keyed by `toolCallId`. Used when `submitToolResult` arrives
157
+ * during the gap between the `tool-call` ChatEvent and the bridge
158
+ * handler's actual invocation — the staged result resolves the next
159
+ * parker invocation for the same id.
160
+ */
161
+ private readonly preSubmittedByToolCallId;
162
+ /**
163
+ * Callbacks invoked exactly once during teardown (`abort` or `dispose`,
164
+ * whichever fires first, or natural pump exit).
165
+ */
166
+ private readonly teardownCallbacks;
167
+ /**
168
+ * Resolved by `recordDecision` (consumer settled) or `teardown`. The
169
+ * outer pump loop awaits this when it sees a suspension chunk before
170
+ * the consumer has settled. `'continued'` means a decision is now in
171
+ * `decisions`; `'done'` means the pump should exit (teardown fired).
172
+ */
173
+ private continuationResolver?;
174
+ private aborted;
175
+ private disposed;
176
+ private timedOut;
177
+ constructor(opts: MastraApprovalCoordinatorOptions);
178
+ initialSink(): GenSink<ChatEvent>;
179
+ onTeardown(callback: () => void): void;
180
+ private runTeardownCallbacks;
181
+ /**
182
+ * Begin pumping the initial Mastra `fullStream`. Stores `runId` and
183
+ * spawns the multi-source pump loop. The consumer iterates
184
+ * `initialSink().generator()` to observe events.
185
+ */
186
+ start(output: MastraStreamOutput): void;
187
+ abort(): void;
188
+ dispose(): void;
189
+ /**
190
+ * Single teardown path for abort / dispose / approval-timeout / pump-
191
+ * error. Drains timers, rejects pending consumer-tool parkers, marks
192
+ * every emitted approval as coordinator-settled, releases Mastra's
193
+ * suspended run, pushes the synthetic terminal pair on the sink, and
194
+ * fires teardown callbacks.
195
+ *
196
+ * The `reason` parameter only changes the sink's terminal shape:
197
+ * `'dispose'` pushes error-only (matches Claude's dispose contract);
198
+ * everything else pushes error+finish.
199
+ */
200
+ private teardown;
201
+ approve(toolCallId: string): void;
202
+ decline(toolCallId: string): void;
203
+ private recordDecision;
204
+ /**
205
+ * Parker callback the consumer-tool bridge `execute` handler invokes
206
+ * when Mastra calls the tool. Returns a Promise the consumer settles
207
+ * via {@link submitToolResult}.
208
+ *
209
+ * Mastra's bridge `execute` knows the `toolCallId` from its execution
210
+ * context, so binding is direct (no FIFO match by name). The
211
+ * pre-submit ladder handles the rare case where `submitToolResult`
212
+ * lands before the bridge handler's `execute` actually runs:
213
+ *
214
+ * 1. A pre-submitted result is staged for this `toolCallId` →
215
+ * resolve immediately.
216
+ * 2. Otherwise → bind under `toolCallId`; the eventual
217
+ * `submitToolResult` resolves us.
218
+ */
219
+ readonly parkConsumerToolCall: (toolCallId: string) => Promise<{
220
+ result: unknown;
221
+ isError?: boolean;
222
+ }>;
223
+ /**
224
+ * Settle a parked consumer-tool call. Issue #529 contract: returns
225
+ * `void`; post-resume events flow through the same `initial` sink the
226
+ * consumer is iterating from `chat()`. Mastra natively continues the
227
+ * agentic loop on the same `fullStream` once the parker Promise
228
+ * resolves (probe-validated, see #529 step 2c).
229
+ */
230
+ submitToolResult(toolCallId: string, result: unknown, isError?: boolean): void;
231
+ /**
232
+ * Multi-source pump loop. Drains the initial Mastra `fullStream` plus
233
+ * every continuation stream returned by `agent.approveToolCall(...)` /
234
+ * `declineToolCall(...)` into the same `initial` sink.
235
+ *
236
+ * Termination conditions:
237
+ * - Saw `finish` chunk on a stream and no further suspension awaits a
238
+ * decision.
239
+ * - Saw an `error` chunk (terminal under SDK contract).
240
+ * - `abort()` / `dispose()` / approval-timeout fired.
241
+ */
242
+ private runPumpLoop;
243
+ /**
244
+ * Pump one `ReadableStream<ChunkType>` into the sink until one of:
245
+ * - `'terminal'`: saw `finish` chunk; sink ended.
246
+ * - `'suspended'`: saw `tool-call-approval` chunk on a non-consumer
247
+ * tool (or auto-approved consumer tool didn't suspend on it). The
248
+ * stream may still be pumping — drain remaining chunks for the
249
+ * Mastra-snapshot-persistence guarantee, then return.
250
+ * - `'pump-error'`: stream threw; sink ended with error+finish.
251
+ */
252
+ private pumpUntilSuspendOrTerminal;
253
+ /**
254
+ * Drive a resume call and set up the next stream for the pump loop.
255
+ * Returns `'done'` if the resume rejected (turn over, error already
256
+ * pushed) or the new `fullStream` to pump next.
257
+ */
258
+ private driveResumeReturning;
259
+ private awaitContinuation;
260
+ /**
261
+ * Push an event onto the sink with MCP-server enrichment applied (when
262
+ * the toolName resolves to a known MCP catalog entry). Mirrors the
263
+ * pre-#529 `createEventStream`'s post-enrichment of `tool-call` /
264
+ * `tool-result` / `tool-approval-request`.
265
+ */
266
+ private routeEnriched;
267
+ private maybeEnrich;
268
+ private emitApprovalRequest;
269
+ private armApprovalTimer;
270
+ private cancelTimer;
271
+ private cancelAllTimers;
272
+ private handleApprovalTimeout;
273
+ }