oh-langfuse 0.1.46 → 0.1.48
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/codex_langfuse_notify.py +106 -85
- package/langfuse_hook.py +36 -3
- package/package.json +1 -1
- package/scripts/codex-langfuse-setup.mjs +11 -1
- package/scripts/metrics-utils.mjs +54 -4
- package/scripts/opencode-langfuse-setup.mjs +20 -2
package/codex_langfuse_notify.py
CHANGED
|
@@ -61,7 +61,6 @@ LOG_FILE = STATE_DIR / "codex_langfuse_notify.log"
|
|
|
61
61
|
|
|
62
62
|
DEBUG = os.environ.get("CODEX_LANGFUSE_DEBUG", "").lower() == "true"
|
|
63
63
|
MAX_CHARS = int(os.environ.get("CODEX_LANGFUSE_MAX_CHARS", "20000"))
|
|
64
|
-
MAX_SKILL_SCAN_CHARS = int(os.environ.get("CODEX_LANGFUSE_SKILL_SCAN_MAX_CHARS", "200000"))
|
|
65
64
|
METRICS_SCHEMA_VERSION = "1.1"
|
|
66
65
|
AGENT_NAME = "codex"
|
|
67
66
|
|
|
@@ -425,6 +424,8 @@ def build_interaction_metadata(
|
|
|
425
424
|
events = list(skill_use_events or [])
|
|
426
425
|
skill_names_all = [str(event.get("skill_name") or "") for event in events if event.get("skill_name")]
|
|
427
426
|
unique_skill_names = list(dict.fromkeys(skill_names_all))
|
|
427
|
+
skill_invocation_modes = [str(event.get("skill_invocation_mode") or "") for event in events if event.get("skill_invocation_mode")]
|
|
428
|
+
skill_agent_paths = [str(event.get("skill_agent_path") or "") for event in events if event.get("skill_agent_path")]
|
|
428
429
|
effective_skill_count = len(events) if events else int(skill_use_count or 0)
|
|
429
430
|
return {
|
|
430
431
|
"source": source,
|
|
@@ -454,6 +455,8 @@ def build_interaction_metadata(
|
|
|
454
455
|
**({
|
|
455
456
|
"skill_names": unique_skill_names,
|
|
456
457
|
"skill_names_all": skill_names_all,
|
|
458
|
+
"skill_invocation_modes": skill_invocation_modes,
|
|
459
|
+
"skill_agent_paths": skill_agent_paths,
|
|
457
460
|
} if events else {}),
|
|
458
461
|
}
|
|
459
462
|
|
|
@@ -483,8 +486,37 @@ def _skill_namespace(name: str) -> str:
|
|
|
483
486
|
return name.split(":", 1)[0] if ":" in name else ""
|
|
484
487
|
|
|
485
488
|
|
|
486
|
-
def
|
|
487
|
-
return
|
|
489
|
+
def _skill_agent_from_interaction_id(interaction_id: str) -> str:
|
|
490
|
+
return str(interaction_id or "unknown").split(":", 1)[0] or "unknown"
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _skill_agent_path(agent: str, detected_by: str) -> str:
|
|
494
|
+
if agent == "codex":
|
|
495
|
+
if detected_by == "codex_explicit_injection":
|
|
496
|
+
return "codex_native_skill_injection"
|
|
497
|
+
if detected_by == "codex_implicit_script":
|
|
498
|
+
return "codex_skill_script_exec"
|
|
499
|
+
if detected_by == "codex_implicit_doc_read":
|
|
500
|
+
return "codex_skill_doc_read"
|
|
501
|
+
if detected_by == "tool_call":
|
|
502
|
+
return "codex_unsupported_skill_tool"
|
|
503
|
+
if detected_by == "skill_file_path":
|
|
504
|
+
return "skill_file_path"
|
|
505
|
+
return detected_by or "metadata"
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _skill_invocation_mode(agent: str, detected_by: str) -> str:
|
|
509
|
+
if agent == "codex" and detected_by == "codex_explicit_injection":
|
|
510
|
+
return "explicit"
|
|
511
|
+
if agent == "codex" and detected_by in ("codex_implicit_script", "codex_implicit_doc_read"):
|
|
512
|
+
return "implicit"
|
|
513
|
+
return "detected"
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _skill_event_type(detected_by: str, agent: str = "unknown") -> str:
|
|
517
|
+
if agent == "codex" and detected_by == "tool_call":
|
|
518
|
+
return "detected"
|
|
519
|
+
return "invoked" if detected_by in ("codex_explicit_injection", "codex_implicit_script", "codex_implicit_doc_read") else "detected"
|
|
488
520
|
|
|
489
521
|
|
|
490
522
|
def _skill_id_segment(name: str) -> str:
|
|
@@ -502,85 +534,78 @@ def _skill_usage(name: str, detected_by: str, skill_call_id: str = "") -> Dict[s
|
|
|
502
534
|
}
|
|
503
535
|
|
|
504
536
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
537
|
+
_CODEX_SCRIPT_RUNNERS = {"python", "python3", "bash", "zsh", "sh", "node", "deno", "ruby", "perl", "pwsh"}
|
|
538
|
+
_CODEX_DOC_READERS = {"cat", "sed", "head", "tail", "less", "more", "bat", "awk"}
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _command_from_tool_input(input_obj: Any) -> str:
|
|
542
|
+
if isinstance(input_obj, dict):
|
|
543
|
+
command = input_obj.get("command")
|
|
544
|
+
if isinstance(command, str) and command.strip():
|
|
545
|
+
return command.strip()
|
|
546
|
+
argv = input_obj.get("cmd") or input_obj.get("argv")
|
|
547
|
+
if isinstance(argv, list):
|
|
548
|
+
return " ".join(str(item) for item in argv)
|
|
549
|
+
if isinstance(input_obj, str):
|
|
550
|
+
return input_obj.strip()
|
|
511
551
|
return ""
|
|
512
552
|
|
|
513
553
|
|
|
514
|
-
def
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
if isinstance(value, list):
|
|
526
|
-
for item in value:
|
|
527
|
-
_collect_strings_limited(item, out, remaining)
|
|
528
|
-
if remaining[0] <= 0:
|
|
529
|
-
break
|
|
530
|
-
return
|
|
531
|
-
if isinstance(value, dict):
|
|
532
|
-
for item in value.values():
|
|
533
|
-
_collect_strings_limited(item, out, remaining)
|
|
534
|
-
if remaining[0] <= 0:
|
|
535
|
-
break
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
def _detect_skill_usages_from_text(text: str, known_skills: set) -> List[Dict[str, str]]:
|
|
554
|
+
def _first_command_token(command: str) -> str:
|
|
555
|
+
match = re.match(r"\s*(?:[A-Za-z]:)?[^\"'\s]*?([^\\/\"'\s]+)(?:\.(?:exe|cmd))?(?=\s|$)", command, re.IGNORECASE)
|
|
556
|
+
return match.group(1).lower() if match else ""
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _accept_known_skill(name: str, known_skills: set) -> str:
|
|
560
|
+
clean = str(name or "").strip()
|
|
561
|
+
return clean if clean and (clean in known_skills or not known_skills) else ""
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def _detect_codex_skill_command(command: str, known_skills: set) -> List[Dict[str, str]]:
|
|
539
565
|
found: List[Dict[str, str]] = []
|
|
540
|
-
if not
|
|
566
|
+
if not command:
|
|
541
567
|
return found
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
568
|
+
|
|
569
|
+
first = _first_command_token(command)
|
|
570
|
+
if first in _CODEX_SCRIPT_RUNNERS:
|
|
571
|
+
script_pattern = r"[\\/](?:skills|skill)[\\/]([^\\/\"'\s]+)[\\/]scripts[\\/][^\"'\s]+\.(?:py|sh|js|ts|rb|pl|ps1)(?=$|[\"'\s])"
|
|
572
|
+
for match in re.finditer(script_pattern, command, re.IGNORECASE):
|
|
573
|
+
name = _accept_known_skill(match.group(1), known_skills)
|
|
574
|
+
if name:
|
|
575
|
+
found.append(_skill_usage(name, "codex_implicit_script"))
|
|
576
|
+
|
|
577
|
+
if first in _CODEX_DOC_READERS:
|
|
578
|
+
doc_pattern = r"[\\/](?:skills|skill)[\\/]([^\\/\"'\s]+)[\\/]SKILL\.md(?=$|[\"'\s])"
|
|
579
|
+
for match in re.finditer(doc_pattern, command, re.IGNORECASE):
|
|
580
|
+
name = _accept_known_skill(match.group(1), known_skills)
|
|
581
|
+
if name:
|
|
582
|
+
found.append(_skill_usage(name, "codex_implicit_doc_read"))
|
|
583
|
+
|
|
557
584
|
return found
|
|
558
585
|
|
|
559
586
|
|
|
560
587
|
def detect_skill_usages(tool_calls: List[Dict[str, Any]], known_skills: set) -> List[Dict[str, str]]:
|
|
561
588
|
found: List[Dict[str, str]] = []
|
|
562
|
-
seen_call_ids: set = set()
|
|
563
589
|
for call in tool_calls or []:
|
|
564
|
-
tool_name = str(call.get("name") or "")
|
|
565
|
-
call_id = str(call.get("id") or call.get("call_id") or call.get("callId") or call.get("tool_call_id") or call.get("toolCallId") or "").strip()
|
|
566
590
|
input_obj = call.get("input") if isinstance(call.get("input"), (dict, list, str)) else {}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
591
|
+
found.extend(_detect_codex_skill_command(_command_from_tool_input(input_obj), known_skills))
|
|
592
|
+
return found
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _detect_codex_explicit_injections(material: Dict[str, Any], known_skills: set) -> List[Dict[str, str]]:
|
|
596
|
+
found: List[Dict[str, str]] = []
|
|
597
|
+
sources = [material.get("user_text"), *(material.get("skill_detection_sources") or [])]
|
|
598
|
+
for source in sources:
|
|
599
|
+
text = extract_text(source) if not isinstance(source, str) else source
|
|
600
|
+
if not text:
|
|
601
|
+
try:
|
|
602
|
+
text = json.dumps(source, ensure_ascii=False)
|
|
603
|
+
except Exception:
|
|
604
|
+
text = str(source)
|
|
605
|
+
for match in re.finditer(r"<skill>\s*<name>\s*([^<\s]+)\s*</name>.*?</skill>", text, re.IGNORECASE | re.DOTALL):
|
|
606
|
+
name = _accept_known_skill(match.group(1), known_skills)
|
|
607
|
+
if name:
|
|
608
|
+
found.append(_skill_usage(name, "codex_explicit_injection"))
|
|
584
609
|
return found
|
|
585
610
|
|
|
586
611
|
|
|
@@ -611,19 +636,8 @@ def _dedupe_turn_skill_usages(usages: List[Dict[str, str]]) -> List[Dict[str, st
|
|
|
611
636
|
|
|
612
637
|
|
|
613
638
|
def detect_turn_skill_usages(material: Dict[str, Any], known_skills: set) -> List[Dict[str, str]]:
|
|
614
|
-
found = list(
|
|
615
|
-
|
|
616
|
-
material.get("user_text"),
|
|
617
|
-
material.get("assistant_text"),
|
|
618
|
-
material.get("skill_detection_sources"),
|
|
619
|
-
]
|
|
620
|
-
strings: List[str] = []
|
|
621
|
-
remaining = [max(0, MAX_SKILL_SCAN_CHARS)]
|
|
622
|
-
for source in sources:
|
|
623
|
-
_collect_strings_limited(source, strings, remaining)
|
|
624
|
-
if remaining[0] <= 0:
|
|
625
|
-
break
|
|
626
|
-
found.extend(_detect_skill_usages_from_text("\n".join(strings), known_skills))
|
|
639
|
+
found = list(_detect_codex_explicit_injections(material, known_skills))
|
|
640
|
+
found.extend(detect_skill_usages(material.get("tool_calls") or [], known_skills))
|
|
627
641
|
return _dedupe_turn_skill_usages(found)
|
|
628
642
|
|
|
629
643
|
|
|
@@ -631,6 +645,7 @@ def build_skill_use_events(interaction_id: str, skill_usages: List[Dict[str, str
|
|
|
631
645
|
events: List[Dict[str, Any]] = []
|
|
632
646
|
deduped: List[Dict[str, str]] = []
|
|
633
647
|
seen_call_ids: set = set()
|
|
648
|
+
agent = _skill_agent_from_interaction_id(interaction_id)
|
|
634
649
|
for skill in skill_usages or []:
|
|
635
650
|
call_id = str(skill.get("skill_call_id") or "").strip()
|
|
636
651
|
if call_id:
|
|
@@ -646,12 +661,15 @@ def build_skill_use_events(interaction_id: str, skill_usages: List[Dict[str, str
|
|
|
646
661
|
continue
|
|
647
662
|
detected_by = str(skill.get("detected_by") or "metadata")
|
|
648
663
|
call_id = str(skill.get("skill_call_id") or "").strip()
|
|
664
|
+
invocation_mode = _skill_invocation_mode(agent, detected_by)
|
|
649
665
|
events.append({
|
|
650
666
|
"skill_use_id": f"{interaction_id}:skill:{index}:{_skill_id_segment(name)}",
|
|
651
667
|
"skill_use_index": index,
|
|
652
668
|
"skill_use_count_in_interaction": total,
|
|
653
|
-
"skill_event_type": _skill_event_type(detected_by),
|
|
654
|
-
"skill_trigger":
|
|
669
|
+
"skill_event_type": _skill_event_type(detected_by, agent),
|
|
670
|
+
"skill_trigger": invocation_mode,
|
|
671
|
+
"skill_invocation_mode": invocation_mode,
|
|
672
|
+
"skill_agent_path": _skill_agent_path(agent, detected_by),
|
|
655
673
|
"skill_name": name,
|
|
656
674
|
"skill_use_count": 1,
|
|
657
675
|
"skill_namespace": skill.get("skill_namespace") or _skill_namespace(name),
|
|
@@ -669,6 +687,9 @@ def summarize_skill_usages(skill_usages: List[Dict[str, str]]) -> List[Dict[str,
|
|
|
669
687
|
continue
|
|
670
688
|
entry = summary.setdefault(name, {"name": name, "count": 0, "detected_by": item.get("detected_by")})
|
|
671
689
|
entry["count"] += 1
|
|
690
|
+
detected_by = str(item.get("detected_by") or "metadata")
|
|
691
|
+
entry.setdefault("skill_invocation_mode", _skill_invocation_mode("codex", detected_by))
|
|
692
|
+
entry.setdefault("skill_agent_path", _skill_agent_path("codex", detected_by))
|
|
672
693
|
return list(summary.values())
|
|
673
694
|
|
|
674
695
|
|
package/langfuse_hook.py
CHANGED
|
@@ -354,6 +354,8 @@ def build_interaction_metadata(
|
|
|
354
354
|
events = list(skill_use_events or [])
|
|
355
355
|
skill_names_all = [str(event.get("skill_name") or "") for event in events if event.get("skill_name")]
|
|
356
356
|
unique_skill_names = list(dict.fromkeys(skill_names_all))
|
|
357
|
+
skill_invocation_modes = [str(event.get("skill_invocation_mode") or "") for event in events if event.get("skill_invocation_mode")]
|
|
358
|
+
skill_agent_paths = [str(event.get("skill_agent_path") or "") for event in events if event.get("skill_agent_path")]
|
|
357
359
|
effective_skill_count = len(events) if events else int(skill_use_count or 0)
|
|
358
360
|
return {
|
|
359
361
|
"source": source,
|
|
@@ -383,6 +385,8 @@ def build_interaction_metadata(
|
|
|
383
385
|
**({
|
|
384
386
|
"skill_names": unique_skill_names,
|
|
385
387
|
"skill_names_all": skill_names_all,
|
|
388
|
+
"skill_invocation_modes": skill_invocation_modes,
|
|
389
|
+
"skill_agent_paths": skill_agent_paths,
|
|
386
390
|
} if events else {}),
|
|
387
391
|
}
|
|
388
392
|
|
|
@@ -408,7 +412,29 @@ def discover_known_skills(extra_roots: Optional[List[Path]] = None) -> set:
|
|
|
408
412
|
def _skill_namespace(name: str) -> str:
|
|
409
413
|
return name.split(":", 1)[0] if ":" in name else ""
|
|
410
414
|
|
|
411
|
-
def
|
|
415
|
+
def _skill_agent_from_interaction_id(interaction_id: str) -> str:
|
|
416
|
+
return str(interaction_id or "unknown").split(":", 1)[0] or "unknown"
|
|
417
|
+
|
|
418
|
+
def _skill_agent_path(agent: str, detected_by: str) -> str:
|
|
419
|
+
if agent == "claude":
|
|
420
|
+
if detected_by == "tool_call":
|
|
421
|
+
return "claude_skill_tool"
|
|
422
|
+
if detected_by == "slash_command":
|
|
423
|
+
return "claude_slash_skill"
|
|
424
|
+
if detected_by == "attribution_skill":
|
|
425
|
+
return "claude_attribution_skill"
|
|
426
|
+
if detected_by == "skill_file_path":
|
|
427
|
+
return "skill_file_path"
|
|
428
|
+
return detected_by or "metadata"
|
|
429
|
+
|
|
430
|
+
def _skill_invocation_mode(agent: str, detected_by: str) -> str:
|
|
431
|
+
if detected_by in ("slash_command", "attribution_skill"):
|
|
432
|
+
return "explicit"
|
|
433
|
+
if detected_by in ("tool_call", "plugin_event"):
|
|
434
|
+
return "implicit"
|
|
435
|
+
return "detected"
|
|
436
|
+
|
|
437
|
+
def _skill_event_type(detected_by: str, agent: str = "unknown") -> str:
|
|
412
438
|
return "invoked" if detected_by in ("tool_call", "plugin_event", "attribution_skill", "slash_command") else "detected"
|
|
413
439
|
|
|
414
440
|
def _skill_id_segment(name: str) -> str:
|
|
@@ -540,6 +566,7 @@ def build_skill_use_events(interaction_id: str, skill_usages: List[Dict[str, str
|
|
|
540
566
|
events: List[Dict[str, Any]] = []
|
|
541
567
|
deduped: List[Dict[str, str]] = []
|
|
542
568
|
seen_call_ids: set = set()
|
|
569
|
+
agent = _skill_agent_from_interaction_id(interaction_id)
|
|
543
570
|
for skill in skill_usages or []:
|
|
544
571
|
call_id = str(skill.get("skill_call_id") or "").strip()
|
|
545
572
|
if call_id:
|
|
@@ -555,12 +582,15 @@ def build_skill_use_events(interaction_id: str, skill_usages: List[Dict[str, str
|
|
|
555
582
|
continue
|
|
556
583
|
detected_by = str(skill.get("detected_by") or "metadata")
|
|
557
584
|
call_id = str(skill.get("skill_call_id") or "").strip()
|
|
585
|
+
invocation_mode = _skill_invocation_mode(agent, detected_by)
|
|
558
586
|
events.append({
|
|
559
587
|
"skill_use_id": f"{interaction_id}:skill:{index}:{_skill_id_segment(name)}",
|
|
560
588
|
"skill_use_index": index,
|
|
561
589
|
"skill_use_count_in_interaction": total,
|
|
562
|
-
"skill_event_type": _skill_event_type(detected_by),
|
|
563
|
-
"skill_trigger":
|
|
590
|
+
"skill_event_type": _skill_event_type(detected_by, agent),
|
|
591
|
+
"skill_trigger": invocation_mode,
|
|
592
|
+
"skill_invocation_mode": invocation_mode,
|
|
593
|
+
"skill_agent_path": _skill_agent_path(agent, detected_by),
|
|
564
594
|
"skill_name": name,
|
|
565
595
|
"skill_use_count": 1,
|
|
566
596
|
"skill_namespace": skill.get("skill_namespace") or _skill_namespace(name),
|
|
@@ -577,6 +607,9 @@ def summarize_skill_usages(skill_usages: List[Dict[str, str]]) -> List[Dict[str,
|
|
|
577
607
|
continue
|
|
578
608
|
entry = summary.setdefault(name, {"name": name, "count": 0, "detected_by": item.get("detected_by")})
|
|
579
609
|
entry["count"] += 1
|
|
610
|
+
detected_by = str(item.get("detected_by") or "metadata")
|
|
611
|
+
entry.setdefault("skill_invocation_mode", _skill_invocation_mode("claude", detected_by))
|
|
612
|
+
entry.setdefault("skill_agent_path", _skill_agent_path("claude", detected_by))
|
|
580
613
|
return list(summary.values())
|
|
581
614
|
|
|
582
615
|
def get_model(msg: Dict[str, Any]) -> str:
|
package/package.json
CHANGED
|
@@ -109,10 +109,20 @@ function listCliCandidatesFromPath(target) {
|
|
|
109
109
|
const args = process.platform === "win32" ? [target] : ["-a", target];
|
|
110
110
|
const result = spawnSync(cmd, args, { encoding: "utf8", windowsHide: true });
|
|
111
111
|
if (result.status !== 0) return [];
|
|
112
|
-
|
|
112
|
+
const candidates = String(result.stdout || "")
|
|
113
113
|
.split(/\r?\n/)
|
|
114
114
|
.map((line) => line.trim())
|
|
115
115
|
.filter(Boolean);
|
|
116
|
+
return process.platform === "win32" ? sortWindowsCliCandidates(candidates) : candidates;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function sortWindowsCliCandidates(candidates) {
|
|
120
|
+
const extPriority = (candidate) => {
|
|
121
|
+
const ext = path.extname(String(candidate || "")).toLowerCase();
|
|
122
|
+
if (ext === ".cmd" || ext === ".bat" || ext === ".exe") return 0;
|
|
123
|
+
return 1;
|
|
124
|
+
};
|
|
125
|
+
return [...candidates].sort((a, b) => extPriority(a) - extPriority(b));
|
|
116
126
|
}
|
|
117
127
|
|
|
118
128
|
function resolveAgentCli({ target, preferred = "", shimDir = "" }) {
|
|
@@ -68,8 +68,46 @@ function skillNamespace(name) {
|
|
|
68
68
|
return String(name || "").includes(":") ? String(name).split(":", 1)[0] : "";
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
function
|
|
72
|
-
return
|
|
71
|
+
function agentFromInteractionId(interactionId) {
|
|
72
|
+
return String(interactionId || "").split(":", 1)[0] || "unknown";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function skillAgentPath(agent, detectedBy) {
|
|
76
|
+
if (agent === "claude") {
|
|
77
|
+
if (detectedBy === "tool_call") return "claude_skill_tool";
|
|
78
|
+
if (detectedBy === "slash_command") return "claude_slash_skill";
|
|
79
|
+
if (detectedBy === "attribution_skill") return "claude_attribution_skill";
|
|
80
|
+
}
|
|
81
|
+
if (agent === "opencode") {
|
|
82
|
+
if (detectedBy === "tool_call") return "opencode_skill_tool";
|
|
83
|
+
if (detectedBy === "slash_command") return "opencode_slash_prompt";
|
|
84
|
+
}
|
|
85
|
+
if (agent === "codex") {
|
|
86
|
+
if (detectedBy === "codex_explicit_injection") return "codex_native_skill_injection";
|
|
87
|
+
if (detectedBy === "codex_implicit_script") return "codex_skill_script_exec";
|
|
88
|
+
if (detectedBy === "codex_implicit_doc_read") return "codex_skill_doc_read";
|
|
89
|
+
if (detectedBy === "tool_call") return "codex_unsupported_skill_tool";
|
|
90
|
+
}
|
|
91
|
+
if (detectedBy === "skill_file_path") return "skill_file_path";
|
|
92
|
+
return detectedBy || "metadata";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function skillInvocationMode(agent, detectedBy) {
|
|
96
|
+
if (agent === "opencode" && detectedBy === "slash_command") return "explicit_request";
|
|
97
|
+
if (agent === "codex" && ["codex_implicit_script", "codex_implicit_doc_read"].includes(detectedBy)) return "implicit";
|
|
98
|
+
if (agent === "codex" && detectedBy === "codex_explicit_injection") return "explicit";
|
|
99
|
+
if (["slash_command", "attribution_skill"].includes(detectedBy)) return "explicit";
|
|
100
|
+
if (["tool_call", "plugin_event"].includes(detectedBy)) return "implicit";
|
|
101
|
+
return "detected";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function skillEventType(detectedBy, agent = "unknown") {
|
|
105
|
+
if (agent === "opencode" && detectedBy === "slash_command") return "requested";
|
|
106
|
+
if (agent === "codex" && detectedBy === "tool_call") return "detected";
|
|
107
|
+
if (["tool_call", "plugin_event", "attribution_skill", "slash_command", "codex_explicit_injection", "codex_implicit_script", "codex_implicit_doc_read"].includes(detectedBy)) {
|
|
108
|
+
return "invoked";
|
|
109
|
+
}
|
|
110
|
+
return "detected";
|
|
73
111
|
}
|
|
74
112
|
|
|
75
113
|
function skillIdSegment(name) {
|
|
@@ -225,6 +263,8 @@ export function buildInteractionMetadata(options = {}) {
|
|
|
225
263
|
? skillUseEvents.map((event) => event.skill_name)
|
|
226
264
|
: normalizeSkillNames(options.skillNames ?? options.skill_names);
|
|
227
265
|
const skillNames = normalizeSkillNames(skillNamesAll);
|
|
266
|
+
const skillInvocationModes = skillUseEvents.map((event) => event.skill_invocation_mode).filter(Boolean);
|
|
267
|
+
const skillAgentPaths = skillUseEvents.map((event) => event.skill_agent_path).filter(Boolean);
|
|
228
268
|
const skillUseCount = skillUseEvents.length || skillNamesAll.length || Number(options.skillUseCount ?? options.skill_use_count ?? 0) || 0;
|
|
229
269
|
const toolCallCount = Math.max(Number(options.toolCallCount ?? options.tool_call_count ?? 0) || 0, skillUseCount);
|
|
230
270
|
const toolResultCount = Math.max(Number(options.toolResultCount ?? options.tool_result_count ?? 0) || 0, skillUseCount);
|
|
@@ -263,6 +303,12 @@ export function buildInteractionMetadata(options = {}) {
|
|
|
263
303
|
if (skillNamesAll.length) {
|
|
264
304
|
metadata.skill_names_all = skillNamesAll;
|
|
265
305
|
}
|
|
306
|
+
if (skillInvocationModes.length) {
|
|
307
|
+
metadata.skill_invocation_modes = skillInvocationModes;
|
|
308
|
+
}
|
|
309
|
+
if (skillAgentPaths.length) {
|
|
310
|
+
metadata.skill_agent_paths = skillAgentPaths;
|
|
311
|
+
}
|
|
266
312
|
|
|
267
313
|
return metadata;
|
|
268
314
|
}
|
|
@@ -276,17 +322,21 @@ function normalizeSkillUseEvents(events) {
|
|
|
276
322
|
|
|
277
323
|
export function buildSkillUseEvents(options = {}) {
|
|
278
324
|
const interactionId = String(options.interactionId ?? options.interaction_id ?? "unknown");
|
|
325
|
+
const agent = String(options.agent || options.source || agentFromInteractionId(interactionId));
|
|
279
326
|
const usages = dedupeSkillUsages(normalizeSkillUsages(options.skillUsages ?? options.skill_usages));
|
|
280
327
|
const total = usages.length;
|
|
281
328
|
return usages.map((usage, index) => {
|
|
282
329
|
const skillUseIndex = index + 1;
|
|
283
330
|
const detectedBy = usage.detected_by;
|
|
331
|
+
const invocationMode = skillInvocationMode(agent, detectedBy);
|
|
284
332
|
return {
|
|
285
333
|
skill_use_id: `${interactionId}:skill:${skillUseIndex}:${skillIdSegment(usage.name)}`,
|
|
286
334
|
skill_use_index: skillUseIndex,
|
|
287
335
|
skill_use_count_in_interaction: total,
|
|
288
|
-
skill_event_type: skillEventType(detectedBy),
|
|
289
|
-
skill_trigger:
|
|
336
|
+
skill_event_type: skillEventType(detectedBy, agent),
|
|
337
|
+
skill_trigger: invocationMode,
|
|
338
|
+
skill_invocation_mode: invocationMode,
|
|
339
|
+
skill_agent_path: skillAgentPath(agent, detectedBy),
|
|
290
340
|
skill_name: usage.name,
|
|
291
341
|
skill_namespace: usage.skill_namespace,
|
|
292
342
|
detected_by: detectedBy,
|
|
@@ -309,7 +309,18 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
309
309
|
" return out;",
|
|
310
310
|
"};",
|
|
311
311
|
"const skillNamespace = (name) => String(name || '').includes(':') ? String(name).split(':', 1)[0] : '';",
|
|
312
|
-
"const
|
|
312
|
+
"const skillAgentPath = (detectedBy) => {",
|
|
313
|
+
" if (detectedBy === 'tool_call') return 'opencode_skill_tool';",
|
|
314
|
+
" if (detectedBy === 'slash_command') return 'opencode_slash_prompt';",
|
|
315
|
+
" if (detectedBy === 'skill_file_path') return 'skill_file_path';",
|
|
316
|
+
" return detectedBy || 'metadata';",
|
|
317
|
+
"};",
|
|
318
|
+
"const skillInvocationMode = (detectedBy) => {",
|
|
319
|
+
" if (detectedBy === 'slash_command') return 'explicit_request';",
|
|
320
|
+
" if (detectedBy === 'tool_call' || detectedBy === 'plugin_event') return 'implicit';",
|
|
321
|
+
" return 'detected';",
|
|
322
|
+
"};",
|
|
323
|
+
"const skillEventType = (detectedBy) => detectedBy === 'slash_command' ? 'requested' : (detectedBy === 'tool_call' || detectedBy === 'plugin_event' ? 'invoked' : 'detected');",
|
|
313
324
|
"const skillIdSegment = (name) => String(name || 'unknown').trim().replace(/[^A-Za-z0-9_.:-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 96) || 'unknown';",
|
|
314
325
|
"",
|
|
315
326
|
"const escapeRegExp = (value) => String(value).replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');",
|
|
@@ -409,12 +420,15 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
409
420
|
" return usages.map((usage, index) => {",
|
|
410
421
|
" const skillName = String(usage.name || '').trim();",
|
|
411
422
|
" const detectedBy = String(usage.detected_by || 'metadata');",
|
|
423
|
+
" const invocationMode = skillInvocationMode(detectedBy);",
|
|
412
424
|
" return {",
|
|
413
425
|
" skill_use_id: `${interactionId}:skill:${index + 1}:${skillIdSegment(skillName)}`,",
|
|
414
426
|
" skill_use_index: index + 1,",
|
|
415
427
|
" skill_use_count_in_interaction: total,",
|
|
416
428
|
" skill_event_type: skillEventType(detectedBy),",
|
|
417
|
-
" skill_trigger:
|
|
429
|
+
" skill_trigger: invocationMode,",
|
|
430
|
+
" skill_invocation_mode: invocationMode,",
|
|
431
|
+
" skill_agent_path: skillAgentPath(detectedBy),",
|
|
418
432
|
" skill_name: skillName,",
|
|
419
433
|
" skill_use_count: 1,",
|
|
420
434
|
" skill_namespace: usage.skill_namespace || skillNamespace(skillName),",
|
|
@@ -580,6 +594,8 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
580
594
|
" const skillUseEvents = buildSkillUseEvents(interactionId, skillUsages);",
|
|
581
595
|
" const skillNames = uniqueSkillNames(skillUsages);",
|
|
582
596
|
" const skillNamesAll = skillUseEvents.map((event) => event.skill_name);",
|
|
597
|
+
" const skillInvocationModes = skillUseEvents.map((event) => event.skill_invocation_mode).filter(Boolean);",
|
|
598
|
+
" const skillAgentPaths = skillUseEvents.map((event) => event.skill_agent_path).filter(Boolean);",
|
|
583
599
|
" const toolCallCount = Math.max(new Set([...(toolCallIdsByMessageId.get(messageId) ?? []), ...(toolCallIdsBySessionId.get(sessionId) ?? [])]).size, skillUseEvents.length);",
|
|
584
600
|
" const toolResultCount = Math.max(new Set([...(toolResultIdsByMessageId.get(messageId) ?? []), ...(toolResultIdsBySessionId.get(sessionId) ?? [])]).size, skillUseEvents.length);",
|
|
585
601
|
' span.setAttribute("oh.langfuse.source", "opencode");',
|
|
@@ -603,6 +619,8 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
603
619
|
' if (skillNames.length) span.setAttribute("langfuse.observation.metadata.skill_names", skillNames);',
|
|
604
620
|
' if (skillNames.length) span.setAttribute("langfuse.observation.metadata.skill_names_csv", skillNames.join(","));',
|
|
605
621
|
' if (skillNamesAll.length) span.setAttribute("langfuse.observation.metadata.skill_names_all", skillNamesAll);',
|
|
622
|
+
' if (skillInvocationModes.length) span.setAttribute("langfuse.observation.metadata.skill_invocation_modes", skillInvocationModes);',
|
|
623
|
+
' if (skillAgentPaths.length) span.setAttribute("langfuse.observation.metadata.skill_agent_paths", skillAgentPaths);',
|
|
606
624
|
' if (tokenMetrics.input !== undefined) span.setAttribute("langfuse.observation.metadata.input_tokens", tokenMetrics.input);',
|
|
607
625
|
' if (tokenMetrics.output !== undefined) span.setAttribute("langfuse.observation.metadata.output_tokens", tokenMetrics.output);',
|
|
608
626
|
' if (total !== undefined) span.setAttribute("langfuse.observation.metadata.total_tokens", total);',
|