@salesforce/sfdx-agent-sdk 0.41.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,14 @@
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
+
6
14
  ## [0.41.0] - 2026-08-03
7
15
 
8
16
  ### 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`
@@ -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);
@@ -191,19 +196,13 @@ export class DefaultAgentManager {
191
196
  this.agents.set(agentId, agent);
192
197
  const agentCreatedAt = this.clock.now();
193
198
  const modelName = runtime.modelConnectivityInfo.model.name;
194
- this.telemetryBus.emit({
195
- type: 'agent-created',
196
- timestamp: agentCreatedAt,
197
- agentId,
198
- projectRoot,
199
- modelName,
200
- });
201
- this.logBus.emit({
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
202
  level: 'info',
203
203
  message: 'Agent created',
204
- timestamp: agentCreatedAt,
205
204
  context: { event_type: 'agent-created', agentId, projectRoot, modelName },
206
- });
205
+ }, agentCreatedAt);
207
206
  if (options.rehydrateThreads) {
208
207
  // If thread enumeration or session attachment fails, the restore is a full failure
209
208
  // (the user would otherwise see an agent marked `ready` whose chat sessions return 404).
@@ -359,7 +358,10 @@ export async function createAgentManager(storageRootFolder, harnessFactory, opti
359
358
  `Update the SDK or harness package.`, AgentSDKErrorType.INCOMPATIBLE_HARNESS);
360
359
  }
361
360
  const agentConnectivityResolver = options?.connectivityResolver ?? new DefaultAgentConnectivityResolver();
362
- 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));
363
365
  }
364
366
  function isSupportedProtocolVersion(version) {
365
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)];
@@ -284,17 +289,13 @@ export class DefaultAgent {
284
289
  this.sessions.clear();
285
290
  await this.harness.destroyAgent(this.agentId);
286
291
  const agentDestroyedAt = this.clock.now();
287
- this.telemetryBus.emit({
288
- type: 'agent-destroyed',
289
- timestamp: agentDestroyedAt,
290
- agentId: this.agentId,
291
- });
292
- this.logBus.emit({
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({
293
295
  level: 'info',
294
296
  message: 'Agent destroyed',
295
- timestamp: agentDestroyedAt,
296
297
  context: { event_type: 'agent-destroyed', agentId: this.agentId },
297
- });
298
+ }, agentDestroyedAt);
298
299
  for (const unsub of this.inboundUnsubs)
299
300
  unsub();
300
301
  for (const unsub of this.parentUnsubs)
@@ -355,18 +356,13 @@ export class DefaultAgent {
355
356
  this.sessions.set(threadId, session);
356
357
  this.sessionSliceUnregisters.set(threadId, () => this.router.unregisterSession(threadId));
357
358
  const sessionCreatedAt = this.clock.now();
358
- this.telemetryBus.emit({
359
- type: 'session-created',
360
- timestamp: sessionCreatedAt,
361
- agentId: this.agentId,
362
- threadId,
363
- });
364
- this.logBus.emit({
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({
365
362
  level: 'info',
366
363
  message: 'Chat session created',
367
- timestamp: sessionCreatedAt,
368
364
  context: { event_type: 'session-created', agentId: this.agentId, threadId },
369
- });
365
+ }, sessionCreatedAt);
370
366
  return session;
371
367
  }
372
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,18 +234,17 @@ 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
- });
240
- this.logBus.emit({
244
+ }, finishedAt);
245
+ this.logBus.emitLog({
241
246
  level: 'error',
242
247
  message: 'Chat stream failed',
243
- timestamp: finishedAt,
244
248
  context: {
245
249
  event_type: 'chat-stream-error',
246
250
  agentId: this.agentId,
@@ -248,21 +252,19 @@ export class DefaultChatSession {
248
252
  durationMs,
249
253
  },
250
254
  error: lastError,
251
- });
255
+ }, finishedAt);
252
256
  }
253
257
  else {
254
- this.telemetryBus.emit({
258
+ this.telemetryBus.emitTelemetry({
255
259
  type: 'chat-stream-completed',
256
- timestamp: finishedAt,
257
260
  agentId: this.agentId,
258
261
  threadId: this.threadId,
259
262
  durationMs,
260
263
  usage: finishUsage,
261
- });
262
- this.logBus.emit({
264
+ }, finishedAt);
265
+ this.logBus.emitLog({
263
266
  level: 'info',
264
267
  message: 'Chat stream completed',
265
- timestamp: finishedAt,
266
268
  context: {
267
269
  event_type: 'chat-stream-completed',
268
270
  agentId: this.agentId,
@@ -270,7 +272,7 @@ export class DefaultChatSession {
270
272
  durationMs,
271
273
  ...(finishUsage !== undefined ? { usage: finishUsage } : {}),
272
274
  },
273
- });
275
+ }, finishedAt);
274
276
  }
275
277
  }
