@ai-agent-forge/plugin-observability 0.88.11 → 0.88.12

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.
@@ -1 +1 @@
1
- {"version":3,"file":"otlp-exporter.d.ts","sourceRoot":"","sources":["../src/otlp-exporter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,KAAK,EAAE,yBAAyB,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAExF,eAAO,MAAM,uBAAuB,OAAQ,CAAC;AAC7C,eAAO,MAAM,4BAA4B,KAAK,CAAC;AAC/C,eAAO,MAAM,uBAAuB,OAAQ,CAAC;AAG7C,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED,2FAA2F;AAC3F,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAErF;AAED,6FAA6F;AAC7F,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEpD;AAgGD,kFAAkF;AAClF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAG7D;AAED,MAAM,WAAW,qBAAqB;IACrC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,SAAS,CAAC,EAAE,CACpB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE;YAAE,cAAc,EAAE,MAAM,CAAA;SAAE,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,WAAW,CAAA;KAAE,KAC5F,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9C,2EAA2E;IAC3E,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAC5C;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,qBAAqB,GAAG,yBAAyB,CA+EnG;AASD,uFAAuF;AACvF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAE/D","sourcesContent":["/**\n * OTLP/HTTP+JSON trace export sink for the first-party observability plugin\n * (design §8 telemetry export seam). Implements `ObservabilityExportSinkV1`\n * over plain `fetch` — no OpenTelemetry SDK dependency; the wire payload is\n * the spec's JSON encoding of `ExportTraceServiceRequest`.\n *\n * Mapping (registered, design §8): one OTLP trace per turn correlation id.\n * Span-bearing records export as spans (`turn.span` root, `llm.response` and\n * `tool.span` parented under it); record kinds without a closed duration\n * (`session.start`, `llm.request`, `lifecycle`, `error`, `decision`,\n * `plugin.log`) and records without a trace id are not exported — the local\n * `agent-forge/trace.v1` file keeps full fidelity. OTel ids are derived\n * deterministically (SHA-256) from the correlation id so parent/child linkage\n * is reproducible across runs and processes.\n *\n * Failure domain: export failures never throw — the first failure of a streak\n * reports through `onFailure`, the batch is retained for the next flush and\n * oldest spans drop (counted) once the cap is hit.\n */\n\nimport { createHash } from \"node:crypto\";\nimport type { ObservabilityExportSinkV1, TraceRecordV1 } from \"@agent-forge/plugin-sdk\";\n\nexport const OTLP_DEFAULT_TIMEOUT_MS = 5_000;\nexport const OTLP_DEFAULT_FLUSH_THRESHOLD = 64;\nexport const OTLP_MAX_BUFFERED_SPANS = 1_024;\nconst OTLP_SIGNAL_PATH = \"/v1/traces\";\n\n/** Deterministic 32-hex OTLP trace id derived from a turn correlation id. */\nexport function deriveOtlpTraceId(correlationId: string): string {\n\treturn createHash(\"sha256\").update(correlationId, \"utf8\").digest(\"hex\").slice(0, 32);\n}\n\n/** Deterministic 16-hex OTLP span id derived from the trace id plus span discriminator. */\nexport function deriveOtlpSpanId(correlationId: string, discriminator: string): string {\n\treturn createHash(\"sha256\").update(`${correlationId}|${discriminator}`, \"utf8\").digest(\"hex\").slice(0, 16);\n}\n\n/** Epoch-ms to OTLP unix-nano string via BigInt (number math loses precision above 2^53). */\nexport function epochMsToUnixNano(ms: number): string {\n\treturn (BigInt(Math.max(0, Math.trunc(ms))) * 1_000_000n).toString();\n}\n\ninterface OtlpAttribute {\n\treadonly key: string;\n\treadonly value: { readonly stringValue: string } | { readonly intValue: string } | { readonly boolValue: boolean };\n}\n\ninterface OtlpSpan {\n\treadonly traceId: string;\n\treadonly spanId: string;\n\treadonly parentSpanId?: string;\n\treadonly name: string;\n\treadonly kind: string;\n\treadonly startTimeUnixNano: string;\n\treadonly endTimeUnixNano: string;\n\treadonly attributes?: readonly OtlpAttribute[];\n\treadonly status: { readonly code: string };\n}\n\nfunction attr(key: string, value: string | number | boolean): OtlpAttribute {\n\tif (typeof value === \"number\") {\n\t\treturn Number.isSafeInteger(value)\n\t\t\t? { key, value: { intValue: String(value) } }\n\t\t\t: { key, value: { stringValue: String(value) } };\n\t}\n\tif (typeof value === \"boolean\") return { key, value: { boolValue: value } };\n\treturn { key, value: { stringValue: value } };\n}\n\n/** Shallow primitive-valued fields of the record's bounded data become span attributes. */\nfunction attributesFromData(data: unknown, limit = 32): OtlpAttribute[] {\n\tif (typeof data !== \"object\" || data === null) return [];\n\tconst out: OtlpAttribute[] = [];\n\tfor (const [key, value] of Object.entries(data as Record<string, unknown>)) {\n\t\tif (out.length >= limit) break;\n\t\tif (typeof value === \"string\" && value.length > 0) out.push(attr(key, value.slice(0, 512)));\n\t\telse if (typeof value === \"number\" && Number.isFinite(value)) out.push(attr(key, value));\n\t\telse if (typeof value === \"boolean\") out.push(attr(key, value));\n\t}\n\treturn out;\n}\n\nfunction spanFromRecord(record: TraceRecordV1): OtlpSpan | null {\n\tconst dur = record.dur;\n\tconst traceId = record.traceId;\n\tif (dur === undefined || traceId === undefined) return null;\n\n\tconst traceHex = deriveOtlpTraceId(traceId);\n\tconst endTime = epochMsToUnixNano(record.t);\n\tconst startTime = epochMsToUnixNano(record.t - dur);\n\tconst status = { code: record.level === \"error\" ? \"STATUS_CODE_ERROR\" : \"STATUS_CODE_OK\" };\n\tconst data = record.data;\n\tconst base = { traceId: traceHex, startTimeUnixNano: startTime, endTimeUnixNano: endTime, status };\n\n\tif (record.kind === \"turn.span\") {\n\t\treturn {\n\t\t\t...base,\n\t\t\tspanId: deriveOtlpSpanId(traceId, \"turn.span\"),\n\t\t\tname: \"agent.turn\",\n\t\t\tkind: \"SPAN_KIND_INTERNAL\",\n\t\t\tattributes: attributesFromData(data),\n\t\t};\n\t}\n\tif (record.kind === \"llm.response\") {\n\t\treturn {\n\t\t\t...base,\n\t\t\tspanId: deriveOtlpSpanId(traceId, \"llm.response\"),\n\t\t\tparentSpanId: deriveOtlpSpanId(traceId, \"turn.span\"),\n\t\t\tname: \"agent.llm\",\n\t\t\tkind: \"SPAN_KIND_CLIENT\",\n\t\t\tattributes: attributesFromData(data),\n\t\t};\n\t}\n\tif (record.kind === \"tool.span\") {\n\t\tconst callId = (\n\t\t\ttypeof (data as Record<string, unknown> | undefined)?.callId === \"string\"\n\t\t\t\t? (data as Record<string, unknown>).callId\n\t\t\t\t: \"\"\n\t\t) as string;\n\t\tconst toolName = (\n\t\t\ttypeof (data as Record<string, unknown> | undefined)?.toolName === \"string\"\n\t\t\t\t? (data as Record<string, unknown>).toolName\n\t\t\t\t: \"\"\n\t\t) as string;\n\t\treturn {\n\t\t\t...base,\n\t\t\tspanId: deriveOtlpSpanId(traceId, `tool.span|${callId}`),\n\t\t\tparentSpanId: deriveOtlpSpanId(traceId, \"turn.span\"),\n\t\t\tname: toolName.length > 0 ? `agent.tool:${toolName}` : \"agent.tool\",\n\t\t\tkind: \"SPAN_KIND_INTERNAL\",\n\t\t\tattributes: attributesFromData(data),\n\t\t};\n\t}\n\treturn null;\n}\n\n/** Normalized trace ingest URL: base URL + signal path unless already present. */\nexport function resolveOtlpTracesUrl(endpoint: string): string {\n\tconst trimmed = endpoint.replace(/\\/+$/, \"\");\n\treturn trimmed.endsWith(OTLP_SIGNAL_PATH) ? trimmed : `${trimmed}${OTLP_SIGNAL_PATH}`;\n}\n\nexport interface OtlpExportSinkOptions {\n\treadonly endpoint: string;\n\treadonly timeoutMs?: number;\n\treadonly flushThreshold?: number;\n\treadonly fetchImpl?: (\n\t\turl: string,\n\t\tinit: { method: \"POST\"; headers: { \"content-type\": string }; body: string; signal: AbortSignal },\n\t) => Promise<{ ok: boolean; status: number }>;\n\t/** Failure channel: first failure of a streak, plus drop notifications. */\n\treadonly onFailure?: (error: Error) => void;\n}\n\nexport function createOtlpTraceExportSink(options: OtlpExportSinkOptions): ObservabilityExportSinkV1 {\n\tconst timeoutMs = options.timeoutMs ?? OTLP_DEFAULT_TIMEOUT_MS;\n\tconst flushThreshold = options.flushThreshold ?? OTLP_DEFAULT_FLUSH_THRESHOLD;\n\tconst url = resolveOtlpTracesUrl(options.endpoint);\n\tconst fetchImpl = options.fetchImpl ?? defaultFetch;\n\n\tlet buffer: OtlpSpan[] = [];\n\tlet dropped = 0;\n\tlet failing = false;\n\tlet inFlight: Promise<void> = Promise.resolve();\n\n\tfunction reportFailure(error: Error): void {\n\t\tif (failing) return;\n\t\tfailing = true;\n\t\ttry {\n\t\t\toptions.onFailure?.(error);\n\t\t} catch {\n\t\t\t// failure reporting must never break the sink\n\t\t}\n\t}\n\n\tasync function deliver(reason: string): Promise<void> {\n\t\tif (buffer.length === 0) return;\n\t\tconst batch = buffer;\n\t\tconst controller = new AbortController();\n\t\tconst timer = setTimeout(() => controller.abort(), timeoutMs);\n\t\ttry {\n\t\t\tconst body = JSON.stringify({\n\t\t\t\tresourceSpans: [\n\t\t\t\t\t{\n\t\t\t\t\t\tresource: { attributes: [attr(\"service.name\", \"agent-forge\")] },\n\t\t\t\t\t\tscopeSpans: [{ scope: { name: \"agent-forge.observability\", version: \"trace.v1\" }, spans: batch }],\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t});\n\t\t\tconst response = await fetchImpl(url, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: { \"content-type\": \"application/json\" },\n\t\t\t\tbody,\n\t\t\t\tsignal: controller.signal,\n\t\t\t});\n\t\t\tif (!response.ok) {\n\t\t\t\treportFailure(new Error(`OTLP export failed (reason=${reason}): HTTP ${response.status}`));\n\t\t\t\tdropOverflow();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tfailing = false;\n\t\t\tbuffer = buffer.slice(batch.length);\n\t\t} catch (error) {\n\t\t\treportFailure(error instanceof Error ? error : new Error(String(error)));\n\t\t\tdropOverflow();\n\t\t} finally {\n\t\t\tclearTimeout(timer);\n\t\t}\n\t}\n\n\tfunction dropOverflow(): void {\n\t\tif (buffer.length <= OTLP_MAX_BUFFERED_SPANS) return;\n\t\tconst excess = buffer.length - OTLP_MAX_BUFFERED_SPANS;\n\t\tbuffer = buffer.slice(excess);\n\t\tdropped += excess;\n\t\treportFailure(new Error(`OTLP export buffer overflow: dropped ${excess} spans (total dropped ${dropped})`));\n\t}\n\n\tfunction flush(reason: string): Promise<void> {\n\t\tconst run = inFlight.then(() => deliver(reason));\n\t\tinFlight = run.catch(() => {});\n\t\treturn run;\n\t}\n\n\treturn {\n\t\tonTraceRecord: (record) => {\n\t\t\tconst span = spanFromRecord(record);\n\t\t\tif (span === null) return;\n\t\t\tbuffer.push(span);\n\t\t\tif (buffer.length >= flushThreshold) void flush(\"threshold\");\n\t\t},\n\t\tflush,\n\t};\n}\n\nfunction defaultFetch(\n\turl: string,\n\tinit: { method: \"POST\"; headers: { \"content-type\": string }; body: string; signal: AbortSignal },\n): Promise<{ ok: boolean; status: number }> {\n\treturn fetch(url, init) as unknown as Promise<{ ok: boolean; status: number }>;\n}\n\n/** Type guard helper used by the plugin wiring: is a record span-exportable at all. */\nexport function isOtlpSpanRecord(record: TraceRecordV1): boolean {\n\treturn spanFromRecord(record) !== null;\n}\n"]}
1
+ {"version":3,"file":"otlp-exporter.d.ts","sourceRoot":"","sources":["../src/otlp-exporter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,KAAK,EAAE,yBAAyB,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAExF,eAAO,MAAM,uBAAuB,OAAQ,CAAC;AAC7C,eAAO,MAAM,4BAA4B,KAAK,CAAC;AAC/C,eAAO,MAAM,uBAAuB,OAAQ,CAAC;AAG7C,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED,2FAA2F;AAC3F,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAErF;AAED,6FAA6F;AAC7F,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEpD;AAgGD,kFAAkF;AAClF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAG7D;AAED,MAAM,WAAW,qBAAqB;IACrC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,SAAS,CAAC,EAAE,CACpB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE;YAAE,cAAc,EAAE,MAAM,CAAA;SAAE,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,WAAW,CAAA;KAAE,KAC5F,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9C,2EAA2E;IAC3E,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAC5C;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,qBAAqB,GAAG,yBAAyB,CAkFnG;AASD,uFAAuF;AACvF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAE/D","sourcesContent":["/**\n * OTLP/HTTP+JSON trace export sink for the first-party observability plugin\n * (design §8 telemetry export seam). Implements `ObservabilityExportSinkV1`\n * over plain `fetch` — no OpenTelemetry SDK dependency; the wire payload is\n * the spec's JSON encoding of `ExportTraceServiceRequest`.\n *\n * Mapping (registered, design §8): one OTLP trace per turn correlation id.\n * Span-bearing records export as spans (`turn.span` root, `llm.response` and\n * `tool.span` parented under it); record kinds without a closed duration\n * (`session.start`, `llm.request`, `lifecycle`, `error`, `decision`,\n * `plugin.log`) and records without a trace id are not exported — the local\n * `agent-forge/trace.v1` file keeps full fidelity. OTel ids are derived\n * deterministically (SHA-256) from the correlation id so parent/child linkage\n * is reproducible across runs and processes.\n *\n * Failure domain: export failures never throw — the first failure of a streak\n * reports through `onFailure`, the batch is retained for the next flush and\n * oldest spans drop (counted) once the cap is hit.\n */\n\nimport { createHash } from \"node:crypto\";\nimport type { ObservabilityExportSinkV1, TraceRecordV1 } from \"@agent-forge/plugin-sdk\";\n\nexport const OTLP_DEFAULT_TIMEOUT_MS = 5_000;\nexport const OTLP_DEFAULT_FLUSH_THRESHOLD = 64;\nexport const OTLP_MAX_BUFFERED_SPANS = 1_024;\nconst OTLP_SIGNAL_PATH = \"/v1/traces\";\n\n/** Deterministic 32-hex OTLP trace id derived from a turn correlation id. */\nexport function deriveOtlpTraceId(correlationId: string): string {\n\treturn createHash(\"sha256\").update(correlationId, \"utf8\").digest(\"hex\").slice(0, 32);\n}\n\n/** Deterministic 16-hex OTLP span id derived from the trace id plus span discriminator. */\nexport function deriveOtlpSpanId(correlationId: string, discriminator: string): string {\n\treturn createHash(\"sha256\").update(`${correlationId}|${discriminator}`, \"utf8\").digest(\"hex\").slice(0, 16);\n}\n\n/** Epoch-ms to OTLP unix-nano string via BigInt (number math loses precision above 2^53). */\nexport function epochMsToUnixNano(ms: number): string {\n\treturn (BigInt(Math.max(0, Math.trunc(ms))) * 1_000_000n).toString();\n}\n\ninterface OtlpAttribute {\n\treadonly key: string;\n\treadonly value: { readonly stringValue: string } | { readonly intValue: string } | { readonly boolValue: boolean };\n}\n\ninterface OtlpSpan {\n\treadonly traceId: string;\n\treadonly spanId: string;\n\treadonly parentSpanId?: string;\n\treadonly name: string;\n\treadonly kind: string;\n\treadonly startTimeUnixNano: string;\n\treadonly endTimeUnixNano: string;\n\treadonly attributes?: readonly OtlpAttribute[];\n\treadonly status: { readonly code: string };\n}\n\nfunction attr(key: string, value: string | number | boolean): OtlpAttribute {\n\tif (typeof value === \"number\") {\n\t\treturn Number.isSafeInteger(value)\n\t\t\t? { key, value: { intValue: String(value) } }\n\t\t\t: { key, value: { stringValue: String(value) } };\n\t}\n\tif (typeof value === \"boolean\") return { key, value: { boolValue: value } };\n\treturn { key, value: { stringValue: value } };\n}\n\n/** Shallow primitive-valued fields of the record's bounded data become span attributes. */\nfunction attributesFromData(data: unknown, limit = 32): OtlpAttribute[] {\n\tif (typeof data !== \"object\" || data === null) return [];\n\tconst out: OtlpAttribute[] = [];\n\tfor (const [key, value] of Object.entries(data as Record<string, unknown>)) {\n\t\tif (out.length >= limit) break;\n\t\tif (typeof value === \"string\" && value.length > 0) out.push(attr(key, value.slice(0, 512)));\n\t\telse if (typeof value === \"number\" && Number.isFinite(value)) out.push(attr(key, value));\n\t\telse if (typeof value === \"boolean\") out.push(attr(key, value));\n\t}\n\treturn out;\n}\n\nfunction spanFromRecord(record: TraceRecordV1): OtlpSpan | null {\n\tconst dur = record.dur;\n\tconst traceId = record.traceId;\n\tif (dur === undefined || traceId === undefined) return null;\n\n\tconst traceHex = deriveOtlpTraceId(traceId);\n\tconst endTime = epochMsToUnixNano(record.t);\n\tconst startTime = epochMsToUnixNano(record.t - dur);\n\tconst status = { code: record.level === \"error\" ? \"STATUS_CODE_ERROR\" : \"STATUS_CODE_OK\" };\n\tconst data = record.data;\n\tconst base = { traceId: traceHex, startTimeUnixNano: startTime, endTimeUnixNano: endTime, status };\n\n\tif (record.kind === \"turn.span\") {\n\t\treturn {\n\t\t\t...base,\n\t\t\tspanId: deriveOtlpSpanId(traceId, \"turn.span\"),\n\t\t\tname: \"agent.turn\",\n\t\t\tkind: \"SPAN_KIND_INTERNAL\",\n\t\t\tattributes: attributesFromData(data),\n\t\t};\n\t}\n\tif (record.kind === \"llm.response\") {\n\t\treturn {\n\t\t\t...base,\n\t\t\tspanId: deriveOtlpSpanId(traceId, \"llm.response\"),\n\t\t\tparentSpanId: deriveOtlpSpanId(traceId, \"turn.span\"),\n\t\t\tname: \"agent.llm\",\n\t\t\tkind: \"SPAN_KIND_CLIENT\",\n\t\t\tattributes: attributesFromData(data),\n\t\t};\n\t}\n\tif (record.kind === \"tool.span\") {\n\t\tconst callId = (\n\t\t\ttypeof (data as Record<string, unknown> | undefined)?.callId === \"string\"\n\t\t\t\t? (data as Record<string, unknown>).callId\n\t\t\t\t: \"\"\n\t\t) as string;\n\t\tconst toolName = (\n\t\t\ttypeof (data as Record<string, unknown> | undefined)?.toolName === \"string\"\n\t\t\t\t? (data as Record<string, unknown>).toolName\n\t\t\t\t: \"\"\n\t\t) as string;\n\t\treturn {\n\t\t\t...base,\n\t\t\tspanId: deriveOtlpSpanId(traceId, `tool.span|${callId}`),\n\t\t\tparentSpanId: deriveOtlpSpanId(traceId, \"turn.span\"),\n\t\t\tname: toolName.length > 0 ? `agent.tool:${toolName}` : \"agent.tool\",\n\t\t\tkind: \"SPAN_KIND_INTERNAL\",\n\t\t\tattributes: attributesFromData(data),\n\t\t};\n\t}\n\treturn null;\n}\n\n/** Normalized trace ingest URL: base URL + signal path unless already present. */\nexport function resolveOtlpTracesUrl(endpoint: string): string {\n\tconst trimmed = endpoint.replace(/\\/+$/, \"\");\n\treturn trimmed.endsWith(OTLP_SIGNAL_PATH) ? trimmed : `${trimmed}${OTLP_SIGNAL_PATH}`;\n}\n\nexport interface OtlpExportSinkOptions {\n\treadonly endpoint: string;\n\treadonly timeoutMs?: number;\n\treadonly flushThreshold?: number;\n\treadonly fetchImpl?: (\n\t\turl: string,\n\t\tinit: { method: \"POST\"; headers: { \"content-type\": string }; body: string; signal: AbortSignal },\n\t) => Promise<{ ok: boolean; status: number }>;\n\t/** Failure channel: first failure of a streak, plus drop notifications. */\n\treadonly onFailure?: (error: Error) => void;\n}\n\nexport function createOtlpTraceExportSink(options: OtlpExportSinkOptions): ObservabilityExportSinkV1 {\n\tconst timeoutMs = options.timeoutMs ?? OTLP_DEFAULT_TIMEOUT_MS;\n\tconst flushThreshold = options.flushThreshold ?? OTLP_DEFAULT_FLUSH_THRESHOLD;\n\tconst url = resolveOtlpTracesUrl(options.endpoint);\n\tconst fetchImpl = options.fetchImpl ?? defaultFetch;\n\n\tlet buffer: OtlpSpan[] = [];\n\tlet dropped = 0;\n\tlet failing = false;\n\tlet inFlight: Promise<void> = Promise.resolve();\n\n\tfunction reportFailure(error: Error): void {\n\t\tif (failing) return;\n\t\tfailing = true;\n\t\ttry {\n\t\t\toptions.onFailure?.(error);\n\t\t} catch {\n\t\t\t// failure reporting must never break the sink\n\t\t}\n\t}\n\n\tasync function deliver(reason: string): Promise<void> {\n\t\tif (buffer.length === 0) return;\n\t\tconst batch = buffer.slice();\n\t\tconst controller = new AbortController();\n\t\tconst timer = setTimeout(() => controller.abort(), timeoutMs);\n\t\ttry {\n\t\t\tconst body = JSON.stringify({\n\t\t\t\tresourceSpans: [\n\t\t\t\t\t{\n\t\t\t\t\t\tresource: { attributes: [attr(\"service.name\", \"agent-forge\")] },\n\t\t\t\t\t\tscopeSpans: [{ scope: { name: \"agent-forge.observability\", version: \"trace.v1\" }, spans: batch }],\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t});\n\t\t\tconst response = await fetchImpl(url, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: { \"content-type\": \"application/json\" },\n\t\t\t\tbody,\n\t\t\t\tsignal: controller.signal,\n\t\t\t});\n\t\t\tif (!response.ok) {\n\t\t\t\treportFailure(new Error(`OTLP export failed (reason=${reason}): HTTP ${response.status}`));\n\t\t\t\tdropOverflow();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tfailing = false;\n\t\t\tbuffer = buffer.slice(batch.length);\n\t\t} catch (error) {\n\t\t\treportFailure(error instanceof Error ? error : new Error(String(error)));\n\t\t\tdropOverflow();\n\t\t} finally {\n\t\t\tclearTimeout(timer);\n\t\t}\n\t}\n\n\tfunction dropOverflow(): void {\n\t\tif (buffer.length <= OTLP_MAX_BUFFERED_SPANS) return;\n\t\tconst excess = buffer.length - OTLP_MAX_BUFFERED_SPANS;\n\t\tbuffer = buffer.slice(excess);\n\t\tdropped += excess;\n\t\treportFailure(new Error(`OTLP export buffer overflow: dropped ${excess} spans (total dropped ${dropped})`));\n\t}\n\n\tfunction flush(reason: string): Promise<void> {\n\t\tconst run = inFlight.then(() => deliver(reason));\n\t\t// deliver() never rejects: every fallible step (body serialization, fetch,\n\t\t// response handling) is wrapped in its internal try/catch and failure\n\t\t// reporting is guarded, so the in-flight chain can carry the promise as-is.\n\t\tinFlight = run;\n\t\treturn run;\n\t}\n\n\treturn {\n\t\tonTraceRecord: (record) => {\n\t\t\tconst span = spanFromRecord(record);\n\t\t\tif (span === null) return;\n\t\t\tbuffer.push(span);\n\t\t\tif (buffer.length >= flushThreshold) void flush(\"threshold\");\n\t\t},\n\t\tflush,\n\t};\n}\n\nfunction defaultFetch(\n\turl: string,\n\tinit: { method: \"POST\"; headers: { \"content-type\": string }; body: string; signal: AbortSignal },\n): Promise<{ ok: boolean; status: number }> {\n\treturn fetch(url, init) as unknown as Promise<{ ok: boolean; status: number }>;\n}\n\n/** Type guard helper used by the plugin wiring: is a record span-exportable at all. */\nexport function isOtlpSpanRecord(record: TraceRecordV1): boolean {\n\treturn spanFromRecord(record) !== null;\n}\n"]}
@@ -137,7 +137,7 @@ export function createOtlpTraceExportSink(options) {
137
137
  async function deliver(reason) {
138
138
  if (buffer.length === 0)
139
139
  return;
140
- const batch = buffer;
140
+ const batch = buffer.slice();
141
141
  const controller = new AbortController();
142
142
  const timer = setTimeout(() => controller.abort(), timeoutMs);
143
143
  try {
@@ -181,7 +181,10 @@ export function createOtlpTraceExportSink(options) {
181
181
  }
182
182
  function flush(reason) {
183
183
  const run = inFlight.then(() => deliver(reason));
184
- inFlight = run.catch(() => { });
184
+ // deliver() never rejects: every fallible step (body serialization, fetch,
185
+ // response handling) is wrapped in its internal try/catch and failure
186
+ // reporting is guarded, so the in-flight chain can carry the promise as-is.
187
+ inFlight = run;
185
188
  return run;
186
189
  }
187
190
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"otlp-exporter.js","sourceRoot":"","sources":["../src/otlp-exporter.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * OTLP/HTTP+JSON trace export sink for the first-party observability plugin\n * (design §8 telemetry export seam). Implements `ObservabilityExportSinkV1`\n * over plain `fetch` — no OpenTelemetry SDK dependency; the wire payload is\n * the spec's JSON encoding of `ExportTraceServiceRequest`.\n *\n * Mapping (registered, design §8): one OTLP trace per turn correlation id.\n * Span-bearing records export as spans (`turn.span` root, `llm.response` and\n * `tool.span` parented under it); record kinds without a closed duration\n * (`session.start`, `llm.request`, `lifecycle`, `error`, `decision`,\n * `plugin.log`) and records without a trace id are not exported — the local\n * `agent-forge/trace.v1` file keeps full fidelity. OTel ids are derived\n * deterministically (SHA-256) from the correlation id so parent/child linkage\n * is reproducible across runs and processes.\n *\n * Failure domain: export failures never throw — the first failure of a streak\n * reports through `onFailure`, the batch is retained for the next flush and\n * oldest spans drop (counted) once the cap is hit.\n */\n\nimport { createHash } from \"node:crypto\";\nimport type { ObservabilityExportSinkV1, TraceRecordV1 } from \"@agent-forge/plugin-sdk\";\n\nexport const OTLP_DEFAULT_TIMEOUT_MS = 5_000;\nexport const OTLP_DEFAULT_FLUSH_THRESHOLD = 64;\nexport const OTLP_MAX_BUFFERED_SPANS = 1_024;\nconst OTLP_SIGNAL_PATH = \"/v1/traces\";\n\n/** Deterministic 32-hex OTLP trace id derived from a turn correlation id. */\nexport function deriveOtlpTraceId(correlationId: string): string {\n\treturn createHash(\"sha256\").update(correlationId, \"utf8\").digest(\"hex\").slice(0, 32);\n}\n\n/** Deterministic 16-hex OTLP span id derived from the trace id plus span discriminator. */\nexport function deriveOtlpSpanId(correlationId: string, discriminator: string): string {\n\treturn createHash(\"sha256\").update(`${correlationId}|${discriminator}`, \"utf8\").digest(\"hex\").slice(0, 16);\n}\n\n/** Epoch-ms to OTLP unix-nano string via BigInt (number math loses precision above 2^53). */\nexport function epochMsToUnixNano(ms: number): string {\n\treturn (BigInt(Math.max(0, Math.trunc(ms))) * 1_000_000n).toString();\n}\n\ninterface OtlpAttribute {\n\treadonly key: string;\n\treadonly value: { readonly stringValue: string } | { readonly intValue: string } | { readonly boolValue: boolean };\n}\n\ninterface OtlpSpan {\n\treadonly traceId: string;\n\treadonly spanId: string;\n\treadonly parentSpanId?: string;\n\treadonly name: string;\n\treadonly kind: string;\n\treadonly startTimeUnixNano: string;\n\treadonly endTimeUnixNano: string;\n\treadonly attributes?: readonly OtlpAttribute[];\n\treadonly status: { readonly code: string };\n}\n\nfunction attr(key: string, value: string | number | boolean): OtlpAttribute {\n\tif (typeof value === \"number\") {\n\t\treturn Number.isSafeInteger(value)\n\t\t\t? { key, value: { intValue: String(value) } }\n\t\t\t: { key, value: { stringValue: String(value) } };\n\t}\n\tif (typeof value === \"boolean\") return { key, value: { boolValue: value } };\n\treturn { key, value: { stringValue: value } };\n}\n\n/** Shallow primitive-valued fields of the record's bounded data become span attributes. */\nfunction attributesFromData(data: unknown, limit = 32): OtlpAttribute[] {\n\tif (typeof data !== \"object\" || data === null) return [];\n\tconst out: OtlpAttribute[] = [];\n\tfor (const [key, value] of Object.entries(data as Record<string, unknown>)) {\n\t\tif (out.length >= limit) break;\n\t\tif (typeof value === \"string\" && value.length > 0) out.push(attr(key, value.slice(0, 512)));\n\t\telse if (typeof value === \"number\" && Number.isFinite(value)) out.push(attr(key, value));\n\t\telse if (typeof value === \"boolean\") out.push(attr(key, value));\n\t}\n\treturn out;\n}\n\nfunction spanFromRecord(record: TraceRecordV1): OtlpSpan | null {\n\tconst dur = record.dur;\n\tconst traceId = record.traceId;\n\tif (dur === undefined || traceId === undefined) return null;\n\n\tconst traceHex = deriveOtlpTraceId(traceId);\n\tconst endTime = epochMsToUnixNano(record.t);\n\tconst startTime = epochMsToUnixNano(record.t - dur);\n\tconst status = { code: record.level === \"error\" ? \"STATUS_CODE_ERROR\" : \"STATUS_CODE_OK\" };\n\tconst data = record.data;\n\tconst base = { traceId: traceHex, startTimeUnixNano: startTime, endTimeUnixNano: endTime, status };\n\n\tif (record.kind === \"turn.span\") {\n\t\treturn {\n\t\t\t...base,\n\t\t\tspanId: deriveOtlpSpanId(traceId, \"turn.span\"),\n\t\t\tname: \"agent.turn\",\n\t\t\tkind: \"SPAN_KIND_INTERNAL\",\n\t\t\tattributes: attributesFromData(data),\n\t\t};\n\t}\n\tif (record.kind === \"llm.response\") {\n\t\treturn {\n\t\t\t...base,\n\t\t\tspanId: deriveOtlpSpanId(traceId, \"llm.response\"),\n\t\t\tparentSpanId: deriveOtlpSpanId(traceId, \"turn.span\"),\n\t\t\tname: \"agent.llm\",\n\t\t\tkind: \"SPAN_KIND_CLIENT\",\n\t\t\tattributes: attributesFromData(data),\n\t\t};\n\t}\n\tif (record.kind === \"tool.span\") {\n\t\tconst callId = (\n\t\t\ttypeof (data as Record<string, unknown> | undefined)?.callId === \"string\"\n\t\t\t\t? (data as Record<string, unknown>).callId\n\t\t\t\t: \"\"\n\t\t) as string;\n\t\tconst toolName = (\n\t\t\ttypeof (data as Record<string, unknown> | undefined)?.toolName === \"string\"\n\t\t\t\t? (data as Record<string, unknown>).toolName\n\t\t\t\t: \"\"\n\t\t) as string;\n\t\treturn {\n\t\t\t...base,\n\t\t\tspanId: deriveOtlpSpanId(traceId, `tool.span|${callId}`),\n\t\t\tparentSpanId: deriveOtlpSpanId(traceId, \"turn.span\"),\n\t\t\tname: toolName.length > 0 ? `agent.tool:${toolName}` : \"agent.tool\",\n\t\t\tkind: \"SPAN_KIND_INTERNAL\",\n\t\t\tattributes: attributesFromData(data),\n\t\t};\n\t}\n\treturn null;\n}\n\n/** Normalized trace ingest URL: base URL + signal path unless already present. */\nexport function resolveOtlpTracesUrl(endpoint: string): string {\n\tconst trimmed = endpoint.replace(/\\/+$/, \"\");\n\treturn trimmed.endsWith(OTLP_SIGNAL_PATH) ? trimmed : `${trimmed}${OTLP_SIGNAL_PATH}`;\n}\n\nexport interface OtlpExportSinkOptions {\n\treadonly endpoint: string;\n\treadonly timeoutMs?: number;\n\treadonly flushThreshold?: number;\n\treadonly fetchImpl?: (\n\t\turl: string,\n\t\tinit: { method: \"POST\"; headers: { \"content-type\": string }; body: string; signal: AbortSignal },\n\t) => Promise<{ ok: boolean; status: number }>;\n\t/** Failure channel: first failure of a streak, plus drop notifications. */\n\treadonly onFailure?: (error: Error) => void;\n}\n\nexport function createOtlpTraceExportSink(options: OtlpExportSinkOptions): ObservabilityExportSinkV1 {\n\tconst timeoutMs = options.timeoutMs ?? OTLP_DEFAULT_TIMEOUT_MS;\n\tconst flushThreshold = options.flushThreshold ?? OTLP_DEFAULT_FLUSH_THRESHOLD;\n\tconst url = resolveOtlpTracesUrl(options.endpoint);\n\tconst fetchImpl = options.fetchImpl ?? defaultFetch;\n\n\tlet buffer: OtlpSpan[] = [];\n\tlet dropped = 0;\n\tlet failing = false;\n\tlet inFlight: Promise<void> = Promise.resolve();\n\n\tfunction reportFailure(error: Error): void {\n\t\tif (failing) return;\n\t\tfailing = true;\n\t\ttry {\n\t\t\toptions.onFailure?.(error);\n\t\t} catch {\n\t\t\t// failure reporting must never break the sink\n\t\t}\n\t}\n\n\tasync function deliver(reason: string): Promise<void> {\n\t\tif (buffer.length === 0) return;\n\t\tconst batch = buffer;\n\t\tconst controller = new AbortController();\n\t\tconst timer = setTimeout(() => controller.abort(), timeoutMs);\n\t\ttry {\n\t\t\tconst body = JSON.stringify({\n\t\t\t\tresourceSpans: [\n\t\t\t\t\t{\n\t\t\t\t\t\tresource: { attributes: [attr(\"service.name\", \"agent-forge\")] },\n\t\t\t\t\t\tscopeSpans: [{ scope: { name: \"agent-forge.observability\", version: \"trace.v1\" }, spans: batch }],\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t});\n\t\t\tconst response = await fetchImpl(url, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: { \"content-type\": \"application/json\" },\n\t\t\t\tbody,\n\t\t\t\tsignal: controller.signal,\n\t\t\t});\n\t\t\tif (!response.ok) {\n\t\t\t\treportFailure(new Error(`OTLP export failed (reason=${reason}): HTTP ${response.status}`));\n\t\t\t\tdropOverflow();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tfailing = false;\n\t\t\tbuffer = buffer.slice(batch.length);\n\t\t} catch (error) {\n\t\t\treportFailure(error instanceof Error ? error : new Error(String(error)));\n\t\t\tdropOverflow();\n\t\t} finally {\n\t\t\tclearTimeout(timer);\n\t\t}\n\t}\n\n\tfunction dropOverflow(): void {\n\t\tif (buffer.length <= OTLP_MAX_BUFFERED_SPANS) return;\n\t\tconst excess = buffer.length - OTLP_MAX_BUFFERED_SPANS;\n\t\tbuffer = buffer.slice(excess);\n\t\tdropped += excess;\n\t\treportFailure(new Error(`OTLP export buffer overflow: dropped ${excess} spans (total dropped ${dropped})`));\n\t}\n\n\tfunction flush(reason: string): Promise<void> {\n\t\tconst run = inFlight.then(() => deliver(reason));\n\t\tinFlight = run.catch(() => {});\n\t\treturn run;\n\t}\n\n\treturn {\n\t\tonTraceRecord: (record) => {\n\t\t\tconst span = spanFromRecord(record);\n\t\t\tif (span === null) return;\n\t\t\tbuffer.push(span);\n\t\t\tif (buffer.length >= flushThreshold) void flush(\"threshold\");\n\t\t},\n\t\tflush,\n\t};\n}\n\nfunction defaultFetch(\n\turl: string,\n\tinit: { method: \"POST\"; headers: { \"content-type\": string }; body: string; signal: AbortSignal },\n): Promise<{ ok: boolean; status: number }> {\n\treturn fetch(url, init) as unknown as Promise<{ ok: boolean; status: number }>;\n}\n\n/** Type guard helper used by the plugin wiring: is a record span-exportable at all. */\nexport function isOtlpSpanRecord(record: TraceRecordV1): boolean {\n\treturn spanFromRecord(record) !== null;\n}\n"]}
1
+ {"version":3,"file":"otlp-exporter.js","sourceRoot":"","sources":["../src/otlp-exporter.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * OTLP/HTTP+JSON trace export sink for the first-party observability plugin\n * (design §8 telemetry export seam). Implements `ObservabilityExportSinkV1`\n * over plain `fetch` — no OpenTelemetry SDK dependency; the wire payload is\n * the spec's JSON encoding of `ExportTraceServiceRequest`.\n *\n * Mapping (registered, design §8): one OTLP trace per turn correlation id.\n * Span-bearing records export as spans (`turn.span` root, `llm.response` and\n * `tool.span` parented under it); record kinds without a closed duration\n * (`session.start`, `llm.request`, `lifecycle`, `error`, `decision`,\n * `plugin.log`) and records without a trace id are not exported — the local\n * `agent-forge/trace.v1` file keeps full fidelity. OTel ids are derived\n * deterministically (SHA-256) from the correlation id so parent/child linkage\n * is reproducible across runs and processes.\n *\n * Failure domain: export failures never throw — the first failure of a streak\n * reports through `onFailure`, the batch is retained for the next flush and\n * oldest spans drop (counted) once the cap is hit.\n */\n\nimport { createHash } from \"node:crypto\";\nimport type { ObservabilityExportSinkV1, TraceRecordV1 } from \"@agent-forge/plugin-sdk\";\n\nexport const OTLP_DEFAULT_TIMEOUT_MS = 5_000;\nexport const OTLP_DEFAULT_FLUSH_THRESHOLD = 64;\nexport const OTLP_MAX_BUFFERED_SPANS = 1_024;\nconst OTLP_SIGNAL_PATH = \"/v1/traces\";\n\n/** Deterministic 32-hex OTLP trace id derived from a turn correlation id. */\nexport function deriveOtlpTraceId(correlationId: string): string {\n\treturn createHash(\"sha256\").update(correlationId, \"utf8\").digest(\"hex\").slice(0, 32);\n}\n\n/** Deterministic 16-hex OTLP span id derived from the trace id plus span discriminator. */\nexport function deriveOtlpSpanId(correlationId: string, discriminator: string): string {\n\treturn createHash(\"sha256\").update(`${correlationId}|${discriminator}`, \"utf8\").digest(\"hex\").slice(0, 16);\n}\n\n/** Epoch-ms to OTLP unix-nano string via BigInt (number math loses precision above 2^53). */\nexport function epochMsToUnixNano(ms: number): string {\n\treturn (BigInt(Math.max(0, Math.trunc(ms))) * 1_000_000n).toString();\n}\n\ninterface OtlpAttribute {\n\treadonly key: string;\n\treadonly value: { readonly stringValue: string } | { readonly intValue: string } | { readonly boolValue: boolean };\n}\n\ninterface OtlpSpan {\n\treadonly traceId: string;\n\treadonly spanId: string;\n\treadonly parentSpanId?: string;\n\treadonly name: string;\n\treadonly kind: string;\n\treadonly startTimeUnixNano: string;\n\treadonly endTimeUnixNano: string;\n\treadonly attributes?: readonly OtlpAttribute[];\n\treadonly status: { readonly code: string };\n}\n\nfunction attr(key: string, value: string | number | boolean): OtlpAttribute {\n\tif (typeof value === \"number\") {\n\t\treturn Number.isSafeInteger(value)\n\t\t\t? { key, value: { intValue: String(value) } }\n\t\t\t: { key, value: { stringValue: String(value) } };\n\t}\n\tif (typeof value === \"boolean\") return { key, value: { boolValue: value } };\n\treturn { key, value: { stringValue: value } };\n}\n\n/** Shallow primitive-valued fields of the record's bounded data become span attributes. */\nfunction attributesFromData(data: unknown, limit = 32): OtlpAttribute[] {\n\tif (typeof data !== \"object\" || data === null) return [];\n\tconst out: OtlpAttribute[] = [];\n\tfor (const [key, value] of Object.entries(data as Record<string, unknown>)) {\n\t\tif (out.length >= limit) break;\n\t\tif (typeof value === \"string\" && value.length > 0) out.push(attr(key, value.slice(0, 512)));\n\t\telse if (typeof value === \"number\" && Number.isFinite(value)) out.push(attr(key, value));\n\t\telse if (typeof value === \"boolean\") out.push(attr(key, value));\n\t}\n\treturn out;\n}\n\nfunction spanFromRecord(record: TraceRecordV1): OtlpSpan | null {\n\tconst dur = record.dur;\n\tconst traceId = record.traceId;\n\tif (dur === undefined || traceId === undefined) return null;\n\n\tconst traceHex = deriveOtlpTraceId(traceId);\n\tconst endTime = epochMsToUnixNano(record.t);\n\tconst startTime = epochMsToUnixNano(record.t - dur);\n\tconst status = { code: record.level === \"error\" ? \"STATUS_CODE_ERROR\" : \"STATUS_CODE_OK\" };\n\tconst data = record.data;\n\tconst base = { traceId: traceHex, startTimeUnixNano: startTime, endTimeUnixNano: endTime, status };\n\n\tif (record.kind === \"turn.span\") {\n\t\treturn {\n\t\t\t...base,\n\t\t\tspanId: deriveOtlpSpanId(traceId, \"turn.span\"),\n\t\t\tname: \"agent.turn\",\n\t\t\tkind: \"SPAN_KIND_INTERNAL\",\n\t\t\tattributes: attributesFromData(data),\n\t\t};\n\t}\n\tif (record.kind === \"llm.response\") {\n\t\treturn {\n\t\t\t...base,\n\t\t\tspanId: deriveOtlpSpanId(traceId, \"llm.response\"),\n\t\t\tparentSpanId: deriveOtlpSpanId(traceId, \"turn.span\"),\n\t\t\tname: \"agent.llm\",\n\t\t\tkind: \"SPAN_KIND_CLIENT\",\n\t\t\tattributes: attributesFromData(data),\n\t\t};\n\t}\n\tif (record.kind === \"tool.span\") {\n\t\tconst callId = (\n\t\t\ttypeof (data as Record<string, unknown> | undefined)?.callId === \"string\"\n\t\t\t\t? (data as Record<string, unknown>).callId\n\t\t\t\t: \"\"\n\t\t) as string;\n\t\tconst toolName = (\n\t\t\ttypeof (data as Record<string, unknown> | undefined)?.toolName === \"string\"\n\t\t\t\t? (data as Record<string, unknown>).toolName\n\t\t\t\t: \"\"\n\t\t) as string;\n\t\treturn {\n\t\t\t...base,\n\t\t\tspanId: deriveOtlpSpanId(traceId, `tool.span|${callId}`),\n\t\t\tparentSpanId: deriveOtlpSpanId(traceId, \"turn.span\"),\n\t\t\tname: toolName.length > 0 ? `agent.tool:${toolName}` : \"agent.tool\",\n\t\t\tkind: \"SPAN_KIND_INTERNAL\",\n\t\t\tattributes: attributesFromData(data),\n\t\t};\n\t}\n\treturn null;\n}\n\n/** Normalized trace ingest URL: base URL + signal path unless already present. */\nexport function resolveOtlpTracesUrl(endpoint: string): string {\n\tconst trimmed = endpoint.replace(/\\/+$/, \"\");\n\treturn trimmed.endsWith(OTLP_SIGNAL_PATH) ? trimmed : `${trimmed}${OTLP_SIGNAL_PATH}`;\n}\n\nexport interface OtlpExportSinkOptions {\n\treadonly endpoint: string;\n\treadonly timeoutMs?: number;\n\treadonly flushThreshold?: number;\n\treadonly fetchImpl?: (\n\t\turl: string,\n\t\tinit: { method: \"POST\"; headers: { \"content-type\": string }; body: string; signal: AbortSignal },\n\t) => Promise<{ ok: boolean; status: number }>;\n\t/** Failure channel: first failure of a streak, plus drop notifications. */\n\treadonly onFailure?: (error: Error) => void;\n}\n\nexport function createOtlpTraceExportSink(options: OtlpExportSinkOptions): ObservabilityExportSinkV1 {\n\tconst timeoutMs = options.timeoutMs ?? OTLP_DEFAULT_TIMEOUT_MS;\n\tconst flushThreshold = options.flushThreshold ?? OTLP_DEFAULT_FLUSH_THRESHOLD;\n\tconst url = resolveOtlpTracesUrl(options.endpoint);\n\tconst fetchImpl = options.fetchImpl ?? defaultFetch;\n\n\tlet buffer: OtlpSpan[] = [];\n\tlet dropped = 0;\n\tlet failing = false;\n\tlet inFlight: Promise<void> = Promise.resolve();\n\n\tfunction reportFailure(error: Error): void {\n\t\tif (failing) return;\n\t\tfailing = true;\n\t\ttry {\n\t\t\toptions.onFailure?.(error);\n\t\t} catch {\n\t\t\t// failure reporting must never break the sink\n\t\t}\n\t}\n\n\tasync function deliver(reason: string): Promise<void> {\n\t\tif (buffer.length === 0) return;\n\t\tconst batch = buffer.slice();\n\t\tconst controller = new AbortController();\n\t\tconst timer = setTimeout(() => controller.abort(), timeoutMs);\n\t\ttry {\n\t\t\tconst body = JSON.stringify({\n\t\t\t\tresourceSpans: [\n\t\t\t\t\t{\n\t\t\t\t\t\tresource: { attributes: [attr(\"service.name\", \"agent-forge\")] },\n\t\t\t\t\t\tscopeSpans: [{ scope: { name: \"agent-forge.observability\", version: \"trace.v1\" }, spans: batch }],\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t});\n\t\t\tconst response = await fetchImpl(url, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: { \"content-type\": \"application/json\" },\n\t\t\t\tbody,\n\t\t\t\tsignal: controller.signal,\n\t\t\t});\n\t\t\tif (!response.ok) {\n\t\t\t\treportFailure(new Error(`OTLP export failed (reason=${reason}): HTTP ${response.status}`));\n\t\t\t\tdropOverflow();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tfailing = false;\n\t\t\tbuffer = buffer.slice(batch.length);\n\t\t} catch (error) {\n\t\t\treportFailure(error instanceof Error ? error : new Error(String(error)));\n\t\t\tdropOverflow();\n\t\t} finally {\n\t\t\tclearTimeout(timer);\n\t\t}\n\t}\n\n\tfunction dropOverflow(): void {\n\t\tif (buffer.length <= OTLP_MAX_BUFFERED_SPANS) return;\n\t\tconst excess = buffer.length - OTLP_MAX_BUFFERED_SPANS;\n\t\tbuffer = buffer.slice(excess);\n\t\tdropped += excess;\n\t\treportFailure(new Error(`OTLP export buffer overflow: dropped ${excess} spans (total dropped ${dropped})`));\n\t}\n\n\tfunction flush(reason: string): Promise<void> {\n\t\tconst run = inFlight.then(() => deliver(reason));\n\t\t// deliver() never rejects: every fallible step (body serialization, fetch,\n\t\t// response handling) is wrapped in its internal try/catch and failure\n\t\t// reporting is guarded, so the in-flight chain can carry the promise as-is.\n\t\tinFlight = run;\n\t\treturn run;\n\t}\n\n\treturn {\n\t\tonTraceRecord: (record) => {\n\t\t\tconst span = spanFromRecord(record);\n\t\t\tif (span === null) return;\n\t\t\tbuffer.push(span);\n\t\t\tif (buffer.length >= flushThreshold) void flush(\"threshold\");\n\t\t},\n\t\tflush,\n\t};\n}\n\nfunction defaultFetch(\n\turl: string,\n\tinit: { method: \"POST\"; headers: { \"content-type\": string }; body: string; signal: AbortSignal },\n): Promise<{ ok: boolean; status: number }> {\n\treturn fetch(url, init) as unknown as Promise<{ ok: boolean; status: number }>;\n}\n\n/** Type guard helper used by the plugin wiring: is a record span-exportable at all. */\nexport function isOtlpSpanRecord(record: TraceRecordV1): boolean {\n\treturn spanFromRecord(record) !== null;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-agent-forge/plugin-observability",
3
- "version": "0.88.11",
3
+ "version": "0.88.12",
4
4
  "description": "Observability plugin for Agent Forge (event journal, problem reports, session trace sink, OTLP export)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -21,7 +21,8 @@
21
21
  "clean": "shx rm -rf dist",
22
22
  "build": "tsgo -p tsconfig.build.json",
23
23
  "test": "vitest --run",
24
- "prepublishOnly": "npm run clean && npm run build"
24
+ "prepublishOnly": "npm run clean && npm run build",
25
+ "test:coverage": "vitest --run --coverage"
25
26
  },
26
27
  "keywords": [
27
28
  "agent-forge",
@@ -43,10 +44,10 @@
43
44
  "node": ">=22.19.0"
44
45
  },
45
46
  "dependencies": {
46
- "@ai-agent-forge/plugin-sdk": "^0.88.11"
47
+ "@ai-agent-forge/plugin-sdk": "^0.88.12"
47
48
  },
48
49
  "devDependencies": {
49
- "shx": "0.4.0",
50
+ "shx": "0.3.4",
50
51
  "vitest": "4.1.11"
51
52
  }
52
53
  }
package/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "agent-forge.plugin.observability",
3
- "version": "0.88.11",
3
+ "version": "0.88.12",
4
4
  "apiVersion": "1-draft",
5
5
  "entry": "./dist/entry.js",
6
6
  "hostVersion": ">=0.84.4",