@salesforce/sfdx-agent-sdk 0.72.0 → 0.74.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,19 @@
3
3
  All notable changes to `@salesforce/sfdx-agent-sdk` are documented in this file.
4
4
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
+ ## [0.74.0] - 2026-09-08
7
+
8
+ ### Features
9
+ - **agent-sdk**: self-describing usage fields on chat-stream-completed log @W-24125082@ ([#800](https://github.com/forcedotcom/agentic-dx/pull/800))
10
+
11
+ ### Chores
12
+ - **deps-dev**: bump the dev-dependencies group with 3 updates ([#795](https://github.com/forcedotcom/agentic-dx/pull/795))
13
+
14
+ ## [0.73.0] - 2026-09-04
15
+
16
+ ### Features
17
+ - **agent-sdk,harness-claude,harness-mastra,harness-openai**: validate frontend tool-call args @W-24023028@ ([#789](https://github.com/forcedotcom/agentic-dx/pull/789))
18
+
6
19
  ## [0.72.0] - 2026-09-02
7
20
 
8
21
  ### Features
package/README.md CHANGED
@@ -174,7 +174,7 @@ A single conversation thread.
174
174
  | `getContextUsage` | `() => ContextUsage` | Snapshot of how much of the model's context window the most recent turn used. |
175
175
  | `addMessages` | `(message: string \| Message[]) => Promise<void>` | Append real transcript messages (`user` / `assistant` / `tool`) to the thread **without requesting an agent response** — the write-only half of a turn. The messages persist, appear in `getMessageHistory()`, and replay to the model as prior conversation on the next `chat()`. Use it to seed earlier turns (e.g. file contents as a user message) before the first live prompt; the SDK equivalent of the service's `POST /messages` with `noReply=true`. **Not** `setSessionContext`: this writes _transcript history_ (visible in `getMessageHistory`, additive); `setSessionContext` writes an _out-of-history overlay object_ (never in history, whole-object replace). `'system'` is not a valid role here — system-level state rides `setSessionContext` / `AgentConfig.instructions`. |
176
176
  | `addContext` | `(message: string \| Message[]) => Promise<void>` | **Deprecated** — renamed to `addMessages` (identical signature/behavior); delegates to it. The old name read as a sibling of `setSessionContext`, but the two are distinct channels. Will be removed in a future release; migrate to `addMessages`. |
177
- | `setSessionContext` | `(content: SessionContext) => Promise<void>` | Replace this session's session-context object in full (whole-object set, not a merge). Persisted per-thread and durable across restart; kept out of message history, so it never appears in `getMessageHistory()`. Rendered into the model's system-level context on every subsequent turn (deterministic, key-ordered) on all three harnesses (**Mastra** W-23632686, **Claude** W-23632691, and **OpenAI** W-23632694). Delegates to `AgentHarness.setSessionContext` — see that method's JSDoc for the full delivery/durability/isolation contract. |
177
+ | `setSessionContext` | `(content: SessionContext) => Promise<void>` | Replace this session's session-context object in full (whole-object set, not a merge). Persisted per-thread and durable across restart; kept out of message history, so it never appears in `getMessageHistory()`. Rendered into the model's system-level context on every subsequent turn (deterministic, key-ordered) on all three harnesses (**Mastra** W-23632686, **Claude** W-23632691, and **OpenAI** W-23632694). Delegates to `AgentHarness.setSessionContext` — see that method's JSDoc for the full delivery/durability/isolation contract. |
178
178
  | `getSessionContext` | `() => Promise<SessionContext>` | Read this session's current session-context object. Returns `{}` (an empty object) — never `null` or `undefined` — when nothing has been set on this thread yet, so callers never need a null-check. Unrelated to `getContextUsage()`, which reports context-window token occupancy, not the seeded context object. |
179
179
  | `subscribe` | `(callback: (event: ChatEvent) => void) => void` | Register a real-time event listener. |
180
180
  | `unsubscribe` | `(callback: (event: ChatEvent) => void) => void` | Remove a listener. |
@@ -783,6 +783,7 @@ from `AgentSDKErrorType`:
783
783
  | `DISPOSED` | `Agent` and `ChatSession` methods called after the owner has been destroyed |
784
784
  | `INCOMPATIBLE_HARNESS` | `createAgentManager()` when the factory advertises an unsupported `protocolVersion`, or the constructed harness reports a `protocolVersion` that differs from the factory's |
785
785
  | `INVALID_MESSAGE_CONTENT` | `ChatSession.chat()` / harness `stream()` when a message part is not valid as input (a `tool-call`/`tool-result` part, or non-base64-string file data); also `ChatSession.setSessionContext()` / harness `setSessionContext()` when the object exceeds a harness's size / nesting bounds (Mastra: 256 KiB serialized, depth 200). `getSessionContext()` never throws on a corrupt stored slot — it soft-skips to `{}` and logs. |
786
+ | `INVALID_TOOL_SCHEMA` | `AgentManager.createAgent()` / `Agent.updateAgentConfig()` when a consumer-declared `AgentConfig.tools[i].inputSchema` is not a compilable JSON Schema. Fails loud at registration (before any harness work) so a schema typo doesn't surface lazily on the model's first call to that tool. |
786
787
  | `MCP_SERVER_DISABLED` | `Agent.reconnectMcpServer()` when the named server is configured with `enabled: false` |
787
788
  | `MCP_SERVER_NOT_FOUND` | `Agent.reconnectMcpServer()` when the server name is not in the agent's `mcpServers` config |
788
789
  | `MODEL_NOT_SUPPORTED_BY_HARNESS` | `AgentManager.createAgent()` / `Agent.updateAgentConfig()` (G8 pre-flight) when the resolved `ModelConnectivityInfo.providerHint` isn't in the harness's `supportedProviderHints`. Surfaces before any harness work runs (no MCP discovery, no subprocess spawn, no language-model construction) so the consumer can branch cleanly on `err.type` and recover without resource cleanup. |
@@ -1559,20 +1560,25 @@ This package publishes two ESM entry points:
1559
1560
  > see the subpath. Modern bundlers (Vite, esbuild, Webpack 5+, tsup, Rollup with `@rollup/plugin-node-resolve` v15+)
1560
1561
  > resolve it natively. This is a harness-author concern only; consumer applications never touch the subpath.
1561
1562
 
1562
- | Export | Surface | Role |
1563
- | ---------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1564
- | `HarnessFactory<H>` | Type only on bare; value+type on `/harness` | Construct a harness of type `H` bound to a storage root. Declares `harnessId` and `protocolVersion`. Default `H = AgentHarness`. |
1565
- | `AgentHarness` | Type only on bare; type on `/harness` | Runtime contract: agent / thread / stream / tool / message lifecycle. Declares its own `harnessId` and `protocolVersion`. |
1566
- | `SUPPORTED_PROTOCOL_VERSIONS` | `/harness` only | Readonly list of harness protocol versions this SDK accepts. `createAgentManager` checks both the factory and the constructed harness. |
1567
- | `HarnessBusOwner` | `/harness` only | Composition helper owning telemetry + log buses with `dispose()` semantics. Reuse it instead of reimplementing bus plumbing. |
1568
- | `lowerStreamInput` | `/harness` only | Validates a `MessagePart[]` and lowers each input part to your runtime's content-block shape. Use it in `stream()` so multimodal caps and `MULTIMODAL_NOT_SUPPORTED` / `INVALID_MESSAGE_CONTENT` semantics match every other harness. |
1569
- | `GenSink<T>` | `/harness` only | Buffered async-generator wrapper for routing `ChatEvent`s to a consumer's `ChatStreamResult.eventStream`. Single-iteration: calling `generator()` twice throws — sinks have one waiter slot and one buffer, two iterators race on both. |
1570
- | `mcpServerConfigEqual` | Bare specifier and `/harness` | Structural deep-equality predicate over `MCPServerConfig`. Use inside `updateAgent` to decide which servers to preserve vs. cycle. Treats `enabled: undefined` and `enabled: true` as equal; compares URLs via `String(url)` (so `URL` instances and strings round-trip); `headers` and `env` are key-order-insensitive; `reconnectionOptions` compares field-wise. |
1571
- | `AlwaysActiveEntry` | `/harness` only | Entry shape consumed by per-harness `toolSearch.alwaysActive` extension fields. Three matching patterns: `{ serverName }` (server-wide), `{ serverName, toolName }` (precise), `{ toolName }` (cross-source). At least one of `serverName` / `toolName` must be present. |
1572
- | `matchesAlwaysActive` | `/harness` only | Predicate `(entries, serverName, toolName) → boolean` consulted per-tool when stamping always-load metadata or partitioning a tool-search pool. Use this instead of pattern-matching entries by hand so harness behavior stays uniform. |
1573
- | `validateAlwaysActiveEntry` | `/harness` only | Throws on a malformed entry (`{}`, both fields empty). Call once per entry at the harness boundary so a typo fails loud at config time rather than silently dropping the entry on every `stream()`. |
1574
- | `splitToolResultsIntoToolMessages` | `/harness` only | Read-side normalizer `(Message[]) → Message[]`. Hoists every completed tool call's `tool-result` part onto its own `role: 'tool'` message (leaving the `tool-call` on the assistant message), so `getMessages()` returns the canonical cross-harness layout. Idempotent; backfills a blank result `toolName` from the matching call; preserves `isError` and (in-memory) `error`. Call it at the end of `getMessages()`. (Whether `error` survives to the returned history is a harness-persistence concern — see `AgentHarness.getMessages`; `isError` always survives.) |
1575
- | `mergeToolResultsIntoAssistant` | `/harness` only | Write-side inverse `(Message[]) → Message[]`. Folds each `role: 'tool'` message's result back adjacent to its `tool-call` in the preceding assistant message, so a runtime that stores a completed call as one merged object round-trips losslessly. Call it at the start of `addMessages()` before persisting. |
1563
+ | Export | Surface | Role |
1564
+ | ---------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1565
+ | `HarnessFactory<H>` | Type only on bare; value+type on `/harness` | Construct a harness of type `H` bound to a storage root. Declares `harnessId` and `protocolVersion`. Default `H = AgentHarness`. |
1566
+ | `AgentHarness` | Type only on bare; type on `/harness` | Runtime contract: agent / thread / stream / tool / message lifecycle. Declares its own `harnessId` and `protocolVersion`. |
1567
+ | `SUPPORTED_PROTOCOL_VERSIONS` | `/harness` only | Readonly list of harness protocol versions this SDK accepts. `createAgentManager` checks both the factory and the constructed harness. |
1568
+ | `HarnessBusOwner` | `/harness` only | Composition helper owning telemetry + log buses with `dispose()` semantics. Reuse it instead of reimplementing bus plumbing. |
1569
+ | `lowerStreamInput` | `/harness` only | Validates a `MessagePart[]` and lowers each input part to your runtime's content-block shape. Use it in `stream()` so multimodal caps and `MULTIMODAL_NOT_SUPPORTED` / `INVALID_MESSAGE_CONTENT` semantics match every other harness. |
1570
+ | `GenSink<T>` | `/harness` only | Buffered async-generator wrapper for routing `ChatEvent`s to a consumer's `ChatStreamResult.eventStream`. Single-iteration: calling `generator()` twice throws — sinks have one waiter slot and one buffer, two iterators race on both. |
1571
+ | `mcpServerConfigEqual` | Bare specifier and `/harness` | Structural deep-equality predicate over `MCPServerConfig`. Use inside `updateAgent` to decide which servers to preserve vs. cycle. Treats `enabled: undefined` and `enabled: true` as equal; compares URLs via `String(url)` (so `URL` instances and strings round-trip); `headers` and `env` are key-order-insensitive; `reconnectionOptions` compares field-wise. |
1572
+ | `AlwaysActiveEntry` | `/harness` only | Entry shape consumed by per-harness `toolSearch.alwaysActive` extension fields. Three matching patterns: `{ serverName }` (server-wide), `{ serverName, toolName }` (precise), `{ toolName }` (cross-source). At least one of `serverName` / `toolName` must be present. |
1573
+ | `matchesAlwaysActive` | `/harness` only | Predicate `(entries, serverName, toolName) → boolean` consulted per-tool when stamping always-load metadata or partitioning a tool-search pool. Use this instead of pattern-matching entries by hand so harness behavior stays uniform. |
1574
+ | `validateAlwaysActiveEntry` | `/harness` only | Throws on a malformed entry (`{}`, both fields empty). Call once per entry at the harness boundary so a typo fails loud at config time rather than silently dropping the entry on every `stream()`. |
1575
+ | `splitToolResultsIntoToolMessages` | `/harness` only | Read-side normalizer `(Message[]) → Message[]`. Hoists every completed tool call's `tool-result` part onto its own `role: 'tool'` message (leaving the `tool-call` on the assistant message), so `getMessages()` returns the canonical cross-harness layout. Idempotent; backfills a blank result `toolName` from the matching call; preserves `isError` and (in-memory) `error`. Call it at the end of `getMessages()`. (Whether `error` survives to the returned history is a harness-persistence concern — see `AgentHarness.getMessages`; `isError` always survives.) |
1576
+ | `mergeToolResultsIntoAssistant` | `/harness` only | Write-side inverse `(Message[]) → Message[]`. Folds each `role: 'tool'` message's result back adjacent to its `tool-call` in the preceding assistant message, so a runtime that stores a completed call as one merged object round-trips losslessly. Call it at the start of `addMessages()` before persisting. |
1577
+ | `validateToolArgs` | `/harness` only | Validates a raw tool-call argument buffer for a consumer/frontend tool (an `AgentConfig.tools` entry with no `execute`) against its declared `inputSchema`, returning a discriminated `'ok' \| 'unparseable'{reason:'not-json'\|'not-object'} \| 'schema' \| 'invalid-schema'` result. `'invalid-schema'` is a consumer misconfiguration (a schema that won't compile) the gate fails **open** on — the parsed args pass through with a loggable `schemaError`, never fed to the model. Call it before forwarding parsed args to the consumer so every harness rejects malformed/schema-violating buffers identically instead of silently collapsing them to `{}`. |
1578
+ | `validateToolSchemaCompiles` | `/harness` only | Registration-time guard: reports whether a consumer tool's declared `inputSchema` compiles, so a typo fails loud at `createAgent` / `updateAgentConfig` (via `INVALID_TOOL_SCHEMA`) rather than lazily at first call. Shares the same compile cache as `validateToolArgs`. |
1579
+ | `TOOL_ARGS_MAX_ATTEMPTS` | `/harness` only | The single shared retry cap (`3`) every harness bounds its per-tool-call consumer-args retries against, so the "will not be retried further" exhaustion wording can't drift between harnesses. |
1580
+ | `describeToolArgsFailure` | `/harness` only | Builds the one canonical model-visible message for a `validateToolArgs` failure outcome, so wording can't drift between harnesses. Pass `{ exhausted: true, maxAttempts }` on the final retry to append a "will not be retried further" clause. Throws on the non-failure outcomes (`'ok'` / `'invalid-schema'`), which are never fed to the model. |
1581
+ | `recordToolArgsFailure` | `/harness` only | Records one consumer-args failure against a caller-owned, tool-NAME-keyed attempt counter (`Map<string, number>`) and returns `{ attempt, exhausted }` (against `TOOL_ARGS_MAX_ATTEMPTS`, or a supplied override). The one shared increment-then-compare computation every harness's gate uses; storage stays caller-owned (harnesses scope the map per-turn/per-thread or per-`stream()`). Reset a name's streak with a plain `counts.delete(name)` on a non-failure outcome. |
1576
1582
 
1577
1583
  Minimal skeleton:
1578
1584
 
@@ -13,6 +13,7 @@ import { AgentSDKError, AgentSDKErrorType } from './errors.js';
13
13
  import { TelemetryRouter } from './internal/telemetry-router.js';
14
14
  import { WireCommunicationRouter } from './internal/wire-communication-router.js';
15
15
  import { AgentIdentityStore } from './internal/agent-identity-store.js';
16
+ import { assertToolSchemasCompile } from './internal/tool-schema-registration.js';
16
17
  import { createTelemetryBus } from './types/telemetry-events.js';
17
18
  import { DefaultAgentConnectivityResolver } from './agent-connectivity-resolver.js';
18
19
  /**
@@ -150,6 +151,11 @@ export class DefaultAgentManager {
150
151
  if (this.agents.has(agentId)) {
151
152
  throw new Error(`Agent with id "${agentId}" already exists`);
152
153
  }
154
+ // Fail loud at registration on a non-compilable consumer tool inputSchema
155
+ // (W-24023028 B1) — before any harness resources are allocated. NOT run on
156
+ // the boot-restore path (see the helper's doc): a bad persisted schema
157
+ // degrades to the runtime gate's fail-open instead of bricking reboot.
158
+ assertToolSchemasCompile(agentConfig);
153
159
  // installAgent validates projectRoot existence — same path as the restore loop.
154
160
  const agent = await this.installAgent(agentId, resolvedProjectRoot, agentConfig, {
155
161
  abortSignal: options?.abortSignal,
package/dist/agent.js CHANGED
@@ -7,6 +7,7 @@ import { toHarnessConfig, } from './harness/harness-config.js';
7
7
  import { normalizeMcpAuthProviders } from './mcp-auth.js';
8
8
  import { DefaultChatSession } from './chat-session.js';
9
9
  import { AgentSDKError, AgentSDKErrorType } from './errors.js';
10
+ import { assertToolSchemasCompile } from './internal/tool-schema-registration.js';
10
11
  import { createTelemetryBus } from './types/telemetry-events.js';
11
12
  /**
12
13
  * Resolves the next consumer-metadata value from the current value and an optional mutation. `replace` deep-clones the
@@ -187,6 +188,14 @@ export class DefaultAgent {
187
188
  }
188
189
  return;
189
190
  }
191
+ // Fail loud on a non-compilable consumer tool inputSchema in THIS update's
192
+ // `tools` delta (W-24023028 B1), before any harness work. Validate only the
193
+ // incoming delta — not the merged `nextConfig` — so a policy-only update
194
+ // (notably the load-bearing `approveToolCall({ remember: true })` path,
195
+ // which passes `toolPolicies` and no `tools`) never re-validates inherited
196
+ // schemas and so can never regress on a pre-guard agent whose persisted
197
+ // schema is bad (that degrades to the runtime gate's fail-open instead).
198
+ assertToolSchemasCompile(config);
190
199
  const orgAliasRequested = Object.prototype.hasOwnProperty.call(config, 'orgAlias');
191
200
  const modelIdRequested = Object.prototype.hasOwnProperty.call(config, 'modelId');
192
201
  let nextModelConnectivityInfo = previousModelConnectivityInfo;
@@ -498,13 +498,18 @@ export declare class DefaultChatSession implements ChatSession {
498
498
  */
499
499
  private effectiveInputTokens;
500
500
  /**
501
- * Compact context-window fields for the `chat-stream-completed` log record —
502
- * the fraction (rounded to a percent), the token amount that fraction is over,
503
- * and the window it is out of. Empty when the turn reported no input-side usage
504
- * (so the completion log stays unchanged pre-first-turn / post-`clearHistory()`).
505
- * Shares {@link getContextUsage}'s last-step numerator via
506
- * {@link effectiveInputTokens}, read from the same `latestUsage` snapshot
507
- * `wrapEventStream` captures.
501
+ * Last-step usage + compact context-window occupancy for the
502
+ * `chat-stream-completed` log record: the last model step's full usage
503
+ * (`lastStepUsage`), the token amount that occupies the window
504
+ * (`contextTokens` the last-step effective input), the window it is out of
505
+ * (`contextWindow`), and the occupancy ratio as a 0–1 float (`usedFraction`).
506
+ * `usedFraction` matches the SDK/service occupancy ratio exactly — the log no
507
+ * longer emits the redundant, lossy `contextUsedPercent` (a consumer wanting a
508
+ * percent computes `round(usedFraction * 100)`) (W-24125082, Phase 1). Empty
509
+ * when the turn reported no input-side usage, so the completion log stays
510
+ * unchanged pre-first-turn / post-`clearHistory()`. Shares
511
+ * {@link getContextUsage}'s last-step numerator via {@link effectiveInputTokens},
512
+ * read from the same `latestUsage` snapshot `wrapEventStream` captures.
508
513
  */
509
514
  private contextUsageLogFields;
510
515
  /**
@@ -202,22 +202,36 @@ export class DefaultChatSession {
202
202
  let sawFinish = false;
203
203
  let lastError;
204
204
  let finishUsage;
205
+ // Count model steps in the turn so the completion log's `turnUsage`
206
+ // (the whole-turn aggregate) is interpretable next to `lastStepUsage`:
207
+ // lastStepUsage.inputTokens ≤ turnUsage.inputTokens ≤ lastStepUsage.inputTokens × stepCount
208
+ // The lower bound is strict (<) only when stepCount > 1; a single-step
209
+ // turn's aggregate equals its one step. `stepCount` counts EVERY model
210
+ // step, including one whose `step-finish` reported undefined usage
211
+ // (W-22692131) — in that gap case `lastStepUsage` carries forward the last
212
+ // reporting step, so the upper bound just carries extra slack.
213
+ let stepCount = 0;
205
214
  try {
206
215
  for await (const event of stream) {
207
216
  this.chatEventBus.emit(event);
208
217
  this.deriveToolTelemetry(event);
209
218
  yield event;
210
- if (event.type === 'step-finish' && event.usage !== undefined) {
211
- // Snapshot the most recent per-step usage. Last-step semantics
212
- // (not the per-turn `finish.usage` aggregate) `finish.usage`
213
- // sums every step inside the turn and double-counts persistent
214
- // context, which is the wrong denominator for "how full is my
215
- // context". An undefined usage on this step is intentionally
216
- // ignored so the prior reading is carried forward gateway-side
217
- // gaps are rare but real (W-22692131) and clobbering with
218
- // undefined would surface as a transient hole consumers can't
219
- // distinguish from a fresh session.
220
- this.latestUsage = event.usage;
219
+ if (event.type === 'step-finish') {
220
+ // Every step-finish is one model step; count it for the log's
221
+ // `stepCount` regardless of whether this step reported usage.
222
+ stepCount++;
223
+ if (event.usage !== undefined) {
224
+ // Snapshot the most recent per-step usage. Last-step semantics
225
+ // (not the per-turn `finish.usage` aggregate)`finish.usage`
226
+ // sums every step inside the turn and double-counts persistent
227
+ // context, which is the wrong denominator for "how full is my
228
+ // context". An undefined usage on this step is intentionally
229
+ // ignored so the prior reading is carried forward — gateway-side
230
+ // gaps are rare but real (W-22692131) and clobbering with
231
+ // undefined would surface as a transient hole consumers can't
232
+ // distinguish from a fresh session.
233
+ this.latestUsage = event.usage;
234
+ }
221
235
  }
222
236
  if (event.type === 'finish') {
223
237
  sawFinish = true;
@@ -304,13 +318,27 @@ export class DefaultChatSession {
304
318
  durationMs,
305
319
  usage: finishUsage,
306
320
  }, finishedAt);
307
- // Fold the post-turn context-window occupancy onto the existing
308
- // completion log — % and token amount, so an operator can watch a
309
- // thread approach its limit (and correlate with a later
310
- // `context-window-exceeded` failure) without subscribing to
311
- // telemetry or adding a separate, noisier log line. Reuses the
312
- // `getContextUsage()` snapshot (last-step semantics), so it is
313
- // present only once a step reported usage; omitted otherwise.
321
+ // Fold the turn's usage + post-turn context-window occupancy onto the
322
+ // existing completion log so an operator can read both without
323
+ // subscribing to telemetry or adding a noisier log line. The two
324
+ // measurements carry self-describing names (W-24125082, Phase 1):
325
+ // - `turnUsage` the whole-turn aggregate (`finish.usage`), the
326
+ // billing/throughput total; sums every step and can exceed the window.
327
+ // - `lastStepUsage` + the occupancy fields the last model step, the
328
+ // right "how full is my context" reading (see contextUsageLogFields).
329
+ // - `stepCount` — makes `turnUsage` interpretable next to `lastStepUsage`.
330
+ // The ambiguous `usage` field is intentionally NOT emitted (the log is the
331
+ // free, unshipped surface — no alias needed; carrying a third duplicate
332
+ // object would only bloat every record). Both blocks preserve today's
333
+ // omit-when-absent behavior: `turnUsage`/`stepCount` are omitted before any
334
+ // step reports, and `contextUsageLogFields()` stays empty pre-first-turn /
335
+ // post-`clearHistory()`.
336
+ //
337
+ // `turnUsage` forwards `finishUsage` BY REFERENCE (unlike `lastStepUsage`,
338
+ // which spreads `latestUsage`): `finishUsage` is a turn-local value discarded
339
+ // when this method returns, so no consumer can observe mutation of it;
340
+ // `latestUsage` is retained session state a later `getContextUsage()` reads,
341
+ // so it must be copied.
314
342
  this.logBus.emitLog({
315
343
  level: 'info',
316
344
  message: 'Chat stream completed',
@@ -319,7 +347,8 @@ export class DefaultChatSession {
319
347
  agentId: this.agentId,
320
348
  threadId: this.threadId,
321
349
  durationMs,
322
- ...(finishUsage !== undefined ? { usage: finishUsage } : {}),
350
+ ...(finishUsage !== undefined ? { turnUsage: finishUsage } : {}),
351
+ ...(stepCount > 0 ? { stepCount } : {}),
323
352
  ...this.contextUsageLogFields(),
324
353
  },
325
354
  }, finishedAt);
@@ -466,13 +495,18 @@ export class DefaultChatSession {
466
495
  return (inputTokens ?? 0) + (cachedInputTokens ?? 0) + (cacheWriteInputTokens ?? 0);
467
496
  }
468
497
  /**
469
- * Compact context-window fields for the `chat-stream-completed` log record —
470
- * the fraction (rounded to a percent), the token amount that fraction is over,
471
- * and the window it is out of. Empty when the turn reported no input-side usage
472
- * (so the completion log stays unchanged pre-first-turn / post-`clearHistory()`).
473
- * Shares {@link getContextUsage}'s last-step numerator via
474
- * {@link effectiveInputTokens}, read from the same `latestUsage` snapshot
475
- * `wrapEventStream` captures.
498
+ * Last-step usage + compact context-window occupancy for the
499
+ * `chat-stream-completed` log record: the last model step's full usage
500
+ * (`lastStepUsage`), the token amount that occupies the window
501
+ * (`contextTokens` the last-step effective input), the window it is out of
502
+ * (`contextWindow`), and the occupancy ratio as a 0–1 float (`usedFraction`).
503
+ * `usedFraction` matches the SDK/service occupancy ratio exactly — the log no
504
+ * longer emits the redundant, lossy `contextUsedPercent` (a consumer wanting a
505
+ * percent computes `round(usedFraction * 100)`) (W-24125082, Phase 1). Empty
506
+ * when the turn reported no input-side usage, so the completion log stays
507
+ * unchanged pre-first-turn / post-`clearHistory()`. Shares
508
+ * {@link getContextUsage}'s last-step numerator via {@link effectiveInputTokens},
509
+ * read from the same `latestUsage` snapshot `wrapEventStream` captures.
476
510
  */
477
511
  contextUsageLogFields() {
478
512
  const contextTokens = this.effectiveInputTokens();
@@ -480,9 +514,11 @@ export class DefaultChatSession {
480
514
  return {};
481
515
  const contextWindow = this.getContextWindow();
482
516
  return {
517
+ // Spread so a later `getContextUsage()` reader can't observe log-side mutation.
518
+ lastStepUsage: { ...this.latestUsage },
483
519
  contextTokens,
484
520
  contextWindow,
485
- contextUsedPercent: Math.round(Math.min(1, Math.max(0, contextTokens / contextWindow)) * 100),
521
+ usedFraction: Math.min(1, Math.max(0, contextTokens / contextWindow)),
486
522
  };
487
523
  }
488
524
  /**
package/dist/errors.d.ts CHANGED
@@ -7,6 +7,13 @@ export declare const AgentSDKErrorType: {
7
7
  readonly INCOMPATIBLE_HARNESS: 'INCOMPATIBLE_HARNESS';
8
8
  readonly INVALID_MCP_AUTH_CONFIG: 'INVALID_MCP_AUTH_CONFIG';
9
9
  readonly INVALID_MESSAGE_CONTENT: 'INVALID_MESSAGE_CONTENT';
10
+ /**
11
+ * A consumer-declared `AgentConfig.tools[i].inputSchema` is not a compilable
12
+ * JSON Schema. Thrown at `createAgent` / `updateAgentConfig` so a schema typo
13
+ * fails loud at registration rather than surfacing lazily (and fail-open) on
14
+ * the model's first call to that tool (W-24023028 B1).
15
+ */
16
+ readonly INVALID_TOOL_SCHEMA: 'INVALID_TOOL_SCHEMA';
10
17
  readonly MCP_SERVER_DISABLED: 'MCP_SERVER_DISABLED';
11
18
  readonly MCP_SERVER_NOT_FOUND: 'MCP_SERVER_NOT_FOUND';
12
19
  readonly MODEL_NOT_SUPPORTED_BY_HARNESS: 'MODEL_NOT_SUPPORTED_BY_HARNESS';
package/dist/errors.js CHANGED
@@ -11,6 +11,13 @@ export const AgentSDKErrorType = {
11
11
  INCOMPATIBLE_HARNESS: 'INCOMPATIBLE_HARNESS',
12
12
  INVALID_MCP_AUTH_CONFIG: 'INVALID_MCP_AUTH_CONFIG',
13
13
  INVALID_MESSAGE_CONTENT: 'INVALID_MESSAGE_CONTENT',
14
+ /**
15
+ * A consumer-declared `AgentConfig.tools[i].inputSchema` is not a compilable
16
+ * JSON Schema. Thrown at `createAgent` / `updateAgentConfig` so a schema typo
17
+ * fails loud at registration rather than surfacing lazily (and fail-open) on
18
+ * the model's first call to that tool (W-24023028 B1).
19
+ */
20
+ INVALID_TOOL_SCHEMA: 'INVALID_TOOL_SCHEMA',
14
21
  MCP_SERVER_DISABLED: 'MCP_SERVER_DISABLED',
15
22
  MCP_SERVER_NOT_FOUND: 'MCP_SERVER_NOT_FOUND',
16
23
  MODEL_NOT_SUPPORTED_BY_HARNESS: 'MODEL_NOT_SUPPORTED_BY_HARNESS',
@@ -51,4 +51,5 @@ export { lowerStreamInput, type InputMessagePart } from './stream-input.js';
51
51
  export { GenSink } from './gen-sink.js';
52
52
  export { matchesAlwaysActive, validateAlwaysActiveEntry, type AlwaysActiveEntry } from './always-active.js';
53
53
  export { splitToolResultsIntoToolMessages, mergeToolResultsIntoAssistant } from './tool-message-normalizer.js';
54
+ export { validateToolArgs, validateToolSchemaCompiles, describeToolArgsFailure, recordToolArgsFailure, TOOL_ARGS_MAX_ATTEMPTS, type ValidateToolArgsResult, type ToolArgsSchemaViolation, } from './tool-args-validation.js';
54
55
  export { resolveToolDeclineModelMessage } from './tool-decline.js';
@@ -10,5 +10,6 @@ export { lowerStreamInput } from './stream-input.js';
10
10
  export { GenSink } from './gen-sink.js';
11
11
  export { matchesAlwaysActive, validateAlwaysActiveEntry } from './always-active.js';
12
12
  export { splitToolResultsIntoToolMessages, mergeToolResultsIntoAssistant } from './tool-message-normalizer.js';
13
+ export { validateToolArgs, validateToolSchemaCompiles, describeToolArgsFailure, recordToolArgsFailure, TOOL_ARGS_MAX_ATTEMPTS, } from './tool-args-validation.js';
13
14
  export { resolveToolDeclineModelMessage } from './tool-decline.js';
14
15
  //# sourceMappingURL=public.js.map
@@ -0,0 +1,121 @@
1
+ /**
2
+ * The single, shared retry cap for the consumer/frontend tool-args gate. Every
3
+ * harness bounds its per-tool-call retries against THIS constant so the
4
+ * exhaustion behavior — and the "will not be retried further after N failed
5
+ * attempts" wording {@link describeToolArgsFailure} builds — cannot drift
6
+ * between harnesses. Owning the cap here, next to the message that names it,
7
+ * keeps the two in lockstep (W-24023028 S2 — replaces the three per-harness
8
+ * literals that a future tune of one could have silently diverged).
9
+ */
10
+ export declare const TOOL_ARGS_MAX_ATTEMPTS = 3;
11
+ /**
12
+ * Records one model-facing tool-args failure against a caller-owned,
13
+ * tool-NAME-keyed attempt counter and reports whether the retry cap is now
14
+ * reached. Every harness gates its consumer/frontend tool-args retries with an
15
+ * identical increment-then-compare against {@link TOOL_ARGS_MAX_ATTEMPTS}; this
16
+ * factors out that one shared computation so the three can't drift on it, while
17
+ * the storage stays caller-owned (the Mastra harness scopes its counter
18
+ * per-`(agentId, threadId)`; Claude/OpenAI scope theirs per-`stream()` adapter
19
+ * instance — the map's lifetime is the harness's concern, not this helper's).
20
+ * Keying by tool NAME (not `toolCallId`) is deliberate: each retry mints a fresh
21
+ * id, so a stable name is the only thing a streak can accumulate against.
22
+ * Callers reset a name's streak on a non-failure outcome via a plain
23
+ * `counts.delete(name)` — a one-liner not worth wrapping.
24
+ */
25
+ export declare function recordToolArgsFailure(counts: Map<string, number>, toolName: string, maxAttempts?: number): {
26
+ attempt: number;
27
+ exhausted: boolean;
28
+ };
29
+ /**
30
+ * One AJV validation failure, reduced to just the fields a model-visible
31
+ * message needs. AJV's own `ErrorObject` never leaks past this module.
32
+ */
33
+ export type ToolArgsSchemaViolation = {
34
+ /** JSON-pointer-ish path to the offending field, e.g. `/city` or `/` (root). */
35
+ path: string;
36
+ /** AJV's human-readable message for this violation, e.g. "must be string". */
37
+ message: string;
38
+ };
39
+ /**
40
+ * Outcome of validating a raw tool-call argument buffer against a consumer
41
+ * tool's declared `inputSchema`.
42
+ *
43
+ * Two of these are model-facing failures the harness feeds back so the model
44
+ * can retry (`unparseable`, `schema`); the other two are not the model's fault
45
+ * and are never fed back (`ok` is success; `invalid-schema` is a consumer
46
+ * misconfiguration the gate fails *open* on — see {@link validateToolArgs}).
47
+ */
48
+ export type ValidateToolArgsResult = {
49
+ outcome: 'ok';
50
+ args: Record<string, unknown>;
51
+ } | {
52
+ outcome: 'unparseable';
53
+ rawBuffer: string;
54
+ reason: 'not-json' | 'not-object';
55
+ } | {
56
+ outcome: 'schema';
57
+ args: Record<string, unknown>;
58
+ violations: ToolArgsSchemaViolation[];
59
+ } | {
60
+ outcome: 'invalid-schema';
61
+ args: Record<string, unknown>;
62
+ schemaError: string;
63
+ };
64
+ /**
65
+ * Validates a raw tool-call argument buffer (the string a model emitted for a
66
+ * consumer/frontend tool call) against that tool's declared `inputSchema`.
67
+ *
68
+ * One code path handles every shape a harness can hand it:
69
+ *
70
+ * 1. `null` / `undefined` / whitespace-only buffer coerces to `'{}'` before
71
+ * parsing — the ONLY special case, and it folds into the normal path rather
72
+ * than short-circuiting, so an empty buffer against a schema with no
73
+ * required fields still validates as `ok:{}`.
74
+ * 2. `JSON.parse` failure ⇒ `unparseable` / `not-json`; a successfully-parsed
75
+ * non-object (array, string, number, `null`) ⇒ `unparseable` / `not-object`.
76
+ * Both echo the *original*, uncoerced buffer so the caller's error message
77
+ * reflects what the model actually sent, and the `reason` lets the message
78
+ * distinguish "fix your JSON syntax" from "send a JSON object".
79
+ * 3. No `schema` declared ⇒ `ok`, the parsed object unvalidated.
80
+ * 4. Schema that will not compile (a consumer typo) ⇒ `invalid-schema`, failing
81
+ * OPEN: the parsed `args` are returned so the caller can let the call
82
+ * through, and `schemaError` lets the caller log the misconfiguration. The
83
+ * model is never told to fix a schema it did not author. (Registration-time
84
+ * validation via {@link validateToolSchemaCompiles} is the loud early
85
+ * signal; this is the safe runtime fallback so a bad schema degrades one
86
+ * tool to no-validation instead of throwing mid-turn — W-24023028 B1.)
87
+ * 5. Otherwise validated against the cached compiled schema ⇒ `ok` or `schema`.
88
+ */
89
+ export declare function validateToolArgs(rawBuffer: string | null | undefined, schema: Record<string, unknown> | undefined): ValidateToolArgsResult;
90
+ /**
91
+ * Registration-time guard: reports whether a consumer tool's declared
92
+ * `inputSchema` compiles. Harnesses call this when an agent is created / updated
93
+ * so a schema typo fails loud at registration rather than lazily on the model's
94
+ * first call to that tool. Shares — and warms — the same compile cache
95
+ * {@link validateToolArgs} uses (W-24023028 B1).
96
+ */
97
+ export declare function validateToolSchemaCompiles(schema: Record<string, unknown>): {
98
+ compilable: true;
99
+ } | {
100
+ compilable: false;
101
+ error: string;
102
+ };
103
+ /**
104
+ * Builds the one canonical model-visible message per model-facing
105
+ * {@link ValidateToolArgsResult} failure kind, reused verbatim by every harness
106
+ * so the wording cannot drift between them.
107
+ *
108
+ * Only the two model-facing failures (`unparseable`, `schema`) have a message;
109
+ * `ok` and `invalid-schema` are never fed to the model, so calling this with
110
+ * either is a programmer error.
111
+ *
112
+ * `opts` pairs `exhausted` with `maxAttempts` as one optional unit (N1): the
113
+ * exhaustion clause interpolates the cap, so a caller cannot ask for the clause
114
+ * without supplying the number — the type makes an `…after undefined failed
115
+ * attempts.` message unconstructible rather than relying on every call site to
116
+ * remember to pass both.
117
+ */
118
+ export declare function describeToolArgsFailure(toolName: string, result: ValidateToolArgsResult, opts?: {
119
+ exhausted: true;
120
+ maxAttempts: number;
121
+ }): string;
@@ -0,0 +1,180 @@
1
+ /*
2
+ * Copyright 2026, Salesforce, Inc. All rights reserved.
3
+ * See LICENSE.txt for license terms.
4
+ */
5
+ import { getErrorMessage } from '@salesforce/agentic-common';
6
+ import { Ajv } from 'ajv';
7
+ /**
8
+ * The single, shared retry cap for the consumer/frontend tool-args gate. Every
9
+ * harness bounds its per-tool-call retries against THIS constant so the
10
+ * exhaustion behavior — and the "will not be retried further after N failed
11
+ * attempts" wording {@link describeToolArgsFailure} builds — cannot drift
12
+ * between harnesses. Owning the cap here, next to the message that names it,
13
+ * keeps the two in lockstep (W-24023028 S2 — replaces the three per-harness
14
+ * literals that a future tune of one could have silently diverged).
15
+ */
16
+ export const TOOL_ARGS_MAX_ATTEMPTS = 3;
17
+ /**
18
+ * Records one model-facing tool-args failure against a caller-owned,
19
+ * tool-NAME-keyed attempt counter and reports whether the retry cap is now
20
+ * reached. Every harness gates its consumer/frontend tool-args retries with an
21
+ * identical increment-then-compare against {@link TOOL_ARGS_MAX_ATTEMPTS}; this
22
+ * factors out that one shared computation so the three can't drift on it, while
23
+ * the storage stays caller-owned (the Mastra harness scopes its counter
24
+ * per-`(agentId, threadId)`; Claude/OpenAI scope theirs per-`stream()` adapter
25
+ * instance — the map's lifetime is the harness's concern, not this helper's).
26
+ * Keying by tool NAME (not `toolCallId`) is deliberate: each retry mints a fresh
27
+ * id, so a stable name is the only thing a streak can accumulate against.
28
+ * Callers reset a name's streak on a non-failure outcome via a plain
29
+ * `counts.delete(name)` — a one-liner not worth wrapping.
30
+ */
31
+ export function recordToolArgsFailure(counts, toolName, maxAttempts = TOOL_ARGS_MAX_ATTEMPTS) {
32
+ const attempt = (counts.get(toolName) ?? 0) + 1;
33
+ counts.set(toolName, attempt);
34
+ return { attempt, exhausted: attempt >= maxAttempts };
35
+ }
36
+ /**
37
+ * Compiled-validator cache keyed by schema object identity. Schema objects are
38
+ * stable for the lifetime of an `AgentConfig` until a consumer supplies a new
39
+ * one via `updateAgentConfig`, so a `WeakMap` gives free invalidation with no
40
+ * explicit clear path. A *failed* compile is cached too, so a malformed schema
41
+ * is compiled (and reported) exactly once rather than re-throwing on every
42
+ * retry (W-24023028 B1).
43
+ */
44
+ const compiledCache = new WeakMap();
45
+ function compile(schema) {
46
+ const cached = compiledCache.get(schema);
47
+ if (cached) {
48
+ return cached;
49
+ }
50
+ let result;
51
+ try {
52
+ // `strict: false` — `inputSchema` is arbitrary consumer-declared JSON
53
+ // Schema, not a dialect this SDK controls. `allErrors: true` so a
54
+ // schema failure can name every offending field, not just the first.
55
+ const validate = new Ajv({ strict: false, allErrors: true }).compile(schema);
56
+ // AJV compiles an `$async: true` schema into a PROMISE-returning validator,
57
+ // not a boolean one — and `.compile()` does NOT throw for it. Our gate calls
58
+ // `validate(args)` synchronously: a returned Promise is always truthy, so a
59
+ // `$async` schema would silently pass EVERY payload as `'ok'` (validation
60
+ // bypass), and the promise it returns rejects unawaited — crashing the host
61
+ // with an unhandled rejection on the first invalid call. Treat an async
62
+ // schema as non-compilable so it degrades through the same fail-open
63
+ // (`'invalid-schema'`) runtime path and is rejected loud at registration by
64
+ // `validateToolSchemaCompiles`, exactly like a schema that won't compile.
65
+ result = validate.$async
66
+ ? { error: 'async JSON Schema ($async: true) is not supported for tool inputSchema validation' }
67
+ : { validate };
68
+ }
69
+ catch (error) {
70
+ // AJV throws synchronously on a malformed schema even with `strict:
71
+ // false` (a bad `type`, a non-object `properties`, an unresolvable
72
+ // `$ref`, a bad `pattern`, …). Capture the reason and cache it so the
73
+ // throw happens once, not on every model retry of that tool.
74
+ result = { error: getErrorMessage(error) };
75
+ }
76
+ compiledCache.set(schema, result);
77
+ return result;
78
+ }
79
+ /**
80
+ * Validates a raw tool-call argument buffer (the string a model emitted for a
81
+ * consumer/frontend tool call) against that tool's declared `inputSchema`.
82
+ *
83
+ * One code path handles every shape a harness can hand it:
84
+ *
85
+ * 1. `null` / `undefined` / whitespace-only buffer coerces to `'{}'` before
86
+ * parsing — the ONLY special case, and it folds into the normal path rather
87
+ * than short-circuiting, so an empty buffer against a schema with no
88
+ * required fields still validates as `ok:{}`.
89
+ * 2. `JSON.parse` failure ⇒ `unparseable` / `not-json`; a successfully-parsed
90
+ * non-object (array, string, number, `null`) ⇒ `unparseable` / `not-object`.
91
+ * Both echo the *original*, uncoerced buffer so the caller's error message
92
+ * reflects what the model actually sent, and the `reason` lets the message
93
+ * distinguish "fix your JSON syntax" from "send a JSON object".
94
+ * 3. No `schema` declared ⇒ `ok`, the parsed object unvalidated.
95
+ * 4. Schema that will not compile (a consumer typo) ⇒ `invalid-schema`, failing
96
+ * OPEN: the parsed `args` are returned so the caller can let the call
97
+ * through, and `schemaError` lets the caller log the misconfiguration. The
98
+ * model is never told to fix a schema it did not author. (Registration-time
99
+ * validation via {@link validateToolSchemaCompiles} is the loud early
100
+ * signal; this is the safe runtime fallback so a bad schema degrades one
101
+ * tool to no-validation instead of throwing mid-turn — W-24023028 B1.)
102
+ * 5. Otherwise validated against the cached compiled schema ⇒ `ok` or `schema`.
103
+ */
104
+ export function validateToolArgs(rawBuffer, schema) {
105
+ const original = rawBuffer ?? '';
106
+ const buffer = original.trim() === '' ? '{}' : original;
107
+ let parsed;
108
+ try {
109
+ parsed = JSON.parse(buffer);
110
+ }
111
+ catch {
112
+ return { outcome: 'unparseable', rawBuffer: original, reason: 'not-json' };
113
+ }
114
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
115
+ return { outcome: 'unparseable', rawBuffer: original, reason: 'not-object' };
116
+ }
117
+ const args = parsed;
118
+ if (!schema) {
119
+ return { outcome: 'ok', args };
120
+ }
121
+ const compiled = compile(schema);
122
+ if ('error' in compiled) {
123
+ return { outcome: 'invalid-schema', args, schemaError: compiled.error };
124
+ }
125
+ if (compiled.validate(args)) {
126
+ return { outcome: 'ok', args };
127
+ }
128
+ const violations = (compiled.validate.errors ?? []).map((error) => ({
129
+ path: error.instancePath || '/',
130
+ message: error.message ?? 'does not match schema',
131
+ }));
132
+ return { outcome: 'schema', args, violations };
133
+ }
134
+ /**
135
+ * Registration-time guard: reports whether a consumer tool's declared
136
+ * `inputSchema` compiles. Harnesses call this when an agent is created / updated
137
+ * so a schema typo fails loud at registration rather than lazily on the model's
138
+ * first call to that tool. Shares — and warms — the same compile cache
139
+ * {@link validateToolArgs} uses (W-24023028 B1).
140
+ */
141
+ export function validateToolSchemaCompiles(schema) {
142
+ const compiled = compile(schema);
143
+ return 'error' in compiled ? { compilable: false, error: compiled.error } : { compilable: true };
144
+ }
145
+ /**
146
+ * Builds the one canonical model-visible message per model-facing
147
+ * {@link ValidateToolArgsResult} failure kind, reused verbatim by every harness
148
+ * so the wording cannot drift between them.
149
+ *
150
+ * Only the two model-facing failures (`unparseable`, `schema`) have a message;
151
+ * `ok` and `invalid-schema` are never fed to the model, so calling this with
152
+ * either is a programmer error.
153
+ *
154
+ * `opts` pairs `exhausted` with `maxAttempts` as one optional unit (N1): the
155
+ * exhaustion clause interpolates the cap, so a caller cannot ask for the clause
156
+ * without supplying the number — the type makes an `…after undefined failed
157
+ * attempts.` message unconstructible rather than relying on every call site to
158
+ * remember to pass both.
159
+ */
160
+ export function describeToolArgsFailure(toolName, result, opts) {
161
+ let message;
162
+ if (result.outcome === 'unparseable') {
163
+ message =
164
+ result.reason === 'not-object'
165
+ ? `Invalid arguments for tool "${toolName}": arguments must be a JSON object.`
166
+ : `Invalid JSON in arguments for tool "${toolName}": the arguments were not valid JSON.`;
167
+ }
168
+ else if (result.outcome === 'schema') {
169
+ const clauses = result.violations.map((v) => `at "${v.path}", ${v.message}`).join('; ');
170
+ message = `Arguments for tool "${toolName}" do not match its schema: ${clauses}`;
171
+ }
172
+ else {
173
+ throw new Error(`describeToolArgsFailure called with a non-failure outcome: "${result.outcome}"`);
174
+ }
175
+ if (opts?.exhausted) {
176
+ message += ` This tool call will not be retried further after ${opts.maxAttempts} failed attempts.`;
177
+ }
178
+ return message;
179
+ }
180
+ //# sourceMappingURL=tool-args-validation.js.map
@@ -0,0 +1,17 @@
1
+ import type { AgentConfig } from '../harness/harness-config.js';
2
+ /**
3
+ * Registration-time guard (W-24023028 B1): asserts that every consumer-declared
4
+ * `AgentConfig.tools[i].inputSchema` compiles, throwing
5
+ * `AgentSDKError(INVALID_TOOL_SCHEMA)` on the first that does not. Called at
6
+ * `createAgent` / `updateAgentConfig` so a schema typo fails loud at
7
+ * registration rather than surfacing lazily — and fail-open — on the model's
8
+ * first call to that tool. This is the cross-harness, single-site complement to
9
+ * the runtime gate's fail-open `'invalid-schema'` outcome: normally a bad schema
10
+ * is caught here; if one ever slips to runtime the gate degrades that one tool
11
+ * to no-validation instead of terminating the turn.
12
+ *
13
+ * It is deliberately NOT called on the boot-restore path — a bad schema
14
+ * persisted before this guard shipped should degrade to fail-open at runtime,
15
+ * not brick the agent on every reboot.
16
+ */
17
+ export declare function assertToolSchemasCompile(config: Pick<AgentConfig, 'tools'>): void;
@@ -0,0 +1,30 @@
1
+ /*
2
+ * Copyright 2026, Salesforce, Inc. All rights reserved.
3
+ * See LICENSE.txt for license terms.
4
+ */
5
+ import { AgentSDKError, AgentSDKErrorType } from '../errors.js';
6
+ import { validateToolSchemaCompiles } from '../harness/tool-args-validation.js';
7
+ /**
8
+ * Registration-time guard (W-24023028 B1): asserts that every consumer-declared
9
+ * `AgentConfig.tools[i].inputSchema` compiles, throwing
10
+ * `AgentSDKError(INVALID_TOOL_SCHEMA)` on the first that does not. Called at
11
+ * `createAgent` / `updateAgentConfig` so a schema typo fails loud at
12
+ * registration rather than surfacing lazily — and fail-open — on the model's
13
+ * first call to that tool. This is the cross-harness, single-site complement to
14
+ * the runtime gate's fail-open `'invalid-schema'` outcome: normally a bad schema
15
+ * is caught here; if one ever slips to runtime the gate degrades that one tool
16
+ * to no-validation instead of terminating the turn.
17
+ *
18
+ * It is deliberately NOT called on the boot-restore path — a bad schema
19
+ * persisted before this guard shipped should degrade to fail-open at runtime,
20
+ * not brick the agent on every reboot.
21
+ */
22
+ export function assertToolSchemasCompile(config) {
23
+ for (const tool of config.tools ?? []) {
24
+ const result = validateToolSchemaCompiles(tool.inputSchema);
25
+ if (!result.compilable) {
26
+ throw new AgentSDKError(`Tool "${tool.name}" declares an inputSchema that does not compile: ${result.error}`, AgentSDKErrorType.INVALID_TOOL_SCHEMA);
27
+ }
28
+ }
29
+ }
30
+ //# sourceMappingURL=tool-schema-registration.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/sfdx-agent-sdk",
3
- "version": "0.72.0",
3
+ "version": "0.74.0",
4
4
  "description": "Harness-agnostic agentic infrastructure for Salesforce developer experience tooling",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -43,13 +43,14 @@
43
43
  "LICENSE.txt"
44
44
  ],
45
45
  "dependencies": {
46
- "@salesforce/agentic-common": "0.19.0"
46
+ "@salesforce/agentic-common": "0.19.0",
47
+ "ajv": "^8.20.0"
47
48
  },
48
49
  "devDependencies": {
49
50
  "@eslint/js": "^10.0.1",
50
- "@salesforce/sfdx-agent-harness-claude": "0.68.0",
51
- "@salesforce/sfdx-agent-harness-mastra": "0.71.0",
52
- "@salesforce/sfdx-agent-harness-openai": "0.37.0",
51
+ "@salesforce/sfdx-agent-harness-claude": "0.70.0",
52
+ "@salesforce/sfdx-agent-harness-mastra": "0.73.0",
53
+ "@salesforce/sfdx-agent-harness-openai": "0.39.0",
53
54
  "@types/node": "^22.20.1",
54
55
  "@vitest/coverage-istanbul": "^4.1.11",
55
56
  "@vitest/eslint-plugin": "^1.6.27",
@@ -59,12 +60,12 @@
59
60
  "eslint-plugin-import": "^2.32.0",
60
61
  "eslint-plugin-n": "^18.3.0",
61
62
  "globals": "^17.12.0",
62
- "lint-staged": "^17.3.0",
63
+ "lint-staged": "^17.4.1",
63
64
  "prettier": "^3.9.6",
64
65
  "rimraf": "^6.1.3",
65
- "tsx": "^4.23.12",
66
+ "tsx": "^4.23.13",
66
67
  "typescript": "^7.0.2",
67
- "typescript-eslint": "^8.68.0",
68
+ "typescript-eslint": "^8.69.0",
68
69
  "vitest": "^4.1.11"
69
70
  },
70
71
  "engines": {