oh-langfuse 0.1.52 → 0.1.53

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/langfuse_hook.py CHANGED
@@ -924,7 +924,7 @@ def emit_turn(
924
924
 
925
925
  # Tool observations
926
926
  for tc in tool_calls:
927
- in_obj = tc["input"]
927
+ in_obj = tc["input"]
928
928
  # truncate tool input if it's a large string payload
929
929
  if isinstance(in_obj, str):
930
930
  in_obj, in_meta = truncate_text(in_obj)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-langfuse",
3
- "version": "0.1.52",
3
+ "version": "0.1.53",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Use npm scripts to configure Claude Code / OpenCode / Codex with Langfuse tracing.",
@@ -312,15 +312,17 @@ function getPatchedLangfuseDistIndexJs() {
312
312
  "const skillAgentPath = (detectedBy) => {",
313
313
  " if (detectedBy === 'tool_call') return 'opencode_skill_tool';",
314
314
  " if (detectedBy === 'slash_command') return 'opencode_slash_prompt';",
315
+ " if (detectedBy === 'natural_language_request') return 'opencode_prompt_skill_name';",
315
316
  " if (detectedBy === 'skill_file_path') return 'skill_file_path';",
316
317
  " return detectedBy || 'metadata';",
317
318
  "};",
318
319
  "const skillInvocationMode = (detectedBy) => {",
319
320
  " if (detectedBy === 'slash_command') return 'explicit_request';",
321
+ " if (detectedBy === 'natural_language_request') return 'implicit_request';",
320
322
  " if (detectedBy === 'tool_call' || detectedBy === 'plugin_event') return 'implicit';",
321
323
  " return 'detected';",
322
324
  "};",
323
- "const skillEventType = (detectedBy) => detectedBy === 'slash_command' ? 'requested' : (detectedBy === 'tool_call' || detectedBy === 'plugin_event' ? 'invoked' : 'detected');",
325
+ "const skillEventType = (detectedBy) => (detectedBy === 'slash_command' || detectedBy === 'natural_language_request') ? 'requested' : (detectedBy === 'tool_call' || detectedBy === 'plugin_event' ? 'invoked' : 'detected');",
324
326
  "const skillIdSegment = (name) => String(name || 'unknown').trim().replace(/[^A-Za-z0-9_.:-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 96) || 'unknown';",
325
327
  "",
326
328
  "const escapeRegExp = (value) => String(value).replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');",
@@ -373,6 +375,18 @@ function getPatchedLangfuseDistIndexJs() {
373
375
  " return out;",
374
376
  "};",
375
377
  "",
378
+ "const detectPromptSkillRequests = (haystack, skills) => {",
379
+ " const text = String(haystack || '');",
380
+ " if (!text.trim()) return [];",
381
+ " const out = [];",
382
+ " for (const skillName of skills) {",
383
+ " const escaped = escapeRegExp(skillName);",
384
+ " const pattern = new RegExp(`(?:请\\\\s*)?(?:使用|调用|启用|采用)\\\\s*[\\\"'“”‘’]?${escaped}(?=$|[\\\\s,,。;;::、])|\\\\b(?:use|invoke|run|apply)\\\\s+[\\\"']?${escaped}(?=$|[\\\\s,,。;;::])`, 'i');",
385
+ " if (pattern.test(text)) out.push({ name: skillName, skill_namespace: skillNamespace(skillName), detected_by: 'natural_language_request', skill_call_id: '' });",
386
+ " }",
387
+ " return out;",
388
+ "};",
389
+ "",
376
390
  "const detectOpencodeSkillUsages = (source, knownSkills = []) => {",
377
391
  " const skills = normalizeSkillNames(knownSkills);",
378
392
  " if (skills.length === 0) return [];",
@@ -393,6 +407,12 @@ function getPatchedLangfuseDistIndexJs() {
393
407
  " }",
394
408
  " found.push(...explicit);",
395
409
  " const haystack = collectStrings(source).join('\\n');",
410
+ " const promptSeen = new Set(explicit.map((usage) => usage.name));",
411
+ " for (const usage of detectPromptSkillRequests(haystack, skills)) {",
412
+ " if (promptSeen.has(usage.name)) continue;",
413
+ " promptSeen.add(usage.name);",
414
+ " found.push(usage);",
415
+ " }",
396
416
  " const pathSeen = new Set();",
397
417
  " for (const match of haystack.matchAll(/(?:^|[\"'\\s])(?:[A-Za-z]:)?[^\"'\\n\\r]*[\\\\/]+([^\\\\/\"'\\n\\r]+)[\\\\/]+SKILL\\.md(?=$|[\"'\\s])/gi)) {",
398
418
  " const skillName = match[1];",
@@ -495,9 +515,11 @@ function getPatchedLangfuseDistIndexJs() {
495
515
  " sdk.start();",
496
516
  " const metricsTracer = trace.getTracer('oh-langfuse-opencode-metrics');",
497
517
  " const knownSkillNames = await collectKnownSkillNames();",
518
+ " const startupSkillUsages = detectOpencodeSkillUsages(process.argv.join('\\n'), knownSkillNames);",
498
519
  " const messageTextById = new Map();",
499
520
  " const skillUsagesByMessageId = new Map();",
500
521
  " const skillUsagesBySessionId = new Map();",
522
+ " const startupSkillSessionIds = new Set();",
501
523
  " const toolCallIdsByMessageId = new Map();",
502
524
  " const toolCallIdsBySessionId = new Map();",
503
525
  " const toolResultIdsByMessageId = new Map();",
@@ -566,6 +588,10 @@ function getPatchedLangfuseDistIndexJs() {
566
588
  " const partType = part?.type ?? '';",
567
589
  " const sessionId = pickEventString(part?.sessionID, part?.sessionId, payload?.sessionID, payload?.sessionId, payload?.session?.id, event?.sessionID, event?.sessionId);",
568
590
  " const messageId = pickEventString(part?.messageID, part?.messageId, payload?.messageID, payload?.messageId, payload?.message?.id, event?.messageID, event?.messageId);",
591
+ " if (sessionId && startupSkillUsages.length && !startupSkillSessionIds.has(sessionId)) {",
592
+ " startupSkillSessionIds.add(sessionId);",
593
+ " rememberSkillUsages(skillUsagesBySessionId, sessionId, startupSkillUsages);",
594
+ " }",
569
595
  " const skillDetectionSources = [part, payload, part?.state?.input, part?.input, payload?.input, part?.state?.metadata, payload?.metadata, part?.state?.file, part?.file];",
570
596
  " const eventSkillUsages = detectOpencodeSkillUsages(skillDetectionSources, knownSkillNames);",
571
597
  " rememberSkillUsages(skillUsagesByMessageId, messageId, eventSkillUsages);",
@@ -339,6 +339,12 @@ function hasMetadataKey(item, key) {
339
339
  return metadataValue(item, key) !== undefined;
340
340
  }
341
341
 
342
+ function metricInteractionId(item, target) {
343
+ return target === "opencode"
344
+ ? directMetadataValue(item, "interaction_id") || metadataValue(item, "interaction_id")
345
+ : metadataValue(item, "interaction_id");
346
+ }
347
+
342
348
  function isAgentTurnObservation(item) {
343
349
  if (item?.name === "Agent Turn" || metadataValue(item, "interaction_count") === 1 || metadataValue(item, "interaction_count") === "1") {
344
350
  return true;
@@ -363,33 +369,76 @@ async function observationsForTrace(config, traceId, since) {
363
369
  return fallback.filter((item) => item.traceId === traceId || item.trace_id === traceId);
364
370
  }
365
371
 
366
- async function verifyMetricObservations(config, found, { since, target }) {
372
+ function mergeMetricCandidates(items) {
373
+ const out = [];
374
+ const seen = new Set();
375
+ for (const item of items || []) {
376
+ if (!item || typeof item !== "object") continue;
377
+ const key = [idOf(item), item.name || "", item.traceId || item.trace_id || ""].join(":");
378
+ if (seen.has(key)) continue;
379
+ seen.add(key);
380
+ out.push(item);
381
+ }
382
+ return out;
383
+ }
384
+
385
+ async function recentMetricCandidates(config, since) {
386
+ const baseParams = { limit: 100, fromTimestamp: since.toISOString() };
387
+ const candidates = [];
388
+ for (const pathname of ["/traces"]) {
389
+ for (const params of [{ ...baseParams, userId: config.userId }, baseParams]) {
390
+ try {
391
+ candidates.push(...dataArray(await langfuseGetLenient(config, pathname, params)));
392
+ } catch (error) {
393
+ if (error.name === "AbortError" || error.status === 404) continue;
394
+ throw error;
395
+ }
396
+ }
397
+ }
398
+ return mergeMetricCandidates(candidates);
399
+ }
400
+
401
+ async function verifyMetricObservations(config, found, { since, target, marker = "" }) {
367
402
  const traceId = traceIdOfFound(found);
368
- let observations = await observationsForTrace(config, traceId, since);
403
+ let observations = mergeMetricCandidates([...(await observationsForTrace(config, traceId, since)), found?.item]);
369
404
 
370
405
  if (!observations.length && Array.isArray(found?.item?.observations)) {
371
- observations = found.item.observations.filter((item) => typeof item === "object");
406
+ observations = mergeMetricCandidates(found.item.observations.filter((item) => typeof item === "object"));
407
+ } else if (Array.isArray(found?.item?.observations)) {
408
+ observations = mergeMetricCandidates([
409
+ ...observations,
410
+ ...found.item.observations.filter((item) => typeof item === "object"),
411
+ ]);
412
+ }
413
+
414
+ let skillUseObservations = observations.filter((item) => item?.name === "Skill Use");
415
+ let interactions = observations.filter(isAgentTurnObservation);
416
+
417
+ if (!interactions.length) {
418
+ const recentRelated = (await recentMetricCandidates(config, since)).filter((item) => marker && containsMarker(item, marker));
419
+ observations = mergeMetricCandidates([...observations, ...recentRelated]);
420
+ skillUseObservations = observations.filter((item) => item?.name === "Skill Use");
421
+ interactions = observations.filter(isAgentTurnObservation);
372
422
  }
373
423
 
374
- const skillUseObservations = observations.filter((item) => item?.name === "Skill Use");
375
424
  if (skillUseObservations.length) {
376
- throw new Error(`Metric verification failed for ${target}: Skill Use observations should not be emitted anymore, found ${skillUseObservations.length}.`);
425
+ throw new Error(`Metric verification failed for ${target}: Skill Use observations should not be emitted as standalone metrics.`);
377
426
  }
378
427
 
379
- const interactions = observations.filter(isAgentTurnObservation);
380
428
  if (!interactions.length) {
381
429
  throw new Error(`Metric verification failed for ${target}: Agent Turn observation was not found for trace ${traceId || found.id}.`);
382
430
  }
383
431
 
384
432
  const byInteractionId = new Map();
433
+ const seenInteractionIds = new Set();
385
434
  for (const item of interactions) {
386
- const interactionId = target === "opencode"
387
- ? directMetadataValue(item, "interaction_id") || metadataValue(item, "interaction_id")
388
- : metadataValue(item, "interaction_id");
435
+ const interactionId = metricInteractionId(item, target);
389
436
  if (!interactionId) {
390
437
  throw new Error(`Metric verification failed for ${target}: Agent Turn is missing interaction_id.`);
391
438
  }
392
- byInteractionId.set(interactionId, (byInteractionId.get(interactionId) || 0) + 1);
439
+ if (seenInteractionIds.has(interactionId)) continue;
440
+ seenInteractionIds.add(interactionId);
441
+ byInteractionId.set(interactionId, 1);
393
442
  for (const key of ["user_id", "token_metrics_available", "tool_call_count", "skill_use_count", "metrics_schema_version"]) {
394
443
  if (!hasMetadataKey(item, key)) {
395
444
  throw new Error(`Metric verification failed for ${target}: Agent Turn is missing ${key}.`);
@@ -415,12 +464,6 @@ async function verifyMetricObservations(config, found, { since, target }) {
415
464
  }
416
465
  }
417
466
 
418
- for (const [interactionId, count] of byInteractionId.entries()) {
419
- if (count !== 1) {
420
- throw new Error(`Metric verification failed for ${target}: interaction_id ${interactionId} appeared ${count} times.`);
421
- }
422
- }
423
-
424
467
  return { traceId, interactionCount: interactions.length };
425
468
  }
426
469
 
@@ -558,7 +601,7 @@ async function main() {
558
601
 
559
602
  const found = await pollLangfuse(config, marker, { ...args, since, target });
560
603
  console.log(`[OK] Langfuse marker found for ${target}: ${found.kind} ${found.id || ""}`.trim());
561
- const metrics = await verifyMetricObservations(config, found, { since, target });
604
+ const metrics = await verifyMetricObservations(config, found, { since, target, marker });
562
605
  console.log(`[OK] Langfuse metrics found for ${target}: Agent Turn x${metrics.interactionCount}`);
563
606
  results.push({ target, marker, langfuse: { kind: found.kind, id: found.id || "", traceId: metrics.traceId }, metrics });
564
607
  }