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