oh-langfuse 0.1.47 → 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.
@@ -424,6 +424,8 @@ def build_interaction_metadata(
424
424
  events = list(skill_use_events or [])
425
425
  skill_names_all = [str(event.get("skill_name") or "") for event in events if event.get("skill_name")]
426
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")]
427
429
  effective_skill_count = len(events) if events else int(skill_use_count or 0)
428
430
  return {
429
431
  "source": source,
@@ -453,6 +455,8 @@ def build_interaction_metadata(
453
455
  **({
454
456
  "skill_names": unique_skill_names,
455
457
  "skill_names_all": skill_names_all,
458
+ "skill_invocation_modes": skill_invocation_modes,
459
+ "skill_agent_paths": skill_agent_paths,
456
460
  } if events else {}),
457
461
  }
458
462
 
@@ -482,8 +486,37 @@ def _skill_namespace(name: str) -> str:
482
486
  return name.split(":", 1)[0] if ":" in name else ""
483
487
 
484
488
 
485
- def _skill_event_type(detected_by: str) -> str:
486
- return "invoked" if detected_by in ("tool_call", "plugin_event", "attribution_skill", "slash_command") else "detected"
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"
487
520
 
488
521
 
489
522
  def _skill_id_segment(name: str) -> str:
@@ -501,25 +534,78 @@ def _skill_usage(name: str, detected_by: str, skill_call_id: str = "") -> Dict[s
501
534
  }
502
535
 
503
536
 
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()
551
+ return ""
552
+
553
+
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]]:
565
+ found: List[Dict[str, str]] = []
566
+ if not command:
567
+ return found
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
+
584
+ return found
585
+
586
+
504
587
  def detect_skill_usages(tool_calls: List[Dict[str, Any]], known_skills: set) -> List[Dict[str, str]]:
505
588
  found: List[Dict[str, str]] = []
506
- seen_call_ids: set = set()
507
589
  for call in tool_calls or []:
508
- tool_name = str(call.get("name") or "")
509
- 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()
510
590
  input_obj = call.get("input") if isinstance(call.get("input"), (dict, list, str)) else {}
511
- if tool_name.lower() == "skill" and isinstance(input_obj, dict):
512
- for key in ("skill_name", "skill", "name"):
513
- value = input_obj.get(key)
514
- if isinstance(value, str) and value.strip():
515
- name = value.strip()
516
- if call_id:
517
- dedupe_key = f"call:{call_id}"
518
- if dedupe_key in seen_call_ids:
519
- break
520
- seen_call_ids.add(dedupe_key)
521
- found.append(_skill_usage(name, "tool_call", call_id))
522
- break
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"))
523
609
  return found
524
610
 
525
611
 
@@ -550,7 +636,8 @@ def _dedupe_turn_skill_usages(usages: List[Dict[str, str]]) -> List[Dict[str, st
550
636
 
551
637
 
552
638
  def detect_turn_skill_usages(material: Dict[str, Any], known_skills: set) -> List[Dict[str, str]]:
553
- found = list(detect_skill_usages(material.get("tool_calls") or [], 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))
554
641
  return _dedupe_turn_skill_usages(found)
555
642
 
556
643
 
@@ -558,6 +645,7 @@ def build_skill_use_events(interaction_id: str, skill_usages: List[Dict[str, str
558
645
  events: List[Dict[str, Any]] = []
559
646
  deduped: List[Dict[str, str]] = []
560
647
  seen_call_ids: set = set()
648
+ agent = _skill_agent_from_interaction_id(interaction_id)
561
649
  for skill in skill_usages or []:
562
650
  call_id = str(skill.get("skill_call_id") or "").strip()
563
651
  if call_id:
@@ -573,12 +661,15 @@ def build_skill_use_events(interaction_id: str, skill_usages: List[Dict[str, str
573
661
  continue
574
662
  detected_by = str(skill.get("detected_by") or "metadata")
575
663
  call_id = str(skill.get("skill_call_id") or "").strip()
664
+ invocation_mode = _skill_invocation_mode(agent, detected_by)
576
665
  events.append({
577
666
  "skill_use_id": f"{interaction_id}:skill:{index}:{_skill_id_segment(name)}",
578
667
  "skill_use_index": index,
579
668
  "skill_use_count_in_interaction": total,
580
- "skill_event_type": _skill_event_type(detected_by),
581
- "skill_trigger": "unknown",
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),
582
673
  "skill_name": name,
583
674
  "skill_use_count": 1,
584
675
  "skill_namespace": skill.get("skill_namespace") or _skill_namespace(name),
@@ -596,6 +687,9 @@ def summarize_skill_usages(skill_usages: List[Dict[str, str]]) -> List[Dict[str,
596
687
  continue
597
688
  entry = summary.setdefault(name, {"name": name, "count": 0, "detected_by": item.get("detected_by")})
598
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))
599
693
  return list(summary.values())
600
694
 
601
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 _skill_event_type(detected_by: str) -> str:
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": "unknown",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-langfuse",
3
- "version": "0.1.47",
3
+ "version": "0.1.48",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Use npm scripts to configure Claude Code / OpenCode / Codex with Langfuse tracing.",
@@ -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
- return String(result.stdout || "")
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 skillEventType(detectedBy) {
72
- return ["tool_call", "plugin_event", "attribution_skill", "slash_command"].includes(detectedBy) ? "invoked" : "detected";
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: "unknown",
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 skillEventType = (detectedBy) => detectedBy === 'tool_call' || detectedBy === 'plugin_event' || detectedBy === 'slash_command' ? 'invoked' : 'detected';",
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: 'unknown',",
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);',