@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 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
- // Strict `!== undefined` (not truthy) so an empty-string
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 { TextSpanLifecycle, ToolInputSpanLifecycle, buildValidatedToolCallChunk, createGatewayFetch, isEmptyUsage, isParameterExcluded, mapAnthropicFinishReason, mapAnthropicUsage, toolResultOutputToString, } from './v3-stream-mapping.js';
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
- if (options.temperature !== undefined && !isParameterExcluded(info.model, 'temperature')) {
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 (thinking, server-tool-result, etc.) are not in
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 TextSpanLifecycle();
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 that span and
358
- // bump the id so post-tool text lands in a fresh span.
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 (thinking, server-tool result, etc.) are
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
- // thinking_delta / signature_delta / citations_delta are not
388
- // currently surfaced — Mastra has no V3 stream-part for them in
389
- // the chat-generations path either.
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 { TextSpanLifecycle, ToolInputSpanLifecycle, buildValidatedToolCallChunk, createGatewayFetch, isEmptyUsage, isParameterExcluded, mapOpenAIUsage, toolResultOutputToString, } from './v3-stream-mapping.js';
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 TextSpanLifecycle();
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 { TextSpanLifecycle, ToolInputSpanLifecycle, buildValidatedToolCallChunk, createGatewayFetch, isEmptyUsage, isParameterExcluded, mapOpenAIFinishReason, mapOpenAIUsage, toolResultOutputToString, } from './v3-stream-mapping.js';
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 TextSpanLifecycle();
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 the AI SDK V3 text-span lifecycle (`text-start` / `text-delta` /
32
- * `text-end`) and the rotation that must happen around tool calls.
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 `text-delta` to sit between a matching `text-start` /
35
- * `text-end` pair sharing the same `id`. When tool calls land mid-stream the
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
- * text delta MUST open a fresh span with a new id. This class owns the
38
- * `open` / `id` state machine so every `LanguageModelV3` impl shares one
39
- * encoding.
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 TextSpanLifecycle {
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 `text-delta`. Opens a span first if none is open. */
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 the AI SDK V3 text-span lifecycle (`text-start` / `text-delta` /
40
- * `text-end`) and the rotation that must happen around tool calls.
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 `text-delta` to sit between a matching `text-start` /
43
- * `text-end` pair sharing the same `id`. When tool calls land mid-stream the
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
- * text delta MUST open a fresh span with a new id. This class owns the
46
- * `open` / `id` state machine so every `LanguageModelV3` impl shares one
47
- * encoding.
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 TextSpanLifecycle {
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: 'text-start', id: this.spanId() });
61
+ controller.enqueue({ type: `${this.kind}-start`, id: this.spanId() });
57
62
  this.open = true;
58
63
  }
59
- /** Emit a `text-delta`. Opens a span first if none is open. */
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: 'text-delta', delta: text, id: this.spanId() });
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: 'text-end', id: this.spanId() });
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 `text-${this.id}`;
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.83.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.84.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.84.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",