@salesforce/sfdx-agent-sdk 0.39.0 → 0.41.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-sdk` are documented in this file.
4
4
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
+ ## [0.41.0] - 2026-08-03
7
+
8
+ ### Features
9
+ - **agent-sdk,harnesses**: emit Tier-1 logBus siblings for lifecycle telemetry @W-23547150@ ([#716](https://github.com/forcedotcom/agentic-dx/pull/716))
10
+
11
+ ## [0.40.0] - 2026-08-03
12
+
13
+ ### Features
14
+ - **agent-sdk,harness-mastra,harness-claude,harness-openai**: observability gaps — mcp-tool-call-completed, llm-retry, responseText/TTFT @W-23528008@ ([#717](https://github.com/forcedotcom/agentic-dx/pull/717))
15
+
6
16
  ## [0.39.0] - 2026-07-30
7
17
 
8
18
  ### Features
package/README.md CHANGED
@@ -1192,6 +1192,9 @@ const unsubscribe = manager.onWireCommunication((event) => {
1192
1192
  case 'llm-response':
1193
1193
  // matching response (or terminal failure)
1194
1194
  break;
1195
+ case 'mcp-tool-call-completed':
1196
+ // completed MCP tool call; redact args/result before persisting
1197
+ break;
1195
1198
  case 'wire-monitoring-not-supported':
1196
1199
  // harness can't emit per-call events for this stream;
1197
1200
  // event.message points at the workaround (typically a debug log file path)
@@ -1200,17 +1203,28 @@ const unsubscribe = manager.onWireCommunication((event) => {
1200
1203
  });
1201
1204
  ```
1202
1205
 
