@ramarivera/coding-agent-langfuse 0.1.19 → 0.1.21

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.
@@ -62,7 +62,11 @@ declare function codexEvents(homeDir: string): BackfillEvent[];
62
62
  declare function claudeEvents(homeDir: string): BackfillEvent[];
63
63
  declare function piEvents(homeDir: string): BackfillEvent[];
64
64
  declare function grokEvents(homeDir: string): BackfillEvent[];
65
- declare function opencodeEvents(homeDir: string, rowLimit?: number): BackfillEvent[];
65
+ declare function opencodeEvents(homeDir: string, options?: {
66
+ rowLimit?: number;
67
+ sinceMs?: number;
68
+ untilMs?: number;
69
+ }): BackfillEvent[];
66
70
  declare function fingerprint(event: BackfillEvent): string;
67
71
  declare function toOtlp(events: BackfillEvent[]): Record<string, unknown>;
68
72
  declare function discoverEvents(options: BackfillOptions): BackfillEvent[];
package/dist/backfill.js CHANGED
@@ -7,8 +7,11 @@ 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
9
  const importIdentityVersions = {
10
+ claude: "v11-tool-results",
10
11
  codex: "v9-codex-conversation-events",
12
+ grok: "v11-chat-history-only",
11
13
  opencode: "v10-opencode-message-parts",
14
+ pi: "v11-tool-results",
12
15
  };
13
16
  const defaultEndpoint = "https://langfuse.ai.roxasroot.net/otel/v1/traces";
14
17
  const deadRemoteEndpoint = "http://langfuse.ai.roxasroot.net:14318/v1/traces";
