@salesforce/sfdx-agent-harness-mastra 0.45.0 → 0.47.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.47.0] - 2026-08-10
7
+
8
+ ### Fixes
9
+ - **harness-mastra**: classify stream truncations @W-23801521 ([#746](https://github.com/forcedotcom/agentic-dx/pull/746))
10
+
11
+ ## [0.46.0] - 2026-08-10
12
+
13
+ ### Fixes
14
+ - **harness-mastra**: normalize empty tool-call args to "{}" across all passthroughs @W-23789221@ ([#745](https://github.com/forcedotcom/agentic-dx/pull/745))
15
+
6
16
  ## [0.45.0] - 2026-08-10
7
17
 
8
18
  ### Fixes
@@ -2,6 +2,7 @@
2
2
  * Copyright 2026, Salesforce, Inc. All rights reserved.
3
3
  * See LICENSE.txt for license terms.
4
4
  */
5
+ import { isNetworkStreamError } from './stream-errors.js';
5
6
  /**
6
7
  * Adapts Mastra `ChunkType` stream events to our {@link ChatEvent} discriminated union.
7
8
  *
@@ -146,6 +147,7 @@ export class MastraEventAdapter {
146
147
  return {
147
148
  type: 'error',
148
149
  error: rawError instanceof Error ? rawError : new Error(String(rawError)),
150
+ ...(isNetworkStreamError(rawError) ? { code: rawError.code } : {}),
149
151
  };
150
152
  }
151
153
  case 'tripwire':
@@ -4,7 +4,8 @@
4
4
  */
5
5
  import { AnthropicBedrock } from '@anthropic-ai/bedrock-sdk';
6
6
  import Anthropic from '@anthropic-ai/sdk';
7
- import { TextSpanLifecycle, createGatewayFetch, isEmptyUsage, isParameterExcluded, mapAnthropicFinishReason, mapAnthropicUsage, toolResultOutputToString, } from './v3-stream-mapping.js';
7
+ import { NetworkStreamError, isStreamTransportError } from './stream-errors.js';
8
+ import { TextSpanLifecycle, createGatewayFetch, isEmptyUsage, isParameterExcluded, mapAnthropicFinishReason, mapAnthropicUsage, normalizeToolArgsString, toolResultOutputToString, } from './v3-stream-mapping.js';
8
9
  /**
9
10
  * Builds the in-process `LanguageModelV3` impl for the Salesforce LLM Gateway's
10
11
  * `/invoke-with-response-stream` (Bedrock-Anthropic pass-through) endpoint.
@@ -266,17 +267,9 @@ async function doStream(ctx, options) {
266
267
  type: 'tool-call',
267
268
  toolCallId: tu.id,
268
269
  toolName: tu.name,
269
- // A no-arg tool (e.g. startBuilding) streams no
270
- // input_json_delta events, so argsBuffer stays ''.
271
- // '' is not a schema-conforming stringified JSON
272
- // object — a strict consumer's JSON.parse throws and
273
- // the model retries. Coerce the empty buffer to '{}'
274
- // (the only valid no-arg value); non-empty buffers,
275
- // even malformed ones, pass through verbatim so we
276
- // never mask a genuinely bad payload. W-23789221;
277
- // matches the non-streaming path + parseToolInput
278
- // (same `.trim()` empty check as the latter).
279
- input: tu.argsBuffer.trim() === '' ? '{}' : tu.argsBuffer,
270
+ // No-arg tool → empty argsBuffer; normalize to '{}'.
271
+ // See normalizeToolArgsString (W-23789221).
272
+ input: normalizeToolArgsString(tu.argsBuffer),
280
273
  });
281
274
  toolUseByIndex.delete(event.index);
282
275
  }
@@ -318,7 +311,10 @@ async function doStream(ctx, options) {
318
311
  controller.close();
319
312
  return;
320
313
  }
321
- controller.enqueue({ type: 'error', error });
314
+ controller.enqueue({
315
+ type: 'error',
316
+ error: isStreamTransportError(error) ? new NetworkStreamError(error) : error,
317
+ });
322
318
  controller.close();
323
319
  }
324
320
  },
@@ -3,7 +3,7 @@
3
3
  * See LICENSE.txt for license terms.
4
4
  */
5
5
  import OpenAI from 'openai';
6
- import { TextSpanLifecycle, createGatewayFetch, isEmptyUsage, isParameterExcluded, mapOpenAIUsage, toolResultOutputToString, } from './v3-stream-mapping.js';
6
+ import { TextSpanLifecycle, createGatewayFetch, isEmptyUsage, isParameterExcluded, mapOpenAIUsage, normalizeToolArgsString, toolResultOutputToString, } from './v3-stream-mapping.js';
7
7
  /**
8
8
  * Builds the in-process `LanguageModelV3` impl that drives the `openai` SDK's
9
9
  * `chat.completions.create({ stream: true })` against an OpenAI-compatible
@@ -94,7 +94,8 @@ async function doGenerate(ctx, options) {
94
94
  type: 'tool-call',
95
95
  toolCallId: tc.id,
96
96
  toolName: tc.function.name,
97
- input: tc.function.arguments ?? '',
97
+ // No-arg tool → empty arguments; normalize to '{}' (W-23789221).
98
+ input: normalizeToolArgsString(tc.function.arguments),
98
99
  });
99
100
  }
100
101
  }
@@ -212,7 +213,8 @@ async function doStream(ctx, options) {
212
213
  type: 'tool-call',
213
214
  toolCallId: acc.id,
214
215
  toolName: acc.name,
215
- input: acc.argsBuffer,
216
+ // No-arg tool → empty argsBuffer; normalize to '{}' (W-23789221).
217
+ input: normalizeToolArgsString(acc.argsBuffer),
216
218
  });
217
219
  }
218
220
  toolCallsByIndex.clear();
@@ -3,7 +3,7 @@
3
3
  * See LICENSE.txt for license terms.
4
4
  */
5
5
  import OpenAI from 'openai';
6
- import { TextSpanLifecycle, createGatewayFetch, isEmptyUsage, isParameterExcluded, mapOpenAIFinishReason, mapOpenAIUsage, toolResultOutputToString, } from './v3-stream-mapping.js';
6
+ import { TextSpanLifecycle, createGatewayFetch, isEmptyUsage, isParameterExcluded, mapOpenAIFinishReason, mapOpenAIUsage, normalizeToolArgsString, toolResultOutputToString, } from './v3-stream-mapping.js';
7
7
  /**
8
8
  * Builds the in-process `LanguageModelV3` impl that drives the `openai` SDK's
9
9
  * `responses.create({ stream: true })` against the Salesforce LLM Gateway's
@@ -89,7 +89,8 @@ async function doGenerate(ctx, options) {
89
89
  type: 'tool-call',
90
90
  toolCallId: item.call_id,
91
91
  toolName: item.name,
92
- input: item.arguments ?? '',
92
+ // No-arg tool → empty arguments; normalize to '{}' (W-23789221).
93
+ input: normalizeToolArgsString(item.arguments),
93
94
  });
94
95
  }
95
96
  // Other output-item variants (reasoning, web/file/code search, MCP) are
@@ -149,7 +150,8 @@ async function doStream(ctx, options) {
149
150
  type: 'tool-call',
150
151
  toolCallId: item.call_id,
151
152
  toolName: item.name,
152
- input: finalArgs,
153
+ // No-arg tool → empty finalArgs; normalize to '{}' (W-23789221).
154
+ input: normalizeToolArgsString(finalArgs),
153
155
  });
154
156
  if (item.id)
155
157
  toolCallById.delete(item.id);
@@ -0,0 +1,8 @@
1
+ export declare const NETWORK_ERROR_CODE: 'network-error';
2
+ export declare class NetworkStreamError extends Error {
3
+ readonly name = "NetworkStreamError";
4
+ readonly code: "network-error";
5
+ constructor(cause: Error);
6
+ }
7
+ export declare function isStreamTransportError(error: unknown): error is Error;
8
+ export declare function isNetworkStreamError(error: unknown): error is NetworkStreamError;
@@ -0,0 +1,33 @@
1
+ /*
2
+ * Copyright 2026, Salesforce, Inc. All rights reserved.
3
+ * See LICENSE.txt for license terms.
4
+ */
5
+ export const NETWORK_ERROR_CODE = 'network-error';
6
+ export class NetworkStreamError extends Error {
7
+ name = 'NetworkStreamError';
8
+ code = NETWORK_ERROR_CODE;
9
+ constructor(cause) {
10
+ super(cause.message, { cause });
11
+ }
12
+ }
13
+ export function isStreamTransportError(error) {
14
+ const seen = new Set();
15
+ let current = error;
16
+ while (current instanceof Error && !seen.has(current)) {
17
+ seen.add(current);
18
+ if ('code' in current && current.code === 'UND_ERR_SOCKET')
19
+ return true;
20
+ const message = current.message.toLowerCase().replace(/[.!]$/, '');
21
+ if (message === 'terminated' ||
22
+ message === 'other side closed' ||
23
+ message === 'truncated event message received') {
24
+ return true;
25
+ }
26
+ current = current.cause;
27
+ }
28
+ return false;
29
+ }
30
+ export function isNetworkStreamError(error) {
31
+ return error instanceof NetworkStreamError;
32
+ }
33
+ //# sourceMappingURL=stream-errors.js.map
@@ -265,6 +265,34 @@ type WireCommunicationLikeEvent = {
265
265
  * worst place for a copy-paste drift to land.
266
266
  */
267
267
  export declare function toolResultOutputToString(output: LanguageModelV3ToolResultOutput): string;
268
+ /**
269
+ * Normalize a streamed/returned tool-call arguments STRING to a
270
+ * schema-conforming stringified JSON object.
271
+ *
272
+ * A no-argument tool (input schema `{ type:'object', properties:{} }`) accrues
273
+ * no argument fragments on the wire — Anthropic streams no `input_json_delta`,
274
+ * the OpenAI Responses/Chat-Completions APIs send no arg deltas — so the
275
+ * accumulated buffer is the empty string `''`. `''` is NOT a schema-conforming
276
+ * stringified JSON object: a strict consumer (Mastra `secureJsonParse`, any
277
+ * `JSON.parse`) throws "the provided JSON could not be parsed" and the model
278
+ * retries, producing the empty-args retry loop (W-23789221; same class as the
279
+ * closed gateway-processor defect W-22696051). `'{}'` is the only valid no-arg
280
+ * value.
281
+ *
282
+ * Empty (including whitespace-only) → `'{}'`. Every non-empty buffer — even a
283
+ * malformed partial like `'{"q":'` — is returned VERBATIM: we never re-parse or
284
+ * re-stringify real content, so a genuinely bad payload still reaches the
285
+ * consumer and is still rejected. Applied at every provider's tool-call
286
+ * emission seam so the rule lives in one auditable place.
287
+ *
288
+ * Sibling to `sfdx-agent-chat-generations`'s `normalizeToolArguments` (the
289
+ * gpt5-processor path). This one additionally coerces a whitespace-only buffer
290
+ * — the pass-through streaming paths accumulate raw wire fragments that could in
291
+ * principle be whitespace, whereas the chat-generations processor normalizes
292
+ * earlier. The packages share no dependency, so the rule is duplicated rather
293
+ * than imported; keep the two in sync if the empty-definition changes.
294
+ */
295
+ export declare function normalizeToolArgsString(args: string | null | undefined): string;
268
296
  /**
269
297
  * Predicate the missing-usage faithfulness invariant (W-22692131) enforces at
270
298
  * every terminal `finish` site: when every input/output token field on the
@@ -595,6 +595,36 @@ export function toolResultOutputToString(output) {
595
595
  return output.reason ?? 'tool execution denied';
596
596
  }
597
597
  }
598
+ /**
599
+ * Normalize a streamed/returned tool-call arguments STRING to a
600
+ * schema-conforming stringified JSON object.
601
+ *
602
+ * A no-argument tool (input schema `{ type:'object', properties:{} }`) accrues
603
+ * no argument fragments on the wire — Anthropic streams no `input_json_delta`,
604
+ * the OpenAI Responses/Chat-Completions APIs send no arg deltas — so the
605
+ * accumulated buffer is the empty string `''`. `''` is NOT a schema-conforming
606
+ * stringified JSON object: a strict consumer (Mastra `secureJsonParse`, any
607
+ * `JSON.parse`) throws "the provided JSON could not be parsed" and the model
608
+ * retries, producing the empty-args retry loop (W-23789221; same class as the
609
+ * closed gateway-processor defect W-22696051). `'{}'` is the only valid no-arg
610
+ * value.
611
+ *
612
+ * Empty (including whitespace-only) → `'{}'`. Every non-empty buffer — even a
613
+ * malformed partial like `'{"q":'` — is returned VERBATIM: we never re-parse or
614
+ * re-stringify real content, so a genuinely bad payload still reaches the
615
+ * consumer and is still rejected. Applied at every provider's tool-call
616
+ * emission seam so the rule lives in one auditable place.
617
+ *
618
+ * Sibling to `sfdx-agent-chat-generations`'s `normalizeToolArguments` (the
619
+ * gpt5-processor path). This one additionally coerces a whitespace-only buffer
620
+ * — the pass-through streaming paths accumulate raw wire fragments that could in
621
+ * principle be whitespace, whereas the chat-generations processor normalizes
622
+ * earlier. The packages share no dependency, so the rule is duplicated rather
623
+ * than imported; keep the two in sync if the empty-definition changes.
624
+ */
625
+ export function normalizeToolArgsString(args) {
626
+ return args && args.trim() !== '' ? args : '{}';
627
+ }
598
628
  /**
599
629
  * Predicate the missing-usage faithfulness invariant (W-22692131) enforces at
600
630
  * every terminal `finish` site: when every input/output token field on the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/sfdx-agent-harness-mastra",
3
- "version": "0.45.0",
3
+ "version": "0.47.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.46.0"
45
+ "@salesforce/sfdx-agent-sdk": "0.48.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.46.0",
50
+ "@salesforce/sfdx-agent-sdk": "0.48.0",
51
51
  "@types/node": "^22.20.1",
52
52
  "@vitest/coverage-istanbul": "^4.1.10",
53
53
  "@vitest/eslint-plugin": "^1.6.26",