@ramarivera/coding-agent-langfuse 0.1.16 → 0.1.18
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/README.md +12 -1
- package/dist/backfill.d.ts +10 -2
- package/dist/backfill.js +169 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,7 +18,18 @@ npx @ramarivera/coding-agent-langfuse@latest \
|
|
|
18
18
|
--agents claude,codex,grok,pi,opencode \
|
|
19
19
|
--endpoint https://langfuse.ai.roxasroot.net/otel/v1/traces \
|
|
20
20
|
--state "$HOME/.local/state/coding-agent-langfuse/backfill-v6.json" \
|
|
21
|
-
--batch-size
|
|
21
|
+
--batch-size 10
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Run live incremental forwarding without putting inference behind a gateway:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
npx @ramarivera/coding-agent-langfuse@latest \
|
|
28
|
+
--agents codex \
|
|
29
|
+
--endpoint https://langfuse.ai.roxasroot.net/otel/v1/traces \
|
|
30
|
+
--state "$HOME/.local/state/coding-agent-langfuse/live-codex.json" \
|
|
31
|
+
--batch-size 10 \
|
|
32
|
+
--follow
|
|
22
33
|
```
|
|
23
34
|
|
|
24
35
|
The importer is fail-fast: the first failed OTLP POST stops the run, prints the
|
package/dist/backfill.d.ts
CHANGED
|
@@ -33,6 +33,9 @@ type BackfillOptions = {
|
|
|
33
33
|
statePath: string;
|
|
34
34
|
homeDir: string;
|
|
35
35
|
dryRun: boolean;
|
|
36
|
+
follow: boolean;
|
|
37
|
+
pollIntervalMs: number;
|
|
38
|
+
idleExitAfterMs?: number;
|
|
36
39
|
limit?: number;
|
|
37
40
|
sinceMs?: number;
|
|
38
41
|
untilMs?: number;
|
|
@@ -50,6 +53,10 @@ type RunSummary = {
|
|
|
50
53
|
endpoint: string;
|
|
51
54
|
statePath: string;
|
|
52
55
|
};
|
|
56
|
+
type FollowSummary = RunSummary & {
|
|
57
|
+
iterations: number;
|
|
58
|
+
follow: true;
|
|
59
|
+
};
|
|
53
60
|
declare function parseArgs(argv: string[]): BackfillOptions;
|
|
54
61
|
declare function codexEvents(homeDir: string): BackfillEvent[];
|
|
55
62
|
declare function claudeEvents(homeDir: string): BackfillEvent[];
|
|
@@ -60,5 +67,6 @@ declare function fingerprint(event: BackfillEvent): string;
|
|
|
60
67
|
declare function toOtlp(events: BackfillEvent[]): Record<string, unknown>;
|
|
61
68
|
declare function discoverEvents(options: BackfillOptions): BackfillEvent[];
|
|
62
69
|
declare function run(options: BackfillOptions): Promise<RunSummary>;
|
|
63
|
-
declare function
|
|
64
|
-
|
|
70
|
+
declare function follow(options: BackfillOptions): Promise<FollowSummary>;
|
|
71
|
+
declare function main(argv?: string[]): Promise<RunSummary | FollowSummary>;
|
|
72
|
+
export { type BackfillEvent, type BackfillOptions, claudeEvents, codexEvents, discoverEvents, fingerprint, follow, grokEvents, main, opencodeEvents, parseArgs, piEvents, run, toOtlp, };
|
package/dist/backfill.js
CHANGED
|
@@ -5,7 +5,7 @@ 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 = "v9-codex-conversation-events";
|
|
9
9
|
const defaultEndpoint = "https://langfuse.ai.roxasroot.net/otel/v1/traces";
|
|
10
10
|
const deadRemoteEndpoint = "http://langfuse.ai.roxasroot.net:14318/v1/traces";
|
|
11
11
|
const defaultStatePath = join(homedir(), ".local/state/coding-agent-langfuse/backfill-v6.json");
|
|
@@ -21,6 +21,9 @@ Options:
|
|
|
21
21
|
--until ISO_OR_MS Only import events before or at this timestamp
|
|
22
22
|
--limit N Stop after N unsent events
|
|
23
23
|
--batch-size N OTLP spans per POST (default: 50)
|
|
24
|
+
--follow Keep scanning and sending newly written events
|
|
25
|
+
--poll-interval-ms N Delay between --follow scans (default: 5000)
|
|
26
|
+
--idle-exit-after-ms N Stop --follow after this much time without new sends
|
|
24
27
|
--dry-run Discover and dedupe without sending or mutating state
|
|
25
28
|
--help Show this help
|
|
26
29
|
`;
|
|
@@ -34,6 +37,9 @@ function parseArgs(argv) {
|
|
|
34
37
|
let sinceMs;
|
|
35
38
|
let untilMs;
|
|
36
39
|
let batchSize = 50;
|
|
40
|
+
let follow = false;
|
|
41
|
+
let pollIntervalMs = 5_000;
|
|
42
|
+
let idleExitAfterMs;
|
|
37
43
|
const agents = new Set(allAgents);
|
|
38
44
|
for (let i = 0; i < argv.length; i++) {
|
|
39
45
|
const arg = argv[i];
|
|
@@ -75,6 +81,15 @@ function parseArgs(argv) {
|
|
|
75
81
|
else if (arg === "--batch-size") {
|
|
76
82
|
batchSize = Number.parseInt(next(), 10);
|
|
77
83
|
}
|
|
84
|
+
else if (arg === "--follow") {
|
|
85
|
+
follow = true;
|
|
86
|
+
}
|
|
87
|
+
else if (arg === "--poll-interval-ms") {
|
|
88
|
+
pollIntervalMs = Number.parseInt(next(), 10);
|
|
89
|
+
}
|
|
90
|
+
else if (arg === "--idle-exit-after-ms") {
|
|
91
|
+
idleExitAfterMs = Number.parseInt(next(), 10);
|
|
92
|
+
}
|
|
78
93
|
else if (arg === "--since") {
|
|
79
94
|
sinceMs = parseTime(next());
|
|
80
95
|
}
|
|
@@ -88,6 +103,13 @@ function parseArgs(argv) {
|
|
|
88
103
|
if (!Number.isFinite(batchSize) || batchSize < 1) {
|
|
89
104
|
throw new Error("--batch-size must be a positive integer");
|
|
90
105
|
}
|
|
106
|
+
if (!Number.isFinite(pollIntervalMs) || pollIntervalMs < 1) {
|
|
107
|
+
throw new Error("--poll-interval-ms must be a positive integer");
|
|
108
|
+
}
|
|
109
|
+
if (idleExitAfterMs !== undefined &&
|
|
110
|
+
(!Number.isFinite(idleExitAfterMs) || idleExitAfterMs < 1)) {
|
|
111
|
+
throw new Error("--idle-exit-after-ms must be a positive integer");
|
|
112
|
+
}
|
|
91
113
|
if (limit !== undefined && (!Number.isFinite(limit) || limit < 1)) {
|
|
92
114
|
throw new Error("--limit must be a positive integer");
|
|
93
115
|
}
|
|
@@ -97,6 +119,9 @@ function parseArgs(argv) {
|
|
|
97
119
|
statePath,
|
|
98
120
|
homeDir,
|
|
99
121
|
dryRun,
|
|
122
|
+
follow,
|
|
123
|
+
pollIntervalMs,
|
|
124
|
+
idleExitAfterMs,
|
|
100
125
|
limit,
|
|
101
126
|
sinceMs,
|
|
102
127
|
untilMs,
|
|
@@ -204,7 +229,10 @@ function extractText(value, maxLength = 4000) {
|
|
|
204
229
|
const text = value
|
|
205
230
|
.map((item) => {
|
|
206
231
|
const record = asRecord(item);
|
|
207
|
-
return asString(record.text) ??
|
|
232
|
+
return asString(record.text) ??
|
|
233
|
+
extractText(record.content, maxLength) ??
|
|
234
|
+
extractText(record.summary, maxLength) ??
|
|
235
|
+
"";
|
|
208
236
|
})
|
|
209
237
|
.filter(Boolean)
|
|
210
238
|
.join("\n");
|
|
@@ -338,7 +366,8 @@ function costDetails(usage, model) {
|
|
|
338
366
|
return undefined;
|
|
339
367
|
}
|
|
340
368
|
function isGenerationEvent(event) {
|
|
341
|
-
return event.usage !== undefined && event.role !== "user"
|
|
369
|
+
return event.usage !== undefined && event.role !== "user" &&
|
|
370
|
+
event.role !== "developer" && event.role !== "system";
|
|
342
371
|
}
|
|
343
372
|
function codexEvents(homeDir) {
|
|
344
373
|
const files = listFiles(join(homeDir, ".codex/sessions"), (path) => path.endsWith(".jsonl"));
|
|
@@ -353,6 +382,7 @@ function codexEvents(homeDir) {
|
|
|
353
382
|
asString(getPath(payload, ["model"]));
|
|
354
383
|
let currentModel = model;
|
|
355
384
|
let currentCwd = cwd;
|
|
385
|
+
let currentTurnRecordId = "session";
|
|
356
386
|
const seenTokenCounts = new Set();
|
|
357
387
|
const events = [
|
|
358
388
|
{
|
|
@@ -381,11 +411,12 @@ function codexEvents(homeDir) {
|
|
|
381
411
|
if (type === "turn_context") {
|
|
382
412
|
currentModel = asString(rowPayload.model) ?? currentModel;
|
|
383
413
|
currentCwd = asString(rowPayload.cwd) ?? currentCwd;
|
|
414
|
+
currentTurnRecordId = `turn-${asString(rowPayload.turn_id) ?? index}`;
|
|
384
415
|
events.push({
|
|
385
416
|
agent: "codex",
|
|
386
417
|
sourcePath: path,
|
|
387
418
|
sessionId,
|
|
388
|
-
recordId:
|
|
419
|
+
recordId: currentTurnRecordId,
|
|
389
420
|
name: "codex turn",
|
|
390
421
|
cwd: currentCwd,
|
|
391
422
|
model: currentModel,
|
|
@@ -395,34 +426,99 @@ function codexEvents(homeDir) {
|
|
|
395
426
|
});
|
|
396
427
|
}
|
|
397
428
|
if (type === "response_item" && itemType === "message") {
|
|
429
|
+
const role = asString(rowPayload.role);
|
|
430
|
+
const text = extractText(rowPayload.content);
|
|
431
|
+
const input = role === "user" || role === "developer" ||
|
|
432
|
+
role === "system"
|
|
433
|
+
? text
|
|
434
|
+
: undefined;
|
|
435
|
+
const output = role === "assistant" ? text : undefined;
|
|
398
436
|
events.push({
|
|
399
437
|
agent: "codex",
|
|
400
438
|
sourcePath: path,
|
|
401
439
|
sessionId,
|
|
402
440
|
recordId: `message-${asString(rowPayload.id) ?? index}`,
|
|
403
|
-
name: `codex ${
|
|
404
|
-
role
|
|
441
|
+
name: `codex ${role ?? "message"}`,
|
|
442
|
+
role,
|
|
405
443
|
model: currentModel,
|
|
406
444
|
cwd: currentCwd,
|
|
407
445
|
startMs: timestamp,
|
|
408
|
-
parentRecordId:
|
|
409
|
-
|
|
446
|
+
parentRecordId: currentTurnRecordId,
|
|
447
|
+
input,
|
|
448
|
+
output,
|
|
410
449
|
usage: normalizeUsage(rowPayload.usage),
|
|
450
|
+
metadata: pick(rowPayload, ["phase"]),
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
if (type === "response_item" && itemType === "reasoning") {
|
|
454
|
+
events.push({
|
|
455
|
+
agent: "codex",
|
|
456
|
+
sourcePath: path,
|
|
457
|
+
sessionId,
|
|
458
|
+
recordId: `reasoning-${index}`,
|
|
459
|
+
name: "codex reasoning",
|
|
460
|
+
model: currentModel,
|
|
461
|
+
cwd: currentCwd,
|
|
462
|
+
startMs: timestamp,
|
|
463
|
+
parentRecordId: currentTurnRecordId,
|
|
464
|
+
output: extractText(rowPayload.content) ??
|
|
465
|
+
extractText(rowPayload.summary),
|
|
466
|
+
metadata: {
|
|
467
|
+
has_encrypted_content: rowPayload.encrypted_content !== undefined,
|
|
468
|
+
},
|
|
411
469
|
});
|
|
412
470
|
}
|
|
413
471
|
if (type === "response_item" && itemType === "function_call") {
|
|
472
|
+
const callId = asString(rowPayload.call_id) ?? `${index}`;
|
|
414
473
|
events.push({
|
|
415
474
|
agent: "codex",
|
|
416
475
|
sourcePath: path,
|
|
417
476
|
sessionId,
|
|
418
|
-
recordId: `tool-${
|
|
477
|
+
recordId: `tool-${callId}`,
|
|
419
478
|
name: `codex tool ${asString(rowPayload.name) ?? "call"}`,
|
|
420
479
|
model: currentModel,
|
|
421
480
|
cwd: currentCwd,
|
|
422
481
|
startMs: timestamp,
|
|
423
|
-
parentRecordId:
|
|
482
|
+
parentRecordId: currentTurnRecordId,
|
|
424
483
|
input: rowPayload.arguments,
|
|
425
|
-
metadata: pick(rowPayload, ["name", "call_id"]),
|
|
484
|
+
metadata: pick(rowPayload, ["name", "namespace", "call_id"]),
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
if (type === "response_item" && itemType === "function_call_output") {
|
|
488
|
+
const callId = asString(rowPayload.call_id) ?? `${index}`;
|
|
489
|
+
events.push({
|
|
490
|
+
agent: "codex",
|
|
491
|
+
sourcePath: path,
|
|
492
|
+
sessionId,
|
|
493
|
+
recordId: `tool-result-${callId}`,
|
|
494
|
+
name: "codex tool result",
|
|
495
|
+
model: currentModel,
|
|
496
|
+
cwd: currentCwd,
|
|
497
|
+
startMs: timestamp,
|
|
498
|
+
parentRecordId: `tool-${callId}`,
|
|
499
|
+
output: rowPayload.output,
|
|
500
|
+
metadata: pick(rowPayload, ["call_id", "status", "execution"]),
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
if (type === "response_item" &&
|
|
504
|
+
(itemType === "tool_search_call" || itemType === "tool_search_output")) {
|
|
505
|
+
const callId = asString(rowPayload.call_id) ?? `${index}`;
|
|
506
|
+
const isOutput = itemType === "tool_search_output";
|
|
507
|
+
events.push({
|
|
508
|
+
agent: "codex",
|
|
509
|
+
sourcePath: path,
|
|
510
|
+
sessionId,
|
|
511
|
+
recordId: `${isOutput ? "tool-search-result" : "tool-search"}-${callId}`,
|
|
512
|
+
name: isOutput ? "codex tool_search result" : "codex tool_search",
|
|
513
|
+
model: currentModel,
|
|
514
|
+
cwd: currentCwd,
|
|
515
|
+
startMs: timestamp,
|
|
516
|
+
parentRecordId: isOutput
|
|
517
|
+
? `tool-search-${callId}`
|
|
518
|
+
: currentTurnRecordId,
|
|
519
|
+
input: isOutput ? undefined : rowPayload.arguments,
|
|
520
|
+
output: isOutput ? rowPayload.tools : undefined,
|
|
521
|
+
metadata: pick(rowPayload, ["call_id", "status", "execution"]),
|
|
426
522
|
});
|
|
427
523
|
}
|
|
428
524
|
if (type === "event_msg" && rowPayload.type === "token_count") {
|
|
@@ -837,6 +933,7 @@ function toOtlp(events) {
|
|
|
837
933
|
attributes: rootAttributes,
|
|
838
934
|
status: { code: 1 },
|
|
839
935
|
};
|
|
936
|
+
const spanIdsByRecordId = new Map(sortedEvents.map((event) => [event.recordId, spanId(event)]));
|
|
840
937
|
const childSpans = sortedEvents.map((event) => {
|
|
841
938
|
const startMs = event.startMs;
|
|
842
939
|
const durationMs = Math.max(1, (event.endMs ?? event.startMs + 1) - event.startMs);
|
|
@@ -898,7 +995,10 @@ function toOtlp(events) {
|
|
|
898
995
|
return {
|
|
899
996
|
traceId: traceId(event),
|
|
900
997
|
spanId: spanId(event),
|
|
901
|
-
parentSpanId:
|
|
998
|
+
parentSpanId: event.parentRecordId &&
|
|
999
|
+
event.parentRecordId !== "session"
|
|
1000
|
+
? spanIdsByRecordId.get(event.parentRecordId) ?? rootSpanId(event)
|
|
1001
|
+
: rootSpanId(event),
|
|
902
1002
|
name: event.name,
|
|
903
1003
|
kind: 1,
|
|
904
1004
|
startTimeUnixNano: ns(startMs),
|
|
@@ -1035,8 +1135,63 @@ async function run(options) {
|
|
|
1035
1135
|
statePath: options.statePath,
|
|
1036
1136
|
};
|
|
1037
1137
|
}
|
|
1138
|
+
function mergeSummary(base, next) {
|
|
1139
|
+
return {
|
|
1140
|
+
discovered: Object.fromEntries(allAgents.map((agent) => [
|
|
1141
|
+
agent,
|
|
1142
|
+
Math.max(base.discovered[agent] ?? 0, next.discovered[agent] ?? 0),
|
|
1143
|
+
])),
|
|
1144
|
+
sent: base.sent + next.sent,
|
|
1145
|
+
skipped: next.skipped,
|
|
1146
|
+
failed: base.failed + next.failed,
|
|
1147
|
+
notAttempted: next.notAttempted,
|
|
1148
|
+
aborted: base.aborted || next.aborted,
|
|
1149
|
+
...(next.error ? { error: next.error } : base.error ? { error: base.error } : {}),
|
|
1150
|
+
dryRun: base.dryRun,
|
|
1151
|
+
endpoint: base.endpoint,
|
|
1152
|
+
statePath: base.statePath,
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
function sleep(ms) {
|
|
1156
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1157
|
+
}
|
|
1158
|
+
async function follow(options) {
|
|
1159
|
+
let iterations = 0;
|
|
1160
|
+
let lastSendOrStartMs = Date.now();
|
|
1161
|
+
let aggregate;
|
|
1162
|
+
while (true) {
|
|
1163
|
+
iterations += 1;
|
|
1164
|
+
const summary = await run(options);
|
|
1165
|
+
aggregate = aggregate === undefined ? summary : mergeSummary(aggregate, summary);
|
|
1166
|
+
if (summary.sent > 0)
|
|
1167
|
+
lastSendOrStartMs = Date.now();
|
|
1168
|
+
if (summary.aborted)
|
|
1169
|
+
break;
|
|
1170
|
+
if (options.idleExitAfterMs !== undefined &&
|
|
1171
|
+
Date.now() - lastSendOrStartMs >= options.idleExitAfterMs) {
|
|
1172
|
+
break;
|
|
1173
|
+
}
|
|
1174
|
+
await sleep(options.pollIntervalMs);
|
|
1175
|
+
}
|
|
1176
|
+
return {
|
|
1177
|
+
...(aggregate ?? {
|
|
1178
|
+
discovered: Object.fromEntries(allAgents.map((agent) => [agent, 0])),
|
|
1179
|
+
sent: 0,
|
|
1180
|
+
skipped: 0,
|
|
1181
|
+
failed: 0,
|
|
1182
|
+
notAttempted: 0,
|
|
1183
|
+
aborted: false,
|
|
1184
|
+
dryRun: options.dryRun,
|
|
1185
|
+
endpoint: options.endpoint,
|
|
1186
|
+
statePath: options.statePath,
|
|
1187
|
+
}),
|
|
1188
|
+
iterations,
|
|
1189
|
+
follow: true,
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1038
1192
|
async function main(argv = process.argv.slice(2)) {
|
|
1039
|
-
const
|
|
1193
|
+
const options = parseArgs(argv);
|
|
1194
|
+
const summary = options.follow ? await follow(options) : await run(options);
|
|
1040
1195
|
console.log(JSON.stringify(summary, null, 2));
|
|
1041
1196
|
return summary;
|
|
1042
1197
|
}
|
|
@@ -1050,4 +1205,4 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|
|
1050
1205
|
process.exit(1);
|
|
1051
1206
|
}
|
|
1052
1207
|
}
|
|
1053
|
-
export { claudeEvents, codexEvents, discoverEvents, fingerprint, grokEvents, main, opencodeEvents, parseArgs, piEvents, run, toOtlp, };
|
|
1208
|
+
export { claudeEvents, codexEvents, discoverEvents, fingerprint, follow, grokEvents, main, opencodeEvents, parseArgs, piEvents, run, toOtlp, };
|