@salesforce/sfdx-agent-harness-mastra 0.8.0 → 0.10.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
@@ -24,6 +24,24 @@ const manager = await createAgentManager('/path/to/storage', new MastraHarnessFa
24
24
  See the [`@salesforce/sfdx-agent-sdk` README](../sfdx-agent-sdk/README.md) for the full consumer-facing API. For
25
25
  internal architecture, see [ARCHITECTURE.md](ARCHITECTURE.md).
26
26
 
27
+ ### Removing the storage directory on Windows
28
+
29
+ `manager.shutdown()` closes the libsql client (forcedotcom/agentic-dx#429), but the underlying SQLite/WAL handle release
30
+ on Windows is asynchronous at the OS level. After `shutdown()` returns, Windows can take anywhere from ~60 ms to several
31
+ seconds to actually release the file lock — so a naïve `fs.rm(storageRoot)` immediately afterward will fail with
32
+ `EBUSY`. Pass `maxRetries` and `retryDelay` so Node retries until the OS lets go:
33
+
34
+ ```ts
35
+ import { rm } from 'node:fs/promises';
36
+
37
+ await manager.shutdown();
38
+ await rm(storageRoot, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
39
+ ```
40
+
41
+ Linux and macOS release the handle eagerly; the retry options are no-ops on those platforms. Tracking upstream at
42
+ [mastra-ai/mastra#17285](https://github.com/mastra-ai/mastra/issues/17285) — when `LibSQLStore` exposes a public
43
+ `close()` that includes a WAL checkpoint, this delay should shrink or disappear.
44
+
27
45
  ## Public API
28
46
 
29
47
  ### Core
@@ -2,7 +2,7 @@ import { Agent } from '@mastra/core/agent';
2
2
  import { SkillSearchProcessor, ToolSearchProcessor } from '@mastra/core/processors';
3
3
  import type { ChunkType } from '@mastra/core/stream';
4
4
  import { MCPClient } from '@mastra/mcp';
5
- import { type AgentHarness, type WithAgentConfig, type ChatStreamResult, type HarnessAgentConfig, type MCPConfiguration, type McpServerInfo, type Message, type StreamOptions, type TelemetryEventCallback, type ToolResultInfo } from '@salesforce/sfdx-agent-sdk';
5
+ import { McpServerStatus, type AgentHarness, type WithAgentConfig, type ChatStreamResult, type HarnessAgentConfig, type MCPConfiguration, type McpServerErrorDetail, type McpServerInfo, type Message, type StreamOptions, type TelemetryEventCallback, type ToolResultInfo } from '@salesforce/sfdx-agent-sdk';
6
6
  import type { MastraModelConfigFactory } from './model-config-factory.js';
7
7
  import { type MastraAgentConfig, type SkillSearchHandle, type ToolSearchHandle } from './tool-search.js';
8
8
  import type { LLMGatewayClient } from '@salesforce/llm-gateway-sdk';
@@ -20,10 +20,19 @@ import { Workspace } from '@mastra/core/workspace';
20
20
  * if the consumer breaks out of the event stream early (common in
21
21
  * approval UIs), we must finish reading the stream to ensure the
22
22
  * snapshot is persisted before we attempt to resume.
23
+ *
24
+ * `disposeAbortListener` is invoked whenever the entry is replaced or
25
+ * removed so the per-run abort listener (installed by `stream()` against
26
+ * the caller's `streamOptions.abortSignal`) cannot fire on a run that is
27
+ * no longer the active one for the thread. Mirrors the SDK invariant
28
+ * "every chat turn ends with exactly one terminal `FinishEvent`" by
29
+ * ensuring we don't accidentally abort a continuation run that
30
+ * `approveToolCall` / `declineToolCall` has already authorized.
23
31
  */
24
32
  interface ActiveRun {
25
33
  runId: string;
26
34
  fullStream: ReadableStream<ChunkType>;
35
+ disposeAbortListener?: () => void;
27
36
  }
28
37
  /**
29
38
  * Per-server MCP state. Each enabled server gets its own MCPClient and
@@ -37,8 +46,27 @@ interface ActiveRun {
37
46
  */
38
47
  interface McpServerState {
39
48
  client: MCPClient;
49
+ /**
50
+ * Last `McpServerStatus` value emitted (or seeded to `Connecting` at
51
+ * construction). Updated synchronously by `emitMcpStatusChange()` so a
52
+ * follow-up `getMcpServerInfo()` call observes the world the most recent
53
+ * `mcp-server-status-changed` event described.
54
+ */
55
+ currentStatus: McpServerStatus;
40
56
  epoch: number;
57
+ /**
58
+ * Sanitized human-readable error message. V8 stack frames and file paths
59
+ * have been stripped — safe for UI banners and telemetry tags. The full
60
+ * error (with `.stack`) is preserved on the `mcp-server-discovery-failed`
61
+ * event's `error` field.
62
+ */
41
63
  error?: string;
64
+ /**
65
+ * Structured failure projection. Mirrors the SDK's `McpServerErrorDetail`
66
+ * shape so consumers can route on category / code / retriable. Populated
67
+ * alongside `error`.
68
+ */
69
+ errorDetail?: McpServerErrorDetail;
42
70
  ready: Promise<void>;
43
71
  settled: boolean;
44
72
  tools: ToolsInput;
@@ -119,10 +147,18 @@ export declare class MastraHarness implements MastraAgentHarness {
119
147
  * would mix process state.
120
148
  */
121
149
  protected readonly activeAgents: Map<string, MastraAgentState>;
150
+ private readonly libsqlClient;
122
151
  private readonly storage;
123
152
  private readonly memory;
124
153
  private readonly mastra;
125
154
  private readonly buses;
155
+ /**
156
+ * Tracks the in-flight storage init so {@link shutdown} can settle it
157
+ * before closing the libsql client. Without this, Mastra's lazy storage
158
+ * init can fire after `client.close()` and surface as an unhandled
159
+ * `CLIENT_CLOSED` rejection (#429 follow-up).
160
+ */
161
+ private readonly storageReady;
126
162
  /**
127
163
  * Mastra-specific extension surface, typed off `MastraAgentHarness`. The
128
164
  * SDK never reads this; consumers reach through `manager.extensions.mastra`.
@@ -134,6 +170,9 @@ export declare class MastraHarness implements MastraAgentHarness {
134
170
  * - MUST disconnect the mcpClient for every active agent before clearing state.
135
171
  * - MUST clear the `activeAgents` map.
136
172
  * - MUST shut down the global `this.mastra` orchestrator.
173
+ * - MUST close the libsql client so the underlying SQLite handle is released
174
+ * (Windows blocks fs.rm of the storage dir while the WAL/shm files are open;
175
+ * see forcedotcom/agentic-dx#429).
137
176
  */
138
177
  shutdown(): Promise<void>;
139
178
  onTelemetry(callback: TelemetryEventCallback): Unsubscribe;
@@ -250,6 +289,10 @@ export declare class MastraHarness implements MastraAgentHarness {
250
289
  * - MUST return a `ChatStreamResult` with `textStream` and `eventStream`.
251
290
  * - On pre-stream rejection from `agent.stream()`, MUST re-throw without emitting telemetry —
252
291
  * the SDK's chat-session boundary catches the rejection and emits `chat-stream-error`.
292
+ * - MUST install a per-run abort listener on `options.abortSignal` so that aborting
293
+ * while a tool-approval is pending tears down the suspended Mastra run and clears
294
+ * the thread's `ActiveRun` entry. See ARCHITECTURE.md → "Tool Approval Flow → Abort
295
+ * during pending approval (#409)".
253
296
  */
254
297
  stream(agentId: string, threadId: string, message: string, options?: StreamOptions): Promise<ChatStreamResult>;
255
298
  /**
@@ -343,6 +386,25 @@ export declare class MastraHarness implements MastraAgentHarness {
343
386
  */
344
387
  private splitIntoChunks;
345
388
  private getAgentStateOrThrow;
389
+ /**
390
+ * Synchronously flips `serverState.currentStatus` (no-op when the next
391
+ * status equals the current) and returns a closure that, when invoked,
392
+ * emits the corresponding `mcp-server-status-changed` event. Returns
393
+ * `undefined` when the status didn't change — the caller has nothing
394
+ * to emit.
395
+ *
396
+ * Splitting the state flip from the emit lets callers update internal
397
+ * state synchronously alongside `settled` (so a synchronous listener
398
+ * reading `getMcpServerInfo()` from inside the discovery telemetry
399
+ * callback observes the post-settle world per issue #373) while still
400
+ * deferring the status-change emit until after the discovery telemetry
401
+ * has fired.
402
+ *
403
+ * The returned closure swallows `DISPOSED` errors the same way the
404
+ * discovery emit sites do: background discovery callbacks can resolve
405
+ * after `shutdown()` has disposed the buses.
406
+ */
407
+ private prepareMcpStatusChange;
346
408
  /**
347
409
  * Runs MCP discovery for one server: emits the
348
410
  * `mcp-server-discovery-started` / `…-completed` / `…-failed` telemetry
@@ -358,11 +420,43 @@ export declare class MastraHarness implements MastraAgentHarness {
358
420
  * Errors observed during discovery are recorded on the entry's `error`
359
421
  * field; the returned promise resolves either way (it only rejects when
360
422
  * an aborted operation throws after we have already cleaned up). The
361
- * `settled` flag is pinned by `.finally()` so `getMcpServerInfo()` can
362
- * tell `Connecting` from `Connected`/`Error`.
423
+ * `settled` flag is pinned BEFORE the terminal telemetry emit so a
424
+ * synchronous listener calling `getMcpServerInfo()` from inside the
425
+ * `mcp-server-discovery-{completed,failed}` callback observes the same
426
+ * world the event described — see issue #373.
363
427
  */
364
428
  private runMcpDiscovery;
365
429
  private toChatStreamResult;
430
+ /**
431
+ * Replace (or set) the active run for a thread, disposing any prior
432
+ * entry's per-run abort listener so the prior signal cannot fire on the
433
+ * new run.
434
+ */
435
+ private replaceActiveRun;
436
+ /**
437
+ * Remove the active run for a thread, disposing its per-run abort listener
438
+ * if present. Safe to call when no entry exists.
439
+ */
440
+ private removeActiveRun;
441
+ /**
442
+ * Wire `abortSignal.abort()` to tear down the suspended Mastra run for
443
+ * `threadId`. Returns a disposer that removes the listener; callers must
444
+ * invoke it whenever the entry is replaced or removed so a stale signal
445
+ * cannot abort an unrelated continuation run.
446
+ *
447
+ * Identity guard: the listener checks `agentState.activeRuns.get(threadId) === run`
448
+ * before calling `abortRunStream`, so a defensive race with another
449
+ * `stream()` call on the same thread cannot double-abort.
450
+ *
451
+ * Closes #409 — without this listener, aborting while a tool-approval is
452
+ * pending leaves Mastra's prepared run alive and `activeRuns[threadId]`
453
+ * pointing at a runId the consumer can no longer settle, so subsequent
454
+ * `approveToolCall` reaches Mastra and surfaces a confusing
455
+ * "could not find a suspended run" error instead of the SDK-level
456
+ * "no active run found" guard.
457
+ */
458
+ private installPerRunAbortListener;
459
+ private handlePerRunAbort;
366
460
  private getMemoryOrThrow;
367
461
  private agentResourceId;
368
462
  /**
@@ -10,6 +10,7 @@ import { Memory } from '@mastra/memory';
10
10
  import { SkillSearchProcessor, ToolSearchProcessor } from '@mastra/core/processors';
11
11
  import { MASTRA_RESOURCE_ID_KEY, MASTRA_THREAD_ID_KEY, RequestContext } from '@mastra/core/request-context';
12
12
  import { LibSQLStore } from '@mastra/libsql';
13
+ import { createClient } from '@libsql/client';
13
14
  import { MCPClient } from '@mastra/mcp';
14
15
  import { AgentSDKError, AgentSDKErrorType, DEFAULT_MAX_STEPS, HarnessBusOwner, McpServerStatus, } from '@salesforce/sfdx-agent-sdk';
15
16
  import { MastraEventAdapter } from './mastra-event-adapter.js';
@@ -17,6 +18,7 @@ import { mapMastraToMessage, mapMessageToMastra, partToTranscriptText } from './
17
18
  import { isStandardSchemaWithJSON, standardSchemaToJSONSchema } from '@mastra/schema-compat';
18
19
  import { mapToolDefinitionsToMastraTools } from './mastra-tool-mapper.js';
19
20
  import { hasEnabledServers, mapToMastraMCPServers } from './mcp-config-mapper.js';
21
+ import { buildMcpErrorDetail, sanitizeMcpErrorMessage } from './mcp-error-classifier.js';
20
22
  import { DefaultMastraModelConfigFactory } from './default-model-config-factory.js';
21
23
  import { partitionPool, } from './tool-search.js';
22
24
  import { buildSummaryPrompt, Clock, getErrorMessage, RealClock, splitFrontmatterAndBody, UUIDGenerator, } from '@salesforce/agentic-common';
@@ -311,10 +313,18 @@ export class MastraHarness {
311
313
  * would mix process state.
312
314
  */
313
315
  activeAgents = new Map();
316
+ libsqlClient;
314
317
  storage;
315
318
  memory;
316
319
  mastra;
317
320
  buses = new HarnessBusOwner();
321
+ /**
322
+ * Tracks the in-flight storage init so {@link shutdown} can settle it
323
+ * before closing the libsql client. Without this, Mastra's lazy storage
324
+ * init can fire after `client.close()` and surface as an unhandled
325
+ * `CLIENT_CLOSED` rejection (#429 follow-up).
326
+ */
327
+ storageReady;
318
328
  /**
319
329
  * Mastra-specific extension surface, typed off `MastraAgentHarness`. The
320
330
  * SDK never reads this; consumers reach through `manager.extensions.mastra`.
@@ -338,9 +348,24 @@ export class MastraHarness {
338
348
  this.modelConfigFactory = modelConfigFactory;
339
349
  this.clock = clock;
340
350
  this.idGenerator = idGenerator;
351
+ // Construct the libsql Client ourselves and inject it into LibSQLStore
352
+ // via the `{ client }` constructor form. We keep our own reference so
353
+ // shutdown() can call client.close() — Mastra's shutdown() does not
354
+ // close the storage, and LibSQLStore declares `client` as private, so
355
+ // we cannot reach in. Without an explicit close, the SQLite WAL/shm
356
+ // handles stay open on Windows and block fs.rm of the storage dir
357
+ // (forcedotcom/agentic-dx#429).
358
+ //
359
+ // Upstream tracking: https://github.com/mastra-ai/mastra/issues/17285
360
+ // — when LibSQLStore exposes a public close(), revert to the simpler
361
+ // `new LibSQLStore({ id, url })` form and call store.close() in
362
+ // shutdown().
363
+ this.libsqlClient = createClient({
364
+ url: `file:${this.storageRootFolder}/mastra.db`,
365
+ });
341
366
  this.storage = new LibSQLStore({
342
367
  id: 'mastra-storage',
343
- url: `file:${this.storageRootFolder}/mastra.db`,
368
+ client: this.libsqlClient,
344
369
  });
345
370
  this.memory = new Memory({
346
371
  storage: this.storage,
@@ -357,15 +382,30 @@ export class MastraHarness {
357
382
  storage: this.storage,
358
383
  memory: { shared: this.memory },
359
384
  });
385
+ // Eagerly run storage init so Mastra's augment-with-init proxy caches
386
+ // the result. Without this, any lazy storage call deferred until after
387
+ // shutdown() (e.g. background tool/agent registration paths in Mastra)
388
+ // races our libsqlClient.close() and surfaces as an unhandled
389
+ // CLIENT_CLOSED rejection. We retain the promise so shutdown() can
390
+ // settle it before closing the client (a swallowed rejection here is
391
+ // fine — Mastra retains its own init proxy and surfaces real init
392
+ // errors at the next storage call site).
393
+ this.storageReady = this.storage.init().catch(() => undefined);
360
394
  }
361
395
  /**
362
396
  * @requirements
363
397
  * - MUST disconnect the mcpClient for every active agent before clearing state.
364
398
  * - MUST clear the `activeAgents` map.
365
399
  * - MUST shut down the global `this.mastra` orchestrator.
400
+ * - MUST close the libsql client so the underlying SQLite handle is released
401
+ * (Windows blocks fs.rm of the storage dir while the WAL/shm files are open;
402
+ * see forcedotcom/agentic-dx#429).
366
403
  */
367
404
  async shutdown() {
368
405
  await Promise.all([...this.activeAgents.values()].map(async (state) => {
406
+ for (const run of state.activeRuns.values())
407
+ run.disposeAbortListener?.();
408
+ state.activeRuns.clear();
369
409
  await Promise.allSettled(Array.from(state.mcpServers.values()).map(async (s) => {
370
410
  await s.ready.catch(() => undefined);
371
411
  await s.client.disconnect();
@@ -374,6 +414,29 @@ export class MastraHarness {
374
414
  }));
375
415
  this.activeAgents.clear();
376
416
  await this.mastra?.shutdown();
417
+ // Settle the constructor's eager init before closing — see the
418
+ // `storageReady` field comment for the why.
419
+ await this.storageReady;
420
+ // Fold the WAL into the main DB and switch journal_mode=DELETE
421
+ // so SQLite unlinks the -wal/-shm sidecars on close. Without this,
422
+ // Windows holds the sidecars open well past `client.close()` and
423
+ // racing `fs.rm` calls (e.g. vitest's per-test cleanup) crash the
424
+ // worker. Mirrors the fix Mastra shipped on `LibSQLStore.close()`
425
+ // in `@mastra/libsql@1.12.0-alpha.0`. Replace this block with a
426
+ // call to `storage.close()` once that release ships stable — see
427
+ // https://github.com/forcedotcom/agentic-dx/issues/475.
428
+ try {
429
+ await this.libsqlClient.execute('PRAGMA wal_checkpoint(TRUNCATE);');
430
+ await this.libsqlClient.execute('PRAGMA journal_mode=DELETE;');
431
+ }
432
+ catch (err) {
433
+ // Best-effort: still close even if checkpoint fails (e.g. the
434
+ // client was already closed by a prior shutdown path).
435
+ this.buses.getLogBus()?.warn('libsql WAL checkpoint failed before close', {
436
+ error: getErrorMessage(err),
437
+ });
438
+ }
439
+ this.libsqlClient.close();
377
440
  this.buses.dispose();
378
441
  }
379
442
  onTelemetry(callback) {
@@ -424,6 +487,7 @@ export class MastraHarness {
424
487
  // after the synchronous `set` and `ready` assignment complete.
425
488
  const entry = {
426
489
  client,
490
+ currentStatus: McpServerStatus.Connecting,
427
491
  epoch: 0,
428
492
  ready: Promise.resolve(),
429
493
  settled: false,
@@ -556,6 +620,9 @@ export class MastraHarness {
556
620
  */
557
621
  async destroyAgent(agentId) {
558
622
  const state = this.getAgentStateOrThrow(agentId);
623
+ for (const run of state.activeRuns.values())
624
+ run.disposeAbortListener?.();
625
+ state.activeRuns.clear();
559
626
  await Promise.allSettled(Array.from(state.mcpServers.values()).map(async (s) => {
560
627
  await s.ready.catch(() => undefined);
561
628
  await s.client.disconnect();
@@ -576,19 +643,35 @@ export class MastraHarness {
576
643
  const state = this.getAgentStateOrThrow(agentId);
577
644
  const results = [];
578
645
  for (const [name, serverState] of state.mcpServers) {
579
- if (!serverState.settled) {
580
- results.push({ name, status: McpServerStatus.Connecting, tools: [] });
581
- }
582
- else if (serverState.error) {
583
- results.push({ error: serverState.error, name, status: McpServerStatus.Error, tools: [] });
584
- }
585
- else {
646
+ // `currentStatus` is the source of truth: it tracks `Reconnecting`
647
+ // (transient state during a reconnect on a previously-Connected
648
+ // server) which the `settled`/`error` pair cannot represent.
649
+ // `settled` still gates whether tools are visible — Mastra's
650
+ // discovery has resolved at most once per epoch.
651
+ const status = serverState.currentStatus;
652
+ if (status === McpServerStatus.Connected && serverState.settled) {
586
653
  results.push({
587
654
  name,
588
655
  status: McpServerStatus.Connected,
589
656
  tools: mapMastraToolsToMcpToolInfo(serverState.tools, this.buses.getLogBus()),
590
657
  });
591
658
  }
659
+ else if (status === McpServerStatus.Error && serverState.settled) {
660
+ const info = {
661
+ error: serverState.error,
662
+ name,
663
+ status: McpServerStatus.Error,
664
+ tools: [],
665
+ };
666
+ if (serverState.errorDetail)
667
+ info.errorDetail = serverState.errorDetail;
668
+ results.push(info);
669
+ }
670
+ else {
671
+ // Connecting (initial), Reconnecting (mid-cycle), or any
672
+ // status whose discovery has not yet settled.
673
+ results.push({ name, status, tools: [] });
674
+ }
592
675
  }
593
676
  if (state.mcpConfig) {
594
677
  for (const [name, serverConfig] of Object.entries(state.mcpConfig)) {
@@ -627,6 +710,17 @@ export class MastraHarness {
627
710
  serverState.error = undefined;
628
711
  serverState.tools = {};
629
712
  serverState.settled = false;
713
+ // Status transition before transport work begins. A reconnect on a
714
+ // previously-`Connected` server lands in `Reconnecting` (transport is
715
+ // being cycled); reconnects from `Error` are a recovery path and use
716
+ // the regular `Connecting` placeholder so the (prev,next) pair on
717
+ // success reads `Connecting → Connected` like an initial discovery.
718
+ // Concurrent reconnects (already `Reconnecting`) coalesce via the
719
+ // emit-helper's same-status no-op.
720
+ const transientStatus = serverState.currentStatus === McpServerStatus.Connected
721
+ ? McpServerStatus.Reconnecting
722
+ : McpServerStatus.Connecting;
723
+ this.prepareMcpStatusChange(agentId, serverName, serverState, transientStatus)?.();
630
724
  // Emit `started` BEFORE awaiting the transport reconnect so subscribers
631
725
  // see started → (completed | failed) timestamps in chronological order.
632
726
  // The previous shape had `started` emitted from the catch block AFTER
@@ -650,9 +744,12 @@ export class MastraHarness {
650
744
  // Drop telemetry + state writes if a newer epoch has superseded us.
651
745
  if (state.mcpServers.get(serverName)?.epoch !== epoch)
652
746
  return;
653
- const message = err instanceof Error ? err.message : String(err);
747
+ const message = sanitizeMcpErrorMessage(err);
748
+ const errorDetail = buildMcpErrorDetail(err);
654
749
  serverState.error = message;
750
+ serverState.errorDetail = errorDetail;
655
751
  serverState.settled = true;
752
+ const emitStatusChange = this.prepareMcpStatusChange(agentId, serverName, serverState, McpServerStatus.Error, message);
656
753
  this.buses.getLogBus()?.warn('MCP server reconnect failed', {
657
754
  serverName,
658
755
  error: message,
@@ -665,12 +762,14 @@ export class MastraHarness {
665
762
  serverName,
666
763
  durationMs: this.clock.now().getTime() - startTime,
667
764
  error: err instanceof Error ? err : new Error(message),
765
+ errorDetail,
668
766
  });
669
767
  }
670
768
  catch (emitErr) {
671
769
  if (!isDisposedError(emitErr))
672
770
  throw emitErr;
673
771
  }
772
+ emitStatusChange?.();
674
773
  return;
675
774
  }
676
775
  // Pass the existing startTime so runMcpDiscovery does NOT emit its own
@@ -703,7 +802,7 @@ export class MastraHarness {
703
802
  * - MUST remove any active run tracked for this thread to prevent stale references.
704
803
  */
705
804
  async destroyThread(agentId, threadId) {
706
- this.getAgentStateOrThrow(agentId).activeRuns.delete(threadId);
805
+ this.removeActiveRun(this.getAgentStateOrThrow(agentId), threadId);
707
806
  const memory = await this.getMemoryOrThrow(agentId);
708
807
  await memory.deleteThread(threadId);
709
808
  }
@@ -830,6 +929,10 @@ export class MastraHarness {
830
929
  * - MUST return a `ChatStreamResult` with `textStream` and `eventStream`.
831
930
  * - On pre-stream rejection from `agent.stream()`, MUST re-throw without emitting telemetry —
832
931
  * the SDK's chat-session boundary catches the rejection and emits `chat-stream-error`.
932
+ * - MUST install a per-run abort listener on `options.abortSignal` so that aborting
933
+ * while a tool-approval is pending tears down the suspended Mastra run and clears
934
+ * the thread's `ActiveRun` entry. See ARCHITECTURE.md → "Tool Approval Flow → Abort
935
+ * during pending approval (#409)".
833
936
  */
834
937
  async stream(agentId, threadId, message, options) {
835
938
  const agentState = this.getAgentStateOrThrow(agentId);
@@ -843,7 +946,7 @@ export class MastraHarness {
843
946
  abortSignal: options?.abortSignal,
844
947
  requireToolApproval: options?.requireToolApproval,
845
948
  });
846
- return this.toChatStreamResult(agentState, threadId, output);
949
+ return this.toChatStreamResult(agentState, threadId, output, options?.abortSignal);
847
950
  }
848
951
  // ── Tool Execution ───────────────────────────────────────────────
849
952
  /**
@@ -1080,6 +1183,47 @@ export class MastraHarness {
1080
1183
  }
1081
1184
  return state;
1082
1185
  }
1186
+ /**
1187
+ * Synchronously flips `serverState.currentStatus` (no-op when the next
1188
+ * status equals the current) and returns a closure that, when invoked,
1189
+ * emits the corresponding `mcp-server-status-changed` event. Returns
1190
+ * `undefined` when the status didn't change — the caller has nothing
1191
+ * to emit.
1192
+ *
1193
+ * Splitting the state flip from the emit lets callers update internal
1194
+ * state synchronously alongside `settled` (so a synchronous listener
1195
+ * reading `getMcpServerInfo()` from inside the discovery telemetry
1196
+ * callback observes the post-settle world per issue #373) while still
1197
+ * deferring the status-change emit until after the discovery telemetry
1198
+ * has fired.
1199
+ *
1200
+ * The returned closure swallows `DISPOSED` errors the same way the
1201
+ * discovery emit sites do: background discovery callbacks can resolve
1202
+ * after `shutdown()` has disposed the buses.
1203
+ */
1204
+ prepareMcpStatusChange(agentId, serverName, serverState, nextStatus, error) {
1205
+ const previousStatus = serverState.currentStatus;
1206
+ if (previousStatus === nextStatus)
1207
+ return undefined;
1208
+ serverState.currentStatus = nextStatus;
1209
+ return () => {
1210
+ try {
1211
+ this.buses.emitTelemetry({
1212
+ type: 'mcp-server-status-changed',
1213
+ timestamp: this.clock.now(),
1214
+ agentId,
1215
+ serverName,
1216
+ previousStatus,
1217
+ nextStatus,
1218
+ ...(error !== undefined ? { error } : {}),
1219
+ });
1220
+ }
1221
+ catch (emitErr) {
1222
+ if (!isDisposedError(emitErr))
1223
+ throw emitErr;
1224
+ }
1225
+ };
1226
+ }
1083
1227
  /**
1084
1228
  * Runs MCP discovery for one server: emits the
1085
1229
  * `mcp-server-discovery-started` / `…-completed` / `…-failed` telemetry
@@ -1095,8 +1239,10 @@ export class MastraHarness {
1095
1239
  * Errors observed during discovery are recorded on the entry's `error`
1096
1240
  * field; the returned promise resolves either way (it only rejects when
1097
1241
  * an aborted operation throws after we have already cleaned up). The
1098
- * `settled` flag is pinned by `.finally()` so `getMcpServerInfo()` can
1099
- * tell `Connecting` from `Connected`/`Error`.
1242
+ * `settled` flag is pinned BEFORE the terminal telemetry emit so a
1243
+ * synchronous listener calling `getMcpServerInfo()` from inside the
1244
+ * `mcp-server-discovery-{completed,failed}` callback observes the same
1245
+ * world the event described — see issue #373.
1100
1246
  */
1101
1247
  runMcpDiscovery(agentId, serverName, client, serverStates, epoch, abortSignal,
1102
1248
  // When set, reuse the caller's startTime and skip emitting `started` —
@@ -1135,7 +1281,13 @@ export class MastraHarness {
1135
1281
  const s = currentEntry();
1136
1282
  if (!s)
1137
1283
  return; // superseded — drop write + telemetry
1284
+ // Flip `settled` AND `currentStatus` BEFORE emitting any
1285
+ // terminal telemetry so a synchronous listener calling
1286
+ // getMcpServerInfo() from inside the `completed` callback
1287
+ // observes the same world the event described (issue #373).
1138
1288
  s.tools = namespacedTools;
1289
+ s.settled = true;
1290
+ const emitStatusChange = this.prepareMcpStatusChange(agentId, serverName, s, McpServerStatus.Connected);
1139
1291
  // listToolsetsWithErrors() runs in the background and can resolve
1140
1292
  // after `shutdown()` has disposed the buses. Swallow only `DISPOSED`
1141
1293
  // so a real listener exception still surfaces.
@@ -1153,6 +1305,7 @@ export class MastraHarness {
1153
1305
  if (!isDisposedError(emitErr))
1154
1306
  throw emitErr;
1155
1307
  }
1308
+ emitStatusChange?.();
1156
1309
  })
1157
1310
  .catch(async (err) => {
1158
1311
  if (abortSignal?.aborted) {
@@ -1164,10 +1317,18 @@ export class MastraHarness {
1164
1317
  const s = currentEntry();
1165
1318
  if (!s)
1166
1319
  return; // superseded — drop write + telemetry
1167
- s.error = err instanceof Error ? err.message : String(err);
1320
+ // Flip `settled` AND `currentStatus` BEFORE emitting any
1321
+ // terminal telemetry — see the matching comment in the
1322
+ // success path above (issue #373).
1323
+ const errorMessage = sanitizeMcpErrorMessage(err);
1324
+ const errorDetail = buildMcpErrorDetail(err);
1325
+ s.error = errorMessage;
1326
+ s.errorDetail = errorDetail;
1327
+ s.settled = true;
1328
+ const emitStatusChange = this.prepareMcpStatusChange(agentId, serverName, s, McpServerStatus.Error, errorMessage);
1168
1329
  this.buses.getLogBus()?.warn('MCP server discovery failed', {
1169
1330
  serverName,
1170
- error: err instanceof Error ? err.message : String(err),
1331
+ error: errorMessage,
1171
1332
  });
1172
1333
  try {
1173
1334
  this.buses.emitTelemetry({
@@ -1177,33 +1338,117 @@ export class MastraHarness {
1177
1338
  serverName,
1178
1339
  durationMs: this.clock.now().getTime() - startTime,
1179
1340
  error: err instanceof Error ? err : new Error(String(err)),
1341
+ errorDetail,
1180
1342
  });
1181
1343
  }
1182
1344
  catch (emitErr) {
1183
1345
  if (!isDisposedError(emitErr))
1184
1346
  throw emitErr;
1185
1347
  }
1348
+ emitStatusChange?.();
1186
1349
  }
1187
- })
1188
- .finally(() => {
1189
- const s = currentEntry();
1190
- if (s)
1191
- s.settled = true;
1192
1350
  });
1193
1351
  }
1194
- toChatStreamResult(agentState, threadId, output) {
1352
+ toChatStreamResult(agentState, threadId, output, abortSignal) {
1195
1353
  // Entries are keyed by threadId so each thread holds at most one entry.
1196
1354
  // Stale entries are overwritten here on the next stream; explicit cleanup
1197
1355
  // happens via destroyThread() / destroyAgent() / shutdown(). Proactive
1198
1356
  // removal on stream completion is not safe because the suspend/resume
1199
1357
  // flow (submitToolResult, approveToolCall) needs the entry after the stream closes.
1200
- agentState.activeRuns.set(threadId, { runId: output.runId, fullStream: output.fullStream });
1358
+ const newRun = { runId: output.runId, fullStream: output.fullStream };
1359
+ if (abortSignal) {
1360
+ newRun.disposeAbortListener = this.installPerRunAbortListener(agentState, threadId, newRun, abortSignal);
1361
+ }
1362
+ this.replaceActiveRun(agentState, threadId, newRun);
1201
1363
  const adapter = new MastraEventAdapter(this.buses.getLogBus());
1202
1364
  return {
1203
- eventStream: this.createEventStream(output.fullStream, adapter, agentState),
1365
+ eventStream: this.createEventStream(output.fullStream, adapter, agentState, abortSignal),
1204
1366
  textStream: this.readableStreamToAsyncGenerator(output.textStream),
1205
1367
  };
1206
1368
  }
1369
+ /**
1370
+ * Replace (or set) the active run for a thread, disposing any prior
1371
+ * entry's per-run abort listener so the prior signal cannot fire on the
1372
+ * new run.
1373
+ */
1374
+ replaceActiveRun(agentState, threadId, run) {
1375
+ const prior = agentState.activeRuns.get(threadId);
1376
+ prior?.disposeAbortListener?.();
1377
+ agentState.activeRuns.set(threadId, run);
1378
+ }
1379
+ /**
1380
+ * Remove the active run for a thread, disposing its per-run abort listener
1381
+ * if present. Safe to call when no entry exists.
1382
+ */
1383
+ removeActiveRun(agentState, threadId) {
1384
+ const prior = agentState.activeRuns.get(threadId);
1385
+ prior?.disposeAbortListener?.();
1386
+ agentState.activeRuns.delete(threadId);
1387
+ }
1388
+ /**
1389
+ * Wire `abortSignal.abort()` to tear down the suspended Mastra run for
1390
+ * `threadId`. Returns a disposer that removes the listener; callers must
1391
+ * invoke it whenever the entry is replaced or removed so a stale signal
1392
+ * cannot abort an unrelated continuation run.
1393
+ *
1394
+ * Identity guard: the listener checks `agentState.activeRuns.get(threadId) === run`
1395
+ * before calling `abortRunStream`, so a defensive race with another
1396
+ * `stream()` call on the same thread cannot double-abort.
1397
+ *
1398
+ * Closes #409 — without this listener, aborting while a tool-approval is
1399
+ * pending leaves Mastra's prepared run alive and `activeRuns[threadId]`
1400
+ * pointing at a runId the consumer can no longer settle, so subsequent
1401
+ * `approveToolCall` reaches Mastra and surfaces a confusing
1402
+ * "could not find a suspended run" error instead of the SDK-level
1403
+ * "no active run found" guard.
1404
+ */
1405
+ installPerRunAbortListener(agentState, threadId, run, abortSignal) {
1406
+ if (abortSignal.aborted) {
1407
+ // Listener attaches *after* agent.stream() has resolved — by which
1408
+ // point Mastra has already observed the same signal and started
1409
+ // its own abort flow. We still need to clear the activeRuns entry
1410
+ // so the SDK's "no active run" guard fires on subsequent
1411
+ // approve/decline calls.
1412
+ queueMicrotask(() => this.handlePerRunAbort(agentState, threadId, run));
1413
+ return () => {
1414
+ /* nothing to remove */
1415
+ };
1416
+ }
1417
+ const onAbort = () => this.handlePerRunAbort(agentState, threadId, run);
1418
+ abortSignal.addEventListener('abort', onAbort, { once: true });
1419
+ return () => abortSignal.removeEventListener('abort', onAbort);
1420
+ }
1421
+ handlePerRunAbort(agentState, threadId, run) {
1422
+ // The body runs synchronously inside `controller.abort()` (via
1423
+ // EventTarget.dispatchEvent), so a thrown error here would surface
1424
+ // as a synchronous throw at the consumer's `controller.abort()`
1425
+ // call site — wrapping the whole body in try/catch keeps that path
1426
+ // contained even if `LogBus.warn` itself throws via a misbehaving
1427
+ // subscriber.
1428
+ try {
1429
+ // Identity guard: only abort if this is still the active run
1430
+ // for the thread. A subsequent stream() / approveToolCall()
1431
+ // call may have replaced the entry; that path's own listener
1432
+ // is responsible for its run.
1433
+ if (agentState.activeRuns.get(threadId) !== run)
1434
+ return;
1435
+ agentState.activeRuns.delete(threadId);
1436
+ agentState.agent.abortRunStream(run.runId);
1437
+ }
1438
+ catch (err) {
1439
+ // Mastra's abortRunStream returns a boolean today; defend
1440
+ // against a future shape change so a regression doesn't crash
1441
+ // the consumer's abort dispatch.
1442
+ try {
1443
+ this.buses
1444
+ .getLogBus()
1445
+ ?.warn(`Failed to abort suspended run "${run.runId}" on thread "${threadId}": ${getErrorMessage(err)}`, { threadId, runId: run.runId }, err instanceof Error ? err : new Error(String(err)));
1446
+ }
1447
+ catch {
1448
+ /* swallow — synchronous abort dispatch must not throw */
1449
+ }
1450
+ }
1451
+ }
1207
1452
  async getMemoryOrThrow(agentId) {
1208
1453
  const agentState = this.getAgentStateOrThrow(agentId);
1209
1454
  const memory = await agentState.agent.getMemory();
@@ -1233,11 +1478,22 @@ export class MastraHarness {
1233
1478
  ctx.set(MASTRA_RESOURCE_ID_KEY, this.agentResourceId(agentId));
1234
1479
  return ctx;
1235
1480
  }
1236
- async *createEventStream(fullStream, adapter, agentState) {
1481
+ async *createEventStream(fullStream, adapter, agentState, abortSignal) {
1482
+ // Tracks whether the consumer has observed a terminal event already.
1483
+ // Either `finish` (natural completion) or any `error` event counts —
1484
+ // the SDK's `wrapEventStream` treats any error as terminal and
1485
+ // appends its own synthetic finish, so synthesizing an abort
1486
+ // terminal on top would duplicate the SDK invariant of one terminal
1487
+ // per turn. The narrower "only abort-coded errors count" predicate
1488
+ // would mis-fire on tripwire/regular errors that race the abort.
1489
+ let sawTerminal = false;
1237
1490
  for await (const chunk of this.readableStreamToAsyncGenerator(fullStream)) {
1238
1491
  const event = adapter.toChatEvent(chunk);
1239
1492
  if (!event)
1240
1493
  continue;
1494
+ if (event.type === 'finish' || event.type === 'error') {
1495
+ sawTerminal = true;
1496
+ }
1241
1497
  if (isEnrichableToolEvent(event)) {
1242
1498
  const toolName = event.type === 'tool-approval-request' ? event.toolCall.toolName : event.toolName;
1243
1499
  const metadata = lookupMcpToolMetadata(agentState, toolName);
@@ -1252,6 +1508,29 @@ export class MastraHarness {
1252
1508
  }
1253
1509
  yield event;
1254
1510
  }
1511
+ // The fullStream closed. If abort fired during a suspended-run path
1512
+ // (consumer aborted while a tool-approval was pending), Mastra
1513
+ // releases the prepared run silently — the fullStream just ends with
1514
+ // no `abort` chunk — so synthesize the terminal pair the SDK
1515
+ // invariant requires. See ARCHITECTURE.md → "Tool Approval Flow →
1516
+ // Abort during pending approval (#409)".
1517
+ //
1518
+ // Adapter-coupling note: this gate depends on `MastraEventAdapter`
1519
+ // not yielding a `finish` chunk between `tool-call-approval` and
1520
+ // the consumer's resume. True today (the adapter only emits
1521
+ // `finish` for the terminal Mastra `finish` chunk). A future
1522
+ // Mastra version that injects a step-level `finish` mid-suspend
1523
+ // would set `sawTerminal` early and silently suppress the synth.
1524
+ // If that lands, switch the predicate to track only the terminal
1525
+ // `finish` (e.g. via a per-stream "suspended" flag).
1526
+ if (!sawTerminal && abortSignal?.aborted) {
1527
+ yield {
1528
+ type: 'error',
1529
+ error: new DOMException('The stream was aborted.', 'AbortError'),
1530
+ code: 'abort',
1531
+ };
1532
+ yield { type: 'finish', finishReason: 'error' };
1533
+ }
1255
1534
  }
1256
1535
  async *readableStreamToAsyncGenerator(stream) {
1257
1536
  const reader = stream.getReader();
@@ -0,0 +1,34 @@
1
+ import type { McpServerErrorCategory, McpServerErrorDetail } from '@salesforce/sfdx-agent-sdk';
2
+ /**
3
+ * Strip V8 stack frames (" at <frame>") and any framing that arrived inside
4
+ * the message string. The result is a single, human-readable summary line safe
5
+ * for UI banners, telemetry tags, and Sentry fingerprints. The full stack is
6
+ * still available on the underlying `Error` instance (`error.stack`) for
7
+ * debug-mode log surfaces.
8
+ *
9
+ * Inputs are loose on purpose: a string passes through, a non-Error object's
10
+ * `String(...)` form is sanitized the same way, and `undefined`/`null` round-trip
11
+ * through `String(...)`.
12
+ */
13
+ export declare function sanitizeMcpErrorMessage(err: unknown): string;
14
+ /**
15
+ * Map a caught error to the SDK's stable {@link McpServerErrorCategory}. The
16
+ * lookup is layered:
17
+ *
18
+ * 1. `DOMException` `AbortError` and Node's `code === 'ABORT_ERR'` map to
19
+ * `'aborted'` regardless of the `.message`.
20
+ * 2. MCP-shaped errors (an `Error` carrying a numeric `code`, see
21
+ * {@link isMcpErrorLike}) match by JSON-RPC 2.0 code (`-32001` etc.).
22
+ * 3. Free-form `.message` text is matched against substrings the underlying
23
+ * transports surface (`401`, `403`, `5xx`, `ECONNREFUSED`, etc.). This is
24
+ * intentionally a small lookup — categories are stable, message shapes
25
+ * are not, so the heuristic stays narrow and falls back to `'unknown'`.
26
+ */
27
+ export declare function classifyMcpError(err: unknown): McpServerErrorCategory;
28
+ /**
29
+ * Build the SDK-side {@link McpServerErrorDetail} from a caught error. The
30
+ * underlying JSON-RPC `code` (when the error is MCP-shaped, see
31
+ * {@link isMcpErrorLike}) is preserved so consumers can route on the code
32
+ * without having to introspect `cause`.
33
+ */
34
+ export declare function buildMcpErrorDetail(err: unknown): McpServerErrorDetail;
@@ -0,0 +1,131 @@
1
+ /*
2
+ * Copyright 2026, Salesforce, Inc. All rights reserved.
3
+ * See LICENSE.txt for license terms.
4
+ */
5
+ import { isAbortError } from '@salesforce/agentic-common';
6
+ /**
7
+ * Structural duck-type for MCP-shaped errors. Matches the upstream
8
+ * `@modelcontextprotocol/sdk` `McpError` class (`extends Error`, numeric
9
+ * `code`) without an `instanceof` check. We avoid the import for two reasons:
10
+ *
11
+ * 1. **Dual-package hazard.** If `@mastra/mcp` ever pins
12
+ * `@modelcontextprotocol/sdk` to a version that diverges from one this
13
+ * package would pin, npm hoists two copies and `instanceof` returns false
14
+ * across realms. Structural matching survives that case.
15
+ * 2. **JSON-RPC codes are the contract, not the class.** The numeric codes
16
+ * (`-32001 RequestTimeout`, `-32603 InternalError`, …) are stable per the
17
+ * JSON-RPC 2.0 spec. Any error with `code: number` originating from
18
+ * MCP-shaped infrastructure will match — which is what consumers want.
19
+ *
20
+ * Node's `SystemError` (e.g. `ECONNREFUSED`) also has a `.code` field but as
21
+ * a `string`, so the `typeof === 'number'` check disambiguates.
22
+ */
23
+ function isMcpErrorLike(err) {
24
+ return err instanceof Error && typeof err.code === 'number';
25
+ }
26
+ /**
27
+ * Strip V8 stack frames (" at <frame>") and any framing that arrived inside
28
+ * the message string. The result is a single, human-readable summary line safe
29
+ * for UI banners, telemetry tags, and Sentry fingerprints. The full stack is
30
+ * still available on the underlying `Error` instance (`error.stack`) for
31
+ * debug-mode log surfaces.
32
+ *
33
+ * Inputs are loose on purpose: a string passes through, a non-Error object's
34
+ * `String(...)` form is sanitized the same way, and `undefined`/`null` round-trip
35
+ * through `String(...)`.
36
+ */
37
+ export function sanitizeMcpErrorMessage(err) {
38
+ const raw = err instanceof Error ? err.message : String(err);
39
+ const lines = raw.split(/\r?\n/);
40
+ // Keep the first line that is NOT a V8 stack frame. V8 frames start with
41
+ // arbitrary leading whitespace followed by "at " (matched at line start).
42
+ for (const line of lines) {
43
+ if (!/^\s*at\s/.test(line)) {
44
+ return line.trim();
45
+ }
46
+ }
47
+ // All lines were stack frames — fall back to the raw single-line input.
48
+ return lines[0]?.trim() ?? '';
49
+ }
50
+ /**
51
+ * Map a caught error to the SDK's stable {@link McpServerErrorCategory}. The
52
+ * lookup is layered:
53
+ *
54
+ * 1. `DOMException` `AbortError` and Node's `code === 'ABORT_ERR'` map to
55
+ * `'aborted'` regardless of the `.message`.
56
+ * 2. MCP-shaped errors (an `Error` carrying a numeric `code`, see
57
+ * {@link isMcpErrorLike}) match by JSON-RPC 2.0 code (`-32001` etc.).
58
+ * 3. Free-form `.message` text is matched against substrings the underlying
59
+ * transports surface (`401`, `403`, `5xx`, `ECONNREFUSED`, etc.). This is
60
+ * intentionally a small lookup — categories are stable, message shapes
61
+ * are not, so the heuristic stays narrow and falls back to `'unknown'`.
62
+ */
63
+ export function classifyMcpError(err) {
64
+ if (isAbortError(err))
65
+ return 'aborted';
66
+ if (isMcpErrorLike(err)) {
67
+ switch (err.code) {
68
+ case -32001: // RequestTimeout
69
+ return 'connect-timeout';
70
+ case -32700: // ParseError
71
+ case -32600: // InvalidRequest
72
+ case -32601: // MethodNotFound
73
+ case -32602: // InvalidParams
74
+ case -32603: // InternalError
75
+ return 'protocol-error';
76
+ case -32000: // ConnectionClosed
77
+ return 'transport-eof';
78
+ default:
79
+ return 'unknown';
80
+ }
81
+ }
82
+ const message = err instanceof Error ? err.message : typeof err === 'string' ? err : '';
83
+ if (/\b401\b/.test(message))
84
+ return 'http-401';
85
+ if (/\b403\b/.test(message))
86
+ return 'http-403';
87
+ if (/\b4\d\d\b/.test(message))
88
+ return 'http-4xx';
89
+ if (/\b5\d\d\b/.test(message))
90
+ return 'http-5xx';
91
+ if (/ECONNREFUSED|ECONNRESET|EPIPE|socket hang up/i.test(message))
92
+ return 'transport-eof';
93
+ if (/timed out|timeout/i.test(message))
94
+ return 'connect-timeout';
95
+ return 'unknown';
96
+ }
97
+ /**
98
+ * Whether the SDK considers the failure transient (worth a retry via
99
+ * `Agent.reconnectMcpServer` / `Agent.refreshMcpAuth`). Aborted operations and
100
+ * permanent protocol violations are non-retriable; everything else is retriable
101
+ * by default — failing-open here is correct because the consumer always has
102
+ * the option to ignore the hint.
103
+ */
104
+ function isCategoryRetriable(category) {
105
+ switch (category) {
106
+ case 'aborted':
107
+ case 'protocol-error':
108
+ case 'config-error':
109
+ return false;
110
+ default:
111
+ return true;
112
+ }
113
+ }
114
+ /**
115
+ * Build the SDK-side {@link McpServerErrorDetail} from a caught error. The
116
+ * underlying JSON-RPC `code` (when the error is MCP-shaped, see
117
+ * {@link isMcpErrorLike}) is preserved so consumers can route on the code
118
+ * without having to introspect `cause`.
119
+ */
120
+ export function buildMcpErrorDetail(err) {
121
+ const category = classifyMcpError(err);
122
+ const detail = {
123
+ category,
124
+ retriable: isCategoryRetriable(category),
125
+ };
126
+ if (isMcpErrorLike(err)) {
127
+ detail.code = err.code;
128
+ }
129
+ return detail;
130
+ }
131
+ //# sourceMappingURL=mcp-error-classifier.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/sfdx-agent-harness-mastra",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Mastra-backed AgentHarness implementation for @salesforce/sfdx-agent-sdk",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -28,20 +28,21 @@
28
28
  ],
29
29
  "dependencies": {
30
30
  "@ai-sdk/provider": "^3.0.10",
31
+ "@libsql/client": "^0.15.15",
31
32
  "@mastra/core": "^1.36.0",
32
33
  "@mastra/libsql": "^1.11.1",
33
34
  "@mastra/mcp": "^1.8.0",
34
35
  "@mastra/memory": "^1.19.0",
35
36
  "@mastra/schema-compat": "^1.2.10",
36
- "@salesforce/agentic-common": "0.5.0",
37
- "@salesforce/llm-gateway-sdk": "0.6.0"
37
+ "@salesforce/agentic-common": "0.6.0",
38
+ "@salesforce/llm-gateway-sdk": "0.8.0"
38
39
  },
39
40
  "peerDependencies": {
40
- "@salesforce/sfdx-agent-sdk": "0.9.0"
41
+ "@salesforce/sfdx-agent-sdk": "0.11.0"
41
42
  },
42
43
  "devDependencies": {
43
44
  "@eslint/js": "^10.0.1",
44
- "@salesforce/sfdx-agent-sdk": "0.9.0",
45
+ "@salesforce/sfdx-agent-sdk": "0.11.0",
45
46
  "@types/node": "^22.19.17",
46
47
  "@vitest/coverage-istanbul": "^4.1.7",
47
48
  "@vitest/eslint-plugin": "^1.6.17",