1203
- **Discriminated union — three variants keyed on `type`:**
1206
+ **Discriminated union — four variants keyed on `type`:**
1207
+
1208
+ | `type` | Fields |
1209
+ | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1210
+ | `llm-request` | `timestamp`, `url`, `method`, `model`, `correlationId?`, `xClientTraceId?`, `body?` |
1211
+ | `llm-response` | `timestamp`, `model`, `correlationId?`, `status`, `xClientTraceId?`, `totalDurationMs?`, `timeToFirstTokenMs?`, `usage?` (`inputTokens?`, `outputTokens?`, `totalTokens?`, `reasoningTokens?`), `responseText?`, `error?` ({ `message` }) |
1212
+ | `mcp-tool-call-completed` | `timestamp`, `serverName`, `toolName`, `bareToolName`, `args`, `result`, `durationMs?` |
1213
+ | `wire-monitoring-not-supported` | `timestamp`, `harnessId`, `message` (free-form text — typically explains why direct wire monitoring isn't available and points at a fallback log file the harness wrote) |
1204
1214
 
1205
- | `type` | Fields |
1206
- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1207
- | `llm-request` | `timestamp`, `url`, `method`, `model`, `body?` |
1208
- | `llm-response` | `timestamp`, `model`, `status`, `xClientTraceId?`, `totalDurationMs?`, `timeToFirstTokenMs?`, `usage?` (`inputTokens?`, `outputTokens?`, `totalTokens?`, `reasoningTokens?`), `responseText?`, `error?` ({ `message` }) |
1209
- | `wire-monitoring-not-supported` | `timestamp`, `harnessId`, `message` (free-form text — typically explains why direct wire monitoring isn't available and points at a fallback log file the harness wrote) |
1215
+ `correlationId` is the harness-guaranteed pairing key stamped identically on a matching `llm-request` / `llm-response`
1216
+ pair. Pair requests and responses on it, **not** on their positions in the event stream. `xClientTraceId` is an optional
1217
+ header value for cross-checking, not the pairing contract.
1210
1218
 
1211
- **Privacy contract.** Events MAY contain user prompts, tool arguments, and model output in the `body` field. They are
1212
- **not** flowed to the structured-log channel by default consumers opt in to wire-level visibility explicitly via
1213
- `onWireCommunication`. Apply your own redaction / scrubbing before persisting or sharing. Note that request HTTP headers
1219
+ `mcp-tool-call-completed` is a **DEBUG-ONLY** diagnostic event: it is opt-in and subscriber-gated like the rest of this
1220
+ channel, is never a `TelemetryEvent`, and is never shipped through telemetry or other externally-shipped event surfaces.
1221
+ Its `args` and `result` are captured verbatim and MAY contain PII. Apply your own redaction / scrubbing before
1222
+ persisting or sharing. `durationMs` is harness-specific: Claude reports RPC latency, while Mastra and OpenAI report an
1223
+ adapter-observed window. Do not compare `durationMs` across harnesses.
1224
+
1225
+ **Privacy contract.** LLM events MAY contain user prompts and model output in `body` / `responseText`; MCP completion
1226
+ events MAY contain PII in verbatim `args` / `result`. These events are **not** flowed to the structured-log channel by
1227
+ default — consumers opt in to wire-level visibility explicitly via `onWireCommunication`. Note that request HTTP headers
1214
1228
  (e.g. `Authorization`) are **not** captured on the events; the harness's fetch wrapper sees them but doesn't forward
1215
1229
  them onto the channel.
1216
1230
 
@@ -1221,17 +1235,26 @@ harness in particular skips writing the subprocess's debug log when nobody is li
1221
1235
  `chat()` you want to inspect: subscriptions added mid-stream miss the in-flight subprocess on Claude (the listener count
1222
1236
  is captured at stream-start).
1223
1237
 
1224
- **Cross-harness coverage.**
1238
+ #### Cross-harness coverage
1239
+
1240
+ This table is the canonical compatibility reference for the SDK's public observability and wire-diagnostic surfaces.
1241
+ Feature availability is intentionally asymmetric where a runtime does not expose a faithful observation seam.
1225
1242
 
1226
- - **Mastra harness**: emits one `llm-request` + one `llm-response` per outbound HTTP call to the LLM provider (every
1227
- provider Bedrock-Anthropic, direct Anthropic, OpenAI Responses, OpenAI Chat Completions). The body is captured from
1228
- the in-process fetch wrapper.
1229
- - **Claude harness**: cannot emit per-call events because the Claude Agent SDK runs the model client in a subprocess
1230
- with no programmatic HTTP-interception seam. Instead, when at least one consumer is subscribed at stream-start, the
1231
- harness writes the subprocess's debug log to `${storageRoot}/claude-debug-logs/${agentId}__${threadId}.log` (one file
1232
- per thread; turns of the same conversation accumulate, separated by greppable `# Turn started: <ISO>` banners) and
1233
- emits a single `wire-monitoring-not-supported` event whose `message` field embeds the file path. The file is left in
1234
- place; the consumer is responsible for retention and cleanup.
1243
+ | Feature | Claude | Mastra | OpenAI |
1244
+ | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1245
+ | Structured `llm-request` / `llm-response` wire events | **Not implemented.** The opaque native subprocess provides no capturable, parseable per-call format. When subscribed, emits `wire-monitoring-not-supported` and writes a subprocess debug-log file. | **Delivered.** Emits one request/response pair per outbound LLM HTTP call. | **Delivered.** Emits one request/response pair per outbound LLM HTTP call. |
1246
+ | Pairing structured LLM events | N/A — no structured LLM wire events. | **Delivered.** Matching request/response events share a harness-generated `correlationId`; pair on it, never event position or `xClientTraceId` alone. | **Delivered.** Matching request/response events share a harness-generated `correlationId`; pair on it, never event position or `xClientTraceId` alone. |
1247
+ | `responseText` and `timeToFirstTokenMs` on `llm-response` | N/A — no structured LLM wire events. | **Delivered.** A streaming-body tee produces one HTTP-body-terminal response event with best-effort response text and first-token latency. | **Delivered.** A streaming-body tee produces one HTTP-body-terminal response event with best-effort response text and first-token latency. |
1248
+ | `mcp-tool-call-completed` debug event | **Delivered.** External-MCP bridge observation with true RPC latency in `durationMs`. | **Delivered.** Adapter/coordinator-observed `durationMs`, which can include a model round-trip. | **Delivered.** Adapter/coordinator-observed `durationMs`, which can include a model round-trip. |
1249
+ | `llm-retry` telemetry | **Delivered.** Emits numeric-only, PII-free data when the Claude Agent SDK reports an actual `api_retry`. | **Documented limitation:** does not emit; the stateless fetch layer has no retry loop or attempt counter. A `429` / `retry-after` response is a failed attempt, not a retry. | **Documented limitation:** does not emit; the stateless fetch layer has no retry loop or attempt counter. A `429` / `retry-after` response is a failed attempt, not a retry. |
1250
+
1251
+ Claude's debug log is written to `${storageRoot}/claude-debug-logs/${agentId}__${threadId}.log` when at least one
1252
+ consumer is subscribed at stream-start (one file per thread; turns of the same conversation accumulate, separated by
1253
+ greppable `# Turn started: <ISO>` banners). It is left in place; the consumer is responsible for retention and cleanup.
1254
+
1255
+ Mastra and OpenAI callers can identify any upstream-retried requests only as distinct `llm-request` / `llm-response`
1256
+ pairs, using `correlationId`, `xClientTraceId`, and timestamps. Neither harness synthesizes a partial `llm-retry` event
1257
+ from an HTTP response header.
1235
1258
 
1236
1259
  #### `WireCommunicationFileWriter`
1237
1260
 
@@ -1253,11 +1276,11 @@ const writer = new WireCommunicationFileWriter(manager, {
1253
1276
  writer.detach();
1254
1277
  ```
1255
1278
 
1256
- The output file is plain Markdown with one `### <iso-timestamp> [llm-request]`, `[llm-response]`, or
1257
- `[wire-monitoring-not-supported]` header per event followed by the populated fields. Optional fields are emitted only
1258
- when set — sparse responses do not pad the file with empty `**Field**: N/A` lines. On the Claude harness, the
1259
- `wire-monitoring-not-supported` block links to the per-thread debug log file where raw wire detail can be inspected
1260
- directly.
1279
+ The output file is plain Markdown with one `### <iso-timestamp> [llm-request]`, `[llm-response]`,
1280
+ `[mcp-tool-call-completed]`, or `[wire-monitoring-not-supported]` header per event followed by the populated fields.
1281
+ Optional fields are emitted only when set — sparse responses do not pad the file with empty `**Field**: N/A` lines. On
1282
+ the Claude harness, the `wire-monitoring-not-supported` block links to the per-thread debug log file where raw wire
1283
+ detail can be inspected directly.
1261
1284
 
1262
1285
  The same privacy contract applies: the resulting file may contain prompts, tool arguments, and model output. Treat it as
1263
1286
  sensitive and apply your own retention / scrubbing rules before sharing.
@@ -1491,6 +1514,7 @@ All variants share `{ type: <discriminant>, timestamp: Date }` plus the fields b
1491
1514
  | `mcp-server-discovery-completed` | `agentId`, `serverName`, `toolCount`, `durationMs` |
1492
1515
  | `mcp-server-discovery-failed` | `agentId`, `serverName`, `durationMs`, `error`, `errorDetail?` ([`McpServerErrorDetail`](#mcpservererrordetail)) |
1493
1516
  | `mcp-server-status-changed` | `agentId`, `serverName`, `previousStatus`, `nextStatus`, `error?` |
1517
+ | `llm-retry` | `attempt`, `maxRetries`, `retryDelayMs`, `errorStatus?` — Claude-only; numeric-only and PII-free |
1494
1518
 
1495
1519
  Each variant is also exported as a named type so consumers can write narrowed handlers: `AgentCreatedEvent`,
1496
1520
  `AgentDestroyedEvent`, `SessionCreatedEvent`, `SessionDestroyedEvent`, `ChatStreamStartedEvent`,
@@ -1522,6 +1546,13 @@ Abandoning the iterator mid-stream emits no terminal telemetry — the stream di
1522
1546
  Harness implementations may emit additional structured logs through their own `LogBus`. Those records surface to
1523
1547
  consumers via the same `onLog` channel.
1524
1548
 
1549
+ Selected lifecycle moments are reported on **both** channels: the typed telemetry event, plus a log record carrying the
1550
+ same facts for log sinks. On those records `message` is human-readable prose intended for display and may be reworded;
1551
+ `context.event_type` carries the matching `TelemetryEvent` type and is the stable field to filter, alert, and join on.
1552
+ Records paired this way today are `agent-created`, `agent-destroyed`, `session-created`, `session-destroyed`,
1553
+ `chat-stream-completed`, `chat-stream-error`, `tool-execution-completed` (only when `isError` is true), and the harness
1554
+ MCP events `mcp-server-discovery-completed`, `mcp-server-discovery-failed`, and `mcp-server-status-changed`.
1555
+
1525
1556
  ## Development
1526
1557
 
1527
1558
  See [DEVELOPING.md](DEVELOPING.md) for build-from-source setup, scripts, E2E testing, and packaging.
@@ -189,12 +189,20 @@ export class DefaultAgentManager {
189
189
  this.wireRouter.registerAgent(agentId);
190
190
  const agent = new DefaultAgent(this.harness, agentId, projectRoot, config, runtime.modelConnectivityInfo, runtime.orgConnection, runtime.orgJwt, this.agentConnectivityResolver, this.harnessSupportedProviderHints, this.hooksForAgent, this.identityStore, this.router, agentSlice, { telemetry: this.telemetryBus, log: this.logBus }, this.clock, this.agentIdGenerator);
191
191
  this.agents.set(agentId, agent);
192
+ const agentCreatedAt = this.clock.now();
193
+ const modelName = runtime.modelConnectivityInfo.model.name;
192
194
  this.telemetryBus.emit({
193
195
  type: 'agent-created',
194
- timestamp: this.clock.now(),
196
+ timestamp: agentCreatedAt,
195
197
  agentId,
196
198
  projectRoot,
197
- modelName: runtime.modelConnectivityInfo.model.name,
199
+ modelName,
200
+ });
201
+ this.logBus.emit({
202
+ level: 'info',
203
+ message: 'Agent created',
204
+ timestamp: agentCreatedAt,
205
+ context: { event_type: 'agent-created', agentId, projectRoot, modelName },
198
206
  });
199
207
  if (options.rehydrateThreads) {
200
208
  // If thread enumeration or session attachment fails, the restore is a full failure
package/dist/agent.js CHANGED
@@ -283,11 +283,18 @@ export class DefaultAgent {
283
283
  }
284
284
  this.sessions.clear();
285
285
  await this.harness.destroyAgent(this.agentId);
286
+ const agentDestroyedAt = this.clock.now();
286
287
  this.telemetryBus.emit({
287
288
  type: 'agent-destroyed',
288
- timestamp: this.clock.now(),
289
+ timestamp: agentDestroyedAt,
289
290
  agentId: this.agentId,
290
291
  });
292
+ this.logBus.emit({
293
+ level: 'info',
294
+ message: 'Agent destroyed',
295
+ timestamp: agentDestroyedAt,
296
+ context: { event_type: 'agent-destroyed', agentId: this.agentId },
297
+ });
291
298
  for (const unsub of this.inboundUnsubs)
292
299
  unsub();
293
300
  for (const unsub of this.parentUnsubs)
@@ -347,12 +354,19 @@ export class DefaultAgent {
347
354
  }, getContextWindow, { clock: this.clock, idGenerator: this.idGenerator, persistRememberedRule });
348
355
  this.sessions.set(threadId, session);
349
356
  this.sessionSliceUnregisters.set(threadId, () => this.router.unregisterSession(threadId));
357
+ const sessionCreatedAt = this.clock.now();
350
358
  this.telemetryBus.emit({
351
359
  type: 'session-created',
352
- timestamp: this.clock.now(),
360
+ timestamp: sessionCreatedAt,
353
361
  agentId: this.agentId,
354
362
  threadId,
355
363
  });
364
+ this.logBus.emit({
365
+ level: 'info',
366
+ message: 'Chat session created',
367
+ timestamp: sessionCreatedAt,
368
+ context: { event_type: 'session-created', agentId: this.agentId, threadId },
369
+ });
356
370
  return session;
357
371
  }
358
372
  detachSession(threadId, session) {
@@ -237,6 +237,18 @@ export class DefaultChatSession {
237
237
  durationMs,
238
238
  error: lastError,
239
239
  });
240
+ this.logBus.emit({
241
+ level: 'error',
242
+ message: 'Chat stream failed',
243
+ timestamp: finishedAt,
244
+ context: {
245
+ event_type: 'chat-stream-error',
246
+ agentId: this.agentId,
247
+ threadId: this.threadId,
248
+ durationMs,
249
+ },
250
+ error: lastError,
251
+ });
240
252
  }
241
253
  else {
242
254
  this.telemetryBus.emit({
@@ -247,6 +259,18 @@ export class DefaultChatSession {
247
259
  durationMs,
248
260
  usage: finishUsage,
249
261
  });
262
+ this.logBus.emit({
263
+ level: 'info',
264
+ message: 'Chat stream completed',
265
+ timestamp: finishedAt,
266
+ context: {
267
+ event_type: 'chat-stream-completed',
268
+ agentId: this.agentId,
269
+ threadId: this.threadId,
270
+ durationMs,
271
+ ...(finishUsage !== undefined ? { usage: finishUsage } : {}),
272
+ },
273
+ });
250
274
  }
251
275
  }
252
276
  /**
@@ -420,12 +444,23 @@ export class DefaultChatSession {
420
444
  if (this.disposed) {
421
445
  return;
422
446
  }
447
+ const sessionDestroyedAt = this.clock.now();
423
448
  this.telemetryBus.emit({
424
449
  type: 'session-destroyed',
425
- timestamp: this.clock.now(),
450
+ timestamp: sessionDestroyedAt,
426
451
  agentId: this.agentId,
427
452
  threadId: this.threadId,
428
453
  });
454
+ this.logBus.emit({
455
+ level: 'info',
456
+ message: 'Chat session destroyed',
457
+ timestamp: sessionDestroyedAt,
458
+ context: {
459
+ event_type: 'session-destroyed',
460
+ agentId: this.agentId,
461
+ threadId: this.threadId,
462
+ },
463
+ });
429
464
  for (const unsub of this.inboundUnsubs)
430
465
  unsub();
431
466
  for (const unsub of this.parentUnsubs)
@@ -436,6 +471,8 @@ export class DefaultChatSession {
436
471
  this.disposed = true;
437
472
  }
438
473
  emitToolApprovalResolved(toolCallId, approved, policyWritten) {
474
+ // No Tier-1 logBus sibling: tool-approval-resolved fires per gated
475
+ // tool call — interactive-UI signal, not on-call triage
439
476
  this.telemetryBus.emit({
440
477
  type: 'tool-approval-resolved',
441
478
  timestamp: this.clock.now(),
@@ -476,6 +513,8 @@ export class DefaultChatSession {
476
513
  deriveToolTelemetry(event) {
477
514
  if (event.type === 'tool-call') {
478
515
  this.toolStartMs.set(event.toolCallId, this.clock.now().getTime());
516
+ // No Tier-1 logBus sibling: tool-execution-started pairs 1:1 with
517
+ // tool-execution-completed; the completion (isError=true) is the triage signal
479
518
  this.telemetryBus.emit({
480
519
  type: 'tool-execution-started',
481
520
  timestamp: this.clock.now(),
@@ -492,19 +531,42 @@ export class DefaultChatSession {
492
531
  if (start === undefined)
493
532
  return;
494
533
  this.toolStartMs.delete(event.toolCallId);
534
+ const completedAt = this.clock.now();
535
+ const durationMs = completedAt.getTime() - start;
536
+ const isError = event.isError === true;
495
537
  this.telemetryBus.emit({
496
538
  type: 'tool-execution-completed',
497
- timestamp: this.clock.now(),
539
+ timestamp: completedAt,
498
540
  agentId: this.agentId,
499
541
  threadId: this.threadId,
500
542
  toolCallId: event.toolCallId,
501
543
  toolName: event.toolName,
502
- durationMs: this.clock.now().getTime() - start,
503
- isError: event.isError === true,
544
+ durationMs,
545
+ isError,
504
546
  ...(event.error ? { error: event.error } : {}),
505
547
  ...(event.annotations ? { annotations: event.annotations } : {}),
506
548
  ...(event.serverName ? { serverName: event.serverName } : {}),
507
549
  });
550
+ // Tier-1 logBus sibling only when isError=true.
551
+ if (isError) {
552
+ this.logBus.emit({
553
+ level: 'error',
554
+ message: 'Tool execution failed',
555
+ timestamp: completedAt,
556
+ context: {
557
+ event_type: 'tool-execution-completed',
558
+ agentId: this.agentId,
559
+ threadId: this.threadId,
560
+ toolCallId: event.toolCallId,
561
+ toolName: event.toolName,
562
+ durationMs,
563
+ isError: true,
564
+ ...(event.annotations ? { annotations: event.annotations } : {}),
565
+ ...(event.serverName ? { serverName: event.serverName } : {}),
566
+ },
567
+ ...(event.error ? { error: event.error } : {}),
568
+ });
569
+ }
508
570
  }
509
571
  else if (event.type === 'tool-approval-request') {
510
572
  // Record the (toolName, serverName?) so a later settle with
@@ -574,6 +636,7 @@ export class DefaultChatSession {
574
636
  */
575
637
  emitChatStreamStarted(trigger) {
576
638
  const startedAt = this.clock.now();
639
+ // chat-stream-started pairs 1:1 with chat-stream-completed, which already carries outcomes
577
640
  this.telemetryBus.emit({
578
641
  type: 'chat-stream-started',
579
642
  timestamp: startedAt,
@@ -597,12 +660,25 @@ export class DefaultChatSession {
597
660
  this.chatEventBus.emit({ type: 'error', error });
598
661
  this.chatEventBus.emit({ type: 'finish', finishReason: 'error' });
599
662
  const finishedAt = this.clock.now();
663
+ const durationMs = finishedAt.getTime() - startedAt.getTime();
600
664
  this.telemetryBus.emit({
601
665
  type: 'chat-stream-error',
602
666
  timestamp: finishedAt,
603
667
  agentId: this.agentId,
604
668
  threadId: this.threadId,
605
- durationMs: finishedAt.getTime() - startedAt.getTime(),
669
+ durationMs,
670
+ error,
671
+ });
672
+ this.logBus.emit({
673
+ level: 'error',
674
+ message: 'Chat stream failed',
675
+ timestamp: finishedAt,
676
+ context: {
677
+ event_type: 'chat-stream-error',
678
+ agentId: this.agentId,
679
+ threadId: this.threadId,
680
+ durationMs,
681
+ },
606
682
  error,
607
683
  });
608
684
  }
@@ -100,11 +100,14 @@ export interface AgentHarness {
100
100
  *
101
101
  * Fidelity may differ between harnesses: the Mastra (in-process) path
102
102
  * emits one `request` + one `response` event per outbound HTTP call,
103
- * with the full body and status visible. The Claude (subprocess) path
104
- * has the seam wired but does not emit yet — emission would parse
105
- * `ANTHROPIC_LOG=debug` stderr from the CLI. Consumers subscribe once
106
- * at the `AgentManager` layer; the SDK forwards events upward through
107
- * the bus hierarchy the same way it does for telemetry and log records.
103
+ * with the full body and status visible. The Claude path runs the model
104
+ * client in an opaque native subprocess with no capturable, parseable
105
+ * per-call debug format, so it emits `wire-monitoring-not-supported`
106
+ * pointing at the subprocess debug-log file rather than structured
107
+ * `llm-request` / `llm-response` events. Structured parsing is deferred
108
+ * pending a stable, capturable debug format. Consumers subscribe once at
109
+ * the `AgentManager` layer; the SDK forwards events upward through the
110
+ * bus hierarchy the same way it does for telemetry and log records.
108
111
  */
109
112
  onWireCommunication(callback: WireCommunicationEventCallback): Unsubscribe;
110
113
  /**
package/dist/index.d.ts CHANGED
@@ -19,11 +19,11 @@ export { type ChatSession, type ChatOptions } from './chat-session.js';
19
19
  export type { AgentConnectivityResolver, ResolvedConnectivity } from './agent-connectivity-resolver.js';
20
20
  export { ApiKeyConnectivityResolver, type ApiKeyConnectivityResolverConfig } from './api-key-connectivity-resolver.js';
21
21
  export type { ModelConnectivityInfo, ProviderHint } from './types/model-connectivity-info.js';
22
- export type { LlmRequestEvent, LlmResponseEvent, WireCommunicationEvent, WireCommunicationEventCallback, WireMonitoringNotSupportedEvent, } from './types/wire-communication-event.js';
22
+ export type { LlmRequestEvent, LlmResponseEvent, McpToolCallCompletedEvent, WireCommunicationEvent, WireCommunicationEventCallback, WireMonitoringNotSupportedEvent, } from './types/wire-communication-event.js';
23
23
  export { WireCommunicationFileWriter, type WireCommunicationEmitter, type WireCommunicationFileWriterOptions, } from './wire-communication-file-writer.js';
24
24
  export type { AgentHarness, HarnessFactory, WithAgentConfig, ConfigOf } from './harness/index.js';
25
25
  export { AgentSDKError, AgentSDKErrorType } from './errors.js';
26
- export type { AgentCreatedEvent, AgentDestroyedEvent, ChatStreamCompletedEvent, ChatStreamErrorEvent, ChatStreamStartedEvent, ChatStreamTrigger, McpServerDiscoveryCompletedEvent, McpServerDiscoveryFailedEvent, McpServerDiscoveryStartedEvent, McpServerStatusChangedEvent, SessionCreatedEvent, SessionDestroyedEvent, TelemetryEvent, TelemetryEventCallback, ToolApprovalPolicyResolvedEvent, ToolApprovalRequestedEvent, ToolApprovalResolvedEvent, ToolExecutionCompletedEvent, ToolExecutionStartedEvent, } from './types/telemetry-events.js';
26
+ export type { AgentCreatedEvent, AgentDestroyedEvent, ChatStreamCompletedEvent, ChatStreamErrorEvent, ChatStreamStartedEvent, ChatStreamTrigger, LlmRetryEvent, McpServerDiscoveryCompletedEvent, McpServerDiscoveryFailedEvent, McpServerDiscoveryStartedEvent, McpServerStatusChangedEvent, SessionCreatedEvent, SessionDestroyedEvent, TelemetryEvent, TelemetryEventCallback, ToolApprovalPolicyResolvedEvent, ToolApprovalRequestedEvent, ToolApprovalResolvedEvent, ToolExecutionCompletedEvent, ToolExecutionStartedEvent, } from './types/telemetry-events.js';
27
27
  export type { LogLevel, LogRecord, Unsubscribe } from '@salesforce/agentic-common';
28
28
  export { resolveMcpServerHeaders } from './mcp-auth.js';
29
29
  export type { OrgConnection, OrgConnectionFactory } from '@salesforce/agentic-common';
@@ -91,10 +91,12 @@ export class TelemetryRouter {
91
91
  return;
92
92
  }
93
93
  }
94
- const agentSlice = this.agentSlices.get(event.agentId);
95
- if (agentSlice) {
96
- agentSlice.telemetry.emit(event);
97
- return;
94
+ if ('agentId' in event) {
95
+ const agentSlice = this.agentSlices.get(event.agentId);
96
+ if (agentSlice) {
97
+ agentSlice.telemetry.emit(event);
98
+ return;
99
+ }
98
100
  }
99
101
  this.unroutedSlice.telemetry.emit(event);
100
102
  }
@@ -178,7 +178,21 @@ export type McpServerStatusChangedEvent = Base<'mcp-server-status-changed'> & {
178
178
  /** Populated when `nextStatus === 'error'`. */
179
179
  error?: string;
180
180
  };
181
- export type TelemetryEvent = AgentCreatedEvent | AgentDestroyedEvent | SessionCreatedEvent | SessionDestroyedEvent | ChatStreamStartedEvent | ChatStreamCompletedEvent | ChatStreamErrorEvent | ToolExecutionStartedEvent | ToolExecutionCompletedEvent | ToolApprovalRequestedEvent | ToolApprovalResolvedEvent | ToolApprovalPolicyResolvedEvent | McpServerDiscoveryStartedEvent | McpServerDiscoveryCompletedEvent | McpServerDiscoveryFailedEvent | McpServerStatusChangedEvent;
181
+ /**
182
+ * Emitted for each retry attempt initiated by the Claude harness.
183
+ *
184
+ * This event is Claude-only by contract: Mastra and OpenAI expose no observable
185
+ * attempt counter. The type has no harness discriminant, but only the Claude
186
+ * harness emits it. Its required retry fields intentionally remain complete so
187
+ * consumers never receive a partial retry record.
188
+ */
189
+ export type LlmRetryEvent = Base<'llm-retry'> & {
190
+ attempt: number;
191
+ maxRetries: number;
192
+ retryDelayMs: number;
193
+ errorStatus?: number;
194
+ };
195
+ export type TelemetryEvent = AgentCreatedEvent | AgentDestroyedEvent | SessionCreatedEvent | SessionDestroyedEvent | ChatStreamStartedEvent | ChatStreamCompletedEvent | ChatStreamErrorEvent | ToolExecutionStartedEvent | ToolExecutionCompletedEvent | ToolApprovalRequestedEvent | ToolApprovalResolvedEvent | ToolApprovalPolicyResolvedEvent | McpServerDiscoveryStartedEvent | McpServerDiscoveryCompletedEvent | McpServerDiscoveryFailedEvent | McpServerStatusChangedEvent | LlmRetryEvent;
182
196
  export type TelemetryEventCallback = (event: TelemetryEvent) => void;
183
197
  export type TelemetryBus = EventBus<TelemetryEvent>;
184
198
  export {};
@@ -1,7 +1,7 @@
1
1
  import type { UsageMetadata } from './usage.js';
2
2
  /**
3
3
  * Wire-level communication events, emitted by the harness layer so consumers
4
- * see the same shape regardless of harness. The union has three members today:
4
+ * see the same shape regardless of harness. The union has four members today:
5
5
  * - `LlmRequestEvent` (`type: 'llm-request'`) — one outbound HTTP request to
6
6
  * the LLM provider.
7
7
  * - `LlmResponseEvent` (`type: 'llm-response'`) — the matching inbound
@@ -12,6 +12,8 @@ import type { UsageMetadata } from './usage.js';
12
12
  * subprocess owns the HTTP traffic). The event carries a free-form
13
13
  * `message` explaining the situation and (when applicable) where the
14
14
  * underlying data was written instead.
15
+ * - `McpToolCallCompletedEvent` (`type: 'mcp-tool-call-completed'`) — an
16
+ * opt-in debug record of an MCP tool call's completed input and output.
15
17
  *
16
18
  * **Privacy contract.** Wire-communication events may contain user prompts,
17
19
  * tool arguments, model output, and other PII. They are opt-in (subscribed
@@ -55,6 +57,18 @@ export type LlmRequestEvent = {
55
57
  method: string;
56
58
  /** Native model id sent on the wire (per `ModelConnectivityInfo.nativeModelId`). */
57
59
  model: string;
60
+ /**
61
+ * Harness-generated per-fetch pairing key. Stamped identically on the matching
62
+ * `LlmResponseEvent`; consumers MUST use it as the reliable pairing key rather
63
+ * than positional event ordering. Producers obtain it from the injected
64
+ * `UniqueIDGenerator` in `@salesforce/agentic-common`, never `randomUUID()`.
65
+ */
66
+ correlationId?: string;
67
+ /**
68
+ * `x-client-trace-id` header value attached to this request. Consumers pair on
69
+ * `correlationId` (the harness-guaranteed key) and may cross-check this value.
70
+ */
71
+ xClientTraceId?: string;
58
72
  /** Request body. Best-effort: partial / absent on harnesses that can't observe it. */
59
73
  body?: Record<string, unknown>;
60
74
  };
@@ -65,6 +79,13 @@ export type LlmResponseEvent = {
65
79
  timestamp: Date;
66
80
  /** Native model id from the matching request. */
67
81
  model: string;
82
+ /**
83
+ * Harness-generated per-fetch pairing key. Stamped identically on the matching
84
+ * `LlmRequestEvent`; consumers MUST use it as the reliable pairing key rather
85
+ * than positional event ordering. Producers obtain it from the injected
86
+ * `UniqueIDGenerator` in `@salesforce/agentic-common`, never `randomUUID()`.
87
+ */
88
+ correlationId?: string;
68
89
  /** HTTP status code; `0` for harness-internal terminal failures with no transport response. */
69
90
  status: number;
70
91
  /** Error captured from a transport / parse failure, if any. */
@@ -85,9 +106,42 @@ export type LlmResponseEvent = {
85
106
  totalDurationMs?: number;
86
107
  /** Best-effort extracted response text (may be omitted by the harness). */
87
108
  responseText?: string;
88
- /** `x-client-trace-id` header value the SDK / harness attached to the request. */
109
+ /**
110
+ * `x-client-trace-id` header value attached to the matching request. Consumers
111
+ * pair on `correlationId` (the harness-guaranteed key) and may cross-check this value.
112
+ */
89
113
  xClientTraceId?: string;
90
114
  };
115
+ /**
116
+ * Debug-only, opt-in, subscriber-gated record of a completed MCP tool call.
117
+ * `args` and `result` are captured verbatim and MAY contain PII, so consumers
118
+ * must redact before persisting or sharing. This is not a telemetry event and
119
+ * MUST NOT be added to `TelemetryEvent` or any externally-shipped surface.
120
+ *
121
+ * `toolName` is the harness-namespaced display name and `bareToolName` is the
122
+ * un-namespaced leaf. `durationMs` semantics differ by harness: the Claude
123
+ * bridge reports true RPC latency, while Mastra and OpenAI report an
124
+ * adapter/coordinator-observed window that may include a model round-trip.
125
+ * Consumers MUST NOT compare durations across harnesses.
126
+ */
127
+ export type McpToolCallCompletedEvent = {
128
+ /** Direction discriminator. */
129
+ type: 'mcp-tool-call-completed';
130
+ /** Wall-clock timestamp when the tool call completed. */
131
+ timestamp: Date;
132
+ /** Logical MCP server name from the configured server catalog. */
133
+ serverName: string;
134
+ /** Harness-namespaced display name presented to the model. */
135
+ toolName: string;
136
+ /** Un-namespaced tool-name leaf. */
137
+ bareToolName: string;
138
+ /** Verbatim arguments passed to the tool. May contain PII. */
139
+ args: Record<string, unknown>;
140
+ /** Verbatim result returned by the tool. May contain PII. */
141
+ result: unknown;
142
+ /** Harness-dependent observed duration; do not compare across harnesses. */
143
+ durationMs?: number;
144
+ };
91
145
  /**
92
146
  * Emitted by harnesses whose runtime doesn't expose a programmatic seam for
93
147
  * per-call wire monitoring. The event signals to consumers that
@@ -120,5 +174,5 @@ export type WireMonitoringNotSupportedEvent = {
120
174
  * Union over every wire-communication event the harness layer can emit.
121
175
  * Discriminate on the `type` field.
122
176
  */
123
- export type WireCommunicationEvent = LlmRequestEvent | LlmResponseEvent | WireMonitoringNotSupportedEvent;
177
+ export type WireCommunicationEvent = LlmRequestEvent | LlmResponseEvent | McpToolCallCompletedEvent | WireMonitoringNotSupportedEvent;
124
178
  export type WireCommunicationEventCallback = (event: WireCommunicationEvent) => void;
@@ -68,6 +68,8 @@ function formatAsMarkdown(event) {
68
68
  return formatRequestMarkdown(event);
69
69
  case 'llm-response':
70
70
  return formatResponseMarkdown(event);
71
+ case 'mcp-tool-call-completed':
72
+ return formatMcpToolCallCompletedMarkdown(event);
71
73
  case 'wire-monitoring-not-supported':
72
74
  return formatWireMonitoringNotSupportedMarkdown(event);
73
75
  }
@@ -135,6 +137,32 @@ function formatResponseMarkdown(event) {
135
137
  }
136
138
  return lines.join('\n');
137
139
  }
140
+ function formatMcpToolCallCompletedMarkdown(event) {
141
+ const lines = [
142
+ `### ${event.timestamp.toISOString()} [${event.type}]`,
143
+ ``,
144
+ `**Server**: ${event.serverName}`,
145
+ ``,
146
+ `**Tool**: ${event.toolName}`,
147
+ ``,
148
+ `**Bare tool**: ${event.bareToolName}`,
149
+ ``,
150
+ `**Args**:`,
151
+ '```json',
152
+ JSON.stringify(event.args, null, 2),
153
+ '```',
154
+ ``,
155
+ `**Result**:`,
156
+ '```json',
157
+ JSON.stringify(event.result, null, 2),
158
+ '```',
159
+ ``,
160
+ ];
161
+ if (event.durationMs !== undefined) {
162
+ lines.push(`**Duration**: ${event.durationMs}`, ``);
163
+ }
164
+ return lines.join('\n');
165
+ }
138
166
  function formatWireMonitoringNotSupportedMarkdown(event) {
139
167
  const dateTime = event.timestamp.toISOString();
140
168
  return [`### ${dateTime} [${event.type}]`, ``, `**Harness**: ${event.harnessId}`, ``, event.message, ``, ``].join('\n');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/sfdx-agent-sdk",
3
- "version": "0.39.0",
3
+ "version": "0.41.0",
4
4
  "description": "Harness-agnostic agentic infrastructure for Salesforce developer experience tooling",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -47,9 +47,9 @@
47
47
  },
48
48
  "devDependencies": {
49
49
  "@eslint/js": "^10.0.1",
50
- "@salesforce/sfdx-agent-harness-claude": "0.35.0",
51
- "@salesforce/sfdx-agent-harness-mastra": "0.38.0",
52
- "@salesforce/sfdx-agent-harness-openai": "0.4.0",
50
+ "@salesforce/sfdx-agent-harness-claude": "0.37.0",
51
+ "@salesforce/sfdx-agent-harness-mastra": "0.40.0",
52
+ "@salesforce/sfdx-agent-harness-openai": "0.6.0",
53
53
  "@types/node": "^22.20.0",
54
54
  "@vitest/coverage-istanbul": "^4.1.10",
55
55
  "@vitest/eslint-plugin": "^1.6.22",