@salesforce/sfdx-agent-harness-mastra 0.89.0 → 0.91.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/CHANGELOG.md +13 -0
- package/dist/mastra-harness.d.ts +10 -10
- package/dist/mastra-harness.js +52 -55
- package/dist/v3-stream-mapping.d.ts +7 -5
- package/dist/v3-stream-mapping.js +20 -10
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
All notable changes to `@salesforce/sfdx-agent-harness-mastra` are documented in this file.
|
|
4
4
|
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
5
|
|
|
6
|
+
## [0.91.0] - 2026-09-24
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
- **agent-sdk,harness-mastra,harness-openai,harness-claude**: stable chatId across compaction — compact in place, never rotate the thread id @W-24153851@ ([#840](https://github.com/forcedotcom/agentic-dx/pull/840))
|
|
10
|
+
|
|
11
|
+
## [0.90.0] - 2026-09-23
|
|
12
|
+
|
|
13
|
+
### Fixes
|
|
14
|
+
- **harness-mastra**: emit full llm-request body in wire-communication events (remove 256KB truncation) @W-24280587@ ([#839](https://github.com/forcedotcom/agentic-dx/pull/839))
|
|
15
|
+
|
|
16
|
+
### Chores
|
|
17
|
+
- **deps**: bump @ai-sdk/provider from 4.0.4 to 4.0.14 ([#838](https://github.com/forcedotcom/agentic-dx/pull/838))
|
|
18
|
+
|
|
6
19
|
## [0.89.0] - 2026-09-21
|
|
7
20
|
|
|
8
21
|
### Features
|
package/dist/mastra-harness.d.ts
CHANGED
|
@@ -464,17 +464,17 @@ export declare class MastraHarness implements MastraAgentHarness {
|
|
|
464
464
|
/**
|
|
465
465
|
* @requirements
|
|
466
466
|
* - MUST throw an error if the specified `agentId` is not found in `activeAgents`.
|
|
467
|
-
* - MUST
|
|
467
|
+
* - MUST compact the thread IN PLACE, keeping the same `threadId` (a SQLite
|
|
468
|
+
* row PK) so `getId()` never changes across compaction (W-24153851).
|
|
468
469
|
* - MUST chunk-summarize transcripts that exceed `MAX_TRANSCRIPT_CHARS`
|
|
469
470
|
* to keep any single model call inside the context window.
|
|
470
|
-
* - MUST surface summarization or
|
|
471
|
-
* with type `COMPACTION_FAILED` and leave the
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
* hygiene, not correctness.
|
|
471
|
+
* - MUST surface summarization or commit failures as `AgentSDKError`
|
|
472
|
+
* with type `COMPACTION_FAILED` and leave the transcript intact on failure.
|
|
473
|
+
* - MUST be fail-closed: summarize FIRST (read-only), then
|
|
474
|
+
* append-the-summary-then-delete-the-originals (append-then-delete-by-
|
|
475
|
+
* captured-id, NOT a re-recall). LibSQL has no atomic multi-row swap, so a
|
|
476
|
+
* crash after the append leaves BOTH the summary and the originals
|
|
477
|
+
* (retryable, larger-but-valid) — never an empty thread.
|
|
478
478
|
* - MUST seed the summary as a user/assistant turn pair where the user
|
|
479
479
|
* turn carries the summary content and the assistant turn is a brief
|
|
480
480
|
* acknowledgment. Storing the summary as an assistant turn (even with
|
|
@@ -483,7 +483,7 @@ export declare class MastraHarness implements MastraAgentHarness {
|
|
|
483
483
|
* it as fabricated, refusing to use the facts on follow-up turns.
|
|
484
484
|
* User turns are treated as authoritative input.
|
|
485
485
|
*/
|
|
486
|
-
compactThread(agentId: string, threadId: string): Promise<
|
|
486
|
+
compactThread(agentId: string, threadId: string): Promise<void>;
|
|
487
487
|
/**
|
|
488
488
|
* Best-effort wrapper around `destroyThread`: swallows the error so the
|
|
489
489
|
* caller's outer flow isn't aborted by a teardown blip, but logs the
|
package/dist/mastra-harness.js
CHANGED
|
@@ -1429,17 +1429,17 @@ export class MastraHarness {
|
|
|
1429
1429
|
/**
|
|
1430
1430
|
* @requirements
|
|
1431
1431
|
* - MUST throw an error if the specified `agentId` is not found in `activeAgents`.
|
|
1432
|
-
* - MUST
|
|
1432
|
+
* - MUST compact the thread IN PLACE, keeping the same `threadId` (a SQLite
|
|
1433
|
+
* row PK) so `getId()` never changes across compaction (W-24153851).
|
|
1433
1434
|
* - MUST chunk-summarize transcripts that exceed `MAX_TRANSCRIPT_CHARS`
|
|
1434
1435
|
* to keep any single model call inside the context window.
|
|
1435
|
-
* - MUST surface summarization or
|
|
1436
|
-
* with type `COMPACTION_FAILED` and leave the
|
|
1437
|
-
*
|
|
1438
|
-
*
|
|
1439
|
-
*
|
|
1440
|
-
*
|
|
1441
|
-
*
|
|
1442
|
-
* hygiene, not correctness.
|
|
1436
|
+
* - MUST surface summarization or commit failures as `AgentSDKError`
|
|
1437
|
+
* with type `COMPACTION_FAILED` and leave the transcript intact on failure.
|
|
1438
|
+
* - MUST be fail-closed: summarize FIRST (read-only), then
|
|
1439
|
+
* append-the-summary-then-delete-the-originals (append-then-delete-by-
|
|
1440
|
+
* captured-id, NOT a re-recall). LibSQL has no atomic multi-row swap, so a
|
|
1441
|
+
* crash after the append leaves BOTH the summary and the originals
|
|
1442
|
+
* (retryable, larger-but-valid) — never an empty thread.
|
|
1443
1443
|
* - MUST seed the summary as a user/assistant turn pair where the user
|
|
1444
1444
|
* turn carries the summary content and the assistant turn is a brief
|
|
1445
1445
|
* acknowledgment. Storing the summary as an assistant turn (even with
|
|
@@ -1449,49 +1449,54 @@ export class MastraHarness {
|
|
|
1449
1449
|
* User turns are treated as authoritative input.
|
|
1450
1450
|
*/
|
|
1451
1451
|
async compactThread(agentId, threadId) {
|
|
1452
|
-
this.getAgentStateOrThrow(agentId);
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
catch (err) {
|
|
1464
|
-
// The new thread was already created; a carry-forward failure
|
|
1465
|
-
// orphans it (never returned, never retired). Delete it before
|
|
1466
|
-
// surfacing COMPACTION_FAILED so a failed compaction leaves no
|
|
1467
|
-
// trace beyond the still-intact source thread.
|
|
1468
|
-
await this.destroyThreadBestEffort(agentId, newThreadId);
|
|
1469
|
-
throw new AgentSDKError(`Failed to compact thread "${threadId}" for agent "${agentId}": ${getErrorMessage(err)}`, AgentSDKErrorType.COMPACTION_FAILED, { cause: err });
|
|
1470
|
-
}
|
|
1471
|
-
// Empty source still gets compacted away — leaving it attached
|
|
1472
|
-
// would be a leak. Mirrors the non-empty path's destroyThread
|
|
1473
|
-
// call below; both paths exit with the source detached.
|
|
1474
|
-
await this.destroyThreadBestEffort(agentId, threadId);
|
|
1475
|
-
return newThreadId;
|
|
1452
|
+
const agentState = this.getAgentStateOrThrow(agentId);
|
|
1453
|
+
// Defense-in-depth against a direct-SDK caller: refuse to rewrite the
|
|
1454
|
+
// transcript out from under an in-flight turn. The SDK's one-turn guard
|
|
1455
|
+
// (auto path) and the service's 409 on an explicit /compact during a turn
|
|
1456
|
+
// already exclude this upstream. Fail fast rather than dispose a live
|
|
1457
|
+
// coordinator (that surfaces a spurious DISPOSED — W-24044572). Mirrors
|
|
1458
|
+
// stream()'s guard.
|
|
1459
|
+
const priorCoordinator = agentState.coordinators.get(threadId);
|
|
1460
|
+
if (priorCoordinator && !priorCoordinator.isSettled()) {
|
|
1461
|
+
throw new AgentSDKError(`A turn is already in progress for thread "${threadId}" on agent "${agentId}". ` +
|
|
1462
|
+
'Compaction cannot run while a turn is active; await the in-flight turn first.', AgentSDKErrorType.SESSION_BUSY);
|
|
1476
1463
|
}
|
|
1464
|
+
const messages = await this.getMessages(agentId, threadId);
|
|
1465
|
+
// Empty source: nothing to compact. In-place keeps the same threadId and
|
|
1466
|
+
// its (physically-disjoint) session-context store, so this is a no-op.
|
|
1467
|
+
if (messages.length === 0)
|
|
1468
|
+
return;
|
|
1477
1469
|
const transcript = messages
|
|
1478
1470
|
.map((m) => {
|
|
1479
1471
|
const text = typeof m.content === 'string' ? m.content : m.content.map((p) => partToTranscriptText(p)).join(' ');
|
|
1480
1472
|
return `${m.role}: ${text}`;
|
|
1481
1473
|
})
|
|
1482
1474
|
.join('\n');
|
|
1483
|
-
// `undefined` until `createThread` succeeds, so the catch can tell a
|
|
1484
|
-
// pre-create failure (nothing to clean up) from a post-create failure
|
|
1485
|
-
// (the new thread is orphaned and must be deleted).
|
|
1486
|
-
let newThreadId;
|
|
1487
1475
|
try {
|
|
1476
|
+
// Capture the ORIGINAL persisted (raw) message ids to delete after
|
|
1477
|
+
// the summary is appended. Raw recall ids — not `getMessages()`'s
|
|
1478
|
+
// post-mapped ids, which split a tool call into two SDK messages —
|
|
1479
|
+
// are what `memory.deleteMessages` keys on. Captured before the
|
|
1480
|
+
// append so the summary pair's freshly-minted ids are never in the
|
|
1481
|
+
// delete set.
|
|
1482
|
+
const memory = await this.getMemoryOrThrow(agentId);
|
|
1483
|
+
const { messages: rawMessages } = await memory.recall({
|
|
1484
|
+
threadId,
|
|
1485
|
+
resourceId: this.agentResourceId(agentId),
|
|
1486
|
+
perPage: false,
|
|
1487
|
+
});
|
|
1488
|
+
const originalIds = rawMessages.map((m) => m.id);
|
|
1489
|
+
// Summarize FIRST (read-only). No mutation has happened yet, so a
|
|
1490
|
+
// summary failure leaves the transcript fully intact.
|
|
1488
1491
|
const summaryText = transcript.length <= MAX_TRANSCRIPT_CHARS
|
|
1489
1492
|
? await this.summarize(agentId, buildSummaryPrompt(transcript))
|
|
1490
1493
|
: await this.summarizeInChunks(agentId, transcript);
|
|
1491
|
-
|
|
1494
|
+
// Append the summary pair to the SAME thread, THEN delete the
|
|
1495
|
+
// originals by captured id. A crash between the two leaves both
|
|
1496
|
+
// (retryable), never an empty thread.
|
|
1492
1497
|
const userTs = this.clock.now();
|
|
1493
1498
|
const assistantTs = this.clock.nextAfter(userTs);
|
|
1494
|
-
await this.addMessages(agentId,
|
|
1499
|
+
await this.addMessages(agentId, threadId, [
|
|
1495
1500
|
{
|
|
1496
1501
|
id: this.idGenerator.getUniqueId(),
|
|
1497
1502
|
role: 'user',
|
|
@@ -1505,26 +1510,18 @@ export class MastraHarness {
|
|
|
1505
1510
|
createdAt: assistantTs,
|
|
1506
1511
|
},
|
|
1507
1512
|
]);
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
await this.carrySessionContextForward(agentId, threadId, newThreadId);
|
|
1513
|
+
if (originalIds.length > 0) {
|
|
1514
|
+
await memory.deleteMessages(originalIds);
|
|
1515
|
+
}
|
|
1512
1516
|
}
|
|
1513
1517
|
catch (err) {
|
|
1514
|
-
// If the failure came after `createThread`, the new thread is
|
|
1515
|
-
// orphaned (never returned to the caller, source not yet retired).
|
|
1516
|
-
// Delete it so a failed compaction leaves no trace beyond the
|
|
1517
|
-
// still-intact source thread.
|
|
1518
|
-
if (newThreadId !== undefined) {
|
|
1519
|
-
await this.destroyThreadBestEffort(agentId, newThreadId);
|
|
1520
|
-
}
|
|
1521
1518
|
throw new AgentSDKError(`Failed to compact thread "${threadId}" for agent "${agentId}": ${getErrorMessage(err)}`, AgentSDKErrorType.COMPACTION_FAILED, { cause: err });
|
|
1522
1519
|
}
|
|
1523
|
-
//
|
|
1524
|
-
//
|
|
1525
|
-
//
|
|
1526
|
-
|
|
1527
|
-
|
|
1520
|
+
// Reset the per-thread consumer-tool retry counter — the rotation-based
|
|
1521
|
+
// compaction used to get this for free via destroyThread; an in-place
|
|
1522
|
+
// compaction keeps the thread, so reset it explicitly. The transcript the
|
|
1523
|
+
// counts described is gone.
|
|
1524
|
+
this.toolArgsRetryCounts.get(agentId)?.delete(threadId);
|
|
1528
1525
|
}
|
|
1529
1526
|
/**
|
|
1530
1527
|
* Best-effort wrapper around `destroyThread`: swallows the error so the
|
|
@@ -280,11 +280,13 @@ export type GatewayFetchWireHooks = {
|
|
|
280
280
|
* fetch resolution to HTTP-body terminality: an owned tee branch observes only
|
|
281
281
|
* recognized assistant text deltas without blocking the provider SDK's branch.
|
|
282
282
|
* Wire-communication events are best-effort: a failed body decode does NOT
|
|
283
|
-
* prevent the call from completing.
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
283
|
+
* prevent the call from completing. When a subscriber is present the request
|
|
284
|
+
* body is captured in full with no size cap (W-24280587) — the channel is
|
|
285
|
+
* opt-in and diagnostic, so fidelity wins; with no subscriber the decode is
|
|
286
|
+
* skipped entirely (the event is dropped by the wire bus anyway), so a
|
|
287
|
+
* subscriber-less production process pays no full-body parse on the hot path.
|
|
288
|
+
* (The streaming *response* observer separately bounds its in-memory text
|
|
289
|
+
* capture at `TRACE_BODY_BYTE_BUDGET`.)
|
|
288
290
|
*/
|
|
289
291
|
export declare function createGatewayFetch(getHeaders: () => Record<string, string> | Promise<Record<string, string>>, wireHooks?: GatewayFetchWireHooks, innerFetch?: typeof fetch,
|
|
290
292
|
/**
|
|
@@ -353,11 +353,13 @@ export function mapOpenAIUsage(usage) {
|
|
|
353
353
|
* fetch resolution to HTTP-body terminality: an owned tee branch observes only
|
|
354
354
|
* recognized assistant text deltas without blocking the provider SDK's branch.
|
|
355
355
|
* Wire-communication events are best-effort: a failed body decode does NOT
|
|
356
|
-
* prevent the call from completing.
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
356
|
+
* prevent the call from completing. When a subscriber is present the request
|
|
357
|
+
* body is captured in full with no size cap (W-24280587) — the channel is
|
|
358
|
+
* opt-in and diagnostic, so fidelity wins; with no subscriber the decode is
|
|
359
|
+
* skipped entirely (the event is dropped by the wire bus anyway), so a
|
|
360
|
+
* subscriber-less production process pays no full-body parse on the hot path.
|
|
361
|
+
* (The streaming *response* observer separately bounds its in-memory text
|
|
362
|
+
* capture at `TRACE_BODY_BYTE_BUDGET`.)
|
|
361
363
|
*/
|
|
362
364
|
export function createGatewayFetch(getHeaders, wireHooks, innerFetch = globalThis.fetch,
|
|
363
365
|
/**
|
|
@@ -411,6 +413,13 @@ logBus) {
|
|
|
411
413
|
const startedAt = clock.now();
|
|
412
414
|
const correlationId = (wireHooks.idGenerator ?? new UUIDGenerator()).getUniqueId();
|
|
413
415
|
const requestStart = performance.now();
|
|
416
|
+
// Decode the (now uncapped) request body ONLY when a subscriber is
|
|
417
|
+
// listening — mirroring the response-observer gating below. In production
|
|
418
|
+
// `wireHooks` is always supplied but subscribers are usually absent, and a
|
|
419
|
+
// real turn's body is large, so an unconditional full-body JSON.parse would
|
|
420
|
+
// be pure hot-path waste for a subscriber-less process (the event is dropped
|
|
421
|
+
// by the wire bus anyway). Fidelity for actual subscribers is unchanged.
|
|
422
|
+
const hasWireSubscribers = wireHooks.hasWireCommunicationSubscribers?.() ?? true;
|
|
414
423
|
wireHooks.emitWireCommunication({
|
|
415
424
|
type: 'llm-request',
|
|
416
425
|
timestamp: startedAt,
|
|
@@ -418,7 +427,7 @@ logBus) {
|
|
|
418
427
|
method,
|
|
419
428
|
model,
|
|
420
429
|
correlationId,
|
|
421
|
-
body: tryDecodeBody(finalInit.body),
|
|
430
|
+
body: hasWireSubscribers ? tryDecodeBody(finalInit.body) : undefined,
|
|
422
431
|
});
|
|
423
432
|
try {
|
|
424
433
|
const response = await innerFetch(input, finalInit);
|
|
@@ -791,10 +800,11 @@ function tryDecodeBody(body) {
|
|
|
791
800
|
return undefined;
|
|
792
801
|
if (typeof body !== 'string')
|
|
793
802
|
return undefined; // Streams / FormData / Blob: skip.
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
}
|
|
803
|
+
// No size cap: the wire-communication channel is opt-in, subscriber-gated, and
|
|
804
|
+
// purely diagnostic — its whole point is fidelity, so emit the full request body
|
|
805
|
+
// (system / messages / tools) verbatim. (W-24280587; a 256 KB cap here used to
|
|
806
|
+
// replace large bodies with a `{ truncated, byteLength }` stub, hiding exactly
|
|
807
|
+
// the fields the channel exists to show.)
|
|
798
808
|
try {
|
|
799
809
|
const parsed = JSON.parse(body);
|
|
800
810
|
return typeof parsed === 'object' && parsed !== null ? parsed : { value: parsed };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/sfdx-agent-harness-mastra",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.91.0",
|
|
4
4
|
"description": "Mastra-backed AgentHarness implementation for @salesforce/sfdx-agent-sdk",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"LICENSE.txt"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@ai-sdk/provider": "4.0.
|
|
32
|
+
"@ai-sdk/provider": "4.0.14",
|
|
33
33
|
"@anthropic-ai/bedrock-sdk": "^0.33.6",
|
|
34
34
|
"@anthropic-ai/sdk": "^0.126.0",
|
|
35
35
|
"@mastra/core": "^1.67.0",
|
|
@@ -42,12 +42,12 @@
|
|
|
42
42
|
"openai": "^7.17.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"@salesforce/sfdx-agent-sdk": "0.
|
|
45
|
+
"@salesforce/sfdx-agent-sdk": "0.92.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@eslint/js": "^10.0.1",
|
|
49
49
|
"@salesforce/harness-conformance": "0.0.1",
|
|
50
|
-
"@salesforce/sfdx-agent-sdk": "0.
|
|
50
|
+
"@salesforce/sfdx-agent-sdk": "0.92.0",
|
|
51
51
|
"@types/node": "^22.20.1",
|
|
52
52
|
"@vitest/coverage-istanbul": "^5.0.1",
|
|
53
53
|
"@vitest/eslint-plugin": "^1.6.27",
|