@poncho-ai/harness 0.33.0 → 0.33.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
 
2
- > @poncho-ai/harness@0.33.0 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
2
+ > @poncho-ai/harness@0.33.1 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
3
3
  > node scripts/embed-docs.js && tsup src/index.ts --format esm --dts
4
4
 
5
5
  [embed-docs] Generated poncho-docs.ts with 4 topics
@@ -8,8 +8,8 @@
8
8
  CLI tsup v8.5.1
9
9
  CLI Target: es2022
10
10
  ESM Build start
11
- ESM dist/index.js 334.17 KB
12
- ESM ⚡️ Build success in 152ms
11
+ ESM dist/index.js 335.08 KB
12
+ ESM ⚡️ Build success in 167ms
13
13
  DTS Build start
14
- DTS ⚡️ Build success in 7104ms
14
+ DTS ⚡️ Build success in 7028ms
15
15
  DTS dist/index.d.ts 33.55 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # @poncho-ai/harness
2
2
 
3
+ ## 0.33.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [`d8fe87c`](https://github.com/cesr/poncho-ai/commit/d8fe87c68d42878829422750f98e3c70a425e3e3) Thanks [@cesr](https://github.com/cesr)! - fix: OTLP trace exporter reliability and error visibility
8
+ - Use provider instance directly instead of global `trace.getTracer()` to avoid silent failure when another library registers a tracer provider first
9
+ - Append `/v1/traces` to base OTLP endpoints so users can pass either the base URL or the full signal-specific URL
10
+ - Surface HTTP status code and response body on export failures
11
+ - Enable OTel diagnostic logger at WARN level for internal SDK errors
12
+
3
13
  ## 0.33.0
4
14
 
5
15
  ### Minor Changes
package/dist/index.js CHANGED
@@ -5343,7 +5343,7 @@ var createSubagentTools = (manager) => [
5343
5343
  ];
5344
5344
 
5345
5345
  // src/harness.ts
5346
- import { trace, context as otelContext, SpanStatusCode, SpanKind } from "@opentelemetry/api";
5346
+ import { trace, context as otelContext, SpanStatusCode, SpanKind, diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
5347
5347
  import { NodeTracerProvider, BatchSpanProcessor } from "@opentelemetry/sdk-trace-node";
5348
5348
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
5349
5349
 
@@ -5357,10 +5357,15 @@ function sanitizeEventForLog(event) {
5357
5357
  return value;
5358
5358
  });
5359
5359
  }
5360
+ var TRACES_PATH = "/v1/traces";
5361
+ function ensureTracesPath(url) {
5362
+ if (url.endsWith(TRACES_PATH)) return url;
5363
+ return url.replace(/\/+$/, "") + TRACES_PATH;
5364
+ }
5360
5365
  function normalizeOtlp(opt) {
5361
5366
  if (!opt) return void 0;
5362
- if (typeof opt === "string") return opt ? { url: opt } : void 0;
5363
- return opt.url ? opt : void 0;
5367
+ if (typeof opt === "string") return opt ? { url: ensureTracesPath(opt) } : void 0;
5368
+ return opt.url ? { ...opt, url: ensureTracesPath(opt.url) } : void 0;
5364
5369
  }
5365
5370
  var TelemetryEmitter = class {
5366
5371
  config;
@@ -5414,7 +5419,10 @@ var TelemetryEmitter = class {
5414
5419
  ]
5415
5420
  })
5416
5421
  });
5417
- } catch {
5422
+ } catch (err) {
5423
+ console.warn(
5424
+ `[poncho][telemetry] OTLP log delivery failed: ${err instanceof Error ? err.message : String(err)}`
5425
+ );
5418
5426
  }
5419
5427
  }
5420
5428
  };
@@ -5507,6 +5515,16 @@ var ToolDispatcher = class {
5507
5515
  };
5508
5516
 
5509
5517
  // src/harness.ts
