@smithers-orchestrator/observability 0.19.0 → 0.20.1

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.19.0",
3
+ "version": "0.20.1",
4
4
  "description": "Concrete Smithers metrics, logging, tracing, and observability integrations",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -29,12 +29,12 @@
29
29
  "@effect/platform": "^0.96.0",
30
30
  "@effect/platform-bun": "^0.89.0",
31
31
  "effect": "^3.21.1",
32
- "@smithers-orchestrator/driver": "0.19.0",
33
- "@smithers-orchestrator/openapi": "0.19.0",
34
- "@smithers-orchestrator/agents": "0.19.0",
35
- "@smithers-orchestrator/scorers": "0.19.0",
36
- "@smithers-orchestrator/memory": "0.19.0",
37
- "@smithers-orchestrator/time-travel": "0.19.0"
32
+ "@smithers-orchestrator/agents": "0.20.1",
33
+ "@smithers-orchestrator/driver": "0.20.1",
34
+ "@smithers-orchestrator/memory": "0.20.1",
35
+ "@smithers-orchestrator/scorers": "0.20.1",
36
+ "@smithers-orchestrator/openapi": "0.20.1",
37
+ "@smithers-orchestrator/time-travel": "0.20.1"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/bun": "latest",
@@ -1,3 +1,9 @@
1
+ import type {
2
+ AgentSessionTranscriptEvent,
3
+ AgentTraceSummary,
4
+ CanonicalAgentTraceEvent,
5
+ } from "./agentTrace";
6
+
1
7
  type RunStatus =
2
8
  | "running"
3
9
  | "waiting-approval"
@@ -351,6 +357,7 @@ export type SmithersEvent =
351
357
  nodeId: string;
352
358
  iteration: number;
353
359
  attempt: number;
360
+ toolCallId: string;
354
361
  toolName: string;
355
362
  seq: number;
356
363
  timestampMs: number;
@@ -361,6 +368,7 @@ export type SmithersEvent =
361
368
  nodeId: string;
362
369
  iteration: number;
363
370
  attempt: number;
371
+ toolCallId: string;
364
372
  toolName: string;
365
373
  seq: number;
366
374
  status: "success" | "error";
@@ -600,4 +608,31 @@ export type SmithersEvent =
600
608
  runId: string;
601
609
  timerId: string;
602
610
  timestampMs: number;
611
+ }
612
+ | {
613
+ type: "AgentTraceEvent";
614
+ runId: string;
615
+ nodeId: string;
616
+ iteration: number;
617
+ attempt: number;
618
+ trace: CanonicalAgentTraceEvent;
619
+ timestampMs: number;
620
+ }
621
+ | {
622
+ type: "AgentTraceSummary";
623
+ runId: string;
624
+ nodeId: string;
625
+ iteration: number;
626
+ attempt: number;
627
+ summary: AgentTraceSummary;
628
+ timestampMs: number;
629
+ }
630
+ | {
631
+ type: "AgentSessionEvent";
632
+ runId: string;
633
+ nodeId: string;
634
+ iteration: number;
635
+ attempt: number;
636
+ transcript: AgentSessionTranscriptEvent;
637
+ timestampMs: number;
603
638
  };
