@ramarivera/coding-agent-langfuse 0.1.47 → 0.1.50
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 +15 -10
- package/dist/backfill.d.ts +1 -0
- package/dist/backfill.js +105 -28
- package/dist/service.js +2 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,8 +60,8 @@ guessing from host names or Windows paths in the Langfuse UI.
|
|
|
60
60
|
There are two config layers:
|
|
61
61
|
|
|
62
62
|
- Global host config: `~/.config/coding-agent-langfuse/path-tags.json`
|
|
63
|
-
- Optional project-local
|
|
64
|
-
|
|
63
|
+
- Optional project-local overlays: every `.langfuse-ca.json` found while
|
|
64
|
+
walking from the session cwd up to the scanned home directory
|
|
65
65
|
|
|
66
66
|
Global config uses prefix rules:
|
|
67
67
|
|
|
@@ -83,7 +83,9 @@ Global config uses prefix rules:
|
|
|
83
83
|
```
|
|
84
84
|
|
|
85
85
|
Project-local config is intentionally smaller and overrides/extends the matched
|
|
86
|
-
global rule
|
|
86
|
+
global rule. Parent overlays are applied before child overlays, so a config at
|
|
87
|
+
`~/work/client-a/.langfuse-ca.json` can tag a whole workstream while nested
|
|
88
|
+
repos add repo-specific fields:
|
|
87
89
|
|
|
88
90
|
```json
|
|
89
91
|
{
|
|
@@ -97,10 +99,13 @@ global rule:
|
|
|
97
99
|
|
|
98
100
|
Matched values are emitted on root trace metadata, observation metadata, and
|
|
99
101
|
top-level OTLP attributes such as `project.tags`, `project.group`, and
|
|
100
|
-
`project.owner`.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
`project.owner`. When the session cwd is inside a Git repository, the exporter
|
|
103
|
+
also emits `git.worktree.path`, `git.branch`, and `git.commit` plus matching
|
|
104
|
+
Langfuse metadata fields. The root span also sets `langfuse.trace.tags`, so
|
|
105
|
+
Langfuse custom dashboards can filter/group by tags, metadata, branch, or
|
|
106
|
+
worktree. Historical repair runs use the same OTLP builder as live follow mode,
|
|
107
|
+
so rerunning with `--force` can backfill newly added tags and Git metadata for
|
|
108
|
+
existing sessions.
|
|
104
109
|
|
|
105
110
|
Use a non-default global config path with:
|
|
106
111
|
|
|
@@ -109,9 +114,9 @@ npx @ramarivera/coding-agent-langfuse@latest \
|
|
|
109
114
|
--path-tags-config "$HOME/.config/coding-agent-langfuse/path-tags.json"
|
|
110
115
|
```
|
|
111
116
|
|
|
112
|
-
Generated services
|
|
113
|
-
|
|
114
|
-
|
|
117
|
+
Generated services keep their command small and rely on the package's default
|
|
118
|
+
global config plus upward project-local discovery. Use `--path-tags-config` only
|
|
119
|
+
for one-off CLI runs that need a non-default global config path.
|
|
115
120
|
|
|
116
121
|
## Cost calculation
|
|
117
122
|
|
package/dist/backfill.d.ts
CHANGED
package/dist/backfill.js
CHANGED
|
@@ -35,6 +35,7 @@ const defaultPathTagsConfigPath = join(homedir(), ".config/coding-agent-langfuse
|
|
|
35
35
|
const currentHost = hostname();
|
|
36
36
|
const projectLocalConfigFile = ".langfuse-ca.json";
|
|
37
37
|
const projectLocalConfigCache = new Map();
|
|
38
|
+
const gitMetadataCache = new Map();
|
|
38
39
|
const kimiFirepassRates = {
|
|
39
40
|
input: 2,
|
|
40
41
|
output: 8,
|
|
@@ -195,7 +196,7 @@ function projectMetadata(cwd) {
|
|
|
195
196
|
projectFolder: projectName,
|
|
196
197
|
};
|
|
197
198
|
}
|
|
198
|
-
function matchPathTags(cwd, config) {
|
|
199
|
+
function matchPathTags(cwd, config, homeDir) {
|
|
199
200
|
if (!cwd)
|
|
200
201
|
return { tags: [], metadata: {} };
|
|
201
202
|
const normalizedCwd = normalizeRulePath(cwd);
|
|
@@ -217,7 +218,7 @@ function matchPathTags(cwd, config) {
|
|
|
217
218
|
const overlays = [
|
|
218
219
|
globalMatch,
|
|
219
220
|
config.project,
|
|
220
|
-
|
|
221
|
+
...loadProjectLocalOverlays(cwd, homeDir),
|
|
221
222
|
].filter((overlay) => overlay !== undefined);
|
|
222
223
|
return overlays.reduce((acc, overlay) => {
|
|
223
224
|
if (!overlay)
|
|
@@ -235,17 +236,69 @@ function matchPathTags(cwd, config) {
|
|
|
235
236
|
projectFolder: undefined,
|
|
236
237
|
});
|
|
237
238
|
}
|
|
238
|
-
function mergeProjectMetadata(cwd, config) {
|
|
239
|
+
function mergeProjectMetadata(cwd, config, homeDir) {
|
|
239
240
|
const project = projectMetadata(cwd);
|
|
240
|
-
const pathTags = matchPathTags(cwd, config);
|
|
241
|
+
const pathTags = matchPathTags(cwd, config, homeDir);
|
|
241
242
|
return {
|
|
242
243
|
...project,
|
|
243
244
|
projectName: pathTags.projectName ?? project.projectName,
|
|
244
245
|
projectFolder: pathTags.projectFolder ?? project.projectFolder,
|
|
245
246
|
tags: pathTags.tags,
|
|
246
247
|
metadata: pathTags.metadata,
|
|
248
|
+
git: loadGitMetadata(cwd),
|
|
247
249
|
};
|
|
248
250
|
}
|
|
251
|
+
function loadGitMetadata(cwd) {
|
|
252
|
+
const gitCwd = nearestExistingDirectory(cwd);
|
|
253
|
+
if (!gitCwd)
|
|
254
|
+
return {};
|
|
255
|
+
const cached = gitMetadataCache.get(gitCwd);
|
|
256
|
+
if (cached)
|
|
257
|
+
return cached;
|
|
258
|
+
const worktreePath = gitOutput(gitCwd, ["rev-parse", "--show-toplevel"]);
|
|
259
|
+
if (!worktreePath) {
|
|
260
|
+
gitMetadataCache.set(gitCwd, {});
|
|
261
|
+
return {};
|
|
262
|
+
}
|
|
263
|
+
const branch = gitOutput(gitCwd, ["branch", "--show-current"]) ??
|
|
264
|
+
gitOutput(gitCwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
265
|
+
const commit = gitOutput(gitCwd, ["rev-parse", "--verify", "HEAD"]);
|
|
266
|
+
const metadata = {
|
|
267
|
+
worktreePath,
|
|
268
|
+
branch: branch === "HEAD" ? undefined : branch,
|
|
269
|
+
commit,
|
|
270
|
+
};
|
|
271
|
+
gitMetadataCache.set(gitCwd, metadata);
|
|
272
|
+
return metadata;
|
|
273
|
+
}
|
|
274
|
+
function gitOutput(cwd, args) {
|
|
275
|
+
try {
|
|
276
|
+
const output = execFileSync("git", ["-C", cwd, ...args], {
|
|
277
|
+
encoding: "utf8",
|
|
278
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
279
|
+
}).trim();
|
|
280
|
+
return output.length > 0 ? output : undefined;
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function nearestExistingDirectory(path) {
|
|
287
|
+
if (!path)
|
|
288
|
+
return undefined;
|
|
289
|
+
let current = path;
|
|
290
|
+
while (true) {
|
|
291
|
+
try {
|
|
292
|
+
return statSync(current).isDirectory() ? current : dirname(current);
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
const parent = dirname(current);
|
|
296
|
+
if (parent === current)
|
|
297
|
+
return undefined;
|
|
298
|
+
current = parent;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
249
302
|
function usage() {
|
|
250
303
|
return `Usage: coding-agent-langfuse-backfill [options]
|
|
251
304
|
|
|
@@ -475,10 +528,12 @@ function loadPathTagsConfig(path) {
|
|
|
475
528
|
});
|
|
476
529
|
return { rules, project: normalizeProjectTagOverlay(root, path) };
|
|
477
530
|
}
|
|
478
|
-
function
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
531
|
+
function loadProjectLocalOverlays(cwd, homeDir) {
|
|
532
|
+
return findProjectLocalConfigs(cwd, homeDir)
|
|
533
|
+
.map((configPath) => loadProjectLocalOverlay(configPath))
|
|
534
|
+
.filter((overlay) => overlay !== undefined);
|
|
535
|
+
}
|
|
536
|
+
function loadProjectLocalOverlay(configPath) {
|
|
482
537
|
const cached = projectLocalConfigCache.get(configPath);
|
|
483
538
|
if (projectLocalConfigCache.has(configPath))
|
|
484
539
|
return cached;
|
|
@@ -492,31 +547,26 @@ function loadProjectLocalOverlay(cwd) {
|
|
|
492
547
|
projectLocalConfigCache.set(configPath, overlay);
|
|
493
548
|
return overlay;
|
|
494
549
|
}
|
|
495
|
-
function
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
}
|
|
504
|
-
catch {
|
|
505
|
-
const parent = dirname(current);
|
|
506
|
-
if (parent === current)
|
|
507
|
-
return undefined;
|
|
508
|
-
current = parent;
|
|
509
|
-
}
|
|
510
|
-
}
|
|
550
|
+
function findProjectLocalConfigs(cwd, homeDir) {
|
|
551
|
+
const start = nearestExistingDirectory(cwd);
|
|
552
|
+
if (!start)
|
|
553
|
+
return [];
|
|
554
|
+
const home = homeDir ? nearestExistingDirectory(homeDir) ?? homeDir : homedir();
|
|
555
|
+
const stopAtHome = isSameOrChildPath(start, home);
|
|
556
|
+
const configPaths = [];
|
|
557
|
+
let current = start;
|
|
511
558
|
while (true) {
|
|
512
559
|
const candidate = join(current, projectLocalConfigFile);
|
|
513
560
|
if (existsSync(candidate))
|
|
514
|
-
|
|
561
|
+
configPaths.push(candidate);
|
|
562
|
+
if (stopAtHome && sameNormalizedPath(current, home))
|
|
563
|
+
break;
|
|
515
564
|
const parent = dirname(current);
|
|
516
565
|
if (parent === current)
|
|
517
|
-
|
|
566
|
+
break;
|
|
518
567
|
current = parent;
|
|
519
568
|
}
|
|
569
|
+
return configPaths.reverse();
|
|
520
570
|
}
|
|
521
571
|
function normalizeProjectTagOverlay(value, sourcePath) {
|
|
522
572
|
const root = asRecord(value);
|
|
@@ -543,6 +593,15 @@ function normalizeRulePath(path) {
|
|
|
543
593
|
const normalized = path.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
544
594
|
return /^[a-z]:\//i.test(normalized) ? normalized.toLowerCase() : normalized;
|
|
545
595
|
}
|
|
596
|
+
function sameNormalizedPath(left, right) {
|
|
597
|
+
return normalizeRulePath(left) === normalizeRulePath(right);
|
|
598
|
+
}
|
|
599
|
+
function isSameOrChildPath(child, parent) {
|
|
600
|
+
const normalizedChild = normalizeRulePath(child);
|
|
601
|
+
const normalizedParent = normalizeRulePath(parent);
|
|
602
|
+
return normalizedChild === normalizedParent ||
|
|
603
|
+
normalizedChild.startsWith(`${normalizedParent}/`);
|
|
604
|
+
}
|
|
546
605
|
function loadCostCatalogFromEnv() {
|
|
547
606
|
let catalog = { ...defaultCostRates };
|
|
548
607
|
const path = process.env.CODING_AGENT_LANGFUSE_COST_RATES_PATH ??
|
|
@@ -1815,6 +1874,7 @@ function toOtlp(events, options = {}) {
|
|
|
1815
1874
|
const maxFieldBytes = options.maxFieldBytes ?? defaultMaxFieldBytes;
|
|
1816
1875
|
const costRates = options.costRates ?? loadCostCatalogFromEnv();
|
|
1817
1876
|
const pathTagsConfig = options.pathTagsConfig ?? { rules: [] };
|
|
1877
|
+
const projectConfigHomeDir = options.homeDir ?? process.env.HOME ?? homedir();
|
|
1818
1878
|
const spansByTrace = new Map();
|
|
1819
1879
|
for (const rawEvent of events) {
|
|
1820
1880
|
const event = limitEventPayload(rawEvent, maxFieldBytes);
|
|
@@ -1829,7 +1889,7 @@ function toOtlp(events, options = {}) {
|
|
|
1829
1889
|
const traceStartMs = sortedEvents[0]?.startMs ?? Date.now();
|
|
1830
1890
|
const traceEndMs = Math.max(...sortedEvents.map((event) => event.endMs ?? event.startMs + 1), traceStartMs + 1);
|
|
1831
1891
|
const shouldEmitRootSpan = sortedEvents.some((event) => event.recordId === "session");
|
|
1832
|
-
const firstProject = mergeProjectMetadata(first.cwd, pathTagsConfig);
|
|
1892
|
+
const firstProject = mergeProjectMetadata(first.cwd, pathTagsConfig, projectConfigHomeDir);
|
|
1833
1893
|
const rootAttributes = [
|
|
1834
1894
|
attr("service.name", `agent.${first.agent}`),
|
|
1835
1895
|
attr("deployment.environment", "local"),
|
|
@@ -1854,6 +1914,9 @@ function toOtlp(events, options = {}) {
|
|
|
1854
1914
|
attr("langfuse.trace.metadata.project_folder", firstProject.projectFolder),
|
|
1855
1915
|
attr("langfuse.trace.metadata.project_tags", nonEmptyArray(firstProject.tags)),
|
|
1856
1916
|
attr("langfuse.trace.metadata.path_tags", nonEmptyRecord(firstProject.metadata)),
|
|
1917
|
+
attr("langfuse.trace.metadata.git_worktree_path", firstProject.git.worktreePath),
|
|
1918
|
+
attr("langfuse.trace.metadata.git_branch", firstProject.git.branch),
|
|
1919
|
+
attr("langfuse.trace.metadata.git_commit", firstProject.git.commit),
|
|
1857
1920
|
attr("langfuse.trace.tags", nonEmptyArray(firstProject.tags)),
|
|
1858
1921
|
attr("langfuse.trace.metadata.import_payload_version", payloadVersion(first)),
|
|
1859
1922
|
attr("langfuse.trace.metadata.import_state_identity", importIdentity(first)),
|
|
@@ -1870,6 +1933,9 @@ function toOtlp(events, options = {}) {
|
|
|
1870
1933
|
attr("langfuse.observation.metadata.project_folder", firstProject.projectFolder),
|
|
1871
1934
|
attr("langfuse.observation.metadata.project_tags", nonEmptyArray(firstProject.tags)),
|
|
1872
1935
|
attr("langfuse.observation.metadata.path_tags", nonEmptyRecord(firstProject.metadata)),
|
|
1936
|
+
attr("langfuse.observation.metadata.git_worktree_path", firstProject.git.worktreePath),
|
|
1937
|
+
attr("langfuse.observation.metadata.git_branch", firstProject.git.branch),
|
|
1938
|
+
attr("langfuse.observation.metadata.git_commit", firstProject.git.commit),
|
|
1873
1939
|
attr("langfuse.observation.metadata.import_payload_version", payloadVersion(first)),
|
|
1874
1940
|
attr("langfuse.observation.metadata.import_state_identity", importIdentity(first)),
|
|
1875
1941
|
attr("langfuse.observation.metadata.langfuse_id_identity", langfuseIdIdentity(first)),
|
|
@@ -1881,6 +1947,9 @@ function toOtlp(events, options = {}) {
|
|
|
1881
1947
|
attr("project.tags", nonEmptyArray(firstProject.tags)),
|
|
1882
1948
|
attr("project.group", firstProject.metadata.project_group),
|
|
1883
1949
|
attr("project.owner", firstProject.metadata.project_owner),
|
|
1950
|
+
attr("git.worktree.path", firstProject.git.worktreePath),
|
|
1951
|
+
attr("git.branch", firstProject.git.branch),
|
|
1952
|
+
attr("git.commit", firstProject.git.commit),
|
|
1884
1953
|
].filter((item) => Boolean(item));
|
|
1885
1954
|
const rootSpan = {
|
|
1886
1955
|
traceId: traceId(first),
|
|
@@ -1900,7 +1969,7 @@ function toOtlp(events, options = {}) {
|
|
|
1900
1969
|
const generation = isGenerationEvent(event);
|
|
1901
1970
|
const usage = usageDetails(event.usage);
|
|
1902
1971
|
const cost = generation ? calculateCost(event, usage, costRates) : undefined;
|
|
1903
|
-
const eventProject = mergeProjectMetadata(event.cwd, pathTagsConfig);
|
|
1972
|
+
const eventProject = mergeProjectMetadata(event.cwd, pathTagsConfig, projectConfigHomeDir);
|
|
1904
1973
|
const attributes = [
|
|
1905
1974
|
attr("service.name", `agent.${event.agent}`),
|
|
1906
1975
|
attr("deployment.environment", "local"),
|
|
@@ -1927,6 +1996,9 @@ function toOtlp(events, options = {}) {
|
|
|
1927
1996
|
attr("langfuse.observation.metadata.project_folder", eventProject.projectFolder),
|
|
1928
1997
|
attr("langfuse.observation.metadata.project_tags", nonEmptyArray(eventProject.tags)),
|
|
1929
1998
|
attr("langfuse.observation.metadata.path_tags", nonEmptyRecord(eventProject.metadata)),
|
|
1999
|
+
attr("langfuse.observation.metadata.git_worktree_path", eventProject.git.worktreePath),
|
|
2000
|
+
attr("langfuse.observation.metadata.git_branch", eventProject.git.branch),
|
|
2001
|
+
attr("langfuse.observation.metadata.git_commit", eventProject.git.commit),
|
|
1930
2002
|
attr("langfuse.observation.metadata.model", modelName ?? event.model),
|
|
1931
2003
|
attr("langfuse.observation.metadata.provider", event.provider),
|
|
1932
2004
|
attr("langfuse.observation.metadata.import_payload_version", payloadVersion(event)),
|
|
@@ -1953,6 +2025,9 @@ function toOtlp(events, options = {}) {
|
|
|
1953
2025
|
attr("project.tags", nonEmptyArray(eventProject.tags)),
|
|
1954
2026
|
attr("project.group", eventProject.metadata.project_group),
|
|
1955
2027
|
attr("project.owner", eventProject.metadata.project_owner),
|
|
2028
|
+
attr("git.worktree.path", eventProject.git.worktreePath),
|
|
2029
|
+
attr("git.branch", eventProject.git.branch),
|
|
2030
|
+
attr("git.commit", eventProject.git.commit),
|
|
1956
2031
|
attr("role", event.role),
|
|
1957
2032
|
attr("agent.model", event.model),
|
|
1958
2033
|
attr("agent.provider", event.provider),
|
|
@@ -2137,6 +2212,7 @@ async function run(options) {
|
|
|
2137
2212
|
maxFieldBytes: options.maxFieldBytes,
|
|
2138
2213
|
costRates: options.costRates,
|
|
2139
2214
|
pathTagsConfig: options.pathTagsConfig,
|
|
2215
|
+
homeDir: options.homeDir,
|
|
2140
2216
|
});
|
|
2141
2217
|
}
|
|
2142
2218
|
catch (error) {
|
|
@@ -2164,6 +2240,7 @@ async function run(options) {
|
|
|
2164
2240
|
maxFieldBytes: options.maxFieldBytes,
|
|
2165
2241
|
costRates: options.costRates,
|
|
2166
2242
|
pathTagsConfig: options.pathTagsConfig,
|
|
2243
|
+
homeDir: options.homeDir,
|
|
2167
2244
|
auth: options.auth,
|
|
2168
2245
|
});
|
|
2169
2246
|
for (const event of batch) {
|
package/dist/service.js
CHANGED
|
@@ -36,11 +36,11 @@ Service options:
|
|
|
36
36
|
`;
|
|
37
37
|
}
|
|
38
38
|
function parseServiceArgs(argv) {
|
|
39
|
-
const action = parseServiceAction(argv[0]);
|
|
40
39
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
41
40
|
console.log(serviceUsage());
|
|
42
41
|
process.exit(0);
|
|
43
42
|
}
|
|
43
|
+
const action = parseServiceAction(argv[0]);
|
|
44
44
|
let platform = currentServicePlatform();
|
|
45
45
|
let agents = [...allAgents];
|
|
46
46
|
let endpoint = process.env.LANGFUSE_BACKFILL_ENDPOINT ?? defaultEndpoint;
|
|
@@ -56,8 +56,7 @@ function parseServiceArgs(argv) {
|
|
|
56
56
|
process.env.LANGFUSE_BACKFILL_COST_RATES_PATH;
|
|
57
57
|
let costRatesJson = process.env.CODING_AGENT_LANGFUSE_COST_RATES_JSON ??
|
|
58
58
|
process.env.LANGFUSE_BACKFILL_COST_RATES_JSON;
|
|
59
|
-
let pathTagsConfigPath
|
|
60
|
-
process.env.LANGFUSE_BACKFILL_PATH_TAGS_CONFIG;
|
|
59
|
+
let pathTagsConfigPath;
|
|
61
60
|
let since;
|
|
62
61
|
let dryRun = false;
|
|
63
62
|
let start = true;
|