@townco/agent 0.1.49 → 0.1.51
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/dist/acp-server/adapter.d.ts +15 -0
- package/dist/acp-server/adapter.js +445 -67
- package/dist/acp-server/http.js +8 -1
- package/dist/acp-server/session-storage.d.ts +19 -0
- package/dist/acp-server/session-storage.js +9 -0
- package/dist/definition/index.d.ts +16 -4
- package/dist/definition/index.js +17 -4
- package/dist/index.d.ts +2 -1
- package/dist/index.js +10 -1
- package/dist/runner/agent-runner.d.ts +13 -2
- package/dist/runner/agent-runner.js +4 -0
- package/dist/runner/hooks/executor.d.ts +18 -1
- package/dist/runner/hooks/executor.js +74 -62
- package/dist/runner/hooks/predefined/compaction-tool.js +19 -3
- package/dist/runner/hooks/predefined/tool-response-compactor.d.ts +6 -0
- package/dist/runner/hooks/predefined/tool-response-compactor.js +461 -0
- package/dist/runner/hooks/registry.js +2 -0
- package/dist/runner/hooks/types.d.ts +39 -3
- package/dist/runner/hooks/types.js +9 -1
- package/dist/runner/langchain/index.d.ts +1 -0
- package/dist/runner/langchain/index.js +523 -321
- package/dist/runner/langchain/model-factory.js +1 -1
- package/dist/runner/langchain/otel-callbacks.d.ts +18 -0
- package/dist/runner/langchain/otel-callbacks.js +123 -0
- package/dist/runner/langchain/tools/subagent.js +21 -1
- package/dist/scaffold/link-local.d.ts +1 -0
- package/dist/scaffold/link-local.js +54 -0
- package/dist/scaffold/project-scaffold.js +1 -0
- package/dist/telemetry/index.d.ts +83 -0
- package/dist/telemetry/index.js +172 -0
- package/dist/telemetry/setup.d.ts +22 -0
- package/dist/telemetry/setup.js +141 -0
- package/dist/templates/index.d.ts +7 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/context-size-calculator.d.ts +29 -0
- package/dist/utils/context-size-calculator.js +78 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/token-counter.d.ts +19 -0
- package/dist/utils/token-counter.js +44 -0
- package/index.ts +16 -1
- package/package.json +24 -7
- package/templates/index.ts +18 -6
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { CallbackHandlerMethods } from "@langchain/core/callbacks/base";
|
|
2
|
+
import { type Context } from "@opentelemetry/api";
|
|
3
|
+
export interface OtelCallbackOptions {
|
|
4
|
+
provider: string;
|
|
5
|
+
model: string;
|
|
6
|
+
parentContext: Context;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Creates OpenTelemetry callback handlers for LangChain LLM calls.
|
|
10
|
+
* These handlers instrument model invocations with OTEL spans and record token usage.
|
|
11
|
+
*
|
|
12
|
+
* @param opts - Configuration for the callbacks
|
|
13
|
+
* @param opts.provider - The LLM provider (e.g., "anthropic", "google_vertexai")
|
|
14
|
+
* @param opts.model - The model identifier
|
|
15
|
+
* @param opts.parentContext - The parent OTEL context to create child spans under
|
|
16
|
+
* @returns CallbackHandlerMethods object that can be passed to LangChain
|
|
17
|
+
*/
|
|
18
|
+
export declare function makeOtelCallbacks(opts: OtelCallbackOptions): CallbackHandlerMethods;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { context } from "@opentelemetry/api";
|
|
2
|
+
import { telemetry } from "../../telemetry/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* OpenTelemetry callback handler for LangChain LLM calls.
|
|
5
|
+
* Creates spans for each LLM request to track model invocations and token usage.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Map to store active spans by their LangChain run ID
|
|
9
|
+
*
|
|
10
|
+
* There's a memory leak opportunity here, but we are OK with that for now.
|
|
11
|
+
*/
|
|
12
|
+
const spansByRunId = new Map();
|
|
13
|
+
/**
|
|
14
|
+
* Serializes LangChain messages to a JSON string for span attributes.
|
|
15
|
+
* Extracts role and content from each message.
|
|
16
|
+
*/
|
|
17
|
+
function serializeMessages(messages) {
|
|
18
|
+
try {
|
|
19
|
+
const flatMessages = messages.flat();
|
|
20
|
+
const serialized = flatMessages.map((msg) => ({
|
|
21
|
+
role: msg._getType(),
|
|
22
|
+
content: typeof msg.content === "string"
|
|
23
|
+
? msg.content
|
|
24
|
+
: JSON.stringify(msg.content),
|
|
25
|
+
}));
|
|
26
|
+
return JSON.stringify(serialized);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
return `[Error serializing messages: ${error}]`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Extracts the system prompt from LangChain messages if present.
|
|
34
|
+
*/
|
|
35
|
+
function extractSystemPrompt(messages) {
|
|
36
|
+
try {
|
|
37
|
+
const flatMessages = messages.flat();
|
|
38
|
+
const systemMessage = flatMessages.find((msg) => msg._getType() === "system");
|
|
39
|
+
if (systemMessage && typeof systemMessage.content === "string") {
|
|
40
|
+
return systemMessage.content;
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
catch (_error) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Creates OpenTelemetry callback handlers for LangChain LLM calls.
|
|
50
|
+
* These handlers instrument model invocations with OTEL spans and record token usage.
|
|
51
|
+
*
|
|
52
|
+
* @param opts - Configuration for the callbacks
|
|
53
|
+
* @param opts.provider - The LLM provider (e.g., "anthropic", "google_vertexai")
|
|
54
|
+
* @param opts.model - The model identifier
|
|
55
|
+
* @param opts.parentContext - The parent OTEL context to create child spans under
|
|
56
|
+
* @returns CallbackHandlerMethods object that can be passed to LangChain
|
|
57
|
+
*/
|
|
58
|
+
export function makeOtelCallbacks(opts) {
|
|
59
|
+
return {
|
|
60
|
+
/**
|
|
61
|
+
* Called when a chat model/LLM request starts.
|
|
62
|
+
* Creates a new OTEL span for this LLM request.
|
|
63
|
+
*/
|
|
64
|
+
async handleChatModelStart(_llm, messages, runId, _parentRunId, _extraParams, tags, _metadata) {
|
|
65
|
+
// Extract system prompt and serialize messages
|
|
66
|
+
const systemPrompt = extractSystemPrompt(messages);
|
|
67
|
+
const serializedMessages = serializeMessages(messages);
|
|
68
|
+
// Create span within the parent context (invocation span)
|
|
69
|
+
// Following OpenTelemetry GenAI semantic conventions:
|
|
70
|
+
// https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/
|
|
71
|
+
const span = context.with(opts.parentContext, () => telemetry.startSpan(`chat ${opts.model}`, {
|
|
72
|
+
"gen_ai.operation.name": "chat",
|
|
73
|
+
"gen_ai.provider.name": opts.provider,
|
|
74
|
+
"gen_ai.request.model": opts.model,
|
|
75
|
+
"gen_ai.input.messages": serializedMessages,
|
|
76
|
+
...(systemPrompt
|
|
77
|
+
? { "gen_ai.system_instructions": systemPrompt }
|
|
78
|
+
: {}),
|
|
79
|
+
// Custom attributes for additional context
|
|
80
|
+
"langchain.run_id": runId,
|
|
81
|
+
...(tags && tags.length > 0
|
|
82
|
+
? { "langchain.tags": tags.join(",") }
|
|
83
|
+
: {}),
|
|
84
|
+
}));
|
|
85
|
+
if (span) {
|
|
86
|
+
spansByRunId.set(runId, span);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
/**
|
|
90
|
+
* Called when an LLM request completes successfully.
|
|
91
|
+
* Records token usage and ends the span.
|
|
92
|
+
*/
|
|
93
|
+
async handleLLMEnd(output, runId) {
|
|
94
|
+
const span = spansByRunId.get(runId);
|
|
95
|
+
if (!span)
|
|
96
|
+
return;
|
|
97
|
+
// Extract token usage from LLM output
|
|
98
|
+
// The structure varies by provider but LangChain normalizes it
|
|
99
|
+
const tokenUsage = output.llmOutput?.tokenUsage;
|
|
100
|
+
if (tokenUsage) {
|
|
101
|
+
const inputTokens = tokenUsage.inputTokens ?? 0;
|
|
102
|
+
const outputTokens = tokenUsage.outputTokens ??
|
|
103
|
+
(tokenUsage.totalTokens != null
|
|
104
|
+
? tokenUsage.totalTokens - inputTokens
|
|
105
|
+
: 0);
|
|
106
|
+
telemetry.recordTokenUsage(inputTokens, outputTokens, span);
|
|
107
|
+
}
|
|
108
|
+
telemetry.endSpan(span);
|
|
109
|
+
spansByRunId.delete(runId);
|
|
110
|
+
},
|
|
111
|
+
/**
|
|
112
|
+
* Called when an LLM request fails with an error.
|
|
113
|
+
* Records the error and ends the span with error status.
|
|
114
|
+
*/
|
|
115
|
+
async handleLLMError(error, runId) {
|
|
116
|
+
const span = spansByRunId.get(runId);
|
|
117
|
+
if (!span)
|
|
118
|
+
return;
|
|
119
|
+
telemetry.endSpan(span, error instanceof Error ? error : new Error(String(error)));
|
|
120
|
+
spansByRunId.delete(runId);
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -3,6 +3,7 @@ import * as fs from "node:fs/promises";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { Readable, Writable } from "node:stream";
|
|
5
5
|
import { ClientSideConnection, ndJsonStream, PROTOCOL_VERSION, } from "@agentclientprotocol/sdk";
|
|
6
|
+
import { context, propagation, trace } from "@opentelemetry/api";
|
|
6
7
|
import { z } from "zod";
|
|
7
8
|
import { SUBAGENT_MODE_KEY } from "../../../acp-server/adapter.js";
|
|
8
9
|
/**
|
|
@@ -235,12 +236,31 @@ async function querySubagent(agentPath, agentWorkingDirectory, query) {
|
|
|
235
236
|
},
|
|
236
237
|
},
|
|
237
238
|
});
|
|
238
|
-
//
|
|
239
|
+
// Prepare OpenTelemetry trace context to propagate to the subagent.
|
|
240
|
+
// We inject from the current active context so the subagent's root
|
|
241
|
+
// invocation span can be a child of whatever span is active when
|
|
242
|
+
// this Task tool runs (ideally the agent.tool_call span).
|
|
243
|
+
if (process.env.DEBUG_TELEMETRY === "true") {
|
|
244
|
+
console.log(`[querySubagent] Tool function executing for agent: ${agentPath}`);
|
|
245
|
+
}
|
|
246
|
+
const otelCarrier = {};
|
|
247
|
+
const activeCtx = context.active();
|
|
248
|
+
const activeSpan = trace.getSpan(activeCtx);
|
|
249
|
+
if (process.env.DEBUG_TELEMETRY === "true") {
|
|
250
|
+
console.log(`[querySubagent] Active span when tool executes:`, activeSpan?.spanContext());
|
|
251
|
+
}
|
|
252
|
+
const ctxForInjection = activeSpan
|
|
253
|
+
? trace.setSpan(activeCtx, activeSpan)
|
|
254
|
+
: activeCtx;
|
|
255
|
+
propagation.inject(ctxForInjection, otelCarrier);
|
|
256
|
+
const hasOtelContext = Object.keys(otelCarrier).length > 0;
|
|
257
|
+
// Create a new session with subagent mode flag and OTEL trace context
|
|
239
258
|
const sessionResponse = await connection?.newSession({
|
|
240
259
|
cwd: agentWorkingDirectory,
|
|
241
260
|
mcpServers: [],
|
|
242
261
|
_meta: {
|
|
243
262
|
[SUBAGENT_MODE_KEY]: true,
|
|
263
|
+
...(hasOtelContext ? { otelTraceContext: otelCarrier } : {}),
|
|
244
264
|
},
|
|
245
265
|
});
|
|
246
266
|
// Send the prompt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function linkLocalPackages(projectPath: string): Promise<void>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { exists } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { $ } from "bun";
|
|
4
|
+
const PACKAGE_PATHS = {
|
|
5
|
+
"@townco/ui": "packages/ui",
|
|
6
|
+
"@townco/core": "packages/core",
|
|
7
|
+
"@townco/tsconfig": "packages/tsconfig",
|
|
8
|
+
"@townco/tui-template": "apps/tui",
|
|
9
|
+
"@townco/gui-template": "apps/gui",
|
|
10
|
+
"@townco/secret": "packages/secret",
|
|
11
|
+
"@townco/agent": "packages/agent",
|
|
12
|
+
"@townco/cli": "apps/cli",
|
|
13
|
+
};
|
|
14
|
+
async function getMonorepoRoot() {
|
|
15
|
+
try {
|
|
16
|
+
// 1. Get git repo root
|
|
17
|
+
const result = await $ `git rev-parse --show-toplevel`.quiet();
|
|
18
|
+
const repoRoot = result.text().trim();
|
|
19
|
+
// 2. Check package.json name === "town"
|
|
20
|
+
const pkgJsonPath = join(repoRoot, "package.json");
|
|
21
|
+
const pkgJson = await Bun.file(pkgJsonPath).json();
|
|
22
|
+
if (pkgJson.name !== "town")
|
|
23
|
+
return null;
|
|
24
|
+
// 3. Check packages/agent and packages/ui exist
|
|
25
|
+
const agentExists = await exists(join(repoRoot, "packages/agent"));
|
|
26
|
+
const uiExists = await exists(join(repoRoot, "packages/ui"));
|
|
27
|
+
if (!agentExists || !uiExists)
|
|
28
|
+
return null;
|
|
29
|
+
return repoRoot;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export async function linkLocalPackages(projectPath) {
|
|
36
|
+
const repoRoot = await getMonorepoRoot();
|
|
37
|
+
if (!repoRoot)
|
|
38
|
+
return; // Not in monorepo, no-op
|
|
39
|
+
console.log("Detected town monorepo, linking local packages...");
|
|
40
|
+
// 1. Register each local package globally
|
|
41
|
+
for (const [, localPath] of Object.entries(PACKAGE_PATHS)) {
|
|
42
|
+
const pkgPath = join(repoRoot, localPath);
|
|
43
|
+
await $ `bun link`.cwd(pkgPath).quiet();
|
|
44
|
+
}
|
|
45
|
+
// 2. Parse project's package.json for @townco/* deps
|
|
46
|
+
const pkgJson = await Bun.file(join(projectPath, "package.json")).json();
|
|
47
|
+
const deps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
|
|
48
|
+
const towncoPackages = Object.keys(deps).filter((name) => name.startsWith("@townco/"));
|
|
49
|
+
// 3. Link each package in the project
|
|
50
|
+
for (const pkgName of towncoPackages) {
|
|
51
|
+
await $ `bun link ${pkgName}`.cwd(projectPath);
|
|
52
|
+
console.log(`Linked ${pkgName}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -21,6 +21,7 @@ function generateProjectPackageJson() {
|
|
|
21
21
|
"@radix-ui/react-select": "^2.2.6",
|
|
22
22
|
"@radix-ui/react-slot": "^1.2.4",
|
|
23
23
|
"@radix-ui/react-tabs": "^1.1.13",
|
|
24
|
+
"@townco/core": "~0.0.23",
|
|
24
25
|
"@townco/ui": "^0.1.0",
|
|
25
26
|
"@townco/agent": "^0.1.20",
|
|
26
27
|
"class-variance-authority": "^0.7.1",
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTelemetry integration for @townco/agent
|
|
3
|
+
* Provides tracing and logging capabilities for agent operations
|
|
4
|
+
*/
|
|
5
|
+
import { type Attributes, type Context, type Span, type Tracer } from "@opentelemetry/api";
|
|
6
|
+
import { type Logger as OTelLogger } from "@opentelemetry/api-logs";
|
|
7
|
+
export interface TelemetryConfig {
|
|
8
|
+
/** Enable telemetry (default: false) */
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
/** Service name for traces and logs (default: '@townco/agent') */
|
|
11
|
+
serviceName?: string;
|
|
12
|
+
/** Base attributes to attach to all spans and logs */
|
|
13
|
+
attributes?: Record<string, string | number>;
|
|
14
|
+
/** Custom tracer instance (optional, uses global registry if not provided) */
|
|
15
|
+
tracer?: Tracer;
|
|
16
|
+
/** Custom logger instance (optional, uses global registry if not provided) */
|
|
17
|
+
logger?: OTelLogger;
|
|
18
|
+
}
|
|
19
|
+
declare class AgentTelemetry {
|
|
20
|
+
private tracer;
|
|
21
|
+
private logger;
|
|
22
|
+
private enabled;
|
|
23
|
+
private serviceName;
|
|
24
|
+
private baseAttributes;
|
|
25
|
+
configure(config: TelemetryConfig): void;
|
|
26
|
+
/**
|
|
27
|
+
* Start a new span
|
|
28
|
+
* @param name - Span name
|
|
29
|
+
* @param attributes - Span attributes
|
|
30
|
+
* @param parentContext - Optional parent context (defaults to active context)
|
|
31
|
+
* @returns Span instance or null if telemetry is disabled
|
|
32
|
+
*/
|
|
33
|
+
startSpan(name: string, attributes?: Attributes, parentContext?: Context): Span | null;
|
|
34
|
+
/**
|
|
35
|
+
* Make a span the active span and run a function within its context
|
|
36
|
+
*/
|
|
37
|
+
withActiveSpan<T>(span: Span | null, fn: () => T): T;
|
|
38
|
+
/**
|
|
39
|
+
* Make a span the active span and run an async function within its context
|
|
40
|
+
*/
|
|
41
|
+
withActiveSpanAsync<T>(span: Span | null, fn: () => Promise<T>): Promise<T>;
|
|
42
|
+
/**
|
|
43
|
+
* End a span with optional error
|
|
44
|
+
*/
|
|
45
|
+
endSpan(span: Span | null, error?: Error): void;
|
|
46
|
+
/**
|
|
47
|
+
* Set attributes on a span
|
|
48
|
+
*/
|
|
49
|
+
setSpanAttributes(span: Span | null, attributes: Attributes): void;
|
|
50
|
+
/**
|
|
51
|
+
* Log a message with optional attributes
|
|
52
|
+
*/
|
|
53
|
+
log(level: "info" | "warn" | "error", message: string, attributes?: Attributes): void;
|
|
54
|
+
/**
|
|
55
|
+
* Record token usage metrics on a span
|
|
56
|
+
* Following OpenTelemetry GenAI semantic conventions:
|
|
57
|
+
* https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/
|
|
58
|
+
*/
|
|
59
|
+
recordTokenUsage(inputTokens: number, outputTokens: number, span?: Span | null): void;
|
|
60
|
+
}
|
|
61
|
+
declare const telemetry: AgentTelemetry;
|
|
62
|
+
/**
|
|
63
|
+
* Configure telemetry for all agents in this process
|
|
64
|
+
* Must be called before creating any agent runners
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```typescript
|
|
68
|
+
* import { configureTelemetry } from '@townco/agent';
|
|
69
|
+
* import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
|
|
70
|
+
*
|
|
71
|
+
* // Set up OTel provider first
|
|
72
|
+
* const provider = new NodeTracerProvider();
|
|
73
|
+
* provider.register();
|
|
74
|
+
*
|
|
75
|
+
* // Enable telemetry
|
|
76
|
+
* configureTelemetry({
|
|
77
|
+
* enabled: true,
|
|
78
|
+
* serviceName: 'my-agent-app',
|
|
79
|
+
* });
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export declare function configureTelemetry(config: TelemetryConfig): void;
|
|
83
|
+
export { telemetry };
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTelemetry integration for @townco/agent
|
|
3
|
+
* Provides tracing and logging capabilities for agent operations
|
|
4
|
+
*/
|
|
5
|
+
import { context, SpanStatusCode, trace, } from "@opentelemetry/api";
|
|
6
|
+
import { logs } from "@opentelemetry/api-logs";
|
|
7
|
+
class AgentTelemetry {
|
|
8
|
+
tracer = null;
|
|
9
|
+
logger = null;
|
|
10
|
+
enabled = false;
|
|
11
|
+
serviceName = "@townco/agent";
|
|
12
|
+
baseAttributes = {};
|
|
13
|
+
configure(config) {
|
|
14
|
+
this.enabled = config.enabled ?? false;
|
|
15
|
+
this.serviceName = config.serviceName ?? this.serviceName;
|
|
16
|
+
this.baseAttributes = config.attributes ?? {};
|
|
17
|
+
if (this.enabled) {
|
|
18
|
+
// Use provided tracer/logger or get from global registry
|
|
19
|
+
this.tracer = config.tracer ?? trace.getTracer(this.serviceName);
|
|
20
|
+
this.logger = config.logger ?? logs.getLogger(this.serviceName);
|
|
21
|
+
// Debug logging
|
|
22
|
+
if (process.env.DEBUG_TELEMETRY === "true") {
|
|
23
|
+
console.log("[Telemetry] Configured:", {
|
|
24
|
+
enabled: this.enabled,
|
|
25
|
+
serviceName: this.serviceName,
|
|
26
|
+
hasTracer: !!this.tracer,
|
|
27
|
+
hasLogger: !!this.logger,
|
|
28
|
+
baseAttributes: this.baseAttributes,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Start a new span
|
|
35
|
+
* @param name - Span name
|
|
36
|
+
* @param attributes - Span attributes
|
|
37
|
+
* @param parentContext - Optional parent context (defaults to active context)
|
|
38
|
+
* @returns Span instance or null if telemetry is disabled
|
|
39
|
+
*/
|
|
40
|
+
startSpan(name, attributes, parentContext) {
|
|
41
|
+
if (!this.enabled || !this.tracer) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
// Use provided context or get the active one
|
|
45
|
+
const ctx = parentContext ?? context.active();
|
|
46
|
+
const span = this.tracer.startSpan(name, {
|
|
47
|
+
attributes: {
|
|
48
|
+
...this.baseAttributes,
|
|
49
|
+
"service.name": this.serviceName,
|
|
50
|
+
...attributes,
|
|
51
|
+
},
|
|
52
|
+
}, ctx);
|
|
53
|
+
// Debug logging
|
|
54
|
+
if (process.env.DEBUG_TELEMETRY === "true") {
|
|
55
|
+
const parentSpan = trace.getSpan(ctx);
|
|
56
|
+
const parentInfo = parentSpan
|
|
57
|
+
? ` (parent: ${parentSpan.spanContext().spanId.slice(0, 8)})`
|
|
58
|
+
: " (root)";
|
|
59
|
+
console.log(`[Telemetry] Started span: ${name}${parentInfo}`, attributes);
|
|
60
|
+
}
|
|
61
|
+
return span;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Make a span the active span and run a function within its context
|
|
65
|
+
*/
|
|
66
|
+
withActiveSpan(span, fn) {
|
|
67
|
+
if (!span) {
|
|
68
|
+
return fn();
|
|
69
|
+
}
|
|
70
|
+
return context.with(trace.setSpan(context.active(), span), fn);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Make a span the active span and run an async function within its context
|
|
74
|
+
*/
|
|
75
|
+
async withActiveSpanAsync(span, fn) {
|
|
76
|
+
if (!span) {
|
|
77
|
+
return fn();
|
|
78
|
+
}
|
|
79
|
+
return context.with(trace.setSpan(context.active(), span), fn);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* End a span with optional error
|
|
83
|
+
*/
|
|
84
|
+
endSpan(span, error) {
|
|
85
|
+
if (!span)
|
|
86
|
+
return;
|
|
87
|
+
if (error) {
|
|
88
|
+
span.recordException(error);
|
|
89
|
+
span.setStatus({
|
|
90
|
+
code: SpanStatusCode.ERROR,
|
|
91
|
+
message: error.message,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
96
|
+
}
|
|
97
|
+
span.end();
|
|
98
|
+
// Debug logging
|
|
99
|
+
if (process.env.DEBUG_TELEMETRY === "true") {
|
|
100
|
+
console.log(`[Telemetry] Ended span${error ? ` with error: ${error.message}` : ""}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Set attributes on a span
|
|
105
|
+
*/
|
|
106
|
+
setSpanAttributes(span, attributes) {
|
|
107
|
+
if (!span)
|
|
108
|
+
return;
|
|
109
|
+
span.setAttributes(attributes);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Log a message with optional attributes
|
|
113
|
+
*/
|
|
114
|
+
log(level, message, attributes) {
|
|
115
|
+
if (!this.enabled || !this.logger) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
this.logger.emit({
|
|
119
|
+
severityText: level.toUpperCase(),
|
|
120
|
+
body: message,
|
|
121
|
+
attributes: {
|
|
122
|
+
...this.baseAttributes,
|
|
123
|
+
...attributes,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Record token usage metrics on a span
|
|
129
|
+
* Following OpenTelemetry GenAI semantic conventions:
|
|
130
|
+
* https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/
|
|
131
|
+
*/
|
|
132
|
+
recordTokenUsage(inputTokens, outputTokens, span) {
|
|
133
|
+
if (!this.enabled)
|
|
134
|
+
return;
|
|
135
|
+
const attrs = {
|
|
136
|
+
"gen_ai.usage.input_tokens": inputTokens,
|
|
137
|
+
"gen_ai.usage.output_tokens": outputTokens,
|
|
138
|
+
"gen_ai.usage.total_tokens": inputTokens + outputTokens,
|
|
139
|
+
};
|
|
140
|
+
if (span) {
|
|
141
|
+
span.setAttributes(attrs);
|
|
142
|
+
}
|
|
143
|
+
this.log("info", "Token usage recorded", attrs);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Singleton instance
|
|
147
|
+
const telemetry = new AgentTelemetry();
|
|
148
|
+
/**
|
|
149
|
+
* Configure telemetry for all agents in this process
|
|
150
|
+
* Must be called before creating any agent runners
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```typescript
|
|
154
|
+
* import { configureTelemetry } from '@townco/agent';
|
|
155
|
+
* import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
|
|
156
|
+
*
|
|
157
|
+
* // Set up OTel provider first
|
|
158
|
+
* const provider = new NodeTracerProvider();
|
|
159
|
+
* provider.register();
|
|
160
|
+
*
|
|
161
|
+
* // Enable telemetry
|
|
162
|
+
* configureTelemetry({
|
|
163
|
+
* enabled: true,
|
|
164
|
+
* serviceName: 'my-agent-app',
|
|
165
|
+
* });
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
export function configureTelemetry(config) {
|
|
169
|
+
telemetry.configure(config);
|
|
170
|
+
}
|
|
171
|
+
// Export singleton for internal use
|
|
172
|
+
export { telemetry };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTelemetry provider setup for @townco/agent
|
|
3
|
+
* Initializes the trace provider, exporter, and propagator
|
|
4
|
+
*/
|
|
5
|
+
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
6
|
+
export interface TelemetrySetupOptions {
|
|
7
|
+
serviceName?: string;
|
|
8
|
+
otlpEndpoint?: string;
|
|
9
|
+
debug?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Initialize OpenTelemetry with OTLP exporter
|
|
13
|
+
* Call this early in your application startup
|
|
14
|
+
*/
|
|
15
|
+
export declare function initializeOpenTelemetry(options?: TelemetrySetupOptions): {
|
|
16
|
+
provider: NodeTracerProvider;
|
|
17
|
+
shutdown: () => Promise<void>;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Initialize OpenTelemetry from environment variables and register shutdown handlers
|
|
21
|
+
*/
|
|
22
|
+
export declare function initializeOpenTelemetryFromEnv(): void;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTelemetry provider setup for @townco/agent
|
|
3
|
+
* Initializes the trace provider, exporter, and propagator
|
|
4
|
+
*/
|
|
5
|
+
import { propagation } from "@opentelemetry/api";
|
|
6
|
+
import { W3CTraceContextPropagator } from "@opentelemetry/core";
|
|
7
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
8
|
+
import { Resource } from "@opentelemetry/resources";
|
|
9
|
+
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
10
|
+
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
11
|
+
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
|
|
12
|
+
import { configureTelemetry } from "./index.js";
|
|
13
|
+
/**
|
|
14
|
+
* Initialize OpenTelemetry with OTLP exporter
|
|
15
|
+
* Call this early in your application startup
|
|
16
|
+
*/
|
|
17
|
+
export function initializeOpenTelemetry(options = {}) {
|
|
18
|
+
const serviceName = options.serviceName ?? "@townco/agent";
|
|
19
|
+
const otlpEndpoint = options.otlpEndpoint ?? "http://localhost:4318";
|
|
20
|
+
const debug = options.debug ?? false;
|
|
21
|
+
// Note: When passing url in code, we must include the full path.
|
|
22
|
+
// The SDK only auto-appends /v1/traces when using OTEL_EXPORTER_OTLP_ENDPOINT env var.
|
|
23
|
+
const traceUrl = otlpEndpoint.endsWith("/")
|
|
24
|
+
? `${otlpEndpoint}v1/traces`
|
|
25
|
+
: `${otlpEndpoint}/v1/traces`;
|
|
26
|
+
if (debug) {
|
|
27
|
+
console.log("Initializing OpenTelemetry...");
|
|
28
|
+
console.log(`Service name: ${serviceName}`);
|
|
29
|
+
console.log(`OTLP base endpoint: ${otlpEndpoint}`);
|
|
30
|
+
console.log(`OTLP trace URL: ${traceUrl}`);
|
|
31
|
+
// Quick connectivity test
|
|
32
|
+
fetch(otlpEndpoint.replace(/\/$/, "") + "/health")
|
|
33
|
+
.then((r) => r.json())
|
|
34
|
+
.then((d) => console.log(`✓ OTLP server reachable:`, d))
|
|
35
|
+
.catch((e) => console.error(`✗ OTLP server NOT reachable:`, e.message));
|
|
36
|
+
}
|
|
37
|
+
const provider = new NodeTracerProvider({
|
|
38
|
+
resource: new Resource({
|
|
39
|
+
[ATTR_SERVICE_NAME]: serviceName,
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
// Configure exporter with logging wrapper
|
|
43
|
+
// Disable keep-alive to work around Bun's http module 'close' event timing issue
|
|
44
|
+
const baseExporter = new OTLPTraceExporter({
|
|
45
|
+
url: traceUrl,
|
|
46
|
+
httpAgentOptions: { keepAlive: false },
|
|
47
|
+
});
|
|
48
|
+
// Wrap exporter to add logging (matches SpanExporter interface)
|
|
49
|
+
const loggingExporter = {
|
|
50
|
+
export: async (spans, resultCallback) => {
|
|
51
|
+
if (debug) {
|
|
52
|
+
console.log(`📤 Exporting ${spans.length} span(s)...`);
|
|
53
|
+
for (const span of spans) {
|
|
54
|
+
console.log(` - ${span.name} (duration: ${span.duration}ms)`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return baseExporter.export(spans, (result) => {
|
|
58
|
+
if (result.code === 0) {
|
|
59
|
+
if (debug) {
|
|
60
|
+
console.log(`✅ Successfully exported ${spans.length} span(s)`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
// Bun has a bug where the 'close' event fires before 'end', causing
|
|
65
|
+
// false "Request timed out" errors. The export usually succeeds anyway.
|
|
66
|
+
// Only log non-timeout errors as actual errors.
|
|
67
|
+
const errorMsg = result.error?.message ?? "";
|
|
68
|
+
if (errorMsg.includes("timed out")) {
|
|
69
|
+
if (debug) {
|
|
70
|
+
console.log(`⚠️ Export reported timeout (Bun http bug - data likely sent successfully)`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
console.error(`❌ Failed to export spans:`, result.error);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
resultCallback(result);
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
shutdown: () => baseExporter.shutdown(),
|
|
81
|
+
};
|
|
82
|
+
// Add batch span processor
|
|
83
|
+
const batchProcessor = new BatchSpanProcessor(loggingExporter, {
|
|
84
|
+
maxQueueSize: 100,
|
|
85
|
+
maxExportBatchSize: 10,
|
|
86
|
+
scheduledDelayMillis: 5000, // Export every 5 seconds (default)
|
|
87
|
+
});
|
|
88
|
+
provider.addSpanProcessor(batchProcessor);
|
|
89
|
+
// Register the provider globally
|
|
90
|
+
provider.register();
|
|
91
|
+
// Configure W3C Trace Context propagator for cross-process traces
|
|
92
|
+
propagation.setGlobalPropagator(new W3CTraceContextPropagator());
|
|
93
|
+
// Now configure our telemetry wrapper
|
|
94
|
+
configureTelemetry({
|
|
95
|
+
enabled: true,
|
|
96
|
+
serviceName,
|
|
97
|
+
attributes: {
|
|
98
|
+
"agent.environment": process.env.NODE_ENV ?? "development",
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
if (debug) {
|
|
102
|
+
console.log("✓ OpenTelemetry fully initialized");
|
|
103
|
+
}
|
|
104
|
+
// Return shutdown function for graceful cleanup
|
|
105
|
+
const shutdown = async () => {
|
|
106
|
+
try {
|
|
107
|
+
await provider.forceFlush();
|
|
108
|
+
await provider.shutdown();
|
|
109
|
+
if (debug) {
|
|
110
|
+
console.log("✓ Telemetry flushed");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
console.error("Error flushing telemetry:", error);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
return { provider, shutdown };
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Initialize OpenTelemetry from environment variables and register shutdown handlers
|
|
121
|
+
*/
|
|
122
|
+
export function initializeOpenTelemetryFromEnv() {
|
|
123
|
+
const debug = process.env.DEBUG_TELEMETRY === "true";
|
|
124
|
+
const { shutdown } = initializeOpenTelemetry({
|
|
125
|
+
serviceName: process.env.OTEL_SERVICE_NAME ?? "@townco/agent",
|
|
126
|
+
otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "http://localhost:4318",
|
|
127
|
+
debug,
|
|
128
|
+
});
|
|
129
|
+
// Register graceful shutdown handlers
|
|
130
|
+
process.on("SIGINT", async () => {
|
|
131
|
+
await shutdown();
|
|
132
|
+
process.exit(0);
|
|
133
|
+
});
|
|
134
|
+
process.on("SIGTERM", async () => {
|
|
135
|
+
await shutdown();
|
|
136
|
+
process.exit(0);
|
|
137
|
+
});
|
|
138
|
+
process.on("beforeExit", async () => {
|
|
139
|
+
await shutdown();
|
|
140
|
+
});
|
|
141
|
+
}
|