@graphorin/observability 0.6.1 → 0.7.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.
Files changed (80) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +24 -4
  3. package/dist/cost/cost-tracker.d.ts.map +1 -1
  4. package/dist/cost/cost-tracker.js +34 -0
  5. package/dist/cost/cost-tracker.js.map +1 -1
  6. package/dist/cost/delegate.d.ts +40 -0
  7. package/dist/cost/delegate.d.ts.map +1 -0
  8. package/dist/cost/delegate.js +31 -0
  9. package/dist/cost/delegate.js.map +1 -0
  10. package/dist/cost/index.d.ts +2 -1
  11. package/dist/cost/index.js +2 -1
  12. package/dist/cost/types.d.ts +39 -0
  13. package/dist/cost/types.d.ts.map +1 -1
  14. package/dist/exporters/types.d.ts +7 -0
  15. package/dist/exporters/types.d.ts.map +1 -1
  16. package/dist/exporters/types.js.map +1 -1
  17. package/dist/exporters/with-validation.d.ts.map +1 -1
  18. package/dist/exporters/with-validation.js +5 -4
  19. package/dist/exporters/with-validation.js.map +1 -1
  20. package/dist/gen-ai/emit.js +9 -1
  21. package/dist/gen-ai/emit.js.map +1 -1
  22. package/dist/index.d.ts +2 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +2 -8
  25. package/dist/index.js.map +1 -1
  26. package/dist/package.js +1 -1
  27. package/dist/package.js.map +1 -1
  28. package/dist/redaction/imperative-patterns.d.ts +3 -3
  29. package/dist/redaction/imperative-patterns.d.ts.map +1 -1
  30. package/dist/redaction/imperative-patterns.js +7 -0
  31. package/dist/redaction/imperative-patterns.js.map +1 -1
  32. package/dist/tracer/sampling.d.ts +6 -1
  33. package/dist/tracer/sampling.d.ts.map +1 -1
  34. package/dist/tracer/sampling.js +7 -1
  35. package/dist/tracer/sampling.js.map +1 -1
  36. package/dist/tracer/span.d.ts.map +1 -1
  37. package/dist/tracer/span.js +12 -3
  38. package/dist/tracer/span.js.map +1 -1
  39. package/package.json +18 -35
  40. package/src/cost/cost-tracker.ts +315 -0
  41. package/src/cost/delegate.ts +79 -0
  42. package/src/cost/index.ts +23 -0
  43. package/src/cost/types.ts +151 -0
  44. package/src/eval/index.ts +16 -0
  45. package/src/eval/runner.ts +146 -0
  46. package/src/eval/types.ts +122 -0
  47. package/src/exporters/console.ts +75 -0
  48. package/src/exporters/index.ts +36 -0
  49. package/src/exporters/jsonl.ts +167 -0
  50. package/src/exporters/otlp-http.ts +178 -0
  51. package/src/exporters/types.ts +85 -0
  52. package/src/exporters/with-validation.ts +176 -0
  53. package/src/gen-ai/emit.ts +157 -0
  54. package/src/gen-ai/index.ts +26 -0
  55. package/src/gen-ai/operation-mapping.ts +89 -0
  56. package/src/gen-ai/system-derivation.ts +84 -0
  57. package/src/gen-ai/types.ts +143 -0
  58. package/src/index.ts +36 -0
  59. package/src/logger/index.ts +13 -0
  60. package/src/logger/logger.ts +178 -0
  61. package/src/openinference/index.ts +133 -0
  62. package/src/redaction/config.ts +50 -0
  63. package/src/redaction/errors.ts +53 -0
  64. package/src/redaction/imperative-patterns.ts +268 -0
  65. package/src/redaction/index.ts +42 -0
  66. package/src/redaction/patterns.ts +263 -0
  67. package/src/redaction/types.ts +116 -0
  68. package/src/redaction/validator.ts +302 -0
  69. package/src/replay/config.ts +58 -0
  70. package/src/replay/index.ts +17 -0
  71. package/src/replay/log.ts +89 -0
  72. package/src/replay/replay.ts +196 -0
  73. package/src/replay/types.ts +112 -0
  74. package/src/telemetry/index.ts +94 -0
  75. package/src/tracer/ids.ts +27 -0
  76. package/src/tracer/index.ts +22 -0
  77. package/src/tracer/sampling.ts +170 -0
  78. package/src/tracer/span-names.ts +52 -0
  79. package/src/tracer/span.ts +175 -0
  80. package/src/tracer/tracer.ts +309 -0
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Mapping table from Graphorin `SpanType` values to canonical
3
+ * `gen_ai.operation.name` enum values.
4
+ *
5
+ * The table mirrors the canonical-mapping reference table published
6
+ * in the architecture documentation and is the authoritative source
7
+ * for the `emitGenAIAttributes` helper.
8
+ *
9
+ * @packageDocumentation
10
+ */
11
+
12
+ import type { SpanType } from '@graphorin/core';
13
+
14
+ import type { GenAIOperationName } from './types.js';
15
+
16
+ const TABLE: ReadonlyArray<readonly [SpanType, GenAIOperationName]> = Object.freeze([
17
+ // E8: match what the runtime actually emits (agent factory stamps
18
+ // `gen_ai.operation.name: 'invoke_agent'` on both run + step spans) and the
19
+ // OTel GenAI operation enum, which has `invoke_agent` and no step concept.
20
+ ['agent.run', 'invoke_agent'],
21
+ ['agent.step', 'invoke_agent'],
22
+ ['agent.handoff', 'agent.handoff'],
23
+ ['agent.suspend', 'agent.suspend'],
24
+ ['agent.resume', 'agent.resume'],
25
+
26
+ ['provider.generate', 'chat'],
27
+ ['provider.stream', 'chat'],
28
+
29
+ ['tool.execute', 'execute_tool'],
30
+ ['tool.approval', 'execute_tool'],
31
+
32
+ ['memory.read.working', 'memory.read'],
33
+ ['memory.read.session', 'memory.read'],
34
+ ['memory.read.episodic', 'memory.read'],
35
+ ['memory.read.semantic', 'memory.read'],
36
+ ['memory.read.procedural', 'memory.read'],
37
+ ['memory.read.shared', 'memory.read'],
38
+ ['memory.write.working', 'memory.write'],
39
+ ['memory.write.session', 'memory.write'],
40
+ ['memory.write.episodic', 'memory.write'],
41
+ ['memory.write.semantic', 'memory.write'],
42
+ ['memory.write.procedural', 'memory.write'],
43
+ ['memory.write.shared', 'memory.write'],
44
+ ['memory.search.working', 'memory.search'],
45
+ ['memory.search.session', 'memory.search'],
46
+ ['memory.search.episodic', 'memory.search'],
47
+ ['memory.search.semantic', 'memory.search'],
48
+ ['memory.search.procedural', 'memory.search'],
49
+ ['memory.search.shared', 'memory.search'],
50
+ ['memory.consolidate.light', 'memory.consolidate'],
51
+ ['memory.consolidate.standard', 'memory.consolidate'],
52
+ ['memory.consolidate.deep', 'memory.consolidate'],
53
+ ['memory.conflict', 'memory.conflict'],
54
+ ['memory.embed', 'embedding'],
55
+
56
+ ['workflow.run', 'workflow.run'],
57
+ ['workflow.step', 'workflow.step'],
58
+ ['workflow.task', 'workflow.task'],
59
+ ['workflow.checkpoint', 'workflow.checkpoint'],
60
+
61
+ ['skill.activate', 'skill.activate'],
62
+ ['skill.load', 'skill.load'],
63
+
64
+ ['mcp.connect', 'mcp.connect'],
65
+ ['mcp.call', 'mcp.call'],
66
+ ['mcp.list-tools', 'mcp.list-tools'],
67
+ ] as const);
68
+
69
+ const LOOKUP = new Map<SpanType, GenAIOperationName>(TABLE);
70
+
71
+ /**
72
+ * Resolve the canonical `gen_ai.operation.name` value for a Graphorin
73
+ * span type. Returns `undefined` if no mapping exists (e.g. for
74
+ * `replay.*` markers, which are emitted via a separate code path).
75
+ *
76
+ * @stable
77
+ */
78
+ export function operationNameFor(type: SpanType): GenAIOperationName | undefined {
79
+ return LOOKUP.get(type);
80
+ }
81
+
82
+ /**
83
+ * Full canonical span-to-operation table - exposed for tooling
84
+ * (documentation generators, fixture tests) that need to introspect
85
+ * the mapping.
86
+ *
87
+ * @stable
88
+ */
89
+ export const OPERATION_NAME_TABLE: ReadonlyArray<readonly [SpanType, GenAIOperationName]> = TABLE;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Auto-derivation lookup table for the `gen_ai.system` attribute.
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+
7
+ import type { GenAISystem } from './types.js';
8
+
9
+ /**
10
+ * Canonical mapping from a provider class name (or substring) to the
11
+ * `gen_ai.system` enum value. The table is an export so consumers
12
+ * (e.g. provider adapters in Phase 06) can introspect or extend it.
13
+ *
14
+ * @stable
15
+ */
16
+ export const PROVIDER_CLASS_TO_GEN_AI_SYSTEM: ReadonlyArray<readonly [RegExp, GenAISystem]> =
17
+ Object.freeze([
18
+ [/anthropic/i, 'anthropic'],
19
+ [/openai(?!compat)/i, 'openai'],
20
+ [/google|gemini/i, 'google'],
21
+ [/mistral/i, 'mistral'],
22
+ [/ollama/i, 'ollama'],
23
+ [/openrouter/i, 'openrouter'],
24
+ [/azure/i, 'azure_ai_inference'],
25
+ [/bedrock|aws/i, 'aws.bedrock'],
26
+ [/cohere/i, 'cohere'],
27
+ [/vertex/i, 'vertex_ai'],
28
+ [/llamacpp|llama-cpp|llama_cpp/i, 'graphorin-llamacpp'],
29
+ [/openaicompatible|openai-compatible/i, 'graphorin-openai-compatible'],
30
+ ] as const);
31
+
32
+ const WARNED_CLASSES = new Set<string>();
33
+ let WARN_SINK: (line: string) => void = (line) => console.warn(line);
34
+
35
+ /**
36
+ * Override the WARN-once sink used for unrecognised provider class
37
+ * names. Useful in tests so the suite does not pollute stderr.
38
+ *
39
+ * @internal
40
+ */
41
+ export function setGenAISystemWarnSink(sink: (line: string) => void): void {
42
+ WARN_SINK = sink;
43
+ }
44
+
45
+ /**
46
+ * Reset the WARN-once cache. Test-only.
47
+ *
48
+ * @internal
49
+ */
50
+ export function _resetGenAISystemWarningsForTesting(): void {
51
+ WARNED_CLASSES.clear();
52
+ }
53
+
54
+ /**
55
+ * Derive the canonical `gen_ai.system` value from a provider class
56
+ * name. Returns `null` when the name does not match any known
57
+ * pattern; callers should declare `Provider.genAiSystem` explicitly
58
+ * in that case.
59
+ *
60
+ * The first time an unknown class name is seen, the function emits
61
+ * one structured WARN line to the configured sink so operators
62
+ * notice the gap. Subsequent lookups for the same class are silent.
63
+ *
64
+ * @stable
65
+ */
66
+ export function deriveGenAISystem(providerClassName: string): GenAISystem | null {
67
+ for (const [regex, system] of PROVIDER_CLASS_TO_GEN_AI_SYSTEM) {
68
+ if (regex.test(providerClassName)) return system;
69
+ }
70
+ warnOnce(providerClassName);
71
+ return null;
72
+ }
73
+
74
+ function warnOnce(providerClassName: string): void {
75
+ if (WARNED_CLASSES.has(providerClassName)) return;
76
+ WARNED_CLASSES.add(providerClassName);
77
+ WARN_SINK(
78
+ `[graphorin/observability] WARN: provider class "${providerClassName}" is not in ` +
79
+ 'the auto-derivation table for `gen_ai.system`. Declare `Provider.genAiSystem` ' +
80
+ 'explicitly on the adapter so OpenTelemetry GenAI dashboards attribute the spans ' +
81
+ 'correctly. See `@graphorin/observability/gen-ai` and the OpenTelemetry GenAI ' +
82
+ 'semantic conventions for the canonical vendor enum.',
83
+ );
84
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Type definitions for the OpenTelemetry GenAI semantic-conventions
3
+ * conformance helpers.
4
+ *
5
+ * @packageDocumentation
6
+ */
7
+
8
+ import type { SpanType } from '@graphorin/core';
9
+
10
+ /**
11
+ * Canonical OpenTelemetry semantic-conventions vendor enum used as the
12
+ * value of the `gen_ai.system` attribute.
13
+ *
14
+ * @stable
15
+ */
16
+ export type GenAISystem =
17
+ | 'anthropic'
18
+ | 'openai'
19
+ | 'google'
20
+ | 'mistral'
21
+ | 'ollama'
22
+ | 'openrouter'
23
+ | 'azure_ai_inference'
24
+ | 'aws.bedrock'
25
+ | 'cohere'
26
+ | 'vertex_ai'
27
+ | 'graphorin-llamacpp'
28
+ | 'graphorin-openai-compatible'
29
+ | (string & { readonly __genAiSystem?: never });
30
+
31
+ /**
32
+ * `gen_ai.tool.type` enum value. Defaults to `'function'` for
33
+ * user-defined and MCP-derived tools without explicit declaration.
34
+ *
35
+ * @stable
36
+ */
37
+ export type GenAIToolType =
38
+ | 'function'
39
+ | 'web_search'
40
+ | 'database'
41
+ | 'code_interpreter'
42
+ | 'image_generation'
43
+ | 'file_search';
44
+
45
+ /**
46
+ * Canonical `gen_ai.operation.name` enum used per the OpenTelemetry
47
+ * GenAI semantic conventions for AI agent + framework spans.
48
+ *
49
+ * @stable
50
+ */
51
+ export type GenAIOperationName =
52
+ // E8: `invoke_agent` is the canonical OTel GenAI agent operation and what
53
+ // the runtime emits; the legacy `agent.run`/`agent.step` members remain for
54
+ // compatibility with older recorded traces.
55
+ | 'invoke_agent'
56
+ | 'agent.run'
57
+ | 'agent.step'
58
+ | 'agent.handoff'
59
+ | 'agent.suspend'
60
+ | 'agent.resume'
61
+ | 'agent.fanout.spawned'
62
+ | 'agent.fanout.merged'
63
+ | 'agent.evaluator.iteration'
64
+ | 'chat'
65
+ | 'embedding'
66
+ | 'execute_tool'
67
+ | 'memory.read'
68
+ | 'memory.write'
69
+ | 'memory.search'
70
+ | 'memory.consolidate'
71
+ | 'memory.conflict'
72
+ | 'workflow.run'
73
+ | 'workflow.step'
74
+ | 'workflow.task'
75
+ | 'workflow.checkpoint'
76
+ | 'mcp.connect'
77
+ | 'mcp.call'
78
+ | 'mcp.list-tools'
79
+ | 'skill.activate'
80
+ | 'skill.load'
81
+ | 'replay.run'
82
+ | 'replay.skipped';
83
+
84
+ /**
85
+ * Per-message event type used by {@link emitGenAIMessageEvents}.
86
+ *
87
+ * @stable
88
+ */
89
+ export type GenAIMessageRole = 'system' | 'user' | 'assistant' | 'tool';
90
+
91
+ /**
92
+ * Single per-message record passed to {@link emitGenAIMessageEvents}.
93
+ *
94
+ * @stable
95
+ */
96
+ export interface GenAIMessage {
97
+ readonly role: GenAIMessageRole;
98
+ readonly content: string;
99
+ /** Optional model-specific metadata (tool calls, names, …). */
100
+ readonly toolCalls?: ReadonlyArray<{
101
+ readonly id: string;
102
+ readonly name: string;
103
+ readonly arguments: string;
104
+ }>;
105
+ readonly toolCallId?: string;
106
+ readonly name?: string;
107
+ }
108
+
109
+ /**
110
+ * Per-span attribute payload expected by {@link emitGenAIAttributes}.
111
+ * The fields mirror the OpenTelemetry GenAI semantic conventions and
112
+ * are merged with the existing Graphorin-prefixed attributes - the
113
+ * `gen_ai.*` family is additive, never replacing.
114
+ *
115
+ * @stable
116
+ */
117
+ export interface GenAIAttributes {
118
+ readonly system?: GenAISystem;
119
+ readonly requestModel?: string;
120
+ readonly responseModel?: string;
121
+ readonly responseId?: string;
122
+ readonly inputTokens?: number;
123
+ readonly outputTokens?: number;
124
+ readonly finishReasons?: ReadonlyArray<string>;
125
+ readonly operation?: GenAIOperationName;
126
+ readonly agentId?: string;
127
+ readonly agentName?: string;
128
+ readonly sessionId?: string;
129
+ readonly toolName?: string;
130
+ readonly toolType?: GenAIToolType;
131
+ readonly toolCallId?: string;
132
+ readonly toolDescription?: string;
133
+ }
134
+
135
+ /**
136
+ * Mapping from a Graphorin `SpanType` to the canonical
137
+ * `gen_ai.operation.name` value. Returns `undefined` for span types
138
+ * that do not have a canonical operation enum (`replay.*` is recorded
139
+ * as `'replay.run'` / `'replay.skipped'` per the doc table).
140
+ *
141
+ * @stable
142
+ */
143
+ export type SpanTypeToOperationName = (type: SpanType) => GenAIOperationName | undefined;
package/src/index.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @graphorin/observability - observability primitives for the
3
+ * Graphorin framework. Ships:
4
+ *
5
+ * - the typed `AISpan<T>` tracer over OpenTelemetry,
6
+ * - the **mandatory** `withValidation()` wrapper for every exporter,
7
+ * - the sensitivity-aware `RedactionValidator` with 14 built-in PII /
8
+ * secret detection patterns,
9
+ * - OpenTelemetry GenAI semantic-conventions conformance helpers,
10
+ * - OpenInference span-kind emission,
11
+ * - the structured `Logger` with span correlation,
12
+ * - the append-only `JSONLExporter` for replay,
13
+ * - sanitized-by-default `Replay` primitives,
14
+ * - the hierarchical `CostTracker`,
15
+ * - and a minimal inline eval runner.
16
+ *
17
+ * The full documentation lives in the package `README.md`.
18
+ *
19
+ * @packageDocumentation
20
+ */
21
+
22
+ /** Canonical version constant, derived from `package.json` at build time. */
23
+ import pkg from '../package.json' with { type: 'json' };
24
+
25
+ export const VERSION: string = pkg.version;
26
+
27
+ export * from './cost/index.js';
28
+ export * from './eval/index.js';
29
+ export * from './exporters/index.js';
30
+ export * from './gen-ai/index.js';
31
+ export * from './logger/index.js';
32
+ export * from './openinference/index.js';
33
+ export * from './redaction/index.js';
34
+ export * from './replay/index.js';
35
+ export * from './telemetry/index.js';
36
+ export * from './tracer/index.js';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Structured logger surface.
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+
7
+ export {
8
+ createLogger,
9
+ getCurrentSpanContext,
10
+ type LoggerFormat,
11
+ type LoggerOptions,
12
+ withCurrentSpan,
13
+ } from './logger.js';
@@ -0,0 +1,178 @@
1
+ /**
2
+ * `createLogger(...)` - structured logger with optional span correlation.
3
+ *
4
+ * The logger is a thin wrapper around `console.{log,info,warn,error}`
5
+ * that adds:
6
+ *
7
+ * - structured fields,
8
+ * - JSON or pretty rendering,
9
+ * - automatic correlation with the current Graphorin span (the
10
+ * {@link CurrentSpanContext} helper carries the live trace + span ids),
11
+ * - sensitivity-aware redaction via the supplied
12
+ * {@link RedactionValidatorInstance}.
13
+ *
14
+ * @packageDocumentation
15
+ */
16
+
17
+ import type { AISpan, LogFields, Logger, LogLevel, Sensitivity } from '@graphorin/core';
18
+
19
+ import { createAsyncContext } from '@graphorin/core';
20
+
21
+ import type { RedactionValidatorInstance } from '../redaction/types.js';
22
+
23
+ /**
24
+ * @stable
25
+ */
26
+ export type LoggerFormat = 'json' | 'pretty';
27
+
28
+ /**
29
+ * Configuration shape for {@link createLogger}.
30
+ *
31
+ * @stable
32
+ */
33
+ export interface LoggerOptions {
34
+ readonly level?: LogLevel;
35
+ readonly format?: LoggerFormat;
36
+ /** Optional validator. Logger fields flow through `validate(...)`. */
37
+ readonly redaction?: RedactionValidatorInstance;
38
+ /** Default sensitivity tier for fields. Defaults to `'internal'`. */
39
+ readonly defaultTier?: Sensitivity;
40
+ /**
41
+ * Sink that receives the rendered line. Defaults to writing to the
42
+ * appropriate `console.*` method.
43
+ */
44
+ readonly sink?: (level: LogLevel, line: string) => void;
45
+ }
46
+
47
+ const LEVELS: ReadonlyArray<LogLevel> = ['trace', 'debug', 'info', 'warn', 'error'] as const;
48
+
49
+ /**
50
+ * @internal - context used to thread the current span id through nested
51
+ * async calls. Exposed via {@link withCurrentSpan} below.
52
+ */
53
+ const SPAN_CONTEXT = createAsyncContext<{ traceId: string; spanId: string }>(
54
+ 'graphorin.observability.current-span',
55
+ );
56
+
57
+ /**
58
+ * Run `fn` with the supplied span as the "current" log-correlation
59
+ * span. The logger picks up the trace + span ids automatically.
60
+ *
61
+ * @stable
62
+ */
63
+ export function withCurrentSpan<R>(
64
+ span: AISpan | undefined,
65
+ fn: () => R | Promise<R>,
66
+ ): R | Promise<R> {
67
+ if (span === undefined) return fn();
68
+ return SPAN_CONTEXT.runAsync({ traceId: span.traceId, spanId: span.id }, async () => fn());
69
+ }
70
+
71
+ /**
72
+ * Read the current span context (if any). Useful for callers that
73
+ * want to attach span metadata to bespoke records.
74
+ *
75
+ * @stable
76
+ */
77
+ export function getCurrentSpanContext():
78
+ | { readonly traceId: string; readonly spanId: string }
79
+ | undefined {
80
+ return SPAN_CONTEXT.get();
81
+ }
82
+
83
+ /**
84
+ * Build a {@link Logger} configured against the supplied options.
85
+ *
86
+ * @stable
87
+ */
88
+ export function createLogger(opts: LoggerOptions = {}): Logger {
89
+ const minLevel = LEVELS.indexOf(opts.level ?? 'info');
90
+ const format = opts.format ?? 'json';
91
+ const redaction = opts.redaction;
92
+ const defaultTier: Sensitivity = opts.defaultTier ?? 'internal';
93
+ const sink = opts.sink ?? defaultSink;
94
+
95
+ function emit(level: LogLevel, message: string, fields?: LogFields): void {
96
+ const idx = LEVELS.indexOf(level);
97
+ if (idx < minLevel) return;
98
+
99
+ const span = SPAN_CONTEXT.get();
100
+ const safeFields = sanitizeFields(fields, redaction, defaultTier);
101
+
102
+ const payload: Record<string, unknown> = {
103
+ level,
104
+ time: new Date().toISOString(),
105
+ message,
106
+ ...safeFields,
107
+ };
108
+ if (span !== undefined) {
109
+ payload.traceId = span.traceId;
110
+ payload.spanId = span.spanId;
111
+ }
112
+
113
+ const line = format === 'json' ? JSON.stringify(payload) : prettyRender(payload);
114
+ sink(level, line);
115
+ }
116
+
117
+ return {
118
+ trace: (message, fields) => emit('trace', message, fields),
119
+ debug: (message, fields) => emit('debug', message, fields),
120
+ info: (message, fields) => emit('info', message, fields),
121
+ warn: (message, fields) => emit('warn', message, fields),
122
+ error: (message, fields) => emit('error', message, fields),
123
+ child(_bindings: LogFields): Logger {
124
+ const inherited = sanitizeFields(_bindings, redaction, defaultTier);
125
+ const parent = createLogger(opts);
126
+ return {
127
+ trace: (m, f) => parent.trace(m, { ...inherited, ...f }),
128
+ debug: (m, f) => parent.debug(m, { ...inherited, ...f }),
129
+ info: (m, f) => parent.info(m, { ...inherited, ...f }),
130
+ warn: (m, f) => parent.warn(m, { ...inherited, ...f }),
131
+ error: (m, f) => parent.error(m, { ...inherited, ...f }),
132
+ child(b: LogFields): Logger {
133
+ return parent.child({ ...inherited, ...b });
134
+ },
135
+ };
136
+ },
137
+ };
138
+ }
139
+
140
+ function sanitizeFields(
141
+ fields: LogFields | undefined,
142
+ validator: RedactionValidatorInstance | undefined,
143
+ tier: Sensitivity,
144
+ ): LogFields {
145
+ if (fields === undefined) return {};
146
+ if (validator === undefined) return { ...fields };
147
+ const out: Record<string, unknown> = {};
148
+ for (const [k, v] of Object.entries(fields)) {
149
+ const result = validator.validate({ value: v, tier, context: { attribute: k } });
150
+ if (result === null) continue;
151
+ out[k] = result.value;
152
+ }
153
+ return out;
154
+ }
155
+
156
+ function defaultSink(level: LogLevel, line: string): void {
157
+ switch (level) {
158
+ case 'trace':
159
+ case 'debug':
160
+ console.debug(line);
161
+ return;
162
+ case 'info':
163
+ console.info(line);
164
+ return;
165
+ case 'warn':
166
+ console.warn(line);
167
+ return;
168
+ case 'error':
169
+ console.error(line);
170
+ return;
171
+ }
172
+ }
173
+
174
+ function prettyRender(payload: Record<string, unknown>): string {
175
+ const { level, time, message, ...rest } = payload;
176
+ const fields = Object.keys(rest).length === 0 ? '' : ` ${JSON.stringify(rest)}`;
177
+ return `[${String(time)}] ${String(level).toUpperCase()} ${String(message)}${fields}`;
178
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * OpenInference span-kind emission. Adds the
3
+ * `openinference.span.kind` attribute (one of `AGENT`, `EVALUATOR`,
4
+ * `LLM`, `TOOL`, `RETRIEVER`, `EMBEDDING`, `CHAIN`) to applicable
5
+ * Graphorin spans.
6
+ *
7
+ * The mapping is the canonical table published in the architecture
8
+ * documentation. Span types without a clean OpenInference equivalent
9
+ * (`skill.*`, `mcp.connect`, `mcp.list-tools`, `replay.*`) are NOT
10
+ * emitted - the caller can introspect via {@link openInferenceKindFor}
11
+ * and decide whether to log a fallback attribute.
12
+ *
13
+ * @packageDocumentation
14
+ */
15
+
16
+ import type { AISpan, SpanType } from '@graphorin/core';
17
+
18
+ import { asGraphorinSpan } from '../tracer/tracer.js';
19
+
20
+ /**
21
+ * Canonical OpenInference span-kind enum.
22
+ *
23
+ * @stable
24
+ */
25
+ export type OpenInferenceSpanKind =
26
+ | 'AGENT'
27
+ | 'EVALUATOR'
28
+ | 'LLM'
29
+ | 'TOOL'
30
+ | 'RETRIEVER'
31
+ | 'EMBEDDING'
32
+ | 'CHAIN'
33
+ | 'RERANKER';
34
+
35
+ const KIND_TABLE: ReadonlyArray<readonly [SpanType, OpenInferenceSpanKind]> = Object.freeze([
36
+ ['agent.run', 'AGENT'],
37
+ ['agent.step', 'AGENT'],
38
+ ['agent.handoff', 'AGENT'],
39
+ ['agent.suspend', 'AGENT'],
40
+ ['agent.resume', 'AGENT'],
41
+
42
+ ['provider.generate', 'LLM'],
43
+ ['provider.stream', 'LLM'],
44
+
45
+ ['tool.execute', 'TOOL'],
46
+ ['tool.approval', 'TOOL'],
47
+
48
+ ['memory.read.working', 'RETRIEVER'],
49
+ ['memory.read.session', 'RETRIEVER'],
50
+ ['memory.read.episodic', 'RETRIEVER'],
51
+ ['memory.read.semantic', 'RETRIEVER'],
52
+ ['memory.read.procedural', 'RETRIEVER'],
53
+ ['memory.read.shared', 'RETRIEVER'],
54
+ ['memory.write.working', 'RETRIEVER'],
55
+ ['memory.write.session', 'RETRIEVER'],
56
+ ['memory.write.episodic', 'RETRIEVER'],
57
+ ['memory.write.semantic', 'RETRIEVER'],
58
+ ['memory.write.procedural', 'RETRIEVER'],
59
+ ['memory.write.shared', 'RETRIEVER'],
60
+ ['memory.search.working', 'RETRIEVER'],
61
+ ['memory.search.session', 'RETRIEVER'],
62
+ ['memory.search.episodic', 'RETRIEVER'],
63
+ ['memory.search.semantic', 'RETRIEVER'],
64
+ ['memory.search.procedural', 'RETRIEVER'],
65
+ ['memory.search.shared', 'RETRIEVER'],
66
+
67
+ ['memory.embed', 'EMBEDDING'],
68
+
69
+ ['memory.consolidate.light', 'CHAIN'],
70
+ ['memory.consolidate.standard', 'CHAIN'],
71
+ ['memory.consolidate.deep', 'CHAIN'],
72
+ ['memory.conflict', 'CHAIN'],
73
+ ['workflow.run', 'CHAIN'],
74
+ ['workflow.step', 'CHAIN'],
75
+ ['workflow.task', 'CHAIN'],
76
+ ['workflow.checkpoint', 'CHAIN'],
77
+
78
+ ['mcp.call', 'TOOL'],
79
+ ] as const);
80
+
81
+ /**
82
+ * Span types intentionally excluded from OpenInference span-kind
83
+ * emission per the canonical table - `skill.*`, `mcp.connect`,
84
+ * `mcp.list-tools`, and `replay.*` markers do not have a clean
85
+ * OpenInference equivalent.
86
+ *
87
+ * @stable
88
+ */
89
+ export const OPEN_INFERENCE_EXCLUDED_TYPES: ReadonlyArray<SpanType> = Object.freeze([
90
+ 'skill.activate',
91
+ 'skill.load',
92
+ 'mcp.connect',
93
+ 'mcp.list-tools',
94
+ ] as const);
95
+
96
+ const KIND_LOOKUP = new Map<SpanType, OpenInferenceSpanKind>(KIND_TABLE);
97
+
98
+ /**
99
+ * Resolve the OpenInference span kind for a Graphorin span type.
100
+ * Returns `null` for types intentionally excluded from emission.
101
+ *
102
+ * @stable
103
+ */
104
+ export function openInferenceKindFor<T extends SpanType>(type: T): OpenInferenceSpanKind | null {
105
+ return KIND_LOOKUP.get(type) ?? null;
106
+ }
107
+
108
+ /**
109
+ * Attach the `openinference.span.kind` attribute to a span. No-op for
110
+ * span types that lack a clean OpenInference equivalent. The attribute
111
+ * is tagged `'public'` because the enum value is bounded and contains
112
+ * no PII.
113
+ *
114
+ * @stable
115
+ */
116
+ export function emitOpenInferenceKind<T extends SpanType>(span: AISpan<T>): void {
117
+ const kind = KIND_LOOKUP.get(span.type);
118
+ if (kind === undefined) return;
119
+ const gs = asGraphorinSpan(span);
120
+ if (gs !== null) {
121
+ gs.setAttribute('openinference.span.kind', kind, { sensitivity: 'public' });
122
+ } else {
123
+ span.setAttributes({ 'openinference.span.kind': kind });
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Full canonical span-to-kind table - exposed for tooling and tests
129
+ * that need to introspect the mapping.
130
+ *
131
+ * @stable
132
+ */
133
+ export const OPEN_INFERENCE_KIND_TABLE = KIND_TABLE;