@@ -590,33 +593,56 @@ function piEvents(homeDir) {
590
593
  continue;
591
594
  const message = asRecord(row.message);
592
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);
593
599
  events.push({
594
600
  agent: "pi",
595
601
  sourcePath: path,
596
602
  sessionId,
597
- recordId: asString(row.id) ?? `message-${index}`,
598
- name: `pi ${asString(message.role) ?? "message"}`,
599
- role: asString(message.role),
603
+ recordId: messageId,
604
+ name: role === "toolResult"
605
+ ? `pi toolResult ${asString(message.toolName) ?? "call"}`
606
+ : `pi ${role ?? "message"}`,
607
+ role,
600
608
  model: asString(message.model) ?? asString(row.model),
601
609
  provider: asString(message.provider),
602
610
  cwd,
603
611
  startMs: getTimestampMs(row.timestamp ?? message.timestamp, startMs + index),
604
612
  endMs: getTimestampMs(message.completed, startMs + index + 1),
605
- parentRecordId: asString(row.parentId) ?? "session",
606
- input: message.role === "user"
613
+ parentRecordId: toolCallId
614
+ ? `tool-${toolCallId}`
615
+ : asString(row.parentId) ?? "session",
616
+ input: role === "user"
607
617
  ? extractText(message.content)
608
618
  : undefined,
609
- output: message.role === "assistant"
619
+ output: role === "assistant" || role === "toolResult"
610
620
  ? extractText(message.content)
611
621
  : undefined,
612
622
  usage,
623
+ metadata: pick(message, ["toolCallId", "toolName", "isError", "stopReason"]),
613
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
+ }
614
640
  for (const tool of toolCallsFromContent(message.content)) {
615
641
  events.push({
616
642
  agent: "pi",
617
643
  sourcePath: path,
618
644
  sessionId,
619
- recordId: tool.id,
645
+ recordId: `tool-${tool.id}`,
620
646
  name: `pi tool ${tool.name}`,
621
647
  cwd,
622
648
  model: asString(message.model),
@@ -630,18 +656,19 @@ function piEvents(homeDir) {
630
656
  });
631
657
  }
632
658
  function grokEvents(homeDir) {
633
- 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"));
634
660
  return genericJsonlEvents("grok", files, "grok session");
635
661
  }
636
- function opencodeEvents(homeDir, rowLimit) {
662
+ function opencodeEvents(homeDir, options = {}) {
637
663
  const db = join(homeDir, ".local/share/opencode/opencode.db");
638
664
  if (!existsSync(db))
639
665
  return [];
666
+ const timeWhere = sqliteTimeWhere(options.sinceMs, options.untilMs);
640
667
  let sessions = [];
641
668
  let messages = [];
642
669
  let parts = [];
643
670
  try {
644
- sessions = sqliteJsonByRowid(db, "session", "id, directory, time_created, time_updated, title, version, slug, project_id", undefined, rowLimit, 5_000);
671
+ sessions = sqliteJsonByRowid(db, "session", "id, directory, time_created, time_updated, title, version, slug, project_id", undefined, options.rowLimit, 5_000);
645
672
  messages = sqliteJsonByRowid(db, "message", [
646
673
  "id",
647
674
  "session_id",
@@ -660,7 +687,7 @@ function opencodeEvents(homeDir, rowLimit) {
660
687
  "json_extract(data, '$.agent') as agent",
661
688
  "json_extract(data, '$.mode') as mode",
662
689
  "json_extract(data, '$.error') as error",
663
- ].join(", "), undefined, rowLimit, 5_000);
690
+ ].join(", "), timeWhere, options.rowLimit, 5_000);
664
691
  parts = sqliteJsonByRowid(db, "part", [
665
692
  "id",
666
693
  "message_id",
@@ -669,7 +696,7 @@ function opencodeEvents(homeDir, rowLimit) {
669
696
  "time_updated",
670
697
  "json_extract(data, '$.type') as type",
671
698
  "json_extract(data, '$') as data",
672
- ].join(", "), undefined, rowLimit, 5_000);
699
+ ].join(", "), timeWhere, options.rowLimit, 5_000);
673
700
  }
674
701
  catch (error) {
675
702
  console.error(`Skipping OpenCode history from ${db}: ${error instanceof Error ? error.message : String(error)}`);
@@ -789,6 +816,13 @@ function opencodeEvents(homeDir, rowLimit) {
789
816
  }
790
817
  return events;
791
818
  }
819
+ function sqliteTimeWhere(sinceMs, untilMs) {
820
+ const conditions = [
821
+ sinceMs === undefined ? undefined : `time_created >= ${Math.trunc(sinceMs)}`,
822
+ untilMs === undefined ? undefined : `time_created <= ${Math.trunc(untilMs)}`,
823
+ ].filter((condition) => Boolean(condition));
824
+ return conditions.length > 0 ? conditions.join(" and ") : undefined;
825
+ }
792
826
  function opencodeTextFromParts(parts) {
793
827
  const text = parts
794
828
  .map((part) => {
@@ -866,6 +900,12 @@ function genericJsonlEvents(agent, files, sessionName) {
866
900
  for (const [index, row] of rows.entries()) {
867
901
  const message = asRecord(row.message);
868
902
  const role = asString(message.role) ?? asString(row.type);
903
+ const recordId = asString(row.uuid) ??
904
+ asString(row.id) ??
905
+ asString(row.toolUseID) ??
906
+ `row-${index}`;
907
+ const toolUseId = asString(row.toolUseID) ?? asString(row.tool_use_id);
908
+ const content = message.content ?? row.content;
869
909
  const timestamp = getTimestampMs(row.timestamp ?? row.time_created, startMs + index);
870
910
  const usage = normalizeUsage(message.usage ?? row.usage);
871
911
  events.push({
@@ -873,22 +913,23 @@ function genericJsonlEvents(agent, files, sessionName) {
873
913
  sourcePath: path,
874
914
  sessionId: asString(row.sessionId) ?? asString(row.session_id) ??
875
915
  sessionId,
876
- recordId: asString(row.uuid) ??
877
- asString(row.id) ??
878
- asString(row.toolUseID) ??
879
- `row-${index}`,
880
- name: `${agent} ${role ?? "event"}`,
916
+ recordId,
917
+ name: role === "tool_result" || role === "toolResult"
918
+ ? `${agent} toolResult ${asString(row.toolName) ?? "call"}`
919
+ : `${agent} ${role ?? "event"}`,
881
920
  role,
882
921
  model: asString(getPath(message, ["model"])) ?? asString(row.model),
883
922
  cwd: asString(row.cwd) ?? cwd,
884
923
  startMs: timestamp,
885
- parentRecordId: asString(row.parentUuid) ?? asString(row.parentId) ??
886
- "session",
924
+ parentRecordId: toolUseId
925
+ ? `tool-${toolUseId}`
926
+ : asString(row.parentUuid) ?? asString(row.parentId) ?? "session",
887
927
  input: role === "user"
888
- ? extractText(message.content ?? row.content)
928
+ ? extractText(content)
889
929
  : undefined,
890
- output: role === "assistant"
891
- ? extractText(message.content ?? row.content)
930
+ output: role === "assistant" || role === "tool_result" ||
931
+ role === "toolResult"
932
+ ? extractText(content)
892
933
  : undefined,
893
934
  usage,
894
935
  metadata: pick(row, [
@@ -899,12 +940,26 @@ function genericJsonlEvents(agent, files, sessionName) {
899
940
  "error",
900
941
  ]),
901
942
  });
902
- for (const tool of toolCallsFromContent(message.content)) {
943
+ for (const reasoning of reasoningFromContent(content)) {
944
+ events.push({
945
+ agent,
946
+ sourcePath: path,
947
+ sessionId,
948
+ recordId: `reasoning-${recordId}-${reasoning.index}`,
949
+ name: `${agent} reasoning`,
950
+ cwd,
951
+ startMs: timestamp,
952
+ parentRecordId: recordId,
953
+ output: reasoning.text,
954
+ metadata: { has_signature: reasoning.hasSignature },
955
+ });
956
+ }
957
+ for (const tool of toolCallsFromContent(content)) {
903
958
  events.push({
904
959
  agent,
905
960
  sourcePath: path,
906
961
  sessionId,
907
- recordId: tool.id,
962
+ recordId: `tool-${tool.id}`,
908
963
  name: `${agent} tool ${tool.name}`,
909
964
  cwd,
910
965
  startMs: timestamp,
@@ -934,6 +989,26 @@ function toolCallsFromContent(content) {
934
989
  ];
935
990
  });
936
991
  }
992
+ function reasoningFromContent(content) {
993
+ if (!Array.isArray(content))
994
+ return [];
995
+ return content.flatMap((item, index) => {
996
+ const record = asRecord(item);
997
+ const type = asString(record.type);
998
+ if (type !== "thinking" && type !== "reasoning")
999
+ return [];
1000
+ const text = extractText(record.thinking ?? record.text, 8000);
1001
+ if (!text)
1002
+ return [];
1003
+ return [
1004
+ {
1005
+ index,
1006
+ text,
1007
+ hasSignature: record.thinkingSignature !== undefined || record.signature !== undefined,
1008
+ },
1009
+ ];
1010
+ });
1011
+ }
937
1012
  function pick(source, keys) {
938
1013
  const out = {};
939
1014
  for (const key of keys) {
@@ -1180,7 +1255,11 @@ function discoverEvents(options) {
1180
1255
  claude: (inner) => claudeEvents(inner.homeDir),
1181
1256
  codex: (inner) => codexEvents(inner.homeDir),
1182
1257
  grok: (inner) => grokEvents(inner.homeDir),
1183
- opencode: (inner) => opencodeEvents(inner.homeDir, inner.limit),
1258
+ opencode: (inner) => opencodeEvents(inner.homeDir, {
1259
+ rowLimit: inner.limit,
1260
+ sinceMs: inner.sinceMs,
1261
+ untilMs: inner.untilMs,
1262
+ }),
1184
1263
  pi: (inner) => piEvents(inner.homeDir),
1185
1264
  };
1186
1265
  return allAgents
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ramarivera/coding-agent-langfuse",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "Universal coding-agent Langfuse backfiller and live OTLP helpers",
5
5
  "type": "module",
6
6
  "license": "MIT",