@salesforce/sfdx-agent-harness-mastra 0.28.0 → 0.30.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 CHANGED
@@ -3,6 +3,20 @@
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.30.0] - 2026-07-07
7
+
8
+ ### Fixes
9
+ - **harness-mastra**: getMessages() round-trips the tool-call part @W-23351939 ([#648](https://github.com/forcedotcom/agentic-dx/pull/648))
10
+
11
+ ### Chores
12
+ - **deps**: bump the mastra group across 1 directory with 5 updates ([#644](https://github.com/forcedotcom/agentic-dx/pull/644))
13
+ - **deps**: bump @anthropic-ai/bedrock-sdk from 0.31.0 to 0.32.0 ([#645](https://github.com/forcedotcom/agentic-dx/pull/645))
14
+ - **deps-dev**: bump the dev-dependencies group with 3 updates ([#642](https://github.com/forcedotcom/agentic-dx/pull/642))
15
+
16
+ ## [0.29.0] - 2026-07-03
17
+
18
+ _No changes — released alongside dependent packages._
19
+
6
20
  ## [0.28.0] - 2026-07-02
7
21
 
8
22
  ### Fixes
@@ -15,13 +15,23 @@ import type { LogBus } from '@salesforce/agentic-common';
15
15
  */
16
16
  export declare function mapMastraToMessage(msg: MastraDBMessage, logBus?: LogBus): Message;
17
17
  /**
18
- * Converts a single Mastra message part to the SDK {@link MessagePart} equivalent.
19
- * Returns `undefined` for part types that exist in Mastra but have no SDK representation.
18
+ * Converts a single Mastra message part to zero or more SDK {@link MessagePart}s.
19
+ *
20
+ * Most Mastra parts map one-to-one, but a **completed** `tool-invocation` maps to
21
+ * *two* SDK parts — a `tool-call` (name + args) followed by a `tool-result` — because
22
+ * a single Mastra object carries both halves once its `state` transitions to `'result'`.
23
+ * Emitting both keeps `getMessages()` history harness-agnostic: the model's original
24
+ * tool call (name + args) survives on read exactly as it does on the Claude harness,
25
+ * which stores the call and result as distinct blocks (#647). Returning an array is what
26
+ * lets one input part expand to two output parts without a special case in the caller.
27
+ *
28
+ * Returns an empty array for part types that exist in Mastra but have no SDK
29
+ * representation (e.g. `source`, `step-start`, `partial-call`, `data-*`).
20
30
  *
21
31
  * @param logBus - When supplied, unrecognized part types emit a `warn` log via this bus.
22
32
  * Tests that don't care about logging can omit it.
23
33
  */
24
- export declare function mapMastraPartToMessagePart(part: MastraMessagePart, logBus?: LogBus): MessagePart | undefined;
34
+ export declare function mapMastraPartToMessagePart(part: MastraMessagePart, logBus?: LogBus): MessagePart[];
25
35
  /**
26
36
  * Converts an SDK {@link Message} into the Mastra persisted message format.
27
37
  * The `role` field is normalised: `'tool'` role (which has no Mastra equivalent)
@@ -47,10 +47,7 @@ export function mapMastraToMessage(msg, logBus) {
47
47
  }
48
48
  const parts = [];
49
49
  for (const part of msg.content.parts) {
50
- const mapped = mapMastraPartToMessagePart(part, logBus);
51
- if (mapped) {
52
- parts.push(mapped);
53
- }
50
+ parts.push(...mapMastraPartToMessagePart(part, logBus));
54
51
  }
55
52
  const consolidated = consolidateAdjacentTextParts(parts);
56
53
  const isSingleText = consolidated.length === 1 && consolidated[0].type === 'text';
@@ -85,8 +82,18 @@ function consolidateAdjacentTextParts(parts) {
85
82
  return result;
86
83
  }
87
84
  /**
88
- * Converts a single Mastra message part to the SDK {@link MessagePart} equivalent.
89
- * Returns `undefined` for part types that exist in Mastra but have no SDK representation.
85
+ * Converts a single Mastra message part to zero or more SDK {@link MessagePart}s.
86
+ *
87
+ * Most Mastra parts map one-to-one, but a **completed** `tool-invocation` maps to
88
+ * *two* SDK parts — a `tool-call` (name + args) followed by a `tool-result` — because
89
+ * a single Mastra object carries both halves once its `state` transitions to `'result'`.
90
+ * Emitting both keeps `getMessages()` history harness-agnostic: the model's original
91
+ * tool call (name + args) survives on read exactly as it does on the Claude harness,
92
+ * which stores the call and result as distinct blocks (#647). Returning an array is what
93
+ * lets one input part expand to two output parts without a special case in the caller.
94
+ *
95
+ * Returns an empty array for part types that exist in Mastra but have no SDK
96
+ * representation (e.g. `source`, `step-start`, `partial-call`, `data-*`).
90
97
  *
91
98
  * @param logBus - When supplied, unrecognized part types emit a `warn` log via this bus.
92
99
  * Tests that don't care about logging can omit it.
@@ -94,60 +101,62 @@ function consolidateAdjacentTextParts(parts) {
94
101
  export function mapMastraPartToMessagePart(part, logBus) {
95
102
  switch (part.type) {
96
103
  case 'text':
97
- return { type: 'text', text: part.text };
104
+ return [{ type: 'text', text: part.text }];
98
105
  case 'reasoning':
99
- return { type: 'reasoning', text: part.reasoning };
106
+ return [{ type: 'reasoning', text: part.reasoning }];
100
107
  case 'tool-invocation': {
101
108
  const inv = part.toolInvocation;
102
109
  if (inv.state === 'partial-call') {
103
- return undefined;
104
- }
105
- if (inv.state === 'result') {
106
- return {
107
- type: 'tool-result',
108
- toolCallId: inv.toolCallId,
109
- toolName: inv.toolName,
110
- result: inv.result,
111
- };
110
+ return [];
112
111
  }
113
- return {
112
+ const toolCall = {
114
113
  type: 'tool-call',
115
114
  toolCallId: inv.toolCallId,
116
115
  toolName: inv.toolName,
117
- args: inv.args,
116
+ args: (inv.args ?? {}),
118
117
  };
118
+ if (inv.state === 'result') {
119
+ // A completed invocation carries both the original call (name + args)
120
+ // and its result; surface both so history round-trips the call the
121
+ // model made, not just its outcome.
122
+ return [
123
+ toolCall,
124
+ { type: 'tool-result', toolCallId: inv.toolCallId, toolName: inv.toolName, result: inv.result },
125
+ ];
126
+ }
127
+ return [toolCall];
119
128
  }
120
129
  case 'file': {
121
130
  // Read path: base64 data is returned verbatim from LibSQL — see ARCHITECTURE.md → "Memory Strategy"
122
131
  // for the storage-cost caveat (no blob offloading; full encoded size per attachment per row).
123
132
  const fileName = readSalesforceFileName(part.providerMetadata);
124
133
  if (part.mimeType === 'image/png' || part.mimeType === 'image/jpeg') {
125
- return { type: 'image', mimeType: part.mimeType, data: part.data, ...(fileName ? { fileName } : {}) };
134
+ return [{ type: 'image', mimeType: part.mimeType, data: part.data, ...(fileName ? { fileName } : {}) }];
126
135
  }
127
136
  if (part.mimeType === 'application/pdf') {
128
- return { type: 'file', mimeType: part.mimeType, data: part.data, ...(fileName ? { fileName } : {}) };
137
+ return [{ type: 'file', mimeType: part.mimeType, data: part.data, ...(fileName ? { fileName } : {}) }];
129
138
  }
130
139
  logBus?.warn('Skipping file MastraMessagePart with unsupported mimeType; omitted from message history', {
131
140
  mimeType: part.mimeType,
132
141
  hint: 'Only image/png, image/jpeg, and application/pdf map to SDK message parts.',
133
142
  });
134
- return undefined;
143
+ return [];
135
144
  }
136
145
  // Part types that exist in Mastra but have no SDK equivalent yet.
137
146
  case 'source':
138
147
  case 'step-start':
139
- return undefined;
148
+ return [];
140
149
  default:
141
150
  // AI SDK v5 data parts use a `data-*` type prefix for framework-specific
142
151
  // metadata (e.g. workspace info). These are harness-internal and expected.
143
152
  if (part.type.startsWith('data-')) {
144
- return undefined;
153
+ return [];
145
154
  }
146
155
  logBus?.warn('Skipping unrecognized MastraMessagePart; omitted from message history', {
147
156
  partType: part.type,
148
157
  hint: 'Update mapMastraPartToMessagePart to handle this type if it should be mapped.',
149
158
  });
150
- return undefined;
159
+ return [];
151
160
  }
152
161
  }
153
162
  /**
@@ -161,7 +170,31 @@ export function mapMessageToMastra(msg, threadId, resourceId) {
161
170
  parts.push({ type: 'text', text: msg.content });
162
171
  }
163
172
  else {
164
- for (const part of msg.content) {
173
+ const content = msg.content;
174
+ for (let i = 0; i < content.length; i++) {
175
+ const part = content[i];
176
+ const next = content[i + 1];
177
+ // Coalesce an adjacent tool-call + tool-result for the same toolCallId
178
+ // into ONE Mastra `tool-invocation(result)` carrying both `args` and
179
+ // `result`. This mirrors how Mastra natively persists a completed call
180
+ // (its MessageList merges the call and result into a single object) and
181
+ // is the exact inverse of the read-path expansion in
182
+ // `mapMastraPartToMessagePart`, so a `getMessages()` → `addContext()`
183
+ // round-trip is lossless and never double-counts the call (#647).
184
+ if (part.type === 'tool-call' && next?.type === 'tool-result' && next.toolCallId === part.toolCallId) {
185
+ parts.push({
186
+ type: 'tool-invocation',
187
+ toolInvocation: {
188
+ state: 'result',
189
+ toolCallId: part.toolCallId,
190
+ toolName: part.toolName,
191
+ args: part.args,
192
+ result: next.result,
193
+ },
194
+ });
195
+ i++; // consumed the paired tool-result
196
+ continue;
197
+ }
165
198
  parts.push(mapMessagePartToMastra(part));
166
199
  }
167
200
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/sfdx-agent-harness-mastra",
3
- "version": "0.28.0",
3
+ "version": "0.30.0",
4
4
  "description": "Mastra-backed AgentHarness implementation for @salesforce/sfdx-agent-sdk",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -29,22 +29,22 @@
29
29
  ],
30
30
  "dependencies": {
31
31
  "@ai-sdk/provider": "^3.0.10",
32
- "@anthropic-ai/bedrock-sdk": "^0.31.0",
32
+ "@anthropic-ai/bedrock-sdk": "^0.32.0",
33
33
  "@anthropic-ai/sdk": "^0.106.0",
34
- "@mastra/core": "^1.47.0",
35
- "@mastra/libsql": "^1.14.2",
36
- "@mastra/mcp": "^1.10.0",
37
- "@mastra/memory": "^1.21.2",
34
+ "@mastra/core": "^1.49.0",
35
+ "@mastra/libsql": "^1.15.0",
36
+ "@mastra/mcp": "^1.13.0",
37
+ "@mastra/memory": "^1.22.1",
38
38
  "@mastra/schema-compat": "^1.2.11",
39
39
  "@salesforce/agentic-common": "0.13.0",
40
40
  "openai": "^6.0.0"
41
41
  },
42
42
  "peerDependencies": {
43
- "@salesforce/sfdx-agent-sdk": "0.29.0"
43
+ "@salesforce/sfdx-agent-sdk": "0.31.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@eslint/js": "^10.0.1",
47
- "@salesforce/sfdx-agent-sdk": "0.29.0",
47
+ "@salesforce/sfdx-agent-sdk": "0.31.0",
48
48
  "@types/node": "^22.20.0",
49
49
  "@vitest/coverage-istanbul": "^4.1.8",
50
50
  "@vitest/eslint-plugin": "^1.6.20",
@@ -55,11 +55,11 @@
55
55
  "eslint-plugin-n": "^18.2.1",
56
56
  "globals": "^17.6.0",
57
57
  "lint-staged": "^17.0.7",
58
- "prettier": "^3.9.0",
58
+ "prettier": "^3.9.4",
59
59
  "rimraf": "^6.1.3",
60
- "tsx": "^4.22.4",
60
+ "tsx": "^4.23.0",
61
61
  "typescript": "^6.0.3",
62
- "typescript-eslint": "^8.61.0",
62
+ "typescript-eslint": "^8.62.1",
63
63
  "vitest": "^4.1.8"
64
64
  },
65
65
  "engines": {