@ramarivera/coding-agent-langfuse 0.1.17 → 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/backfill.js +198 -16
  2. package/package.json +1 -1
package/dist/backfill.js CHANGED
@@ -6,6 +6,10 @@ import { homedir } from "node:os";
6
6
  import { dirname, join } from "node:path";
7
7
  const allAgents = ["claude", "codex", "grok", "opencode", "pi"];
8
8
  const importIdentityVersion = "v8-cached-input-token-split";
9
+ const importIdentityVersions = {
10
+ codex: "v9-codex-conversation-events",
11
+ opencode: "v10-opencode-message-parts",
12
+ };
9
13
  const defaultEndpoint = "https://langfuse.ai.roxasroot.net/otel/v1/traces";
10
14
  const deadRemoteEndpoint = "http://langfuse.ai.roxasroot.net:14318/v1/traces";
11
15
  const defaultStatePath = join(homedir(), ".local/state/coding-agent-langfuse/backfill-v6.json");
@@ -229,7 +233,10 @@ function extractText(value, maxLength = 4000) {
229
233
  const text = value
230
234
  .map((item) => {
231
235
  const record = asRecord(item);
232
- return asString(record.text) ?? asString(record.content) ?? "";
236
+ return asString(record.text) ??
237
+ extractText(record.content, maxLength) ??
238
+ extractText(record.summary, maxLength) ??
239
+ "";
233
240
  })
234
241
  .filter(Boolean)
235
242
  .join("\n");
@@ -363,7 +370,8 @@ function costDetails(usage, model) {
363
370
  return undefined;
364
371
  }
365
372
  function isGenerationEvent(event) {
366
- return event.usage !== undefined && event.role !== "user";
373
+ return event.usage !== undefined && event.role !== "user" &&
374
+ event.role !== "developer" && event.role !== "system";
367
375
  }
368
376
  function codexEvents(homeDir) {
369
377
  const files = listFiles(join(homeDir, ".codex/sessions"), (path) => path.endsWith(".jsonl"));
@@ -378,6 +386,7 @@ function codexEvents(homeDir) {
378
386
  asString(getPath(payload, ["model"]));
379
387
  let currentModel = model;
380
388
  let currentCwd = cwd;
389
+ let currentTurnRecordId = "session";
381
390
  const seenTokenCounts = new Set();
382
391
  const events = [
383
392
  {
@@ -406,11 +415,12 @@ function codexEvents(homeDir) {
406
415
  if (type === "turn_context") {
407
416
  currentModel = asString(rowPayload.model) ?? currentModel;
408
417
  currentCwd = asString(rowPayload.cwd) ?? currentCwd;
418
+ currentTurnRecordId = `turn-${asString(rowPayload.turn_id) ?? index}`;
409
419
  events.push({
410
420
  agent: "codex",
411
421
  sourcePath: path,
412
422
  sessionId,
413
- recordId: `turn-${asString(rowPayload.turn_id) ?? index}`,
423
+ recordId: currentTurnRecordId,
414
424
  name: "codex turn",
415
425
  cwd: currentCwd,
416
426
  model: currentModel,
@@ -420,34 +430,99 @@ function codexEvents(homeDir) {
420
430
  });
421
431
  }
422
432
  if (type === "response_item" && itemType === "message") {
433
+ const role = asString(rowPayload.role);
434
+ const text = extractText(rowPayload.content);
435
+ const input = role === "user" || role === "developer" ||
436
+ role === "system"
437
+ ? text
438
+ : undefined;
439
+ const output = role === "assistant" ? text : undefined;
423
440
  events.push({
424
441
  agent: "codex",
425
442
  sourcePath: path,
426
443
  sessionId,
427
444
  recordId: `message-${asString(rowPayload.id) ?? index}`,
428
- name: `codex ${asString(rowPayload.role) ?? "message"}`,
429
- role: asString(rowPayload.role),
445
+ name: `codex ${role ?? "message"}`,
446
+ role,
430
447
  model: currentModel,
431
448
  cwd: currentCwd,
432
449
  startMs: timestamp,
433
- parentRecordId: "session",
434
- output: extractText(rowPayload.content),
450
+ parentRecordId: currentTurnRecordId,
451
+ input,
452
+ output,
435
453
  usage: normalizeUsage(rowPayload.usage),
454
+ metadata: pick(rowPayload, ["phase"]),
455
+ });
456
+ }
457
+ if (type === "response_item" && itemType === "reasoning") {
458
+ events.push({
459
+ agent: "codex",
460
+ sourcePath: path,
461
+ sessionId,
462
+ recordId: `reasoning-${index}`,
463
+ name: "codex reasoning",
464
+ model: currentModel,
465
+ cwd: currentCwd,
466
+ startMs: timestamp,
467
+ parentRecordId: currentTurnRecordId,
468
+ output: extractText(rowPayload.content) ??
469
+ extractText(rowPayload.summary),
470
+ metadata: {
471
+ has_encrypted_content: rowPayload.encrypted_content !== undefined,
472
+ },
436
473
  });
437
474
  }
438
475
  if (type === "response_item" && itemType === "function_call") {
476
+ const callId = asString(rowPayload.call_id) ?? `${index}`;
439
477
  events.push({
440
478
  agent: "codex",
441
479
  sourcePath: path,
442
480
  sessionId,
443
- recordId: `tool-${asString(rowPayload.call_id) ?? index}`,
481
+ recordId: `tool-${callId}`,
444
482
  name: `codex tool ${asString(rowPayload.name) ?? "call"}`,
445
483
  model: currentModel,
446
484
  cwd: currentCwd,
447
485
  startMs: timestamp,
448
- parentRecordId: "session",
486
+ parentRecordId: currentTurnRecordId,
449
487
  input: rowPayload.arguments,
450
- metadata: pick(rowPayload, ["name", "call_id"]),
488
+ metadata: pick(rowPayload, ["name", "namespace", "call_id"]),
489
+ });
490
+ }
491
+ if (type === "response_item" && itemType === "function_call_output") {
492
+ const callId = asString(rowPayload.call_id) ?? `${index}`;
493
+ events.push({
494
+ agent: "codex",
495
+ sourcePath: path,
496
+ sessionId,
497
+ recordId: `tool-result-${callId}`,
498
+ name: "codex tool result",
499
+ model: currentModel,
500
+ cwd: currentCwd,
501
+ startMs: timestamp,
502
+ parentRecordId: `tool-${callId}`,
503
+ output: rowPayload.output,
504
+ metadata: pick(rowPayload, ["call_id", "status", "execution"]),
505
+ });
506
+ }
507
+ if (type === "response_item" &&
508
+ (itemType === "tool_search_call" || itemType === "tool_search_output")) {
509
+ const callId = asString(rowPayload.call_id) ?? `${index}`;
510
+ const isOutput = itemType === "tool_search_output";
511
+ events.push({
512
+ agent: "codex",
513
+ sourcePath: path,
514
+ sessionId,
515
+ recordId: `${isOutput ? "tool-search-result" : "tool-search"}-${callId}`,
516
+ name: isOutput ? "codex tool_search result" : "codex tool_search",
517
+ model: currentModel,
518
+ cwd: currentCwd,
519
+ startMs: timestamp,
520
+ parentRecordId: isOutput
521
+ ? `tool-search-${callId}`
522
+ : currentTurnRecordId,
523
+ input: isOutput ? undefined : rowPayload.arguments,
524
+ output: isOutput ? rowPayload.tools : undefined,
525
+ metadata: pick(rowPayload, ["call_id", "status", "execution"]),
451
526
  });
452
527
  }
453
528
  if (type === "event_msg" && rowPayload.type === "token_count") {
@@ -564,6 +639,7 @@ function opencodeEvents(homeDir, rowLimit) {
564
639
  return [];
565
640
  let sessions = [];
566
641
  let messages = [];
642
+ let parts = [];
567
643
  try {
568
644
  sessions = sqliteJsonByRowid(db, "session", "id, directory, time_created, time_updated, title, version, slug, project_id", undefined, rowLimit, 5_000);
569
645
  messages = sqliteJsonByRowid(db, "message", [
@@ -585,12 +661,31 @@ function opencodeEvents(homeDir, rowLimit) {
585
661
  "json_extract(data, '$.mode') as mode",
586
662
  "json_extract(data, '$.error') as error",
587
663
  ].join(", "), undefined, rowLimit, 5_000);
664
+ parts = sqliteJsonByRowid(db, "part", [
665
+ "id",
666
+ "message_id",
667
+ "session_id",
668
+ "time_created",
669
+ "time_updated",
670
+ "json_extract(data, '$.type') as type",
671
+ "json_extract(data, '$') as data",
672
+ ].join(", "), undefined, rowLimit, 5_000);
588
673
  }
589
674
  catch (error) {
590
675
  console.error(`Skipping OpenCode history from ${db}: ${error instanceof Error ? error.message : String(error)}`);
591
676
  return [];
592
677
  }
593
678
  const sessionsById = new Map(sessions.map((row) => [asString(row.id), row]));
679
+ const partsByMessageId = new Map();
680
+ for (const part of parts) {
681
+ const messageId = asString(part.message_id);
682
+ if (!messageId)
683
+ continue;
684
+ partsByMessageId.set(messageId, [
685
+ ...(partsByMessageId.get(messageId) ?? []),
686
+ part,
687
+ ]);
688
+ }
594
689
  const events = [];
595
690
  for (const session of sessions) {
596
691
  const sessionId = asString(session.id);
@@ -611,15 +706,21 @@ function opencodeEvents(homeDir, rowLimit) {
611
706
  for (const message of messages) {
612
707
  const sessionId = asString(message.session_id);
613
708
  const session = sessionsById.get(sessionId);
709
+ const messageId = asString(message.id) ?? stableId(JSON.stringify(message));
710
+ const role = asString(message.role);
711
+ const messageParts = [...(partsByMessageId.get(messageId) ?? [])].sort((a, b) => getTimestampMs(a.time_created) - getTimestampMs(b.time_created));
712
+ const textOutput = opencodeTextFromParts(messageParts);
713
+ const textInput = role === "user" ? textOutput : undefined;
714
+ const output = role === "assistant" ? textOutput : undefined;
614
715
  const tokens = normalizeUsage(parseMaybeJson(message.tokens));
615
716
  const usage = tokens ?? normalizeUsage(parseMaybeJson(message.usage));
616
717
  events.push({
617
718
  agent: "opencode",
618
719
  sourcePath: db,
619
720
  sessionId: sessionId ?? stableId(db),
620
- recordId: asString(message.id) ?? stableId(JSON.stringify(message)),
621
- name: `opencode ${asString(message.role) ?? "message"}`,
622
- role: asString(message.role),
721
+ recordId: messageId,
722
+ name: `opencode ${role ?? "message"}`,
723
+ role,
623
724
  model: asString(message.model_id) ?? asString(message.nested_model_id),
624
725
  provider: asString(message.provider_id) ??
625
726
  asString(message.nested_provider_id),
@@ -628,12 +729,86 @@ function opencodeEvents(homeDir, rowLimit) {
628
729
  startMs: getTimestampMs(message.time_created),
629
730
  endMs: getTimestampMs(message.time_updated),
630
731
  parentRecordId: asString(message.parent_id) ?? "session",
732
+ input: textInput,
733
+ output,
631
734
  usage,
632
735
  metadata: pick(message, ["agent", "mode", "error"]),
633
736
  });
737
+ for (const part of messageParts) {
738
+ const data = asRecord(parseMaybeJson(part.data));
739
+ const type = asString(part.type) ?? asString(data.type);
740
+ const partId = asString(part.id) ?? stableId(JSON.stringify(part));
741
+ const partStartMs = getTimestampMs(part.time_created, getTimestampMs(message.time_created));
742
+ if (type === "reasoning") {
743
+ events.push({
744
+ agent: "opencode",
745
+ sourcePath: db,
746
+ sessionId: sessionId ?? stableId(db),
747
+ recordId: `reasoning-${partId}`,
748
+ name: "opencode reasoning",
749
+ model: asString(message.model_id) ?? asString(message.nested_model_id),
750
+ provider: asString(message.provider_id) ??
751
+ asString(message.nested_provider_id),
752
+ cwd: asString(message.cwd) ??
753
+ asString(asRecord(session).directory),
754
+ startMs: partStartMs,
755
+ endMs: getTimestampMs(part.time_updated, partStartMs + 1),
756
+ parentRecordId: messageId,
757
+ output: extractText(data.text),
758
+ metadata: {
759
+ has_encrypted_content: getPath(data, ["metadata", "openai", "reasoningEncryptedContent"]) !== undefined,
760
+ },
761
+ });
762
+ }
763
+ if (type === "tool") {
764
+ const state = asRecord(data.state);
765
+ const callId = asString(data.callID) ?? asString(data.call_id) ?? partId;
766
+ events.push({
767
+ agent: "opencode",
768
+ sourcePath: db,
769
+ sessionId: sessionId ?? stableId(db),
770
+ recordId: `tool-${callId}`,
771
+ name: `opencode tool ${asString(data.tool) ?? "call"}`,
772
+ model: asString(message.model_id) ?? asString(message.nested_model_id),
773
+ provider: asString(message.provider_id) ??
774
+ asString(message.nested_provider_id),
775
+ cwd: asString(message.cwd) ??
776
+ asString(asRecord(session).directory),
777
+ startMs: partStartMs,
778
+ endMs: getTimestampMs(part.time_updated, partStartMs + 1),
779
+ parentRecordId: messageId,
780
+ input: state.input,
781
+ output: state.output,
782
+ metadata: {
783
+ status: asString(state.status),
784
+ title: asString(state.title),
785
+ },
786
+ });
787
+ }
788
+ }
634
789
  }
635
790
  return events;
636
791
  }
792
+ function opencodeTextFromParts(parts) {
793
+ const text = parts
794
+ .map((part) => {
795
+ const data = asRecord(parseMaybeJson(part.data));
796
+ const type = asString(part.type) ?? asString(data.type);
797
+ if (type === "text")
798
+ return extractText(data.text, 8000);
799
+ if (type === "file") {
800
+ const filename = asString(data.filename);
801
+ const url = asString(data.url);
802
+ if (filename && url)
803
+ return `${filename}\n${url}`;
804
+ return filename ?? url;
805
+ }
806
+ return undefined;
807
+ })
808
+ .filter((value) => Boolean(value))
809
+ .join("\n");
810
+ return text ? text.slice(0, 8000) : undefined;
811
+ }
637
812
  function sqliteJson(db, sql) {
638
813
  const output = execFileSync("sqlite3", ["-readonly", "-json", db, sql], {
639
814
  encoding: "utf8",
@@ -780,11 +955,14 @@ function parseMaybeJson(value) {
780
955
  function stableId(input) {
781
956
  return createHash("sha256").update(input).digest("hex").slice(0, 32);
782
957
  }
958
+ function importIdentity(event) {
959
+ return importIdentityVersions[event.agent] ?? importIdentityVersion;
960
+ }
783
961
  function fingerprint(event) {
784
- return `${importIdentityVersion}:${event.agent}:${event.sessionId}:${event.recordId}`;
962
+ return `${importIdentity(event)}:${event.agent}:${event.sessionId}:${event.recordId}`;
785
963
  }
786
964
  function traceFingerprint(event) {
787
- return `${importIdentityVersion}:${event.agent}:${event.sessionId}`;
965
+ return `${importIdentity(event)}:${event.agent}:${event.sessionId}`;
788
966
  }
789
967
  function traceId(event) {
790
968
  return stableId(traceFingerprint(event));
@@ -862,6 +1040,7 @@ function toOtlp(events) {
862
1040
  attributes: rootAttributes,
863
1041
  status: { code: 1 },
864
1042
  };
1043
+ const spanIdsByRecordId = new Map(sortedEvents.map((event) => [event.recordId, spanId(event)]));
865
1044
  const childSpans = sortedEvents.map((event) => {
866
1045
  const startMs = event.startMs;
867
1046
  const durationMs = Math.max(1, (event.endMs ?? event.startMs + 1) - event.startMs);
@@ -923,7 +1102,10 @@ function toOtlp(events) {
923
1102
  return {
924
1103
  traceId: traceId(event),
925
1104
  spanId: spanId(event),
926
- parentSpanId: rootSpanId(event),
1105
+ parentSpanId: event.parentRecordId &&
1106
+ event.parentRecordId !== "session"
1107
+ ? spanIdsByRecordId.get(event.parentRecordId) ?? rootSpanId(event)
1108
+ : rootSpanId(event),
927
1109
  name: event.name,
928
1110
  kind: 1,
929
1111
  startTimeUnixNano: ns(startMs),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ramarivera/coding-agent-langfuse",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "Universal coding-agent Langfuse backfiller and live OTLP helpers",
5
5
  "type": "module",
6
6
  "license": "MIT",