276
278
  /**
@@ -445,22 +447,17 @@ export class DefaultChatSession {
445
447
  return;
446
448
  }
447
449
  const sessionDestroyedAt = this.clock.now();
448
- this.telemetryBus.emit({
449
- type: 'session-destroyed',
450
- timestamp: sessionDestroyedAt,
451
- agentId: this.agentId,
452
- threadId: this.threadId,
453
- });
454
- this.logBus.emit({
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({
455
453
  level: 'info',
456
454
  message: 'Chat session destroyed',
457
- timestamp: sessionDestroyedAt,
458
455
  context: {
459
456
  event_type: 'session-destroyed',
460
457
  agentId: this.agentId,
461
458
  threadId: this.threadId,
462
459
  },
463
- });
460
+ }, sessionDestroyedAt);
464
461
  for (const unsub of this.inboundUnsubs)
465
462
  unsub();
466
463
  for (const unsub of this.parentUnsubs)
@@ -473,9 +470,8 @@ export class DefaultChatSession {
473
470
  emitToolApprovalResolved(toolCallId, approved, policyWritten) {
474
471
  // No Tier-1 logBus sibling: tool-approval-resolved fires per gated
475
472
  // tool call — interactive-UI signal, not on-call triage
476
- this.telemetryBus.emit({
473
+ this.telemetryBus.emitTelemetry({
477
474
  type: 'tool-approval-resolved',
478
- timestamp: this.clock.now(),
479
475
  agentId: this.agentId,
480
476
  threadId: this.threadId,
481
477
  toolCallId,
@@ -515,9 +511,8 @@ export class DefaultChatSession {
515
511
  this.toolStartMs.set(event.toolCallId, this.clock.now().getTime());
516
512
  // No Tier-1 logBus sibling: tool-execution-started pairs 1:1 with
517
513
  // tool-execution-completed; the completion (isError=true) is the triage signal
518
- this.telemetryBus.emit({
514
+ this.telemetryBus.emitTelemetry({
519
515
  type: 'tool-execution-started',
520
- timestamp: this.clock.now(),
521
516
  agentId: this.agentId,
522
517
  threadId: this.threadId,
523
518
  toolCallId: event.toolCallId,
@@ -534,9 +529,8 @@ export class DefaultChatSession {
534
529
  const completedAt = this.clock.now();
535
530
  const durationMs = completedAt.getTime() - start;
536
531
  const isError = event.isError === true;
537
- this.telemetryBus.emit({
532
+ this.telemetryBus.emitTelemetry({
538
533
  type: 'tool-execution-completed',
539
- timestamp: completedAt,
540
534
  agentId: this.agentId,
541
535
  threadId: this.threadId,
542
536
  toolCallId: event.toolCallId,
@@ -546,13 +540,12 @@ export class DefaultChatSession {
546
540
  ...(event.error ? { error: event.error } : {}),
547
541
  ...(event.annotations ? { annotations: event.annotations } : {}),
548
542
  ...(event.serverName ? { serverName: event.serverName } : {}),
549
- });
550
- // Tier-1 logBus sibling only when isError=true.
543
+ }, completedAt);
544
+ // Tier-1 logBus sibling only when isError=true; share the tick with the telemetry event.
551
545
  if (isError) {
552
- this.logBus.emit({
546
+ this.logBus.emitLog({
553
547
  level: 'error',
554
548
  message: 'Tool execution failed',
555
- timestamp: completedAt,
556
549
  context: {
557
550
  event_type: 'tool-execution-completed',
558
551
  agentId: this.agentId,
@@ -565,7 +558,7 @@ export class DefaultChatSession {
565
558
  ...(event.serverName ? { serverName: event.serverName } : {}),
566
559
  },
567
560
  ...(event.error ? { error: event.error } : {}),
568
- });
561
+ }, completedAt);
569
562
  }
570
563
  }
571
564
  else if (event.type === 'tool-approval-request') {
@@ -585,9 +578,8 @@ export class DefaultChatSession {
585
578
  toolName: event.bareToolName ?? event.toolCall.toolName,
586
579
  ...(event.serverName ? { serverName: event.serverName } : {}),
587
580
  });
588
- this.telemetryBus.emit({
581
+ this.telemetryBus.emitTelemetry({
589
582
  type: 'tool-approval-requested',
590
- timestamp: this.clock.now(),
591
583
  agentId: this.agentId,
592
584
  threadId: this.threadId,
593
585
  toolCallId: event.toolCall.toolCallId,
@@ -636,14 +628,9 @@ export class DefaultChatSession {
636
628
  */
637
629
  emitChatStreamStarted(trigger) {
638
630
  const startedAt = this.clock.now();
639
- // chat-stream-started pairs 1:1 with chat-stream-completed, which already carries outcomes
640
- this.telemetryBus.emit({
641
- type: 'chat-stream-started',
642
- timestamp: startedAt,
643
- agentId: this.agentId,
644
- threadId: this.threadId,
645
- trigger,
646
- });
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);
647
634
  return startedAt;
648
635
  }
649
636
  /**
@@ -661,18 +648,11 @@ export class DefaultChatSession {
661
648
  this.chatEventBus.emit({ type: 'finish', finishReason: 'error' });
662
649
  const finishedAt = this.clock.now();
663
650
  const durationMs = finishedAt.getTime() - startedAt.getTime();
664
- this.telemetryBus.emit({
665
- type: 'chat-stream-error',
666
- timestamp: finishedAt,
667
- agentId: this.agentId,
668
- threadId: this.threadId,
669
- durationMs,
670
- error,
671
- });
672
- this.logBus.emit({
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({
673
654
  level: 'error',
674
655
  message: 'Chat stream failed',
675
- timestamp: finishedAt,
676
656
  context: {
677
657
  event_type: 'chat-stream-error',
678
658
  agentId: this.agentId,
@@ -680,7 +660,7 @@ export class DefaultChatSession {
680
660
  durationMs,
681
661
  },
682
662
  error,
683
- });
663
+ }, finishedAt);
684
664
  }
685
665
  /**
686
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.41.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.37.0",
51
- "@salesforce/sfdx-agent-harness-mastra": "0.40.0",
52
- "@salesforce/sfdx-agent-harness-openai": "0.6.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",