@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.
- package/dist/backfill.js +200 -26
- 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 = "
|
|
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:
|
|
594
|
-
name:
|
|
595
|
-
|
|
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:
|
|
602
|
-
|
|
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:
|
|
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:
|
|
692
|
-
name: `opencode ${
|
|
693
|
-
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
|
|
773
|
-
|
|
774
|
-
asString(row.
|
|
775
|
-
|
|
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:
|
|
782
|
-
|
|
916
|
+
parentRecordId: toolUseId
|
|
917
|
+
? `tool-${toolUseId}`
|
|
918
|
+
: asString(row.parentUuid) ?? asString(row.parentId) ?? "session",
|
|
783
919
|
input: role === "user"
|
|
784
|
-
? extractText(
|
|
920
|
+
? extractText(content)
|
|
785
921
|
: undefined,
|
|
786
|
-
output: role === "assistant"
|
|
787
|
-
|
|
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
|
|
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 `${
|
|
1029
|
+
return `${importIdentity(event)}:${event.agent}:${event.sessionId}:${event.recordId}`;
|
|
856
1030
|
}
|
|
857
1031
|
function traceFingerprint(event) {
|
|
858
|
-
return `${
|
|
1032
|
+
return `${importIdentity(event)}:${event.agent}:${event.sessionId}`;
|
|
859
1033
|
}
|
|
860
1034
|
function traceId(event) {
|
|
861
1035
|
return stableId(traceFingerprint(event));
|