@smithers-orchestrator/observability 0.20.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 +7 -7
- package/src/SmithersEvent.ts +35 -0
- package/src/_otelLogBuilders.js +94 -0
- package/src/_sessionFileResolvers.js +146 -0
- package/src/_traceEventNormalizers.js +397 -0
- package/src/_traceRedaction.js +62 -0
- package/src/agentSessionEventToOtelLogRecord.js +52 -0
- package/src/agentTrace.ts +154 -0
- package/src/agentTraceCapabilities.js +138 -0
- package/src/canonicalTraceEventToOtelLogRecord.js +52 -0
- package/src/detectAgentFamily.js +28 -0
- package/src/detectCaptureMode.js +24 -0
- package/src/emitOtelLogRecord.js +19 -0
- package/src/kindPhase.js +17 -0
- package/src/logging.js +64 -4
- package/src/normalizeStructuredEvent.js +15 -0
- package/src/resolveAgentTraceCapabilities.js +59 -0
- package/src/unsupportedKindsForCapabilities.js +32 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smithers-orchestrator/observability",
|
|
3
|
-
"version": "0.20.
|
|
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/agents": "0.20.
|
|
33
|
-
"@smithers-orchestrator/driver": "0.20.
|
|
34
|
-
"@smithers-orchestrator/
|
|
35
|
-
"@smithers-orchestrator/
|
|
36
|
-
"@smithers-orchestrator/openapi": "0.20.
|
|
37
|
-
"@smithers-orchestrator/time-travel": "0.20.
|
|
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",
|
package/src/SmithersEvent.ts
CHANGED
|
@@ -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
|
+
}
|