@salesforce/sfdx-agent-harness-mastra 0.80.0 → 0.82.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,15 @@
|
|
|
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.82.0] - 2026-09-16
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
- **harness-mastra**: stream tool-call argument deltas from pass-through builders @W-23954558@ ([#824](https://github.com/forcedotcom/agentic-dx/pull/824))
|
|
10
|
+
|
|
11
|
+
## [0.81.0] - 2026-09-15
|
|
12
|
+
|
|
13
|
+
_No changes — released alongside dependent packages._
|
|
14
|
+
|
|
6
15
|
## [0.80.0] - 2026-09-15
|
|
7
16
|
|
|
8
17
|
### Chores
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { AnthropicBedrock } from '@anthropic-ai/bedrock-sdk';
|
|
6
6
|
import Anthropic from '@anthropic-ai/sdk';
|
|
7
7
|
import { EmptyStreamError, NetworkStreamError, isStreamTransportError, recognizeBedrockValidationError, } from './stream-errors.js';
|
|
8
|
-
import { TextSpanLifecycle, buildValidatedToolCallChunk, createGatewayFetch, isEmptyUsage, isParameterExcluded, mapAnthropicFinishReason, mapAnthropicUsage, toolResultOutputToString, } from './v3-stream-mapping.js';
|
|
8
|
+
import { TextSpanLifecycle, ToolInputSpanLifecycle, buildValidatedToolCallChunk, createGatewayFetch, isEmptyUsage, isParameterExcluded, mapAnthropicFinishReason, mapAnthropicUsage, toolResultOutputToString, } from './v3-stream-mapping.js';
|
|
9
9
|
/**
|
|
10
10
|
* Builds the in-process `LanguageModelV3` impl for the Salesforce LLM Gateway's
|
|
11
11
|
* `/invoke-with-response-stream` (Bedrock-Anthropic pass-through) endpoint.
|
|
@@ -357,10 +357,14 @@ async function doStream(ctx, options) {
|
|
|
357
357
|
// A tool_use block follows any open text — close that span and
|
|
358
358
|
// bump the id so post-tool text lands in a fresh span.
|
|
359
359
|
textSpan.rotate(controller);
|
|
360
|
+
// Stream incremental tool-call arg deltas alongside the
|
|
361
|
+
// unchanged terminal `tool-call` (W-23954558); see
|
|
362
|
+
// ToolInputSpanLifecycle.
|
|
360
363
|
toolUseByIndex.set(event.index, {
|
|
361
364
|
id: block.id,
|
|
362
365
|
name: block.name,
|
|
363
366
|
argsBuffer: '',
|
|
367
|
+
input: new ToolInputSpanLifecycle(block.id, block.name, ctx.logBus),
|
|
364
368
|
});
|
|
365
369
|
}
|
|
366
370
|
// Other block types (thinking, server-tool result, etc.) are
|
|
@@ -375,8 +379,10 @@ async function doStream(ctx, options) {
|
|
|
375
379
|
}
|
|
376
380
|
else if (delta.type === 'input_json_delta') {
|
|
377
381
|
const tu = toolUseByIndex.get(event.index);
|
|
378
|
-
if (tu)
|
|
382
|
+
if (tu) {
|
|
379
383
|
tu.argsBuffer += delta.partial_json;
|
|
384
|
+
tu.input.delta(controller, delta.partial_json);
|
|
385
|
+
}
|
|
380
386
|
}
|
|
381
387
|
// thinking_delta / signature_delta / citations_delta are not
|
|
382
388
|
// currently surfaced — Mastra has no V3 stream-part for them in
|
|
@@ -388,6 +394,9 @@ async function doStream(ctx, options) {
|
|
|
388
394
|
if (tu) {
|
|
389
395
|
sawToolCalls = true;
|
|
390
396
|
sawContentOrUsage = true;
|
|
397
|
+
// Close the tool-input span before the unchanged terminal
|
|
398
|
+
// `tool-call` (wire order start → delta* → end → call).
|
|
399
|
+
tu.input.close(controller, tu.argsBuffer);
|
|
391
400
|
for (const part of buildValidatedToolCallChunk(tu.id, tu.name, tu.argsBuffer, toolSchemas.get(tu.name), ctx.getConsumerToolNames().has(tu.name), { retryCounts: ctx.getToolArgsRetryCounts(options), logBus: ctx.logBus })) {
|
|
392
401
|
controller.enqueue(part);
|
|
393
402
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import OpenAI from 'openai';
|
|
6
6
|
import { EmptyStreamError } from './stream-errors.js';
|
|
7
|
-
import { TextSpanLifecycle, buildValidatedToolCallChunk, createGatewayFetch, isEmptyUsage, isParameterExcluded, mapOpenAIUsage, toolResultOutputToString, } from './v3-stream-mapping.js';
|
|
7
|
+
import { TextSpanLifecycle, ToolInputSpanLifecycle, buildValidatedToolCallChunk, createGatewayFetch, isEmptyUsage, isParameterExcluded, mapOpenAIUsage, toolResultOutputToString, } from './v3-stream-mapping.js';
|
|
8
8
|
/**
|
|
9
9
|
* Builds the in-process `LanguageModelV3` impl that drives the `openai` SDK's
|
|
10
10
|
* `chat.completions.create({ stream: true })` against an OpenAI-compatible
|
|
@@ -202,6 +202,28 @@ async function doStream(ctx, options) {
|
|
|
202
202
|
if (tc.function?.arguments)
|
|
203
203
|
acc.argsBuffer += tc.function.arguments;
|
|
204
204
|
}
|
|
205
|
+
// Stream incremental tool-call arg deltas alongside the
|
|
206
|
+
// unchanged terminal `tool-call` (W-23954558; see
|
|
207
|
+
// ToolInputSpanLifecycle). Chat Completions has no discrete
|
|
208
|
+
// start/done event, so the span opens lazily once an `id` is
|
|
209
|
+
// known (OpenAI-compatible servers send it on the first delta)
|
|
210
|
+
// and closes at the flush.
|
|
211
|
+
if (!acc.input) {
|
|
212
|
+
if (acc.id) {
|
|
213
|
+
acc.input = new ToolInputSpanLifecycle(acc.id, acc.name, ctx.logBus);
|
|
214
|
+
// Stream whatever accrued before the id was known
|
|
215
|
+
// (normally just this chunk's fragment) so the deltas
|
|
216
|
+
// still concatenate to the terminal input.
|
|
217
|
+
acc.input.delta(controller, acc.argsBuffer);
|
|
218
|
+
}
|
|
219
|
+
// No id yet: args stay buffered; the catch-up delta above
|
|
220
|
+
// flushes them once the id arrives (OpenAI-compatible servers
|
|
221
|
+
// send `id` on the first delta, so this only defers the
|
|
222
|
+
// pathological id-less-first-delta case).
|
|
223
|
+
}
|
|
224
|
+
else if (tc.function?.arguments) {
|
|
225
|
+
acc.input.delta(controller, tc.function.arguments);
|
|
226
|
+
}
|
|
205
227
|
}
|
|
206
228
|
}
|
|
207
229
|
if (choice.finish_reason !== null && choice.finish_reason !== undefined) {
|
|
@@ -232,6 +254,10 @@ async function doStream(ctx, options) {
|
|
|
232
254
|
continue;
|
|
233
255
|
}
|
|
234
256
|
sawToolCalls = true;
|
|
257
|
+
// Close the tool-input span before the unchanged terminal
|
|
258
|
+
// `tool-call`. `acc.id` is non-empty here (the guard above
|
|
259
|
+
// `continue`s otherwise), so `acc.input` was constructed.
|
|
260
|
+
acc.input?.close(controller, acc.argsBuffer);
|
|
235
261
|
for (const part of buildValidatedToolCallChunk(acc.id, acc.name, acc.argsBuffer, toolSchemas.get(acc.name), ctx.getConsumerToolNames().has(acc.name), { retryCounts: ctx.getToolArgsRetryCounts(options), logBus: ctx.logBus })) {
|
|
236
262
|
controller.enqueue(part);
|
|
237
263
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import OpenAI from 'openai';
|
|
6
6
|
import { EmptyStreamError, GatewayResponseError } from './stream-errors.js';
|
|
7
|
-
import { TextSpanLifecycle, buildValidatedToolCallChunk, createGatewayFetch, isEmptyUsage, isParameterExcluded, mapOpenAIFinishReason, mapOpenAIUsage, toolResultOutputToString, } from './v3-stream-mapping.js';
|
|
7
|
+
import { TextSpanLifecycle, ToolInputSpanLifecycle, buildValidatedToolCallChunk, createGatewayFetch, isEmptyUsage, isParameterExcluded, mapOpenAIFinishReason, mapOpenAIUsage, toolResultOutputToString, } from './v3-stream-mapping.js';
|
|
8
8
|
/**
|
|
9
9
|
* Builds the in-process `LanguageModelV3` impl that drives the `openai` SDK's
|
|
10
10
|
* `responses.create({ stream: true })` against the Salesforce LLM Gateway's
|
|
@@ -148,18 +148,30 @@ async function doStream(ctx, options) {
|
|
|
148
148
|
// A tool_call follows any open text — close that span and bump
|
|
149
149
|
// the id so post-tool text lands in a fresh span.
|
|
150
150
|
textSpan.rotate(controller);
|
|
151
|
+
const argsBuffer = item.arguments ?? '';
|
|
152
|
+
// Stream incremental tool-call arg deltas alongside the
|
|
153
|
+
// unchanged terminal `tool-call` (W-23954558); see
|
|
154
|
+
// ToolInputSpanLifecycle.
|
|
155
|
+
const input = new ToolInputSpanLifecycle(item.call_id, item.name, ctx.logBus);
|
|
151
156
|
toolCallById.set(item.id ?? item.call_id, {
|
|
152
157
|
id: item.call_id,
|
|
153
158
|
name: item.name,
|
|
154
|
-
argsBuffer
|
|
159
|
+
argsBuffer,
|
|
160
|
+
input,
|
|
155
161
|
});
|
|
162
|
+
// Responses streams args via function_call_arguments.delta,
|
|
163
|
+
// so `arguments` is normally empty here; forward any
|
|
164
|
+
// pre-populated buffer as a fragment.
|
|
165
|
+
input.delta(controller, argsBuffer);
|
|
156
166
|
}
|
|
157
167
|
break;
|
|
158
168
|
}
|
|
159
169
|
case 'response.function_call_arguments.delta': {
|
|
160
170
|
const tc = toolCallById.get(event.item_id);
|
|
161
|
-
if (tc)
|
|
171
|
+
if (tc) {
|
|
162
172
|
tc.argsBuffer += event.delta;
|
|
173
|
+
tc.input.delta(controller, event.delta);
|
|
174
|
+
}
|
|
163
175
|
break;
|
|
164
176
|
}
|
|
165
177
|
case 'response.output_item.done': {
|
|
@@ -169,6 +181,10 @@ async function doStream(ctx, options) {
|
|
|
169
181
|
const finalArgs = item.arguments ?? tc?.argsBuffer ?? '';
|
|
170
182
|
sawToolCalls = true;
|
|
171
183
|
sawContent = true;
|
|
184
|
+
// Close the tool-input span before the unchanged terminal
|
|
185
|
+
// `tool-call` (handles the atomic case where args arrive
|
|
186
|
+
// only here, with no incremental deltas).
|
|
187
|
+
tc?.input.close(controller, finalArgs);
|
|
172
188
|
for (const part of buildValidatedToolCallChunk(item.call_id, item.name, finalArgs, toolSchemas.get(item.name), ctx.getConsumerToolNames().has(item.name), { retryCounts: ctx.getToolArgsRetryCounts(options), logBus: ctx.logBus })) {
|
|
173
189
|
controller.enqueue(part);
|
|
174
190
|
}
|
|
@@ -63,6 +63,55 @@ export declare class TextSpanLifecycle {
|
|
|
63
63
|
}): void;
|
|
64
64
|
private spanId;
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Encodes the AI SDK V3 tool-input streaming lifecycle (`tool-input-start` /
|
|
68
|
+
* `tool-input-delta` / `tool-input-end`) that surfaces incremental tool-call
|
|
69
|
+
* argument fragments to consumers (W-23954558). One instance per tool call.
|
|
70
|
+
*
|
|
71
|
+
* These parts are emitted IN ADDITION TO — never instead of — the authoritative
|
|
72
|
+
* terminal `tool-call` part (built by {@link buildValidatedToolCallChunk}), in
|
|
73
|
+
* real wire order: `start → delta* → end → tool-call`.
|
|
74
|
+
*
|
|
75
|
+
* Load-bearing invariant: `@mastra/core`'s stream pipeline, once it sees a
|
|
76
|
+
* `tool-input-start`, derives the final tool call's `args` from the ACCUMULATED
|
|
77
|
+
* `tool-input-delta` fragments — NOT from the terminal `tool-call.input`. So the
|
|
78
|
+
* concatenation of the deltas this class emits MUST reproduce the argument
|
|
79
|
+
* string the terminal `tool-call` carries, or Mastra surfaces truncated/empty
|
|
80
|
+
* args. This class enforces that:
|
|
81
|
+
*
|
|
82
|
+
* - The span is opened LAZILY on the first non-empty fragment, so a genuinely
|
|
83
|
+
* no-argument call (zero fragments) never opens a span and Mastra falls back
|
|
84
|
+
* to the terminal `input` (`'{}'`). An empty fragment is dropped (Mastra drops
|
|
85
|
+
* a zero-length `tool-call-delta` anyway).
|
|
86
|
+
* - {@link close} reconciles before the terminal part: if fragments streamed
|
|
87
|
+
* incrementally, it emits any un-streamed tail of `finalArgs`; if the args
|
|
88
|
+
* arrived atomically (some providers deliver the complete args only on the
|
|
89
|
+
* terminal event, with no incremental deltas — e.g. the OpenAI Responses
|
|
90
|
+
* `output_item.done`), it opens the span and streams the whole buffer as one
|
|
91
|
+
* delta so the accumulator still matches. Either way the emitted deltas
|
|
92
|
+
* concatenate to exactly `finalArgs`.
|
|
93
|
+
*/
|
|
94
|
+
export declare class ToolInputSpanLifecycle {
|
|
95
|
+
private readonly id;
|
|
96
|
+
private readonly toolName;
|
|
97
|
+
private readonly logBus?;
|
|
98
|
+
private started;
|
|
99
|
+
private streamed;
|
|
100
|
+
constructor(id: string, toolName: string, logBus?: LogBus | undefined);
|
|
101
|
+
/** Forward one raw argument fragment as a `tool-input-delta`, opening the span on first use. Empty fragments are dropped. */
|
|
102
|
+
delta(controller: {
|
|
103
|
+
enqueue: (part: LanguageModelV3StreamPart) => void;
|
|
104
|
+
}, fragment: string): void;
|
|
105
|
+
/**
|
|
106
|
+
* Reconcile the streamed deltas against the authoritative `finalArgs` (the
|
|
107
|
+
* exact string handed to the terminal `tool-call`) and close the span. Call
|
|
108
|
+
* immediately BEFORE emitting the terminal `tool-call`. No-op emission for a
|
|
109
|
+
* never-opened, empty-args span so a no-arg call stays span-free.
|
|
110
|
+
*/
|
|
111
|
+
close(controller: {
|
|
112
|
+
enqueue: (part: LanguageModelV3StreamPart) => void;
|
|
113
|
+
}, finalArgs: string): void;
|
|
114
|
+
}
|
|
66
115
|
/**
|
|
67
116
|
* Maps a Salesforce LLM Gateway raw finish-reason string (the gateway-normalized
|
|
68
117
|
* vocabulary that spans GPT-5 and Claude — `stop`, `length`, `tool_calls`,
|
|
@@ -81,6 +81,92 @@ export class TextSpanLifecycle {
|
|
|
81
81
|
return `text-${this.id}`;
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Encodes the AI SDK V3 tool-input streaming lifecycle (`tool-input-start` /
|
|
86
|
+
* `tool-input-delta` / `tool-input-end`) that surfaces incremental tool-call
|
|
87
|
+
* argument fragments to consumers (W-23954558). One instance per tool call.
|
|
88
|
+
*
|
|
89
|
+
* These parts are emitted IN ADDITION TO — never instead of — the authoritative
|
|
90
|
+
* terminal `tool-call` part (built by {@link buildValidatedToolCallChunk}), in
|
|
91
|
+
* real wire order: `start → delta* → end → tool-call`.
|
|
92
|
+
*
|
|
93
|
+
* Load-bearing invariant: `@mastra/core`'s stream pipeline, once it sees a
|
|
94
|
+
* `tool-input-start`, derives the final tool call's `args` from the ACCUMULATED
|
|
95
|
+
* `tool-input-delta` fragments — NOT from the terminal `tool-call.input`. So the
|
|
96
|
+
* concatenation of the deltas this class emits MUST reproduce the argument
|
|
97
|
+
* string the terminal `tool-call` carries, or Mastra surfaces truncated/empty
|
|
98
|
+
* args. This class enforces that:
|
|
99
|
+
*
|
|
100
|
+
* - The span is opened LAZILY on the first non-empty fragment, so a genuinely
|
|
101
|
+
* no-argument call (zero fragments) never opens a span and Mastra falls back
|
|
102
|
+
* to the terminal `input` (`'{}'`). An empty fragment is dropped (Mastra drops
|
|
103
|
+
* a zero-length `tool-call-delta` anyway).
|
|
104
|
+
* - {@link close} reconciles before the terminal part: if fragments streamed
|
|
105
|
+
* incrementally, it emits any un-streamed tail of `finalArgs`; if the args
|
|
106
|
+
* arrived atomically (some providers deliver the complete args only on the
|
|
107
|
+
* terminal event, with no incremental deltas — e.g. the OpenAI Responses
|
|
108
|
+
* `output_item.done`), it opens the span and streams the whole buffer as one
|
|
109
|
+
* delta so the accumulator still matches. Either way the emitted deltas
|
|
110
|
+
* concatenate to exactly `finalArgs`.
|
|
111
|
+
*/
|
|
112
|
+
export class ToolInputSpanLifecycle {
|
|
113
|
+
id;
|
|
114
|
+
toolName;
|
|
115
|
+
logBus;
|
|
116
|
+
started = false;
|
|
117
|
+
streamed = '';
|
|
118
|
+
constructor(id, toolName, logBus) {
|
|
119
|
+
this.id = id;
|
|
120
|
+
this.toolName = toolName;
|
|
121
|
+
this.logBus = logBus;
|
|
122
|
+
}
|
|
123
|
+
/** Forward one raw argument fragment as a `tool-input-delta`, opening the span on first use. Empty fragments are dropped. */
|
|
124
|
+
delta(controller, fragment) {
|
|
125
|
+
if (fragment.length === 0)
|
|
126
|
+
return;
|
|
127
|
+
if (!this.started) {
|
|
128
|
+
controller.enqueue({ type: 'tool-input-start', id: this.id, toolName: this.toolName });
|
|
129
|
+
this.started = true;
|
|
130
|
+
}
|
|
131
|
+
controller.enqueue({ type: 'tool-input-delta', id: this.id, delta: fragment });
|
|
132
|
+
this.streamed += fragment;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Reconcile the streamed deltas against the authoritative `finalArgs` (the
|
|
136
|
+
* exact string handed to the terminal `tool-call`) and close the span. Call
|
|
137
|
+
* immediately BEFORE emitting the terminal `tool-call`. No-op emission for a
|
|
138
|
+
* never-opened, empty-args span so a no-arg call stays span-free.
|
|
139
|
+
*/
|
|
140
|
+
close(controller, finalArgs) {
|
|
141
|
+
if (!this.started) {
|
|
142
|
+
// Atomic-args case: the provider delivered the complete args only on the
|
|
143
|
+
// terminal event (no incremental fragments). Stream the whole buffer once
|
|
144
|
+
// so Mastra's accumulator reproduces the args. A truly empty buffer leaves
|
|
145
|
+
// the span unopened and Mastra uses the terminal `input`.
|
|
146
|
+
this.delta(controller, finalArgs);
|
|
147
|
+
}
|
|
148
|
+
else if (this.streamed !== finalArgs) {
|
|
149
|
+
if (finalArgs.startsWith(this.streamed)) {
|
|
150
|
+
// Incremental fragments under-covered the final buffer (e.g. a terminal
|
|
151
|
+
// event carried a more-complete `arguments` than the streamed deltas).
|
|
152
|
+
// Emit the un-streamed tail so the accumulator still equals `finalArgs`.
|
|
153
|
+
this.delta(controller, finalArgs.slice(this.streamed.length));
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
// Genuine divergence: the streamed deltas are NOT a prefix of the terminal
|
|
157
|
+
// args (a provider that re-normalized the JSON on the terminal event). We
|
|
158
|
+
// can't un-emit, so the delta-derived args (Mastra accumulator, AG-UI
|
|
159
|
+
// TOOL_CALL_ARGS) will be wrong with no terminal ARGS to correct them.
|
|
160
|
+
// Real providers stream prefixes, so this is unreachable in practice —
|
|
161
|
+
// field a signal so a future provider quirk surfaces instead of silently
|
|
162
|
+
// corrupting args. Lengths only (args may carry PII).
|
|
163
|
+
this.logBus?.warn('Tool-call arg deltas diverged from the terminal args; streamed deltas do not reconstruct the full arguments downstream', { toolName: this.toolName, streamedLength: this.streamed.length, finalLength: finalArgs.length });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (this.started)
|
|
167
|
+
controller.enqueue({ type: 'tool-input-end', id: this.id });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
84
170
|
/**
|
|
85
171
|
* Maps a Salesforce LLM Gateway raw finish-reason string (the gateway-normalized
|
|
86
172
|
* vocabulary that spans GPT-5 and Claude — `stop`, `length`, `tool_calls`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/sfdx-agent-harness-mastra",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.82.0",
|
|
4
4
|
"description": "Mastra-backed AgentHarness implementation for @salesforce/sfdx-agent-sdk",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -42,12 +42,12 @@
|
|
|
42
42
|
"openai": "^7.0.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"@salesforce/sfdx-agent-sdk": "0.
|
|
45
|
+
"@salesforce/sfdx-agent-sdk": "0.83.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.83.0",
|
|
51
51
|
"@types/node": "^22.20.1",
|
|
52
52
|
"@vitest/coverage-istanbul": "^4.1.11",
|
|
53
53
|
"@vitest/eslint-plugin": "^1.6.27",
|