@salesforce/sfdx-agent-harness-mastra 0.9.0 → 0.11.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 +48 -11
- package/dist/index.d.ts +1 -1
- package/dist/mastra-harness-factory.d.ts +19 -2
- package/dist/mastra-harness-factory.js +8 -4
- package/dist/mastra-harness.d.ts +93 -7
- package/dist/mastra-harness.js +253 -43
- package/dist/mcp-config-mapper.d.ts +4 -1
- package/dist/mcp-config-mapper.js +8 -2
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -46,14 +46,15 @@ Linux and macOS release the handle eagerly; the retry options are no-ops on thos
|
|
|
46
46
|
|
|
47
47
|
### Core
|
|
48
48
|
|
|
49
|
-
| Export | Kind | Description
|
|
50
|
-
| ------------------------------------- | --------- |
|
|
51
|
-
| `MastraHarnessFactory` | class | `HarnessFactory` that produces a Mastra-backed `AgentHarness` bound to a storage root.
|
|
52
|
-
| `
|
|
53
|
-
| `
|
|
54
|
-
| `
|
|
55
|
-
| `
|
|
56
|
-
| `
|
|
49
|
+
| Export | Kind | Description |
|
|
50
|
+
| ------------------------------------- | --------- | ------------------------------------------------------------------------------------------- |
|
|
51
|
+
| `MastraHarnessFactory` | class | `HarnessFactory` that produces a Mastra-backed `AgentHarness` bound to a storage root. |
|
|
52
|
+
| `MastraHarnessFactoryConfig` | interface | Configuration for `MastraHarnessFactory` (`modelConfigFactory?`, `toolApprovalTimeoutMs?`). |
|
|
53
|
+
| `MastraModelConfigFactory` | interface | Strategy for creating model configurations; injected into `MastraHarnessFactory`. |
|
|
54
|
+
| `ResolvedModelConfig` | type | Return type of `MastraModelConfigFactory.create()` — wraps a `MastraModelConfig`. |
|
|
55
|
+
| `DefaultMastraModelConfigFactory` | class | Default factory using the Salesforce LLM Gateway (`SalesforceMastraModelGateway`). |
|
|
56
|
+
| `LLMGExpressMastraModelConfigFactory` | class | Factory for LLM Gateway Express (API-key-authenticated, OpenAI-compatible endpoint). |
|
|
57
|
+
| `LLMGExpressConfig` | type | Configuration for `LLMGExpressMastraModelConfigFactory` (`apiKey`, `baseUrl`, `modelId`). |
|
|
57
58
|
|
|
58
59
|
### Extension surface
|
|
59
60
|
|
|
@@ -151,19 +152,55 @@ for await (const event of result.eventStream) {
|
|
|
151
152
|
}
|
|
152
153
|
```
|
|
153
154
|
|
|
155
|
+
### Tool approval timeout
|
|
156
|
+
|
|
157
|
+
When `requireToolApproval: true` is set on a `chat()` call, the harness suspends the workflow on each
|
|
158
|
+
`tool-approval-request` and waits for the consumer to call `approveToolCall(...)` / `declineToolCall(...)` /
|
|
159
|
+
`submitToolResult(...)`. To bound how long a pending approval can wait — for example, when a UI process crashes or a
|
|
160
|
+
user walks away — the factory accepts a `toolApprovalTimeoutMs` option. It defaults to `600_000` (10 minutes) and
|
|
161
|
+
matches the `ClaudeHarnessFactoryConfig` knob of the same name.
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
const harnessFactory = new MastraHarnessFactory({
|
|
165
|
+
toolApprovalTimeoutMs: 60_000, // auto-deny pending approvals after 60s
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
When the timer fires, the harness aborts the suspended Mastra run, drops the `activeRuns` entry for the thread, and
|
|
170
|
+
yields a synthetic terminal pair on the active `eventStream`:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
{ type: 'error', error: Error('Tool approval timed out after 60000ms.'), code: 'tool-approval-timeout' }
|
|
174
|
+
{ type: 'finish', finishReason: 'error' }
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Subsequent `approveToolCall` / `declineToolCall` / `submitToolResult` calls for the timed-out tool reject with
|
|
178
|
+
`no active run found for thread "<id>"`. The granularity is per thread — each suspended `ActiveRun` arms its own timer
|
|
179
|
+
because Mastra suspends one tool approval at a time per thread; multi-tool turns get a fresh timer with each
|
|
180
|
+
continuation. To observe the timeout, keep iterating the `eventStream` past the `tool-approval-request` event so the
|
|
181
|
+
synthetic terminal pair lands in your loop:
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
for await (const event of result.eventStream) {
|
|
185
|
+
if (event.type === 'error' && event.code === 'tool-approval-timeout') {
|
|
186
|
+
// Pending approval was never answered.
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
154
191
|
### Using LLM Gateway Express
|
|
155
192
|
|
|
156
193
|
```ts
|
|
157
194
|
import { createAgentManager } from '@salesforce/sfdx-agent-sdk';
|
|
158
195
|
import { MastraHarnessFactory, LLMGExpressMastraModelConfigFactory } from '@salesforce/sfdx-agent-harness-mastra';
|
|
159
196
|
|
|
160
|
-
const harnessFactory = new MastraHarnessFactory(
|
|
161
|
-
new LLMGExpressMastraModelConfigFactory({
|
|
197
|
+
const harnessFactory = new MastraHarnessFactory({
|
|
198
|
+
modelConfigFactory: new LLMGExpressMastraModelConfigFactory({
|
|
162
199
|
apiKey: process.env.LLMG_EXPRESS_API_KEY!,
|
|
163
200
|
baseUrl: process.env.LLMG_EXPRESS_BASE_URL!,
|
|
164
201
|
modelId: 'claude-sonnet-4-20250514',
|
|
165
202
|
}),
|
|
166
|
-
);
|
|
203
|
+
});
|
|
167
204
|
|
|
168
205
|
const manager = await createAgentManager('/path/to/storage', harnessFactory);
|
|
169
206
|
```
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { MastraHarnessFactory } from './mastra-harness-factory.js';
|
|
1
|
+
export { MastraHarnessFactory, type MastraHarnessFactoryConfig } from './mastra-harness-factory.js';
|
|
2
2
|
export type { MastraAgentHarness } from './mastra-harness.js';
|
|
3
3
|
export { type MastraAgentConfig, type MastraToolSearchConfig, type SkillSearchHandle, type ToolSearchHandle, formatMcpToolName, } from './tool-search.js';
|
|
4
4
|
export type { MastraModelConfigFactory, ResolvedModelConfig } from './model-config-factory.js';
|
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
import type { HarnessFactory } from '@salesforce/sfdx-agent-sdk';
|
|
2
2
|
import { type MastraAgentHarness } from './mastra-harness.js';
|
|
3
3
|
import type { MastraModelConfigFactory } from './model-config-factory.js';
|
|
4
|
+
export interface MastraHarnessFactoryConfig {
|
|
5
|
+
/**
|
|
6
|
+
* Strategy for resolving the Mastra model configuration. Defaults to
|
|
7
|
+
* `DefaultMastraModelConfigFactory`, which targets the standard LLM
|
|
8
|
+
* Gateway path.
|
|
9
|
+
*/
|
|
10
|
+
modelConfigFactory?: MastraModelConfigFactory;
|
|
11
|
+
/**
|
|
12
|
+
* How long to wait for a `requireToolApproval` consumer to call
|
|
13
|
+
* `approveToolCall` / `declineToolCall` before auto-denying via
|
|
14
|
+
* `agent.abortRunStream(...)` and yielding a synthetic `error`
|
|
15
|
+
* (`code: 'tool-approval-timeout'`) + `finish` (`finishReason: 'error'`)
|
|
16
|
+
* terminal pair. Defaults to `600_000` (10 minutes), matching
|
|
17
|
+
* `ClaudeHarnessFactoryConfig.toolApprovalTimeoutMs`.
|
|
18
|
+
*/
|
|
19
|
+
toolApprovalTimeoutMs?: number;
|
|
20
|
+
}
|
|
4
21
|
/** Mastra-backed factory for creating AgentHarness instances. */
|
|
5
22
|
export declare class MastraHarnessFactory implements HarnessFactory<MastraAgentHarness> {
|
|
6
|
-
private readonly
|
|
23
|
+
private readonly config;
|
|
7
24
|
readonly harnessId = "mastra";
|
|
8
25
|
readonly protocolVersion = 1;
|
|
9
|
-
constructor(
|
|
26
|
+
constructor(config?: MastraHarnessFactoryConfig);
|
|
10
27
|
create(storageRootFolder: string): Promise<MastraAgentHarness>;
|
|
11
28
|
}
|
|
@@ -3,16 +3,20 @@
|
|
|
3
3
|
* See LICENSE.txt for license terms.
|
|
4
4
|
*/
|
|
5
5
|
import { MastraHarness } from './mastra-harness.js';
|
|
6
|
+
const DEFAULT_TOOL_APPROVAL_TIMEOUT_MS = 600_000;
|
|
6
7
|
/** Mastra-backed factory for creating AgentHarness instances. */
|
|
7
8
|
export class MastraHarnessFactory {
|
|
8
|
-
|
|
9
|
+
config;
|
|
9
10
|
harnessId = 'mastra';
|
|
10
11
|
protocolVersion = 1;
|
|
11
|
-
constructor(
|
|
12
|
-
this.
|
|
12
|
+
constructor(config = {}) {
|
|
13
|
+
this.config = config;
|
|
13
14
|
}
|
|
14
15
|
async create(storageRootFolder) {
|
|
15
|
-
return new MastraHarness(storageRootFolder,
|
|
16
|
+
return new MastraHarness(storageRootFolder, {
|
|
17
|
+
modelConfigFactory: this.config.modelConfigFactory,
|
|
18
|
+
toolApprovalTimeoutMs: this.config.toolApprovalTimeoutMs ?? DEFAULT_TOOL_APPROVAL_TIMEOUT_MS,
|
|
19
|
+
});
|
|
16
20
|
}
|
|
17
21
|
}
|
|
18
22
|
//# sourceMappingURL=mastra-harness-factory.js.map
|
package/dist/mastra-harness.d.ts
CHANGED
|
@@ -33,6 +33,31 @@ interface ActiveRun {
|
|
|
33
33
|
runId: string;
|
|
34
34
|
fullStream: ReadableStream<ChunkType>;
|
|
35
35
|
disposeAbortListener?: () => void;
|
|
36
|
+
/**
|
|
37
|
+
* Clears the per-run tool-approval timeout (`setTimeout` handle wrapped in
|
|
38
|
+
* a closure). Invoked at every site that overwrites or removes the entry,
|
|
39
|
+
* mirroring `disposeAbortListener` semantics. See ARCHITECTURE.md →
|
|
40
|
+
* "Approval timeout (#408)".
|
|
41
|
+
*/
|
|
42
|
+
disposeTimeout?: () => void;
|
|
43
|
+
/**
|
|
44
|
+
* Whether the originating `stream()` call was made with
|
|
45
|
+
* `requireToolApproval: true`. Carried forward across continuations
|
|
46
|
+
* (`approveToolCall`, `declineToolCall`, `submitToolResult`) so each
|
|
47
|
+
* continuation arms a fresh approval-timeout timer.
|
|
48
|
+
*/
|
|
49
|
+
requireToolApproval?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Set by `handleApprovalTimeout` and read by `createEventStream`'s
|
|
52
|
+
* `Promise.race` against the next `reader.read()`. When the race
|
|
53
|
+
* resolves to this signal, the generator yields the synthetic `error`
|
|
54
|
+
* (`code: 'tool-approval-timeout'`) + `finish` terminal pair without
|
|
55
|
+
* needing the underlying `fullStream` to close — Mastra's
|
|
56
|
+
* `abortRunStream` is a silent no-op for suspended runs, so we cannot
|
|
57
|
+
* count on the stream closing on its own.
|
|
58
|
+
*/
|
|
59
|
+
timedOutSignal?: Promise<'timed-out'>;
|
|
60
|
+
timedOut?: boolean;
|
|
36
61
|
}
|
|
37
62
|
/**
|
|
38
63
|
* Per-server MCP state. Each enabled server gets its own MCPClient and
|
|
@@ -134,11 +159,20 @@ export type MastraAgentHarness = WithAgentConfig<AgentHarness & {
|
|
|
134
159
|
};
|
|
135
160
|
};
|
|
136
161
|
}, MastraAgentConfig>;
|
|
162
|
+
/**
|
|
163
|
+
* Construction options for {@link MastraHarness}. Internal — consumers
|
|
164
|
+
* configure the harness via {@link MastraHarnessFactory} (which forwards a
|
|
165
|
+
* curated subset of these fields). Tests pass stubs (`clock`, `idGenerator`)
|
|
166
|
+
* directly.
|
|
167
|
+
*/
|
|
168
|
+
export interface MastraHarnessOptions {
|
|
169
|
+
modelConfigFactory?: MastraModelConfigFactory;
|
|
170
|
+
clock?: Clock;
|
|
171
|
+
idGenerator?: UniqueIDGenerator;
|
|
172
|
+
toolApprovalTimeoutMs?: number;
|
|
173
|
+
}
|
|
137
174
|
export declare class MastraHarness implements MastraAgentHarness {
|
|
138
175
|
private readonly storageRootFolder;
|
|
139
|
-
private readonly modelConfigFactory;
|
|
140
|
-
private readonly clock;
|
|
141
|
-
private readonly idGenerator;
|
|
142
176
|
readonly harnessId = "mastra";
|
|
143
177
|
readonly protocolVersion = 1;
|
|
144
178
|
/**
|
|
@@ -164,7 +198,11 @@ export declare class MastraHarness implements MastraAgentHarness {
|
|
|
164
198
|
* SDK never reads this; consumers reach through `manager.extensions.mastra`.
|
|
165
199
|
*/
|
|
166
200
|
readonly extensions: MastraAgentHarness['extensions'];
|
|
167
|
-
|
|
201
|
+
private readonly modelConfigFactory;
|
|
202
|
+
private readonly clock;
|
|
203
|
+
private readonly idGenerator;
|
|
204
|
+
private readonly toolApprovalTimeoutMs;
|
|
205
|
+
constructor(storageRootFolder: string, opts?: MastraHarnessOptions);
|
|
168
206
|
/**
|
|
169
207
|
* @requirements
|
|
170
208
|
* - MUST disconnect the mcpClient for every active agent before clearing state.
|
|
@@ -429,13 +467,13 @@ export declare class MastraHarness implements MastraAgentHarness {
|
|
|
429
467
|
private toChatStreamResult;
|
|
430
468
|
/**
|
|
431
469
|
* Replace (or set) the active run for a thread, disposing any prior
|
|
432
|
-
* entry's per-run abort listener
|
|
433
|
-
* new run.
|
|
470
|
+
* entry's per-run abort listener and approval-timeout timer so neither
|
|
471
|
+
* can fire on the new run.
|
|
434
472
|
*/
|
|
435
473
|
private replaceActiveRun;
|
|
436
474
|
/**
|
|
437
475
|
* Remove the active run for a thread, disposing its per-run abort listener
|
|
438
|
-
* if present. Safe to call when no entry exists.
|
|
476
|
+
* and approval-timeout timer if present. Safe to call when no entry exists.
|
|
439
477
|
*/
|
|
440
478
|
private removeActiveRun;
|
|
441
479
|
/**
|
|
@@ -457,6 +495,46 @@ export declare class MastraHarness implements MastraAgentHarness {
|
|
|
457
495
|
*/
|
|
458
496
|
private installPerRunAbortListener;
|
|
459
497
|
private handlePerRunAbort;
|
|
498
|
+
/**
|
|
499
|
+
* Arm a one-shot tool-approval timer for `run`. Returns a disposer that
|
|
500
|
+
* clears the timer; callers must invoke it whenever the entry is replaced
|
|
501
|
+
* or removed so a stale timer cannot fire on an unrelated continuation.
|
|
502
|
+
*
|
|
503
|
+
* Closes #408 — without this, an unanswered `requireToolApproval: true`
|
|
504
|
+
* request leaves Mastra's prepared run alive and `activeRuns[threadId]`
|
|
505
|
+
* pointing at a `runId` the consumer never settles, so the suspended
|
|
506
|
+
* workflow leaks until the manager shuts down. Mirrors the
|
|
507
|
+
* `ClaudeStreamBroker` per-pending-approval timer in
|
|
508
|
+
* `@salesforce/sfdx-agent-harness-claude`; the granularity differs (per
|
|
509
|
+
* thread here, per `toolCallId` there) because Mastra suspends one tool
|
|
510
|
+
* approval at a time per thread.
|
|
511
|
+
*/
|
|
512
|
+
private installApprovalTimeout;
|
|
513
|
+
/**
|
|
514
|
+
* Fire-once handler invoked when `installApprovalTimeout`'s timer expires
|
|
515
|
+
* before the consumer calls `approveToolCall` / `declineToolCall` /
|
|
516
|
+
* `submitToolResult`.
|
|
517
|
+
*
|
|
518
|
+
* Identity guard: if the entry has already been replaced or removed
|
|
519
|
+
* (consumer settled in flight, abort fired, agent destroyed), no-op. Same
|
|
520
|
+
* race posture as `handlePerRunAbort` and as Claude's
|
|
521
|
+
* `ClaudeStreamBroker.handleApprovalTimeout` early-return.
|
|
522
|
+
*
|
|
523
|
+
* On fire: flag the run, drop the entry from `activeRuns` (so subsequent
|
|
524
|
+
* `approveToolCall` etc. surface the SDK-level "no active run found"
|
|
525
|
+
* guard), call `agent.abortRunStream(runId)` (best-effort — for a
|
|
526
|
+
* suspended run this is a no-op since the prepared-runs registry has
|
|
527
|
+
* already released it), and resolve the `timedOutSignal` so
|
|
528
|
+
* `createEventStream`'s `Promise.race` against the next `reader.read()`
|
|
529
|
+
* picks the timeout signal and yields the synthetic `error`
|
|
530
|
+
* (`code: 'tool-approval-timeout'`) + `finish` terminal pair. The #409
|
|
531
|
+
* abort path doesn't need a side signal because Mastra closes the
|
|
532
|
+
* fullStream natively when the caller's `abortSignal` fires; the timeout
|
|
533
|
+
* path has no such signal and Mastra's `abortRunStream` is a silent
|
|
534
|
+
* no-op for suspended runs, so the generator must learn about the
|
|
535
|
+
* timeout out-of-band.
|
|
536
|
+
*/
|
|
537
|
+
private handleApprovalTimeout;
|
|
460
538
|
private getMemoryOrThrow;
|
|
461
539
|
private agentResourceId;
|
|
462
540
|
/**
|
|
@@ -472,6 +550,14 @@ export declare class MastraHarness implements MastraAgentHarness {
|
|
|
472
550
|
* actually match what the processor stored.
|
|
473
551
|
*/
|
|
474
552
|
private buildRequestContext;
|
|
553
|
+
/**
|
|
554
|
+
* Resolve when `signal` aborts; resolves immediately if already aborted.
|
|
555
|
+
* Used in `createEventStream`'s suspended-wait branch so the generator
|
|
556
|
+
* wakes on `controller.abort()` even though the underlying `fullStream`
|
|
557
|
+
* has already returned `done` (Mastra closes the stream right after
|
|
558
|
+
* `tool-call-approval`).
|
|
559
|
+
*/
|
|
560
|
+
private waitForAbort;
|
|
475
561
|
private createEventStream;
|
|
476
562
|
private readableStreamToAsyncGenerator;
|
|
477
563
|
/**
|
package/dist/mastra-harness.js
CHANGED
|
@@ -302,9 +302,6 @@ function mapMastraToolsToMcpToolInfo(tools, logBus) {
|
|
|
302
302
|
}
|
|
303
303
|
export class MastraHarness {
|
|
304
304
|
storageRootFolder;
|
|
305
|
-
modelConfigFactory;
|
|
306
|
-
clock;
|
|
307
|
-
idGenerator;
|
|
308
305
|
harnessId = 'mastra';
|
|
309
306
|
protocolVersion = 1;
|
|
310
307
|
/**
|
|
@@ -343,11 +340,16 @@ export class MastraHarness {
|
|
|
343
340
|
},
|
|
344
341
|
},
|
|
345
342
|
};
|
|
346
|
-
|
|
343
|
+
modelConfigFactory;
|
|
344
|
+
clock;
|
|
345
|
+
idGenerator;
|
|
346
|
+
toolApprovalTimeoutMs;
|
|
347
|
+
constructor(storageRootFolder, opts = {}) {
|
|
347
348
|
this.storageRootFolder = storageRootFolder;
|
|
348
|
-
this.modelConfigFactory = modelConfigFactory;
|
|
349
|
-
this.clock = clock;
|
|
350
|
-
this.idGenerator = idGenerator;
|
|
349
|
+
this.modelConfigFactory = opts.modelConfigFactory ?? new DefaultMastraModelConfigFactory();
|
|
350
|
+
this.clock = opts.clock ?? new RealClock();
|
|
351
|
+
this.idGenerator = opts.idGenerator ?? new UUIDGenerator();
|
|
352
|
+
this.toolApprovalTimeoutMs = opts.toolApprovalTimeoutMs ?? 600_000;
|
|
351
353
|
// Construct the libsql Client ourselves and inject it into LibSQLStore
|
|
352
354
|
// via the `{ client }` constructor form. We keep our own reference so
|
|
353
355
|
// shutdown() can call client.close() — Mastra's shutdown() does not
|
|
@@ -403,8 +405,10 @@ export class MastraHarness {
|
|
|
403
405
|
*/
|
|
404
406
|
async shutdown() {
|
|
405
407
|
await Promise.all([...this.activeAgents.values()].map(async (state) => {
|
|
406
|
-
for (const run of state.activeRuns.values())
|
|
408
|
+
for (const run of state.activeRuns.values()) {
|
|
407
409
|
run.disposeAbortListener?.();
|
|
410
|
+
run.disposeTimeout?.();
|
|
411
|
+
}
|
|
408
412
|
state.activeRuns.clear();
|
|
409
413
|
await Promise.allSettled(Array.from(state.mcpServers.values()).map(async (s) => {
|
|
410
414
|
await s.ready.catch(() => undefined);
|
|
@@ -417,6 +421,25 @@ export class MastraHarness {
|
|
|
417
421
|
// Settle the constructor's eager init before closing — see the
|
|
418
422
|
// `storageReady` field comment for the why.
|
|
419
423
|
await this.storageReady;
|
|
424
|
+
// Fold the WAL into the main DB and switch journal_mode=DELETE
|
|
425
|
+
// so SQLite unlinks the -wal/-shm sidecars on close. Without this,
|
|
426
|
+
// Windows holds the sidecars open well past `client.close()` and
|
|
427
|
+
// racing `fs.rm` calls (e.g. vitest's per-test cleanup) crash the
|
|
428
|
+
// worker. Mirrors the fix Mastra shipped on `LibSQLStore.close()`
|
|
429
|
+
// in `@mastra/libsql@1.12.0-alpha.0`. Replace this block with a
|
|
430
|
+
// call to `storage.close()` once that release ships stable — see
|
|
431
|
+
// https://github.com/forcedotcom/agentic-dx/issues/475.
|
|
432
|
+
try {
|
|
433
|
+
await this.libsqlClient.execute('PRAGMA wal_checkpoint(TRUNCATE);');
|
|
434
|
+
await this.libsqlClient.execute('PRAGMA journal_mode=DELETE;');
|
|
435
|
+
}
|
|
436
|
+
catch (err) {
|
|
437
|
+
// Best-effort: still close even if checkpoint fails (e.g. the
|
|
438
|
+
// client was already closed by a prior shutdown path).
|
|
439
|
+
this.buses.getLogBus()?.warn('libsql WAL checkpoint failed before close', {
|
|
440
|
+
error: getErrorMessage(err),
|
|
441
|
+
});
|
|
442
|
+
}
|
|
420
443
|
this.libsqlClient.close();
|
|
421
444
|
this.buses.dispose();
|
|
422
445
|
}
|
|
@@ -601,8 +624,10 @@ export class MastraHarness {
|
|
|
601
624
|
*/
|
|
602
625
|
async destroyAgent(agentId) {
|
|
603
626
|
const state = this.getAgentStateOrThrow(agentId);
|
|
604
|
-
for (const run of state.activeRuns.values())
|
|
627
|
+
for (const run of state.activeRuns.values()) {
|
|
605
628
|
run.disposeAbortListener?.();
|
|
629
|
+
run.disposeTimeout?.();
|
|
630
|
+
}
|
|
606
631
|
state.activeRuns.clear();
|
|
607
632
|
await Promise.allSettled(Array.from(state.mcpServers.values()).map(async (s) => {
|
|
608
633
|
await s.ready.catch(() => undefined);
|
|
@@ -927,7 +952,7 @@ export class MastraHarness {
|
|
|
927
952
|
abortSignal: options?.abortSignal,
|
|
928
953
|
requireToolApproval: options?.requireToolApproval,
|
|
929
954
|
});
|
|
930
|
-
return this.toChatStreamResult(agentState, threadId, output, options?.abortSignal);
|
|
955
|
+
return this.toChatStreamResult(agentState, threadId, output, options?.abortSignal, options?.requireToolApproval);
|
|
931
956
|
}
|
|
932
957
|
// ── Tool Execution ───────────────────────────────────────────────
|
|
933
958
|
/**
|
|
@@ -952,6 +977,7 @@ export class MastraHarness {
|
|
|
952
977
|
if (!activeRun) {
|
|
953
978
|
throw new Error(`MastraHarness.submitToolResult(): no active run found for thread "${threadId}" on agent "${agentId}"`);
|
|
954
979
|
}
|
|
980
|
+
const requireToolApproval = activeRun.requireToolApproval;
|
|
955
981
|
await this.drainReadableStream(activeRun.fullStream);
|
|
956
982
|
const output = await agentState.agent.resumeStream({ approved: true, result: toolResult.result, isError: toolResult.isError }, {
|
|
957
983
|
runId: activeRun.runId,
|
|
@@ -962,7 +988,7 @@ export class MastraHarness {
|
|
|
962
988
|
},
|
|
963
989
|
requestContext: this.buildRequestContext(agentId, threadId),
|
|
964
990
|
});
|
|
965
|
-
return this.toChatStreamResult(agentState, threadId, output);
|
|
991
|
+
return this.toChatStreamResult(agentState, threadId, output, undefined, requireToolApproval);
|
|
966
992
|
}
|
|
967
993
|
// ── Tool Approval ────────────────────────────────────────────────
|
|
968
994
|
/**
|
|
@@ -990,6 +1016,7 @@ export class MastraHarness {
|
|
|
990
1016
|
if (!activeRun) {
|
|
991
1017
|
throw new Error(`MastraHarness.approveToolCall(): no active run found for thread "${threadId}" on agent "${agentId}"`);
|
|
992
1018
|
}
|
|
1019
|
+
const requireToolApproval = activeRun.requireToolApproval;
|
|
993
1020
|
await this.drainReadableStream(activeRun.fullStream);
|
|
994
1021
|
const output = await agentState.agent.approveToolCall({
|
|
995
1022
|
runId: activeRun.runId,
|
|
@@ -1000,7 +1027,7 @@ export class MastraHarness {
|
|
|
1000
1027
|
},
|
|
1001
1028
|
requestContext: this.buildRequestContext(agentId, threadId),
|
|
1002
1029
|
});
|
|
1003
|
-
return this.toChatStreamResult(agentState, threadId, output);
|
|
1030
|
+
return this.toChatStreamResult(agentState, threadId, output, undefined, requireToolApproval);
|
|
1004
1031
|
}
|
|
1005
1032
|
/**
|
|
1006
1033
|
* @requirements
|
|
@@ -1022,6 +1049,7 @@ export class MastraHarness {
|
|
|
1022
1049
|
if (!activeRun) {
|
|
1023
1050
|
throw new Error(`MastraHarness.declineToolCall(): no active run found for thread "${threadId}" on agent "${agentId}"`);
|
|
1024
1051
|
}
|
|
1052
|
+
const requireToolApproval = activeRun.requireToolApproval;
|
|
1025
1053
|
await this.drainReadableStream(activeRun.fullStream);
|
|
1026
1054
|
const output = await agentState.agent.declineToolCall({
|
|
1027
1055
|
runId: activeRun.runId,
|
|
@@ -1032,7 +1060,7 @@ export class MastraHarness {
|
|
|
1032
1060
|
},
|
|
1033
1061
|
requestContext: this.buildRequestContext(agentId, threadId),
|
|
1034
1062
|
});
|
|
1035
|
-
return this.toChatStreamResult(agentState, threadId, output);
|
|
1063
|
+
return this.toChatStreamResult(agentState, threadId, output, undefined, requireToolApproval);
|
|
1036
1064
|
}
|
|
1037
1065
|
// ── Message History ──────────────────────────────────────────────
|
|
1038
1066
|
/**
|
|
@@ -1330,40 +1358,53 @@ export class MastraHarness {
|
|
|
1330
1358
|
}
|
|
1331
1359
|
});
|
|
1332
1360
|
}
|
|
1333
|
-
toChatStreamResult(agentState, threadId, output, abortSignal) {
|
|
1361
|
+
toChatStreamResult(agentState, threadId, output, abortSignal, requireToolApproval) {
|
|
1334
1362
|
// Entries are keyed by threadId so each thread holds at most one entry.
|
|
1335
1363
|
// Stale entries are overwritten here on the next stream; explicit cleanup
|
|
1336
1364
|
// happens via destroyThread() / destroyAgent() / shutdown(). Proactive
|
|
1337
1365
|
// removal on stream completion is not safe because the suspend/resume
|
|
1338
1366
|
// flow (submitToolResult, approveToolCall) needs the entry after the stream closes.
|
|
1339
1367
|
const newRun = { runId: output.runId, fullStream: output.fullStream };
|
|
1368
|
+
if (requireToolApproval) {
|
|
1369
|
+
newRun.requireToolApproval = true;
|
|
1370
|
+
// Deferred promise the generator races against reader.read() so the
|
|
1371
|
+
// timer wins without depending on the stream closing — Mastra's
|
|
1372
|
+
// abortRunStream is a silent no-op for suspended runs.
|
|
1373
|
+
let resolveTimedOut;
|
|
1374
|
+
newRun.timedOutSignal = new Promise((resolve) => {
|
|
1375
|
+
resolveTimedOut = resolve;
|
|
1376
|
+
});
|
|
1377
|
+
newRun.disposeTimeout = this.installApprovalTimeout(agentState, threadId, newRun, resolveTimedOut);
|
|
1378
|
+
}
|
|
1340
1379
|
if (abortSignal) {
|
|
1341
1380
|
newRun.disposeAbortListener = this.installPerRunAbortListener(agentState, threadId, newRun, abortSignal);
|
|
1342
1381
|
}
|
|
1343
1382
|
this.replaceActiveRun(agentState, threadId, newRun);
|
|
1344
1383
|
const adapter = new MastraEventAdapter(this.buses.getLogBus());
|
|
1345
1384
|
return {
|
|
1346
|
-
eventStream: this.createEventStream(output.fullStream, adapter, agentState, abortSignal),
|
|
1385
|
+
eventStream: this.createEventStream(output.fullStream, adapter, agentState, newRun, abortSignal),
|
|
1347
1386
|
textStream: this.readableStreamToAsyncGenerator(output.textStream),
|
|
1348
1387
|
};
|
|
1349
1388
|
}
|
|
1350
1389
|
/**
|
|
1351
1390
|
* Replace (or set) the active run for a thread, disposing any prior
|
|
1352
|
-
* entry's per-run abort listener
|
|
1353
|
-
* new run.
|
|
1391
|
+
* entry's per-run abort listener and approval-timeout timer so neither
|
|
1392
|
+
* can fire on the new run.
|
|
1354
1393
|
*/
|
|
1355
1394
|
replaceActiveRun(agentState, threadId, run) {
|
|
1356
1395
|
const prior = agentState.activeRuns.get(threadId);
|
|
1357
1396
|
prior?.disposeAbortListener?.();
|
|
1397
|
+
prior?.disposeTimeout?.();
|
|
1358
1398
|
agentState.activeRuns.set(threadId, run);
|
|
1359
1399
|
}
|
|
1360
1400
|
/**
|
|
1361
1401
|
* Remove the active run for a thread, disposing its per-run abort listener
|
|
1362
|
-
* if present. Safe to call when no entry exists.
|
|
1402
|
+
* and approval-timeout timer if present. Safe to call when no entry exists.
|
|
1363
1403
|
*/
|
|
1364
1404
|
removeActiveRun(agentState, threadId) {
|
|
1365
1405
|
const prior = agentState.activeRuns.get(threadId);
|
|
1366
1406
|
prior?.disposeAbortListener?.();
|
|
1407
|
+
prior?.disposeTimeout?.();
|
|
1367
1408
|
agentState.activeRuns.delete(threadId);
|
|
1368
1409
|
}
|
|
1369
1410
|
/**
|
|
@@ -1413,6 +1454,7 @@ export class MastraHarness {
|
|
|
1413
1454
|
// is responsible for its run.
|
|
1414
1455
|
if (agentState.activeRuns.get(threadId) !== run)
|
|
1415
1456
|
return;
|
|
1457
|
+
run.disposeTimeout?.();
|
|
1416
1458
|
agentState.activeRuns.delete(threadId);
|
|
1417
1459
|
agentState.agent.abortRunStream(run.runId);
|
|
1418
1460
|
}
|
|
@@ -1430,6 +1472,63 @@ export class MastraHarness {
|
|
|
1430
1472
|
}
|
|
1431
1473
|
}
|
|
1432
1474
|
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Arm a one-shot tool-approval timer for `run`. Returns a disposer that
|
|
1477
|
+
* clears the timer; callers must invoke it whenever the entry is replaced
|
|
1478
|
+
* or removed so a stale timer cannot fire on an unrelated continuation.
|
|
1479
|
+
*
|
|
1480
|
+
* Closes #408 — without this, an unanswered `requireToolApproval: true`
|
|
1481
|
+
* request leaves Mastra's prepared run alive and `activeRuns[threadId]`
|
|
1482
|
+
* pointing at a `runId` the consumer never settles, so the suspended
|
|
1483
|
+
* workflow leaks until the manager shuts down. Mirrors the
|
|
1484
|
+
* `ClaudeStreamBroker` per-pending-approval timer in
|
|
1485
|
+
* `@salesforce/sfdx-agent-harness-claude`; the granularity differs (per
|
|
1486
|
+
* thread here, per `toolCallId` there) because Mastra suspends one tool
|
|
1487
|
+
* approval at a time per thread.
|
|
1488
|
+
*/
|
|
1489
|
+
installApprovalTimeout(agentState, threadId, run, signalTimedOut) {
|
|
1490
|
+
const timer = setTimeout(() => this.handleApprovalTimeout(agentState, threadId, run, signalTimedOut), this.toolApprovalTimeoutMs);
|
|
1491
|
+
return () => clearTimeout(timer);
|
|
1492
|
+
}
|
|
1493
|
+
/**
|
|
1494
|
+
* Fire-once handler invoked when `installApprovalTimeout`'s timer expires
|
|
1495
|
+
* before the consumer calls `approveToolCall` / `declineToolCall` /
|
|
1496
|
+
* `submitToolResult`.
|
|
1497
|
+
*
|
|
1498
|
+
* Identity guard: if the entry has already been replaced or removed
|
|
1499
|
+
* (consumer settled in flight, abort fired, agent destroyed), no-op. Same
|
|
1500
|
+
* race posture as `handlePerRunAbort` and as Claude's
|
|
1501
|
+
* `ClaudeStreamBroker.handleApprovalTimeout` early-return.
|
|
1502
|
+
*
|
|
1503
|
+
* On fire: flag the run, drop the entry from `activeRuns` (so subsequent
|
|
1504
|
+
* `approveToolCall` etc. surface the SDK-level "no active run found"
|
|
1505
|
+
* guard), call `agent.abortRunStream(runId)` (best-effort — for a
|
|
1506
|
+
* suspended run this is a no-op since the prepared-runs registry has
|
|
1507
|
+
* already released it), and resolve the `timedOutSignal` so
|
|
1508
|
+
* `createEventStream`'s `Promise.race` against the next `reader.read()`
|
|
1509
|
+
* picks the timeout signal and yields the synthetic `error`
|
|
1510
|
+
* (`code: 'tool-approval-timeout'`) + `finish` terminal pair. The #409
|
|
1511
|
+
* abort path doesn't need a side signal because Mastra closes the
|
|
1512
|
+
* fullStream natively when the caller's `abortSignal` fires; the timeout
|
|
1513
|
+
* path has no such signal and Mastra's `abortRunStream` is a silent
|
|
1514
|
+
* no-op for suspended runs, so the generator must learn about the
|
|
1515
|
+
* timeout out-of-band.
|
|
1516
|
+
*/
|
|
1517
|
+
handleApprovalTimeout(agentState, threadId, run, signalTimedOut) {
|
|
1518
|
+
if (agentState.activeRuns.get(threadId) !== run)
|
|
1519
|
+
return;
|
|
1520
|
+
run.timedOut = true;
|
|
1521
|
+
agentState.activeRuns.delete(threadId);
|
|
1522
|
+
try {
|
|
1523
|
+
agentState.agent.abortRunStream(run.runId);
|
|
1524
|
+
}
|
|
1525
|
+
catch (err) {
|
|
1526
|
+
this.buses
|
|
1527
|
+
.getLogBus()
|
|
1528
|
+
?.warn(`Failed to abort suspended run "${run.runId}" on tool-approval timeout for thread "${threadId}": ${getErrorMessage(err)}`, { threadId, runId: run.runId, timeoutMs: this.toolApprovalTimeoutMs }, err instanceof Error ? err : new Error(String(err)));
|
|
1529
|
+
}
|
|
1530
|
+
signalTimedOut('timed-out');
|
|
1531
|
+
}
|
|
1433
1532
|
async getMemoryOrThrow(agentId) {
|
|
1434
1533
|
const agentState = this.getAgentStateOrThrow(agentId);
|
|
1435
1534
|
const memory = await agentState.agent.getMemory();
|
|
@@ -1459,7 +1558,21 @@ export class MastraHarness {
|
|
|
1459
1558
|
ctx.set(MASTRA_RESOURCE_ID_KEY, this.agentResourceId(agentId));
|
|
1460
1559
|
return ctx;
|
|
1461
1560
|
}
|
|
1462
|
-
|
|
1561
|
+
/**
|
|
1562
|
+
* Resolve when `signal` aborts; resolves immediately if already aborted.
|
|
1563
|
+
* Used in `createEventStream`'s suspended-wait branch so the generator
|
|
1564
|
+
* wakes on `controller.abort()` even though the underlying `fullStream`
|
|
1565
|
+
* has already returned `done` (Mastra closes the stream right after
|
|
1566
|
+
* `tool-call-approval`).
|
|
1567
|
+
*/
|
|
1568
|
+
waitForAbort(signal) {
|
|
1569
|
+
if (signal.aborted)
|
|
1570
|
+
return Promise.resolve('aborted');
|
|
1571
|
+
return new Promise((resolve) => {
|
|
1572
|
+
signal.addEventListener('abort', () => resolve('aborted'), { once: true });
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
async *createEventStream(fullStream, adapter, agentState, run, abortSignal) {
|
|
1463
1576
|
// Tracks whether the consumer has observed a terminal event already.
|
|
1464
1577
|
// Either `finish` (natural completion) or any `error` event counts —
|
|
1465
1578
|
// the SDK's `wrapEventStream` treats any error as terminal and
|
|
@@ -1468,33 +1581,122 @@ export class MastraHarness {
|
|
|
1468
1581
|
// per turn. The narrower "only abort-coded errors count" predicate
|
|
1469
1582
|
// would mis-fire on tripwire/regular errors that race the abort.
|
|
1470
1583
|
let sawTerminal = false;
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1584
|
+
// Tracks whether the consumer is currently sitting on a yielded
|
|
1585
|
+
// `tool-approval-request` with no settle/abort/timeout decision yet.
|
|
1586
|
+
// When Mastra suspends a run it closes the underlying `fullStream`
|
|
1587
|
+
// immediately after the `tool-call-approval` chunk — `reader.read()`
|
|
1588
|
+
// returns `{ done: true }` synchronously. If we treated that as
|
|
1589
|
+
// "stream completed", the timeout timer would never get a chance to
|
|
1590
|
+
// fire (the generator would exit before the timer's macrotask), and
|
|
1591
|
+
// the consumer's `for await` would terminate with no terminal event.
|
|
1592
|
+
// While suspended, wait on the per-run timeout signal instead — the
|
|
1593
|
+
// timer is the only thing that can move the run forward.
|
|
1594
|
+
let approvalPending = false;
|
|
1595
|
+
const reader = fullStream.getReader();
|
|
1596
|
+
// Wraps a `read()` so `Promise.race` can disambiguate which side won.
|
|
1597
|
+
// The timeout signal resolves to the literal `'timed-out'`; a `read()`
|
|
1598
|
+
// rejection is reported as `{ kind: 'rejected', err }` and the loop
|
|
1599
|
+
// body decides how to interpret it: silence ONLY when `run.timedOut`
|
|
1600
|
+
// is set (the lock-release race after the timer fires is benign);
|
|
1601
|
+
// anything else is a real upstream error that must propagate so the
|
|
1602
|
+
// SDK's `wrapEventStream` boundary can synthesize the terminal pair.
|
|
1603
|
+
const readChunk = () => reader.read().then((r) => (r.done ? { kind: 'done' } : { kind: 'chunk', value: r.value }), (err) => ({ kind: 'rejected', err }));
|
|
1604
|
+
try {
|
|
1605
|
+
while (true) {
|
|
1606
|
+
let outcome;
|
|
1607
|
+
if (approvalPending) {
|
|
1608
|
+
// Stream is suspended on a tool-approval. The underlying
|
|
1609
|
+
// `fullStream` returns `done` immediately after Mastra
|
|
1610
|
+
// emits `tool-call-approval` (snapshot is persisted, then
|
|
1611
|
+
// the stream closes), so we cannot rely on it for further
|
|
1612
|
+
// signal — wait for whichever side decides the run's fate
|
|
1613
|
+
// (timeout fire or caller's abortSignal). Continued reads
|
|
1614
|
+
// would loop returning `done` and burn CPU. If neither
|
|
1615
|
+
// signal is present (e.g. consumer set `requireToolApproval:
|
|
1616
|
+
// true` without an `abortSignal` on a harness with no
|
|
1617
|
+
// timeout), fall through to a plain `readChunk` — the
|
|
1618
|
+
// generator will exit cleanly on `done` and the consumer
|
|
1619
|
+
// gets no terminal, matching the pre-fix behavior for
|
|
1620
|
+
// that misconfiguration.
|
|
1621
|
+
const waiters = [];
|
|
1622
|
+
if (run.timedOutSignal)
|
|
1623
|
+
waiters.push(run.timedOutSignal);
|
|
1624
|
+
if (abortSignal)
|
|
1625
|
+
waiters.push(this.waitForAbort(abortSignal));
|
|
1626
|
+
outcome = waiters.length > 0 ? await Promise.race(waiters) : await readChunk();
|
|
1627
|
+
}
|
|
1628
|
+
else {
|
|
1629
|
+
// Race the next read against the per-run timeout signal so
|
|
1630
|
+
// the generator wakes up immediately when the timer fires —
|
|
1631
|
+
// Mastra's `abortRunStream` is a silent no-op for suspended
|
|
1632
|
+
// runs, so the underlying stream may never close on its
|
|
1633
|
+
// own.
|
|
1634
|
+
outcome = run.timedOutSignal
|
|
1635
|
+
? await Promise.race([readChunk(), run.timedOutSignal])
|
|
1636
|
+
: await readChunk();
|
|
1637
|
+
}
|
|
1638
|
+
if (outcome === 'timed-out')
|
|
1639
|
+
break;
|
|
1640
|
+
if (outcome === 'aborted')
|
|
1641
|
+
break;
|
|
1642
|
+
if (outcome.kind === 'done')
|
|
1643
|
+
break;
|
|
1644
|
+
if (outcome.kind === 'rejected') {
|
|
1645
|
+
// Only the lock-release race after the timer fires should
|
|
1646
|
+
// be silenced; a real upstream error must surface so
|
|
1647
|
+
// `wrapEventStream` can emit `chat-stream-error` + finish.
|
|
1648
|
+
if (run.timedOut)
|
|
1649
|
+
break;
|
|
1650
|
+
throw outcome.err instanceof Error ? outcome.err : new Error(String(outcome.err));
|
|
1651
|
+
}
|
|
1652
|
+
const event = adapter.toChatEvent(outcome.value);
|
|
1653
|
+
if (!event)
|
|
1487
1654
|
continue;
|
|
1655
|
+
if (event.type === 'finish' || event.type === 'error') {
|
|
1656
|
+
sawTerminal = true;
|
|
1657
|
+
// Stream is on its way out — disposing the timer here
|
|
1658
|
+
// prevents the 10-min armed-but-unused fire on streams
|
|
1659
|
+
// that completed naturally without ever pending an
|
|
1660
|
+
// approval. See ARCHITECTURE.md → "Approval timeout (#408)".
|
|
1661
|
+
run.disposeTimeout?.();
|
|
1488
1662
|
}
|
|
1663
|
+
if (event.type === 'tool-approval-request') {
|
|
1664
|
+
// Mastra suspends the run after this chunk; the
|
|
1665
|
+
// underlying `fullStream` will return `done` next read.
|
|
1666
|
+
// Switch to "wait for timeout" mode so we don't exit the
|
|
1667
|
+
// generator before the timer can fire.
|
|
1668
|
+
approvalPending = true;
|
|
1669
|
+
}
|
|
1670
|
+
if (isEnrichableToolEvent(event)) {
|
|
1671
|
+
const toolName = event.type === 'tool-approval-request' ? event.toolCall.toolName : event.toolName;
|
|
1672
|
+
const metadata = lookupMcpToolMetadata(agentState, toolName);
|
|
1673
|
+
if (metadata) {
|
|
1674
|
+
yield {
|
|
1675
|
+
...event,
|
|
1676
|
+
serverName: metadata.serverName,
|
|
1677
|
+
...(metadata.annotations ? { annotations: metadata.annotations } : {}),
|
|
1678
|
+
};
|
|
1679
|
+
continue;
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
yield event;
|
|
1489
1683
|
}
|
|
1490
|
-
yield event;
|
|
1491
1684
|
}
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
//
|
|
1496
|
-
//
|
|
1497
|
-
//
|
|
1685
|
+
finally {
|
|
1686
|
+
reader.releaseLock();
|
|
1687
|
+
}
|
|
1688
|
+
// The fullStream closed. If a suspended-run terminal cause fired —
|
|
1689
|
+
// consumer never answered a `requireToolApproval` request (#408) or
|
|
1690
|
+
// consumer aborted while a tool-approval was pending (#409) — Mastra
|
|
1691
|
+
// releases the prepared run silently, so the fullStream just ends
|
|
1692
|
+
// with no terminal chunk. Synthesize the terminal pair the SDK
|
|
1693
|
+
// invariant requires. See ARCHITECTURE.md → "Tool Approval Flow".
|
|
1694
|
+
//
|
|
1695
|
+
// Ordering: check `run.timedOut` BEFORE `abortSignal?.aborted` because
|
|
1696
|
+
// `handleApprovalTimeout` calls `agent.abortRunStream(...)`, which on
|
|
1697
|
+
// some Mastra paths flips the abort signal. Pinning timeout-first
|
|
1698
|
+
// means the consumer's `error.code` is the most informative one
|
|
1699
|
+
// (`'tool-approval-timeout'` rather than `'abort'`).
|
|
1498
1700
|
//
|
|
1499
1701
|
// Adapter-coupling note: this gate depends on `MastraEventAdapter`
|
|
1500
1702
|
// not yielding a `finish` chunk between `tool-call-approval` and
|
|
@@ -1504,7 +1706,15 @@ export class MastraHarness {
|
|
|
1504
1706
|
// would set `sawTerminal` early and silently suppress the synth.
|
|
1505
1707
|
// If that lands, switch the predicate to track only the terminal
|
|
1506
1708
|
// `finish` (e.g. via a per-stream "suspended" flag).
|
|
1507
|
-
if (!sawTerminal &&
|
|
1709
|
+
if (!sawTerminal && run.timedOut) {
|
|
1710
|
+
yield {
|
|
1711
|
+
type: 'error',
|
|
1712
|
+
error: new Error(`Tool approval timed out after ${this.toolApprovalTimeoutMs}ms.`),
|
|
1713
|
+
code: 'tool-approval-timeout',
|
|
1714
|
+
};
|
|
1715
|
+
yield { type: 'finish', finishReason: 'error' };
|
|
1716
|
+
}
|
|
1717
|
+
else if (!sawTerminal && abortSignal?.aborted) {
|
|
1508
1718
|
yield {
|
|
1509
1719
|
type: 'error',
|
|
1510
1720
|
error: new DOMException('The stream was aborted.', 'AbortError'),
|
|
@@ -13,7 +13,10 @@ export declare function hasEnabledServers(config: MCPConfiguration | undefined):
|
|
|
13
13
|
* - `MCPStdioServerConfig` maps to a Mastra stdio definition (`command`, `args`, `env`).
|
|
14
14
|
* - `MCPRemoteServerConfig` maps to a Mastra HTTP definition (`url` as a `URL` object,
|
|
15
15
|
* `headers` forwarded via `requestInit`).
|
|
16
|
-
* - Per-server `timeout` is preserved when provided.
|
|
16
|
+
* - Per-server `timeout` is preserved when provided. For remote servers it is bound to both
|
|
17
|
+
* Mastra's `timeout` (post-connect request lifetime) and `connectTimeout` (initial transport
|
|
18
|
+
* handshake) so the configured value covers both phases — Mastra's hardcoded 3s
|
|
19
|
+
* `connectTimeout` default would otherwise abort cold-start handshakes before `timeout` applied.
|
|
17
20
|
* - When `orgJwt` is provided, remote servers get a `fetch` that delegates to
|
|
18
21
|
* {@link resolveMcpServerHeaders} per-request to inject auth for Salesforce Platform URLs.
|
|
19
22
|
*/
|
|
@@ -19,7 +19,10 @@ export function hasEnabledServers(config) {
|
|
|
19
19
|
* - `MCPStdioServerConfig` maps to a Mastra stdio definition (`command`, `args`, `env`).
|
|
20
20
|
* - `MCPRemoteServerConfig` maps to a Mastra HTTP definition (`url` as a `URL` object,
|
|
21
21
|
* `headers` forwarded via `requestInit`).
|
|
22
|
-
* - Per-server `timeout` is preserved when provided.
|
|
22
|
+
* - Per-server `timeout` is preserved when provided. For remote servers it is bound to both
|
|
23
|
+
* Mastra's `timeout` (post-connect request lifetime) and `connectTimeout` (initial transport
|
|
24
|
+
* handshake) so the configured value covers both phases — Mastra's hardcoded 3s
|
|
25
|
+
* `connectTimeout` default would otherwise abort cold-start handshakes before `timeout` applied.
|
|
23
26
|
* - When `orgJwt` is provided, remote servers get a `fetch` that delegates to
|
|
24
27
|
* {@link resolveMcpServerHeaders} per-request to inject auth for Salesforce Platform URLs.
|
|
25
28
|
*/
|
|
@@ -46,7 +49,10 @@ export function mapToMastraMCPServers(config, orgJwt) {
|
|
|
46
49
|
requestInit: { headers: serverConfig.headers },
|
|
47
50
|
}),
|
|
48
51
|
...(orgJwt && { fetch: createAuthFetch(serverConfig.url, orgJwt, serverConfig.headers) }),
|
|
49
|
-
...(serverConfig.timeout !== undefined && {
|
|
52
|
+
...(serverConfig.timeout !== undefined && {
|
|
53
|
+
timeout: serverConfig.timeout,
|
|
54
|
+
connectTimeout: serverConfig.timeout,
|
|
55
|
+
}),
|
|
50
56
|
};
|
|
51
57
|
result[name] = def;
|
|
52
58
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/sfdx-agent-harness-mastra",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Mastra-backed AgentHarness implementation for @salesforce/sfdx-agent-sdk",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -35,14 +35,14 @@
|
|
|
35
35
|
"@mastra/memory": "^1.19.0",
|
|
36
36
|
"@mastra/schema-compat": "^1.2.10",
|
|
37
37
|
"@salesforce/agentic-common": "0.6.0",
|
|
38
|
-
"@salesforce/llm-gateway-sdk": "0.
|
|
38
|
+
"@salesforce/llm-gateway-sdk": "0.8.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@salesforce/sfdx-agent-sdk": "0.
|
|
41
|
+
"@salesforce/sfdx-agent-sdk": "0.12.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@eslint/js": "^10.0.1",
|
|
45
|
-
"@salesforce/sfdx-agent-sdk": "0.
|
|
45
|
+
"@salesforce/sfdx-agent-sdk": "0.12.0",
|
|
46
46
|
"@types/node": "^22.19.17",
|
|
47
47
|
"@vitest/coverage-istanbul": "^4.1.7",
|
|
48
48
|
"@vitest/eslint-plugin": "^1.6.17",
|