@compilr-dev/agents 0.3.14 → 0.3.15

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.
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Native OpenTelemetry Hooks
3
+ *
4
+ * Creates real OTel spans during agent execution with proper context propagation
5
+ * and gen_ai semantic conventions. Unlike the post-hoc `createOTelExporter()`,
6
+ * these hooks create spans in real time with correct parent-child relationships.
7
+ *
8
+ * Requires `@opentelemetry/api` (regular dependency, no-ops without SDK registered).
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { createOTelHooks } from '@compilr-dev/agents';
13
+ *
14
+ * const hooks = createOTelHooks({
15
+ * providerName: 'claude',
16
+ * tracerName: 'my-agent',
17
+ * });
18
+ *
19
+ * const agent = new Agent({ provider, hooks });
20
+ * ```
21
+ */
22
+ import { trace, context, SpanStatusCode, SpanKind as OTelSpanKind } from '@opentelemetry/api';
23
+ import { GenAIAttributes, AgentAttributes, PROVIDER_TO_SYSTEM } from './otel-attributes.js';
24
+ /**
25
+ * Truncate a value to a maximum length for span attributes
26
+ */
27
+ function truncateValue(value, maxLength) {
28
+ const str = typeof value === 'string' ? value : JSON.stringify(value);
29
+ if (str.length <= maxLength)
30
+ return str;
31
+ return str.slice(0, maxLength - 3) + '...';
32
+ }
33
+ /**
34
+ * Create native OpenTelemetry hooks for agent instrumentation.
35
+ *
36
+ * These hooks create real OTel spans during execution, as opposed to the
37
+ * post-hoc `createOTelExporter()` which recreates spans after the fact.
38
+ *
39
+ * Span hierarchy:
40
+ * ```
41
+ * agent.iteration [INTERNAL]
42
+ * ├── gen_ai.chat [CLIENT] (gen_ai.system, tokens, model)
43
+ * ├── agent.tool.read_file [INTERNAL] (tool.name, success, duration)
44
+ * └── agent.tool.bash [INTERNAL]
45
+ * ```
46
+ *
47
+ * @param config - Optional configuration
48
+ * @returns HooksConfig ready to pass to Agent constructor
49
+ */
50
+ export function createOTelHooks(config = {}) {
51
+ const { tracerName = '@compilr-dev/agents', tracerVersion, providerName, traceIterations = true, traceLLM = true, traceTools = true, includeIO = false, truncateAt = 1000, } = config;
52
+ const tracer = trace.getTracer(tracerName, tracerVersion);
53
+ const system = providerName ? (PROVIDER_TO_SYSTEM[providerName] ?? providerName) : undefined;
54
+ // Track active spans per session
55
+ const activeSessions = new Map();
56
+ const getSession = (sessionId) => {
57
+ let session = activeSessions.get(sessionId);
58
+ if (!session) {
59
+ session = {};
60
+ activeSessions.set(sessionId, session);
61
+ }
62
+ return session;
63
+ };
64
+ const hooks = {};
65
+ // ==========================================================================
66
+ // Iteration Hooks
67
+ // ==========================================================================
68
+ if (traceIterations) {
69
+ const beforeIteration = (ctx) => {
70
+ const session = getSession(ctx.sessionId);
71
+ const span = tracer.startSpan('agent.iteration', {
72
+ kind: OTelSpanKind.INTERNAL,
73
+ attributes: {
74
+ [AgentAttributes.SESSION_ID]: ctx.sessionId,
75
+ [AgentAttributes.ITERATION_NUMBER]: ctx.iteration,
76
+ [AgentAttributes.ITERATION_MAX]: ctx.maxIterations,
77
+ [AgentAttributes.MESSAGE_COUNT]: ctx.messages.length,
78
+ },
79
+ }, context.active());
80
+ session.iterationSpan = span;
81
+ session.iterationContext = trace.setSpan(context.active(), span);
82
+ return undefined;
83
+ };
84
+ const afterIteration = (ctx) => {
85
+ const session = getSession(ctx.sessionId);
86
+ const span = session.iterationSpan;
87
+ if (span) {
88
+ span.setAttributes({
89
+ [AgentAttributes.TOOL_CALL_COUNT]: ctx.toolCalls.length,
90
+ [AgentAttributes.COMPLETED_WITH_TEXT]: ctx.completedWithText,
91
+ });
92
+ span.setStatus({ code: SpanStatusCode.OK });
93
+ span.end();
94
+ session.iterationSpan = undefined;
95
+ session.iterationContext = undefined;
96
+ }
97
+ return undefined;
98
+ };
99
+ hooks.beforeIteration = [beforeIteration];
100
+ hooks.afterIteration = [afterIteration];
101
+ }
102
+ // ==========================================================================
103
+ // LLM Hooks
104
+ // ==========================================================================
105
+ if (traceLLM) {
106
+ const beforeLLM = (ctx) => {
107
+ const session = getSession(ctx.sessionId);
108
+ const parentCtx = session.iterationContext ?? context.active();
109
+ const attributes = {
110
+ [GenAIAttributes.OPERATION_NAME]: 'chat',
111
+ [AgentAttributes.SESSION_ID]: ctx.sessionId,
112
+ [AgentAttributes.MESSAGE_COUNT]: ctx.messages.length,
113
+ };
114
+ if (system) {
115
+ attributes[GenAIAttributes.SYSTEM] = system;
116
+ }
117
+ if (includeIO && ctx.messages.length > 0) {
118
+ const lastMessage = ctx.messages[ctx.messages.length - 1];
119
+ if (typeof lastMessage.content === 'string') {
120
+ attributes['gen_ai.request.last_message'] = truncateValue(lastMessage.content, truncateAt);
121
+ }
122
+ }
123
+ const span = tracer.startSpan('gen_ai.chat', {
124
+ kind: OTelSpanKind.CLIENT,
125
+ attributes,
126
+ }, parentCtx);
127
+ session.llmSpan = span;
128
+ return undefined;
129
+ };
130
+ const afterLLM = (ctx) => {
131
+ const session = getSession(ctx.sessionId);
132
+ const span = session.llmSpan;
133
+ if (span) {
134
+ if (ctx.usage) {
135
+ span.setAttributes({
136
+ [GenAIAttributes.USAGE_INPUT_TOKENS]: ctx.usage.inputTokens,
137
+ [GenAIAttributes.USAGE_OUTPUT_TOKENS]: ctx.usage.outputTokens,
138
+ });
139
+ }
140
+ if (ctx.model) {
141
+ span.setAttribute(GenAIAttributes.RESPONSE_MODEL, ctx.model);
142
+ }
143
+ // Set finish reason based on whether tool calls were made
144
+ const finishReason = ctx.toolUses.length > 0 ? 'tool_calls' : 'end_turn';
145
+ span.setAttribute(GenAIAttributes.RESPONSE_FINISH_REASON, finishReason);
146
+ if (includeIO && ctx.text) {
147
+ span.setAttribute('gen_ai.response.text', truncateValue(ctx.text, truncateAt));
148
+ }
149
+ span.setStatus({ code: SpanStatusCode.OK });
150
+ span.end();
151
+ session.llmSpan = undefined;
152
+ }
153
+ return undefined;
154
+ };
155
+ hooks.beforeLLM = [beforeLLM];
156
+ hooks.afterLLM = [afterLLM];
157
+ }
158
+ // ==========================================================================
159
+ // Tool Hooks
160
+ // ==========================================================================
161
+ if (traceTools) {
162
+ const beforeTool = (ctx) => {
163
+ const session = getSession(ctx.sessionId);
164
+ // Tools are children of the iteration span (LLM span is already ended)
165
+ const parentCtx = session.iterationContext ?? context.active();
166
+ const attributes = {
167
+ [AgentAttributes.TOOL_NAME]: ctx.toolName,
168
+ [AgentAttributes.SESSION_ID]: ctx.sessionId,
169
+ };
170
+ if (includeIO) {
171
+ attributes['agent.tool.input'] = truncateValue(ctx.input, truncateAt);
172
+ }
173
+ const span = tracer.startSpan(`agent.tool.${ctx.toolName}`, {
174
+ kind: OTelSpanKind.INTERNAL,
175
+ attributes,
176
+ }, parentCtx);
177
+ session.toolSpan = span;
178
+ return undefined;
179
+ };
180
+ const afterTool = (ctx) => {
181
+ const session = getSession(ctx.sessionId);
182
+ const span = session.toolSpan;
183
+ if (span) {
184
+ span.setAttributes({
185
+ [AgentAttributes.TOOL_SUCCESS]: ctx.result.success,
186
+ [AgentAttributes.TOOL_DURATION_MS]: ctx.durationMs,
187
+ });
188
+ if (!ctx.result.success && ctx.result.error) {
189
+ span.setStatus({ code: SpanStatusCode.ERROR, message: ctx.result.error });
190
+ }
191
+ else {
192
+ span.setStatus({ code: SpanStatusCode.OK });
193
+ }
194
+ if (includeIO && ctx.result.result !== undefined) {
195
+ span.setAttribute('agent.tool.output', truncateValue(ctx.result.result, truncateAt));
196
+ }
197
+ span.end();
198
+ session.toolSpan = undefined;
199
+ }
200
+ return undefined;
201
+ };
202
+ hooks.beforeTool = [beforeTool];
203
+ hooks.afterTool = [afterTool];
204
+ }
205
+ // ==========================================================================
206
+ // Error Hook
207
+ // ==========================================================================
208
+ const onError = (ctx) => {
209
+ const session = getSession(ctx.sessionId);
210
+ // Record exception on the most specific active span
211
+ const span = session.toolSpan ?? session.llmSpan ?? session.iterationSpan;
212
+ if (span) {
213
+ span.recordException(ctx.error);
214
+ span.setStatus({ code: SpanStatusCode.ERROR, message: ctx.error.message });
215
+ }
216
+ return undefined;
217
+ };
218
+ hooks.onError = [onError];
219
+ return hooks;
220
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compilr-dev/agents",
3
- "version": "0.3.14",
3
+ "version": "0.3.15",
4
4
  "description": "Lightweight multi-LLM agent library for building CLI AI assistants",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -67,6 +67,7 @@
67
67
  "@anthropic-ai/sdk": "^0.74.0",
68
68
  "@eslint/js": "^9.39.1",
69
69
  "@modelcontextprotocol/sdk": "^1.23.0",
70
+ "@opentelemetry/sdk-trace-base": "^2.5.1",
70
71
  "@types/node": "^25.2.3",
71
72
  "@vitest/coverage-v8": "^4.0.18",
72
73
  "dotenv": "^17.2.3",
@@ -78,6 +79,7 @@
78
79
  },
79
80
  "dependencies": {
80
81
  "@google/genai": "^1.42.0",
82
+ "@opentelemetry/api": "^1.9.0",
81
83
  "js-tiktoken": "^1.0.21"
82
84
  },
83
85
  "overrides": {