@salesforce/sfdx-agent-harness-mastra 0.1.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.
Files changed (40) hide show
  1. package/LICENSE.txt +21 -0
  2. package/README.md +66 -0
  3. package/dist/default-model-config-factory.d.ts +11 -0
  4. package/dist/default-model-config-factory.js +18 -0
  5. package/dist/default-model-config-factory.js.map +1 -0
  6. package/dist/index.d.ts +4 -0
  7. package/dist/index.js +8 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/llmg-express-model-config-factory.d.ts +23 -0
  10. package/dist/llmg-express-model-config-factory.js +28 -0
  11. package/dist/llmg-express-model-config-factory.js.map +1 -0
  12. package/dist/mastra-event-adapter.d.ts +73 -0
  13. package/dist/mastra-event-adapter.js +204 -0
  14. package/dist/mastra-event-adapter.js.map +1 -0
  15. package/dist/mastra-harness-factory.d.ts +9 -0
  16. package/dist/mastra-harness-factory.js +17 -0
  17. package/dist/mastra-harness-factory.js.map +1 -0
  18. package/dist/mastra-harness.d.ts +292 -0
  19. package/dist/mastra-harness.js +838 -0
  20. package/dist/mastra-harness.js.map +1 -0
  21. package/dist/mastra-message-mapper.d.ts +42 -0
  22. package/dist/mastra-message-mapper.js +195 -0
  23. package/dist/mastra-message-mapper.js.map +1 -0
  24. package/dist/mastra-tool-mapper.d.ts +29 -0
  25. package/dist/mastra-tool-mapper.js +55 -0
  26. package/dist/mastra-tool-mapper.js.map +1 -0
  27. package/dist/mcp-config-mapper.d.ts +20 -0
  28. package/dist/mcp-config-mapper.js +73 -0
  29. package/dist/mcp-config-mapper.js.map +1 -0
  30. package/dist/model-config-factory.d.ts +16 -0
  31. package/dist/model-config-factory.js +6 -0
  32. package/dist/model-config-factory.js.map +1 -0
  33. package/dist/salesforce-llm-gateway.d.ts +29 -0
  34. package/dist/salesforce-llm-gateway.js +266 -0
  35. package/dist/salesforce-llm-gateway.js.map +1 -0
  36. package/dist/test/tsconfig.tsbuildinfo +1 -0
  37. package/dist/utils.d.ts +10 -0
  38. package/dist/utils.js +29 -0
  39. package/dist/utils.js.map +1 -0
  40. package/package.json +71 -0
