@salesforce/sfdx-agent-harness-mastra 0.7.0 → 0.9.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,18 +20,53 @@ 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
30
39
  * independent discovery promise so readiness is tracked individually.
40
+ *
41
+ * `epoch` increments on every fresh discovery (initial create + each
42
+ * reconnect). Discovery callbacks captured against an earlier epoch drop their
43
+ * writes when a newer discovery has already started for the same server, so an
44
+ * orphaned in-flight `listToolsetsWithErrors()` cannot overwrite a fresher
45
+ * discovery's `tools` / `error` / `settled` state.
31
46
  */
32
47
  interface McpServerState {
33
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;
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
+ */
34
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;
35
70
  ready: Promise<void>;
36
71
  settled: boolean;
37
72
  tools: ToolsInput;
@@ -112,10 +147,18 @@ export declare class MastraHarness implements MastraAgentHarness {
112
147
  * would mix process state.
113
148
  */
114
149
  protected readonly activeAgents: Map<string, MastraAgentState>;
150
+ private readonly libsqlClient;
115
151
  private readonly storage;
116
152
  private readonly memory;
117
153
  private readonly mastra;
118
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;
119
162
  /**
120
163
  * Mastra-specific extension surface, typed off `MastraAgentHarness`. The
121
164
  * SDK never reads this; consumers reach through `manager.extensions.mastra`.
@@ -127,6 +170,9 @@ export declare class MastraHarness implements MastraAgentHarness {
127
170
  * - MUST disconnect the mcpClient for every active agent before clearing state.
128
171
  * - MUST clear the `activeAgents` map.
129
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).
130
176
  */
131
177
  shutdown(): Promise<void>;
132
178
  onTelemetry(callback: TelemetryEventCallback): Unsubscribe;
@@ -165,6 +211,18 @@ export declare class MastraHarness implements MastraAgentHarness {
165
211
  */
166
212
  getAgentIds(): Promise<string[]>;
167
213
  getMcpServerInfo(agentId: string): McpServerInfo[];
214
+ /**
215
+ * Eager implementation of {@link AgentHarness.reconnectMcpServer}: calls
216
+ * Mastra's `MCPClient.reconnectServer(name)` primitive, supersedes any
217
+ * in-flight discovery (replace-ready semantics — latest call wins), and
218
+ * awaits the new discovery before resolving.
219
+ *
220
+ * Programming errors (unknown agent / unknown server / disabled) reject.
221
+ * Transport / discovery failures are recorded on the per-server state
222
+ * (visible via {@link getMcpServerInfo}) and a `mcp-server-discovery-*`
223
+ * telemetry pair fires; the returned promise still resolves.
224
+ */
225
+ reconnectMcpServer(agentId: string, serverName: string): Promise<void>;
168
226
  /**
169
227
  * @requirements
170
228
  * - MUST throw an error if the specified `agentId` is not found in `activeAgents`.
@@ -215,6 +273,14 @@ export declare class MastraHarness implements MastraAgentHarness {
215
273
  * User turns are treated as authoritative input.
216
274
  */
217
275
  compactThread(agentId: string, threadId: string): Promise<string>;
276
+ /**
277
+ * Best-effort wrapper around `destroyThread`: swallows the error so the
278
+ * caller's outer flow isn't aborted by a teardown blip, but logs the
279
+ * failure at warn level so a future regression in `destroyThread` is
280
+ * still observable. Used by `compactThread` on both the empty-source and
281
+ * non-empty paths.
282
+ */
283
+ private destroyThreadBestEffort;
218
284
  /**
219
285
  * @requirements
220
286
  * - MUST throw an error if the specified `agentId` is not found in `activeAgents`.
@@ -223,6 +289,10 @@ export declare class MastraHarness implements MastraAgentHarness {
223
289
  * - MUST return a `ChatStreamResult` with `textStream` and `eventStream`.
224
290
  * - On pre-stream rejection from `agent.stream()`, MUST re-throw without emitting telemetry —
225
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)".
226
296
  */
227
297
  stream(agentId: string, threadId: string, message: string, options?: StreamOptions): Promise<ChatStreamResult>;
228
298
  /**
@@ -302,7 +372,6 @@ export declare class MastraHarness implements MastraAgentHarness {
302
372
  * directly.
303
373
  */
304
374
  private summarize;
305
- private buildSummaryPrompt;
306
375
  /**
307
376
  * Two-pass summarization for transcripts above the char budget. The
308
377
  * transcript is split into roughly-equal chunks each ≤ MAX_TRANSCRIPT_CHARS,
@@ -317,9 +386,92 @@ export declare class MastraHarness implements MastraAgentHarness {
317
386
  */
318
387
  private splitIntoChunks;
319
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;
408
+ /**
409
+ * Runs MCP discovery for one server: emits the
410
+ * `mcp-server-discovery-started` / `…-completed` / `…-failed` telemetry
411
+ * triple and writes results into the corresponding entry in `serverStates`.
412
+ *
413
+ * The caller MUST `set()` the entry into `serverStates` either before
414
+ * calling this helper (the `createAgent` path constructs the entry
415
+ * synchronously after this returns the promise) or before any await
416
+ * boundary in the helper's chain — the chain reads back via
417
+ * `serverStates.get(serverName)!` to write `tools` / `error`. Both call
418
+ * sites today put the entry in place before the first microtask boundary.
419
+ *
420
+ * Errors observed during discovery are recorded on the entry's `error`
421
+ * field; the returned promise resolves either way (it only rejects when
422
+ * an aborted operation throws after we have already cleaned up). The
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.
427
+ */
428
+ private runMcpDiscovery;
320
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;
321
460
  private getMemoryOrThrow;
322
461
  private agentResourceId;
462
+ /**
463
+ * Build a `RequestContext` carrying the thread + resource ids under the
464
+ * reserved keys Mastra reads when input processors (e.g. `ToolSearchProcessor`,
465
+ * `SkillSearchProcessor`) call `requestContext.get(MASTRA_THREAD_ID_KEY)`.
466
+ *
467
+ * `memory: { thread, resource }` populates Mastra's memory layer but is NOT
468
+ * propagated into the request context, so processors that key per-thread
469
+ * state by `MASTRA_THREAD_ID_KEY` would otherwise see `undefined` and fall
470
+ * back to the `'default'` bucket — folding every consumer thread into one.
471
+ * Surfacing the ids here lets `ToolSearchHandle.clearState(threadId)`
472
+ * actually match what the processor stored.
473
+ */
474
+ private buildRequestContext;
323
475
  private createEventStream;
324
476
  private readableStreamToAsyncGenerator;
325
477
  /**