5518
+ function formatOtlpError(err) {
5519
+ if (!(err instanceof Error)) return String(err);
5520
+ const parts = [];
5521
+ const code = err.code;
5522
+ if (code != null) parts.push(`HTTP ${code}`);
5523
+ if (err.message) parts.push(err.message);
5524
+ const data = err.data;
5525
+ if (data) parts.push(data);
5526
+ return parts.join(" \u2014 ") || "unknown error";
5527
+ }
5510
5528
  var now = () => Date.now();
5511
5529
  var FIRST_CHUNK_TIMEOUT_MS = 9e4;
5512
5530
  var MAX_TRANSIENT_STEP_RETRIES = 1;
@@ -6603,6 +6621,7 @@ var AgentHarness = class _AgentHarness {
6603
6621
  const telemetryEnabled = config?.telemetry?.enabled !== false;
6604
6622
  const otlpConfig = telemetryEnabled ? normalizeOtlp(config?.telemetry?.otlp) : void 0;
6605
6623
  if (otlpConfig) {
6624
+ diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.WARN);
6606
6625
  const exporter = new OTLPTraceExporter({
6607
6626
  url: otlpConfig.url,
6608
6627
  headers: otlpConfig.headers
@@ -6615,7 +6634,7 @@ var AgentHarness = class _AgentHarness {
6615
6634
  provider2.register();
6616
6635
  this.otlpTracerProvider = provider2;
6617
6636
  this.hasOtlpExporter = true;
6618
- console.info(`[poncho][telemetry] OTLP exporter active \u2192 ${otlpConfig.url}`);
6637
+ console.info(`[poncho][telemetry] OTLP trace exporter active \u2192 ${otlpConfig.url}`);
6619
6638
  }
6620
6639
  }
6621
6640
  async buildBrowserStoragePersistence(config, sessionId) {
@@ -6761,17 +6780,13 @@ var AgentHarness = class _AgentHarness {
6761
6780
  await this.mcpBridge?.stopLocalServers();
6762
6781
  if (this.otlpSpanProcessor) {
6763
6782
  await this.otlpSpanProcessor.shutdown().catch((err) => {
6764
- console.warn(
6765
- `[poncho][telemetry] OTLP span processor shutdown error: ${err instanceof Error ? err.message : String(err)}`
6766
- );
6783
+ console.warn(`[poncho][telemetry] OTLP span processor shutdown error: ${formatOtlpError(err)}`);
6767
6784
  });
6768
6785
  this.otlpSpanProcessor = void 0;
6769
6786
  }
6770
6787
  if (this.otlpTracerProvider) {
6771
6788
  await this.otlpTracerProvider.shutdown().catch((err) => {
6772
- console.warn(
6773
- `[poncho][telemetry] OTLP tracer provider shutdown error: ${err instanceof Error ? err.message : String(err)}`
6774
- );
6789
+ console.warn(`[poncho][telemetry] OTLP tracer provider shutdown error: ${formatOtlpError(err)}`);
6775
6790
  });
6776
6791
  this.otlpTracerProvider = void 0;
6777
6792
  }
@@ -6785,8 +6800,8 @@ var AgentHarness = class _AgentHarness {
6785
6800
  * child spans (LLM calls via AI SDK, tool execution) group under one trace.
6786
6801
  */
6787
6802
  async *runWithTelemetry(input) {
6788
- if (this.hasOtlpExporter) {
6789
- const tracer = trace.getTracer("gen_ai");
6803
+ if (this.hasOtlpExporter && this.otlpTracerProvider) {
6804
+ const tracer = this.otlpTracerProvider.getTracer("gen_ai");
6790
6805
  const agentName = this.parsedAgent?.frontmatter.name ?? "agent";
6791
6806
  const rootSpan = tracer.startSpan(`invoke_agent ${agentName}`, {
6792
6807
  kind: SpanKind.INTERNAL,
@@ -6815,7 +6830,9 @@ var AgentHarness = class _AgentHarness {
6815
6830
  rootSpan.end();
6816
6831
  try {
6817
6832
  await this.otlpSpanProcessor?.forceFlush();
6818
- } catch {
6833
+ } catch (err) {
6834
+ const detail = formatOtlpError(err);
6835
+ console.warn(`[poncho][telemetry] OTLP span flush failed: ${detail}`);
6819
6836
  }
6820
6837
  }
6821
6838
  } else {
@@ -7684,8 +7701,8 @@ ${textContent}` };
7684
7701
  return;
7685
7702
  }
7686
7703
  const toolSpans = /* @__PURE__ */ new Map();
7687
- if (this.hasOtlpExporter) {
7688
- const tracer = trace.getTracer("gen_ai");
7704
+ if (this.hasOtlpExporter && this.otlpTracerProvider) {
7705
+ const tracer = this.otlpTracerProvider.getTracer("gen_ai");
7689
7706
  for (const call of approvedCalls) {
7690
7707
  const toolDef = this.dispatcher.get(call.name);
7691
7708
  toolSpans.set(call.id, tracer.startSpan(`execute_tool ${call.name}`, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/harness",
3
- "version": "0.33.0",
3
+ "version": "0.33.1",
4
4
  "description": "Agent execution runtime - conversation loop, tool dispatch, streaming",
5
5
  "repository": {
6
6
  "type": "git",
package/src/harness.ts CHANGED
@@ -37,10 +37,22 @@ import { createSkillTools, normalizeScriptPolicyPath } from "./skill-tools.js";
37
37
  import { createSearchTools } from "./search-tools.js";
38
38
  import { createSubagentTools } from "./subagent-tools.js";
39
39
  import type { SubagentManager } from "./subagent-manager.js";
40
- import { trace, context as otelContext, SpanStatusCode, SpanKind } from "@opentelemetry/api";
40
+ import { trace, context as otelContext, SpanStatusCode, SpanKind, diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
41
41
  import { NodeTracerProvider, BatchSpanProcessor } from "@opentelemetry/sdk-trace-node";
42
42
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
43
43
  import { normalizeOtlp } from "./telemetry.js";
44
+
45
+ /** Extract useful details from OTLPExporterError (has .code + .data) or plain Error. */
46
+ function formatOtlpError(err: unknown): string {
47
+ if (!(err instanceof Error)) return String(err);
48
+ const parts: string[] = [];
49
+ const code = (err as { code?: number }).code;
50
+ if (code != null) parts.push(`HTTP ${code}`);
51
+ if (err.message) parts.push(err.message);
52
+ const data = (err as { data?: string }).data;
53
+ if (data) parts.push(data);
54
+ return parts.join(" — ") || "unknown error";
55
+ }
44
56
  import {
45
57
  isSiblingScriptsPattern,
46
58
  matchesRelativeScriptPattern,
@@ -1346,6 +1358,7 @@ export class AgentHarness {
1346
1358
  const telemetryEnabled = config?.telemetry?.enabled !== false;
1347
1359
  const otlpConfig = telemetryEnabled ? normalizeOtlp(config?.telemetry?.otlp) : undefined;
1348
1360
  if (otlpConfig) {
1361
+ diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.WARN);
1349
1362
  const exporter = new OTLPTraceExporter({
1350
1363
  url: otlpConfig.url,
1351
1364
  headers: otlpConfig.headers,
@@ -1358,7 +1371,7 @@ export class AgentHarness {
1358
1371
  provider.register();
1359
1372
  this.otlpTracerProvider = provider;
1360
1373
  this.hasOtlpExporter = true;
1361
- console.info(`[poncho][telemetry] OTLP exporter active → ${otlpConfig.url}`);
1374
+ console.info(`[poncho][telemetry] OTLP trace exporter active → ${otlpConfig.url}`);
1362
1375
  }
1363
1376
  }
1364
1377
 
@@ -1523,21 +1536,13 @@ export class AgentHarness {
1523
1536
  await this.mcpBridge?.stopLocalServers();
1524
1537
  if (this.otlpSpanProcessor) {
1525
1538
  await this.otlpSpanProcessor.shutdown().catch((err) => {
1526
- console.warn(
1527
- `[poncho][telemetry] OTLP span processor shutdown error: ${
1528
- err instanceof Error ? err.message : String(err)
1529
- }`,
1530
- );
1539
+ console.warn(`[poncho][telemetry] OTLP span processor shutdown error: ${formatOtlpError(err)}`);
1531
1540
  });
1532
1541
  this.otlpSpanProcessor = undefined;
1533
1542
  }
1534
1543
  if (this.otlpTracerProvider) {
1535
1544
  await this.otlpTracerProvider.shutdown().catch((err) => {
1536
- console.warn(
1537
- `[poncho][telemetry] OTLP tracer provider shutdown error: ${
1538
- err instanceof Error ? err.message : String(err)
1539
- }`,
1540
- );
1545
+ console.warn(`[poncho][telemetry] OTLP tracer provider shutdown error: ${formatOtlpError(err)}`);
1541
1546
  });
1542
1547
  this.otlpTracerProvider = undefined;
1543
1548
  }
@@ -1553,8 +1558,8 @@ export class AgentHarness {
1553
1558
  * child spans (LLM calls via AI SDK, tool execution) group under one trace.
1554
1559
  */
1555
1560
  async *runWithTelemetry(input: RunInput): AsyncGenerator<AgentEvent> {
1556
- if (this.hasOtlpExporter) {
1557
- const tracer = trace.getTracer("gen_ai");
1561
+ if (this.hasOtlpExporter && this.otlpTracerProvider) {
1562
+ const tracer = this.otlpTracerProvider.getTracer("gen_ai");
1558
1563
  const agentName = this.parsedAgent?.frontmatter.name ?? "agent";
1559
1564
 
1560
1565
  const rootSpan = tracer.startSpan(`invoke_agent ${agentName}`, {
@@ -1586,7 +1591,10 @@ export class AgentHarness {
1586
1591
  rootSpan.end();
1587
1592
  try {
1588
1593
  await this.otlpSpanProcessor?.forceFlush();
1589
- } catch { /* best-effort */ }
1594
+ } catch (err: unknown) {
1595
+ const detail = formatOtlpError(err);
1596
+ console.warn(`[poncho][telemetry] OTLP span flush failed: ${detail}`);
1597
+ }
1590
1598
  }
1591
1599
  } else {
1592
1600
  yield* this.run(input);
@@ -2642,8 +2650,8 @@ ${boundedMainMemory.trim()}`
2642
2650
  // OTel GenAI execute_tool spans for tool call visibility in traces
2643
2651
  type OtelSpan = ReturnType<ReturnType<typeof trace.getTracer>["startSpan"]>;
2644
2652
  const toolSpans = new Map<string, OtelSpan>();
2645
- if (this.hasOtlpExporter) {
2646
- const tracer = trace.getTracer("gen_ai");
2653
+ if (this.hasOtlpExporter && this.otlpTracerProvider) {
2654
+ const tracer = this.otlpTracerProvider.getTracer("gen_ai");
2647
2655
  for (const call of approvedCalls) {
2648
2656
  const toolDef = this.dispatcher.get(call.name);
2649
2657
  toolSpans.set(call.id, tracer.startSpan(`execute_tool ${call.name}`, {
package/src/telemetry.ts CHANGED
@@ -18,10 +18,26 @@ export interface OtlpConfig {
18
18
 
19
19
  export type OtlpOption = string | OtlpConfig;
20
20
 
21
+ const TRACES_PATH = "/v1/traces";
22
+
23
+ /**
24
+ * Ensures the OTLP URL points to the trace-ingest endpoint.
25
+ *
26
+ * Users typically configure the *base* OTLP endpoint (e.g.
27
+ * `https://gateway.example.com/api/v1/otlp`) but `OTLPTraceExporter` uses
28
+ * the `url` constructor option as-is — it only appends `/v1/traces`
29
+ * automatically when reading from the `OTEL_EXPORTER_OTLP_ENDPOINT` env var.
30
+ * Without this fixup traces are POSTed to the wrong path and silently lost.
31
+ */
32
+ function ensureTracesPath(url: string): string {
33
+ if (url.endsWith(TRACES_PATH)) return url;
34
+ return url.replace(/\/+$/, "") + TRACES_PATH;
35
+ }
36
+
21
37
  export function normalizeOtlp(opt: OtlpOption | undefined): OtlpConfig | undefined {
22
38
  if (!opt) return undefined;
23
- if (typeof opt === "string") return opt ? { url: opt } : undefined;
24
- return opt.url ? opt : undefined;
39
+ if (typeof opt === "string") return opt ? { url: ensureTracesPath(opt) } : undefined;
40
+ return opt.url ? { ...opt, url: ensureTracesPath(opt.url) } : undefined;
25
41
  }
26
42
 
27
43
  export interface TelemetryConfig {
@@ -87,8 +103,12 @@ export class TelemetryEmitter {
87
103
  ],
88
104
  }),
89
105
  });
90
- } catch {
91
- // Ignore telemetry delivery failures.
106
+ } catch (err) {
107
+ console.warn(
108
+ `[poncho][telemetry] OTLP log delivery failed: ${
109
+ err instanceof Error ? err.message : String(err)
110
+ }`,
111
+ );
92
112
  }
93
113
  }
94
114
 
@@ -1,5 +1,42 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
- import { TelemetryEmitter } from "../src/telemetry.js";
2
+ import { TelemetryEmitter, normalizeOtlp } from "../src/telemetry.js";
3
+
4
+ describe("normalizeOtlp", () => {
5
+ it("appends /v1/traces to a base URL string", () => {
6
+ expect(normalizeOtlp("https://gateway.example.com/api/v1/otlp")).toEqual({
7
+ url: "https://gateway.example.com/api/v1/otlp/v1/traces",
8
+ });
9
+ });
10
+
11
+ it("does not double-append when URL already ends with /v1/traces", () => {
12
+ expect(normalizeOtlp("https://api.honeycomb.io/v1/traces")).toEqual({
13
+ url: "https://api.honeycomb.io/v1/traces",
14
+ });
15
+ });
16
+
17
+ it("strips trailing slashes before appending", () => {
18
+ expect(normalizeOtlp("https://gateway.example.com/")).toEqual({
19
+ url: "https://gateway.example.com/v1/traces",
20
+ });
21
+ });
22
+
23
+ it("appends /v1/traces to object config", () => {
24
+ expect(
25
+ normalizeOtlp({
26
+ url: "https://gateway.example.com/otlp",
27
+ headers: { Authorization: "Bearer tok" },
28
+ }),
29
+ ).toEqual({
30
+ url: "https://gateway.example.com/otlp/v1/traces",
31
+ headers: { Authorization: "Bearer tok" },
32
+ });
33
+ });
34
+
35
+ it("returns undefined for falsy input", () => {
36
+ expect(normalizeOtlp(undefined)).toBeUndefined();
37
+ expect(normalizeOtlp("")).toBeUndefined();
38
+ });
39
+ });
3
40
 
4
41
  describe("telemetry emitter", () => {
5
42
  it("delegates to custom handler when configured", async () => {
@@ -14,7 +51,7 @@ describe("telemetry emitter", () => {
14
51
  global.fetch = vi.fn().mockRejectedValue(new Error("network down"));
15
52
 
16
53
  const emitter = new TelemetryEmitter({
17
- otlp: "https://otel.example.com/v1/logs",
54
+ otlp: "https://otel.example.com/v1/traces",
18
55
  });
19
56
 
20
57
  await expect(