neatlogs 1.0.8 → 1.0.9

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/index.cjs CHANGED
@@ -5899,7 +5899,7 @@ async function removeTag(name, tag) {
5899
5899
  }
5900
5900
 
5901
5901
  // src/version.ts
5902
- var __version__ = "1.0.8";
5902
+ var __version__ = "1.0.9";
5903
5903
 
5904
5904
  // src/init.ts
5905
5905
  var logger13 = getLogger();
@@ -10333,189 +10333,615 @@ function recordError8(span2, err) {
10333
10333
  span2.end();
10334
10334
  }
10335
10335
 
10336
- // src/opencode-plugin.ts
10337
- var import_api20 = require("@opentelemetry/api");
10338
- var TRACER_NAME14 = "neatlogs.opencode";
10339
- var _initialized2 = false;
10340
- async function ensureInitialized() {
10341
- if (_initialized2) return;
10342
- _initialized2 = true;
10343
- try {
10344
- await init({
10345
- apiKey: process.env.NEATLOGS_API_KEY,
10346
- // The plugin bootstraps itself inside opencode's process — there is no user
10347
- // init() to pass an endpoint, so honor NEATLOGS_ENDPOINT from the env (e.g.
10348
- // a local backend). init() otherwise defaults to the hosted cloud.
10349
- ...process.env.NEATLOGS_ENDPOINT ? { endpoint: process.env.NEATLOGS_ENDPOINT } : {},
10350
- workflowName: process.env.NEATLOGS_WORKFLOW_NAME || "opencode",
10351
- // opencode is long-running and event-driven; keep auto session off so each
10352
- // span's conversation id is the opencode session id we set explicitly.
10353
- autoSession: false
10354
- });
10355
- } catch {
10336
+ // src/opencode-trace-shipper.ts
10337
+ var import_protobufjs = __toESM(require("protobufjs"));
10338
+ var PACKAGE_NAME = "neatlogs.opencode";
10339
+ var OTLP_PROTO_JSON = {
10340
+ nested: {
10341
+ opentelemetry: {
10342
+ nested: {
10343
+ proto: {
10344
+ nested: {
10345
+ common: {
10346
+ nested: {
10347
+ v1: {
10348
+ nested: {
10349
+ AnyValue: {
10350
+ oneofs: {
10351
+ value: {
10352
+ oneof: [
10353
+ "stringValue",
10354
+ "boolValue",
10355
+ "intValue",
10356
+ "doubleValue",
10357
+ "arrayValue",
10358
+ "kvlistValue",
10359
+ "bytesValue"
10360
+ ]
10361
+ }
10362
+ },
10363
+ fields: {
10364
+ stringValue: { type: "string", id: 1 },
10365
+ boolValue: { type: "bool", id: 2 },
10366
+ intValue: { type: "int64", id: 3 },
10367
+ doubleValue: { type: "double", id: 4 },
10368
+ arrayValue: { type: "ArrayValue", id: 5 },
10369
+ kvlistValue: { type: "KeyValueList", id: 6 },
10370
+ bytesValue: { type: "bytes", id: 7 }
10371
+ }
10372
+ },
10373
+ ArrayValue: { fields: { values: { rule: "repeated", type: "AnyValue", id: 1 } } },
10374
+ KeyValueList: { fields: { values: { rule: "repeated", type: "KeyValue", id: 1 } } },
10375
+ KeyValue: {
10376
+ fields: { key: { type: "string", id: 1 }, value: { type: "AnyValue", id: 2 } }
10377
+ },
10378
+ InstrumentationScope: {
10379
+ fields: { name: { type: "string", id: 1 }, version: { type: "string", id: 2 } }
10380
+ }
10381
+ }
10382
+ }
10383
+ }
10384
+ },
10385
+ resource: {
10386
+ nested: {
10387
+ v1: {
10388
+ nested: {
10389
+ Resource: {
10390
+ fields: {
10391
+ attributes: {
10392
+ rule: "repeated",
10393
+ type: "opentelemetry.proto.common.v1.KeyValue",
10394
+ id: 1
10395
+ }
10396
+ }
10397
+ }
10398
+ }
10399
+ }
10400
+ }
10401
+ },
10402
+ trace: {
10403
+ nested: {
10404
+ v1: {
10405
+ nested: {
10406
+ ResourceSpans: {
10407
+ fields: {
10408
+ resource: { type: "opentelemetry.proto.resource.v1.Resource", id: 1 },
10409
+ scopeSpans: { rule: "repeated", type: "ScopeSpans", id: 2 }
10410
+ }
10411
+ },
10412
+ ScopeSpans: {
10413
+ fields: {
10414
+ scope: { type: "opentelemetry.proto.common.v1.InstrumentationScope", id: 1 },
10415
+ spans: { rule: "repeated", type: "Span", id: 2 }
10416
+ }
10417
+ },
10418
+ Span: {
10419
+ fields: {
10420
+ traceId: { type: "bytes", id: 1 },
10421
+ spanId: { type: "bytes", id: 2 },
10422
+ traceState: { type: "string", id: 3 },
10423
+ parentSpanId: { type: "bytes", id: 4 },
10424
+ name: { type: "string", id: 5 },
10425
+ kind: { type: "SpanKind", id: 6 },
10426
+ startTimeUnixNano: { type: "fixed64", id: 7 },
10427
+ endTimeUnixNano: { type: "fixed64", id: 8 },
10428
+ attributes: {
10429
+ rule: "repeated",
10430
+ type: "opentelemetry.proto.common.v1.KeyValue",
10431
+ id: 9
10432
+ },
10433
+ droppedAttributesCount: { type: "uint32", id: 10 },
10434
+ status: { type: "Status", id: 15 }
10435
+ }
10436
+ },
10437
+ Status: {
10438
+ fields: { message: { type: "string", id: 2 }, code: { type: "StatusCode", id: 3 } }
10439
+ },
10440
+ StatusCode: {
10441
+ values: { STATUS_CODE_UNSET: 0, STATUS_CODE_OK: 1, STATUS_CODE_ERROR: 2 }
10442
+ },
10443
+ SpanKind: {
10444
+ values: {
10445
+ SPAN_KIND_UNSPECIFIED: 0,
10446
+ SPAN_KIND_INTERNAL: 1,
10447
+ SPAN_KIND_SERVER: 2,
10448
+ SPAN_KIND_CLIENT: 3,
10449
+ SPAN_KIND_PRODUCER: 4,
10450
+ SPAN_KIND_CONSUMER: 5
10451
+ }
10452
+ }
10453
+ }
10454
+ }
10455
+ }
10456
+ },
10457
+ collector: {
10458
+ nested: {
10459
+ trace: {
10460
+ nested: {
10461
+ v1: {
10462
+ nested: {
10463
+ ExportTraceServiceRequest: {
10464
+ fields: {
10465
+ resourceSpans: {
10466
+ rule: "repeated",
10467
+ type: "opentelemetry.proto.trace.v1.ResourceSpans",
10468
+ id: 1
10469
+ }
10470
+ }
10471
+ }
10472
+ }
10473
+ }
10474
+ }
10475
+ }
10476
+ }
10477
+ }
10478
+ }
10479
+ }
10480
+ }
10481
+ }
10482
+ }
10483
+ };
10484
+ var protoRoot = import_protobufjs.default.Root.fromJSON(OTLP_PROTO_JSON);
10485
+ var ExportTraceServiceRequest = protoRoot.lookupType(
10486
+ "opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest"
10487
+ );
10488
+ var SpanStatusCode16 = { UNSET: 0, OK: 1, ERROR: 2 };
10489
+ function randomBytes3(length) {
10490
+ const out = new Uint8Array(length);
10491
+ const crypto2 = globalThis.crypto;
10492
+ if (crypto2 && typeof crypto2.getRandomValues === "function") {
10493
+ crypto2.getRandomValues(out);
10494
+ return out;
10495
+ }
10496
+ for (let i = 0; i < out.length; i++) out[i] = Math.floor(Math.random() * 256);
10497
+ return out;
10498
+ }
10499
+ function generateTraceId() {
10500
+ return randomBytes3(16);
10501
+ }
10502
+ function generateSpanId() {
10503
+ return randomBytes3(8);
10504
+ }
10505
+ function nowNanoString() {
10506
+ return (BigInt(Date.now()) * 1000000n).toString();
10507
+ }
10508
+ function msToNanoString(ms) {
10509
+ return (BigInt(Math.floor(ms)) * 1000000n).toString();
10510
+ }
10511
+ function attrStr(key, value) {
10512
+ if (value === void 0 || value === null) return void 0;
10513
+ return { key, value: { stringValue: String(value) } };
10514
+ }
10515
+ function attrInt(key, value) {
10516
+ if (value === void 0 || value === null || !Number.isFinite(value)) return void 0;
10517
+ return { key, value: { intValue: String(Math.trunc(value)) } };
10518
+ }
10519
+ function attrDouble(key, value) {
10520
+ if (value === void 0 || value === null || !Number.isFinite(value)) return void 0;
10521
+ return { key, value: { doubleValue: value } };
10522
+ }
10523
+ var TraceShipper = class {
10524
+ apiKey;
10525
+ endpoint;
10526
+ debug;
10527
+ maxRetries;
10528
+ workflowName;
10529
+ queue = [];
10530
+ prefix = "[neatlogs/opencode]";
10531
+ constructor(opts) {
10532
+ this.apiKey = opts.apiKey;
10533
+ this.endpoint = opts.endpoint.endsWith("/") ? opts.endpoint.slice(0, -1) : opts.endpoint;
10534
+ this.debug = !!opts.debug;
10535
+ this.maxRetries = opts.maxRetries ?? 3;
10536
+ this.workflowName = opts.workflowName || "";
10537
+ }
10538
+ enqueue(span2) {
10539
+ this.queue.push(span2);
10540
+ }
10541
+ get pending() {
10542
+ return this.queue.length;
10543
+ }
10544
+ /**
10545
+ * Ship all queued spans in a single awaited POST. Resolves only once the HTTP
10546
+ * response is received (or all retries are exhausted) — so a short-lived host
10547
+ * can safely exit immediately after awaiting this.
10548
+ */
10549
+ async flush() {
10550
+ if (this.queue.length === 0) return;
10551
+ if (!this.apiKey) {
10552
+ this.queue = [];
10553
+ return;
10554
+ }
10555
+ const spans = this.queue.splice(0);
10556
+ const payload = this.buildProtobuf(spans);
10557
+ const url = `${this.endpoint}/v1/traces`;
10558
+ if (this.debug) console.log(`${this.prefix} Shipping ${spans.length} spans to ${url}`);
10559
+ for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
10560
+ try {
10561
+ const resp = await fetch(url, {
10562
+ method: "POST",
10563
+ headers: {
10564
+ "Content-Type": "application/x-protobuf",
10565
+ "x-api-key": this.apiKey
10566
+ },
10567
+ body: payload
10568
+ });
10569
+ if (resp.ok) {
10570
+ if (this.debug) console.log(`${this.prefix} Shipped ${spans.length} spans`);
10571
+ return;
10572
+ }
10573
+ if (resp.status === 401) {
10574
+ console.warn(`${this.prefix} Invalid API key (401) \u2014 dropping ${spans.length} spans`);
10575
+ return;
10576
+ }
10577
+ if (resp.status < 500 && resp.status !== 429) {
10578
+ if (this.debug) console.warn(`${this.prefix} HTTP ${resp.status} \u2014 dropping spans`);
10579
+ return;
10580
+ }
10581
+ } catch (err) {
10582
+ if (this.debug) {
10583
+ console.warn(
10584
+ `${this.prefix} Attempt ${attempt}/${this.maxRetries}: ${err.message}`
10585
+ );
10586
+ }
10587
+ }
10588
+ if (attempt < this.maxRetries) {
10589
+ await new Promise((r) => setTimeout(r, 500 * 2 ** (attempt - 1)));
10590
+ }
10591
+ }
10592
+ console.warn(`${this.prefix} Failed to ship ${spans.length} spans after ${this.maxRetries} attempts`);
10593
+ }
10594
+ buildProtobuf(spans) {
10595
+ const protoSpans = spans.map((span2) => ({
10596
+ traceId: span2.traceId,
10597
+ spanId: span2.spanId,
10598
+ parentSpanId: span2.parentSpanId || void 0,
10599
+ name: span2.name,
10600
+ kind: span2.kind,
10601
+ startTimeUnixNano: nanoToLong(span2.startTimeUnixNano),
10602
+ endTimeUnixNano: nanoToLong(span2.endTimeUnixNano),
10603
+ attributes: span2.attributes.map((a) => ({
10604
+ key: a.key,
10605
+ value: a.value.intValue !== void 0 ? { intValue: nanoToLong(a.value.intValue) } : a.value
10606
+ })),
10607
+ status: span2.status ? { code: span2.status.code, message: span2.status.message } : void 0
10608
+ }));
10609
+ const resourceAttributes = [
10610
+ { key: "service.name", value: { stringValue: "neatlogs.opencode" } },
10611
+ { key: "service.version", value: { stringValue: __version__ } }
10612
+ ];
10613
+ if (this.workflowName) {
10614
+ resourceAttributes.push({
10615
+ key: "neatlogs.workflow_name",
10616
+ value: { stringValue: this.workflowName }
10617
+ });
10618
+ }
10619
+ const message = {
10620
+ resourceSpans: [
10621
+ {
10622
+ resource: { attributes: resourceAttributes },
10623
+ scopeSpans: [
10624
+ {
10625
+ scope: { name: PACKAGE_NAME, version: __version__ },
10626
+ spans: protoSpans
10627
+ }
10628
+ ]
10629
+ }
10630
+ ]
10631
+ };
10632
+ const errMsg = ExportTraceServiceRequest.verify(message);
10633
+ if (errMsg && this.debug) console.warn(`${this.prefix} Proto verify: ${errMsg}`);
10634
+ return ExportTraceServiceRequest.encode(ExportTraceServiceRequest.fromObject(message)).finish();
10356
10635
  }
10636
+ };
10637
+ function nanoToLong(nanoStr) {
10638
+ const big = BigInt(nanoStr);
10639
+ const low = Number(big & 0xffffffffn);
10640
+ const high = Number(big >> 32n & 0xffffffffn);
10641
+ return { low, high, unsigned: true };
10357
10642
  }
10643
+
10644
+ // src/opencode-plugin.ts
10645
+ var DEFAULT_ENDPOINT = "https://staging-cloud.neatlogs.com";
10358
10646
  var NeatlogsOpencodePlugin = async (_ctx) => {
10359
- await ensureInitialized();
10360
- const sessions = /* @__PURE__ */ new Map();
10361
- const tracer = import_api20.trace.getTracer(TRACER_NAME14);
10647
+ const apiKey = (process.env.NEATLOGS_API_KEY ?? "").trim();
10648
+ const endpoint = (process.env.NEATLOGS_ENDPOINT ?? DEFAULT_ENDPOINT).trim();
10649
+ const workflowName = process.env.NEATLOGS_WORKFLOW_NAME || "opencode";
10362
10650
  const captureSystemPrompt = String(process.env.NEATLOGS_CAPTURE_SYSTEM_PROMPT || "").toLowerCase() === "true";
10651
+ const debug = String(process.env.NEATLOGS_DEBUG || "").toLowerCase() === "true";
10652
+ const shipper = new TraceShipper({ apiKey, endpoint, workflowName, debug });
10653
+ const sessions = /* @__PURE__ */ new Map();
10363
10654
  function stateFor(sessionID) {
10364
10655
  let s = sessions.get(sessionID);
10365
10656
  if (!s) {
10366
- s = { toolSpans: /* @__PURE__ */ new Map(), messageText: /* @__PURE__ */ new Map(), emitted: /* @__PURE__ */ new Set(), lastUserText: "" };
10657
+ s = {
10658
+ traceId: generateTraceId(),
10659
+ toolStarts: /* @__PURE__ */ new Map(),
10660
+ outputParts: /* @__PURE__ */ new Map(),
10661
+ toolCalls: /* @__PURE__ */ new Map(),
10662
+ processed: /* @__PURE__ */ new Set(),
10663
+ currentInput: "",
10664
+ lastAssistantText: ""
10665
+ };
10367
10666
  sessions.set(sessionID, s);
10368
10667
  }
10369
10668
  return s;
10370
10669
  }
10670
+ function startRoot(st, sessionID) {
10671
+ if (st.rootSpan) return;
10672
+ st.rootSpan = {
10673
+ spanId: generateSpanId(),
10674
+ name: "opencode.session",
10675
+ startNano: nowNanoString(),
10676
+ attributes: [
10677
+ { key: "neatlogs.span.kind", value: { stringValue: "AGENT" } },
10678
+ { key: "neatlogs.agent.framework", value: { stringValue: "opencode" } },
10679
+ { key: "neatlogs.conversation.id", value: { stringValue: sessionID } }
10680
+ ]
10681
+ };
10682
+ }
10683
+ async function closeAndFlush(st, sessionID) {
10684
+ if (st.rootSpan) {
10685
+ const attrs = st.rootSpan.attributes.slice();
10686
+ setIO(attrs, "AGENT", st.currentInput || void 0, st.lastAssistantText || void 0);
10687
+ shipper.enqueue({
10688
+ traceId: st.traceId,
10689
+ spanId: st.rootSpan.spanId,
10690
+ name: st.rootSpan.name,
10691
+ kind: 1,
10692
+ startTimeUnixNano: st.rootSpan.startNano,
10693
+ endTimeUnixNano: nowNanoString(),
10694
+ attributes: attrs,
10695
+ status: { code: SpanStatusCode16.OK }
10696
+ });
10697
+ const m = nowNanoString();
10698
+ shipper.enqueue({
10699
+ traceId: st.traceId,
10700
+ spanId: generateSpanId(),
10701
+ parentSpanId: st.rootSpan.spanId,
10702
+ name: "neatlogs.trace.complete",
10703
+ kind: 1,
10704
+ startTimeUnixNano: m,
10705
+ endTimeUnixNano: m,
10706
+ attributes: [
10707
+ { key: "neatlogs.trace.complete", value: { boolValue: true } },
10708
+ { key: "neatlogs.internal", value: { boolValue: true } },
10709
+ { key: "neatlogs.span.kind", value: { stringValue: "Neatlogs.INTERNAL" } }
10710
+ ]
10711
+ });
10712
+ st.rootSpan = void 0;
10713
+ }
10714
+ await shipper.flush();
10715
+ void sessionID;
10716
+ }
10371
10717
  return {
10372
- /**
10373
- * Global event bus. opencode emits message + session lifecycle events here.
10374
- * We use it to emit an LLM span when an assistant message completes, and to
10375
- * track user-message text + assistant text from part updates.
10376
- */
10718
+ /** Fired when the user submits a prompt — open the turn's AGENT root. */
10719
+ "chat.message": async (_input, output) => {
10720
+ try {
10721
+ const sessionID = String(_input?.sessionID ?? output?.sessionID ?? "");
10722
+ if (!sessionID) return;
10723
+ const st = stateFor(sessionID);
10724
+ const parts = output?.parts ?? [];
10725
+ const text = Array.isArray(parts) ? parts.filter((p) => p?.type === "text" && typeof p.text === "string").map((p) => p.text).join("\n") : "";
10726
+ if (text) st.currentInput = text;
10727
+ startRoot(st, sessionID);
10728
+ } catch {
10729
+ }
10730
+ },
10731
+ /** Capture the system prompt (opt-in). */
10732
+ "experimental.chat.system.transform": async (_input, output) => {
10733
+ try {
10734
+ if (!captureSystemPrompt) return;
10735
+ const sessionID = String(_input?.sessionID ?? "");
10736
+ const parts = output?.system ?? output?.parts ?? output;
10737
+ const joined = Array.isArray(parts) ? parts.map((p) => typeof p === "string" ? p : p?.text ?? "").join("\n") : String(parts ?? "");
10738
+ if (sessionID && joined) stateFor(sessionID).systemPrompt = joined;
10739
+ } catch {
10740
+ }
10741
+ },
10742
+ /** Global event bus — message parts, assistant completions, session idle. */
10377
10743
  event: async ({ event }) => {
10378
10744
  try {
10379
- await handleEvent2(tracer, sessions, stateFor, captureSystemPrompt, event);
10745
+ await handleEvent2(shipper, sessions, stateFor, startRoot, closeAndFlush, event);
10380
10746
  } catch {
10381
10747
  }
10382
10748
  },
10383
- /** Fired before a tool runs: open a TOOL span keyed by callID. */
10749
+ /** Tool start record start time + args (span built atomically in `after`). */
10384
10750
  "tool.execute.before": async (input, output) => {
10385
10751
  try {
10386
- const sessionID = input?.sessionID ?? "";
10387
- const callID = input?.callID ?? input?.tool ?? "";
10752
+ const sessionID = String(input?.sessionID ?? "");
10753
+ if (!sessionID) return;
10388
10754
  const st = stateFor(sessionID);
10389
- const span2 = tracer.startSpan(
10390
- `opencode.tool.${input?.tool ?? "tool"}`,
10391
- {
10392
- attributes: {
10393
- "neatlogs.span.kind": "TOOL",
10394
- "neatlogs.tool.name": String(input?.tool ?? ""),
10395
- ...sessionID ? { "neatlogs.conversation.id": String(sessionID) } : {},
10396
- ...output?.args !== void 0 ? { "input.value": safeStringify15(output.args) } : {}
10397
- }
10398
- },
10399
- import_api20.context.active()
10400
- );
10401
- if (callID) st.toolSpans.set(String(callID), span2);
10755
+ startRoot(st, sessionID);
10756
+ const callID = String(input?.callID ?? input?.tool ?? "");
10757
+ st.toolStarts.set(callID, {
10758
+ spanId: generateSpanId(),
10759
+ startNano: nowNanoString(),
10760
+ tool: String(input?.tool ?? "tool"),
10761
+ args: output?.args
10762
+ });
10402
10763
  } catch {
10403
10764
  }
10404
10765
  },
10405
- /** Fired after a tool runs: close the matching TOOL span. */
10406
- "tool.execute.after": async (input, output) => {
10766
+ /** Tool end enqueue the completed TOOL span (parented to the turn root). */
10767
+ "tool.execute.after": async (input, result) => {
10407
10768
  try {
10408
- const sessionID = input?.sessionID ?? "";
10409
- const callID = String(input?.callID ?? input?.tool ?? "");
10769
+ const sessionID = String(input?.sessionID ?? "");
10770
+ if (!sessionID) return;
10410
10771
  const st = stateFor(sessionID);
10411
- const span2 = st.toolSpans.get(callID);
10412
- if (!span2) return;
10413
- if (output?.title) span2.setAttribute("neatlogs.tool.title", String(output.title));
10414
- const out = output?.output ?? output?.result;
10772
+ const callID = String(input?.callID ?? input?.tool ?? "");
10773
+ const start = st.toolStarts.get(callID);
10774
+ if (!start) return;
10775
+ st.toolStarts.delete(callID);
10776
+ const attrs = [
10777
+ { key: "neatlogs.span.kind", value: { stringValue: "TOOL" } },
10778
+ { key: "neatlogs.tool.name", value: { stringValue: start.tool } },
10779
+ { key: "neatlogs.conversation.id", value: { stringValue: sessionID } }
10780
+ ];
10781
+ if (start.args !== void 0) {
10782
+ setIO(attrs, "TOOL", safeStringify15(start.args), void 0);
10783
+ push(attrs, attrStr("neatlogs.tool.input", safeStringify15(start.args)));
10784
+ }
10785
+ if (result?.title) push(attrs, attrStr("neatlogs.tool.title", String(result.title)));
10786
+ const out = result?.output ?? result?.result;
10415
10787
  if (out !== void 0) {
10416
- span2.setAttribute("output.value", typeof out === "string" ? out : safeStringify15(out));
10417
- }
10418
- if (output?.metadata !== void 0) {
10419
- span2.setAttribute("neatlogs.tool.metadata", safeStringify15(output.metadata));
10420
- }
10421
- span2.setStatus({ code: import_api20.SpanStatusCode.OK });
10422
- span2.end();
10423
- st.toolSpans.delete(callID);
10788
+ const o = typeof out === "string" ? out : safeStringify15(out);
10789
+ setIO(attrs, "TOOL", void 0, o);
10790
+ push(attrs, attrStr("neatlogs.tool.output", o));
10791
+ }
10792
+ if (result?.metadata !== void 0) {
10793
+ push(attrs, attrStr("neatlogs.tool.metadata", safeStringify15(result.metadata)));
10794
+ }
10795
+ shipper.enqueue({
10796
+ traceId: st.traceId,
10797
+ spanId: start.spanId,
10798
+ parentSpanId: st.rootSpan?.spanId,
10799
+ name: `opencode.tool.${start.tool}`,
10800
+ kind: 1,
10801
+ startTimeUnixNano: start.startNano,
10802
+ endTimeUnixNano: nowNanoString(),
10803
+ attributes: attrs,
10804
+ status: { code: SpanStatusCode16.OK }
10805
+ });
10424
10806
  } catch {
10425
10807
  }
10426
10808
  }
10427
10809
  };
10428
10810
  };
10429
- function handleEvent2(tracer, sessions, stateFor, captureSystemPrompt, event) {
10811
+ function handleEvent2(shipper, sessions, stateFor, startRoot, closeAndFlush, event) {
10430
10812
  const type = event?.type;
10431
10813
  const props = event?.properties ?? {};
10432
10814
  if (type === "message.part.updated" || type === "message.part.completed") {
10433
10815
  const part = props.part ?? props;
10434
10816
  const messageID = part?.messageID ?? part?.message_id;
10435
10817
  const sessionID = part?.sessionID ?? props.sessionID;
10436
- if (part?.type === "text" && typeof part?.text === "string" && messageID && sessionID) {
10437
- const st = stateFor(String(sessionID));
10438
- st.messageText.set(String(messageID), part.text);
10818
+ if (!messageID || !sessionID) return void 0;
10819
+ const st = stateFor(String(sessionID));
10820
+ if (part?.type === "text" && typeof part?.text === "string") {
10821
+ st.outputParts.set(String(messageID), part.text);
10822
+ } else if (part?.type === "tool" && part?.tool) {
10823
+ const list = st.toolCalls.get(String(messageID)) ?? [];
10824
+ const callID = String(part.callID ?? part.tool);
10825
+ const input = part?.state?.input ?? {};
10826
+ const existing = list.find((t) => t.callID === callID);
10827
+ if (existing) existing.input = input;
10828
+ else list.push({ name: String(part.tool), input, callID });
10829
+ st.toolCalls.set(String(messageID), list);
10439
10830
  }
10440
- return;
10831
+ return void 0;
10441
10832
  }
10442
10833
  if (type === "message.updated" || type === "message.completed") {
10443
10834
  const info = props.info ?? props.message ?? props;
10444
10835
  const sessionID = info?.sessionID ?? info?.session_id;
10445
- if (!sessionID) return;
10836
+ if (!sessionID) return void 0;
10837
+ if (info?.role !== "assistant") return void 0;
10838
+ const completed = info?.time?.completed ?? info?.completed;
10839
+ const id = String(info?.id ?? "");
10840
+ if (!completed || !id) return void 0;
10446
10841
  const st = stateFor(String(sessionID));
10447
- if (info?.role === "user") {
10448
- const text = messageText2(info) || st.messageText.get(String(info?.id)) || "";
10449
- if (text) st.lastUserText = text;
10450
- return;
10451
- }
10452
- if (info?.role === "assistant") {
10453
- const completed = info?.time?.completed ?? info?.completed;
10454
- const id = String(info?.id ?? "");
10455
- if (!completed || st.emitted.has(id)) return;
10456
- st.emitted.add(id);
10457
- emitLlmSpan2(tracer, st, captureSystemPrompt, info, String(sessionID));
10458
- }
10459
- return;
10842
+ if (st.processed.has(id)) return void 0;
10843
+ st.processed.add(id);
10844
+ startRoot(st, String(sessionID));
10845
+ emitLlmSpan2(shipper, st, info, String(sessionID));
10846
+ return shipper.flush().catch(() => void 0);
10460
10847
  }
10461
10848
  if (type === "session.idle" || type === "session.deleted") {
10462
10849
  const sessionID = props.sessionID ?? props.info?.id;
10463
10850
  if (!sessionID) return void 0;
10464
10851
  const st = sessions.get(String(sessionID));
10465
10852
  if (!st) return void 0;
10466
- for (const ts of st.toolSpans.values()) {
10467
- try {
10468
- ts.end();
10469
- } catch {
10470
- }
10471
- }
10472
- st.toolSpans.clear();
10853
+ st.toolStarts.clear();
10854
+ const p = closeAndFlush(st, String(sessionID));
10473
10855
  if (type === "session.deleted") sessions.delete(String(sessionID));
10474
- return flush().catch(() => false);
10856
+ return p;
10475
10857
  }
10476
10858
  return void 0;
10477
10859
  }
10478
- function emitLlmSpan2(tracer, st, captureSystemPrompt, info, sessionID) {
10860
+ function emitLlmSpan2(shipper, st, info, sessionID) {
10479
10861
  const model = info?.modelID ?? info?.model ?? "";
10480
10862
  const provider = info?.providerID ?? info?.provider ?? "";
10481
- const attrs = {
10482
- "neatlogs.span.kind": "LLM",
10483
- "neatlogs.conversation.id": sessionID
10484
- };
10485
- if (model) attrs["neatlogs.llm.model_name"] = String(model);
10486
- if (provider) attrs["neatlogs.llm.provider"] = String(provider);
10487
- if (captureSystemPrompt && info?.system) {
10488
- const sys = Array.isArray(info.system) ? info.system.join("\n") : String(info.system);
10489
- attrs["neatlogs.llm.input_messages.0.role"] = "system";
10490
- attrs["neatlogs.llm.input_messages.0.content"] = sys;
10491
- }
10492
- let inIdx = captureSystemPrompt && info?.system ? 1 : 0;
10493
- if (st.lastUserText) {
10494
- attrs[`neatlogs.llm.input_messages.${inIdx}.role`] = "user";
10495
- attrs[`neatlogs.llm.input_messages.${inIdx}.content`] = st.lastUserText;
10863
+ const attrs = [
10864
+ { key: "neatlogs.span.kind", value: { stringValue: "LLM" } },
10865
+ { key: "neatlogs.conversation.id", value: { stringValue: sessionID } }
10866
+ ];
10867
+ push(attrs, attrStr("neatlogs.llm.model_name", model ? String(model) : void 0));
10868
+ push(attrs, attrStr("neatlogs.llm.provider", provider ? String(provider) : void 0));
10869
+ let inIdx = 0;
10870
+ if (st.systemPrompt) {
10871
+ push(attrs, attrStr(`neatlogs.llm.input_messages.${inIdx}.role`, "system"));
10872
+ push(attrs, attrStr(`neatlogs.llm.input_messages.${inIdx}.content`, st.systemPrompt));
10496
10873
  inIdx++;
10497
10874
  }
10498
- const outText = messageText2(info) || st.messageText.get(String(info?.id)) || "";
10875
+ if (st.currentInput) {
10876
+ push(attrs, attrStr(`neatlogs.llm.input_messages.${inIdx}.role`, "user"));
10877
+ push(attrs, attrStr(`neatlogs.llm.input_messages.${inIdx}.content`, st.currentInput));
10878
+ setIO(attrs, "LLM", st.currentInput, void 0);
10879
+ }
10880
+ const outText = st.outputParts.get(String(info?.id)) || messageText2(info) || "";
10881
+ const toolCalls = st.toolCalls.get(String(info?.id)) ?? [];
10499
10882
  if (outText) {
10500
- attrs["neatlogs.llm.output_messages.0.role"] = "assistant";
10501
- attrs["neatlogs.llm.output_messages.0.content"] = outText;
10883
+ push(attrs, attrStr("neatlogs.llm.output_messages.0.role", "assistant"));
10884
+ push(attrs, attrStr("neatlogs.llm.output_messages.0.content", outText));
10885
+ setIO(attrs, "LLM", void 0, outText);
10886
+ st.lastAssistantText = outText;
10887
+ }
10888
+ if (toolCalls.length) {
10889
+ toolCalls.forEach((tc, j) => {
10890
+ push(attrs, attrStr(`neatlogs.llm.tool_calls.${j}.name`, tc.name));
10891
+ push(attrs, attrStr(`neatlogs.llm.tool_calls.${j}.arguments`, safeStringify15(tc.input ?? {})));
10892
+ });
10893
+ if (!outText) {
10894
+ const rendered = toolCalls.map((tc) => `\u2192 ${tc.name}(${safeStringify15(tc.input ?? {})})`).join("\n");
10895
+ push(attrs, attrStr("neatlogs.llm.output_messages.0.role", "assistant"));
10896
+ push(attrs, attrStr("neatlogs.llm.output_messages.0.content", rendered));
10897
+ setIO(attrs, "LLM", void 0, rendered);
10898
+ }
10502
10899
  }
10503
10900
  const tokens = info?.tokens;
10504
10901
  if (tokens) {
10505
- if (tokens.input != null) attrs["neatlogs.llm.token_count.prompt"] = tokens.input;
10506
- if (tokens.output != null) attrs["neatlogs.llm.token_count.completion"] = tokens.output;
10902
+ push(attrs, attrInt("neatlogs.llm.token_count.prompt", tokens.input));
10903
+ push(attrs, attrInt("neatlogs.llm.token_count.completion", tokens.output));
10507
10904
  if (tokens.input != null && tokens.output != null) {
10508
- attrs["neatlogs.llm.token_count.total"] = tokens.input + tokens.output;
10509
- }
10510
- if (tokens.reasoning != null) attrs["neatlogs.llm.token_count.reasoning"] = tokens.reasoning;
10511
- if (tokens.cache?.read != null) attrs["neatlogs.llm.token_count.cache_read"] = tokens.cache.read;
10512
- if (tokens.cache?.write != null) attrs["neatlogs.llm.token_count.cache_write"] = tokens.cache.write;
10905
+ push(attrs, attrInt("neatlogs.llm.token_count.total", tokens.input + tokens.output));
10906
+ }
10907
+ push(attrs, attrInt("neatlogs.llm.token_count.reasoning", tokens.reasoning));
10908
+ push(attrs, attrInt("neatlogs.llm.token_count.cache_read", tokens.cache?.read));
10909
+ push(attrs, attrInt("neatlogs.llm.token_count.cache_write", tokens.cache?.write));
10910
+ }
10911
+ push(attrs, attrDouble("neatlogs.llm.cost_usd", info?.cost));
10912
+ const createdMs = info?.time?.created;
10913
+ const startNano = typeof createdMs === "number" ? msToNanoString(Math.floor(createdMs)) : nowNanoString();
10914
+ shipper.enqueue({
10915
+ traceId: st.traceId,
10916
+ spanId: generateSpanId(),
10917
+ parentSpanId: st.rootSpan?.spanId,
10918
+ name: `opencode.llm.${model || "model"}`,
10919
+ kind: 1,
10920
+ startTimeUnixNano: startNano,
10921
+ endTimeUnixNano: nowNanoString(),
10922
+ attributes: attrs,
10923
+ status: { code: SpanStatusCode16.OK }
10924
+ });
10925
+ if (info?.id) {
10926
+ st.outputParts.delete(String(info.id));
10927
+ st.toolCalls.delete(String(info.id));
10928
+ }
10929
+ }
10930
+ function push(arr, kv) {
10931
+ if (kv) arr.push(kv);
10932
+ }
10933
+ function setIO(arr, kind, input, output) {
10934
+ const k = kind.toLowerCase();
10935
+ if (input !== void 0) {
10936
+ push(arr, attrStr("input.value", input));
10937
+ push(arr, attrStr("neatlogs.input.value", input));
10938
+ push(arr, attrStr(`neatlogs.${k}.input`, input));
10939
+ }
10940
+ if (output !== void 0) {
10941
+ push(arr, attrStr("output.value", output));
10942
+ push(arr, attrStr("neatlogs.output.value", output));
10943
+ push(arr, attrStr(`neatlogs.${k}.output`, output));
10513
10944
  }
10514
- if (info?.cost != null) attrs["neatlogs.llm.cost_usd"] = info.cost;
10515
- const span2 = tracer.startSpan(`opencode.llm.${model || "model"}`, { attributes: attrs }, import_api20.context.active());
10516
- span2.setStatus({ code: import_api20.SpanStatusCode.OK });
10517
- span2.end();
10518
- if (info?.id) st.messageText.delete(String(info.id));
10519
10945
  }
10520
10946
  function messageText2(info) {
10521
10947
  if (!info) return "";
@@ -10535,7 +10961,7 @@ function safeStringify15(value) {
10535
10961
  try {
10536
10962
  return JSON.stringify(value) ?? "";
10537
10963
  } catch {
10538
- return "";
10964
+ return String(value);
10539
10965
  }
10540
10966
  }
10541
10967