@ramarivera/coding-agent-langfuse 0.1.18 → 0.1.20

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 +200 -26
  2. package/package.json +1 -1
package/dist/backfill.js CHANGED
@@ -5,7 +5,14 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSy
5
5
  import { homedir } from "node:os";
6
6
  import { dirname, join } from "node:path";
7
7
  const allAgents = ["claude", "codex", "grok", "opencode", "pi"];
8
- const importIdentityVersion = "v9-codex-conversation-events";
8
+ const importIdentityVersion = "v8-cached-input-token-split";
9
+ const importIdentityVersions = {
10
+ claude: "v11-tool-results",
11
+ codex: "v9-codex-conversation-events",
12
+ grok: "v11-chat-history-only",
13
+ opencode: "v10-opencode-message-parts",
14
+ pi: "v11-tool-results",
15
+ };
9
16
  const defaultEndpoint = "https://langfuse.ai.roxasroot.net/otel/v1/traces";
10
17
  const deadRemoteEndpoint = "http://langfuse.ai.roxasroot.net:14318/v1/traces";
11
18
  const defaultStatePath = join(homedir(), ".local/state/coding-agent-langfuse/backfill-v6.json");
@@ -586,33 +593,56 @@ function piEvents(homeDir) {
586
593
  continue;
587
594
  const message = asRecord(row.message);
588
595
  const usage = normalizeUsage(message.usage);
596
+ const role = asString(message.role);
597
+ const messageId = asString(row.id) ?? `message-${index}`;
598
+ const toolCallId = asString(message.toolCallId);
589
599
  events.push({
590
600
  agent: "pi",
591
601
  sourcePath: path,
592
602
  sessionId,
593
- recordId: asString(row.id) ?? `message-${index}`,
594
- name: `pi ${asString(message.role) ?? "message"}`,
595
- role: asString(message.role),
603
+ recordId: messageId,
604
+ name: role === "toolResult"
605
+ ? `pi toolResult ${asString(message.toolName) ?? "call"}`
606
+ : `pi ${role ?? "message"}`,
607
+ role,
596
608
  model: asString(message.model) ?? asString(row.model),
597
609
  provider: asString(message.provider),
598
610
  cwd,
599
611
  startMs: getTimestampMs(row.timestamp ?? message.timestamp, startMs + index),
600
612
  endMs: getTimestampMs(message.completed, startMs + index + 1),
601
- parentRecordId: asString(row.parentId) ?? "session",
602
- input: message.role === "user"
613
+ parentRecordId: toolCallId
614
+ ? `tool-${toolCallId}`
615
+ : asString(row.parentId) ?? "session",
616
+ input: role === "user"
603
617
  ? extractText(message.content)
604
618
  : undefined,
605
- output: message.role === "assistant"
619
+ output: role === "assistant" || role === "toolResult"
606
620
  ? extractText(message.content)
607
621
  : undefined,
608
622
  usage,
623
+ metadata: pick(message, ["toolCallId", "toolName", "isError", "stopReason"]),
609
624
  });
625
+ for (const reasoning of reasoningFromContent(message.content)) {
626
+ events.push({
627
+ agent: "pi",
628
+ sourcePath: path,
629
+ sessionId,
630
+ recordId: `reasoning-${messageId}-${reasoning.index}`,
631
+ name: "pi reasoning",
632
+ cwd,
633
+ model: asString(message.model),
634
+ startMs: getTimestampMs(row.timestamp, startMs + index),
635
+ parentRecordId: messageId,
636
+ output: reasoning.text,
637
+ metadata: { has_signature: reasoning.hasSignature },
638
+ });
639
+ }
610
640
  for (const tool of toolCallsFromContent(message.content)) {
611
641
  events.push({
612
642
  agent: "pi",
613
643
  sourcePath: path,
614
644
  sessionId,
615
- recordId: tool.id,
645
+ recordId: `tool-${tool.id}`,
616
646
  name: `pi tool ${tool.name}`,
617
647
  cwd,
618
648
  model: asString(message.model),
@@ -626,7 +656,7 @@ function piEvents(homeDir) {
626
656
  });
627
657
  }
628
658
  function grokEvents(homeDir) {
629
- const files = listFiles(join(homeDir, ".grok/sessions"), (path) => path.endsWith(".jsonl"));
659
+ const files = listFiles(join(homeDir, ".grok/sessions"), (path) => path.endsWith("chat_history.jsonl"));
630
660
  return genericJsonlEvents("grok", files, "grok session");
631
661
  }
632
662
  function opencodeEvents(homeDir, rowLimit) {
@@ -635,6 +665,7 @@ function opencodeEvents(homeDir, rowLimit) {
635
665
  return [];
636
666
  let sessions = [];
637
667
  let messages = [];
668
+ let parts = [];
638
669
  try {
639
670
  sessions = sqliteJsonByRowid(db, "session", "id, directory, time_created, time_updated, title, version, slug, project_id", undefined, rowLimit, 5_000);
640
671
  messages = sqliteJsonByRowid(db, "message", [
@@ -656,12 +687,31 @@ function opencodeEvents(homeDir, rowLimit) {
656
687
  "json_extract(data, '$.mode') as mode",
657
688
  "json_extract(data, '$.error') as error",
658
689
  ].join(", "), undefined, rowLimit, 5_000);
690
+ parts = sqliteJsonByRowid(db, "part", [
691
+ "id",
692
+ "message_id",
693
+ "session_id",
694
+ "time_created",
695
+ "time_updated",
696
+ "json_extract(data, '$.type') as type",
697
+ "json_extract(data, '$') as data",
698
+ ].join(", "), undefined, rowLimit, 5_000);
659
699
  }
660
700
  catch (error) {
661
701
  console.error(`Skipping OpenCode history from ${db}: ${error instanceof Error ? error.message : String(error)}`);
662
702
  return [];
663
703
  }
664
704
  const sessionsById = new Map(sessions.map((row) => [asString(row.id), row]));
705
+ const partsByMessageId = new Map();
706
+ for (const part of parts) {
707
+ const messageId = asString(part.message_id);
708
+ if (!messageId)
709
+ continue;
710
+ partsByMessageId.set(messageId, [
711
+ ...(partsByMessageId.get(messageId) ?? []),
712
+ part,
713
+ ]);
714
+ }
665
715
  const events = [];
666
716
  for (const session of sessions) {
667
717
  const sessionId = asString(session.id);
@@ -682,15 +732,21 @@ function opencodeEvents(homeDir, rowLimit) {
682
732
  for (const message of messages) {
683
733
  const sessionId = asString(message.session_id);
684
734
  const session = sessionsById.get(sessionId);
735
+ const messageId = asString(message.id) ?? stableId(JSON.stringify(message));
736
+ const role = asString(message.role);
737
+ const messageParts = [...(partsByMessageId.get(messageId) ?? [])].sort((a, b) => getTimestampMs(a.time_created) - getTimestampMs(b.time_created));
738
+ const textOutput = opencodeTextFromParts(messageParts);
739
+ const textInput = role === "user" ? textOutput : undefined;
740
+ const output = role === "assistant" ? textOutput : undefined;
685
741
  const tokens = normalizeUsage(parseMaybeJson(message.tokens));
686
742
  const usage = tokens ?? normalizeUsage(parseMaybeJson(message.usage));
687
743
  events.push({
688
744
  agent: "opencode",
689
745
  sourcePath: db,
690
746
  sessionId: sessionId ?? stableId(db),
691
- recordId: asString(message.id) ?? stableId(JSON.stringify(message)),
692
- name: `opencode ${asString(message.role) ?? "message"}`,
693
- role: asString(message.role),
747
+ recordId: messageId,
748
+ name: `opencode ${role ?? "message"}`,
749
+ role,
694
750
  model: asString(message.model_id) ?? asString(message.nested_model_id),
695
751
  provider: asString(message.provider_id) ??
696
752
  asString(message.nested_provider_id),
@@ -699,12 +755,86 @@ function opencodeEvents(homeDir, rowLimit) {
699
755
  startMs: getTimestampMs(message.time_created),
700
756
  endMs: getTimestampMs(message.time_updated),
701
757
  parentRecordId: asString(message.parent_id) ?? "session",
758
+ input: textInput,
759
+ output,
702
760
  usage,
703
761
  metadata: pick(message, ["agent", "mode", "error"]),
704
762
  });
763
+ for (const part of messageParts) {
764
+ const data = asRecord(parseMaybeJson(part.data));
765
+ const type = asString(part.type) ?? asString(data.type);
766
+ const partId = asString(part.id) ?? stableId(JSON.stringify(part));
767
+ const partStartMs = getTimestampMs(part.time_created, getTimestampMs(message.time_created));
768
+ if (type === "reasoning") {
769
+ events.push({
770
+ agent: "opencode",
771
+ sourcePath: db,
772
+ sessionId: sessionId ?? stableId(db),
773
+ recordId: `reasoning-${partId}`,
774
+ name: "opencode reasoning",
775
+ model: asString(message.model_id) ?? asString(message.nested_model_id),
776
+ provider: asString(message.provider_id) ??
777
+ asString(message.nested_provider_id),
778
+ cwd: asString(message.cwd) ??
779
+ asString(asRecord(session).directory),
780
+ startMs: partStartMs,
781
+ endMs: getTimestampMs(part.time_updated, partStartMs + 1),
782
+ parentRecordId: messageId,
783
+ output: extractText(data.text),
784
+ metadata: {
785
+ has_encrypted_content: getPath(data, ["metadata", "openai", "reasoningEncryptedContent"]) !== undefined,
786
+ },
787
+ });
788
+ }
789
+ if (type === "tool") {
790
+ const state = asRecord(data.state);
791
+ const callId = asString(data.callID) ?? asString(data.call_id) ?? partId;
792
+ events.push({
793
+ agent: "opencode",
794
+ sourcePath: db,
795
+ sessionId: sessionId ?? stableId(db),
796
+ recordId: `tool-${callId}`,
797
+ name: `opencode tool ${asString(data.tool) ?? "call"}`,
798
+ model: asString(message.model_id) ?? asString(message.nested_model_id),
799
+ provider: asString(message.provider_id) ??
800
+ asString(message.nested_provider_id),
801
+ cwd: asString(message.cwd) ??
802
+ asString(asRecord(session).directory),
803
+ startMs: partStartMs,
804
+ endMs: getTimestampMs(part.time_updated, partStartMs + 1),
805
+ parentRecordId: messageId,
806
+ input: state.input,
807
+ output: state.output,
808
+ metadata: {
809
+ status: asString(state.status),
810
+ title: asString(state.title),
811
+ },
812
+ });
813
+ }
814
+ }
705
815
  }
706
816
  return events;
707
817
  }
818
+ function opencodeTextFromParts(parts) {
819
+ const text = parts
820
+ .map((part) => {
821
+ const data = asRecord(parseMaybeJson(part.data));
822
+ const type = asString(part.type) ?? asString(data.type);
823
+ if (type === "text")
824
+ return extractText(data.text, 8000);
825
+ if (type === "file") {
826
+ const filename = asString(data.filename);
827
+ const url = asString(data.url);
828
+ if (filename && url)
829
+ return `${filename}\n${url}`;
830
+ return filename ?? url;
831
+ }
832
+ return undefined;
833
+ })
834
+ .filter((value) => Boolean(value))
835
+ .join("\n");
836
+ return text ? text.slice(0, 8000) : undefined;
837
+ }
708
838
  function sqliteJson(db, sql) {
709
839
  const output = execFileSync("sqlite3", ["-readonly", "-json", db, sql], {
710
840
  encoding: "utf8",
@@ -762,6 +892,12 @@ function genericJsonlEvents(agent, files, sessionName) {
762
892
  for (const [index, row] of rows.entries()) {
763
893
  const message = asRecord(row.message);
764
894
  const role = asString(message.role) ?? asString(row.type);
895
+ const recordId = asString(row.uuid) ??
896
+ asString(row.id) ??
897
+ asString(row.toolUseID) ??
898
+ `row-${index}`;
899
+ const toolUseId = asString(row.toolUseID) ?? asString(row.tool_use_id);
900
+ const content = message.content ?? row.content;
765
901
  const timestamp = getTimestampMs(row.timestamp ?? row.time_created, startMs + index);
766
902
  const usage = normalizeUsage(message.usage ?? row.usage);
767
903
  events.push({
@@ -769,22 +905,23 @@ function genericJsonlEvents(agent, files, sessionName) {
769
905
  sourcePath: path,
770
906
  sessionId: asString(row.sessionId) ?? asString(row.session_id) ??
771
907
  sessionId,
772
- recordId: asString(row.uuid) ??
773
- asString(row.id) ??
774
- asString(row.toolUseID) ??
775
- `row-${index}`,
776
- name: `${agent} ${role ?? "event"}`,
908
+ recordId,
909
+ name: role === "tool_result" || role === "toolResult"
910
+ ? `${agent} toolResult ${asString(row.toolName) ?? "call"}`
911
+ : `${agent} ${role ?? "event"}`,
777
912
  role,
778
913
  model: asString(getPath(message, ["model"])) ?? asString(row.model),
779
914
  cwd: asString(row.cwd) ?? cwd,
780
915
  startMs: timestamp,
781
- parentRecordId: asString(row.parentUuid) ?? asString(row.parentId) ??
782
- "session",
916
+ parentRecordId: toolUseId
917
+ ? `tool-${toolUseId}`
918
+ : asString(row.parentUuid) ?? asString(row.parentId) ?? "session",
783
919
  input: role === "user"
784
- ? extractText(message.content ?? row.content)
920
+ ? extractText(content)
785
921
  : undefined,
786
- output: role === "assistant"
787
- ? extractText(message.content ?? row.content)
922
+ output: role === "assistant" || role === "tool_result" ||
923
+ role === "toolResult"
924
+ ? extractText(content)
788
925
  : undefined,
789
926
  usage,
790
927
  metadata: pick(row, [
@@ -795,12 +932,26 @@ function genericJsonlEvents(agent, files, sessionName) {
795
932
  "error",
796
933
  ]),
797
934
  });
798
- for (const tool of toolCallsFromContent(message.content)) {
935
+ for (const reasoning of reasoningFromContent(content)) {
936
+ events.push({
937
+ agent,
938
+ sourcePath: path,
939
+ sessionId,
940
+ recordId: `reasoning-${recordId}-${reasoning.index}`,
941
+ name: `${agent} reasoning`,
942
+ cwd,
943
+ startMs: timestamp,
944
+ parentRecordId: recordId,
945
+ output: reasoning.text,
946
+ metadata: { has_signature: reasoning.hasSignature },
947
+ });
948
+ }
949
+ for (const tool of toolCallsFromContent(content)) {
799
950
  events.push({
800
951
  agent,
801
952
  sourcePath: path,
802
953
  sessionId,
803
- recordId: tool.id,
954
+ recordId: `tool-${tool.id}`,
804
955
  name: `${agent} tool ${tool.name}`,
805
956
  cwd,
806
957
  startMs: timestamp,
@@ -830,6 +981,26 @@ function toolCallsFromContent(content) {
830
981
  ];
831
982
  });
832
983
  }
984
+ function reasoningFromContent(content) {
985
+ if (!Array.isArray(content))
986
+ return [];
987
+ return content.flatMap((item, index) => {
988
+ const record = asRecord(item);
989
+ const type = asString(record.type);
990
+ if (type !== "thinking" && type !== "reasoning")
991
+ return [];
992
+ const text = extractText(record.thinking ?? record.text, 8000);
993
+ if (!text)
994
+ return [];
995
+ return [
996
+ {
997
+ index,
998
+ text,
999
+ hasSignature: record.thinkingSignature !== undefined || record.signature !== undefined,
1000
+ },
1001
+ ];
1002
+ });
1003
+ }
833
1004
  function pick(source, keys) {
834
1005
  const out = {};
835
1006
  for (const key of keys) {
@@ -851,11 +1022,14 @@ function parseMaybeJson(value) {
851
1022
  function stableId(input) {
852
1023
  return createHash("sha256").update(input).digest("hex").slice(0, 32);
853
1024
  }
1025
+ function importIdentity(event) {
1026
+ return importIdentityVersions[event.agent] ?? importIdentityVersion;
1027
+ }
854
1028
  function fingerprint(event) {
855
- return `${importIdentityVersion}:${event.agent}:${event.sessionId}:${event.recordId}`;
1029
+ return `${importIdentity(event)}:${event.agent}:${event.sessionId}:${event.recordId}`;
856
1030
  }
857
1031
  function traceFingerprint(event) {
858
- return `${importIdentityVersion}:${event.agent}:${event.sessionId}`;
1032
+ return `${importIdentity(event)}:${event.agent}:${event.sessionId}`;
859
1033
  }
860
1034
  function traceId(event) {
861
1035
  return stableId(traceFingerprint(event));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ramarivera/coding-agent-langfuse",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "Universal coding-agent Langfuse backfiller and live OTLP helpers",
5
5
  "type": "module",
6
6
  "license": "MIT",