@@ -0,0 +1,94 @@
1
+ /**
2
+ * @typedef {"INFO" | "WARN" | "ERROR"} OtelLogSeverity
3
+ *
4
+ * @typedef {{
5
+ * body: string;
6
+ * attributes: Record<string, unknown>;
7
+ * severity: OtelLogSeverity;
8
+ * }} OtelLogRecord
9
+ *
10
+ * @typedef {import('./agentTrace.ts').CanonicalAgentTraceEvent} CanonicalAgentTraceEvent
11
+ */
12
+
13
+ /**
14
+ * @param {Record<string, unknown>} base
15
+ * @param {Record<string, string | number | boolean>} annotations
16
+ * @returns {Record<string, unknown>}
17
+ */
18
+ export function buildOtelAttributes(base, annotations) {
19
+ /** @type {Record<string, unknown>} */
20
+ const attributes = {};
21
+ for (const [key, value] of Object.entries(base)) {
22
+ if (value !== undefined) attributes[key] = value;
23
+ }
24
+ for (const [key, value] of Object.entries(annotations)) {
25
+ attributes[key.startsWith("custom.") ? key : `custom.${key}`] = value;
26
+ }
27
+ return attributes;
28
+ }
29
+
30
+ /**
31
+ * @param {{
32
+ * category: "agent-trace" | "agent-session";
33
+ * payload: unknown;
34
+ * raw: unknown;
35
+ * redaction: { applied: boolean; ruleIds: string[] };
36
+ * annotations: Record<string, string | number | boolean>;
37
+ * }} body
38
+ * @param {Record<string, unknown>} attributes
39
+ * @param {OtelLogSeverity} severity
40
+ * @returns {OtelLogRecord}
41
+ */
42
+ export function buildOtelLogRecord(body, attributes, severity) {
43
+ return {
44
+ body: JSON.stringify({
45
+ category: body.category,
46
+ payload: body.payload,
47
+ raw: body.raw,
48
+ redaction: body.redaction,
49
+ annotations: body.annotations,
50
+ }),
51
+ attributes,
52
+ severity,
53
+ };
54
+ }
55
+
56
+ /**
57
+ * @param {CanonicalAgentTraceEvent} event
58
+ * @returns {OtelLogSeverity}
59
+ */
60
+ export function inferCanonicalSeverity(event) {
61
+ return event.event.kind === "capture.error"
62
+ ? "ERROR"
63
+ : event.event.kind === "capture.warning" || event.event.kind === "stderr"
64
+ ? "WARN"
65
+ : "INFO";
66
+ }
67
+
68
+ /**
69
+ * @param {unknown} raw
70
+ * @returns {OtelLogSeverity}
71
+ */
72
+ export function inferSessionSeverity(raw) {
73
+ const row = /** @type {any} */ (raw);
74
+ const rowType = String(row?.type ?? "").toLowerCase();
75
+ if (row?.is_error === true ||
76
+ row?.isError === true ||
77
+ row?.error ||
78
+ row?.errorMessage ||
79
+ row?.message?.stopReason === "error" ||
80
+ row?.message?.errorMessage ||
81
+ rowType.includes("error")) {
82
+ return "ERROR";
83
+ }
84
+ if (rowType.includes("warning")) return "WARN";
85
+ return "INFO";
86
+ }
87
+
88
+ /**
89
+ * @param {CanonicalAgentTraceEvent} event
90
+ * @returns {boolean}
91
+ */
92
+ export function shouldExportTraceEventToOtel(event) {
93
+ return event.event.kind !== "artifact.created";
94
+ }
@@ -0,0 +1,146 @@
1
+ import { readFile, readdir, stat } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ /**
6
+ * @param {string} dir
7
+ * @returns {Promise<string[]>}
8
+ */
9
+ async function listJsonlFiles(dir) {
10
+ try {
11
+ const entries = await readdir(dir, { withFileTypes: true });
12
+ const nested = await Promise.all(entries.map(async (entry) => {
13
+ const path = join(dir, entry.name);
14
+ if (entry.isDirectory()) return listJsonlFiles(path);
15
+ return entry.isFile() && path.endsWith(".jsonl") ? [path] : [];
16
+ }));
17
+ return nested.flat();
18
+ } catch {
19
+ return [];
20
+ }
21
+ }
22
+
23
+ /**
24
+ * @param {any} agent
25
+ * @returns {string[]}
26
+ */
27
+ function buildCodexSessionRoots(agent) {
28
+ const custom = agent?.opts?.sessionDir ?? agent?.opts?.codexSessionDir;
29
+ if (typeof custom === "string" && custom) return [custom];
30
+ if (String(agent?.constructor?.name ?? "") !== "CodexAgent") return [];
31
+ return [join(homedir(), ".codex", "sessions")];
32
+ }
33
+
34
+ /**
35
+ * @param {any} agent
36
+ * @returns {string[]}
37
+ */
38
+ function buildClaudeSessionRoots(agent) {
39
+ const custom = agent?.opts?.sessionDir ??
40
+ agent?.opts?.claudeProjectsDir ??
41
+ agent?.opts?.projectsDir;
42
+ if (typeof custom === "string" && custom) return [custom];
43
+ if (String(agent?.constructor?.name ?? "") !== "ClaudeCodeAgent") return [];
44
+ return [join(homedir(), ".claude", "projects")];
45
+ }
46
+
47
+ /**
48
+ * @param {any} agent
49
+ * @returns {string[]}
50
+ */
51
+ function buildPiSessionRoots(agent) {
52
+ if (typeof agent?.opts?.session === "string" && agent.opts.session) {
53
+ return [agent.opts.session];
54
+ }
55
+ const custom = agent?.opts?.sessionDir;
56
+ if (typeof custom === "string" && custom) return [custom];
57
+ if (String(agent?.constructor?.name ?? "") !== "PiAgent") return [];
58
+ return [join(homedir(), ".pi", "agent", "sessions")];
59
+ }
60
+
61
+ /**
62
+ * @param {string} cwd
63
+ * @returns {string}
64
+ */
65
+ function sanitizeClaudeProjectPath(cwd) {
66
+ return cwd.replace(/[\\/]/g, "-");
67
+ }
68
+
69
+ /**
70
+ * @param {unknown} sessionCwd
71
+ * @param {string} cwd
72
+ * @returns {boolean}
73
+ */
74
+ function isCorrelatedSessionCwd(sessionCwd, cwd) {
75
+ if (typeof sessionCwd !== "string" || !sessionCwd) return false;
76
+ return (sessionCwd === cwd ||
77
+ sessionCwd.startsWith(`${cwd}/`) ||
78
+ cwd.startsWith(`${sessionCwd}/`));
79
+ }
80
+
81
+ /**
82
+ * @param {any} agent
83
+ * @param {string} [sessionId]
84
+ * @returns {Promise<string | null>}
85
+ */
86
+ export async function resolvePiSessionFile(agent, sessionId) {
87
+ if (typeof agent?.opts?.session === "string" && agent.opts.session) {
88
+ return agent.opts.session;
89
+ }
90
+ if (!sessionId) return null;
91
+ for (const root of buildPiSessionRoots(agent)) {
92
+ const files = await listJsonlFiles(root);
93
+ const match = files.find((file) => file.includes(sessionId));
94
+ if (match) return match;
95
+ }
96
+ return null;
97
+ }
98
+
99
+ /**
100
+ * @param {any} agent
101
+ * @param {string} cwd
102
+ * @param {string} [sessionId]
103
+ * @returns {Promise<string | null>}
104
+ */
105
+ export async function resolveClaudeSessionFile(agent, cwd, sessionId) {
106
+ if (!sessionId) return null;
107
+ for (const root of buildClaudeSessionRoots(agent)) {
108
+ const direct = join(root, sanitizeClaudeProjectPath(cwd), `${sessionId}.jsonl`);
109
+ try {
110
+ const info = await stat(direct);
111
+ if (info.isFile()) return direct;
112
+ } catch {}
113
+ const files = await listJsonlFiles(root);
114
+ const match = files.find((file) => file.endsWith(`/${sessionId}.jsonl`));
115
+ if (match) return match;
116
+ }
117
+ return null;
118
+ }
119
+
120
+ /**
121
+ * @param {any} agent
122
+ * @param {string} cwd
123
+ * @param {number} startedAtMs
124
+ * @returns {Promise<string | null>}
125
+ */
126
+ export async function resolveCodexSessionFile(agent, cwd, startedAtMs) {
127
+ const day = new Date(startedAtMs);
128
+ const dayRoots = buildCodexSessionRoots(agent).map((root) => join(root, String(day.getUTCFullYear()), String(day.getUTCMonth() + 1).padStart(2, "0"), String(day.getUTCDate()).padStart(2, "0")));
129
+ const candidates = (await Promise.all(dayRoots.map((root) => listJsonlFiles(root)))).flat();
130
+ /** @type {{ file: string; delta: number } | null} */
131
+ let best = null;
132
+ for (const file of candidates) {
133
+ try {
134
+ const firstLine = (await readFile(file, "utf8")).split(/\r?\n/, 1)[0];
135
+ if (!firstLine) continue;
136
+ const parsed = JSON.parse(firstLine);
137
+ if (parsed?.type !== "session_meta") continue;
138
+ const sessionCwd = parsed?.payload?.cwd;
139
+ const sessionTs = Date.parse(String(parsed?.payload?.timestamp ?? parsed?.timestamp ?? ""));
140
+ if (!isCorrelatedSessionCwd(sessionCwd, cwd) || !Number.isFinite(sessionTs)) continue;
141
+ const delta = Math.abs(sessionTs - startedAtMs);
142
+ if (!best || delta < best.delta) best = { file, delta };
143
+ } catch {}
144
+ }
145
+ return best?.file ?? null;
146
+ }