oh-langfuse 0.1.41 → 0.1.43
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 +62 -85
- package/bin/cli.js +21 -14
- package/codex_langfuse_notify.py +139 -59
- package/langfuse_hook.py +223 -57
- package/package.json +35 -35
- package/scripts/auto-update-runtime.mjs +4 -2
- package/scripts/codex-langfuse-setup.mjs +163 -11
- package/scripts/langfuse-check.mjs +11 -5
- package/scripts/langfuse-setup.mjs +155 -12
- package/scripts/metrics-utils.mjs +134 -10
- package/scripts/opencode-langfuse-setup.mjs +118 -61
- package/scripts/real-self-verify.mjs +13 -8
|
@@ -170,13 +170,14 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
170
170
|
' span.setAttribute("ai.telemetry.metadata.userId", userId);',
|
|
171
171
|
' span.setAttribute("oh.langfuse.source", "opencode");',
|
|
172
172
|
' span.setAttribute("oh.langfuse.user_id", userId);',
|
|
173
|
-
' span.setAttribute("oh.langfuse.metrics_schema_version", "1.
|
|
173
|
+
' span.setAttribute("oh.langfuse.metrics_schema_version", "1.1");',
|
|
174
174
|
' span.setAttribute("langfuse.trace.metadata.langfuse_user_id", userId);',
|
|
175
175
|
' span.setAttribute("langfuse.observation.metadata.langfuse_user_id", userId);',
|
|
176
176
|
' span.setAttribute("langfuse.metadata.langfuse_user_id", userId);',
|
|
177
177
|
' span.setAttribute("langfuse.observation.metadata.source", "opencode");',
|
|
178
|
+
' span.setAttribute("langfuse.observation.metadata.agent", "opencode");',
|
|
178
179
|
' span.setAttribute("langfuse.observation.metadata.user_id", userId);',
|
|
179
|
-
' span.setAttribute("langfuse.observation.metadata.metrics_schema_version", "1.
|
|
180
|
+
' span.setAttribute("langfuse.observation.metadata.metrics_schema_version", "1.1");',
|
|
180
181
|
" },",
|
|
181
182
|
" onEnd: (span) => {",
|
|
182
183
|
' if (typeof span.setAttribute !== "function") return;',
|
|
@@ -185,13 +186,14 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
185
186
|
' span.setAttribute("ai.telemetry.metadata.userId", userId);',
|
|
186
187
|
' span.setAttribute("oh.langfuse.source", "opencode");',
|
|
187
188
|
' span.setAttribute("oh.langfuse.user_id", userId);',
|
|
188
|
-
' span.setAttribute("oh.langfuse.metrics_schema_version", "1.
|
|
189
|
+
' span.setAttribute("oh.langfuse.metrics_schema_version", "1.1");',
|
|
189
190
|
' span.setAttribute("langfuse.trace.metadata.langfuse_user_id", userId);',
|
|
190
191
|
' span.setAttribute("langfuse.observation.metadata.langfuse_user_id", userId);',
|
|
191
192
|
' span.setAttribute("langfuse.metadata.langfuse_user_id", userId);',
|
|
192
193
|
' span.setAttribute("langfuse.observation.metadata.source", "opencode");',
|
|
194
|
+
' span.setAttribute("langfuse.observation.metadata.agent", "opencode");',
|
|
193
195
|
' span.setAttribute("langfuse.observation.metadata.user_id", userId);',
|
|
194
|
-
' span.setAttribute("langfuse.observation.metadata.metrics_schema_version", "1.
|
|
196
|
+
' span.setAttribute("langfuse.observation.metadata.metrics_schema_version", "1.1");',
|
|
195
197
|
" },",
|
|
196
198
|
" forceFlush: async () => {},",
|
|
197
199
|
" shutdown: async () => {},",
|
|
@@ -227,10 +229,11 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
227
229
|
"const writeOpencodeMetricAttributes = (span, userId) => {",
|
|
228
230
|
" const attrs = span.attributes ?? {};",
|
|
229
231
|
' writeSpanAttribute(span, "oh.langfuse.source", "opencode");',
|
|
230
|
-
' writeSpanAttribute(span, "oh.langfuse.metrics_schema_version", "1.
|
|
232
|
+
' writeSpanAttribute(span, "oh.langfuse.metrics_schema_version", "1.1");',
|
|
231
233
|
' if (userId) writeSpanAttribute(span, "oh.langfuse.user_id", userId);',
|
|
232
234
|
' writeMetric(span, "source", "opencode");',
|
|
233
|
-
' writeMetric(span, "
|
|
235
|
+
' writeMetric(span, "agent", "opencode");',
|
|
236
|
+
' writeMetric(span, "metrics_schema_version", "1.1");',
|
|
234
237
|
' if (userId) writeMetric(span, "user_id", userId);',
|
|
235
238
|
' const hasModel = attrs["ai.model.id"] || attrs["ai.model.provider"] || attrs["gen_ai.request.model"];',
|
|
236
239
|
' const isTool = attrs["ai.toolCall.name"] || attrs["ai.toolCall.id"];',
|
|
@@ -305,7 +308,9 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
305
308
|
" }",
|
|
306
309
|
" return out;",
|
|
307
310
|
"};",
|
|
308
|
-
"const
|
|
311
|
+
"const skillNamespace = (name) => String(name || '').includes(':') ? String(name).split(':', 1)[0] : '';",
|
|
312
|
+
"const skillEventType = (detectedBy) => detectedBy === 'tool_call' || detectedBy === 'plugin_event' ? 'invoked' : 'detected';",
|
|
313
|
+
"const skillIdSegment = (name) => String(name || 'unknown').trim().replace(/[^A-Za-z0-9_.:-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 96) || 'unknown';",
|
|
309
314
|
"",
|
|
310
315
|
"const escapeRegExp = (value) => String(value).replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');",
|
|
311
316
|
"",
|
|
@@ -329,17 +334,69 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
329
334
|
" return out;",
|
|
330
335
|
"};",
|
|
331
336
|
"",
|
|
332
|
-
"const
|
|
337
|
+
"const collectExplicitSkillUsages = (value, out = []) => {",
|
|
338
|
+
" if (value == null) return out;",
|
|
339
|
+
" if (Array.isArray(value)) {",
|
|
340
|
+
" for (const item of value) collectExplicitSkillUsages(item, out);",
|
|
341
|
+
" return out;",
|
|
342
|
+
" }",
|
|
343
|
+
" if (typeof value !== 'object') return out;",
|
|
344
|
+
" const toolName = pickEventString(value.tool, value.toolName, value.name).toLowerCase();",
|
|
345
|
+
" const input = value.input ?? value.state?.input ?? value.properties?.input;",
|
|
346
|
+
" if (toolName === 'skill' && input && typeof input === 'object' && !Array.isArray(input)) {",
|
|
347
|
+
" const skill = pickEventString(input.skill_name, input.skill, input.name);",
|
|
348
|
+
" const callId = pickEventString(value.callID, value.callId, value.id, value.toolCallID, value.toolCallId);",
|
|
349
|
+
" if (skill) out.push({ name: skill, skill_namespace: skillNamespace(skill), detected_by: 'tool_call', skill_call_id: callId });",
|
|
350
|
+
" }",
|
|
351
|
+
" for (const item of Object.values(value)) collectExplicitSkillUsages(item, out);",
|
|
352
|
+
" return out;",
|
|
353
|
+
"};",
|
|
354
|
+
"",
|
|
355
|
+
"const detectOpencodeSkillUsages = (source, knownSkills = []) => {",
|
|
333
356
|
" const skills = normalizeSkillNames(knownSkills);",
|
|
334
357
|
" if (skills.length === 0) return [];",
|
|
335
|
-
" const haystack = collectStrings(source).join('\\n');",
|
|
336
358
|
" const found = [];",
|
|
337
|
-
"
|
|
338
|
-
"
|
|
339
|
-
"
|
|
359
|
+
" const explicitSeen = new Set();",
|
|
360
|
+
" const explicit = [];",
|
|
361
|
+
" for (const usage of collectExplicitSkillUsages(source).filter((item) => skills.includes(item.name))) {",
|
|
362
|
+
" if (usage.skill_call_id) {",
|
|
363
|
+
" const key = `call:${usage.skill_call_id}`;",
|
|
364
|
+
" if (explicitSeen.has(key)) continue;",
|
|
365
|
+
" explicitSeen.add(key);",
|
|
366
|
+
" }",
|
|
367
|
+
" explicit.push(usage);",
|
|
368
|
+
" }",
|
|
369
|
+
" found.push(...explicit);",
|
|
370
|
+
" const haystack = collectStrings(source).join('\\n');",
|
|
371
|
+
" const pathSeen = new Set();",
|
|
372
|
+
" for (const match of haystack.matchAll(/(?:^|[\"'\\s])(?:[A-Za-z]:)?[^\"'\\n\\r]*[\\\\/]+([^\\\\/\"'\\n\\r]+)[\\\\/]+SKILL\\.md(?=$|[\"'\\s])/gi)) {",
|
|
373
|
+
" const skillName = match[1];",
|
|
374
|
+
" if (!skills.includes(skillName) || explicit.some((usage) => usage.name === skillName) || pathSeen.has(skillName)) continue;",
|
|
375
|
+
" pathSeen.add(skillName);",
|
|
376
|
+
" found.push({ name: skillName, skill_namespace: skillNamespace(skillName), detected_by: 'skill_file_path', skill_call_id: '' });",
|
|
340
377
|
" }",
|
|
341
378
|
" return found;",
|
|
342
379
|
"};",
|
|
380
|
+
"const uniqueSkillNames = (usages) => normalizeSkillNames((usages || []).map((usage) => usage.name));",
|
|
381
|
+
"const buildSkillUseEvents = (interactionId, skillUsages) => {",
|
|
382
|
+
" const total = skillUsages.length;",
|
|
383
|
+
" return skillUsages.map((usage, index) => {",
|
|
384
|
+
" const skillName = String(usage.name || '').trim();",
|
|
385
|
+
" const detectedBy = String(usage.detected_by || 'metadata');",
|
|
386
|
+
" return {",
|
|
387
|
+
" skill_use_id: `${interactionId}:skill:${index + 1}:${skillIdSegment(skillName)}`,",
|
|
388
|
+
" skill_use_index: index + 1,",
|
|
389
|
+
" skill_use_count_in_interaction: total,",
|
|
390
|
+
" skill_event_type: skillEventType(detectedBy),",
|
|
391
|
+
" skill_trigger: 'unknown',",
|
|
392
|
+
" skill_name: skillName,",
|
|
393
|
+
" skill_use_count: 1,",
|
|
394
|
+
" skill_namespace: usage.skill_namespace || skillNamespace(skillName),",
|
|
395
|
+
" detected_by: detectedBy,",
|
|
396
|
+
" ...(usage.skill_call_id ? { skill_call_id: usage.skill_call_id } : {}),",
|
|
397
|
+
" };",
|
|
398
|
+
" }).filter((event) => event.skill_name);",
|
|
399
|
+
"};",
|
|
343
400
|
"",
|
|
344
401
|
"const collectKnownSkillNames = async () => {",
|
|
345
402
|
" const dirs = [",
|
|
@@ -399,7 +456,7 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
399
456
|
" const metricsTracer = trace.getTracer('oh-langfuse-opencode-metrics');",
|
|
400
457
|
" const knownSkillNames = await collectKnownSkillNames();",
|
|
401
458
|
" const messageTextById = new Map();",
|
|
402
|
-
" const
|
|
459
|
+
" const skillUsagesByMessageId = new Map();",
|
|
403
460
|
" const toolCallIdsByMessageId = new Map();",
|
|
404
461
|
" const toolCallIdsBySessionId = new Map();",
|
|
405
462
|
" const toolResultIdsByMessageId = new Map();",
|
|
@@ -438,14 +495,20 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
438
495
|
" void shutdown('process.exit').finally(() => originalExit(code));",
|
|
439
496
|
" });",
|
|
440
497
|
"",
|
|
441
|
-
" const
|
|
442
|
-
" if (!messageId || !
|
|
443
|
-
" let
|
|
444
|
-
" if (!
|
|
445
|
-
"
|
|
446
|
-
"
|
|
498
|
+
" const rememberSkillUsages = (messageId, usages) => {",
|
|
499
|
+
" if (!messageId || !usages.length) return;",
|
|
500
|
+
" let list = skillUsagesByMessageId.get(messageId);",
|
|
501
|
+
" if (!list) {",
|
|
502
|
+
" list = [];",
|
|
503
|
+
" skillUsagesByMessageId.set(messageId, list);",
|
|
504
|
+
" }",
|
|
505
|
+
" const seen = new Set(list.map((usage) => usage.skill_call_id ? `call:${usage.skill_call_id}` : `${usage.name}:${usage.detected_by}`));",
|
|
506
|
+
" for (const usage of usages) {",
|
|
507
|
+
" const key = usage.skill_call_id ? `call:${usage.skill_call_id}` : `${usage.name}:${usage.detected_by}`;",
|
|
508
|
+
" if (seen.has(key)) continue;",
|
|
509
|
+
" seen.add(key);",
|
|
510
|
+
" list.push(usage);",
|
|
447
511
|
" }",
|
|
448
|
-
" for (const name of names) set.add(name);",
|
|
449
512
|
" };",
|
|
450
513
|
" const rememberToolActivity = (map, key, activity, kind) => {",
|
|
451
514
|
" if (!key || !activity?.[kind]) return;",
|
|
@@ -456,33 +519,15 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
456
519
|
" }",
|
|
457
520
|
" set.add(activity.toolCallId || `${kind}:${set.size + 1}`);",
|
|
458
521
|
" };",
|
|
459
|
-
" const emitSkillUseSpans = ({ skillNames, sessionId, messageId, interactionId }) => {",
|
|
460
|
-
" for (const skillName of skillNames.slice(0, MAX_SKILL_USE_SPANS_PER_INTERACTION)) {",
|
|
461
|
-
" const span = metricsTracer.startSpan('Skill Use');",
|
|
462
|
-
' span.setAttribute("oh.langfuse.source", "opencode");',
|
|
463
|
-
' span.setAttribute("oh.langfuse.user_id", userId || "");',
|
|
464
|
-
' span.setAttribute("oh.langfuse.metrics_schema_version", "1.0");',
|
|
465
|
-
' span.setAttribute("langfuse.observation.metadata.source", "opencode");',
|
|
466
|
-
' span.setAttribute("langfuse.observation.metadata.user_id", userId || "");',
|
|
467
|
-
' span.setAttribute("langfuse.observation.metadata.session_id", sessionId || "unknown");',
|
|
468
|
-
' span.setAttribute("langfuse.observation.metadata.message_id", messageId || "unknown");',
|
|
469
|
-
' span.setAttribute("langfuse.observation.metadata.interaction_id", interactionId);',
|
|
470
|
-
' span.setAttribute("langfuse.observation.metadata.metrics_schema_version", "1.0");',
|
|
471
|
-
' span.setAttribute("langfuse.observation.metadata.skill_name", skillName);',
|
|
472
|
-
' span.setAttribute("langfuse.observation.metadata.skill_use_count", 1);',
|
|
473
|
-
" span.end();",
|
|
474
|
-
" }",
|
|
475
|
-
" };",
|
|
476
|
-
"",
|
|
477
522
|
" const recordInteractionMetric = (event) => {",
|
|
478
523
|
" const payload = eventPayload(event);",
|
|
479
524
|
" const part = eventPart(event);",
|
|
480
525
|
" const partType = part?.type ?? '';",
|
|
481
526
|
" const sessionId = pickEventString(part?.sessionID, part?.sessionId, payload?.sessionID, payload?.sessionId, payload?.session?.id);",
|
|
482
527
|
" const messageId = pickEventString(part?.messageID, part?.messageId, payload?.messageID, payload?.messageId, payload?.message?.id);",
|
|
483
|
-
" const skillDetectionSources = [part?.state?.input, part?.input, payload?.input, part?.state?.metadata, payload?.metadata, part?.state?.file, part?.file];",
|
|
484
|
-
" const
|
|
485
|
-
"
|
|
528
|
+
" const skillDetectionSources = [part, payload, part?.state?.input, part?.input, payload?.input, part?.state?.metadata, payload?.metadata, part?.state?.file, part?.file];",
|
|
529
|
+
" const eventSkillUsages = detectOpencodeSkillUsages(skillDetectionSources, knownSkillNames);",
|
|
530
|
+
" rememberSkillUsages(messageId, eventSkillUsages);",
|
|
486
531
|
" const toolActivity = countOpencodeToolActivity(event);",
|
|
487
532
|
" rememberToolActivity(toolCallIdsByMessageId, messageId, toolActivity, 'toolCallCount');",
|
|
488
533
|
" rememberToolActivity(toolCallIdsBySessionId, sessionId, toolActivity, 'toolCallCount');",
|
|
@@ -490,7 +535,7 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
490
535
|
" rememberToolActivity(toolResultIdsBySessionId, sessionId, toolActivity, 'toolResultCount');",
|
|
491
536
|
" if (partType === 'text' && messageId && typeof part.text === 'string') {",
|
|
492
537
|
" messageTextById.set(messageId, part.text);",
|
|
493
|
-
"
|
|
538
|
+
" rememberSkillUsages(messageId, detectOpencodeSkillUsages(part.text, knownSkillNames));",
|
|
494
539
|
" return;",
|
|
495
540
|
" }",
|
|
496
541
|
" if (partType !== 'step-finish' || !messageId || emittedMessageIds.has(messageId)) return;",
|
|
@@ -498,30 +543,36 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
498
543
|
" const tokenMetrics = tokenMetricsFromPart(part);",
|
|
499
544
|
" const total = tokenMetrics.total ?? (tokenMetrics.input !== undefined && tokenMetrics.output !== undefined ? tokenMetrics.input + tokenMetrics.output : undefined);",
|
|
500
545
|
" const tokenAvailable = [tokenMetrics.input, tokenMetrics.output, total, tokenMetrics.cacheRead, tokenMetrics.reasoning].some((value) => value !== undefined);",
|
|
501
|
-
" const span = metricsTracer.startSpan('
|
|
546
|
+
" const span = metricsTracer.startSpan('Agent Turn');",
|
|
502
547
|
" const text = messageTextById.get(messageId) || '';",
|
|
503
|
-
" const
|
|
504
|
-
" const
|
|
505
|
-
" const
|
|
548
|
+
" const skillUsages = [...(skillUsagesByMessageId.get(messageId) ?? [])];",
|
|
549
|
+
" const interactionId = `opencode:${userId || \"unknown\"}:${sessionId || \"unknown\"}:${messageId}`;",
|
|
550
|
+
" const skillUseEvents = buildSkillUseEvents(interactionId, skillUsages);",
|
|
551
|
+
" const skillNames = uniqueSkillNames(skillUsages);",
|
|
552
|
+
" const skillNamesAll = skillUseEvents.map((event) => event.skill_name);",
|
|
553
|
+
" const toolCallCount = Math.max(new Set([...(toolCallIdsByMessageId.get(messageId) ?? []), ...(toolCallIdsBySessionId.get(sessionId) ?? [])]).size, skillUseEvents.length);",
|
|
554
|
+
" const toolResultCount = Math.max(new Set([...(toolResultIdsByMessageId.get(messageId) ?? []), ...(toolResultIdsBySessionId.get(sessionId) ?? [])]).size, skillUseEvents.length);",
|
|
506
555
|
' span.setAttribute("oh.langfuse.source", "opencode");',
|
|
507
556
|
' span.setAttribute("oh.langfuse.user_id", userId || "");',
|
|
508
|
-
' span.setAttribute("oh.langfuse.metrics_schema_version", "1.
|
|
557
|
+
' span.setAttribute("oh.langfuse.metrics_schema_version", "1.1");',
|
|
509
558
|
' span.setAttribute("langfuse.observation.metadata.source", "opencode");',
|
|
559
|
+
' span.setAttribute("langfuse.observation.metadata.agent", "opencode");',
|
|
510
560
|
' span.setAttribute("langfuse.observation.metadata.user_id", userId || "");',
|
|
511
561
|
' span.setAttribute("langfuse.observation.metadata.session_id", sessionId || "unknown");',
|
|
512
|
-
' const interactionId = `opencode:${userId || "unknown"}:${sessionId || "unknown"}:${messageId}`;',
|
|
513
562
|
' span.setAttribute("langfuse.observation.metadata.interaction_id", interactionId);',
|
|
514
|
-
' span.setAttribute("langfuse.observation.metadata.metrics_schema_version", "1.
|
|
563
|
+
' span.setAttribute("langfuse.observation.metadata.metrics_schema_version", "1.1");',
|
|
515
564
|
' span.setAttribute("langfuse.observation.metadata.interaction_count", 1);',
|
|
516
565
|
' span.setAttribute("langfuse.observation.metadata.user_message_count", 1);',
|
|
517
566
|
' span.setAttribute("langfuse.observation.metadata.assistant_message_count", 1);',
|
|
518
567
|
' span.setAttribute("langfuse.observation.metadata.token_metrics_available", tokenAvailable);',
|
|
519
568
|
' span.setAttribute("langfuse.observation.metadata.tool_call_count", toolCallCount);',
|
|
520
569
|
' span.setAttribute("langfuse.observation.metadata.tool_result_count", toolResultCount);',
|
|
521
|
-
' span.setAttribute("langfuse.observation.metadata.skill_use_count",
|
|
570
|
+
' span.setAttribute("langfuse.observation.metadata.skill_use_count", skillUseEvents.length);',
|
|
571
|
+
' span.setAttribute("langfuse.observation.metadata.unique_skill_count", skillNames.length);',
|
|
572
|
+
' span.setAttribute("langfuse.observation.metadata.repeated_skill_count", Math.max(0, skillUseEvents.length - skillNames.length));',
|
|
522
573
|
' if (skillNames.length) span.setAttribute("langfuse.observation.metadata.skill_names", skillNames);',
|
|
523
|
-
' if (skillNames.length) span.setAttribute("langfuse.observation.metadata.skill_names_json", JSON.stringify(skillNames));',
|
|
524
574
|
' if (skillNames.length) span.setAttribute("langfuse.observation.metadata.skill_names_csv", skillNames.join(","));',
|
|
575
|
+
' if (skillNamesAll.length) span.setAttribute("langfuse.observation.metadata.skill_names_all", skillNamesAll);',
|
|
525
576
|
' if (tokenMetrics.input !== undefined) span.setAttribute("langfuse.observation.metadata.input_tokens", tokenMetrics.input);',
|
|
526
577
|
' if (tokenMetrics.output !== undefined) span.setAttribute("langfuse.observation.metadata.output_tokens", tokenMetrics.output);',
|
|
527
578
|
' if (total !== undefined) span.setAttribute("langfuse.observation.metadata.total_tokens", total);',
|
|
@@ -529,9 +580,8 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
529
580
|
' if (tokenMetrics.reasoning !== undefined) span.setAttribute("langfuse.observation.metadata.reasoning_tokens", tokenMetrics.reasoning);',
|
|
530
581
|
' if (text) span.setAttribute("langfuse.observation.metadata.output_text_preview", text.slice(0, 512));',
|
|
531
582
|
" span.end();",
|
|
532
|
-
" emitSkillUseSpans({ skillNames, sessionId, messageId, interactionId });",
|
|
533
583
|
" messageTextById.delete(messageId);",
|
|
534
|
-
"
|
|
584
|
+
" skillUsagesByMessageId.delete(messageId);",
|
|
535
585
|
" toolCallIdsByMessageId.delete(messageId);",
|
|
536
586
|
" toolCallIdsBySessionId.delete(sessionId);",
|
|
537
587
|
" toolResultIdsByMessageId.delete(messageId);",
|
|
@@ -597,6 +647,18 @@ function shQuote(s) {
|
|
|
597
647
|
return `'${String(s).replace(/'/g, "'\"'\"'")}'`;
|
|
598
648
|
}
|
|
599
649
|
|
|
650
|
+
function autoUpdateRuntimePath() {
|
|
651
|
+
return path.join(rootDir, "scripts", "auto-update-runtime.mjs");
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function windowsAutoUpdateCommand(target) {
|
|
655
|
+
return `call ${cmdQuote(process.execPath)} ${cmdQuote(autoUpdateRuntimePath())} ${target} --skip-check`;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function unixAutoUpdateCommand(target) {
|
|
659
|
+
return `${shQuote(process.execPath)} ${shQuote(autoUpdateRuntimePath())} ${target} --skip-check || true`;
|
|
660
|
+
}
|
|
661
|
+
|
|
600
662
|
function writeWindowsUpdateCheckScript(dir) {
|
|
601
663
|
if (process.platform !== "win32") return null;
|
|
602
664
|
ensureDir(dir);
|
|
@@ -624,14 +686,13 @@ function writeOpencodeCommandShim(opencodeDir, { publicKey, secretKey, baseUrl,
|
|
|
624
686
|
ensureDir(shimDir);
|
|
625
687
|
if (process.platform === "win32") {
|
|
626
688
|
const shim = path.join(shimDir, "opencode.cmd");
|
|
627
|
-
writeWindowsUpdateCheckScript(shimDir);
|
|
628
689
|
const cmd = ["@echo off", "REM Auto-generated by scripts/opencode-langfuse-setup.mjs"];
|
|
629
690
|
cmd.push("set OH_LANGFUSE_OPENCODE_SHIM=1");
|
|
630
691
|
cmd.push(`set LANGFUSE_PUBLIC_KEY=${publicKey}`);
|
|
631
692
|
cmd.push(`set LANGFUSE_SECRET_KEY=${secretKey}`);
|
|
632
693
|
cmd.push(`set LANGFUSE_BASEURL=${baseUrl}`);
|
|
633
694
|
if (userId) cmd.push(`set LANGFUSE_USER_ID=${userId}`);
|
|
634
|
-
cmd.push(
|
|
695
|
+
cmd.push(windowsAutoUpdateCommand("opencode"));
|
|
635
696
|
cmd.push(`call ${cmdQuote(realOpencodeCli)} %*`);
|
|
636
697
|
cmd.push("exit /b %ERRORLEVEL%");
|
|
637
698
|
fs.writeFileSync(shim, cmd.join("\r\n") + "\r\n", "utf8");
|
|
@@ -648,9 +709,7 @@ function writeOpencodeCommandShim(opencodeDir, { publicKey, secretKey, baseUrl,
|
|
|
648
709
|
`export LANGFUSE_SECRET_KEY=${shQuote(secretKey)}`,
|
|
649
710
|
`export LANGFUSE_BASEURL=${shQuote(baseUrl)}`,
|
|
650
711
|
userId ? `export LANGFUSE_USER_ID=${shQuote(userId)}` : null,
|
|
651
|
-
|
|
652
|
-
' npx -y oh-langfuse@latest auto-update opencode --skip-check --notify-only || true',
|
|
653
|
-
"fi",
|
|
712
|
+
unixAutoUpdateCommand("opencode"),
|
|
654
713
|
`exec ${shQuote(realOpencodeCli)} "$@"`,
|
|
655
714
|
""
|
|
656
715
|
].filter(Boolean);
|
|
@@ -670,9 +729,7 @@ function writeUnixLauncherSh(opencodeDir, { publicKey, secretKey, baseUrl, userI
|
|
|
670
729
|
`export LANGFUSE_SECRET_KEY=${shQuote(secretKey)}`,
|
|
671
730
|
`export LANGFUSE_BASEURL=${shQuote(baseUrl)}`,
|
|
672
731
|
userId ? `export LANGFUSE_USER_ID=${shQuote(userId)}` : null,
|
|
673
|
-
|
|
674
|
-
' npx -y oh-langfuse@latest auto-update opencode --skip-check --notify-only || true',
|
|
675
|
-
"fi",
|
|
732
|
+
unixAutoUpdateCommand("opencode"),
|
|
676
733
|
'if [ -x "$HOME/.opencode/bin/opencode" ]; then',
|
|
677
734
|
' exec "$HOME/.opencode/bin/opencode" "$@"',
|
|
678
735
|
"fi",
|
|
@@ -214,7 +214,7 @@ function triggerOpenCode(prompt, args, env) {
|
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
function triggerCodex(prompt, args, env) {
|
|
217
|
-
const cmd = String(args.codexCmd || findOnPath(process.platform === "win32" ? ["codex.cmd", "codex"] : ["codex"]));
|
|
217
|
+
const cmd = String(args.codexCmd || findOnPath(process.platform === "win32" ? ["codex.cmd", "codex.exe", "codex"] : ["codex"]));
|
|
218
218
|
if (!cmd) return { status: 127, stdout: "", stderr: "Codex CLI not found. Set --codexCmd=<path>.", command: "codex", args: [] };
|
|
219
219
|
const candidates = [
|
|
220
220
|
["exec", prompt],
|
|
@@ -339,8 +339,8 @@ function hasMetadataKey(item, key) {
|
|
|
339
339
|
return metadataValue(item, key) !== undefined;
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
-
function
|
|
343
|
-
if (item?.name === "
|
|
342
|
+
function isAgentTurnObservation(item) {
|
|
343
|
+
if (item?.name === "Agent Turn" || metadataValue(item, "interaction_count") === 1 || metadataValue(item, "interaction_count") === "1") {
|
|
344
344
|
return true;
|
|
345
345
|
}
|
|
346
346
|
const metadata = item?.metadata || {};
|
|
@@ -371,9 +371,14 @@ async function verifyMetricObservations(config, found, { since, target }) {
|
|
|
371
371
|
observations = found.item.observations.filter((item) => typeof item === "object");
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
-
const
|
|
374
|
+
const skillUseObservations = observations.filter((item) => item?.name === "Skill Use");
|
|
375
|
+
if (skillUseObservations.length) {
|
|
376
|
+
throw new Error(`Metric verification failed for ${target}: Skill Use observations should not be emitted anymore, found ${skillUseObservations.length}.`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const interactions = observations.filter(isAgentTurnObservation);
|
|
375
380
|
if (!interactions.length) {
|
|
376
|
-
throw new Error(`Metric verification failed for ${target}:
|
|
381
|
+
throw new Error(`Metric verification failed for ${target}: Agent Turn observation was not found for trace ${traceId || found.id}.`);
|
|
377
382
|
}
|
|
378
383
|
|
|
379
384
|
const byInteractionId = new Map();
|
|
@@ -382,12 +387,12 @@ async function verifyMetricObservations(config, found, { since, target }) {
|
|
|
382
387
|
? directMetadataValue(item, "interaction_id") || metadataValue(item, "interaction_id")
|
|
383
388
|
: metadataValue(item, "interaction_id");
|
|
384
389
|
if (!interactionId) {
|
|
385
|
-
throw new Error(`Metric verification failed for ${target}:
|
|
390
|
+
throw new Error(`Metric verification failed for ${target}: Agent Turn is missing interaction_id.`);
|
|
386
391
|
}
|
|
387
392
|
byInteractionId.set(interactionId, (byInteractionId.get(interactionId) || 0) + 1);
|
|
388
393
|
for (const key of ["user_id", "token_metrics_available", "tool_call_count", "skill_use_count", "metrics_schema_version"]) {
|
|
389
394
|
if (!hasMetadataKey(item, key)) {
|
|
390
|
-
throw new Error(`Metric verification failed for ${target}:
|
|
395
|
+
throw new Error(`Metric verification failed for ${target}: Agent Turn is missing ${key}.`);
|
|
391
396
|
}
|
|
392
397
|
}
|
|
393
398
|
if (target === "opencode") {
|
|
@@ -554,7 +559,7 @@ async function main() {
|
|
|
554
559
|
const found = await pollLangfuse(config, marker, { ...args, since, target });
|
|
555
560
|
console.log(`[OK] Langfuse marker found for ${target}: ${found.kind} ${found.id || ""}`.trim());
|
|
556
561
|
const metrics = await verifyMetricObservations(config, found, { since, target });
|
|
557
|
-
console.log(`[OK] Langfuse metrics found for ${target}:
|
|
562
|
+
console.log(`[OK] Langfuse metrics found for ${target}: Agent Turn x${metrics.interactionCount}`);
|
|
558
563
|
results.push({ target, marker, langfuse: { kind: found.kind, id: found.id || "", traceId: metrics.traceId }, metrics });
|
|
559
564
|
}
|
|
560
565
|
|