@salesforce/sfdx-agent-harness-mastra 0.83.0 → 0.85.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 +10 -0
- package/dist/mastra-event-adapter.d.ts +1 -0
- package/dist/mastra-event-adapter.js +33 -7
- package/dist/pass-through-anthropic.js +112 -13
- package/dist/pass-through-openai-chat-completions.js +2 -2
- package/dist/pass-through-openai.js +2 -2
- package/dist/v3-stream-mapping.d.ts +16 -9
- package/dist/v3-stream-mapping.js +18 -13
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
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.85.0] - 2026-09-16
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
- **harness-mastra**: wire Claude extended thinking through the Anthropic pass-through @W-24074389@ ([#825](https://github.com/forcedotcom/agentic-dx/pull/825))
|
|
10
|
+
|
|
11
|
+
## [0.84.0] - 2026-09-16
|
|
12
|
+
|
|
13
|
+
### Fixes
|
|
14
|
+
- **harness-mastra**: carry toolName on tool-call-delta so AG-UI TOOL_CALL_START isn't blank @W-23954558@ ([#826](https://github.com/forcedotcom/agentic-dx/pull/826))
|
|
15
|
+
|
|
6
16
|
## [0.83.0] - 2026-09-16
|
|
7
17
|
|
|
8
18
|
_No changes — released alongside dependent packages._
|
|
@@ -56,6 +56,7 @@ import type { ChatEvent } from '@salesforce/sfdx-agent-sdk';
|
|
|
56
56
|
export declare class MastraEventAdapter {
|
|
57
57
|
private readonly logBus?;
|
|
58
58
|
private stepIndex;
|
|
59
|
+
private readonly toolNamesByCallId;
|
|
59
60
|
/**
|
|
60
61
|
* @param logBus - Optional structured-log sink. When supplied, the adapter emits a `debug`
|
|
61
62
|
* log for every chunk it intentionally drops — informational lifecycle events, known-but-
|
|
@@ -60,6 +60,12 @@ import { isClassifiedStreamError } from './stream-errors.js';
|
|
|
60
60
|
export class MastraEventAdapter {
|
|
61
61
|
logBus;
|
|
62
62
|
stepIndex = 0;
|
|
63
|
+
// `toolCallId → toolName`, populated from each `tool-call-input-streaming-start`
|
|
64
|
+
// chunk. Mastra omits `toolName` from the `tool-call-delta` chunks that follow
|
|
65
|
+
// (it rides only on the streaming-start), so this lets `tool-call-delta` recover
|
|
66
|
+
// it — see the `tool-call-delta` case. Per-stream instance, so it never grows
|
|
67
|
+
// unbounded or leaks across turns.
|
|
68
|
+
toolNamesByCallId = new Map();
|
|
63
69
|
/**
|
|
64
70
|
* @param logBus - Optional structured-log sink. When supplied, the adapter emits a `debug`
|
|
65
71
|
* log for every chunk it intentionally drops — informational lifecycle events, known-but-
|
|
@@ -208,26 +214,46 @@ export class MastraEventAdapter {
|
|
|
208
214
|
toolName: chunk.payload.toolName ?? 'unknown',
|
|
209
215
|
output: chunk.payload.output,
|
|
210
216
|
};
|
|
211
|
-
case 'tool-call-delta':
|
|
217
|
+
case 'tool-call-delta': {
|
|
218
|
+
// Mastra drops `toolName` from this chunk — it carries it only on the
|
|
219
|
+
// preceding `tool-call-input-streaming-start` — so recover it from the
|
|
220
|
+
// per-stream map. Downstream AG-UI opens its TOOL_CALL_START off the
|
|
221
|
+
// first delta and can't correct a blank name from the terminal
|
|
222
|
+
// `tool-call`, so a missing name here surfaces as a blank tool name.
|
|
223
|
+
// Strict `!== undefined` (not truthy) so an empty-string toolName is
|
|
224
|
+
// forwarded honestly instead of silently swallowed.
|
|
225
|
+
const toolName = chunk.payload.toolName ?? this.toolNamesByCallId.get(chunk.payload.toolCallId);
|
|
212
226
|
return {
|
|
213
227
|
type: 'tool-call-delta',
|
|
214
228
|
toolCallId: chunk.payload.toolCallId,
|
|
215
|
-
|
|
216
|
-
// toolName from upstream — unlikely but possible — is
|
|
217
|
-
// forwarded honestly instead of silently swallowed.
|
|
218
|
-
...(chunk.payload.toolName !== undefined ? { toolName: chunk.payload.toolName } : {}),
|
|
229
|
+
...(toolName !== undefined ? { toolName } : {}),
|
|
219
230
|
argsTextDelta: chunk.payload.argsTextDelta,
|
|
220
231
|
};
|
|
232
|
+
}
|
|
221
233
|
// Known harness chunk types not yet supported in the SDK's ChatEvent
|
|
222
234
|
// model. Silently dropped to avoid leaking harness internals onto the
|
|
223
235
|
// public stream. Candidates for future ChatEvent variants when the
|
|
224
236
|
// corresponding features (citations, file generation, tool input
|
|
225
237
|
// lifecycle bookends, tool suspension, structured output) are
|
|
226
238
|
// supported.
|
|
227
|
-
case 'source':
|
|
228
|
-
case 'file':
|
|
229
239
|
case 'tool-call-input-streaming-start':
|
|
240
|
+
// Not surfaced on the public stream, but its `toolName` is the only
|
|
241
|
+
// place Mastra carries it for the streamed args — stash it so the
|
|
242
|
+
// following `tool-call-delta` chunks can recover it (see that case).
|
|
243
|
+
if (chunk.payload.toolName !== undefined) {
|
|
244
|
+
this.toolNamesByCallId.set(chunk.payload.toolCallId, chunk.payload.toolName);
|
|
245
|
+
}
|
|
246
|
+
this.logBus?.debug('Skipping unsupported Mastra chunk type', { chunkType: chunk.type });
|
|
247
|
+
return undefined;
|
|
230
248
|
case 'tool-call-input-streaming-end':
|
|
249
|
+
// End of the streamed-args span: no more deltas for this id will
|
|
250
|
+
// arrive, and the terminal `tool-call` carries its own name, so drop
|
|
251
|
+
// the tracked entry to keep the map bounded within a long stream.
|
|
252
|
+
this.toolNamesByCallId.delete(chunk.payload.toolCallId);
|
|
253
|
+
this.logBus?.debug('Skipping unsupported Mastra chunk type', { chunkType: chunk.type });
|
|
254
|
+
return undefined;
|
|
255
|
+
case 'source':
|
|
256
|
+
case 'file':
|
|
231
257
|
case 'tool-call-suspended':
|
|
232
258
|
case 'object':
|
|
233
259
|
this.logBus?.debug('Skipping unsupported Mastra chunk type', { chunkType: chunk.type });
|
|
@@ -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 {
|
|
8
|
+
import { SpanLifecycle, 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.
|
|
@@ -170,9 +170,17 @@ async function buildAnthropicRequest(ctx, options) {
|
|
|
170
170
|
commonParams.system = system;
|
|
171
171
|
if (tools.length > 0)
|
|
172
172
|
commonParams.tools = tools;
|
|
173
|
+
// Request extended thinking for Claude models that declare a thinking shape
|
|
174
|
+
// (`Model.thinkingMode`), so the model's planning narration streams back as
|
|
175
|
+
// `thinking` blocks the response path maps to `reasoning-delta` — reaching
|
|
176
|
+
// parity with the Claude harness, which drives the same per-model shape.
|
|
177
|
+
const thinkingEngaged = applyThinkingConfig(commonParams, info.model, info.providerHint);
|
|
173
178
|
// The gateway deprecated `temperature` for Claude Opus 4.7+ and rejects the
|
|
174
|
-
// request if it's present; drop it when the bound model excludes it.
|
|
175
|
-
|
|
179
|
+
// request if it's present; drop it when the bound model excludes it. Anthropic
|
|
180
|
+
// also rejects a non-default `temperature` whenever extended thinking is on, so
|
|
181
|
+
// drop it there too (the `enabled`/`adaptive` models that don't already exclude
|
|
182
|
+
// it — Claude 4.5/4.6 — would otherwise 400 the moment thinking is requested).
|
|
183
|
+
if (options.temperature !== undefined && !thinkingEngaged && !isParameterExcluded(info.model, 'temperature')) {
|
|
176
184
|
commonParams.temperature = options.temperature;
|
|
177
185
|
}
|
|
178
186
|
if (options.stopSequences !== undefined)
|
|
@@ -247,6 +255,57 @@ export function applyCacheBreakpoints(commonParams) {
|
|
|
247
255
|
}
|
|
248
256
|
}
|
|
249
257
|
}
|
|
258
|
+
/** Reasoning-effort directive paired with the adaptive `thinking` block; the
|
|
259
|
+
* gateway's Bedrock backend only emits thinking content for Claude 4.6+ when it
|
|
260
|
+
* is present. `'high'` matches the Claude Agent SDK's own default effort the
|
|
261
|
+
* Claude harness inherits, so both harnesses drive the same reasoning depth. The
|
|
262
|
+
* single seam for a future configurable reasoning-effort knob — see ARCHITECTURE.md. */
|
|
263
|
+
const DEFAULT_ADAPTIVE_THINKING_EFFORT = 'high';
|
|
264
|
+
/** Extended-thinking token budget for models on the legacy `enabled` shape
|
|
265
|
+
* (Claude 4.5). Anthropic requires `budget_tokens` ≥ 1024 and `< max_tokens`;
|
|
266
|
+
* the value is clamped against the request's `max_tokens` so the answer keeps
|
|
267
|
+
* room. */
|
|
268
|
+
const DEFAULT_THINKING_BUDGET_TOKENS = 4096;
|
|
269
|
+
const MIN_THINKING_BUDGET_TOKENS = 1024;
|
|
270
|
+
/**
|
|
271
|
+
* Requests extended thinking on `commonParams` per the bound model's
|
|
272
|
+
* `Model.thinkingMode`, mirroring the Claude harness's `mapThinkingMode` (which
|
|
273
|
+
* drives the same per-model shape onto the Claude CLI). Returns whether thinking
|
|
274
|
+
* was engaged so the caller can suppress `temperature` — Anthropic rejects a
|
|
275
|
+
* non-default temperature when extended thinking is on.
|
|
276
|
+
*
|
|
277
|
+
* The two Anthropic thinking shapes are NOT interchangeable across model
|
|
278
|
+
* generations, so the shape is a per-model fact carried on `thinkingMode`:
|
|
279
|
+
* - `'adaptive'` (Claude 4.6+): `{ type: 'adaptive' }` + `output_config.effort`.
|
|
280
|
+
* - `'enabled'` (Claude 4.5): `{ type: 'enabled', budget_tokens }`, budget
|
|
281
|
+
* clamped to `[1024, max_tokens)`; skipped when `max_tokens` can't fit the floor.
|
|
282
|
+
* - `'disabled'` / `undefined`: no thinking config — the request omits it.
|
|
283
|
+
*
|
|
284
|
+
* **Bedrock-only.** Thinking is requested only on the `bedrock-anthropic` gateway
|
|
285
|
+
* path. The direct first-party `anthropic` path is skipped: it enforces
|
|
286
|
+
* thinking-block echo-back on tool-use continuations, but `translatePromptToAnthropic`
|
|
287
|
+
* can't faithfully re-send a prior thinking block (no re-sendable signature in the
|
|
288
|
+
* summarized reasoning we surface, and Mastra's `reasoning` MessagePart has no slot
|
|
289
|
+
* to persist one), so enabling it there would 400 multi-turn tool use. Bedrock does
|
|
290
|
+
* not enforce that rule (verified on `orgfarm-e2e`).
|
|
291
|
+
*/
|
|
292
|
+
function applyThinkingConfig(params, model, providerHint) {
|
|
293
|
+
if (providerHint !== 'bedrock-anthropic')
|
|
294
|
+
return false;
|
|
295
|
+
const mode = model.thinkingMode;
|
|
296
|
+
if (mode === undefined || mode === 'disabled')
|
|
297
|
+
return false;
|
|
298
|
+
if (mode === 'adaptive') {
|
|
299
|
+
params.thinking = { type: 'adaptive', display: 'summarized' };
|
|
300
|
+
params.output_config = { ...params.output_config, effort: DEFAULT_ADAPTIVE_THINKING_EFFORT };
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
const budget = Math.max(MIN_THINKING_BUDGET_TOKENS, Math.min(DEFAULT_THINKING_BUDGET_TOKENS, params.max_tokens - MIN_THINKING_BUDGET_TOKENS));
|
|
304
|
+
if (budget >= params.max_tokens)
|
|
305
|
+
return false;
|
|
306
|
+
params.thinking = { type: 'enabled', budget_tokens: budget };
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
250
309
|
async function doGenerate(ctx, options) {
|
|
251
310
|
const { client, commonParams, toolSchemas } = await buildAnthropicRequest(ctx, options);
|
|
252
311
|
// Forward the turn's AbortSignal into the SDK's RequestOptions so firing the
|
|
@@ -266,13 +325,16 @@ async function doGenerate(ctx, options) {
|
|
|
266
325
|
if (block.type === 'text') {
|
|
267
326
|
content.push({ type: 'text', text: block.text });
|
|
268
327
|
}
|
|
328
|
+
else if (block.type === 'thinking') {
|
|
329
|
+
content.push({ type: 'reasoning', text: block.thinking });
|
|
330
|
+
}
|
|
269
331
|
else if (block.type === 'tool_use') {
|
|
270
332
|
sawToolCalls = true;
|
|
271
333
|
const argsBuffer = typeof block.input === 'string' ? block.input : JSON.stringify(block.input ?? {});
|
|
272
334
|
content.push(...buildValidatedToolCallChunk(block.id, block.name, argsBuffer, toolSchemas.get(block.name), ctx.getConsumerToolNames().has(block.name), { retryCounts: ctx.getToolArgsRetryCounts(options), logBus: ctx.logBus }));
|
|
273
335
|
}
|
|
274
|
-
// Other block types (
|
|
275
|
-
// the V3 ChatEvent surface — same posture as doStream.
|
|
336
|
+
// Other block types (redacted_thinking, server-tool-result, etc.) are
|
|
337
|
+
// not in the V3 ChatEvent surface — same posture as doStream.
|
|
276
338
|
}
|
|
277
339
|
const finishReason = mapAnthropicFinishReason(message.stop_reason, sawToolCalls);
|
|
278
340
|
const usage = mapAnthropicUsage(message.usage);
|
|
@@ -321,7 +383,14 @@ async function doStream(ctx, options) {
|
|
|
321
383
|
}
|
|
322
384
|
const stream = new ReadableStream({
|
|
323
385
|
start: async (controller) => {
|
|
324
|
-
const textSpan = new
|
|
386
|
+
const textSpan = new SpanLifecycle();
|
|
387
|
+
// Extended-thinking narration streams on its own V3 reasoning span,
|
|
388
|
+
// so it surfaces as `reasoning-delta` (→ AG-UI REASONING_MESSAGE_CONTENT)
|
|
389
|
+
// rather than bleeding into the answer's `text-delta`.
|
|
390
|
+
const reasoningSpan = new SpanLifecycle('reasoning');
|
|
391
|
+
// Content-block indexes currently carrying a `thinking` block, so the
|
|
392
|
+
// matching `content_block_stop` closes the reasoning span.
|
|
393
|
+
const thinkingIndexes = new Set();
|
|
325
394
|
// Per-content-block buffers — Anthropic streams tool_use input as a
|
|
326
395
|
// sequence of input_json_delta strings on a single content block.
|
|
327
396
|
const toolUseByIndex = new Map();
|
|
@@ -353,9 +422,18 @@ async function doStream(ctx, options) {
|
|
|
353
422
|
// (or after a tool_use rotated us forward).
|
|
354
423
|
textSpan.openIfNeeded(controller);
|
|
355
424
|
}
|
|
425
|
+
else if (block.type === 'thinking') {
|
|
426
|
+
// Thinking blocks precede text/tool_use in the stream; open a
|
|
427
|
+
// reasoning span that the matching content_block_stop closes.
|
|
428
|
+
thinkingIndexes.add(event.index);
|
|
429
|
+
reasoningSpan.openIfNeeded(controller);
|
|
430
|
+
}
|
|
356
431
|
else if (block.type === 'tool_use') {
|
|
357
|
-
// A tool_use block follows any open text — close
|
|
358
|
-
// bump the id so post-tool text lands in a fresh
|
|
432
|
+
// A tool_use block follows any open text/reasoning — close those
|
|
433
|
+
// spans and bump the text id so post-tool text lands in a fresh
|
|
434
|
+
// span. Reasoning normally closes on its own content_block_stop;
|
|
435
|
+
// this covers a (non-conformant) interleaved thinking → tool_use.
|
|
436
|
+
reasoningSpan.rotate(controller);
|
|
359
437
|
textSpan.rotate(controller);
|
|
360
438
|
// Stream incremental tool-call arg deltas alongside the
|
|
361
439
|
// unchanged terminal `tool-call` (W-23954558); see
|
|
@@ -367,8 +445,8 @@ async function doStream(ctx, options) {
|
|
|
367
445
|
input: new ToolInputSpanLifecycle(block.id, block.name, ctx.logBus),
|
|
368
446
|
});
|
|
369
447
|
}
|
|
370
|
-
// Other block types (
|
|
371
|
-
// intentionally ignored — they're not in the V3 ChatEvent surface.
|
|
448
|
+
// Other block types (redacted_thinking, server-tool result, etc.)
|
|
449
|
+
// are intentionally ignored — they're not in the V3 ChatEvent surface.
|
|
372
450
|
break;
|
|
373
451
|
}
|
|
374
452
|
case 'content_block_delta': {
|
|
@@ -377,6 +455,10 @@ async function doStream(ctx, options) {
|
|
|
377
455
|
textSpan.delta(controller, delta.text);
|
|
378
456
|
sawContentOrUsage = true;
|
|
379
457
|
}
|
|
458
|
+
else if (delta.type === 'thinking_delta') {
|
|
459
|
+
reasoningSpan.delta(controller, delta.thinking);
|
|
460
|
+
sawContentOrUsage = true;
|
|
461
|
+
}
|
|
380
462
|
else if (delta.type === 'input_json_delta') {
|
|
381
463
|
const tu = toolUseByIndex.get(event.index);
|
|
382
464
|
if (tu) {
|
|
@@ -384,12 +466,18 @@ async function doStream(ctx, options) {
|
|
|
384
466
|
tu.input.delta(controller, delta.partial_json);
|
|
385
467
|
}
|
|
386
468
|
}
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
//
|
|
469
|
+
// signature_delta / citations_delta are not surfaced — the
|
|
470
|
+
// encrypted thinking signature has no V3 stream-part and no
|
|
471
|
+
// consumer surface (AG-UI ReasoningEncryptedValue is deferred).
|
|
390
472
|
break;
|
|
391
473
|
}
|
|
392
474
|
case 'content_block_stop': {
|
|
475
|
+
if (thinkingIndexes.delete(event.index)) {
|
|
476
|
+
// rotate (close + bump id) so a later thinking block in the
|
|
477
|
+
// same response opens a fresh reasoning span rather than
|
|
478
|
+
// reusing this one's id.
|
|
479
|
+
reasoningSpan.rotate(controller);
|
|
480
|
+
}
|
|
393
481
|
const tu = toolUseByIndex.get(event.index);
|
|
394
482
|
if (tu) {
|
|
395
483
|
sawToolCalls = true;
|
|
@@ -411,6 +499,7 @@ async function doStream(ctx, options) {
|
|
|
411
499
|
break;
|
|
412
500
|
}
|
|
413
501
|
case 'message_stop': {
|
|
502
|
+
reasoningSpan.closeIfOpen(controller);
|
|
414
503
|
textSpan.closeIfOpen(controller);
|
|
415
504
|
const finishReason = mapAnthropicFinishReason(finishRaw, sawToolCalls);
|
|
416
505
|
const usage = finalizeUsage(mergedUsage, ctx, finishReason.raw);
|
|
@@ -430,6 +519,7 @@ async function doStream(ctx, options) {
|
|
|
430
519
|
// swallows the abort), and its terminal event is the coordinator's
|
|
431
520
|
// teardown, not an empty-stream error.
|
|
432
521
|
if (!terminalEmitted) {
|
|
522
|
+
reasoningSpan.closeIfOpen(controller);
|
|
433
523
|
textSpan.closeIfOpen(controller);
|
|
434
524
|
if (!sawContentOrUsage && !options.abortSignal?.aborted) {
|
|
435
525
|
controller.enqueue({
|
|
@@ -520,6 +610,15 @@ export function translatePromptToAnthropic(prompt) {
|
|
|
520
610
|
input: parseToolInput(tc.input),
|
|
521
611
|
});
|
|
522
612
|
}
|
|
613
|
+
// `reasoning` parts are intentionally NOT re-sent as `thinking` blocks.
|
|
614
|
+
// Anthropic's first-party API requires a tool-use turn's preceding
|
|
615
|
+
// thinking block (with its signature) to be echoed back, but the
|
|
616
|
+
// gateway's Bedrock adaptive path does not enforce this — multi-step
|
|
617
|
+
// MCP tool loops run green with thinking enabled (orgfarm-e2e). A faithful
|
|
618
|
+
// echo isn't achievable here anyway: the summarized reasoning we surface
|
|
619
|
+
// carries no re-sendable signature, and Mastra's `reasoning` MessagePart
|
|
620
|
+
// has no signature slot to persist one across turns. Emitting a thinking
|
|
621
|
+
// block without a valid signature would 400 — strictly worse than dropping it.
|
|
523
622
|
}
|
|
524
623
|
if (blocks.length > 0)
|
|
525
624
|
messages.push({ role: 'assistant', content: blocks });
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import OpenAI from 'openai';
|
|
6
6
|
import { EmptyStreamError } from './stream-errors.js';
|
|
7
|
-
import {
|
|
7
|
+
import { SpanLifecycle, 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
|
|
@@ -137,7 +137,7 @@ async function doStream(ctx, options) {
|
|
|
137
137
|
const sdkStream = await client.chat.completions.create(params, { signal: options.abortSignal });
|
|
138
138
|
const stream = new ReadableStream({
|
|
139
139
|
start: async (controller) => {
|
|
140
|
-
const textSpan = new
|
|
140
|
+
const textSpan = new SpanLifecycle();
|
|
141
141
|
// Tool calls accumulate across delta chunks. Chat Completions delivers
|
|
142
142
|
// them keyed by `index` on the delta; the upstream may also report `id`
|
|
143
143
|
// and `function.name` on the first delta and only `function.arguments`
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import OpenAI from 'openai';
|
|
6
6
|
import { EmptyStreamError, GatewayResponseError } from './stream-errors.js';
|
|
7
|
-
import {
|
|
7
|
+
import { SpanLifecycle, 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
|
|
@@ -120,7 +120,7 @@ async function doStream(ctx, options) {
|
|
|
120
120
|
const sdkStream = await client.responses.create(params, { signal: options.abortSignal });
|
|
121
121
|
const stream = new ReadableStream({
|
|
122
122
|
start: async (controller) => {
|
|
123
|
-
const textSpan = new
|
|
123
|
+
const textSpan = new SpanLifecycle();
|
|
124
124
|
// Tool-call output_items, keyed by their `item_id`. Populated on
|
|
125
125
|
// `output_item.added(type=function_call)`, args buffered on
|
|
126
126
|
// `function_call_arguments.delta`, finalized on `output_item.done`.
|
|
@@ -27,25 +27,32 @@ import type { Model } from '@salesforce/sfdx-agent-sdk';
|
|
|
27
27
|
* deprecated parameter declare `excludedParameters`.
|
|
28
28
|
*/
|
|
29
29
|
export declare function isParameterExcluded(model: Model, parameter: string): boolean;
|
|
30
|
+
/** The AI SDK V3 span kinds this lifecycle drives — `text` for assistant
|
|
31
|
+
* answer text, `reasoning` for extended-thinking narration. Both share the
|
|
32
|
+
* identical `${kind}-start` / `${kind}-delta` / `${kind}-end` shape. */
|
|
33
|
+
export type SpanKind = 'text' | 'reasoning';
|
|
30
34
|
/**
|
|
31
|
-
* Encodes
|
|
32
|
-
* `
|
|
35
|
+
* Encodes an AI SDK V3 content-span lifecycle (`${kind}-start` /
|
|
36
|
+
* `${kind}-delta` / `${kind}-end`) and the rotation that must happen around
|
|
37
|
+
* tool calls.
|
|
33
38
|
*
|
|
34
|
-
* V3 requires every
|
|
35
|
-
*
|
|
39
|
+
* V3 requires every `${kind}-delta` to sit between a matching `${kind}-start` /
|
|
40
|
+
* `${kind}-end` pair sharing the same `id`. When tool calls land mid-stream the
|
|
36
41
|
* current span MUST be closed before the tool-call parts land, and the next
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
42
|
+
* delta MUST open a fresh span with a new id. This class owns the `open` / `id`
|
|
43
|
+
* state machine so every `LanguageModelV3` impl shares one encoding for both
|
|
44
|
+
* assistant text and reasoning (Anthropic `thinking`) spans.
|
|
40
45
|
*/
|
|
41
|
-
export declare class
|
|
46
|
+
export declare class SpanLifecycle {
|
|
47
|
+
private readonly kind;
|
|
42
48
|
private open;
|
|
43
49
|
private id;
|
|
50
|
+
constructor(kind?: SpanKind);
|
|
44
51
|
/** Open a span if none is open. No-op when a span is already open. */
|
|
45
52
|
openIfNeeded(controller: {
|
|
46
53
|
enqueue: (part: LanguageModelV3StreamPart) => void;
|
|
47
54
|
}): void;
|
|
48
|
-
/** Emit a
|
|
55
|
+
/** Emit a `${kind}-delta`. Opens a span first if none is open. */
|
|
49
56
|
delta(controller: {
|
|
50
57
|
enqueue: (part: LanguageModelV3StreamPart) => void;
|
|
51
58
|
}, text: string): void;
|
|
@@ -36,36 +36,41 @@ export function isParameterExcluded(model, parameter) {
|
|
|
36
36
|
return model.excludedParameters?.includes(parameter) ?? false;
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
|
-
* Encodes
|
|
40
|
-
* `
|
|
39
|
+
* Encodes an AI SDK V3 content-span lifecycle (`${kind}-start` /
|
|
40
|
+
* `${kind}-delta` / `${kind}-end`) and the rotation that must happen around
|
|
41
|
+
* tool calls.
|
|
41
42
|
*
|
|
42
|
-
* V3 requires every
|
|
43
|
-
*
|
|
43
|
+
* V3 requires every `${kind}-delta` to sit between a matching `${kind}-start` /
|
|
44
|
+
* `${kind}-end` pair sharing the same `id`. When tool calls land mid-stream the
|
|
44
45
|
* current span MUST be closed before the tool-call parts land, and the next
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
46
|
+
* delta MUST open a fresh span with a new id. This class owns the `open` / `id`
|
|
47
|
+
* state machine so every `LanguageModelV3` impl shares one encoding for both
|
|
48
|
+
* assistant text and reasoning (Anthropic `thinking`) spans.
|
|
48
49
|
*/
|
|
49
|
-
export class
|
|
50
|
+
export class SpanLifecycle {
|
|
51
|
+
kind;
|
|
50
52
|
open = false;
|
|
51
53
|
id = 0;
|
|
54
|
+
constructor(kind = 'text') {
|
|
55
|
+
this.kind = kind;
|
|
56
|
+
}
|
|
52
57
|
/** Open a span if none is open. No-op when a span is already open. */
|
|
53
58
|
openIfNeeded(controller) {
|
|
54
59
|
if (this.open)
|
|
55
60
|
return;
|
|
56
|
-
controller.enqueue({ type:
|
|
61
|
+
controller.enqueue({ type: `${this.kind}-start`, id: this.spanId() });
|
|
57
62
|
this.open = true;
|
|
58
63
|
}
|
|
59
|
-
/** Emit a
|
|
64
|
+
/** Emit a `${kind}-delta`. Opens a span first if none is open. */
|
|
60
65
|
delta(controller, text) {
|
|
61
66
|
this.openIfNeeded(controller);
|
|
62
|
-
controller.enqueue({ type:
|
|
67
|
+
controller.enqueue({ type: `${this.kind}-delta`, delta: text, id: this.spanId() });
|
|
63
68
|
}
|
|
64
69
|
/** Close the open span (if any). No-op when no span is open. */
|
|
65
70
|
closeIfOpen(controller) {
|
|
66
71
|
if (!this.open)
|
|
67
72
|
return;
|
|
68
|
-
controller.enqueue({ type:
|
|
73
|
+
controller.enqueue({ type: `${this.kind}-end`, id: this.spanId() });
|
|
69
74
|
this.open = false;
|
|
70
75
|
}
|
|
71
76
|
/**
|
|
@@ -78,7 +83,7 @@ export class TextSpanLifecycle {
|
|
|
78
83
|
this.id++;
|
|
79
84
|
}
|
|
80
85
|
spanId() {
|
|
81
|
-
return
|
|
86
|
+
return `${this.kind}-${this.id}`;
|
|
82
87
|
}
|
|
83
88
|
}
|
|
84
89
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/sfdx-agent-harness-mastra",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.85.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.86.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.86.0",
|
|
51
51
|
"@types/node": "^22.20.1",
|
|
52
52
|
"@vitest/coverage-istanbul": "^4.1.11",
|
|
53
53
|
"@vitest/eslint-plugin": "^1.6.27",
|