@@ -0,0 +1,292 @@
1
+ import { Agent } from '@mastra/core/agent';
2
+ import type { ChunkType } from '@mastra/core/stream';
3
+ import { MCPClient } from '@mastra/mcp';
4
+ import { type AgentHarness, type ChatStreamResult, type HarnessAgentConfig, type MCPConfiguration, type McpServerInfo, type Message, type StreamOptions, type TelemetryEventCallback, type ToolResultInfo } from '@salesforce/sfdx-agent-sdk';
5
+ import type { MastraModelConfigFactory } from './model-config-factory.js';
6
+ import type { LLMGatewayClient } from '@salesforce/llm-gateway-sdk';
7
+ import { Clock, type LogRecord, type UniqueIDGenerator, type Unsubscribe } from '@salesforce/agentic-common';
8
+ import { type ToolsInput } from './utils.js';
9
+ import { Workspace } from '@mastra/core/workspace';
10
+ /**
11
+ * Tracks the active workflow run for a single thread.
12
+ *
13
+ * Mastra uses `runId` to locate the suspended workflow snapshot when
14
+ * `approveToolCall()` / `declineToolCall()` resume a stream. The
15
+ * `fullStream` reference is kept so we can drain any unconsumed chunks
16
+ * before resuming — Mastra persists its workflow snapshot asynchronously
17
+ * after emitting the last stream chunk (e.g. `tool-call-approval`), so
18
+ * if the consumer breaks out of the event stream early (common in
19
+ * approval UIs), we must finish reading the stream to ensure the
20
+ * snapshot is persisted before we attempt to resume.
21
+ */
22
+ interface ActiveRun {
23
+ runId: string;
24
+ fullStream: ReadableStream<ChunkType>;
25
+ }
26
+ /**
27
+ * Per-server MCP state. Each enabled server gets its own MCPClient and
28
+ * independent discovery promise so readiness is tracked individually.
29
+ */
30
+ interface McpServerState {
31
+ client: MCPClient;
32
+ error?: string;
33
+ ready: Promise<void>;
34
+ settled: boolean;
35
+ tools: ToolsInput;
36
+ }
37
+ /**
38
+ * Internal state for a single agent. Holds Mastra-specific instances so the
39
+ * generic API never sees them.
40
+ */
41
+ interface MastraAgentState {
42
+ /** Mastra Agent instance. */
43
+ agent: Agent;
44
+ /** Per-server MCP state, keyed by server name. Empty map when no MCP servers are configured. */
45
+ mcpServers: Map<string, McpServerState>;
46
+ /** Original MCP configuration, retained so getMcpServerInfo can report disabled servers. */
47
+ mcpConfig?: MCPConfiguration;
48
+ /** Mastra Workspace providing built-in file tools to the agent. */
49
+ mastraWorkspace?: Workspace;
50
+ /** Tracks the active workflow run per thread for tool approval. */
51
+ activeRuns: Map<string, ActiveRun>;
52
+ }
53
+ export declare class MastraHarness implements AgentHarness {
54
+ private readonly storageRootFolder;
55
+ private readonly modelConfigFactory;
56
+ private readonly clock;
57
+ private readonly idGenerator;
58
+ readonly harnessId = "mastra";
59
+ readonly protocolVersion = 1;
60
+ /**
61
+ * Registry of agentId → Mastra state. Holds per-agent MCPClient instances
62
+ * so each agent has isolated server connections; sharing across agents
63
+ * would mix process state.
64
+ */
65
+ protected readonly activeAgents: Map<string, MastraAgentState>;
66
+ private readonly storage;
67
+ private readonly memory;
68
+ private readonly mastra;
69
+ private readonly buses;
70
+ constructor(storageRootFolder: string, modelConfigFactory?: MastraModelConfigFactory, clock?: Clock, idGenerator?: UniqueIDGenerator);
71
+ /**
72
+ * @requirements
73
+ * - MUST disconnect the mcpClient for every active agent before clearing state.
74
+ * - MUST clear the `activeAgents` map.
75
+ * - MUST shut down the global `this.mastra` orchestrator.
76
+ */
77
+ shutdown(): Promise<void>;
78
+ onTelemetry(callback: TelemetryEventCallback): Unsubscribe;
79
+ onLog(callback: (record: LogRecord) => void): Unsubscribe;
80
+ /**
81
+ * @requirements
82
+ * - MUST throw an error if the harness has not been initialized.
83
+ * - MUST throw an error if an agent with the given `config.agentId` is already registered.
84
+ * - MUST instantiate a Mastra `Agent` with instructions, model, and memory.
85
+ * - MUST provide a fallback model string if modelId is absent.
86
+ * - MUST register the agent with the Mastra instance for gateway resolution.
87
+ * - MUST construct a per-agent `MCPClient` if `config.mcpServers` has enabled servers and
88
+ * kick off `listToolsetsWithErrors()` in the background (non-blocking). MCP tools become available
89
+ * once discovery completes; until then the agent operates with custom tools only.
90
+ * - MUST use a tools function (DynamicArgument) so the agent picks up MCP tools dynamically.
91
+ * - MUST disconnect and discard the `MCPClient` if tool discovery fails, to avoid orphaned processes.
92
+ * - MUST respect the `abortSignal` option: check before MCP setup, race listToolsetsWithErrors() against it.
93
+ * - MUST namespace MCP tool names as `${serverName}_${toolName}` to avoid collisions with custom tools.
94
+ * - MUST convert `config.tools` (SDK `ToolDefinition[]`) to Mastra `ToolAction` entries via
95
+ * `mapToolDefinitionsToMastraTools()` and merge with namespaced MCP-discovered tools.
96
+ * - MUST store the original `MCPConfiguration` so getMcpServerInfo can report disabled servers.
97
+ */
98
+ createAgent(agentId: string, projectRoot: string, llmGatewayClient: LLMGatewayClient, config?: HarnessAgentConfig, options?: {
99
+ abortSignal?: AbortSignal;
100
+ }): Promise<void>;
101
+ /**
102
+ * @requirements
103
+ * - MUST throw an error if the specified `agentId` is not found in `activeAgents`.
104
+ * - MUST disconnect all per-server MCP clients to terminate any spawned processes.
105
+ * - MUST remove the agent from the internal `activeAgents` map.
106
+ */
107
+ destroyAgent(agentId: string): Promise<boolean>;
108
+ /**
109
+ * @requirements
110
+ * - MUST return an array of strings representing the keys currently present in `activeAgents`.
111
+ */
112
+ getAgentIds(): Promise<string[]>;
113
+ getMcpServerInfo(agentId: string): McpServerInfo[];
114
+ /**
115
+ * @requirements
116
+ * - MUST throw an error if the specified `agentId` is not found in `activeAgents`.
117
+ * - MUST call `memory.createThread()` with the resourceId scoped to this agent.
118
+ * - MUST use the provided `threadId` if supplied, otherwise generate a new one.
119
+ * - MUST return the ID of the created thread.
120
+ */
121
+ createThread(agentId: string, threadId?: string): Promise<string>;
122
+ /**
123
+ * @requirements
124
+ * - MUST throw an error if the specified `agentId` is not found in `activeAgents`.
125
+ * - MUST delete the thread and its history from the storage backend.
126
+ * - MUST remove any active run tracked for this thread to prevent stale references.
127
+ */
128
+ destroyThread(agentId: string, threadId: string): Promise<void>;
129
+ /**
130
+ * @requirements
131
+ * - MUST throw an error if the specified `agentId` is not found in `activeAgents`.
132
+ * - MUST query threads scoped to this agent and return their IDs.
133
+ */
134
+ getThreadIds(agentId: string): Promise<string[]>;
135
+ /**
136
+ * @requirements
137
+ * - MUST throw an error if the specified `agentId` is not found in `activeAgents`.
138
+ * - MUST clone the source thread's messages into a new thread.
139
+ */
140
+ cloneThread(agentId: string, sourceThreadId: string, targetThreadId?: string): Promise<string>;
141
+ /**
142
+ * @requirements
143
+ * - MUST throw an error if the specified `agentId` is not found in `activeAgents`.
144
+ * - MUST create a new thread seeded with an LLM-generated summary.
145
+ * - MUST chunk-summarize transcripts that exceed `MAX_TRANSCRIPT_CHARS`
146
+ * to keep any single model call inside the context window.
147
+ * - MUST surface summarization or seeding failures as `AgentSDKError`
148
+ * with type `COMPACTION_FAILED` and leave the source thread intact on
149
+ * failure.
150
+ * - MUST destroy the source thread on success so it no longer appears in
151
+ * `getThreadIds()`. Cleanup of the source thread is best-effort: if the
152
+ * delete fails the new thread is still returned, since the user's data
153
+ * has already been persisted there. An orphaned source thread is
154
+ * hygiene, not correctness.
155
+ * - MUST seed the summary as a user/assistant turn pair where the user
156
+ * turn carries the summary content and the assistant turn is a brief
157
+ * acknowledgment. Storing the summary as an assistant turn (even with
158
+ * a preceding user prompt) is empirically unreliable: the model
159
+ * recognizes it has no record of producing that summary and rejects
160
+ * it as fabricated, refusing to use the facts on follow-up turns.
161
+ * User turns are treated as authoritative input.
162
+ */
163
+ compactThread(agentId: string, threadId: string): Promise<string>;
164
+ /**
165
+ * @requirements
166
+ * - MUST throw an error if the specified `agentId` is not found in `activeAgents`.
167
+ * - MUST invoke `agent.stream()` with memory context for the thread.
168
+ * - MUST capture the `runId` for tool approval flows.
169
+ * - MUST return a `ChatStreamResult` with `textStream` and `eventStream`.
170
+ * - On pre-stream rejection from `agent.stream()`, MUST re-throw without emitting telemetry —
171
+ * the SDK's chat-session boundary catches the rejection and emits `chat-stream-error`.
172
+ */
173
+ stream(agentId: string, threadId: string, message: string, options?: StreamOptions): Promise<ChatStreamResult>;
174
+ /**
175
+ * @requirements
176
+ * - MUST throw an error if the specified `agentId` is not found in `activeAgents`.
177
+ * - MUST throw an error if no active run exists for `threadId` (the consumer must
178
+ * have started a stream via `stream()` first).
179
+ * - MUST drain the previous stream before resuming to avoid reader-lock conflicts.
180
+ * - MUST call `agent.resumeStream()` with the client-provided tool result packed
181
+ * into `resumeData` (`{ approved: true, result, isError }`). The bridge execute
182
+ * handler registered in the tool mapper reads this data and returns it, allowing
183
+ * the result to flow through Mastra's native agentic loop.
184
+ * - MUST update `activeRuns` with the new `runId` from the continuation output.
185
+ * - MUST return a `ChatStreamResult` with `textStream` and `eventStream`.
186
+ * - On pre-stream rejection from `agent.resumeStream()`, MUST re-throw without emitting
187
+ * telemetry — the SDK's chat-session boundary catches the rejection and emits
188
+ * `chat-stream-error`.
189
+ */
190
+ submitToolResult(agentId: string, threadId: string, toolResult: ToolResultInfo): Promise<ChatStreamResult>;
191
+ /**
192
+ * @requirements
193
+ * - MUST throw an error if the specified `agentId` is not found in `activeAgents`.
194
+ * - MUST throw an error if no active `runId` is stored for `threadId` in `activeRuns`
195
+ * (i.e. the caller has not yet started a stream on that thread).
196
+ * - MUST invoke `agent.approveToolCall()` with the stored `runId`, the `toolCallId`, and
197
+ * the thread's memory context (`thread: threadId`, `resource: AGENT_INSTANCE:<agentId>`).
198
+ * Mastra then unblocks the suspended tool execution, runs the tool, and generates the
199
+ * continuation response.
200
+ * - MUST update `activeRuns` with the new `runId` from the continuation output so that
201
+ * any subsequent `approveToolCall()` / `declineToolCall()` calls within the same thread
202
+ * reference the most recent run.
203
+ * - MUST return a `ChatStreamResult` with `textStream` and `eventStream` from the
204
+ * continuation output, following the same pattern as AI SDK, ACP, and Mastra's own
205
+ * streaming APIs where approval produces a new continuation stream.
206
+ * - On pre-stream rejection from `agent.approveToolCall()`, MUST re-throw without emitting
207
+ * telemetry — the SDK's chat-session boundary catches the rejection and emits
208
+ * `chat-stream-error`.
209
+ */
210
+ approveToolCall(agentId: string, threadId: string, toolCallId: string): Promise<ChatStreamResult>;
211
+ /**
212
+ * @requirements
213
+ * - MUST throw an error if the specified `agentId` is not found in `activeAgents`.
214
+ * - MUST throw an error if no active `runId` is stored for `threadId` in `activeRuns`.
215
+ * - MUST invoke `agent.declineToolCall()` with the stored `runId`, the `toolCallId`, and
216
+ * the thread's memory context. Mastra cancels the pending tool call and generates a
217
+ * continuation response acknowledging the decline.
218
+ * - MUST update `activeRuns` with the new `runId` from the continuation output.
219
+ * - MUST return a `ChatStreamResult` with `textStream` and `eventStream` from the
220
+ * continuation output.
221
+ * - On pre-stream rejection from `agent.declineToolCall()`, MUST re-throw without emitting
222
+ * telemetry — the SDK's chat-session boundary catches the rejection and emits
223
+ * `chat-stream-error`.
224
+ */
225
+ declineToolCall(agentId: string, threadId: string, toolCallId: string): Promise<ChatStreamResult>;
226
+ /**
227
+ * @requirements
228
+ * - MUST throw an error if the specified `agentId` is not found.
229
+ * - MUST retrieve messages via `memory.recall()` and map to our `Message` format.
230
+ */
231
+ getMessages(agentId: string, threadId: string): Promise<Message[]>;
232
+ /**
233
+ * @requirements
234
+ * - MUST throw an error if the specified `agentId` is not found.
235
+ * - MUST delete all messages in the thread.
236
+ */
237
+ clearMessages(agentId: string, threadId: string): Promise<void>;
238
+ /**
239
+ * @requirements
240
+ * - MUST throw an error if the specified `agentId` is not found.
241
+ * - MUST save context messages without triggering an LLM response.
242
+ */
243
+ addContext(agentId: string, threadId: string, messages: Message[]): Promise<void>;
244
+ /**
245
+ * Issue a single non-agentic model call (`maxSteps: 1`) for one-shot
246
+ * summarization. Used internally by `compactThread()`; not part of the
247
+ * `AgentHarness` contract because the SDK never calls summarization
248
+ * directly.
249
+ */
250
+ private summarize;
251
+ private buildSummaryPrompt;
252
+ /**
253
+ * Two-pass summarization for transcripts above the char budget. The
254
+ * transcript is split into roughly-equal chunks each ≤ MAX_TRANSCRIPT_CHARS,
255
+ * each chunk is summarized, and the concatenated chunk summaries are
256
+ * summarized again into the final summary.
257
+ */
258
+ private summarizeInChunks;
259
+ /**
260
+ * Splits a string into ≤ `maxChars`-sized chunks, preferring line
261
+ * boundaries so message turns are not torn across chunks. Falls back to
262
+ * a hard slice if a single line itself exceeds the budget.
263
+ */
264
+ private splitIntoChunks;
265
+ private getAgentStateOrThrow;
266
+ private toChatStreamResult;
267
+ private getMemoryOrThrow;
268
+ private agentResourceId;
269
+ private createEventStream;
270
+ private readableStreamToAsyncGenerator;
271
+ /**
272
+ * Consume any remaining chunks from a ReadableStream, discarding them.
273
+ *
274
+ * Mastra persists workflow snapshots asynchronously after emitting the
275
+ * last stream chunk. If the consumer broke out of the event stream early
276
+ * (e.g. on `tool-approval-request`), the reader lock was released but
277
+ * the snapshot may not yet be persisted. Draining the remaining chunks
278
+ * guarantees the snapshot is ready before we call `agent.approveToolCall()`
279
+ * or `agent.declineToolCall()`.
280
+ *
281
+ * If the stream was already fully consumed (normal finish, no break),
282
+ * this is a no-op — the reader gets `done` immediately.
283
+ *
284
+ * No timeout guard: this stream is always the `fullStream` from a Mastra
285
+ * agent call backed by a bounded LLM completion. The model will always
286
+ * emit a stop reason and Mastra will close the stream. A true hang (e.g.,
287
+ * network drop with no gateway timeout) would surface as a rejected
288
+ * promise from `reader.read()`, not an infinite loop.
289
+ */
290
+ private drainReadableStream;
291
+ }
292
+ export {};