@salesforce/sfdx-agent-sdk 0.40.0 → 0.42.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.42.0] - 2026-08-04
7
+
8
+ ### Features
9
+ - **agentic-common,agent-sdk**: reusable env-aware event-bus hierarchy @W-23694153@ ([#732](https://github.com/forcedotcom/agentic-dx/pull/732))
10
+
11
+ ### Chores
12
+ - **deps-dev**: bump eslint from 10.7.0 to 10.8.0 in the eslint group ([#728](https://github.com/forcedotcom/agentic-dx/pull/728))
13
+
14
+ ## [0.41.0] - 2026-08-03
15
+
16
+ ### Features
17
+ - **agent-sdk,harnesses**: emit Tier-1 logBus siblings for lifecycle telemetry @W-23547150@ ([#716](https://github.com/forcedotcom/agentic-dx/pull/716))
18
+
6
19
  ## [0.40.0] - 2026-08-03
7
20
 
8
21
  ### Features
package/README.md CHANGED
@@ -228,6 +228,37 @@ function onApprovalRequest(event: ToolApprovalRequestEvent): Promise<boolean> {
228
228
  }
229
229
  ```
230
230
 
231
+ ### Environment Context
232
+
233
+ Every `TelemetryEvent` and `LogRecord` the SDK emits carries optional, **flat** identity fields — stamped once, at the
234
+ event's construction site, by the origin bus (not appended per-subscriber). Subscribe via `onTelemetry` / `onLog` at any
235
+ scope and read the fields directly (e.g. `event.orgId`); because they're flat top-level fields (not nested), log
236
+ pipelines like Pino → Splunk can filter on them without dotted-path escaping.
237
+
238
+ ```typescript
239
+ type EnvironmentFields = {
240
+ instanceType?: string; // INSTANCE_TYPE — what kind of running instance this is
241
+ instanceId?: string; // INSTANCE_ID — this specific running instance
242
+ orgId?: string; // ORG_ID
243
+ userId?: string; // USER_ID
244
+ featureId?: string; // FEATURE_ID, falling back to legacy LLMG_FEATURE_ID
245
+ };
246
+ ```
247
+
248
+ The fields are populated from these **generic** environment variables, read once when the buses are constructed. Each
249
+ field is optional — present only when its variable holds a non-empty value, so in a plain local process the fields are
250
+ absent entirely. A deployment maps its own concepts onto the generic vars at the environment boundary (e.g. a DX
251
+ Workspace container exports `INSTANCE_TYPE=$DXW_TEMPLATE_NAME` / `INSTANCE_ID=$SFDX_INSTANCE_ID`), so the SDK never
252
+ learns deployment-specific names. These fields are a convenience for single-tenant deployments and are subject to
253
+ change; the type and readers are re-exported from `@salesforce/agentic-common`:
254
+
255
+ - `readEnvironmentContext(env?)` — maps the generic env vars onto an `EnvironmentFields` bag.
256
+ - `resolveFeatureId(env?)` — `FEATURE_ID` → legacy `LLMG_FEATURE_ID` → `undefined`.
257
+
258
+ The bus hierarchy that stamps these (`TimestampedEventBus` / `EnvironmentAwareEventBus` / the reusable
259
+ `BaseTelemetryEventBus`) lives in `@salesforce/agentic-common` — see that package's README to build env-stamped
260
+ telemetry for a service of your own.
261
+
231
262
  ### Configuration Types
232
263
 
233
264
  #### `AgentConfig`
@@ -1546,6 +1577,13 @@ Abandoning the iterator mid-stream emits no terminal telemetry — the stream di
1546
1577
  Harness implementations may emit additional structured logs through their own `LogBus`. Those records surface to
1547
1578
  consumers via the same `onLog` channel.
1548
1579
 
1580
+ Selected lifecycle moments are reported on **both** channels: the typed telemetry event, plus a log record carrying the
1581
+ same facts for log sinks. On those records `message` is human-readable prose intended for display and may be reworded;
1582
+ `context.event_type` carries the matching `TelemetryEvent` type and is the stable field to filter, alert, and join on.
1583
+ Records paired this way today are `agent-created`, `agent-destroyed`, `session-created`, `session-destroyed`,
1584
+ `chat-stream-completed`, `chat-stream-error`, `tool-execution-completed` (only when `isError` is true), and the harness
1585
+ MCP events `mcp-server-discovery-completed`, `mcp-server-discovery-failed`, and `mcp-server-status-changed`.
1586
+
1549
1587
  ## Development
1550
1588
 
1551
1589
  See [DEVELOPING.md](DEVELOPING.md) for build-from-source setup, scripts, E2E testing, and packaging.
@@ -4,7 +4,7 @@ import type { HarnessFactory } from './harness/harness-factory.js';
4
4
  import { type AgentConfig } from './harness/harness-config.js';
5
5
  import { type Agent } from './agent.js';
6
6
  import type { HooksForAgent } from './types/redaction.js';
7
- import type { TelemetryEventCallback } from './types/telemetry-events.js';
7
+ import { type TelemetryEventCallback } from './types/telemetry-events.js';
8
8
  import type { WireCommunicationEventCallback } from './types/wire-communication-event.js';
9
9
  import type { ProviderHint } from './types/model-connectivity-info.js';
10
10
  import { type AgentConnectivityResolver } from './agent-connectivity-resolver.js';
@@ -12,6 +12,7 @@ import { AgentSDKError, AgentSDKErrorType } from './errors.js';
12
12
  import { TelemetryRouter } from './internal/telemetry-router.js';
13
13
  import { WireCommunicationRouter } from './internal/wire-communication-router.js';
14
14
  import { AgentIdentityStore } from './internal/agent-identity-store.js';
15
+ import { createTelemetryBus } from './types/telemetry-events.js';
15
16
  import { DefaultAgentConnectivityResolver } from './agent-connectivity-resolver.js';
16
17
  /**
17
18
  * Concrete implementation of {@link AgentManager}. **Not exported** from
@@ -32,7 +33,10 @@ export class DefaultAgentManager {
32
33
  identityStore;
33
34
  agents = new Map();
34
35
  restoreFailures = [];
35
- telemetryBus = new EventBus();
36
+ // Constructed in the constructor body so it uses the injected `clock` (tests inject a StubClock for
37
+ // deterministic timestamps); env context is self-read from the process. The `logBus` is supplied by
38
+ // `createAgentManager` (already clock-bound) so the identity store can share it.
39
+ telemetryBus;
36
40
  logBus;
37
41
  wireBus = new EventBus();
38
42
  router;
@@ -47,6 +51,7 @@ export class DefaultAgentManager {
47
51
  this.identityStore = identityStore;
48
52
  this.agentIdGenerator = agentIdGenerator;
49
53
  this.clock = clock;
54
+ this.telemetryBus = createTelemetryBus(this.clock);
50
55
  this.logBus = logBus;
51
56
  this.router = new TelemetryRouter(harness);
52
57
  this.wireRouter = new WireCommunicationRouter(harness);
@@ -189,13 +194,15 @@ export class DefaultAgentManager {
189
194
  this.wireRouter.registerAgent(agentId);
190
195
  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
196
  this.agents.set(agentId, agent);
192
- this.telemetryBus.emit({
193
- type: 'agent-created',
194
- timestamp: this.clock.now(),
195
- agentId,
196
- projectRoot,
197
- modelName: runtime.modelConnectivityInfo.model.name,
198
- });
197
+ const agentCreatedAt = this.clock.now();
198
+ const modelName = runtime.modelConnectivityInfo.model.name;
199
+ // Tier-1 pair: share one tick across the telemetry event and its colocated log sibling.
200
+ this.telemetryBus.emitTelemetry({ type: 'agent-created', agentId, projectRoot, modelName }, agentCreatedAt);
201
+ this.logBus.emitLog({
202
+ level: 'info',
203
+ message: 'Agent created',
204
+ context: { event_type: 'agent-created', agentId, projectRoot, modelName },
205
+ }, agentCreatedAt);
199
206
  if (options.rehydrateThreads) {
200
207
  // If thread enumeration or session attachment fails, the restore is a full failure
201
208
  // (the user would otherwise see an agent marked `ready` whose chat sessions return 404).
@@ -351,7 +358,10 @@ export async function createAgentManager(storageRootFolder, harnessFactory, opti
351
358
  `Update the SDK or harness package.`, AgentSDKErrorType.INCOMPATIBLE_HARNESS);
352
359
  }
353
360
  const agentConnectivityResolver = options?.connectivityResolver ?? new DefaultAgentConnectivityResolver();
354
- return DefaultAgentManager.__build(harness, harnessFactory.supportedProviderHints, agentConnectivityResolver, options?.hooksForAgent, storageRootFolder, new UUIDGenerator(), new RealClock(), new LogBus());
361
+ const clock = new RealClock();
362
+ return DefaultAgentManager.__build(harness, harnessFactory.supportedProviderHints, agentConnectivityResolver, options?.hooksForAgent, storageRootFolder, new UUIDGenerator(), clock,
363
+ // The manager's root log bus shares the manager clock and self-reads the process environment context.
364
+ new LogBus(clock));
355
365
  }
356
366
  function isSupportedProtocolVersion(version) {
357
367
  return (typeof version === 'number' &&
package/dist/agent.d.ts CHANGED
@@ -8,7 +8,7 @@ import type { ModelConnectivityInfo, ProviderHint } from './types/model-connecti
8
8
  import type { AgentIdentityStore } from './internal/agent-identity-store.js';
9
9
  import type { TelemetryRouter, TelemetrySlice } from './internal/telemetry-router.js';
10
10
  import type { HooksForAgent } from './types/redaction.js';
11
- import type { TelemetryBus, TelemetryEventCallback } from './types/telemetry-events.js';
11
+ import { type TelemetryBus, type TelemetryEventCallback } from './types/telemetry-events.js';
12
12
  /**
13
13
  * Parent bus pair wired at construction time so an agent's events bubble upward into the manager's buses.
14
14
  */
package/dist/agent.js CHANGED
@@ -2,10 +2,11 @@
2
2
  * Copyright 2026, Salesforce, Inc. All rights reserved.
3
3
  * See LICENSE.txt for license terms.
4
4
  */
5
- import { EventBus, LogBus, RealClock, UUIDGenerator, } from '@salesforce/agentic-common';
5
+ import { LogBus, RealClock, UUIDGenerator, } from '@salesforce/agentic-common';
6
6
  import { toHarnessConfig } from './harness/harness-config.js';
7
7
  import { DefaultChatSession } from './chat-session.js';
8
8
  import { AgentSDKError, AgentSDKErrorType } from './errors.js';
9
+ import { createTelemetryBus } from './types/telemetry-events.js';
9
10
  /**
10
11
  * Default implementation of {@link Agent} that delegates
11
12
  * agent and thread operations to an {@link AgentHarness}.
@@ -25,8 +26,10 @@ export class DefaultAgent {
25
26
  sessions = new Map();
26
27
  sessionSliceUnregisters = new Map();
27
28
  router;
28
- telemetryBus = new EventBus();
29
- logBus = new LogBus();
29
+ // Constructed in the constructor body so they use the injected `clock` (tests inject a StubClock for
30
+ // deterministic timestamps); env context is self-read from the process.
31
+ telemetryBus;
32
+ logBus;
30
33
  inboundUnsubs;
31
34
  parentUnsubs;
32
35
  clock;
@@ -67,6 +70,8 @@ export class DefaultAgent {
67
70
  this.identityStore = identityStore;
68
71
  this.router = router;
69
72
  this.clock = clock;
73
+ this.telemetryBus = createTelemetryBus(this.clock);
74
+ this.logBus = new LogBus(this.clock);
70
75
  this.idGenerator = idGenerator;
71
76
  this.inboundUnsubs = [inbound.telemetry.forwardTo(this.telemetryBus), inbound.log.forwardTo(this.logBus)];
72
77
  this.parentUnsubs = [this.telemetryBus.forwardTo(parent.telemetry), this.logBus.forwardTo(parent.log)];
@@ -283,11 +288,14 @@ export class DefaultAgent {
283
288
  }
284
289
  this.sessions.clear();
285
290
  await this.harness.destroyAgent(this.agentId);
286
- this.telemetryBus.emit({
287
- type: 'agent-destroyed',
288
- timestamp: this.clock.now(),
289
- agentId: this.agentId,
290
- });
291
+ const agentDestroyedAt = this.clock.now();
292
+ // Tier-1 pair: share one tick across the telemetry event and its colocated log sibling.
293
+ this.telemetryBus.emitTelemetry({ type: 'agent-destroyed', agentId: this.agentId }, agentDestroyedAt);
294
+ this.logBus.emitLog({
295
+ level: 'info',
296
+ message: 'Agent destroyed',
297
+ context: { event_type: 'agent-destroyed', agentId: this.agentId },
298
+ }, agentDestroyedAt);
291
299
  for (const unsub of this.inboundUnsubs)
292
300
  unsub();
293
301
  for (const unsub of this.parentUnsubs)
@@ -347,12 +355,14 @@ export class DefaultAgent {
347
355
  }, getContextWindow, { clock: this.clock, idGenerator: this.idGenerator, persistRememberedRule });
348
356
  this.sessions.set(threadId, session);
349
357
  this.sessionSliceUnregisters.set(threadId, () => this.router.unregisterSession(threadId));
350
- this.telemetryBus.emit({
351
- type: 'session-created',
352
- timestamp: this.clock.now(),
353
- agentId: this.agentId,
354
- threadId,
355
- });
358
+ const sessionCreatedAt = this.clock.now();
359
+ // Tier-1 pair: share one tick across the telemetry event and its colocated log sibling.
360
+ this.telemetryBus.emitTelemetry({ type: 'session-created', agentId: this.agentId, threadId }, sessionCreatedAt);
361
+ this.logBus.emitLog({
362
+ level: 'info',
363
+ message: 'Chat session created',
364
+ context: { event_type: 'session-created', agentId: this.agentId, threadId },
365
+ }, sessionCreatedAt);
356
366
  return session;
357
367
  }
358
368
  detachSession(threadId, session) {
@@ -4,7 +4,7 @@ import type { StreamOptions } from './harness/harness-config.js';
4
4
  import type { TelemetrySlice } from './internal/telemetry-router.js';
5
5
  import type { ChatEvent, ChatStreamResult } from './types/events.js';
6
6
  import type { Message, MessagePart } from './types/messages.js';
7
- import type { TelemetryBus, TelemetryEventCallback } from './types/telemetry-events.js';
7
+ import { type TelemetryBus, type TelemetryEventCallback } from './types/telemetry-events.js';
8
8
  import type { ToolPolicyRule, ToolResultInfo } from './types/tools.js';
9
9
  import type { ContextUsage } from './types/usage.js';
10
10
  /**
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { backfillCreatedAt, EventBus, LogBus, RealClock, UUIDGenerator, } from '@salesforce/agentic-common';
6
6
  import { AgentSDKError, AgentSDKErrorType } from './errors.js';
7
+ import { createTelemetryBus, } from './types/telemetry-events.js';
7
8
  /**
8
9
  * Default implementation of {@link ChatSession} that delegates all operations
9
10
  * to an {@link AgentHarness}. The session holds its agent ID and thread ID
@@ -14,8 +15,10 @@ export class DefaultChatSession {
14
15
  agentId;
15
16
  threadId;
16
17
  chatEventBus = new EventBus();
17
- telemetryBus = new EventBus();
18
- logBus = new LogBus();
18
+ // Constructed in the constructor body so they use the injected `clock` (tests inject a StubClock for
19
+ // deterministic timestamps); env context is self-read from the process.
20
+ telemetryBus;
21
+ logBus;
19
22
  inboundUnsubs;
20
23
  parentUnsubs;
21
24
  clock;
@@ -86,6 +89,8 @@ export class DefaultChatSession {
86
89
  this.clock = deps.clock ?? new RealClock();
87
90
  this.idGenerator = deps.idGenerator ?? new UUIDGenerator();
88
91
  this.persistRememberedRule = deps.persistRememberedRule;
92
+ this.telemetryBus = createTelemetryBus(this.clock);
93
+ this.logBus = new LogBus(this.clock);
89
94
  this.inboundUnsubs = [inbound.telemetry.forwardTo(this.telemetryBus), inbound.log.forwardTo(this.logBus)];
90
95
  this.parentUnsubs = [this.telemetryBus.forwardTo(parent.telemetry), this.logBus.forwardTo(parent.log)];
91
96
  }
@@ -229,24 +234,45 @@ export class DefaultChatSession {
229
234
  const finishedAt = this.clock.now();
230
235
  const durationMs = finishedAt.getTime() - startedAt.getTime();
231
236
  if (lastError !== undefined) {
232
- this.telemetryBus.emit({
237
+ // Tier-1 pair: share one tick across the telemetry event and its colocated log sibling.
238
+ this.telemetryBus.emitTelemetry({
233
239
  type: 'chat-stream-error',
234
- timestamp: finishedAt,
235
240
  agentId: this.agentId,
236
241
  threadId: this.threadId,
237
242
  durationMs,
238
243
  error: lastError,
239
- });
244
+ }, finishedAt);
245
+ this.logBus.emitLog({
246
+ level: 'error',
247
+ message: 'Chat stream failed',
248
+ context: {
249
+ event_type: 'chat-stream-error',
250
+ agentId: this.agentId,
251
+ threadId: this.threadId,
252
+ durationMs,
253
+ },
254
+ error: lastError,
255
+ }, finishedAt);
240
256
  }
241
257
  else {
242
- this.telemetryBus.emit({
258
+ this.telemetryBus.emitTelemetry({
243
259
  type: 'chat-stream-completed',
244
- timestamp: finishedAt,
245
260
  agentId: this.agentId,
246
261
  threadId: this.threadId,
247
262
  durationMs,
248
263
  usage: finishUsage,
249
- });
264
+ }, finishedAt);
265
+ this.logBus.emitLog({
266
+ level: 'info',
267
+ message: 'Chat stream completed',
268
+ context: {
269
+ event_type: 'chat-stream-completed',
270
+ agentId: this.agentId,
271
+ threadId: this.threadId,
272
+ durationMs,
273
+ ...(finishUsage !== undefined ? { usage: finishUsage } : {}),
274
+ },
275
+ }, finishedAt);
250
276
  }
251
277
  }
252
278
  /**
@@ -420,12 +446,18 @@ export class DefaultChatSession {
420
446
  if (this.disposed) {
421
447
  return;
422
448
  }
423
- this.telemetryBus.emit({
424
- type: 'session-destroyed',
425
- timestamp: this.clock.now(),
426
- agentId: this.agentId,
427
- threadId: this.threadId,
428
- });
449
+ const sessionDestroyedAt = this.clock.now();
450
+ // Tier-1 pair: share one tick across the telemetry event and its colocated log sibling.
451
+ this.telemetryBus.emitTelemetry({ type: 'session-destroyed', agentId: this.agentId, threadId: this.threadId }, sessionDestroyedAt);
452
+ this.logBus.emitLog({
453
+ level: 'info',
454
+ message: 'Chat session destroyed',
455
+ context: {
456
+ event_type: 'session-destroyed',
457
+ agentId: this.agentId,
458
+ threadId: this.threadId,
459
+ },
460
+ }, sessionDestroyedAt);
429
461
  for (const unsub of this.inboundUnsubs)
430
462
  unsub();
431
463
  for (const unsub of this.parentUnsubs)
@@ -436,9 +468,10 @@ export class DefaultChatSession {
436
468
  this.disposed = true;
437
469
  }
438
470
  emitToolApprovalResolved(toolCallId, approved, policyWritten) {
439
- this.telemetryBus.emit({
471
+ // No Tier-1 logBus sibling: tool-approval-resolved fires per gated
472
+ // tool call — interactive-UI signal, not on-call triage
473
+ this.telemetryBus.emitTelemetry({
440
474
  type: 'tool-approval-resolved',
441
- timestamp: this.clock.now(),
442
475
  agentId: this.agentId,
443
476
  threadId: this.threadId,
444
477
  toolCallId,
@@ -476,9 +509,10 @@ export class DefaultChatSession {
476
509
  deriveToolTelemetry(event) {
477
510
  if (event.type === 'tool-call') {
478
511
  this.toolStartMs.set(event.toolCallId, this.clock.now().getTime());
479
- this.telemetryBus.emit({
512
+ // No Tier-1 logBus sibling: tool-execution-started pairs 1:1 with
513
+ // tool-execution-completed; the completion (isError=true) is the triage signal
514
+ this.telemetryBus.emitTelemetry({
480
515
  type: 'tool-execution-started',
481
- timestamp: this.clock.now(),
482
516
  agentId: this.agentId,
483
517
  threadId: this.threadId,
484
518
  toolCallId: event.toolCallId,
@@ -492,19 +526,40 @@ export class DefaultChatSession {
492
526
  if (start === undefined)
493
527
  return;
494
528
  this.toolStartMs.delete(event.toolCallId);
495
- this.telemetryBus.emit({
529
+ const completedAt = this.clock.now();
530
+ const durationMs = completedAt.getTime() - start;
531
+ const isError = event.isError === true;
532
+ this.telemetryBus.emitTelemetry({
496
533
  type: 'tool-execution-completed',
497
- timestamp: this.clock.now(),
498
534
  agentId: this.agentId,
499
535
  threadId: this.threadId,
500
536
  toolCallId: event.toolCallId,
501
537
  toolName: event.toolName,
502
- durationMs: this.clock.now().getTime() - start,
503
- isError: event.isError === true,
538
+ durationMs,
539
+ isError,
504
540
  ...(event.error ? { error: event.error } : {}),
505
541
  ...(event.annotations ? { annotations: event.annotations } : {}),
506
542
  ...(event.serverName ? { serverName: event.serverName } : {}),
507
- });
543
+ }, completedAt);
544
+ // Tier-1 logBus sibling only when isError=true; share the tick with the telemetry event.
545
+ if (isError) {
546
+ this.logBus.emitLog({
547
+ level: 'error',
548
+ message: 'Tool execution failed',
549
+ context: {
550
+ event_type: 'tool-execution-completed',
551
+ agentId: this.agentId,
552
+ threadId: this.threadId,
553
+ toolCallId: event.toolCallId,
554
+ toolName: event.toolName,
555
+ durationMs,
556
+ isError: true,
557
+ ...(event.annotations ? { annotations: event.annotations } : {}),
558
+ ...(event.serverName ? { serverName: event.serverName } : {}),
559
+ },
560
+ ...(event.error ? { error: event.error } : {}),
561
+ }, completedAt);
562
+ }
508
563
  }
509
564
  else if (event.type === 'tool-approval-request') {
510
565
  // Record the (toolName, serverName?) so a later settle with
@@ -523,9 +578,8 @@ export class DefaultChatSession {
523
578
  toolName: event.bareToolName ?? event.toolCall.toolName,
524
579
  ...(event.serverName ? { serverName: event.serverName } : {}),
525
580
  });
526
- this.telemetryBus.emit({
581
+ this.telemetryBus.emitTelemetry({
527
582
  type: 'tool-approval-requested',
528
- timestamp: this.clock.now(),
529
583
  agentId: this.agentId,
530
584
  threadId: this.threadId,
531
585
  toolCallId: event.toolCall.toolCallId,
@@ -574,13 +628,9 @@ export class DefaultChatSession {
574
628
  */
575
629
  emitChatStreamStarted(trigger) {
576
630
  const startedAt = this.clock.now();
577
- this.telemetryBus.emit({
578
- type: 'chat-stream-started',
579
- timestamp: startedAt,
580
- agentId: this.agentId,
581
- threadId: this.threadId,
582
- trigger,
583
- });
631
+ // chat-stream-started pairs 1:1 with chat-stream-completed, which already carries outcomes.
632
+ // Pin the timestamp to `startedAt` (also threaded out for duration math), rather than re-reading the clock.
633
+ this.telemetryBus.emitTelemetry({ type: 'chat-stream-started', agentId: this.agentId, threadId: this.threadId, trigger }, startedAt);
584
634
  return startedAt;
585
635
  }
586
636
  /**
@@ -597,14 +647,20 @@ export class DefaultChatSession {
597
647
  this.chatEventBus.emit({ type: 'error', error });
598
648
  this.chatEventBus.emit({ type: 'finish', finishReason: 'error' });
599
649
  const finishedAt = this.clock.now();
600
- this.telemetryBus.emit({
601
- type: 'chat-stream-error',
602
- timestamp: finishedAt,
603
- agentId: this.agentId,
604
- threadId: this.threadId,
605
- durationMs: finishedAt.getTime() - startedAt.getTime(),
650
+ const durationMs = finishedAt.getTime() - startedAt.getTime();
651
+ // Tier-1 pair: share one tick across the telemetry event and its colocated log sibling.
652
+ this.telemetryBus.emitTelemetry({ type: 'chat-stream-error', agentId: this.agentId, threadId: this.threadId, durationMs, error }, finishedAt);
653
+ this.logBus.emitLog({
654
+ level: 'error',
655
+ message: 'Chat stream failed',
656
+ context: {
657
+ event_type: 'chat-stream-error',
658
+ agentId: this.agentId,
659
+ threadId: this.threadId,
660
+ durationMs,
661
+ },
606
662
  error,
607
- });
663
+ }, finishedAt);
608
664
  }
609
665
  /**
610
666
  * issue #529 contract change: a settle call (`approveToolCall` /
@@ -1,5 +1,5 @@
1
- import { LogBus, type LogRecord, type Unsubscribe } from '@salesforce/agentic-common';
2
- import type { TelemetryEvent, TelemetryEventCallback } from '../types/telemetry-events.js';
1
+ import { type Clock, type EnvironmentFields, LogBus, type LogRecord, type Unsubscribe } from '@salesforce/agentic-common';
2
+ import { type TelemetryEvent, type TelemetryEventCallback } from '../types/telemetry-events.js';
3
3
  import type { WireCommunicationEvent, WireCommunicationEventCallback } from '../types/wire-communication-event.js';
4
4
  /**
5
5
  * Composition helper used by `AgentHarness` implementations to own telemetry and log buses.
@@ -16,6 +16,14 @@ export declare class HarnessBusOwner {
16
16
  private readonly logBus;
17
17
  private readonly wireBus;
18
18
  private disposed;
19
+ /**
20
+ * @param clock - Time source for event timestamps. Harnesses that construct their own event `timestamp`
21
+ * (the common case) can leave this defaulted; the value is preserved regardless via the emit override.
22
+ * @param environment - Identity context stamped onto telemetry/log events. Defaults to reading the
23
+ * process's generic identity env vars (production zero-wiring); tests pass an explicit context (e.g. `{}`)
24
+ * for determinism.
25
+ */
26
+ constructor(clock?: Clock, environment?: EnvironmentFields);
19
27
  /**
20
28
  * Returns the log bus so harness implementations can hand it to pure helpers (e.g. message
21
29
  * mappers / event adapters) that need to emit structured logs. Returns `undefined` after
@@ -2,8 +2,9 @@
2
2
  * Copyright 2026, Salesforce, Inc. All rights reserved.
3
3
  * See LICENSE.txt for license terms.
4
4
  */
5
- import { EventBus, LogBus } from '@salesforce/agentic-common';
5
+ import { EventBus, LogBus, } from '@salesforce/agentic-common';
6
6
  import { AgentSDKError, AgentSDKErrorType } from '../errors.js';
7
+ import { createTelemetryBus, } from '../types/telemetry-events.js';
7
8
  /**
8
9
  * Composition helper used by `AgentHarness` implementations to own telemetry and log buses.
9
10
  *
@@ -15,10 +16,24 @@ import { AgentSDKError, AgentSDKErrorType } from '../errors.js';
15
16
  * can reuse it.
16
17
  */
17
18
  export class HarnessBusOwner {
18
- telemetryBus = new EventBus();
19
- logBus = new LogBus();
19
+ // Env-aware origin buses stamp the process environment fields (plus preserve the harness-supplied
20
+ // timestamp) onto every telemetry event / log record emitted here. The wire bus is intentionally a plain
21
+ // EventBus — wire-communication events carry no environment context.
22
+ telemetryBus;
23
+ logBus;
20
24
  wireBus = new EventBus();
21
25
  disposed = false;
26
+ /**
27
+ * @param clock - Time source for event timestamps. Harnesses that construct their own event `timestamp`
28
+ * (the common case) can leave this defaulted; the value is preserved regardless via the emit override.
29
+ * @param environment - Identity context stamped onto telemetry/log events. Defaults to reading the
30
+ * process's generic identity env vars (production zero-wiring); tests pass an explicit context (e.g. `{}`)
31
+ * for determinism.
32
+ */
33
+ constructor(clock, environment) {
34
+ this.telemetryBus = createTelemetryBus(clock, environment);
35
+ this.logBus = new LogBus(clock, environment);
36
+ }
22
37
  /**
23
38
  * Returns the log bus so harness implementations can hand it to pure helpers (e.g. message
24
39
  * mappers / event adapters) that need to emit structured logs. Returns `undefined` after
@@ -65,11 +80,18 @@ export class HarnessBusOwner {
65
80
  }
66
81
  emitTelemetry(event) {
67
82
  this.assertNotDisposed();
68
- this.telemetryBus.emit(event);
83
+ // Harnesses construct a full event with their own `timestamp` but do NOT set env-identity fields — those
84
+ // are bus-owned, stamped by the env-aware bus below. We split off `timestamp` (passed as the override so
85
+ // the harness's real emit time is preserved) and forward the rest as the payload. (The bus folds the
86
+ // payload last, so a harness that DID set an env field would override the bus's value; no harness does,
87
+ // so this is moot — env identity is the bus's responsibility by design.)
88
+ const { timestamp, ...payload } = event;
89
+ this.telemetryBus.emitTelemetry(payload, timestamp);
69
90
  }
70
91
  emitLog(record) {
71
92
  this.assertNotDisposed();
72
- this.logBus.emit(record);
93
+ const { timestamp, ...payload } = record;
94
+ this.logBus.emitLog(payload, timestamp);
73
95
  }
74
96
  logDebug(message, context) {
75
97
  this.assertNotDisposed();
package/dist/index.d.ts CHANGED
@@ -25,6 +25,8 @@ export type { AgentHarness, HarnessFactory, WithAgentConfig, ConfigOf } from './
25
25
  export { AgentSDKError, AgentSDKErrorType } from './errors.js';
26
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
+ export type { EnvironmentFields } from '@salesforce/agentic-common';
29
+ export { readEnvironmentContext, resolveFeatureId } from '@salesforce/agentic-common';
28
30
  export { resolveMcpServerHeaders } from './mcp-auth.js';
29
31
  export type { OrgConnection, OrgConnectionFactory } from '@salesforce/agentic-common';
30
32
  export type { JSONWebToken, JWTOptions, RequiredJWTHeaders, RequiredJWTPayload } from '@salesforce/agentic-common';
package/dist/index.js CHANGED
@@ -21,6 +21,7 @@ export { ApiKeyConnectivityResolver } from './api-key-connectivity-resolver.js';
21
21
  export { WireCommunicationFileWriter, } from './wire-communication-file-writer.js';
22
22
  // ── Errors ───────────────────────────────────────────────────────────
23
23
  export { AgentSDKError, AgentSDKErrorType } from './errors.js';
24
+ export { readEnvironmentContext, resolveFeatureId } from '@salesforce/agentic-common';
24
25
  // ── MCP Auth ────────────────────────────────────────────────────────
25
26
  export { resolveMcpServerHeaders } from './mcp-auth.js';
26
27
  export { Workspace } from './workspace.js';
@@ -1,14 +1,19 @@
1
- import { LogBus } from '@salesforce/agentic-common';
1
+ import { EventBus, LogBus } from '@salesforce/agentic-common';
2
2
  import type { AgentHarness } from '../harness/agent-harness.js';
3
- import type { TelemetryBus } from '../types/telemetry-events.js';
3
+ import type { TelemetryEvent } from '../types/telemetry-events.js';
4
4
  /**
5
5
  * A pair of buses (telemetry + log) scoped to a single agent, session, or the unrouted catch-all.
6
6
  *
7
7
  * `DefaultAgent` and `DefaultChatSession` receive their slice at construction time and wire `forwardTo()`
8
8
  * from the slice bus into their own bus so events bubble upward through the hierarchy.
9
+ *
10
+ * Slice buses are **forward-only**: they re-emit events routed from the harness (or forwarded up from a child)
11
+ * and never construct events themselves, so they carry no environment context — a plain `EventBus<TelemetryEvent>`
12
+ * and a zero-context `LogBus`. Environment stamping happens once, on the origin bus that constructs the event
13
+ * (a harness `HarnessBusOwner` bus, or a manager/agent/session's own telemetry/log bus).
9
14
  */
10
15
  export type TelemetrySlice = {
11
- readonly telemetry: TelemetryBus;
16
+ readonly telemetry: EventBus<TelemetryEvent>;
12
17
  readonly log: LogBus;
13
18
  };
14
19
  /**
@@ -122,8 +122,11 @@ export class TelemetryRouter {
122
122
  }
123
123
  static createSlice() {
124
124
  return {
125
+ // Forward-only buses: they only re-emit routed/forwarded (already-stamped) events, never originate
126
+ // one, so the LogBus is explicitly zero-context — no `process.env` read, and no risk of a future
127
+ // origin-method call re-stamping a forwarded record.
125
128
  telemetry: new EventBus(),
126
- log: new LogBus(),
129
+ log: new LogBus(undefined, {}),
127
130
  };
128
131
  }
129
132
  }
@@ -1,4 +1,4 @@
1
- import type { EventBus } from '@salesforce/agentic-common';
1
+ import { BaseTelemetryEventBus, type EnvironmentAwareEvent } from '@salesforce/agentic-common';
2
2
  import type { McpServerErrorDetail, McpServerStatus, McpToolAnnotations } from '../mcp-config.js';
3
3
  import type { Decision, ToolPolicyRule } from './tools.js';
4
4
  import type { UsageMetadata } from './usage.js';
@@ -8,10 +8,13 @@ import type { UsageMetadata } from './usage.js';
8
8
  * The union is stable public API; consumers subscribe via `onTelemetry()` on `AgentManager`, `Agent`, or
9
9
  * `ChatSession`. Routing fields (`agentId`, `threadId`) are typed on the variants that carry them so
10
10
  * internal dispatch does not need to reach into a context bag.
11
+ *
12
+ * Every variant extends {@link EnvironmentAwareEvent}, so it carries `timestamp` plus the flat, optional
13
+ * environment-context identity fields (`instanceType`, `instanceId`, `orgId`, `userId`, `featureId`) — stamped
14
+ * by the origin {@link TelemetryBus}, never mutated on by downstream subscribers.
11
15
  */
12
- type Base<T extends string> = {
16
+ type Base<T extends string> = EnvironmentAwareEvent & {
13
17
  type: T;
14
- timestamp: Date;
15
18
  };
16
19
  export type AgentCreatedEvent = Base<'agent-created'> & {
17
20
  agentId: string;
@@ -194,5 +197,37 @@ export type LlmRetryEvent = Base<'llm-retry'> & {
194
197
  };
195
198
  export type TelemetryEvent = AgentCreatedEvent | AgentDestroyedEvent | SessionCreatedEvent | SessionDestroyedEvent | ChatStreamStartedEvent | ChatStreamCompletedEvent | ChatStreamErrorEvent | ToolExecutionStartedEvent | ToolExecutionCompletedEvent | ToolApprovalRequestedEvent | ToolApprovalResolvedEvent | ToolApprovalPolicyResolvedEvent | McpServerDiscoveryStartedEvent | McpServerDiscoveryCompletedEvent | McpServerDiscoveryFailedEvent | McpServerStatusChangedEvent | LlmRetryEvent;
196
199
  export type TelemetryEventCallback = (event: TelemetryEvent) => void;
197
- export type TelemetryBus = EventBus<TelemetryEvent>;
200
+ /**
201
+ * The domain fields a caller supplies to {@link TelemetryBus.emitTelemetry} — a {@link TelemetryEvent} minus the
202
+ * bus-owned base fields (`timestamp` and the environment-identity fields). Distributive over the union so each
203
+ * variant keeps its own domain fields. Subscribers still receive the full `TelemetryEvent`; the bus adds the
204
+ * base fields at the origin.
205
+ */
206
+ export type TelemetryEventInput = TelemetryEvent extends infer T ? T extends EnvironmentAwareEvent ? Omit<T, keyof EnvironmentAwareEvent> : never : never;
207
+ /**
208
+ * The SDK's telemetry bus: the reusable {@link BaseTelemetryEventBus} from `@salesforce/agentic-common`
209
+ * specialized to the SDK's {@link TelemetryEvent} union. Callers pass only a variant's domain fields to
210
+ * {@link emitTelemetry}; the bus builds the full event through the inherited `super`-chained `create*Event`
211
+ * builders, stamping `timestamp` (from the injected clock, or a pinned override) and the process environment
212
+ * context. Subscribers receive the full `TelemetryEvent`.
213
+ *
214
+ * Because the SDK's variants are concrete, `{ ...base, ...payload }` is a provable `TelemetryEvent` — no type
215
+ * assertion. Future harness-specific telemetry would follow the same pattern with its own union subclass.
216
+ */
217
+ export declare class TelemetryBus extends BaseTelemetryEventBus<TelemetryEvent> {
218
+ /**
219
+ * Emit a telemetry event from its domain fields (`type` plus variant fields). The bus builds the full event
220
+ * off the `super`-chained env-aware base — the payload already carries its own `type`, so this folds it onto
221
+ * the base rather than calling `createBaseTelemetryEvent` (which is for concrete `emit<Variant>` sugar that
222
+ * hardcodes the type). Stamps `timestamp` (injected clock, or a pinned override) and the environment
223
+ * context. Subscribers receive the full `TelemetryEvent`.
224
+ *
225
+ * Narrows the base-class `emitTelemetry(event: E)` to accept only domain fields ({@link TelemetryEventInput}
226
+ * omits the bus-owned base fields). A caller passing a full `TelemetryEvent` still type-checks — the extra
227
+ * base fields are just re-spread and harmlessly overwritten by the freshly-built base.
228
+ */
229
+ emitTelemetry(event: TelemetryEventInput, timestamp?: Date): void;
230
+ }
231
+ /** Constructs a {@link TelemetryBus}. Mirrors `new LogBus(...)` from `@salesforce/agentic-common`. */
232
+ export declare function createTelemetryBus(...args: ConstructorParameters<typeof TelemetryBus>): TelemetryBus;
198
233
  export {};
@@ -2,5 +2,35 @@
2
2
  * Copyright 2026, Salesforce, Inc. All rights reserved.
3
3
  * See LICENSE.txt for license terms.
4
4
  */
5
- export {};
5
+ import { BaseTelemetryEventBus } from '@salesforce/agentic-common';
6
+ /**
7
+ * The SDK's telemetry bus: the reusable {@link BaseTelemetryEventBus} from `@salesforce/agentic-common`
8
+ * specialized to the SDK's {@link TelemetryEvent} union. Callers pass only a variant's domain fields to
9
+ * {@link emitTelemetry}; the bus builds the full event through the inherited `super`-chained `create*Event`
10
+ * builders, stamping `timestamp` (from the injected clock, or a pinned override) and the process environment
11
+ * context. Subscribers receive the full `TelemetryEvent`.
12
+ *
13
+ * Because the SDK's variants are concrete, `{ ...base, ...payload }` is a provable `TelemetryEvent` — no type
14
+ * assertion. Future harness-specific telemetry would follow the same pattern with its own union subclass.
15
+ */
16
+ export class TelemetryBus extends BaseTelemetryEventBus {
17
+ /**
18
+ * Emit a telemetry event from its domain fields (`type` plus variant fields). The bus builds the full event
19
+ * off the `super`-chained env-aware base — the payload already carries its own `type`, so this folds it onto
20
+ * the base rather than calling `createBaseTelemetryEvent` (which is for concrete `emit<Variant>` sugar that
21
+ * hardcodes the type). Stamps `timestamp` (injected clock, or a pinned override) and the environment
22
+ * context. Subscribers receive the full `TelemetryEvent`.
23
+ *
24
+ * Narrows the base-class `emitTelemetry(event: E)` to accept only domain fields ({@link TelemetryEventInput}
25
+ * omits the bus-owned base fields). A caller passing a full `TelemetryEvent` still type-checks — the extra
26
+ * base fields are just re-spread and harmlessly overwritten by the freshly-built base.
27
+ */
28
+ emitTelemetry(event, timestamp) {
29
+ this.emit({ ...this.createEnvironmentAwareEvent(timestamp), ...event });
30
+ }
31
+ }
32
+ /** Constructs a {@link TelemetryBus}. Mirrors `new LogBus(...)` from `@salesforce/agentic-common`. */
33
+ export function createTelemetryBus(...args) {
34
+ return new TelemetryBus(...args);
35
+ }
6
36
  //# sourceMappingURL=telemetry-events.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/sfdx-agent-sdk",
3
- "version": "0.40.0",
3
+ "version": "0.42.0",
4
4
  "description": "Harness-agnostic agentic infrastructure for Salesforce developer experience tooling",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -43,17 +43,17 @@
43
43
  "LICENSE.txt"
44
44
  ],
45
45
  "dependencies": {
46
- "@salesforce/agentic-common": "0.13.0"
46
+ "@salesforce/agentic-common": "0.14.0"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@eslint/js": "^10.0.1",
50
- "@salesforce/sfdx-agent-harness-claude": "0.36.0",
51
- "@salesforce/sfdx-agent-harness-mastra": "0.39.0",
52
- "@salesforce/sfdx-agent-harness-openai": "0.5.0",
50
+ "@salesforce/sfdx-agent-harness-claude": "0.38.0",
51
+ "@salesforce/sfdx-agent-harness-mastra": "0.41.0",
52
+ "@salesforce/sfdx-agent-harness-openai": "0.7.0",
53
53
  "@types/node": "^22.20.0",
54
54
  "@vitest/coverage-istanbul": "^4.1.10",
55
55
  "@vitest/eslint-plugin": "^1.6.22",
56
- "eslint": "^10.7.0",
56
+ "eslint": "^10.8.0",
57
57
  "eslint-config-prettier": "^10.1.8",
58
58
  "eslint-import-resolver-typescript": "^4.4.5",
59
59
  "eslint-plugin-import": "^2.32.0",