toad-eye 2.3.0 → 2.4.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/README.md +123 -30
- package/dist/agent.d.ts +17 -5
- package/dist/agent.js +78 -16
- package/dist/core/metrics.js +1 -1
- package/dist/core/spans.js +1 -1
- package/dist/core/tracer.d.ts +6 -0
- package/dist/core/tracer.js +9 -0
- package/dist/instrumentations/create.js +1 -1
- package/dist/types/attributes.d.ts +17 -0
- package/dist/types/attributes.js +20 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/spans.d.ts +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,8 @@ You don't know what your LLMs cost, how slow they are, or why they fail. toad-ey
|
|
|
18
18
|
|
|
19
19
|
## Quick start
|
|
20
20
|
|
|
21
|
+
**Prerequisites:** [Docker Desktop](https://docs.docker.com/get-started/get-docker/) (or Docker Engine + Compose plugin)
|
|
22
|
+
|
|
21
23
|
```bash
|
|
22
24
|
npm install toad-eye
|
|
23
25
|
npx toad-eye init # scaffold observability stack
|
|
@@ -25,6 +27,8 @@ npx toad-eye up # start Grafana + Prometheus + Jaeger
|
|
|
25
27
|
npx toad-eye demo # send mock LLM traffic, see data immediately
|
|
26
28
|
```
|
|
27
29
|
|
|
30
|
+
> Dashboards will be empty until data arrives. Run `npx toad-eye demo` to send mock traffic and verify the stack works.
|
|
31
|
+
|
|
28
32
|
```typescript
|
|
29
33
|
import { initObservability } from "toad-eye";
|
|
30
34
|
|
|
@@ -36,6 +40,8 @@ initObservability({
|
|
|
36
40
|
// That's it. Every SDK call is auto-traced — including streaming.
|
|
37
41
|
```
|
|
38
42
|
|
|
43
|
+
> ⚠️ By default, toad-eye records prompt and completion text in spans. Set `recordContent: false` for PII-sensitive workloads (healthcare, finance, legal).
|
|
44
|
+
|
|
39
45
|
Open [localhost:3100](http://localhost:3100) (Grafana, admin/admin) to see your dashboards.
|
|
40
46
|
|
|
41
47
|
## What you get
|
|
@@ -46,16 +52,24 @@ Open [localhost:3100](http://localhost:3100) (Grafana, admin/admin) to see your
|
|
|
46
52
|
| **8 Grafana dashboards** | Overview, Cost, Latency, Errors, Model Comparison, FinOps, Provider Health, Agent Workflow |
|
|
47
53
|
| **Cost tracking** | Per-request USD cost, daily totals, projected monthly spend |
|
|
48
54
|
| **Budget guards** | Daily/per-user/per-model spend limits — warn, block, or auto-downgrade |
|
|
49
|
-
| **Multi-agent tracing** | ReAct steps, handoffs, loop detection, maxSteps guard |
|
|
50
|
-
| **Shadow guardrails** | Record what _would_ be blocked without blocking — tune thresholds on live traffic |
|
|
51
|
-
| **Quality proxy** | Empty response rate, latency per token — zero-dependency quality metrics |
|
|
52
|
-
| **Semantic drift** | Detect silent quality degradation via embedding comparison |
|
|
53
55
|
| **Privacy controls** | Built-in PII redaction (email, SSN, CC, phone), hashing, content masking |
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
|
56
|
+
|
|
57
|
+
<details>
|
|
58
|
+
<summary>Advanced features</summary>
|
|
59
|
+
|
|
60
|
+
| Feature | Description |
|
|
61
|
+
| ----------------------- | --------------------------------------------------------------------------------- |
|
|
62
|
+
| **Multi-agent tracing** | ReAct steps, handoffs, loop detection, maxSteps guard |
|
|
63
|
+
| **Shadow guardrails** | Record what _would_ be blocked without blocking — tune thresholds on live traffic |
|
|
64
|
+
| **Quality proxy** | Empty response rate, latency per token — zero-dependency quality metrics |
|
|
65
|
+
| **Semantic drift** | Detect silent quality degradation via embedding comparison |
|
|
66
|
+
| **Tail sampling** | Keep 100% errors + slow traces, sample healthy traffic via OTel Collector |
|
|
67
|
+
| **Alerting** | Cost spikes, latency anomalies, error rate — via Telegram, Slack, email, webhook |
|
|
68
|
+
| **FinOps attribution** | Break down costs by team, user, feature, environment |
|
|
69
|
+
| **TTFT metric** | Time To First Token for streaming — separate from total duration |
|
|
70
|
+
| **Trace export** | Convert production Jaeger traces into regression test cases |
|
|
71
|
+
|
|
72
|
+
</details>
|
|
59
73
|
|
|
60
74
|
## Auto-instrumentation 🤖
|
|
61
75
|
|
|
@@ -77,17 +91,35 @@ Vercel AI SDK uses a SpanProcessor (not monkey-patching) — add `instrument: ['
|
|
|
77
91
|
<details>
|
|
78
92
|
<summary>Manual instrumentation (custom providers)</summary>
|
|
79
93
|
|
|
94
|
+
Use `traceLLMCall` when you need to instrument a provider not in the auto-instrument list, or want explicit control over span data:
|
|
95
|
+
|
|
80
96
|
```typescript
|
|
97
|
+
import OpenAI from "openai";
|
|
81
98
|
import { initObservability, traceLLMCall } from "toad-eye";
|
|
82
99
|
|
|
83
100
|
initObservability({ serviceName: "my-app" });
|
|
84
101
|
|
|
102
|
+
const openai = new OpenAI();
|
|
103
|
+
|
|
85
104
|
const result = await traceLLMCall(
|
|
86
|
-
{ provider: "
|
|
87
|
-
() =>
|
|
105
|
+
{ provider: "openai", model: "gpt-4o", prompt: "Hello" },
|
|
106
|
+
async () => {
|
|
107
|
+
const res = await openai.chat.completions.create({
|
|
108
|
+
model: "gpt-4o",
|
|
109
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
completion: res.choices[0]?.message.content ?? "",
|
|
113
|
+
inputTokens: res.usage?.prompt_tokens ?? 0,
|
|
114
|
+
outputTokens: res.usage?.completion_tokens ?? 0,
|
|
115
|
+
// cost is auto-calculated from model + tokens if omitted
|
|
116
|
+
};
|
|
117
|
+
},
|
|
88
118
|
);
|
|
89
119
|
```
|
|
90
120
|
|
|
121
|
+
> Or skip this entirely — use `instrument: ['openai']` for zero-code instrumentation.
|
|
122
|
+
|
|
91
123
|
</details>
|
|
92
124
|
|
|
93
125
|
## Budget guards
|
|
@@ -114,11 +146,36 @@ initObservability({
|
|
|
114
146
|
|
|
115
147
|
## Agent observability
|
|
116
148
|
|
|
117
|
-
Structured tracing for ReAct agents with multi-agent support, handoffs, and loop detection.
|
|
149
|
+
Structured tracing for ReAct agents with multi-agent support, handoffs, and loop detection. Span names follow [OTel GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-agent-spans/).
|
|
118
150
|
|
|
119
151
|
```typescript
|
|
120
152
|
import { traceAgentQuery } from "toad-eye";
|
|
121
153
|
|
|
154
|
+
const result = await traceAgentQuery(
|
|
155
|
+
{
|
|
156
|
+
query: "What's the weather like?",
|
|
157
|
+
agentName: "weather-bot", // → gen_ai.agent.name (OTel semconv)
|
|
158
|
+
},
|
|
159
|
+
async (step) => {
|
|
160
|
+
step({ type: "think", stepNumber: 1, content: "Need weather data" });
|
|
161
|
+
const data = await getWeather();
|
|
162
|
+
step({
|
|
163
|
+
type: "act",
|
|
164
|
+
stepNumber: 2,
|
|
165
|
+
toolName: "get_weather",
|
|
166
|
+
toolType: "function",
|
|
167
|
+
});
|
|
168
|
+
step({ type: "answer", stepNumber: 3, content: data.summary });
|
|
169
|
+
return { answer: data.summary };
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
// Produces spans: invoke_agent weather-bot → execute_tool get_weather
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
<details>
|
|
176
|
+
<summary>Full ReAct pattern (think → act → observe → answer)</summary>
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
122
179
|
const result = await traceAgentQuery(
|
|
123
180
|
"Is anything dangerous near Earth?",
|
|
124
181
|
async (step) => {
|
|
@@ -144,6 +201,8 @@ const result = await traceAgentQuery(
|
|
|
144
201
|
);
|
|
145
202
|
```
|
|
146
203
|
|
|
204
|
+
</details>
|
|
205
|
+
|
|
147
206
|
## FinOps attribution
|
|
148
207
|
|
|
149
208
|
Track costs by team, user, feature, or any business dimension:
|
|
@@ -201,6 +260,14 @@ monitor.checkInBackground(response, "openai", "gpt-4o");
|
|
|
201
260
|
|
|
202
261
|
### Privacy controls
|
|
203
262
|
|
|
263
|
+
| Goal | Config |
|
|
264
|
+
| --------------------------- | -------------------------------- |
|
|
265
|
+
| Don't store text at all | `recordContent: false` |
|
|
266
|
+
| Remove common PII | `redactDefaults: true` |
|
|
267
|
+
| Remove custom patterns | `redactPatterns: [/regex/g]` |
|
|
268
|
+
| Store hashes only (no text) | `hashContent: true, salt: "..."` |
|
|
269
|
+
| Audit what was masked | `auditMasking: true` |
|
|
270
|
+
|
|
204
271
|
```typescript
|
|
205
272
|
initObservability({
|
|
206
273
|
serviceName: "my-app",
|
|
@@ -289,11 +356,25 @@ Self-hosted mode remains the default. Cloud mode activates automatically when `a
|
|
|
289
356
|
| **Provider Health** | Provider status (healthy/degraded/down), uptime, error breakdown |
|
|
290
357
|
| **Agent Workflow** | Steps per query, tool usage frequency, step type breakdown |
|
|
291
358
|
|
|
359
|
+
## Subpath imports
|
|
360
|
+
|
|
361
|
+
Advanced modules are available as separate entry points to keep your bundle lean:
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
import { initObservability, traceLLMCall } from "toad-eye"; // core
|
|
365
|
+
import { AlertManager } from "toad-eye/alerts"; // alerting
|
|
366
|
+
import { createDriftMonitor } from "toad-eye/drift"; // semantic drift
|
|
367
|
+
import { exportTrace } from "toad-eye/export"; // trace → YAML
|
|
368
|
+
import { ToadEyeAISpanProcessor, withToadEye } from "toad-eye/vercel"; // Vercel AI SDK
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
> Everything is also re-exported from the main `"toad-eye"` entry point for convenience.
|
|
372
|
+
|
|
292
373
|
## CLI
|
|
293
374
|
|
|
294
375
|
| Command | Description |
|
|
295
376
|
| -------------------------------- | ----------------------------------------------- |
|
|
296
|
-
| `npx toad-eye init`
|
|
377
|
+
| `npx toad-eye init [--force]` | Scaffold Docker Compose + observability configs |
|
|
297
378
|
| `npx toad-eye up` | Start the stack |
|
|
298
379
|
| `npx toad-eye down` | Stop the stack |
|
|
299
380
|
| `npx toad-eye status` | Show running services and URLs |
|
|
@@ -330,22 +411,26 @@ Self-hosted mode remains the default. Cloud mode activates automatically when `a
|
|
|
330
411
|
| `gen_ai.toad_eye.response.empty` | Counter | Empty/whitespace responses |
|
|
331
412
|
| `gen_ai.toad_eye.response.latency_per_token` | Histogram | Generation speed (ms/token) |
|
|
332
413
|
|
|
333
|
-
### Span attributes
|
|
334
|
-
|
|
335
|
-
| Attribute
|
|
336
|
-
|
|
|
337
|
-
| `gen_ai.
|
|
338
|
-
| `gen_ai.
|
|
339
|
-
| `gen_ai.
|
|
340
|
-
| `gen_ai.usage.
|
|
341
|
-
| `gen_ai.
|
|
342
|
-
| `gen_ai.
|
|
343
|
-
| `gen_ai.agent.
|
|
344
|
-
| `gen_ai.agent.
|
|
345
|
-
| `gen_ai.
|
|
346
|
-
| `gen_ai.
|
|
347
|
-
| `gen_ai.toad_eye.
|
|
348
|
-
| `
|
|
414
|
+
### Span attributes (OTel GenAI semconv)
|
|
415
|
+
|
|
416
|
+
| Attribute | Description |
|
|
417
|
+
| ---------------------------------- | ----------------------------------------------- |
|
|
418
|
+
| `gen_ai.operation.name` | `chat`, `invoke_agent`, `execute_tool` |
|
|
419
|
+
| `gen_ai.provider.name` | `anthropic`, `gemini`, `openai` |
|
|
420
|
+
| `gen_ai.request.model` | Model identifier |
|
|
421
|
+
| `gen_ai.usage.input_tokens` | Tokens in the prompt |
|
|
422
|
+
| `gen_ai.usage.output_tokens` | Tokens in the completion |
|
|
423
|
+
| `gen_ai.request.temperature` | Temperature parameter |
|
|
424
|
+
| `gen_ai.agent.name` | Agent name (for agent spans) |
|
|
425
|
+
| `gen_ai.agent.id` | Agent identifier |
|
|
426
|
+
| `gen_ai.tool.name` | Tool name for execute_tool spans |
|
|
427
|
+
| `gen_ai.tool.type` | `function`, `extension`, `retrieval`, `builtin` |
|
|
428
|
+
| `gen_ai.toad_eye.cost` | Cost in USD |
|
|
429
|
+
| `gen_ai.toad_eye.agent.step.type` | ReAct step: think/act/observe/answer/handoff |
|
|
430
|
+
| `gen_ai.toad_eye.agent.handoff.to` | Target agent for handoff steps |
|
|
431
|
+
| `gen_ai.toad_eye.agent.loop_count` | Agent loop iterations (observe→think) |
|
|
432
|
+
| `gen_ai.toad_eye.guard.mode` | Guard mode: shadow/enforce |
|
|
433
|
+
| `session.id` | Session identifier (if configured) |
|
|
349
434
|
|
|
350
435
|
</details>
|
|
351
436
|
|
|
@@ -358,9 +443,17 @@ Self-hosted mode remains the default. Cloud mode activates automatically when `a
|
|
|
358
443
|
| Prometheus | [localhost:9090](http://localhost:9090) |
|
|
359
444
|
| OTel Collector | [localhost:4318](http://localhost:4318) |
|
|
360
445
|
|
|
446
|
+
## OTel GenAI semconv
|
|
447
|
+
|
|
448
|
+
toad-eye follows [OTel GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/) for span names, attributes, and metrics. Traces are natively compatible with Jaeger, Arize Phoenix, SigNoz, Datadog, Langfuse, and other OTel backends.
|
|
449
|
+
|
|
450
|
+
See [COMPATIBILITY.md](https://github.com/vola-trebla/toad-eye/blob/main/packages/instrumentation/COMPATIBILITY.md) for a full backend support matrix.
|
|
451
|
+
|
|
452
|
+
Set `OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental` to disable deprecated attribute aliases and emit only the latest conventions.
|
|
453
|
+
|
|
361
454
|
## Tech stack
|
|
362
455
|
|
|
363
456
|
- TypeScript, OpenTelemetry SDK 2.x, OTel GenAI semantic conventions
|
|
364
457
|
- Hono (demo server + cloud ingestion server)
|
|
365
458
|
- Docker Compose (Prometheus, Jaeger, Grafana, OTel Collector)
|
|
366
|
-
- Vitest (
|
|
459
|
+
- Vitest (252+ tests)
|
package/dist/agent.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentStepInput, AgentQueryOptions } from "./types/index.js";
|
|
1
|
+
import type { AgentStepInput, AgentQueryOptions, AgentQueryInput } from "./types/index.js";
|
|
2
2
|
/**
|
|
3
3
|
* Record a single agent step as a child span (ReAct pattern).
|
|
4
4
|
* Supports: think, act, observe, answer, handoff.
|
|
@@ -7,6 +7,9 @@ declare function traceAgentStep(input: AgentStepInput): void;
|
|
|
7
7
|
/**
|
|
8
8
|
* Wrap an entire agent query in a parent span with ReAct tracing.
|
|
9
9
|
*
|
|
10
|
+
* Accepts either a string query (backward-compatible) or an object with
|
|
11
|
+
* agent metadata per OTel GenAI spec (agentName, agentId).
|
|
12
|
+
*
|
|
10
13
|
* Supports multi-agent via nesting — child traceAgentQuery calls
|
|
11
14
|
* automatically become child spans in Jaeger.
|
|
12
15
|
*
|
|
@@ -15,18 +18,27 @@ declare function traceAgentStep(input: AgentStepInput): void;
|
|
|
15
18
|
*
|
|
16
19
|
* @example
|
|
17
20
|
* ```ts
|
|
21
|
+
* // Simple form (backward-compatible)
|
|
22
|
+
* await traceAgentQuery('Is anything dangerous?', async (step) => { ... });
|
|
23
|
+
*
|
|
24
|
+
* // Object form with agent metadata (OTel GenAI spec)
|
|
25
|
+
* await traceAgentQuery(
|
|
26
|
+
* { query: 'Is anything dangerous?', agentName: 'space-monitor', agentId: 'agent-001' },
|
|
27
|
+
* async (step) => { ... }
|
|
28
|
+
* );
|
|
29
|
+
*
|
|
18
30
|
* // Multi-agent: orchestrator delegates to specialist
|
|
19
|
-
* await traceAgentQuery('orchestrator', async (step) => {
|
|
31
|
+
* await traceAgentQuery({ query: 'Analyze data', agentName: 'orchestrator' }, async (step) => {
|
|
20
32
|
* step({ type: 'think', stepNumber: 1, content: 'Need domain expert' });
|
|
21
33
|
* step({ type: 'handoff', stepNumber: 2, toAgent: 'specialist', handoffReason: 'domain expertise' });
|
|
22
34
|
*
|
|
23
|
-
* const result = await traceAgentQuery('specialist', async (step) => {
|
|
24
|
-
* step({ type: 'act', stepNumber: 1, toolName: 'analyze' });
|
|
35
|
+
* const result = await traceAgentQuery({ query: 'Analyze', agentName: 'specialist' }, async (step) => {
|
|
36
|
+
* step({ type: 'act', stepNumber: 1, toolName: 'analyze', toolType: 'function' });
|
|
25
37
|
* step({ type: 'answer', stepNumber: 2, content: 'analysis complete' });
|
|
26
38
|
* return { answer: 'done' };
|
|
27
39
|
* });
|
|
28
40
|
* });
|
|
29
41
|
* ```
|
|
30
42
|
*/
|
|
31
|
-
export declare function traceAgentQuery<T>(
|
|
43
|
+
export declare function traceAgentQuery<T>(queryOrInput: string | AgentQueryInput, fn: (step: (input: AgentStepInput) => void) => Promise<T>, options?: AgentQueryOptions): Promise<T>;
|
|
32
44
|
export { traceAgentStep };
|
package/dist/agent.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { trace, SpanStatusCode } from "@opentelemetry/api";
|
|
2
2
|
import { GEN_AI_ATTRS, INSTRUMENTATION_NAME } from "./types/index.js";
|
|
3
3
|
import { recordAgentSteps, recordAgentToolUsage } from "./core/metrics.js";
|
|
4
|
-
import { getConfig } from "./core/tracer.js";
|
|
4
|
+
import { getConfig, shouldEmitDeprecatedAttrs } from "./core/tracer.js";
|
|
5
5
|
const tracer = trace.getTracer(INSTRUMENTATION_NAME);
|
|
6
6
|
const DEFAULT_MAX_STEPS = 25;
|
|
7
7
|
/**
|
|
@@ -9,25 +9,52 @@ const DEFAULT_MAX_STEPS = 25;
|
|
|
9
9
|
* Supports: think, act, observe, answer, handoff.
|
|
10
10
|
*/
|
|
11
11
|
function traceAgentStep(input) {
|
|
12
|
-
const
|
|
12
|
+
const spanName = input.type === "act" && input.toolName !== undefined
|
|
13
|
+
? `execute_tool ${input.toolName}`
|
|
14
|
+
: `gen_ai.agent.step.${input.type}`;
|
|
15
|
+
const span = tracer.startSpan(spanName);
|
|
13
16
|
const config = getConfig();
|
|
14
17
|
const recordContent = config?.recordContent !== false;
|
|
18
|
+
const emitDeprecated = shouldEmitDeprecatedAttrs();
|
|
15
19
|
span.setAttributes({
|
|
16
|
-
|
|
17
|
-
[GEN_AI_ATTRS.
|
|
20
|
+
// Canonical toad_eye namespace
|
|
21
|
+
[GEN_AI_ATTRS.TOAD_AGENT_STEP_TYPE]: input.type,
|
|
22
|
+
[GEN_AI_ATTRS.TOAD_AGENT_STEP_NUMBER]: input.stepNumber,
|
|
23
|
+
// Deprecated aliases (skipped when OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental)
|
|
24
|
+
...(emitDeprecated && {
|
|
25
|
+
[GEN_AI_ATTRS.AGENT_STEP_TYPE]: input.type,
|
|
26
|
+
[GEN_AI_ATTRS.AGENT_STEP_NUMBER]: input.stepNumber,
|
|
27
|
+
}),
|
|
18
28
|
...(input.toolName !== undefined && {
|
|
19
|
-
|
|
29
|
+
// OTel spec attribute
|
|
30
|
+
[GEN_AI_ATTRS.TOOL_NAME]: input.toolName,
|
|
31
|
+
// Deprecated alias
|
|
32
|
+
...(emitDeprecated && {
|
|
33
|
+
[GEN_AI_ATTRS.AGENT_TOOL_NAME]: input.toolName,
|
|
34
|
+
}),
|
|
35
|
+
}),
|
|
36
|
+
...(input.toolType !== undefined && {
|
|
37
|
+
[GEN_AI_ATTRS.TOOL_TYPE]: input.toolType,
|
|
20
38
|
}),
|
|
21
39
|
...(recordContent &&
|
|
22
40
|
input.content !== undefined && {
|
|
23
|
-
[GEN_AI_ATTRS.
|
|
41
|
+
[GEN_AI_ATTRS.TOAD_AGENT_STEP_CONTENT]: input.content,
|
|
42
|
+
...(emitDeprecated && {
|
|
43
|
+
[GEN_AI_ATTRS.AGENT_STEP_CONTENT]: input.content,
|
|
44
|
+
}),
|
|
24
45
|
}),
|
|
25
46
|
// Handoff attributes
|
|
26
47
|
...(input.toAgent !== undefined && {
|
|
27
|
-
[GEN_AI_ATTRS.
|
|
48
|
+
[GEN_AI_ATTRS.TOAD_AGENT_HANDOFF_TO]: input.toAgent,
|
|
49
|
+
...(emitDeprecated && {
|
|
50
|
+
[GEN_AI_ATTRS.AGENT_HANDOFF_TO]: input.toAgent,
|
|
51
|
+
}),
|
|
28
52
|
}),
|
|
29
53
|
...(input.handoffReason !== undefined && {
|
|
30
|
-
[GEN_AI_ATTRS.
|
|
54
|
+
[GEN_AI_ATTRS.TOAD_AGENT_HANDOFF_REASON]: input.handoffReason,
|
|
55
|
+
...(emitDeprecated && {
|
|
56
|
+
[GEN_AI_ATTRS.AGENT_HANDOFF_REASON]: input.handoffReason,
|
|
57
|
+
}),
|
|
31
58
|
}),
|
|
32
59
|
});
|
|
33
60
|
if (input.type === "act" && input.toolName !== undefined) {
|
|
@@ -39,6 +66,9 @@ function traceAgentStep(input) {
|
|
|
39
66
|
/**
|
|
40
67
|
* Wrap an entire agent query in a parent span with ReAct tracing.
|
|
41
68
|
*
|
|
69
|
+
* Accepts either a string query (backward-compatible) or an object with
|
|
70
|
+
* agent metadata per OTel GenAI spec (agentName, agentId).
|
|
71
|
+
*
|
|
42
72
|
* Supports multi-agent via nesting — child traceAgentQuery calls
|
|
43
73
|
* automatically become child spans in Jaeger.
|
|
44
74
|
*
|
|
@@ -47,26 +77,52 @@ function traceAgentStep(input) {
|
|
|
47
77
|
*
|
|
48
78
|
* @example
|
|
49
79
|
* ```ts
|
|
80
|
+
* // Simple form (backward-compatible)
|
|
81
|
+
* await traceAgentQuery('Is anything dangerous?', async (step) => { ... });
|
|
82
|
+
*
|
|
83
|
+
* // Object form with agent metadata (OTel GenAI spec)
|
|
84
|
+
* await traceAgentQuery(
|
|
85
|
+
* { query: 'Is anything dangerous?', agentName: 'space-monitor', agentId: 'agent-001' },
|
|
86
|
+
* async (step) => { ... }
|
|
87
|
+
* );
|
|
88
|
+
*
|
|
50
89
|
* // Multi-agent: orchestrator delegates to specialist
|
|
51
|
-
* await traceAgentQuery('orchestrator', async (step) => {
|
|
90
|
+
* await traceAgentQuery({ query: 'Analyze data', agentName: 'orchestrator' }, async (step) => {
|
|
52
91
|
* step({ type: 'think', stepNumber: 1, content: 'Need domain expert' });
|
|
53
92
|
* step({ type: 'handoff', stepNumber: 2, toAgent: 'specialist', handoffReason: 'domain expertise' });
|
|
54
93
|
*
|
|
55
|
-
* const result = await traceAgentQuery('specialist', async (step) => {
|
|
56
|
-
* step({ type: 'act', stepNumber: 1, toolName: 'analyze' });
|
|
94
|
+
* const result = await traceAgentQuery({ query: 'Analyze', agentName: 'specialist' }, async (step) => {
|
|
95
|
+
* step({ type: 'act', stepNumber: 1, toolName: 'analyze', toolType: 'function' });
|
|
57
96
|
* step({ type: 'answer', stepNumber: 2, content: 'analysis complete' });
|
|
58
97
|
* return { answer: 'done' };
|
|
59
98
|
* });
|
|
60
99
|
* });
|
|
61
100
|
* ```
|
|
62
101
|
*/
|
|
63
|
-
export async function traceAgentQuery(
|
|
64
|
-
|
|
102
|
+
export async function traceAgentQuery(queryOrInput, fn, options) {
|
|
103
|
+
const resolved = typeof queryOrInput === "string"
|
|
104
|
+
? { query: queryOrInput, agentName: undefined, agentId: undefined }
|
|
105
|
+
: queryOrInput;
|
|
106
|
+
const spanName = resolved.agentName
|
|
107
|
+
? `invoke_agent ${resolved.agentName}`
|
|
108
|
+
: `invoke_agent`;
|
|
109
|
+
return tracer.startActiveSpan(spanName, async (span) => {
|
|
65
110
|
const config = getConfig();
|
|
66
111
|
const recordContent = config?.recordContent !== false;
|
|
112
|
+
const emitDeprecated = shouldEmitDeprecatedAttrs();
|
|
67
113
|
const maxSteps = options?.maxSteps ?? DEFAULT_MAX_STEPS;
|
|
114
|
+
// OTel GenAI agent attributes
|
|
115
|
+
if (resolved.agentName !== undefined) {
|
|
116
|
+
span.setAttribute(GEN_AI_ATTRS.AGENT_NAME, resolved.agentName);
|
|
117
|
+
}
|
|
118
|
+
if (resolved.agentId !== undefined) {
|
|
119
|
+
span.setAttribute(GEN_AI_ATTRS.AGENT_ID, resolved.agentId);
|
|
120
|
+
}
|
|
68
121
|
if (recordContent) {
|
|
69
|
-
span.setAttribute(GEN_AI_ATTRS.
|
|
122
|
+
span.setAttribute(GEN_AI_ATTRS.TOAD_AGENT_STEP_CONTENT, resolved.query);
|
|
123
|
+
if (emitDeprecated) {
|
|
124
|
+
span.setAttribute(GEN_AI_ATTRS.AGENT_STEP_CONTENT, resolved.query);
|
|
125
|
+
}
|
|
70
126
|
}
|
|
71
127
|
let stepCount = 0;
|
|
72
128
|
let loopCount = 0;
|
|
@@ -91,13 +147,19 @@ export async function traceAgentQuery(query, fn, options) {
|
|
|
91
147
|
}
|
|
92
148
|
traceAgentStep(input);
|
|
93
149
|
});
|
|
94
|
-
span.setAttribute(GEN_AI_ATTRS.
|
|
150
|
+
span.setAttribute(GEN_AI_ATTRS.TOAD_AGENT_LOOP_COUNT, loopCount);
|
|
151
|
+
if (emitDeprecated) {
|
|
152
|
+
span.setAttribute(GEN_AI_ATTRS.AGENT_LOOP_COUNT, loopCount);
|
|
153
|
+
}
|
|
95
154
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
96
155
|
return result;
|
|
97
156
|
}
|
|
98
157
|
catch (error) {
|
|
99
158
|
const message = error instanceof Error ? error.message : String(error);
|
|
100
|
-
span.setAttribute(GEN_AI_ATTRS.
|
|
159
|
+
span.setAttribute(GEN_AI_ATTRS.TOAD_AGENT_LOOP_COUNT, loopCount);
|
|
160
|
+
if (emitDeprecated) {
|
|
161
|
+
span.setAttribute(GEN_AI_ATTRS.AGENT_LOOP_COUNT, loopCount);
|
|
162
|
+
}
|
|
101
163
|
span.setStatus({ code: SpanStatusCode.ERROR, message });
|
|
102
164
|
throw error;
|
|
103
165
|
}
|
package/dist/core/metrics.js
CHANGED
|
@@ -107,7 +107,7 @@ export function recordAgentSteps(stepCount) {
|
|
|
107
107
|
}
|
|
108
108
|
export function recordAgentToolUsage(toolName) {
|
|
109
109
|
agentToolUsage.add(1, {
|
|
110
|
-
[GEN_AI_ATTRS.
|
|
110
|
+
[GEN_AI_ATTRS.TOOL_NAME]: toolName,
|
|
111
111
|
});
|
|
112
112
|
}
|
|
113
113
|
export function recordGuardEvaluation(ruleName) {
|
package/dist/core/spans.js
CHANGED
|
@@ -153,7 +153,7 @@ export async function traceLLMCall(input, fn) {
|
|
|
153
153
|
recordBudgetDowngraded(override.budget);
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
|
-
return tracer.startActiveSpan(`
|
|
156
|
+
return tracer.startActiveSpan(`chat ${effectiveInput.model}`, async (span) => {
|
|
157
157
|
const start = performance.now();
|
|
158
158
|
setBaseAttributes(span, effectiveInput);
|
|
159
159
|
try {
|
package/dist/core/tracer.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { ToadEyeConfig } from "../types/index.js";
|
|
2
2
|
import { BudgetTracker } from "../budget/index.js";
|
|
3
3
|
export declare function getConfig(): ToadEyeConfig | null;
|
|
4
|
+
/**
|
|
5
|
+
* Check if deprecated semconv aliases should be emitted.
|
|
6
|
+
* When OTEL_SEMCONV_STABILITY_OPT_IN includes "gen_ai_latest_experimental",
|
|
7
|
+
* only new (canonical) attributes are emitted — deprecated aliases are skipped.
|
|
8
|
+
*/
|
|
9
|
+
export declare function shouldEmitDeprecatedAttrs(): boolean;
|
|
4
10
|
export declare function getBudgetTracker(): BudgetTracker | null;
|
|
5
11
|
export declare function initObservability(config: ToadEyeConfig): void;
|
|
6
12
|
export declare function shutdown(): Promise<void>;
|
package/dist/core/tracer.js
CHANGED
|
@@ -18,6 +18,15 @@ let budgetTracker = null;
|
|
|
18
18
|
export function getConfig() {
|
|
19
19
|
return currentConfig;
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Check if deprecated semconv aliases should be emitted.
|
|
23
|
+
* When OTEL_SEMCONV_STABILITY_OPT_IN includes "gen_ai_latest_experimental",
|
|
24
|
+
* only new (canonical) attributes are emitted — deprecated aliases are skipped.
|
|
25
|
+
*/
|
|
26
|
+
export function shouldEmitDeprecatedAttrs() {
|
|
27
|
+
const optIn = process.env["OTEL_SEMCONV_STABILITY_OPT_IN"] ?? "";
|
|
28
|
+
return !optIn.split(",").includes("gen_ai_latest_experimental");
|
|
29
|
+
}
|
|
21
30
|
export function getBudgetTracker() {
|
|
22
31
|
return budgetTracker;
|
|
23
32
|
}
|
|
@@ -84,7 +84,7 @@ original, patch, providerName) {
|
|
|
84
84
|
recordBudgetDowngraded(override.budget);
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
-
const span = tracer.startSpan(`
|
|
87
|
+
const span = tracer.startSpan(`chat ${effectiveModel}`);
|
|
88
88
|
const ctx = trace.setSpan(context.active(), span);
|
|
89
89
|
const sessionId = config?.sessionExtractor?.() ?? config?.sessionId;
|
|
90
90
|
span.setAttributes({
|
|
@@ -15,12 +15,29 @@ export declare const GEN_AI_ATTRS: {
|
|
|
15
15
|
readonly OPERATION: "gen_ai.operation.name";
|
|
16
16
|
readonly FINISH_REASONS: "gen_ai.response.finish_reasons";
|
|
17
17
|
readonly ERROR: "error.type";
|
|
18
|
+
readonly AGENT_NAME: "gen_ai.agent.name";
|
|
19
|
+
readonly AGENT_ID: "gen_ai.agent.id";
|
|
20
|
+
readonly TOOL_NAME: "gen_ai.tool.name";
|
|
21
|
+
readonly TOOL_TYPE: "gen_ai.tool.type";
|
|
22
|
+
readonly TOAD_AGENT_STEP_TYPE: "gen_ai.toad_eye.agent.step.type";
|
|
23
|
+
readonly TOAD_AGENT_STEP_NUMBER: "gen_ai.toad_eye.agent.step.number";
|
|
24
|
+
readonly TOAD_AGENT_STEP_CONTENT: "gen_ai.toad_eye.agent.step.content";
|
|
25
|
+
readonly TOAD_AGENT_HANDOFF_TO: "gen_ai.toad_eye.agent.handoff.to";
|
|
26
|
+
readonly TOAD_AGENT_HANDOFF_REASON: "gen_ai.toad_eye.agent.handoff.reason";
|
|
27
|
+
readonly TOAD_AGENT_LOOP_COUNT: "gen_ai.toad_eye.agent.loop_count";
|
|
28
|
+
/** @deprecated Use TOAD_AGENT_STEP_TYPE instead. Will be removed in v3.0. */
|
|
18
29
|
readonly AGENT_STEP_TYPE: "gen_ai.agent.step.type";
|
|
30
|
+
/** @deprecated Use TOAD_AGENT_STEP_NUMBER instead. Will be removed in v3.0. */
|
|
19
31
|
readonly AGENT_STEP_NUMBER: "gen_ai.agent.step.number";
|
|
32
|
+
/** @deprecated Use TOOL_NAME instead. Will be removed in v3.0. */
|
|
20
33
|
readonly AGENT_TOOL_NAME: "gen_ai.agent.tool.name";
|
|
34
|
+
/** @deprecated Use TOAD_AGENT_STEP_CONTENT instead. Will be removed in v3.0. */
|
|
21
35
|
readonly AGENT_STEP_CONTENT: "gen_ai.agent.step.content";
|
|
36
|
+
/** @deprecated Use TOAD_AGENT_HANDOFF_TO instead. Will be removed in v3.0. */
|
|
22
37
|
readonly AGENT_HANDOFF_TO: "gen_ai.agent.handoff.to";
|
|
38
|
+
/** @deprecated Use TOAD_AGENT_HANDOFF_REASON instead. Will be removed in v3.0. */
|
|
23
39
|
readonly AGENT_HANDOFF_REASON: "gen_ai.agent.handoff.reason";
|
|
40
|
+
/** @deprecated Use TOAD_AGENT_LOOP_COUNT instead. Will be removed in v3.0. */
|
|
24
41
|
readonly AGENT_LOOP_COUNT: "gen_ai.agent.loop_count";
|
|
25
42
|
readonly GUARD_MODE: "gen_ai.toad_eye.guard.mode";
|
|
26
43
|
readonly GUARD_PASSED: "gen_ai.toad_eye.guard.passed";
|
package/dist/types/attributes.js
CHANGED
|
@@ -16,13 +16,32 @@ export const GEN_AI_ATTRS = {
|
|
|
16
16
|
OPERATION: "gen_ai.operation.name",
|
|
17
17
|
FINISH_REASONS: "gen_ai.response.finish_reasons",
|
|
18
18
|
ERROR: "error.type",
|
|
19
|
-
//
|
|
19
|
+
// OTel GenAI agent attributes
|
|
20
|
+
AGENT_NAME: "gen_ai.agent.name",
|
|
21
|
+
AGENT_ID: "gen_ai.agent.id",
|
|
22
|
+
TOOL_NAME: "gen_ai.tool.name",
|
|
23
|
+
TOOL_TYPE: "gen_ai.tool.type",
|
|
24
|
+
// toad-eye ReAct extensions (gen_ai.toad_eye.agent.* — correct namespace)
|
|
25
|
+
TOAD_AGENT_STEP_TYPE: "gen_ai.toad_eye.agent.step.type",
|
|
26
|
+
TOAD_AGENT_STEP_NUMBER: "gen_ai.toad_eye.agent.step.number",
|
|
27
|
+
TOAD_AGENT_STEP_CONTENT: "gen_ai.toad_eye.agent.step.content",
|
|
28
|
+
TOAD_AGENT_HANDOFF_TO: "gen_ai.toad_eye.agent.handoff.to",
|
|
29
|
+
TOAD_AGENT_HANDOFF_REASON: "gen_ai.toad_eye.agent.handoff.reason",
|
|
30
|
+
TOAD_AGENT_LOOP_COUNT: "gen_ai.toad_eye.agent.loop_count",
|
|
31
|
+
// Agent step attributes — @deprecated aliases (will be removed in v3.0)
|
|
32
|
+
/** @deprecated Use TOAD_AGENT_STEP_TYPE instead. Will be removed in v3.0. */
|
|
20
33
|
AGENT_STEP_TYPE: "gen_ai.agent.step.type",
|
|
34
|
+
/** @deprecated Use TOAD_AGENT_STEP_NUMBER instead. Will be removed in v3.0. */
|
|
21
35
|
AGENT_STEP_NUMBER: "gen_ai.agent.step.number",
|
|
36
|
+
/** @deprecated Use TOOL_NAME instead. Will be removed in v3.0. */
|
|
22
37
|
AGENT_TOOL_NAME: "gen_ai.agent.tool.name",
|
|
38
|
+
/** @deprecated Use TOAD_AGENT_STEP_CONTENT instead. Will be removed in v3.0. */
|
|
23
39
|
AGENT_STEP_CONTENT: "gen_ai.agent.step.content",
|
|
40
|
+
/** @deprecated Use TOAD_AGENT_HANDOFF_TO instead. Will be removed in v3.0. */
|
|
24
41
|
AGENT_HANDOFF_TO: "gen_ai.agent.handoff.to",
|
|
42
|
+
/** @deprecated Use TOAD_AGENT_HANDOFF_REASON instead. Will be removed in v3.0. */
|
|
25
43
|
AGENT_HANDOFF_REASON: "gen_ai.agent.handoff.reason",
|
|
44
|
+
/** @deprecated Use TOAD_AGENT_LOOP_COUNT instead. Will be removed in v3.0. */
|
|
26
45
|
AGENT_LOOP_COUNT: "gen_ai.agent.loop_count",
|
|
27
46
|
// Guard (shadow mode) attributes
|
|
28
47
|
GUARD_MODE: "gen_ai.toad_eye.guard.mode",
|
package/dist/types/index.d.ts
CHANGED
|
@@ -14,4 +14,4 @@ export type { ToadEyeConfig, SamplingConfig } from "./config.js";
|
|
|
14
14
|
export { GEN_AI_ATTRS, LLM_ATTRS } from "./attributes.js";
|
|
15
15
|
export { GEN_AI_METRICS, LLM_METRICS } from "./metrics.js";
|
|
16
16
|
export type { MetricName } from "./metrics.js";
|
|
17
|
-
export type { SpanStatus, LLMSpanAttributes, AgentStepType, AgentStepInput, AgentQueryOptions, GuardMode, GuardResult, } from "./spans.js";
|
|
17
|
+
export type { SpanStatus, LLMSpanAttributes, AgentStepType, AgentStepInput, AgentQueryOptions, AgentQueryInput, GuardMode, GuardResult, } from "./spans.js";
|
package/dist/types/spans.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface AgentStepInput {
|
|
|
9
9
|
readonly stepNumber: number;
|
|
10
10
|
readonly content?: string | undefined;
|
|
11
11
|
readonly toolName?: string | undefined;
|
|
12
|
+
/** Tool type per OTel GenAI spec: function, extension, retrieval, builtin */
|
|
13
|
+
readonly toolType?: "function" | "extension" | "retrieval" | "builtin" | undefined;
|
|
12
14
|
/** Target agent name for handoff steps */
|
|
13
15
|
readonly toAgent?: string | undefined;
|
|
14
16
|
/** Reason for handoff */
|
|
@@ -19,6 +21,14 @@ export interface AgentQueryOptions {
|
|
|
19
21
|
/** Max steps before recording a warning. Default: 25 */
|
|
20
22
|
readonly maxSteps?: number | undefined;
|
|
21
23
|
}
|
|
24
|
+
/** Input for traceAgentQuery — object form with agent metadata */
|
|
25
|
+
export interface AgentQueryInput {
|
|
26
|
+
readonly query: string;
|
|
27
|
+
/** Agent name — maps to gen_ai.agent.name and span name invoke_agent {agentName} */
|
|
28
|
+
readonly agentName?: string | undefined;
|
|
29
|
+
/** Agent identifier — maps to gen_ai.agent.id */
|
|
30
|
+
readonly agentId?: string | undefined;
|
|
31
|
+
}
|
|
22
32
|
/** Guard execution mode */
|
|
23
33
|
export type GuardMode = "shadow" | "enforce";
|
|
24
34
|
/**
|