@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.
Files changed (43) hide show
  1. package/dist/acp-server/adapter.d.ts +15 -0
  2. package/dist/acp-server/adapter.js +445 -67
  3. package/dist/acp-server/http.js +8 -1
  4. package/dist/acp-server/session-storage.d.ts +19 -0
  5. package/dist/acp-server/session-storage.js +9 -0
  6. package/dist/definition/index.d.ts +16 -4
  7. package/dist/definition/index.js +17 -4
  8. package/dist/index.d.ts +2 -1
  9. package/dist/index.js +10 -1
  10. package/dist/runner/agent-runner.d.ts +13 -2
  11. package/dist/runner/agent-runner.js +4 -0
  12. package/dist/runner/hooks/executor.d.ts +18 -1
  13. package/dist/runner/hooks/executor.js +74 -62
  14. package/dist/runner/hooks/predefined/compaction-tool.js +19 -3
  15. package/dist/runner/hooks/predefined/tool-response-compactor.d.ts +6 -0
  16. package/dist/runner/hooks/predefined/tool-response-compactor.js +461 -0
  17. package/dist/runner/hooks/registry.js +2 -0
  18. package/dist/runner/hooks/types.d.ts +39 -3
  19. package/dist/runner/hooks/types.js +9 -1
  20. package/dist/runner/langchain/index.d.ts +1 -0
  21. package/dist/runner/langchain/index.js +523 -321
  22. package/dist/runner/langchain/model-factory.js +1 -1
  23. package/dist/runner/langchain/otel-callbacks.d.ts +18 -0
  24. package/dist/runner/langchain/otel-callbacks.js +123 -0
  25. package/dist/runner/langchain/tools/subagent.js +21 -1
  26. package/dist/scaffold/link-local.d.ts +1 -0
  27. package/dist/scaffold/link-local.js +54 -0
  28. package/dist/scaffold/project-scaffold.js +1 -0
  29. package/dist/telemetry/index.d.ts +83 -0
  30. package/dist/telemetry/index.js +172 -0
  31. package/dist/telemetry/setup.d.ts +22 -0
  32. package/dist/telemetry/setup.js +141 -0
  33. package/dist/templates/index.d.ts +7 -0
  34. package/dist/tsconfig.tsbuildinfo +1 -1
  35. package/dist/utils/context-size-calculator.d.ts +29 -0
  36. package/dist/utils/context-size-calculator.js +78 -0
  37. package/dist/utils/index.d.ts +2 -0
  38. package/dist/utils/index.js +2 -0
  39. package/dist/utils/token-counter.d.ts +19 -0
  40. package/dist/utils/token-counter.js +44 -0
  41. package/index.ts +16 -1
  42. package/package.json +24 -7
  43. package/templates/index.ts +18 -6
@@ -74,7 +74,7 @@ export function createModelFromString(modelString) {
74
74
  location: "global",
75
75
  authOptions: {
76
76
  credentials: parsedEnv,
77
- projectId: parsedEnv["project_id"],
77
+ projectId: parsedEnv.project_id,
78
78
  },
79
79
  });
80
80
  }
@@ -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
- // Create a new session with subagent mode flag
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
+ }