@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.
- package/dist/backfill.js +198 -16
- 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) ??
|
|
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:
|
|
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 ${
|
|
429
|
-
role
|
|
445
|
+
name: `codex ${role ?? "message"}`,
|
|
446
|
+
role,
|
|
430
447
|
model: currentModel,
|
|
431
448
|
cwd: currentCwd,
|
|
432
449
|
startMs: timestamp,
|
|
433
|
-
parentRecordId:
|
|
434
|
-
|
|
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-${
|
|
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:
|
|
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:
|
|
621
|
-
name: `opencode ${
|
|
622
|
-
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 `${
|
|
962
|
+
return `${importIdentity(event)}:${event.agent}:${event.sessionId}:${event.recordId}`;
|
|
785
963
|
}
|
|
786
964
|
function traceFingerprint(event) {
|
|
787
|
-
return `${
|
|
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:
|
|
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),
|