@smithers-orchestrator/observability 0.24.0 → 0.25.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smithers-orchestrator/observability",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "Concrete Smithers metrics, logging, tracing, and observability integrations",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -35,7 +35,7 @@
35
35
  "@effect/platform": "^0.96.0",
36
36
  "@effect/platform-bun": "^0.89.0",
37
37
  "effect": "^3.21.1",
38
- "@smithers-orchestrator/agents": "0.24.0"
38
+ "@smithers-orchestrator/agents": "0.25.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/bun": "latest",
@@ -7,4 +7,5 @@ export type ResolvedSmithersObservabilityOptions = {
7
7
  readonly serviceName: string;
8
8
  readonly logFormat: SmithersLogFormat;
9
9
  readonly logLevel: LogLevel.LogLevel;
10
+ readonly installLogger: boolean;
10
11
  };
@@ -229,6 +229,11 @@ export type SmithersEvent =
229
229
  runId: string;
230
230
  frameNo: number;
231
231
  xmlHash: string;
232
+ trigger?: {
233
+ reason: string;
234
+ nodeId?: string;
235
+ iteration?: number;
236
+ };
232
237
  timestampMs: number;
233
238
  }
234
239
  | {
@@ -7,4 +7,5 @@ export type SmithersObservabilityOptions = {
7
7
  readonly serviceName?: string;
8
8
  readonly logFormat?: SmithersLogFormat;
9
9
  readonly logLevel?: LogLevel.LogLevel | string;
10
+ readonly installLogger?: boolean;
10
11
  };
@@ -5,6 +5,19 @@ import { mergeCorrelationContext } from "./mergeCorrelationContext.js";
5
5
  /** @typedef {import("./CorrelationPatch.ts").CorrelationPatch} CorrelationPatch */
6
6
 
7
7
  /**
8
+ * Bridge the Effect-tracked correlation context onto the imperative
9
+ * AsyncLocalStorage store so plain (non-Effect) `getCurrentCorrelationContext()`
10
+ * reads — e.g. from the imperative logger — see the active run/node correlation
11
+ * while the effect executes.
12
+ *
13
+ * IMPORTANT: run the resulting effect with `Effect.runPromise`/`runFork`, never
14
+ * `Effect.runSync`. The acquire step calls `AsyncLocalStorage.enterWith()`, which
15
+ * mutates the *caller's* async context. Under `runSync` the caller is whatever
16
+ * synchronous context invoked it (e.g. a test-runner's root context); enabling
17
+ * ALS async-hooks there leaks into every subsequent timer/promise on that
18
+ * context. `runPromise`/`runFork` execute on an ephemeral fiber context, keeping
19
+ * the enterWith scoped to that fiber.
20
+ *
8
21
  * @template A, E, R
9
22
  * @param {Effect.Effect<A, E, R>} effect
10
23
  * @param {CorrelationPatch} patch
@@ -1,16 +1,7 @@
1
- import { Context, Effect, Layer } from "effect";
2
- import { renderPrometheusSamples, toPrometheusMetricName, } from "./_corePrometheus.js";
3
- /** @typedef {import("./MetricName.ts").MetricName} MetricName */
1
+ import { Context } from "effect";
2
+ import { toPrometheusMetricName } from "./_corePrometheus.js";
4
3
  /** @typedef {import("./SmithersMetricType.ts").SmithersMetricType} SmithersMetricType */
5
- /** @typedef {import("./SmithersMetricUnit.ts").SmithersMetricUnit} SmithersMetricUnit */
6
- /** @typedef {import("./_corePrometheusShape.ts").MetricLabels} MetricLabels */
7
- /** @typedef {import("./_corePrometheusShape.ts").PrometheusSample} PrometheusSample */
8
- /** @typedef {import("./_coreMetricsShape.ts").CounterEntry} CounterEntry */
9
- /** @typedef {import("./_coreMetricsShape.ts").GaugeEntry} GaugeEntry */
10
- /** @typedef {import("./_coreMetricsShape.ts").HistogramEntry} HistogramEntry */
11
- /** @typedef {import("./_coreMetricsShape.ts").MetricsSnapshot} MetricsSnapshot */
12
4
  /** @typedef {import("./_coreMetricsShape.ts").MetricsServiceShape} MetricsServiceShape */
13
- /** @typedef {import("./_coreMetricsShape.ts").SmithersMetricEvent} SmithersMetricEvent */
14
5
  /** @typedef {import("./SmithersMetricDefinition.ts").SmithersMetricDefinition} SmithersMetricDefinition */
15
6
 
16
7
  /**
@@ -204,307 +195,3 @@ export const smithersMetrics = Object.freeze(Object.fromEntries(smithersMetricCa
204
195
  const _MetricsServiceBase = /** @type {Context.TagClass<MetricsService, "MetricsService", MetricsServiceShape>} */ (/** @type {unknown} */ (Context.Tag("MetricsService")()));
205
196
  export class MetricsService extends _MetricsServiceBase {
206
197
  }
207
- const DEFAULT_HISTOGRAM_BUCKETS = [
208
- 1,
209
- 5,
210
- 10,
211
- 25,
212
- 50,
213
- 100,
214
- 250,
215
- 500,
216
- 1_000,
217
- 2_500,
218
- 5_000,
219
- 10_000,
220
- 30_000,
221
- ];
222
- /**
223
- * @param {MetricLabels} [labels]
224
- * @returns {string}
225
- */
226
- function labelsKey(labels = {}) {
227
- return JSON.stringify(Object.entries(labels).sort(([left], [right]) => left.localeCompare(right)));
228
- }
229
- /**
230
- * @param {string} name
231
- * @param {MetricLabels} [labels]
232
- * @returns {string}
233
- */
234
- function metricKey(name, labels) {
235
- return `${name}|${labelsKey(labels)}`;
236
- }
237
- /**
238
- * @param {MetricLabels} [labels]
239
- * @returns {MetricLabels}
240
- */
241
- function cloneLabels(labels = {}) {
242
- return Object.freeze({ ...labels });
243
- }
244
- /**
245
- * @returns {Context.Tag.Service<MetricsService>}
246
- */
247
- export function makeInMemoryMetricsService() {
248
- const registry = new Map();
249
- const processStartMs = Date.now();
250
- const asyncExternalWaitCounts = {
251
- approval: 0,
252
- event: 0,
253
- };
254
- /**
255
- * @param {string} name
256
- * @param {MetricLabels} [labels]
257
- * @returns {CounterEntry}
258
- */
259
- function upsertCounter(name, labels) {
260
- const key = metricKey(name, labels);
261
- const existing = registry.get(key);
262
- if (existing?.type === "counter")
263
- return existing;
264
- const created = {
265
- type: "counter",
266
- value: 0,
267
- labels: cloneLabels(labels),
268
- };
269
- registry.set(key, created);
270
- return created;
271
- }
272
- /**
273
- * @param {string} name
274
- * @param {MetricLabels} [labels]
275
- * @returns {GaugeEntry}
276
- */
277
- function upsertGauge(name, labels) {
278
- const key = metricKey(name, labels);
279
- const existing = registry.get(key);
280
- if (existing?.type === "gauge")
281
- return existing;
282
- const created = {
283
- type: "gauge",
284
- value: 0,
285
- labels: cloneLabels(labels),
286
- };
287
- registry.set(key, created);
288
- return created;
289
- }
290
- /**
291
- * @param {string} name
292
- * @param {MetricLabels} [labels]
293
- * @returns {HistogramEntry}
294
- */
295
- function upsertHistogram(name, labels) {
296
- const key = metricKey(name, labels);
297
- const existing = registry.get(key);
298
- if (existing?.type === "histogram")
299
- return existing;
300
- const created = {
301
- type: "histogram",
302
- sum: 0,
303
- count: 0,
304
- labels: cloneLabels(labels),
305
- buckets: new Map(DEFAULT_HISTOGRAM_BUCKETS.map((bucket) => [bucket, 0])),
306
- };
307
- registry.set(key, created);
308
- return created;
309
- }
310
- /**
311
- * @returns {PrometheusSample[]}
312
- */
313
- function samples() {
314
- return [...registry.entries()].map(([key, entry]) => {
315
- const name = key.slice(0, key.indexOf("|"));
316
- if (entry.type === "histogram") {
317
- return {
318
- name,
319
- type: entry.type,
320
- labels: entry.labels,
321
- buckets: new Map(entry.buckets),
322
- sum: entry.sum,
323
- count: entry.count,
324
- };
325
- }
326
- return {
327
- name,
328
- type: entry.type,
329
- labels: entry.labels,
330
- value: entry.value,
331
- };
332
- });
333
- }
334
- const service = {
335
- increment: (name, labels) => service.incrementBy(name, 1, labels),
336
- incrementBy: (name, value, labels) => Effect.sync(() => {
337
- const key = metricKey(name, labels);
338
- const existing = registry.get(key);
339
- const definition = smithersMetricCatalogByName.get(name);
340
- if (existing?.type === "gauge" || definition?.type === "gauge") {
341
- upsertGauge(name, labels).value += value;
342
- return;
343
- }
344
- upsertCounter(name, labels).value += value;
345
- }),
346
- gauge: (name, value, labels) => Effect.sync(() => {
347
- upsertGauge(name, labels).value = value;
348
- }),
349
- histogram: (name, value, labels) => Effect.sync(() => {
350
- const entry = upsertHistogram(name, labels);
351
- entry.count += 1;
352
- entry.sum += value;
353
- for (const boundary of DEFAULT_HISTOGRAM_BUCKETS) {
354
- if (value <= boundary) {
355
- entry.buckets.set(boundary, (entry.buckets.get(boundary) ?? 0) + 1);
356
- }
357
- }
358
- }),
359
- recordEvent: (event) => {
360
- const eventType = String(event.type);
361
- const countEvent = service.increment("smithers.events.emitted_total", {
362
- type: eventType,
363
- });
364
- switch (event.type) {
365
- case "RunStarted":
366
- return Effect.all([
367
- countEvent,
368
- service.increment("smithers.runs.total"),
369
- service.incrementBy("smithers.runs.active", 1),
370
- ], { discard: true });
371
- case "RunFinished":
372
- return Effect.all([
373
- countEvent,
374
- service.incrementBy("smithers.runs.active", -1),
375
- service.increment("smithers.runs.finished_total"),
376
- ], { discard: true });
377
- case "RunFailed":
378
- return Effect.all([
379
- countEvent,
380
- service.incrementBy("smithers.runs.active", -1),
381
- service.increment("smithers.runs.failed_total"),
382
- service.increment("smithers.errors.total"),
383
- ], { discard: true });
384
- case "RunCancelled":
385
- return Effect.all([
386
- countEvent,
387
- service.incrementBy("smithers.runs.active", -1),
388
- service.increment("smithers.runs.cancelled_total"),
389
- ], { discard: true });
390
- case "RunContinuedAsNew":
391
- return Effect.all([countEvent, service.increment("smithers.runs.continued_total")], { discard: true });
392
- case "NodeStarted":
393
- return Effect.all([
394
- countEvent,
395
- service.increment("smithers.nodes.started"),
396
- service.incrementBy("smithers.nodes.active", 1),
397
- ], { discard: true });
398
- case "NodeFinished":
399
- return Effect.all([
400
- countEvent,
401
- service.increment("smithers.nodes.finished"),
402
- service.incrementBy("smithers.nodes.active", -1),
403
- typeof event.durationMs === "number"
404
- ? service.histogram("smithers.node.duration_ms", event.durationMs)
405
- : Effect.void,
406
- ], { discard: true });
407
- case "NodeFailed":
408
- return Effect.all([
409
- countEvent,
410
- service.increment("smithers.nodes.failed"),
411
- service.increment("smithers.errors.total"),
412
- service.incrementBy("smithers.nodes.active", -1),
413
- ], { discard: true });
414
- case "CacheHit":
415
- return Effect.all([countEvent, service.increment("smithers.cache.hits")], { discard: true });
416
- case "CacheMiss":
417
- return Effect.all([countEvent, service.increment("smithers.cache.misses")], { discard: true });
418
- case "ApprovalRequested":
419
- return Effect.all([
420
- countEvent,
421
- service.increment("smithers.approvals.requested"),
422
- service.incrementBy("smithers.approval.pending", 1),
423
- ], { discard: true });
424
- case "ApprovalResolved": {
425
- const approved = event.approved === true || event.status === "approved";
426
- return Effect.all([
427
- countEvent,
428
- service.increment(approved
429
- ? "smithers.approvals.granted"
430
- : "smithers.approvals.denied"),
431
- service.incrementBy("smithers.approval.pending", -1),
432
- ], { discard: true });
433
- }
434
- case "TimerCreated":
435
- return Effect.all([
436
- countEvent,
437
- service.increment("smithers.timers.created"),
438
- service.incrementBy("smithers.timers.pending", 1),
439
- ], { discard: true });
440
- case "TimerFired":
441
- return Effect.all([
442
- countEvent,
443
- service.increment("smithers.timers.fired"),
444
- service.incrementBy("smithers.timers.pending", -1),
445
- ], { discard: true });
446
- case "TaskHeartbeat":
447
- return Effect.all([countEvent, service.increment("smithers.heartbeats.total")], { discard: true });
448
- case "TaskHeartbeatTimeout":
449
- return Effect.all([
450
- countEvent,
451
- service.increment("smithers.heartbeats.timeout_total"),
452
- service.increment("smithers.errors.total"),
453
- ], { discard: true });
454
- case "TokenUsageReported": {
455
- const effects = [countEvent];
456
- const labels = {
457
- ...(typeof event.agent === "string" ? { agent: event.agent } : {}),
458
- ...(typeof event.model === "string" ? { model: event.model } : {}),
459
- };
460
- /**
461
- * @param {string} name
462
- * @param {unknown} value
463
- */
464
- const push = (name, value) => {
465
- if (typeof value === "number" && Number.isFinite(value) && value > 0) {
466
- effects.push(service.incrementBy(name, value, labels));
467
- }
468
- };
469
- push("smithers.tokens.input_total", event.inputTokens);
470
- push("smithers.tokens.output_total", event.outputTokens);
471
- push("smithers.tokens.cache_read_total", event.cacheReadTokens);
472
- push("smithers.tokens.cache_write_total", event.cacheWriteTokens);
473
- push("smithers.tokens.reasoning_total", event.reasoningTokens);
474
- return Effect.all(effects, { discard: true });
475
- }
476
- default:
477
- return countEvent;
478
- }
479
- },
480
- updateProcessMetrics: () => Effect.sync(() => {
481
- const uptimeS = (Date.now() - processStartMs) / 1000;
482
- const mem = process.memoryUsage();
483
- upsertGauge("smithers.process.uptime_seconds").value = uptimeS;
484
- upsertGauge("smithers.process.memory_rss_bytes").value = mem.rss;
485
- upsertGauge("smithers.process.heap_used_bytes").value = mem.heapUsed;
486
- }),
487
- updateAsyncExternalWaitPending: (kind, delta) => Effect.sync(() => {
488
- asyncExternalWaitCounts[kind] = Math.max(0, asyncExternalWaitCounts[kind] + delta);
489
- upsertGauge("smithers.external_wait.async_pending", { kind }).value =
490
- asyncExternalWaitCounts[kind];
491
- }),
492
- renderPrometheus: () => Effect.sync(() => renderPrometheusSamples(samples())),
493
- snapshot: () => Effect.sync(() => new Map(registry)),
494
- };
495
- return service;
496
- }
497
- /** @type {Layer.Layer<MetricsService, never, never>} */
498
- export const MetricsServiceLive = Layer.sync(MetricsService, makeInMemoryMetricsService);
499
- /** @type {Layer.Layer<MetricsService, never, never>} */
500
- export const MetricsServiceNoop = Layer.succeed(MetricsService, {
501
- increment: () => Effect.void,
502
- incrementBy: () => Effect.void,
503
- gauge: () => Effect.void,
504
- histogram: () => Effect.void,
505
- recordEvent: () => Effect.void,
506
- updateProcessMetrics: () => Effect.void,
507
- updateAsyncExternalWaitPending: () => Effect.void,
508
- renderPrometheus: () => Effect.succeed(""),
509
- snapshot: () => Effect.succeed(new Map()),
510
- });
@@ -1,5 +1,6 @@
1
1
  import { Context, Effect, Layer } from "effect";
2
2
  import { correlationContextToLogAnnotations, getCurrentCorrelationContext, withCorrelationContext, } from "./_coreCorrelation/index.js";
3
+ import { smithersSpanAttributeAliases } from "./_smithersSpanAttributeAliases.js";
3
4
  /** @typedef {import("./SmithersLogFormat.ts").SmithersLogFormat} SmithersLogFormat */
4
5
  /** @typedef {import("./_coreTracingShape.ts").SmithersSpanAttributesInput} SmithersSpanAttributesInput */
5
6
  /** @typedef {import("./_coreTracingShape.ts").TracingServiceShape} TracingServiceShape */
@@ -30,25 +31,7 @@ export function getCurrentSmithersTraceAnnotations() {
30
31
  * @returns {Record<string, unknown>}
31
32
  */
32
33
  export function makeSmithersSpanAttributes(attributes = {}) {
33
- const aliases = {
34
- runId: "smithers.run_id",
35
- run_id: "smithers.run_id",
36
- workflowName: "smithers.workflow_name",
37
- workflow_name: "smithers.workflow_name",
38
- nodeId: "smithers.node_id",
39
- node_id: "smithers.node_id",
40
- iteration: "smithers.iteration",
41
- attempt: "smithers.attempt",
42
- nodeLabel: "smithers.node_label",
43
- node_label: "smithers.node_label",
44
- toolName: "smithers.tool_name",
45
- tool_name: "smithers.tool_name",
46
- agent: "smithers.agent",
47
- model: "smithers.model",
48
- status: "smithers.status",
49
- waitReason: "smithers.wait_reason",
50
- wait_reason: "smithers.wait_reason",
51
- };
34
+ const aliases = smithersSpanAttributeAliases;
52
35
  const result = {};
53
36
  for (const [key, value] of Object.entries(attributes)) {
54
37
  if (value !== undefined) {
@@ -114,18 +97,22 @@ export function annotateSmithersTrace(attributes = {}) {
114
97
  */
115
98
  export function withSmithersSpan(name, effect, attributes) {
116
99
  const spanAttributes = makeSmithersSpanAttributes(attributes);
117
- const annotations = correlationContextToLogAnnotations(getCurrentCorrelationContext());
118
- let program = effect;
119
- if (Object.keys(spanAttributes).length > 0) {
120
- program = program.pipe(Effect.annotateSpans(spanAttributes));
121
- }
122
- if (hasAttributes(attributes)) {
123
- program = program.pipe(Effect.annotateLogs(attributes));
124
- }
125
- if (annotations) {
126
- program = program.pipe(Effect.annotateLogs(annotations));
127
- }
128
- return program.pipe(Effect.withLogSpan(name), Effect.withSpan(inferSmithersSpanName(name, attributes)));
100
+ return Effect.suspend(() => {
101
+ const annotations = correlationContextToLogAnnotations(getCurrentCorrelationContext());
102
+ let program = effect;
103
+ if (Object.keys(spanAttributes).length > 0) {
104
+ program = program.pipe(Effect.annotateSpans(spanAttributes));
105
+ }
106
+ if (hasAttributes(attributes)) {
107
+ program = program.pipe(Effect.annotateLogs(attributes));
108
+ }
109
+ if (annotations) {
110
+ program = program.pipe(Effect.annotateLogs(annotations));
111
+ }
112
+ return program.pipe(Effect.withLogSpan(name), Effect.withSpan(inferSmithersSpanName(name, attributes), {
113
+ attributes: spanAttributes,
114
+ }));
115
+ });
129
116
  }
130
117
  /** @type {Layer.Layer<TracingService, never, never>} */
131
118
  export const TracingServiceLive = Layer.succeed(TracingService, {
@@ -27,7 +27,8 @@ const rules = [
27
27
  {
28
28
  id: "secret-ish",
29
29
  pattern: /\b(?:api[_-]?key|token|secret|password)=([^\s"']+)/gi,
30
- replace: "",
30
+ // No `replace` field: redactValue special-cases this rule by id and
31
+ // rewrites the captured value itself, so a top-level replace is never read.
31
32
  },
32
33
  ];
33
34
 
@@ -45,5 +45,11 @@ function makeService(options) {
45
45
  */
46
46
  export function createSmithersObservabilityLayer(options = {}) {
47
47
  const resolved = resolveSmithersObservabilityOptions(options);
48
- return Layer.mergeAll(BunContext.layer, Logger.replace(Logger.defaultLogger, resolveLogger(resolved.logFormat)), Logger.minimumLogLevel(resolved.logLevel), createSmithersOtelLayer(resolved), MetricsServiceLive, TracingServiceLive, Layer.succeed(SmithersObservability, makeService(resolved)));
48
+ const loggerLayers = resolved.installLogger
49
+ ? [
50
+ Logger.replace(Logger.defaultLogger, resolveLogger(resolved.logFormat)),
51
+ Logger.minimumLogLevel(resolved.logLevel),
52
+ ]
53
+ : [];
54
+ return Layer.mergeAll(BunContext.layer, ...loggerLayers, createSmithersOtelLayer(resolved), MetricsServiceLive, TracingServiceLive, Layer.succeed(SmithersObservability, makeService(resolved)));
49
55
  }
package/src/index.d.ts CHANGED
@@ -13,6 +13,7 @@ type ResolvedSmithersObservabilityOptions$2 = {
13
13
  readonly serviceName: string;
14
14
  readonly logFormat: SmithersLogFormat$1;
15
15
  readonly logLevel: LogLevel.LogLevel;
16
+ readonly installLogger: boolean;
16
17
  };
17
18
 
18
19
  type SmithersObservabilityService$1 = {
@@ -27,6 +28,7 @@ type SmithersObservabilityOptions$4 = {
27
28
  readonly serviceName?: string;
28
29
  readonly logFormat?: SmithersLogFormat$1;
29
30
  readonly logLevel?: LogLevel.LogLevel | string;
31
+ readonly installLogger?: boolean;
30
32
  };
31
33
 
32
34
  type MetricLabels$1 = Readonly<Record<string, string | number | boolean>>;
@@ -47,6 +49,89 @@ type SmithersMetricDefinition$2 = {
47
49
  readonly boundaries?: readonly number[];
48
50
  };
49
51
 
52
+ type AgentFamily = "pi" | "codex" | "claude-code" | "antigravity" | "gemini" | "kimi" | "openai" | "anthropic" | "amp" | "forge" | "unknown";
53
+ type AgentCaptureMode = "sdk-events" | "rpc-events" | "cli-json-stream" | "cli-json" | "cli-text" | "artifact-import";
54
+ type TraceCompleteness = "full-observed" | "partial-observed" | "final-only" | "capture-failed";
55
+ type CanonicalAgentTraceEventKind = "session.start" | "session.end" | "turn.start" | "turn.end" | "message.start" | "message.update" | "message.end" | "assistant.text.delta" | "assistant.thinking.delta" | "assistant.message.final" | "tool.execution.start" | "tool.execution.update" | "tool.execution.end" | "tool.result" | "retry.start" | "retry.end" | "compaction.start" | "compaction.end" | "stderr" | "stdout" | "usage" | "capture.warning" | "capture.error" | "artifact.created";
56
+ type CanonicalAgentTraceEventPhase = "agent" | "turn" | "message" | "tool" | "session" | "capture" | "artifact";
57
+ type CanonicalAgentTraceEvent = {
58
+ traceVersion: "1";
59
+ runId: string;
60
+ workflowPath?: string;
61
+ workflowHash?: string;
62
+ nodeId: string;
63
+ iteration: number;
64
+ attempt: number;
65
+ timestampMs: number;
66
+ event: {
67
+ sequence: number;
68
+ kind: CanonicalAgentTraceEventKind;
69
+ phase: CanonicalAgentTraceEventPhase;
70
+ };
71
+ source: {
72
+ agentFamily: AgentFamily;
73
+ captureMode: AgentCaptureMode;
74
+ rawType?: string;
75
+ rawEventId?: string;
76
+ observed: boolean;
77
+ };
78
+ traceCompleteness: TraceCompleteness;
79
+ payload: Record<string, unknown> | null;
80
+ raw: unknown;
81
+ redaction: {
82
+ applied: boolean;
83
+ ruleIds: string[];
84
+ };
85
+ annotations: Record<string, string | number | boolean>;
86
+ };
87
+ type AgentTraceSummary = {
88
+ traceVersion: "1";
89
+ runId: string;
90
+ workflowPath?: string;
91
+ workflowHash?: string;
92
+ nodeId: string;
93
+ iteration: number;
94
+ attempt: number;
95
+ traceStartedAtMs: number;
96
+ traceFinishedAtMs: number;
97
+ agentFamily: AgentFamily;
98
+ agentId?: string;
99
+ model?: string;
100
+ captureMode: AgentCaptureMode;
101
+ traceCompleteness: TraceCompleteness;
102
+ unsupportedEventKinds: CanonicalAgentTraceEventKind[];
103
+ missingExpectedEventKinds: CanonicalAgentTraceEventKind[];
104
+ rawArtifactRefs: string[];
105
+ };
106
+ type AgentSessionTranscriptEvent = {
107
+ transcriptVersion: "1";
108
+ runId: string;
109
+ workflowPath?: string;
110
+ workflowHash?: string;
111
+ nodeId: string;
112
+ iteration: number;
113
+ attempt: number;
114
+ timestampMs: number;
115
+ event: {
116
+ sequence: number;
117
+ rowType: string;
118
+ };
119
+ source: {
120
+ agentFamily: AgentFamily;
121
+ captureMode: AgentCaptureMode;
122
+ ingestSource: "live" | "artifact";
123
+ observedLive: boolean;
124
+ providerSessionId?: string;
125
+ providerThreadId?: string;
126
+ };
127
+ raw: unknown;
128
+ redaction: {
129
+ applied: boolean;
130
+ ruleIds: string[];
131
+ };
132
+ annotations: Record<string, string | number | boolean>;
133
+ };
134
+
50
135
  type RunStatus = "running" | "waiting-approval" | "waiting-event" | "waiting-timer" | "finished" | "continued" | "failed" | "cancelled";
51
136
  type RunState = "running" | "waiting-approval" | "waiting-event" | "waiting-timer" | "recovering" | "stale" | "orphaned" | "failed" | "cancelled" | "succeeded" | "unknown";
52
137
  type AgentCliActionKind = "turn" | "command" | "tool" | "file_change" | "web_search" | "todo_list" | "reasoning" | "warning" | "note";
@@ -230,6 +315,11 @@ type SmithersEvent$2 = {
230
315
  runId: string;
231
316
  frameNo: number;
232
317
  xmlHash: string;
318
+ trigger?: {
319
+ reason: string;
320
+ nodeId?: string;
321
+ iteration?: number;
322
+ };
233
323
  timestampMs: number;
234
324
  } | {
235
325
  type: "NodePending";
@@ -565,6 +655,30 @@ type SmithersEvent$2 = {
565
655
  runId: string;
566
656
  timerId: string;
567
657
  timestampMs: number;
658
+ } | {
659
+ type: "AgentTraceEvent";
660
+ runId: string;
661
+ nodeId: string;
662
+ iteration: number;
663
+ attempt: number;
664
+ trace: CanonicalAgentTraceEvent;
665
+ timestampMs: number;
666
+ } | {
667
+ type: "AgentTraceSummary";
668
+ runId: string;
669
+ nodeId: string;
670
+ iteration: number;
671
+ attempt: number;
672
+ summary: AgentTraceSummary;
673
+ timestampMs: number;
674
+ } | {
675
+ type: "AgentSessionEvent";
676
+ runId: string;
677
+ nodeId: string;
678
+ iteration: number;
679
+ attempt: number;
680
+ transcript: AgentSessionTranscriptEvent;
681
+ timestampMs: number;
568
682
  };
569
683
 
570
684
  type MetricName = string;
@@ -995,11 +1109,40 @@ declare function correlationContextToLogAnnotations(context?: CorrelationContext
995
1109
  type CorrelationContext$1 = CorrelationContext$5;
996
1110
 
997
1111
  /**
1112
+ * Temporary compatibility shim for legacy, non-Effect callers.
1113
+ *
1114
+ * Unlike the FiberRef-based core implementation
1115
+ * ({@link import("./_coreCorrelation/updateCurrentCorrelationContext.js").updateCurrentCorrelationContext}),
1116
+ * which returns an Effect and sets a fresh merged context on the
1117
+ * `correlationContextFiberRef`, this shim runs synchronously and applies the
1118
+ * patch by **mutating the current context object in place** via
1119
+ * `Object.assign(current, next)`. Any references already holding the current
1120
+ * context object will observe the mutation. This in-place semantics is
1121
+ * intentional and exists only to preserve behavior for callers that captured a
1122
+ * context reference before the Effect-based API existed.
1123
+ *
1124
+ * If there is no current context, the patch is a no-op (nothing is created).
1125
+ *
1126
+ * @deprecated Prefer the Effect-returning
1127
+ * `updateCurrentCorrelationContext` from
1128
+ * `@smithers-orchestrator/observability` (the `_coreCorrelation` version),
1129
+ * which does not mutate shared state. This shim will be removed once legacy
1130
+ * callers migrate.
1131
+ *
998
1132
  * @param {CorrelationPatch} patch
1133
+ * @returns {void}
999
1134
  */
1000
1135
  declare function updateCurrentCorrelationContext(patch: CorrelationPatch$1): void;
1001
1136
  type CorrelationPatch$1 = CorrelationPatch$5;
1002
1137
 
1138
+ /**
1139
+ * Install the Effect runtime used by fire-and-forget observability logs.
1140
+ * Returns a restore function so tests and embedded hosts can scope overrides.
1141
+ *
1142
+ * @param {SmithersLogRunner | null} runner
1143
+ * @returns {() => void}
1144
+ */
1145
+ declare function setSmithersLogRunner(runner: SmithersLogRunner | null): () => void;
1003
1146
  /**
1004
1147
  * @param {string} message
1005
1148
  * @param {LogAnnotations} [annotations]
@@ -1024,6 +1167,10 @@ declare function logWarning(message: string, annotations?: LogAnnotations, span?
1024
1167
  * @param {string} [span]
1025
1168
  */
1026
1169
  declare function logError(message: string, annotations?: LogAnnotations, span?: string): void;
1170
+ type SmithersLogRunner = {
1171
+ runFork: (effect: Effect.Effect<void, never, never>) => unknown;
1172
+ runPromise: (effect: Effect.Effect<void, never, never>) => Promise<void>;
1173
+ };
1027
1174
  type LogAnnotations = Record<string, unknown> | undefined;
1028
1175
 
1029
1176
  type CorrelationContext = CorrelationContext$5;
@@ -1039,4 +1186,4 @@ type SmithersMetricDefinition = SmithersMetricDefinition$2;
1039
1186
  type SmithersObservabilityOptions = SmithersObservabilityOptions$4;
1040
1187
  type SmithersObservabilityService = SmithersObservabilityService$1;
1041
1188
 
1042
- export { type CorrelationContext, CorrelationContextLive, type CorrelationContextPatch, CorrelationContextService, type CorrelationPatch, type MetricLabels, MetricsService, MetricsServiceLive, type MetricsServiceShape, type MetricsSnapshot, type ResolvedSmithersObservabilityOptions, type SmithersEvent, type SmithersLogFormat, type SmithersMetricDefinition, SmithersObservability, type SmithersObservabilityOptions, type SmithersObservabilityService, TracingService, TracingServiceLive, activeNodes, activeRuns, annotateSmithersTrace, approvalPending, approvalWaitDuration, approvalsDenied, approvalsGranted, approvalsRequested, attemptDuration, cacheHits, cacheMisses, correlationContextFiberRef, correlationContextToLogAnnotations, createSmithersObservabilityLayer, createSmithersOtelLayer, createSmithersRuntimeLayer, dbQueryDuration, dbRetries, dbTransactionDuration, dbTransactionRetries, dbTransactionRollbacks, errorsTotal, eventsEmittedTotal, externalWaitAsyncPending, getCurrentCorrelationContext, getCurrentCorrelationContextEffect, getCurrentSmithersTraceAnnotations, getCurrentSmithersTraceSpan, hotReloadDuration, hotReloadFailures, hotReloads, httpRequestDuration, httpRequests, logDebug, logError, logInfo, logWarning, makeSmithersSpanAttributes, mergeCorrelationContext, metricsServiceAdapter, nodeDuration, nodeRetriesTotal, nodesFailed, nodesFinished, nodesStarted, processHeapUsedBytes, processMemoryRssBytes, processUptimeSeconds, prometheusContentType, promptSizeBytes, renderPrometheusMetrics, replaysStarted, resolveSmithersObservabilityOptions, responseSizeBytes, rewindDurationMs, rewindFramesDeleted, rewindRollbackTotal, rewindSandboxesReverted, rewindTotal, runDuration, runForksCreated, runWithCorrelationContext, runsAncestryDepth, runsCancelledTotal, runsCarriedStateBytes, runsContinuedTotal, runsFailedTotal, runsFinishedTotal, runsResumedTotal, runsTotal, sandboxActive, sandboxBundleSizeBytes, sandboxCompletedTotal, sandboxCreatedTotal, sandboxDurationMs, sandboxPatchCount, sandboxTransportDurationMs, schedulerConcurrencyUtilization, schedulerQueueDepth, schedulerWaitDuration, scorerEventsFailed, scorerEventsFinished, scorerEventsStarted, smithersMetricCatalog, smithersMetrics, smithersSpanNames, snapshotDuration, snapshotsCaptured, timerDelayDuration, timersCancelled, timersCreated, timersFired, timersPending, toPrometheusMetricName, tokensCacheReadTotal, tokensCacheWriteTotal, tokensContextWindowBucketTotal, tokensContextWindowPerCall, tokensInputPerCall, tokensInputTotal, tokensOutputPerCall, tokensOutputTotal, tokensReasoningTotal, toolCallErrorsTotal, toolCallsTotal, toolDuration, toolOutputTruncatedTotal, trackEvent as trackSmithersEvent, updateCurrentCorrelationContext, updateProcessMetrics, vcsDuration, withCorrelationContext, withCurrentCorrelationContext, withSmithersSpan };
1189
+ export { type CorrelationContext, CorrelationContextLive, type CorrelationContextPatch, CorrelationContextService, type CorrelationPatch, type MetricLabels, MetricsService, MetricsServiceLive, type MetricsServiceShape, type MetricsSnapshot, type ResolvedSmithersObservabilityOptions, type SmithersEvent, type SmithersLogFormat, type SmithersMetricDefinition, SmithersObservability, type SmithersObservabilityOptions, type SmithersObservabilityService, TracingService, TracingServiceLive, activeNodes, activeRuns, annotateSmithersTrace, approvalPending, approvalWaitDuration, approvalsDenied, approvalsGranted, approvalsRequested, attemptDuration, cacheHits, cacheMisses, correlationContextFiberRef, correlationContextToLogAnnotations, createSmithersObservabilityLayer, createSmithersOtelLayer, createSmithersRuntimeLayer, dbQueryDuration, dbRetries, dbTransactionDuration, dbTransactionRetries, dbTransactionRollbacks, errorsTotal, eventsEmittedTotal, externalWaitAsyncPending, getCurrentCorrelationContext, getCurrentCorrelationContextEffect, getCurrentSmithersTraceAnnotations, getCurrentSmithersTraceSpan, hotReloadDuration, hotReloadFailures, hotReloads, httpRequestDuration, httpRequests, logDebug, logError, logInfo, logWarning, makeSmithersSpanAttributes, mergeCorrelationContext, metricsServiceAdapter, nodeDuration, nodeRetriesTotal, nodesFailed, nodesFinished, nodesStarted, processHeapUsedBytes, processMemoryRssBytes, processUptimeSeconds, prometheusContentType, promptSizeBytes, renderPrometheusMetrics, replaysStarted, resolveSmithersObservabilityOptions, responseSizeBytes, rewindDurationMs, rewindFramesDeleted, rewindRollbackTotal, rewindSandboxesReverted, rewindTotal, runDuration, runForksCreated, runWithCorrelationContext, runsAncestryDepth, runsCancelledTotal, runsCarriedStateBytes, runsContinuedTotal, runsFailedTotal, runsFinishedTotal, runsResumedTotal, runsTotal, sandboxActive, sandboxBundleSizeBytes, sandboxCompletedTotal, sandboxCreatedTotal, sandboxDurationMs, sandboxPatchCount, sandboxTransportDurationMs, schedulerConcurrencyUtilization, schedulerQueueDepth, schedulerWaitDuration, scorerEventsFailed, scorerEventsFinished, scorerEventsStarted, setSmithersLogRunner, smithersMetricCatalog, smithersMetrics, smithersSpanNames, snapshotDuration, snapshotsCaptured, timerDelayDuration, timersCancelled, timersCreated, timersFired, timersPending, toPrometheusMetricName, tokensCacheReadTotal, tokensCacheWriteTotal, tokensContextWindowBucketTotal, tokensContextWindowPerCall, tokensInputPerCall, tokensInputTotal, tokensOutputPerCall, tokensOutputTotal, tokensReasoningTotal, toolCallErrorsTotal, toolCallsTotal, toolDuration, toolOutputTruncatedTotal, trackEvent as trackSmithersEvent, updateCurrentCorrelationContext, updateProcessMetrics, vcsDuration, withCorrelationContext, withCurrentCorrelationContext, withSmithersSpan };
package/src/index.js CHANGED
@@ -32,4 +32,4 @@ export { rewindTotal, rewindRollbackTotal, rewindDurationMs, rewindFramesDeleted
32
32
  export { activeNodes, activeRuns, approvalPending, externalWaitAsyncPending, approvalsDenied, approvalsGranted, approvalsRequested, approvalWaitDuration, timerDelayDuration, timersCancelled, timersCreated, timersFired, timersPending, attemptDuration, cacheHits, cacheMisses, dbQueryDuration, dbRetries, dbTransactionDuration, dbTransactionRetries, dbTransactionRollbacks, errorsTotal, eventsEmittedTotal, hotReloadDuration, hotReloadFailures, hotReloads, httpRequestDuration, httpRequests, nodeDuration, nodeRetriesTotal, nodesFailed, nodesFinished, nodesStarted, processHeapUsedBytes, processMemoryRssBytes, processUptimeSeconds, promptSizeBytes, responseSizeBytes, runDuration, runsCancelledTotal, runsContinuedTotal, runsFailedTotal, runsFinishedTotal, runsResumedTotal, runsAncestryDepth, runsCarriedStateBytes, sandboxActive, sandboxBundleSizeBytes, sandboxCompletedTotal, sandboxCreatedTotal, sandboxDurationMs, sandboxPatchCount, sandboxTransportDurationMs, runsTotal, schedulerConcurrencyUtilization, schedulerQueueDepth, schedulerWaitDuration, tokensCacheReadTotal, tokensCacheWriteTotal, tokensContextWindowBucketTotal, tokensContextWindowPerCall, tokensInputPerCall, tokensInputTotal, tokensOutputPerCall, tokensOutputTotal, tokensReasoningTotal, toolCallErrorsTotal, toolCallsTotal, toolDuration, toolOutputTruncatedTotal, scorerEventsStarted, scorerEventsFinished, scorerEventsFailed, snapshotsCaptured, runForksCreated, replaysStarted, snapshotDuration, trackEvent as trackSmithersEvent, updateProcessMetrics, vcsDuration, toPrometheusMetricName, smithersMetricCatalog, metricsServiceAdapter, } from "./metrics/index.js";
33
33
  export { correlationContextFiberRef, correlationContextToLogAnnotations, CorrelationContextLive, CorrelationContextService, getCurrentCorrelationContext, getCurrentCorrelationContextEffect, mergeCorrelationContext, runWithCorrelationContext, withCorrelationContext, withCurrentCorrelationContext, } from "./correlation.js";
34
34
  export { updateCurrentCorrelationContext } from "./correlation.js";
35
- export { logDebug, logInfo, logWarning, logError, } from "./logging.js";
35
+ export { logDebug, logInfo, logWarning, logError, setSmithersLogRunner, } from "./logging.js";
package/src/logging.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Effect } from "effect";
1
+ import { Effect, Logger, LogLevel } from "effect";
2
2
  import { getCurrentSmithersTraceAnnotations } from "./getCurrentSmithersTraceAnnotations.js";
3
3
  import { correlationContextToLogAnnotations, getCurrentCorrelationContext, withCurrentCorrelationContext, } from "./correlation.js";
4
4
  /**
@@ -11,6 +11,7 @@ const LOG_LEVEL_DEBUG = 1;
11
11
  const LOG_LEVEL_INFO = 2;
12
12
  const LOG_LEVEL_WARNING = 3;
13
13
  const LOG_LEVEL_ERROR = 4;
14
+ const LOG_RUNNER_KEY = Symbol.for("smithers.observability.logRunner");
14
15
 
15
16
  /** @returns {number} */
16
17
  function resolveMinLevel() {
@@ -31,6 +32,58 @@ function resolveMinLevel() {
31
32
 
32
33
  const minLevel = resolveMinLevel();
33
34
 
35
+ /**
36
+ * @typedef {{
37
+ * runFork: (effect: Effect.Effect<void, never, never>) => unknown;
38
+ * runPromise: (effect: Effect.Effect<void, never, never>) => Promise<void>;
39
+ * }} SmithersLogRunner
40
+ */
41
+
42
+ /** @returns {{ runner: SmithersLogRunner | null }} */
43
+ function getRunnerState() {
44
+ const globalState = /** @type {typeof globalThis & { [LOG_RUNNER_KEY]?: { runner: SmithersLogRunner | null } }} */ (globalThis);
45
+ globalState[LOG_RUNNER_KEY] ??= { runner: null };
46
+ return globalState[LOG_RUNNER_KEY];
47
+ }
48
+
49
+ /** @type {SmithersLogRunner} */
50
+ const defaultRunner = {
51
+ runFork: (effect) => Effect.runFork(effect),
52
+ runPromise: (effect) => Effect.runPromise(effect),
53
+ };
54
+
55
+ /** @returns {SmithersLogRunner} */
56
+ function getLogRunner() {
57
+ return getRunnerState().runner ?? defaultRunner;
58
+ }
59
+
60
+ /**
61
+ * Install the Effect runtime used by fire-and-forget observability logs.
62
+ * Returns a restore function so tests and embedded hosts can scope overrides.
63
+ *
64
+ * @param {SmithersLogRunner | null} runner
65
+ * @returns {() => void}
66
+ */
67
+ export function setSmithersLogRunner(runner) {
68
+ const state = getRunnerState();
69
+ const previous = state.runner;
70
+ state.runner = runner;
71
+ return () => {
72
+ state.runner = previous;
73
+ };
74
+ }
75
+
76
+ /** @param {number} level */
77
+ function toEffectLogLevel(level) {
78
+ switch (level) {
79
+ case LOG_LEVEL_DEBUG: return LogLevel.Debug;
80
+ case LOG_LEVEL_INFO: return LogLevel.Info;
81
+ case LOG_LEVEL_WARNING: return LogLevel.Warning;
82
+ case LOG_LEVEL_ERROR: return LogLevel.Error;
83
+ default: return LogLevel.All;
84
+ }
85
+ }
86
+
34
87
  /**
35
88
  * @param {Effect.Effect<void, never, never>} effect
36
89
  * @param {LogAnnotations} [annotations]
@@ -66,7 +119,12 @@ function buildLogProgram(effect, annotations, span) {
66
119
  function emitLog(effect, annotations, span, level = LOG_LEVEL_INFO) {
67
120
  if (level < minLevel) return;
68
121
  const program = buildLogProgram(effect, annotations, span);
69
- if (program) void Effect.runFork(program);
122
+ if (!program) return;
123
+ try {
124
+ void getLogRunner().runFork(program.pipe(Logger.withMinimumLogLevel(toEffectLogLevel(level))));
125
+ } catch {
126
+ // Logging must never break the caller.
127
+ }
70
128
  }
71
129
 
72
130
  /**
@@ -79,7 +137,12 @@ function emitLog(effect, annotations, span, level = LOG_LEVEL_INFO) {
79
137
  async function emitLogAwait(effect, annotations, span, level = LOG_LEVEL_INFO) {
80
138
  if (level < minLevel) return;
81
139
  const program = buildLogProgram(effect, annotations, span);
82
- if (program) await Effect.runPromise(program);
140
+ if (!program) return;
141
+ try {
142
+ await getLogRunner().runPromise(program.pipe(Logger.withMinimumLogLevel(toEffectLogLevel(level))));
143
+ } catch {
144
+ // Logging must never break the caller.
145
+ }
83
146
  }
84
147
  /**
85
148
  * @param {string} message
@@ -0,0 +1,2 @@
1
+ import { Metric } from "effect";
2
+ export const gatewayRunEventBackpressureDisconnectTotal = Metric.counter("smithers.gateway.run_event_backpressure_disconnect_total");
@@ -92,6 +92,7 @@ export { gatewayWebhooksRejectedTotal } from "./gatewayWebhooksRejectedTotal.js"
92
92
  export { devtoolsSubscribeTotal } from "./devtoolsSubscribeTotal.js";
93
93
  export { devtoolsEventTotal } from "./devtoolsEventTotal.js";
94
94
  export { devtoolsBackpressureDisconnectTotal } from "./devtoolsBackpressureDisconnectTotal.js";
95
+ export { gatewayRunEventBackpressureDisconnectTotal } from "./gatewayRunEventBackpressureDisconnectTotal.js";
95
96
  export { eventsEmittedTotal } from "./eventsEmittedTotal.js";
96
97
  export { taskHeartbeatsTotal } from "./taskHeartbeatsTotal.js";
97
98
  export { taskHeartbeatTimeoutTotal } from "./taskHeartbeatTimeoutTotal.js";
@@ -75,5 +75,6 @@ export function resolveSmithersObservabilityOptions(options = {}) {
75
75
  ? resolveLogFormat(options.logFormat)
76
76
  : resolveLogFormat(process.env.SMITHERS_LOG_FORMAT),
77
77
  logLevel: resolveLogLevel(options.logLevel ?? process.env.SMITHERS_LOG_LEVEL),
78
+ installLogger: options.installLogger !== false,
78
79
  };
79
80
  }