open-multi-agent-kit 0.98.3 → 0.98.4
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/CHANGELOG.md +32 -0
- package/README.md +11 -2
- package/dist/cli/args.d.ts +1 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +14 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +3 -0
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/mcp-attach.d.ts +3 -3
- package/dist/cli/mcp-attach.d.ts.map +1 -1
- package/dist/cli/mcp-attach.js +5 -4
- package/dist/cli/mcp-attach.js.map +1 -1
- package/dist/cli/model-contract.d.ts +5 -0
- package/dist/cli/model-contract.d.ts.map +1 -0
- package/dist/cli/model-contract.js +45 -0
- package/dist/cli/model-contract.js.map +1 -0
- package/dist/commands/adaptorch-doctor-cli.d.ts.map +1 -1
- package/dist/commands/adaptorch-doctor-cli.js +32 -10
- package/dist/commands/adaptorch-doctor-cli.js.map +1 -1
- package/dist/commands/init-cli.d.ts +12 -0
- package/dist/commands/init-cli.d.ts.map +1 -0
- package/dist/commands/init-cli.js +76 -0
- package/dist/commands/init-cli.js.map +1 -0
- package/dist/commands/provider-sync-cli.d.ts +27 -0
- package/dist/commands/provider-sync-cli.d.ts.map +1 -0
- package/dist/commands/provider-sync-cli.js +177 -0
- package/dist/commands/provider-sync-cli.js.map +1 -0
- package/dist/commands/run-command.d.ts +8 -0
- package/dist/commands/run-command.d.ts.map +1 -0
- package/dist/commands/run-command.js +26 -0
- package/dist/commands/run-command.js.map +1 -0
- package/dist/core/active-skill-state.d.ts +7 -0
- package/dist/core/active-skill-state.d.ts.map +1 -0
- package/dist/core/active-skill-state.js +25 -0
- package/dist/core/active-skill-state.js.map +1 -0
- package/dist/core/agent-session-services.d.ts +2 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +2 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +0 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +30 -85
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/codex-chatgpt-web-bridge.d.ts +99 -0
- package/dist/core/codex-chatgpt-web-bridge.d.ts.map +1 -0
- package/dist/core/codex-chatgpt-web-bridge.js +192 -0
- package/dist/core/codex-chatgpt-web-bridge.js.map +1 -0
- package/dist/core/codex-chatgpt-web-sync.d.ts +50 -0
- package/dist/core/codex-chatgpt-web-sync.d.ts.map +1 -0
- package/dist/core/codex-chatgpt-web-sync.js +119 -0
- package/dist/core/codex-chatgpt-web-sync.js.map +1 -0
- package/dist/core/compaction/compaction-headroom.d.ts +30 -0
- package/dist/core/compaction/compaction-headroom.d.ts.map +1 -0
- package/dist/core/compaction/compaction-headroom.js +48 -0
- package/dist/core/compaction/compaction-headroom.js.map +1 -0
- package/dist/core/compaction/compaction.d.ts +2 -29
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +19 -47
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/compaction/overflow-retry-guard.d.ts +20 -0
- package/dist/core/compaction/overflow-retry-guard.d.ts.map +1 -0
- package/dist/core/compaction/overflow-retry-guard.js +38 -0
- package/dist/core/compaction/overflow-retry-guard.js.map +1 -0
- package/dist/core/context-budget-headroom-types.d.ts.map +1 -1
- package/dist/core/context-budget-headroom-types.js +1 -1
- package/dist/core/context-budget-headroom-types.js.map +1 -1
- package/dist/core/context-budget-token-counter-types.d.ts +27 -0
- package/dist/core/context-budget-token-counter-types.d.ts.map +1 -0
- package/dist/core/context-budget-token-counter-types.js +2 -0
- package/dist/core/context-budget-token-counter-types.js.map +1 -0
- package/dist/core/context-budget-token-counter.d.ts +2 -26
- package/dist/core/context-budget-token-counter.d.ts.map +1 -1
- package/dist/core/context-budget-token-counter.js +2 -0
- package/dist/core/context-budget-token-counter.js.map +1 -1
- package/dist/core/context-budget-v2-scoring.d.ts.map +1 -1
- package/dist/core/context-budget-v2-scoring.js +14 -10
- package/dist/core/context-budget-v2-scoring.js.map +1 -1
- package/dist/core/domain-router.d.ts.map +1 -1
- package/dist/core/domain-router.js +1 -1
- package/dist/core/domain-router.js.map +1 -1
- package/dist/core/grok-harness-dispatch.d.ts +4 -0
- package/dist/core/grok-harness-dispatch.d.ts.map +1 -1
- package/dist/core/grok-harness-dispatch.js +20 -3
- package/dist/core/grok-harness-dispatch.js.map +1 -1
- package/dist/core/grok-harness.d.ts +15 -9
- package/dist/core/grok-harness.d.ts.map +1 -1
- package/dist/core/grok-harness.js +34 -8
- package/dist/core/grok-harness.js.map +1 -1
- package/dist/core/mcp/tools.d.ts +2 -2
- package/dist/core/mcp/tools.d.ts.map +1 -1
- package/dist/core/mcp/tools.js +29 -3
- package/dist/core/mcp/tools.js.map +1 -1
- package/dist/core/model-registry-schema.d.ts +1081 -0
- package/dist/core/model-registry-schema.d.ts.map +1 -0
- package/dist/core/model-registry-schema.js +148 -0
- package/dist/core/model-registry-schema.js.map +1 -0
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +1 -146
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts +2 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +2 -1
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/prompt-preset.d.ts +1 -1
- package/dist/core/prompt-preset.d.ts.map +1 -1
- package/dist/core/prompt-preset.js +16 -1
- package/dist/core/prompt-preset.js.map +1 -1
- package/dist/core/provider-display-names.d.ts.map +1 -1
- package/dist/core/provider-display-names.js +1 -0
- package/dist/core/provider-display-names.js.map +1 -1
- package/dist/core/provider-resilience.d.ts +8 -0
- package/dist/core/provider-resilience.d.ts.map +1 -1
- package/dist/core/provider-resilience.js +20 -3
- package/dist/core/provider-resilience.js.map +1 -1
- package/dist/core/provider-usage-types.d.ts +42 -0
- package/dist/core/provider-usage-types.d.ts.map +1 -0
- package/dist/core/provider-usage-types.js +2 -0
- package/dist/core/provider-usage-types.js.map +1 -0
- package/dist/core/provider-usage.d.ts +2 -31
- package/dist/core/provider-usage.d.ts.map +1 -1
- package/dist/core/provider-usage.js +4 -0
- package/dist/core/provider-usage.js.map +1 -1
- package/dist/core/redaction.d.ts.map +1 -1
- package/dist/core/redaction.js +18 -2
- package/dist/core/redaction.js.map +1 -1
- package/dist/core/sandbox/policy-merge.d.ts +6 -0
- package/dist/core/sandbox/policy-merge.d.ts.map +1 -0
- package/dist/core/sandbox/policy-merge.js +74 -0
- package/dist/core/sandbox/policy-merge.js.map +1 -0
- package/dist/core/sandbox/policy-paths.d.ts +4 -0
- package/dist/core/sandbox/policy-paths.d.ts.map +1 -0
- package/dist/core/sandbox/policy-paths.js +21 -0
- package/dist/core/sandbox/policy-paths.js.map +1 -0
- package/dist/core/sandbox/policy-types.d.ts +75 -0
- package/dist/core/sandbox/policy-types.d.ts.map +1 -0
- package/dist/core/sandbox/policy-types.js +2 -0
- package/dist/core/sandbox/policy-types.js.map +1 -0
- package/dist/core/sandbox/policy.d.ts +3 -77
- package/dist/core/sandbox/policy.d.ts.map +1 -1
- package/dist/core/sandbox/policy.js +3 -98
- package/dist/core/sandbox/policy.js.map +1 -1
- package/dist/core/sdk-provider-stream.d.ts +14 -0
- package/dist/core/sdk-provider-stream.d.ts.map +1 -0
- package/dist/core/sdk-provider-stream.js +38 -0
- package/dist/core/sdk-provider-stream.js.map +1 -0
- package/dist/core/sdk.d.ts +5 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +17 -33
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-failure-cause.d.ts.map +1 -1
- package/dist/core/session-failure-cause.js +10 -3
- package/dist/core/session-failure-cause.js.map +1 -1
- package/dist/core/session-run-termination.d.ts +18 -0
- package/dist/core/session-run-termination.d.ts.map +1 -0
- package/dist/core/session-run-termination.js +61 -0
- package/dist/core/session-run-termination.js.map +1 -0
- package/dist/core/session-termination.d.ts.map +1 -1
- package/dist/core/session-termination.js +1 -1
- package/dist/core/session-termination.js.map +1 -1
- package/dist/core/skill-selector.d.ts +40 -0
- package/dist/core/skill-selector.d.ts.map +1 -0
- package/dist/core/skill-selector.js +102 -0
- package/dist/core/skill-selector.js.map +1 -0
- package/dist/core/tools/render-utils.d.ts.map +1 -1
- package/dist/core/tools/render-utils.js +2 -4
- package/dist/core/tools/render-utils.js.map +1 -1
- package/dist/core/turn-metrics-record.d.ts +55 -0
- package/dist/core/turn-metrics-record.d.ts.map +1 -0
- package/dist/core/turn-metrics-record.js +149 -0
- package/dist/core/turn-metrics-record.js.map +1 -0
- package/dist/core/turn-metrics.d.ts +4 -56
- package/dist/core/turn-metrics.d.ts.map +1 -1
- package/dist/core/turn-metrics.js +12 -60
- package/dist/core/turn-metrics.js.map +1 -1
- package/dist/guardrails/evidence-system.d.ts +1 -1
- package/dist/guardrails/evidence-system.d.ts.map +1 -1
- package/dist/guardrails/evidence-system.js +18 -29
- package/dist/guardrails/evidence-system.js.map +1 -1
- package/dist/guardrails/merge-gate-result.d.ts +4 -0
- package/dist/guardrails/merge-gate-result.d.ts.map +1 -0
- package/dist/guardrails/merge-gate-result.js +32 -0
- package/dist/guardrails/merge-gate-result.js.map +1 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +22 -22
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/interactive-login-options.d.ts +27 -0
- package/dist/modes/interactive/interactive-login-options.d.ts.map +1 -0
- package/dist/modes/interactive/interactive-login-options.js +54 -0
- package/dist/modes/interactive/interactive-login-options.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts +7 -4
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +138 -140
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/interactive-resume-command.d.ts +3 -0
- package/dist/modes/interactive/interactive-resume-command.d.ts.map +1 -0
- package/dist/modes/interactive/interactive-resume-command.js +24 -0
- package/dist/modes/interactive/interactive-resume-command.js.map +1 -0
- package/dist/modes/interactive/interactive-tool-result.d.ts +32 -0
- package/dist/modes/interactive/interactive-tool-result.d.ts.map +1 -0
- package/dist/modes/interactive/interactive-tool-result.js +59 -0
- package/dist/modes/interactive/interactive-tool-result.js.map +1 -0
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +22 -0
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/utils/clipboard-image.d.ts.map +1 -1
- package/dist/utils/clipboard-image.js +10 -60
- package/dist/utils/clipboard-image.js.map +1 -1
- package/dist/utils/terminal-links.d.ts +13 -0
- package/dist/utils/terminal-links.d.ts.map +1 -0
- package/dist/utils/terminal-links.js +68 -0
- package/dist/utils/terminal-links.js.map +1 -0
- package/dist/utils/windows-clipboard-image.d.ts +16 -0
- package/dist/utils/windows-clipboard-image.d.ts.map +1 -0
- package/dist/utils/windows-clipboard-image.js +87 -0
- package/dist/utils/windows-clipboard-image.js.map +1 -0
- package/docs/adaptorch-onboarding.md +80 -0
- package/docs/containerization.md +12 -0
- package/docs/context-files.md +97 -0
- package/docs/correctness-wall.md +8 -0
- package/docs/custom-provider.md +35 -0
- package/docs/development.md +18 -3
- package/docs/environment-variables.md +2 -0
- package/docs/grok-harness.md +4 -0
- package/docs/harness-boundaries.md +154 -0
- package/docs/harness-improvements.md +90 -0
- package/docs/json.md +19 -0
- package/docs/keybindings.md +1 -1
- package/docs/mcp.md +22 -2
- package/docs/metrics.md +66 -16
- package/docs/model-catalog-refresh.md +190 -0
- package/docs/model-contract.md +168 -0
- package/docs/models.md +21 -4
- package/docs/provider-resilience.md +5 -3
- package/docs/providers.md +68 -1
- package/docs/quickstart.md +4 -0
- package/docs/release-audit-0.98.4.md +112 -0
- package/docs/review-bundle-followup.md +195 -0
- package/docs/runtime-algorithms.md +19 -0
- package/docs/sdk.md +11 -2
- package/docs/sessions.md +1 -1
- package/docs/settings.md +17 -1
- package/docs/skills.md +2 -0
- package/docs/tb21-audit.md +131 -0
- package/docs/terminal-setup.md +20 -0
- package/docs/usage.md +23 -1
- package/docs/windows.md +48 -0
- package/examples/README.md +4 -0
- package/examples/context/AGENTS.md +64 -0
- package/examples/context/CLAUDE.md +17 -0
- package/examples/context/INTERNET.md +42 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +18 -18
- package/package.json +6 -6
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-selector.d.ts","sourceRoot":"","sources":["../../src/core/skill-selector.ts"],"names":[],"mappings":"AAYA,4DAA0D;AAC1D,eAAO,MAAM,mBAAmB,IAAI,CAAC;AACrC,4DAA4D;AAC5D,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAC1C;;;GAGG;AACH,eAAO,MAAM,oBAAoB,OAAO,CAAC;AACzC,oFAAoF;AACpF,eAAO,MAAM,sBAAsB,OAAO,CAAC;AAE3C,KAAK,eAAe,GAAG,WAAW,GAAG,WAAW,GAAG,UAAU,CAAC;AAE9D,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC7B;AAED,UAAU,UAAU;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACvB;AAED,UAAU,aAAc,SAAQ,UAAU;IACzC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,SAAS,cAAc,EAAE,CAAC;IAC3C,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,4EAA4E;IAC5E,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAED,MAAM,WAAW,oBAAoB;IACpC,QAAQ,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;IAC5C,QAAQ,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,CAAC;IACvC,QAAQ,CAAC,UAAU,EAAE,eAAe,CAAC;IACrC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACxB;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,oBAAoB,CAgD3E","sourcesContent":["/**\n * Query-aware skill grant selection.\n *\n * Scores candidates with {@link scoreSkillRelevance}, then applies the same\n * ranking/confidence shape as domain routing: weak/strong thresholds, a 2–3\n * skill cap, input-order ties, and an ambiguity flag when a tentative leader\n * is within the documented margin of the runner-up.\n *\n * I/O-free. Callers still decide whether to mark the result active.\n */\nimport { scoreSkillRelevance } from \"./context-budget-relevance.ts\";\n\n/** Documented lane grant cap: load at most 2–3 skills. */\nexport const MAX_SELECTED_SKILLS = 3;\n/** Coverage at/above which a skill is a confident match. */\nexport const SKILL_STRONG_THRESHOLD = 0.7;\n/**\n * Minimum score to grant a skill. Neutral (no-query) relevance is 0.3, so the\n * weak floor sits above that and ignores idle overlap.\n */\nexport const SKILL_WEAK_THRESHOLD = 0.35;\n/** If the runner-up is within this margin of a tentative leader, flag ambiguous. */\nexport const SKILL_AMBIGUITY_MARGIN = 0.08;\n\ntype SkillConfidence = \"confident\" | \"tentative\" | \"fallback\";\n\nexport interface SkillCandidate {\n\treadonly name: string;\n\treadonly description: string;\n}\n\ninterface SkillScore {\n\treadonly name: string;\n\treadonly score: number;\n}\n\ninterface SelectedSkill extends SkillScore {\n\treadonly description: string;\n}\n\nexport interface SelectSkillsInput {\n\treadonly task: string;\n\treadonly skills: readonly SkillCandidate[];\n\treadonly max?: number;\n\t/** Path segments become independent, camelCase-aware skill-name signals. */\n\treadonly paths?: readonly string[];\n}\n\nexport interface SkillSelectionResult {\n\treadonly selected: readonly SelectedSkill[];\n\treadonly scores: readonly SkillScore[];\n\treadonly confidence: SkillConfidence;\n\treadonly ambiguous: boolean;\n\treadonly reason: string;\n}\n\nexport function selectSkills(input: SelectSkillsInput): SkillSelectionResult {\n\tconst task = input.task.trim();\n\tconst pathTokens = pathHintTokens(input.paths);\n\tconst max = resolveMax(input.max);\n\tconst skills = uniqueSkills(input.skills);\n\tif ((task === \"\" && pathTokens.length === 0) || skills.length === 0) {\n\t\treturn fallback([], \"no skill signals detected\");\n\t}\n\n\tconst ranked = skills\n\t\t.map((skill, index) => {\n\t\t\tlet score = task === \"\" ? 0 : scoreSkillRelevance(skill, task);\n\t\t\tconst pathIdentity = { name: skill.name, description: \"\" };\n\t\t\tfor (const token of pathTokens) {\n\t\t\t\tscore = Math.max(score, scoreSkillRelevance(pathIdentity, token));\n\t\t\t}\n\t\t\treturn { skill, index, score };\n\t\t})\n\t\t.sort((left, right) => {\n\t\t\tif (right.score !== left.score) return right.score - left.score;\n\t\t\treturn left.index - right.index;\n\t\t});\n\n\tconst scores: SkillScore[] = ranked.map((entry) => ({ name: entry.skill.name, score: entry.score }));\n\tconst aboveWeak = ranked.filter((entry) => entry.score >= SKILL_WEAK_THRESHOLD);\n\tif (aboveWeak.length === 0) {\n\t\treturn fallback(scores, \"no skill signals detected\");\n\t}\n\n\tconst leader = aboveWeak[0];\n\tif (!leader) {\n\t\treturn fallback(scores, \"no skill signals detected\");\n\t}\n\tconst selected = aboveWeak.slice(0, Math.max(0, max)).map((entry) => ({\n\t\tname: entry.skill.name,\n\t\tdescription: entry.skill.description,\n\t\tscore: entry.score,\n\t}));\n\tconst runner = aboveWeak[1];\n\tconst confidence: SkillConfidence = leader.score >= SKILL_STRONG_THRESHOLD ? \"confident\" : \"tentative\";\n\tconst ambiguous =\n\t\tconfidence === \"tentative\" && runner !== undefined && leader.score - runner.score <= SKILL_AMBIGUITY_MARGIN;\n\tconst reason =\n\t\tambiguous && runner\n\t\t\t? `ambiguous: ${leader.skill.name}(${formatScore(leader.score)}) vs ${runner.skill.name}(${formatScore(runner.score)})`\n\t\t\t: `${leader.skill.name} selected (${formatScore(leader.score)})`;\n\n\treturn { selected, scores, confidence, ambiguous, reason };\n}\n\nfunction fallback(scores: readonly SkillScore[], reason: string): SkillSelectionResult {\n\treturn { selected: [], scores, confidence: \"fallback\", ambiguous: false, reason };\n}\n\nfunction formatScore(score: number): string {\n\treturn score.toFixed(2);\n}\n\nfunction resolveMax(max: number | undefined): number {\n\tif (max === undefined || !Number.isFinite(max) || max < 0) return MAX_SELECTED_SKILLS;\n\treturn Math.min(Math.floor(max), MAX_SELECTED_SKILLS);\n}\n\nfunction uniqueSkills(skills: readonly SkillCandidate[]): SkillCandidate[] {\n\tconst seen = new Set<string>();\n\tconst unique: SkillCandidate[] = [];\n\tfor (const skill of skills) {\n\t\tif (seen.has(skill.name)) continue;\n\t\tseen.add(skill.name);\n\t\tunique.push(skill);\n\t}\n\treturn unique;\n}\n\nfunction pathHintTokens(paths: readonly string[] | undefined): string[] {\n\tif (!paths || paths.length === 0) return [];\n\tconst tokens = new Set<string>();\n\tfor (const path of paths) {\n\t\tconst splitCase = path.replace(/([A-Z]+)([A-Z][a-z])/g, \"$1 $2\").replace(/([a-z0-9])([A-Z])/g, \"$1 $2\");\n\t\tfor (const raw of splitCase.toLowerCase().split(/[/\\\\._\\s-]+/)) {\n\t\t\tif (raw.length > 0) tokens.add(raw);\n\t\t}\n\t}\n\treturn [...tokens];\n}\n"]}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query-aware skill grant selection.
|
|
3
|
+
*
|
|
4
|
+
* Scores candidates with {@link scoreSkillRelevance}, then applies the same
|
|
5
|
+
* ranking/confidence shape as domain routing: weak/strong thresholds, a 2–3
|
|
6
|
+
* skill cap, input-order ties, and an ambiguity flag when a tentative leader
|
|
7
|
+
* is within the documented margin of the runner-up.
|
|
8
|
+
*
|
|
9
|
+
* I/O-free. Callers still decide whether to mark the result active.
|
|
10
|
+
*/
|
|
11
|
+
import { scoreSkillRelevance } from "./context-budget-relevance.js";
|
|
12
|
+
/** Documented lane grant cap: load at most 2–3 skills. */
|
|
13
|
+
export const MAX_SELECTED_SKILLS = 3;
|
|
14
|
+
/** Coverage at/above which a skill is a confident match. */
|
|
15
|
+
export const SKILL_STRONG_THRESHOLD = 0.7;
|
|
16
|
+
/**
|
|
17
|
+
* Minimum score to grant a skill. Neutral (no-query) relevance is 0.3, so the
|
|
18
|
+
* weak floor sits above that and ignores idle overlap.
|
|
19
|
+
*/
|
|
20
|
+
export const SKILL_WEAK_THRESHOLD = 0.35;
|
|
21
|
+
/** If the runner-up is within this margin of a tentative leader, flag ambiguous. */
|
|
22
|
+
export const SKILL_AMBIGUITY_MARGIN = 0.08;
|
|
23
|
+
export function selectSkills(input) {
|
|
24
|
+
const task = input.task.trim();
|
|
25
|
+
const pathTokens = pathHintTokens(input.paths);
|
|
26
|
+
const max = resolveMax(input.max);
|
|
27
|
+
const skills = uniqueSkills(input.skills);
|
|
28
|
+
if ((task === "" && pathTokens.length === 0) || skills.length === 0) {
|
|
29
|
+
return fallback([], "no skill signals detected");
|
|
30
|
+
}
|
|
31
|
+
const ranked = skills
|
|
32
|
+
.map((skill, index) => {
|
|
33
|
+
let score = task === "" ? 0 : scoreSkillRelevance(skill, task);
|
|
34
|
+
const pathIdentity = { name: skill.name, description: "" };
|
|
35
|
+
for (const token of pathTokens) {
|
|
36
|
+
score = Math.max(score, scoreSkillRelevance(pathIdentity, token));
|
|
37
|
+
}
|
|
38
|
+
return { skill, index, score };
|
|
39
|
+
})
|
|
40
|
+
.sort((left, right) => {
|
|
41
|
+
if (right.score !== left.score)
|
|
42
|
+
return right.score - left.score;
|
|
43
|
+
return left.index - right.index;
|
|
44
|
+
});
|
|
45
|
+
const scores = ranked.map((entry) => ({ name: entry.skill.name, score: entry.score }));
|
|
46
|
+
const aboveWeak = ranked.filter((entry) => entry.score >= SKILL_WEAK_THRESHOLD);
|
|
47
|
+
if (aboveWeak.length === 0) {
|
|
48
|
+
return fallback(scores, "no skill signals detected");
|
|
49
|
+
}
|
|
50
|
+
const leader = aboveWeak[0];
|
|
51
|
+
if (!leader) {
|
|
52
|
+
return fallback(scores, "no skill signals detected");
|
|
53
|
+
}
|
|
54
|
+
const selected = aboveWeak.slice(0, Math.max(0, max)).map((entry) => ({
|
|
55
|
+
name: entry.skill.name,
|
|
56
|
+
description: entry.skill.description,
|
|
57
|
+
score: entry.score,
|
|
58
|
+
}));
|
|
59
|
+
const runner = aboveWeak[1];
|
|
60
|
+
const confidence = leader.score >= SKILL_STRONG_THRESHOLD ? "confident" : "tentative";
|
|
61
|
+
const ambiguous = confidence === "tentative" && runner !== undefined && leader.score - runner.score <= SKILL_AMBIGUITY_MARGIN;
|
|
62
|
+
const reason = ambiguous && runner
|
|
63
|
+
? `ambiguous: ${leader.skill.name}(${formatScore(leader.score)}) vs ${runner.skill.name}(${formatScore(runner.score)})`
|
|
64
|
+
: `${leader.skill.name} selected (${formatScore(leader.score)})`;
|
|
65
|
+
return { selected, scores, confidence, ambiguous, reason };
|
|
66
|
+
}
|
|
67
|
+
function fallback(scores, reason) {
|
|
68
|
+
return { selected: [], scores, confidence: "fallback", ambiguous: false, reason };
|
|
69
|
+
}
|
|
70
|
+
function formatScore(score) {
|
|
71
|
+
return score.toFixed(2);
|
|
72
|
+
}
|
|
73
|
+
function resolveMax(max) {
|
|
74
|
+
if (max === undefined || !Number.isFinite(max) || max < 0)
|
|
75
|
+
return MAX_SELECTED_SKILLS;
|
|
76
|
+
return Math.min(Math.floor(max), MAX_SELECTED_SKILLS);
|
|
77
|
+
}
|
|
78
|
+
function uniqueSkills(skills) {
|
|
79
|
+
const seen = new Set();
|
|
80
|
+
const unique = [];
|
|
81
|
+
for (const skill of skills) {
|
|
82
|
+
if (seen.has(skill.name))
|
|
83
|
+
continue;
|
|
84
|
+
seen.add(skill.name);
|
|
85
|
+
unique.push(skill);
|
|
86
|
+
}
|
|
87
|
+
return unique;
|
|
88
|
+
}
|
|
89
|
+
function pathHintTokens(paths) {
|
|
90
|
+
if (!paths || paths.length === 0)
|
|
91
|
+
return [];
|
|
92
|
+
const tokens = new Set();
|
|
93
|
+
for (const path of paths) {
|
|
94
|
+
const splitCase = path.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/([a-z0-9])([A-Z])/g, "$1 $2");
|
|
95
|
+
for (const raw of splitCase.toLowerCase().split(/[/\\._\s-]+/)) {
|
|
96
|
+
if (raw.length > 0)
|
|
97
|
+
tokens.add(raw);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return [...tokens];
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=skill-selector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-selector.js","sourceRoot":"","sources":["../../src/core/skill-selector.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Query-aware skill grant selection.\n *\n * Scores candidates with {@link scoreSkillRelevance}, then applies the same\n * ranking/confidence shape as domain routing: weak/strong thresholds, a 2–3\n * skill cap, input-order ties, and an ambiguity flag when a tentative leader\n * is within the documented margin of the runner-up.\n *\n * I/O-free. Callers still decide whether to mark the result active.\n */\nimport { scoreSkillRelevance } from \"./context-budget-relevance.ts\";\n\n/** Documented lane grant cap: load at most 2–3 skills. */\nexport const MAX_SELECTED_SKILLS = 3;\n/** Coverage at/above which a skill is a confident match. */\nexport const SKILL_STRONG_THRESHOLD = 0.7;\n/**\n * Minimum score to grant a skill. Neutral (no-query) relevance is 0.3, so the\n * weak floor sits above that and ignores idle overlap.\n */\nexport const SKILL_WEAK_THRESHOLD = 0.35;\n/** If the runner-up is within this margin of a tentative leader, flag ambiguous. */\nexport const SKILL_AMBIGUITY_MARGIN = 0.08;\n\ntype SkillConfidence = \"confident\" | \"tentative\" | \"fallback\";\n\nexport interface SkillCandidate {\n\treadonly name: string;\n\treadonly description: string;\n}\n\ninterface SkillScore {\n\treadonly name: string;\n\treadonly score: number;\n}\n\ninterface SelectedSkill extends SkillScore {\n\treadonly description: string;\n}\n\nexport interface SelectSkillsInput {\n\treadonly task: string;\n\treadonly skills: readonly SkillCandidate[];\n\treadonly max?: number;\n\t/** Path segments become independent, camelCase-aware skill-name signals. */\n\treadonly paths?: readonly string[];\n}\n\nexport interface SkillSelectionResult {\n\treadonly selected: readonly SelectedSkill[];\n\treadonly scores: readonly SkillScore[];\n\treadonly confidence: SkillConfidence;\n\treadonly ambiguous: boolean;\n\treadonly reason: string;\n}\n\nexport function selectSkills(input: SelectSkillsInput): SkillSelectionResult {\n\tconst task = input.task.trim();\n\tconst pathTokens = pathHintTokens(input.paths);\n\tconst max = resolveMax(input.max);\n\tconst skills = uniqueSkills(input.skills);\n\tif ((task === \"\" && pathTokens.length === 0) || skills.length === 0) {\n\t\treturn fallback([], \"no skill signals detected\");\n\t}\n\n\tconst ranked = skills\n\t\t.map((skill, index) => {\n\t\t\tlet score = task === \"\" ? 0 : scoreSkillRelevance(skill, task);\n\t\t\tconst pathIdentity = { name: skill.name, description: \"\" };\n\t\t\tfor (const token of pathTokens) {\n\t\t\t\tscore = Math.max(score, scoreSkillRelevance(pathIdentity, token));\n\t\t\t}\n\t\t\treturn { skill, index, score };\n\t\t})\n\t\t.sort((left, right) => {\n\t\t\tif (right.score !== left.score) return right.score - left.score;\n\t\t\treturn left.index - right.index;\n\t\t});\n\n\tconst scores: SkillScore[] = ranked.map((entry) => ({ name: entry.skill.name, score: entry.score }));\n\tconst aboveWeak = ranked.filter((entry) => entry.score >= SKILL_WEAK_THRESHOLD);\n\tif (aboveWeak.length === 0) {\n\t\treturn fallback(scores, \"no skill signals detected\");\n\t}\n\n\tconst leader = aboveWeak[0];\n\tif (!leader) {\n\t\treturn fallback(scores, \"no skill signals detected\");\n\t}\n\tconst selected = aboveWeak.slice(0, Math.max(0, max)).map((entry) => ({\n\t\tname: entry.skill.name,\n\t\tdescription: entry.skill.description,\n\t\tscore: entry.score,\n\t}));\n\tconst runner = aboveWeak[1];\n\tconst confidence: SkillConfidence = leader.score >= SKILL_STRONG_THRESHOLD ? \"confident\" : \"tentative\";\n\tconst ambiguous =\n\t\tconfidence === \"tentative\" && runner !== undefined && leader.score - runner.score <= SKILL_AMBIGUITY_MARGIN;\n\tconst reason =\n\t\tambiguous && runner\n\t\t\t? `ambiguous: ${leader.skill.name}(${formatScore(leader.score)}) vs ${runner.skill.name}(${formatScore(runner.score)})`\n\t\t\t: `${leader.skill.name} selected (${formatScore(leader.score)})`;\n\n\treturn { selected, scores, confidence, ambiguous, reason };\n}\n\nfunction fallback(scores: readonly SkillScore[], reason: string): SkillSelectionResult {\n\treturn { selected: [], scores, confidence: \"fallback\", ambiguous: false, reason };\n}\n\nfunction formatScore(score: number): string {\n\treturn score.toFixed(2);\n}\n\nfunction resolveMax(max: number | undefined): number {\n\tif (max === undefined || !Number.isFinite(max) || max < 0) return MAX_SELECTED_SKILLS;\n\treturn Math.min(Math.floor(max), MAX_SELECTED_SKILLS);\n}\n\nfunction uniqueSkills(skills: readonly SkillCandidate[]): SkillCandidate[] {\n\tconst seen = new Set<string>();\n\tconst unique: SkillCandidate[] = [];\n\tfor (const skill of skills) {\n\t\tif (seen.has(skill.name)) continue;\n\t\tseen.add(skill.name);\n\t\tunique.push(skill);\n\t}\n\treturn unique;\n}\n\nfunction pathHintTokens(paths: readonly string[] | undefined): string[] {\n\tif (!paths || paths.length === 0) return [];\n\tconst tokens = new Set<string>();\n\tfor (const path of paths) {\n\t\tconst splitCase = path.replace(/([A-Z]+)([A-Z][a-z])/g, \"$1 $2\").replace(/([a-z0-9])([A-Z])/g, \"$1 $2\");\n\t\tfor (const raw of splitCase.toLowerCase().split(/[/\\\\._\\s-]+/)) {\n\t\t\tif (raw.length > 0) tokens.add(raw);\n\t\t}\n\t}\n\treturn [...tokens];\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-utils.d.ts","sourceRoot":"","sources":["../../../src/core/tools/render-utils.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"render-utils.d.ts","sourceRoot":"","sources":["../../../src/core/tools/render-utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAExD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,wCAAwC,CAAC;AAKpE,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAOjD;AAED,wBAAgB,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAGjF;AAED,wBAAgB,GAAG,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAIjD;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,wBAAgB,aAAa,CAC5B,MAAM,EAAE;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,GAAG,SAAS,EACzG,UAAU,EAAE,OAAO,GACjB,MAAM,CAsBR;AAED,MAAM,MAAM,oBAAoB,CAAC,QAAQ,IAAI;IAC5C,OAAO,EAAE,CAAC,WAAW,GAAG,YAAY,CAAC,EAAE,CAAC;IACxC,OAAO,EAAE,QAAQ,CAAC;CAClB,CAAC;AAEF,wBAAgB,cAAc,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAEnD;AAED,wBAAgB,cAAc,CAC7B,OAAO,EAAE,MAAM,GAAG,IAAI,EACtB,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,GAClC,MAAM,CAKR","sourcesContent":["import * as os from \"node:os\";\nimport type { ImageContent, TextContent } from \"omk-ai\";\nimport { getCapabilities, getImageDimensions, hyperlink, imageFallback } from \"omk-tui\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport { stripAnsi } from \"../../utils/ansi.ts\";\nimport { sanitizeBinaryOutput } from \"../../utils/shell.ts\";\nimport { terminalFileUrl } from \"../../utils/terminal-links.ts\";\n\nexport function shortenPath(path: unknown): string {\n\tif (typeof path !== \"string\") return \"\";\n\tconst home = os.homedir();\n\tif (path.startsWith(home)) {\n\t\treturn `~${path.slice(home.length)}`;\n\t}\n\treturn path;\n}\n\nexport function linkPath(styledText: string, rawPath: string, cwd: string): string {\n\tif (!getCapabilities().hyperlinks) return styledText;\n\treturn hyperlink(styledText, terminalFileUrl(rawPath, cwd));\n}\n\nexport function str(value: unknown): string | null {\n\tif (typeof value === \"string\") return value;\n\tif (value == null) return \"\";\n\treturn null;\n}\n\nexport function replaceTabs(text: string): string {\n\treturn text.replace(/\\t/g, \" \");\n}\n\nexport function normalizeDisplayText(text: string): string {\n\treturn text.replace(/\\r/g, \"\");\n}\n\nexport function getTextOutput(\n\tresult: { content: Array<{ type: string; text?: string; data?: string; mimeType?: string }> } | undefined,\n\tshowImages: boolean,\n): string {\n\tif (!result) return \"\";\n\n\tconst textBlocks = result.content.filter((c) => c.type === \"text\");\n\tconst imageBlocks = result.content.filter((c) => c.type === \"image\");\n\n\tlet output = textBlocks.map((c) => sanitizeBinaryOutput(stripAnsi(c.text || \"\")).replace(/\\r/g, \"\")).join(\"\\n\");\n\n\tconst caps = getCapabilities();\n\tif (imageBlocks.length > 0 && (!caps.images || !showImages)) {\n\t\tconst imageIndicators = imageBlocks\n\t\t\t.map((img) => {\n\t\t\t\tconst mimeType = img.mimeType ?? \"image/unknown\";\n\t\t\t\tconst dims =\n\t\t\t\t\timg.data && img.mimeType ? (getImageDimensions(img.data, img.mimeType) ?? undefined) : undefined;\n\t\t\t\treturn imageFallback(mimeType, dims);\n\t\t\t})\n\t\t\t.join(\"\\n\");\n\t\toutput = output ? `${output}\\n${imageIndicators}` : imageIndicators;\n\t}\n\n\treturn output;\n}\n\nexport type ToolRenderResultLike<TDetails> = {\n\tcontent: (TextContent | ImageContent)[];\n\tdetails: TDetails;\n};\n\nexport function invalidArgText(theme: Theme): string {\n\treturn theme.fg(\"error\", \"[invalid arg]\");\n}\n\nexport function renderToolPath(\n\trawPath: string | null,\n\ttheme: Theme,\n\tcwd: string,\n\toptions?: { emptyFallback?: string },\n): string {\n\tif (rawPath === null) return invalidArgText(theme);\n\tconst value = rawPath || options?.emptyFallback;\n\tif (!value) return theme.fg(\"toolOutput\", \"...\");\n\treturn linkPath(theme.fg(\"accent\", shortenPath(value)), value, cwd);\n}\n"]}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import * as os from "node:os";
|
|
2
|
-
import { pathToFileURL } from "node:url";
|
|
3
2
|
import { getCapabilities, getImageDimensions, hyperlink, imageFallback } from "omk-tui";
|
|
4
3
|
import { stripAnsi } from "../../utils/ansi.js";
|
|
5
|
-
import { resolvePath } from "../../utils/paths.js";
|
|
6
4
|
import { sanitizeBinaryOutput } from "../../utils/shell.js";
|
|
5
|
+
import { terminalFileUrl } from "../../utils/terminal-links.js";
|
|
7
6
|
export function shortenPath(path) {
|
|
8
7
|
if (typeof path !== "string")
|
|
9
8
|
return "";
|
|
@@ -16,8 +15,7 @@ export function shortenPath(path) {
|
|
|
16
15
|
export function linkPath(styledText, rawPath, cwd) {
|
|
17
16
|
if (!getCapabilities().hyperlinks)
|
|
18
17
|
return styledText;
|
|
19
|
-
|
|
20
|
-
return hyperlink(styledText, pathToFileURL(absolutePath).href);
|
|
18
|
+
return hyperlink(styledText, terminalFileUrl(rawPath, cwd));
|
|
21
19
|
}
|
|
22
20
|
export function str(value) {
|
|
23
21
|
if (typeof value === "string")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-utils.js","sourceRoot":"","sources":["../../../src/core/tools/render-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"render-utils.js","sourceRoot":"","sources":["../../../src/core/tools/render-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAE9B,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAExF,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAEhE,MAAM,UAAU,WAAW,CAAC,IAAa,EAAU;IAClD,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAC;IACxC,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC;IAC1B,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3B,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;IACtC,CAAC;IACD,OAAO,IAAI,CAAC;AAAA,CACZ;AAED,MAAM,UAAU,QAAQ,CAAC,UAAkB,EAAE,OAAe,EAAE,GAAW,EAAU;IAClF,IAAI,CAAC,eAAe,EAAE,CAAC,UAAU;QAAE,OAAO,UAAU,CAAC;IACrD,OAAO,SAAS,CAAC,UAAU,EAAE,eAAe,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC;AAAA,CAC5D;AAED,MAAM,UAAU,GAAG,CAAC,KAAc,EAAiB;IAClD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5C,IAAI,KAAK,IAAI,IAAI;QAAE,OAAO,EAAE,CAAC;IAC7B,OAAO,IAAI,CAAC;AAAA,CACZ;AAED,MAAM,UAAU,WAAW,CAAC,IAAY,EAAU;IACjD,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;AAAA,CAClC;AAED,MAAM,UAAU,oBAAoB,CAAC,IAAY,EAAU;IAC1D,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AAAA,CAC/B;AAED,MAAM,UAAU,aAAa,CAC5B,MAAyG,EACzG,UAAmB,EACV;IACT,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,CAAC;IAEvB,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC;IACnE,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC;IAErE,IAAI,MAAM,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,oBAAoB,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEhH,MAAM,IAAI,GAAG,eAAe,EAAE,CAAC;IAC/B,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QAC7D,MAAM,eAAe,GAAG,WAAW;aACjC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC;YACb,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,IAAI,eAAe,CAAC;YACjD,MAAM,IAAI,GACT,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,QAAQ,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YAClG,OAAO,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;QAAA,CACrC,CAAC;aACD,IAAI,CAAC,IAAI,CAAC,CAAC;QACb,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,KAAK,eAAe,EAAE,CAAC,CAAC,CAAC,eAAe,CAAC;IACrE,CAAC;IAED,OAAO,MAAM,CAAC;AAAA,CACd;AAOD,MAAM,UAAU,cAAc,CAAC,KAAY,EAAU;IACpD,OAAO,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC;AAAA,CAC1C;AAED,MAAM,UAAU,cAAc,CAC7B,OAAsB,EACtB,KAAY,EACZ,GAAW,EACX,OAAoC,EAC3B;IACT,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,cAAc,CAAC,KAAK,CAAC,CAAC;IACnD,MAAM,KAAK,GAAG,OAAO,IAAI,OAAO,EAAE,aAAa,CAAC;IAChD,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC,EAAE,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC;IACjD,OAAO,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;AAAA,CACpE","sourcesContent":["import * as os from \"node:os\";\nimport type { ImageContent, TextContent } from \"omk-ai\";\nimport { getCapabilities, getImageDimensions, hyperlink, imageFallback } from \"omk-tui\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport { stripAnsi } from \"../../utils/ansi.ts\";\nimport { sanitizeBinaryOutput } from \"../../utils/shell.ts\";\nimport { terminalFileUrl } from \"../../utils/terminal-links.ts\";\n\nexport function shortenPath(path: unknown): string {\n\tif (typeof path !== \"string\") return \"\";\n\tconst home = os.homedir();\n\tif (path.startsWith(home)) {\n\t\treturn `~${path.slice(home.length)}`;\n\t}\n\treturn path;\n}\n\nexport function linkPath(styledText: string, rawPath: string, cwd: string): string {\n\tif (!getCapabilities().hyperlinks) return styledText;\n\treturn hyperlink(styledText, terminalFileUrl(rawPath, cwd));\n}\n\nexport function str(value: unknown): string | null {\n\tif (typeof value === \"string\") return value;\n\tif (value == null) return \"\";\n\treturn null;\n}\n\nexport function replaceTabs(text: string): string {\n\treturn text.replace(/\\t/g, \" \");\n}\n\nexport function normalizeDisplayText(text: string): string {\n\treturn text.replace(/\\r/g, \"\");\n}\n\nexport function getTextOutput(\n\tresult: { content: Array<{ type: string; text?: string; data?: string; mimeType?: string }> } | undefined,\n\tshowImages: boolean,\n): string {\n\tif (!result) return \"\";\n\n\tconst textBlocks = result.content.filter((c) => c.type === \"text\");\n\tconst imageBlocks = result.content.filter((c) => c.type === \"image\");\n\n\tlet output = textBlocks.map((c) => sanitizeBinaryOutput(stripAnsi(c.text || \"\")).replace(/\\r/g, \"\")).join(\"\\n\");\n\n\tconst caps = getCapabilities();\n\tif (imageBlocks.length > 0 && (!caps.images || !showImages)) {\n\t\tconst imageIndicators = imageBlocks\n\t\t\t.map((img) => {\n\t\t\t\tconst mimeType = img.mimeType ?? \"image/unknown\";\n\t\t\t\tconst dims =\n\t\t\t\t\timg.data && img.mimeType ? (getImageDimensions(img.data, img.mimeType) ?? undefined) : undefined;\n\t\t\t\treturn imageFallback(mimeType, dims);\n\t\t\t})\n\t\t\t.join(\"\\n\");\n\t\toutput = output ? `${output}\\n${imageIndicators}` : imageIndicators;\n\t}\n\n\treturn output;\n}\n\nexport type ToolRenderResultLike<TDetails> = {\n\tcontent: (TextContent | ImageContent)[];\n\tdetails: TDetails;\n};\n\nexport function invalidArgText(theme: Theme): string {\n\treturn theme.fg(\"error\", \"[invalid arg]\");\n}\n\nexport function renderToolPath(\n\trawPath: string | null,\n\ttheme: Theme,\n\tcwd: string,\n\toptions?: { emptyFallback?: string },\n): string {\n\tif (rawPath === null) return invalidArgText(theme);\n\tconst value = rawPath || options?.emptyFallback;\n\tif (!value) return theme.fg(\"toolOutput\", \"...\");\n\treturn linkPath(theme.fg(\"accent\", shortenPath(value)), value, cwd);\n}\n"]}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type RuntimeProvenance } from "./runtime-provenance.ts";
|
|
2
|
+
export declare const TURN_METRICS_SCHEMA_VERSION: "omk-turn-metrics-2";
|
|
3
|
+
/** Compatibility constant: maximum failure text inspected for classification, never retained. */
|
|
4
|
+
export declare const MAX_ERROR_CHARS = 200;
|
|
5
|
+
declare const ERROR_CLASSES: readonly ["timeout", "aborted", "permission", "not_found", "invalid_input", "unknown"];
|
|
6
|
+
type ErrorClass = (typeof ERROR_CLASSES)[number];
|
|
7
|
+
export interface ToolCallMetric {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly durationMs: number;
|
|
10
|
+
readonly ok: boolean;
|
|
11
|
+
/** Input-only failure text. Never written to the metrics stream. */
|
|
12
|
+
readonly error?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface ToolCallMetricRecord extends Omit<ToolCallMetric, "error"> {
|
|
15
|
+
readonly errorClass?: ErrorClass;
|
|
16
|
+
}
|
|
17
|
+
export interface TurnUsageMetric {
|
|
18
|
+
readonly input: number;
|
|
19
|
+
readonly output: number;
|
|
20
|
+
readonly cacheRead: number;
|
|
21
|
+
readonly cacheWrite: number;
|
|
22
|
+
readonly costUsd: number;
|
|
23
|
+
}
|
|
24
|
+
export interface TurnMetricInput {
|
|
25
|
+
readonly sessionId: string;
|
|
26
|
+
readonly turnIndex: number;
|
|
27
|
+
readonly provider?: string;
|
|
28
|
+
readonly model?: string;
|
|
29
|
+
readonly startedAtEpochMs: number;
|
|
30
|
+
readonly endedAtEpochMs: number;
|
|
31
|
+
readonly timeToFirstChunkMs?: number;
|
|
32
|
+
readonly usage?: TurnUsageMetric;
|
|
33
|
+
readonly stopReason?: string;
|
|
34
|
+
readonly toolCalls?: readonly ToolCallMetric[];
|
|
35
|
+
readonly compacted?: boolean;
|
|
36
|
+
readonly failovers?: number;
|
|
37
|
+
readonly contextCache?: {
|
|
38
|
+
readonly planHit: boolean;
|
|
39
|
+
readonly hits: number;
|
|
40
|
+
readonly misses: number;
|
|
41
|
+
};
|
|
42
|
+
readonly runtimeProvenance?: RuntimeProvenance;
|
|
43
|
+
}
|
|
44
|
+
export interface TurnMetricRecord extends Omit<TurnMetricInput, "toolCalls"> {
|
|
45
|
+
readonly schemaVersion: typeof TURN_METRICS_SCHEMA_VERSION;
|
|
46
|
+
readonly durationMs: number;
|
|
47
|
+
readonly toolCalls: readonly ToolCallMetricRecord[];
|
|
48
|
+
readonly toolCallCount: number;
|
|
49
|
+
readonly toolFailureCount: number;
|
|
50
|
+
}
|
|
51
|
+
export declare function buildTurnMetricRecord(input: TurnMetricInput): TurnMetricRecord;
|
|
52
|
+
/** Invalid records count as malformed; legacy records are projected, never rewritten on disk. */
|
|
53
|
+
export declare function parseTurnMetricRecord(line: string): TurnMetricRecord | undefined;
|
|
54
|
+
export {};
|
|
55
|
+
//# sourceMappingURL=turn-metrics-record.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turn-metrics-record.d.ts","sourceRoot":"","sources":["../../src/core/turn-metrics-record.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,KAAK,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAEzF,eAAO,MAAM,2BAA2B,sBAAgC,CAAC;AACzE,iGAAiG;AACjG,eAAO,MAAM,eAAe,MAAM,CAAC;AACnC,QAAA,MAAM,aAAa,wFAAyF,CAAC;AAC7G,KAAK,UAAU,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAGjD,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,oEAAoE;IACpE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACxB;AACD,MAAM,WAAW,oBAAqB,SAAQ,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC;IAC1E,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC;CACjC;AACD,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CACzB;AACD,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,KAAK,CAAC,EAAE,eAAe,CAAC;IACjC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IAC/C,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,CAAC,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACtG,QAAQ,CAAC,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;CAC/C;AACD,MAAM,WAAW,gBAAiB,SAAQ,IAAI,CAAC,eAAe,EAAE,WAAW,CAAC;IAC3E,QAAQ,CAAC,aAAa,EAAE,OAAO,2BAA2B,CAAC;IAC3D,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,SAAS,oBAAoB,EAAE,CAAC;IACpD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;CAClC;AA+GD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,eAAe,GAAG,gBAAgB,CAE9E;AAED,iGAAiG;AACjG,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAiBhF","sourcesContent":["import { parseRuntimeProvenance, type RuntimeProvenance } from \"./runtime-provenance.ts\";\n\nexport const TURN_METRICS_SCHEMA_VERSION = \"omk-turn-metrics-2\" as const;\n/** Compatibility constant: maximum failure text inspected for classification, never retained. */\nexport const MAX_ERROR_CHARS = 200;\nconst ERROR_CLASSES = [\"timeout\", \"aborted\", \"permission\", \"not_found\", \"invalid_input\", \"unknown\"] as const;\ntype ErrorClass = (typeof ERROR_CLASSES)[number];\nconst STOP_REASONS = [\"stop\", \"length\", \"toolUse\", \"error\", \"aborted\"];\n\nexport interface ToolCallMetric {\n\treadonly name: string;\n\treadonly durationMs: number;\n\treadonly ok: boolean;\n\t/** Input-only failure text. Never written to the metrics stream. */\n\treadonly error?: string;\n}\nexport interface ToolCallMetricRecord extends Omit<ToolCallMetric, \"error\"> {\n\treadonly errorClass?: ErrorClass;\n}\nexport interface TurnUsageMetric {\n\treadonly input: number;\n\treadonly output: number;\n\treadonly cacheRead: number;\n\treadonly cacheWrite: number;\n\treadonly costUsd: number;\n}\nexport interface TurnMetricInput {\n\treadonly sessionId: string;\n\treadonly turnIndex: number;\n\treadonly provider?: string;\n\treadonly model?: string;\n\treadonly startedAtEpochMs: number;\n\treadonly endedAtEpochMs: number;\n\treadonly timeToFirstChunkMs?: number;\n\treadonly usage?: TurnUsageMetric;\n\treadonly stopReason?: string;\n\treadonly toolCalls?: readonly ToolCallMetric[];\n\treadonly compacted?: boolean;\n\treadonly failovers?: number;\n\treadonly contextCache?: { readonly planHit: boolean; readonly hits: number; readonly misses: number };\n\treadonly runtimeProvenance?: RuntimeProvenance;\n}\nexport interface TurnMetricRecord extends Omit<TurnMetricInput, \"toolCalls\"> {\n\treadonly schemaVersion: typeof TURN_METRICS_SCHEMA_VERSION;\n\treadonly durationMs: number;\n\treadonly toolCalls: readonly ToolCallMetricRecord[];\n\treadonly toolCallCount: number;\n\treadonly toolFailureCount: number;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\nfunction object(value: unknown): Record<string, unknown> {\n\tif (!isRecord(value)) throw new TypeError(\"Invalid metric object\");\n\treturn value;\n}\nfunction finite(value: unknown): number {\n\tif (typeof value !== \"number\" || !Number.isFinite(value)) throw new TypeError(\"Invalid metric number\");\n\treturn value;\n}\nfunction quantity(value: unknown, integer = false): number {\n\tconst number = finite(value);\n\tif (number < 0 || (integer && !Number.isSafeInteger(number))) throw new TypeError(\"Invalid metric quantity\");\n\treturn number;\n}\nfunction identifier(value: unknown): string {\n\tif (typeof value !== \"string\" || !value.trim() || value.length > 512 || /[\\u0000-\\u001f\\u007f]/u.test(value))\n\t\tthrow new TypeError(\"Invalid metric identifier\");\n\treturn value;\n}\nfunction boolean(value: unknown): boolean {\n\tif (typeof value !== \"boolean\") throw new TypeError(\"Invalid metric boolean\");\n\treturn value;\n}\nfunction classifyError(value: unknown): ErrorClass {\n\tif (value !== undefined && typeof value !== \"string\") throw new TypeError(\"Invalid metric error text\");\n\tconst text = value?.slice(0, MAX_ERROR_CHARS) ?? \"\";\n\tif (/timed? out|timeout/iu.test(text)) return \"timeout\";\n\tif (/abort|cancel/iu.test(text)) return \"aborted\";\n\tif (/permission|eacces|eperm/iu.test(text)) return \"permission\";\n\tif (/not found|enoent/iu.test(text)) return \"not_found\";\n\tif (/invalid|validation/iu.test(text)) return \"invalid_input\";\n\treturn \"unknown\";\n}\nfunction parseErrorClass(value: unknown): ErrorClass | undefined {\n\tif (value === undefined) return undefined;\n\tfor (const code of ERROR_CLASSES) if (value === code) return code;\n\tthrow new TypeError(\"Invalid metric error class\");\n}\n\n/** Explicit projection is shared by new input and legacy/current persisted records. */\nfunction project(value: unknown, source: \"input\" | \"v1\" | \"v2\"): TurnMetricRecord {\n\tconst input = object(value);\n\tconst rawTools = input.toolCalls === undefined ? [] : input.toolCalls;\n\tif (!Array.isArray(rawTools) || rawTools.length > 10000) throw new TypeError(\"Invalid metric tool list\");\n\tconst toolCalls = Array.from(rawTools, (raw): ToolCallMetricRecord => {\n\t\tconst call = object(raw);\n\t\tconst ok = boolean(call.ok);\n\t\tif (source === \"v2\" && call.error !== undefined) throw new TypeError(\"Raw errors are not v2 metrics\");\n\t\tconst errorClass = source === \"v2\" ? parseErrorClass(call.errorClass) : classifyError(call.error);\n\t\treturn {\n\t\t\tname: identifier(call.name),\n\t\t\tdurationMs: source === \"input\" ? Math.max(0, Math.round(finite(call.durationMs))) : quantity(call.durationMs),\n\t\t\tok,\n\t\t\t...(!ok && errorClass ? { errorClass } : {}),\n\t\t};\n\t});\n\tconst usage = input.usage === undefined ? undefined : object(input.usage);\n\tconst contextCache = input.contextCache === undefined ? undefined : object(input.contextCache);\n\tconst runtimeProvenance =\n\t\tinput.runtimeProvenance === undefined ? undefined : parseRuntimeProvenance(input.runtimeProvenance);\n\tconst startedAtEpochMs = quantity(input.startedAtEpochMs);\n\tconst endedAtEpochMs = quantity(input.endedAtEpochMs);\n\tif (\n\t\tinput.stopReason !== undefined &&\n\t\t(typeof input.stopReason !== \"string\" || !STOP_REASONS.includes(input.stopReason))\n\t)\n\t\tthrow new TypeError(\"Invalid metric stop reason\");\n\treturn {\n\t\tschemaVersion: TURN_METRICS_SCHEMA_VERSION,\n\t\tsessionId: identifier(input.sessionId),\n\t\tturnIndex: quantity(input.turnIndex, true),\n\t\tstartedAtEpochMs,\n\t\tendedAtEpochMs,\n\t\tdurationMs: Math.max(0, endedAtEpochMs - startedAtEpochMs),\n\t\ttoolCalls,\n\t\ttoolCallCount: toolCalls.length,\n\t\ttoolFailureCount: toolCalls.filter((call) => !call.ok).length,\n\t\t...(input.provider === undefined ? {} : { provider: identifier(input.provider) }),\n\t\t...(input.model === undefined ? {} : { model: identifier(input.model) }),\n\t\t...(input.timeToFirstChunkMs === undefined ? {} : { timeToFirstChunkMs: quantity(input.timeToFirstChunkMs) }),\n\t\t...(usage === undefined\n\t\t\t? {}\n\t\t\t: {\n\t\t\t\t\tusage: {\n\t\t\t\t\t\tinput: quantity(usage.input, true),\n\t\t\t\t\t\toutput: quantity(usage.output, true),\n\t\t\t\t\t\tcacheRead: quantity(usage.cacheRead, true),\n\t\t\t\t\t\tcacheWrite: quantity(usage.cacheWrite, true),\n\t\t\t\t\t\tcostUsd: quantity(usage.costUsd),\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t...(input.stopReason === undefined ? {} : { stopReason: input.stopReason }),\n\t\t...(input.compacted === undefined ? {} : { compacted: boolean(input.compacted) }),\n\t\t...(input.failovers === undefined ? {} : { failovers: quantity(input.failovers, true) }),\n\t\t...(contextCache === undefined\n\t\t\t? {}\n\t\t\t: {\n\t\t\t\t\tcontextCache: {\n\t\t\t\t\t\tplanHit: boolean(contextCache.planHit),\n\t\t\t\t\t\thits: quantity(contextCache.hits, true),\n\t\t\t\t\t\tmisses: quantity(contextCache.misses, true),\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t...(runtimeProvenance ? { runtimeProvenance } : {}),\n\t};\n}\n\nexport function buildTurnMetricRecord(input: TurnMetricInput): TurnMetricRecord {\n\treturn project(input, \"input\");\n}\n\n/** Invalid records count as malformed; legacy records are projected, never rewritten on disk. */\nexport function parseTurnMetricRecord(line: string): TurnMetricRecord | undefined {\n\ttry {\n\t\tconst value = object(JSON.parse(line));\n\t\tconst version = value.schemaVersion;\n\t\tif (version !== \"omk-turn-metrics-1\" && version !== TURN_METRICS_SCHEMA_VERSION) return undefined;\n\t\tconst projected = project(value, version === \"omk-turn-metrics-1\" ? \"v1\" : \"v2\");\n\t\tif (\n\t\t\tquantity(value.durationMs) !== projected.durationMs ||\n\t\t\tquantity(value.toolCallCount, true) !== projected.toolCallCount ||\n\t\t\tquantity(value.toolFailureCount, true) !== projected.toolFailureCount\n\t\t)\n\t\t\treturn undefined;\n\t\treturn projected;\n\t} catch (error) {\n\t\tif (error instanceof SyntaxError || error instanceof TypeError) return undefined;\n\t\tthrow error;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { parseRuntimeProvenance } from "./runtime-provenance.js";
|
|
2
|
+
export const TURN_METRICS_SCHEMA_VERSION = "omk-turn-metrics-2";
|
|
3
|
+
/** Compatibility constant: maximum failure text inspected for classification, never retained. */
|
|
4
|
+
export const MAX_ERROR_CHARS = 200;
|
|
5
|
+
const ERROR_CLASSES = ["timeout", "aborted", "permission", "not_found", "invalid_input", "unknown"];
|
|
6
|
+
const STOP_REASONS = ["stop", "length", "toolUse", "error", "aborted"];
|
|
7
|
+
function isRecord(value) {
|
|
8
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
function object(value) {
|
|
11
|
+
if (!isRecord(value))
|
|
12
|
+
throw new TypeError("Invalid metric object");
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
function finite(value) {
|
|
16
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
17
|
+
throw new TypeError("Invalid metric number");
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function quantity(value, integer = false) {
|
|
21
|
+
const number = finite(value);
|
|
22
|
+
if (number < 0 || (integer && !Number.isSafeInteger(number)))
|
|
23
|
+
throw new TypeError("Invalid metric quantity");
|
|
24
|
+
return number;
|
|
25
|
+
}
|
|
26
|
+
function identifier(value) {
|
|
27
|
+
if (typeof value !== "string" || !value.trim() || value.length > 512 || /[\u0000-\u001f\u007f]/u.test(value))
|
|
28
|
+
throw new TypeError("Invalid metric identifier");
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
function boolean(value) {
|
|
32
|
+
if (typeof value !== "boolean")
|
|
33
|
+
throw new TypeError("Invalid metric boolean");
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
function classifyError(value) {
|
|
37
|
+
if (value !== undefined && typeof value !== "string")
|
|
38
|
+
throw new TypeError("Invalid metric error text");
|
|
39
|
+
const text = value?.slice(0, MAX_ERROR_CHARS) ?? "";
|
|
40
|
+
if (/timed? out|timeout/iu.test(text))
|
|
41
|
+
return "timeout";
|
|
42
|
+
if (/abort|cancel/iu.test(text))
|
|
43
|
+
return "aborted";
|
|
44
|
+
if (/permission|eacces|eperm/iu.test(text))
|
|
45
|
+
return "permission";
|
|
46
|
+
if (/not found|enoent/iu.test(text))
|
|
47
|
+
return "not_found";
|
|
48
|
+
if (/invalid|validation/iu.test(text))
|
|
49
|
+
return "invalid_input";
|
|
50
|
+
return "unknown";
|
|
51
|
+
}
|
|
52
|
+
function parseErrorClass(value) {
|
|
53
|
+
if (value === undefined)
|
|
54
|
+
return undefined;
|
|
55
|
+
for (const code of ERROR_CLASSES)
|
|
56
|
+
if (value === code)
|
|
57
|
+
return code;
|
|
58
|
+
throw new TypeError("Invalid metric error class");
|
|
59
|
+
}
|
|
60
|
+
/** Explicit projection is shared by new input and legacy/current persisted records. */
|
|
61
|
+
function project(value, source) {
|
|
62
|
+
const input = object(value);
|
|
63
|
+
const rawTools = input.toolCalls === undefined ? [] : input.toolCalls;
|
|
64
|
+
if (!Array.isArray(rawTools) || rawTools.length > 10000)
|
|
65
|
+
throw new TypeError("Invalid metric tool list");
|
|
66
|
+
const toolCalls = Array.from(rawTools, (raw) => {
|
|
67
|
+
const call = object(raw);
|
|
68
|
+
const ok = boolean(call.ok);
|
|
69
|
+
if (source === "v2" && call.error !== undefined)
|
|
70
|
+
throw new TypeError("Raw errors are not v2 metrics");
|
|
71
|
+
const errorClass = source === "v2" ? parseErrorClass(call.errorClass) : classifyError(call.error);
|
|
72
|
+
return {
|
|
73
|
+
name: identifier(call.name),
|
|
74
|
+
durationMs: source === "input" ? Math.max(0, Math.round(finite(call.durationMs))) : quantity(call.durationMs),
|
|
75
|
+
ok,
|
|
76
|
+
...(!ok && errorClass ? { errorClass } : {}),
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
const usage = input.usage === undefined ? undefined : object(input.usage);
|
|
80
|
+
const contextCache = input.contextCache === undefined ? undefined : object(input.contextCache);
|
|
81
|
+
const runtimeProvenance = input.runtimeProvenance === undefined ? undefined : parseRuntimeProvenance(input.runtimeProvenance);
|
|
82
|
+
const startedAtEpochMs = quantity(input.startedAtEpochMs);
|
|
83
|
+
const endedAtEpochMs = quantity(input.endedAtEpochMs);
|
|
84
|
+
if (input.stopReason !== undefined &&
|
|
85
|
+
(typeof input.stopReason !== "string" || !STOP_REASONS.includes(input.stopReason)))
|
|
86
|
+
throw new TypeError("Invalid metric stop reason");
|
|
87
|
+
return {
|
|
88
|
+
schemaVersion: TURN_METRICS_SCHEMA_VERSION,
|
|
89
|
+
sessionId: identifier(input.sessionId),
|
|
90
|
+
turnIndex: quantity(input.turnIndex, true),
|
|
91
|
+
startedAtEpochMs,
|
|
92
|
+
endedAtEpochMs,
|
|
93
|
+
durationMs: Math.max(0, endedAtEpochMs - startedAtEpochMs),
|
|
94
|
+
toolCalls,
|
|
95
|
+
toolCallCount: toolCalls.length,
|
|
96
|
+
toolFailureCount: toolCalls.filter((call) => !call.ok).length,
|
|
97
|
+
...(input.provider === undefined ? {} : { provider: identifier(input.provider) }),
|
|
98
|
+
...(input.model === undefined ? {} : { model: identifier(input.model) }),
|
|
99
|
+
...(input.timeToFirstChunkMs === undefined ? {} : { timeToFirstChunkMs: quantity(input.timeToFirstChunkMs) }),
|
|
100
|
+
...(usage === undefined
|
|
101
|
+
? {}
|
|
102
|
+
: {
|
|
103
|
+
usage: {
|
|
104
|
+
input: quantity(usage.input, true),
|
|
105
|
+
output: quantity(usage.output, true),
|
|
106
|
+
cacheRead: quantity(usage.cacheRead, true),
|
|
107
|
+
cacheWrite: quantity(usage.cacheWrite, true),
|
|
108
|
+
costUsd: quantity(usage.costUsd),
|
|
109
|
+
},
|
|
110
|
+
}),
|
|
111
|
+
...(input.stopReason === undefined ? {} : { stopReason: input.stopReason }),
|
|
112
|
+
...(input.compacted === undefined ? {} : { compacted: boolean(input.compacted) }),
|
|
113
|
+
...(input.failovers === undefined ? {} : { failovers: quantity(input.failovers, true) }),
|
|
114
|
+
...(contextCache === undefined
|
|
115
|
+
? {}
|
|
116
|
+
: {
|
|
117
|
+
contextCache: {
|
|
118
|
+
planHit: boolean(contextCache.planHit),
|
|
119
|
+
hits: quantity(contextCache.hits, true),
|
|
120
|
+
misses: quantity(contextCache.misses, true),
|
|
121
|
+
},
|
|
122
|
+
}),
|
|
123
|
+
...(runtimeProvenance ? { runtimeProvenance } : {}),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
export function buildTurnMetricRecord(input) {
|
|
127
|
+
return project(input, "input");
|
|
128
|
+
}
|
|
129
|
+
/** Invalid records count as malformed; legacy records are projected, never rewritten on disk. */
|
|
130
|
+
export function parseTurnMetricRecord(line) {
|
|
131
|
+
try {
|
|
132
|
+
const value = object(JSON.parse(line));
|
|
133
|
+
const version = value.schemaVersion;
|
|
134
|
+
if (version !== "omk-turn-metrics-1" && version !== TURN_METRICS_SCHEMA_VERSION)
|
|
135
|
+
return undefined;
|
|
136
|
+
const projected = project(value, version === "omk-turn-metrics-1" ? "v1" : "v2");
|
|
137
|
+
if (quantity(value.durationMs) !== projected.durationMs ||
|
|
138
|
+
quantity(value.toolCallCount, true) !== projected.toolCallCount ||
|
|
139
|
+
quantity(value.toolFailureCount, true) !== projected.toolFailureCount)
|
|
140
|
+
return undefined;
|
|
141
|
+
return projected;
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (error instanceof SyntaxError || error instanceof TypeError)
|
|
145
|
+
return undefined;
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=turn-metrics-record.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turn-metrics-record.js","sourceRoot":"","sources":["../../src/core/turn-metrics-record.ts"],"names":[],"mappings":"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","sourcesContent":["import { parseRuntimeProvenance, type RuntimeProvenance } from \"./runtime-provenance.ts\";\n\nexport const TURN_METRICS_SCHEMA_VERSION = \"omk-turn-metrics-2\" as const;\n/** Compatibility constant: maximum failure text inspected for classification, never retained. */\nexport const MAX_ERROR_CHARS = 200;\nconst ERROR_CLASSES = [\"timeout\", \"aborted\", \"permission\", \"not_found\", \"invalid_input\", \"unknown\"] as const;\ntype ErrorClass = (typeof ERROR_CLASSES)[number];\nconst STOP_REASONS = [\"stop\", \"length\", \"toolUse\", \"error\", \"aborted\"];\n\nexport interface ToolCallMetric {\n\treadonly name: string;\n\treadonly durationMs: number;\n\treadonly ok: boolean;\n\t/** Input-only failure text. Never written to the metrics stream. */\n\treadonly error?: string;\n}\nexport interface ToolCallMetricRecord extends Omit<ToolCallMetric, \"error\"> {\n\treadonly errorClass?: ErrorClass;\n}\nexport interface TurnUsageMetric {\n\treadonly input: number;\n\treadonly output: number;\n\treadonly cacheRead: number;\n\treadonly cacheWrite: number;\n\treadonly costUsd: number;\n}\nexport interface TurnMetricInput {\n\treadonly sessionId: string;\n\treadonly turnIndex: number;\n\treadonly provider?: string;\n\treadonly model?: string;\n\treadonly startedAtEpochMs: number;\n\treadonly endedAtEpochMs: number;\n\treadonly timeToFirstChunkMs?: number;\n\treadonly usage?: TurnUsageMetric;\n\treadonly stopReason?: string;\n\treadonly toolCalls?: readonly ToolCallMetric[];\n\treadonly compacted?: boolean;\n\treadonly failovers?: number;\n\treadonly contextCache?: { readonly planHit: boolean; readonly hits: number; readonly misses: number };\n\treadonly runtimeProvenance?: RuntimeProvenance;\n}\nexport interface TurnMetricRecord extends Omit<TurnMetricInput, \"toolCalls\"> {\n\treadonly schemaVersion: typeof TURN_METRICS_SCHEMA_VERSION;\n\treadonly durationMs: number;\n\treadonly toolCalls: readonly ToolCallMetricRecord[];\n\treadonly toolCallCount: number;\n\treadonly toolFailureCount: number;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\nfunction object(value: unknown): Record<string, unknown> {\n\tif (!isRecord(value)) throw new TypeError(\"Invalid metric object\");\n\treturn value;\n}\nfunction finite(value: unknown): number {\n\tif (typeof value !== \"number\" || !Number.isFinite(value)) throw new TypeError(\"Invalid metric number\");\n\treturn value;\n}\nfunction quantity(value: unknown, integer = false): number {\n\tconst number = finite(value);\n\tif (number < 0 || (integer && !Number.isSafeInteger(number))) throw new TypeError(\"Invalid metric quantity\");\n\treturn number;\n}\nfunction identifier(value: unknown): string {\n\tif (typeof value !== \"string\" || !value.trim() || value.length > 512 || /[\\u0000-\\u001f\\u007f]/u.test(value))\n\t\tthrow new TypeError(\"Invalid metric identifier\");\n\treturn value;\n}\nfunction boolean(value: unknown): boolean {\n\tif (typeof value !== \"boolean\") throw new TypeError(\"Invalid metric boolean\");\n\treturn value;\n}\nfunction classifyError(value: unknown): ErrorClass {\n\tif (value !== undefined && typeof value !== \"string\") throw new TypeError(\"Invalid metric error text\");\n\tconst text = value?.slice(0, MAX_ERROR_CHARS) ?? \"\";\n\tif (/timed? out|timeout/iu.test(text)) return \"timeout\";\n\tif (/abort|cancel/iu.test(text)) return \"aborted\";\n\tif (/permission|eacces|eperm/iu.test(text)) return \"permission\";\n\tif (/not found|enoent/iu.test(text)) return \"not_found\";\n\tif (/invalid|validation/iu.test(text)) return \"invalid_input\";\n\treturn \"unknown\";\n}\nfunction parseErrorClass(value: unknown): ErrorClass | undefined {\n\tif (value === undefined) return undefined;\n\tfor (const code of ERROR_CLASSES) if (value === code) return code;\n\tthrow new TypeError(\"Invalid metric error class\");\n}\n\n/** Explicit projection is shared by new input and legacy/current persisted records. */\nfunction project(value: unknown, source: \"input\" | \"v1\" | \"v2\"): TurnMetricRecord {\n\tconst input = object(value);\n\tconst rawTools = input.toolCalls === undefined ? [] : input.toolCalls;\n\tif (!Array.isArray(rawTools) || rawTools.length > 10000) throw new TypeError(\"Invalid metric tool list\");\n\tconst toolCalls = Array.from(rawTools, (raw): ToolCallMetricRecord => {\n\t\tconst call = object(raw);\n\t\tconst ok = boolean(call.ok);\n\t\tif (source === \"v2\" && call.error !== undefined) throw new TypeError(\"Raw errors are not v2 metrics\");\n\t\tconst errorClass = source === \"v2\" ? parseErrorClass(call.errorClass) : classifyError(call.error);\n\t\treturn {\n\t\t\tname: identifier(call.name),\n\t\t\tdurationMs: source === \"input\" ? Math.max(0, Math.round(finite(call.durationMs))) : quantity(call.durationMs),\n\t\t\tok,\n\t\t\t...(!ok && errorClass ? { errorClass } : {}),\n\t\t};\n\t});\n\tconst usage = input.usage === undefined ? undefined : object(input.usage);\n\tconst contextCache = input.contextCache === undefined ? undefined : object(input.contextCache);\n\tconst runtimeProvenance =\n\t\tinput.runtimeProvenance === undefined ? undefined : parseRuntimeProvenance(input.runtimeProvenance);\n\tconst startedAtEpochMs = quantity(input.startedAtEpochMs);\n\tconst endedAtEpochMs = quantity(input.endedAtEpochMs);\n\tif (\n\t\tinput.stopReason !== undefined &&\n\t\t(typeof input.stopReason !== \"string\" || !STOP_REASONS.includes(input.stopReason))\n\t)\n\t\tthrow new TypeError(\"Invalid metric stop reason\");\n\treturn {\n\t\tschemaVersion: TURN_METRICS_SCHEMA_VERSION,\n\t\tsessionId: identifier(input.sessionId),\n\t\tturnIndex: quantity(input.turnIndex, true),\n\t\tstartedAtEpochMs,\n\t\tendedAtEpochMs,\n\t\tdurationMs: Math.max(0, endedAtEpochMs - startedAtEpochMs),\n\t\ttoolCalls,\n\t\ttoolCallCount: toolCalls.length,\n\t\ttoolFailureCount: toolCalls.filter((call) => !call.ok).length,\n\t\t...(input.provider === undefined ? {} : { provider: identifier(input.provider) }),\n\t\t...(input.model === undefined ? {} : { model: identifier(input.model) }),\n\t\t...(input.timeToFirstChunkMs === undefined ? {} : { timeToFirstChunkMs: quantity(input.timeToFirstChunkMs) }),\n\t\t...(usage === undefined\n\t\t\t? {}\n\t\t\t: {\n\t\t\t\t\tusage: {\n\t\t\t\t\t\tinput: quantity(usage.input, true),\n\t\t\t\t\t\toutput: quantity(usage.output, true),\n\t\t\t\t\t\tcacheRead: quantity(usage.cacheRead, true),\n\t\t\t\t\t\tcacheWrite: quantity(usage.cacheWrite, true),\n\t\t\t\t\t\tcostUsd: quantity(usage.costUsd),\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t...(input.stopReason === undefined ? {} : { stopReason: input.stopReason }),\n\t\t...(input.compacted === undefined ? {} : { compacted: boolean(input.compacted) }),\n\t\t...(input.failovers === undefined ? {} : { failovers: quantity(input.failovers, true) }),\n\t\t...(contextCache === undefined\n\t\t\t? {}\n\t\t\t: {\n\t\t\t\t\tcontextCache: {\n\t\t\t\t\t\tplanHit: boolean(contextCache.planHit),\n\t\t\t\t\t\thits: quantity(contextCache.hits, true),\n\t\t\t\t\t\tmisses: quantity(contextCache.misses, true),\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t...(runtimeProvenance ? { runtimeProvenance } : {}),\n\t};\n}\n\nexport function buildTurnMetricRecord(input: TurnMetricInput): TurnMetricRecord {\n\treturn project(input, \"input\");\n}\n\n/** Invalid records count as malformed; legacy records are projected, never rewritten on disk. */\nexport function parseTurnMetricRecord(line: string): TurnMetricRecord | undefined {\n\ttry {\n\t\tconst value = object(JSON.parse(line));\n\t\tconst version = value.schemaVersion;\n\t\tif (version !== \"omk-turn-metrics-1\" && version !== TURN_METRICS_SCHEMA_VERSION) return undefined;\n\t\tconst projected = project(value, version === \"omk-turn-metrics-1\" ? \"v1\" : \"v2\");\n\t\tif (\n\t\t\tquantity(value.durationMs) !== projected.durationMs ||\n\t\t\tquantity(value.toolCallCount, true) !== projected.toolCallCount ||\n\t\t\tquantity(value.toolFailureCount, true) !== projected.toolFailureCount\n\t\t)\n\t\t\treturn undefined;\n\t\treturn projected;\n\t} catch (error) {\n\t\tif (error instanceof SyntaxError || error instanceof TypeError) return undefined;\n\t\tthrow error;\n\t}\n}\n"]}
|
|
@@ -12,65 +12,13 @@
|
|
|
12
12
|
* a failed write is dropped, never surfaced as a turn error.
|
|
13
13
|
*
|
|
14
14
|
* Privacy: records counts, durations, ids, and error *classes*. It never
|
|
15
|
-
* records prompt text, tool arguments, tool output, or file contents.
|
|
16
|
-
*
|
|
15
|
+
* records prompt text, tool arguments, tool output, or file contents. New
|
|
16
|
+
* records retain bounded error classes only; raw failure text is never serialized.
|
|
17
17
|
*/
|
|
18
|
-
import { type
|
|
19
|
-
export
|
|
18
|
+
import { type TurnMetricInput, type TurnUsageMetric } from "./turn-metrics-record.ts";
|
|
19
|
+
export * from "./turn-metrics-record.ts";
|
|
20
20
|
/** Rotate once the active file passes this size. */
|
|
21
21
|
export declare const DEFAULT_MAX_METRICS_BYTES: number;
|
|
22
|
-
/** Cap on a retained error string. Enough to classify, too short to carry a payload. */
|
|
23
|
-
export declare const MAX_ERROR_CHARS = 200;
|
|
24
|
-
export interface ToolCallMetric {
|
|
25
|
-
readonly name: string;
|
|
26
|
-
readonly durationMs: number;
|
|
27
|
-
readonly ok: boolean;
|
|
28
|
-
/** Truncated failure text, present only when `ok` is false. */
|
|
29
|
-
readonly error?: string;
|
|
30
|
-
}
|
|
31
|
-
export interface TurnUsageMetric {
|
|
32
|
-
readonly input: number;
|
|
33
|
-
readonly output: number;
|
|
34
|
-
readonly cacheRead: number;
|
|
35
|
-
readonly cacheWrite: number;
|
|
36
|
-
readonly costUsd: number;
|
|
37
|
-
}
|
|
38
|
-
export interface TurnMetricInput {
|
|
39
|
-
readonly sessionId: string;
|
|
40
|
-
readonly turnIndex: number;
|
|
41
|
-
readonly provider?: string;
|
|
42
|
-
readonly model?: string;
|
|
43
|
-
readonly startedAtEpochMs: number;
|
|
44
|
-
readonly endedAtEpochMs: number;
|
|
45
|
-
/** Time to the first streamed assistant chunk, when observed. */
|
|
46
|
-
readonly timeToFirstChunkMs?: number;
|
|
47
|
-
readonly usage?: TurnUsageMetric;
|
|
48
|
-
readonly stopReason?: string;
|
|
49
|
-
readonly toolCalls?: readonly ToolCallMetric[];
|
|
50
|
-
/** Whether compaction ran during this turn. */
|
|
51
|
-
readonly compacted?: boolean;
|
|
52
|
-
/** Provider failover attempts made during this turn. */
|
|
53
|
-
readonly failovers?: number;
|
|
54
|
-
/** Context-budget cache outcome for the turn's prompt build. */
|
|
55
|
-
readonly contextCache?: {
|
|
56
|
-
readonly planHit: boolean;
|
|
57
|
-
readonly hits: number;
|
|
58
|
-
readonly misses: number;
|
|
59
|
-
};
|
|
60
|
-
/** Requested/selected/response model and thinking projection. Advisory metadata only. */
|
|
61
|
-
readonly runtimeProvenance?: RuntimeProvenance;
|
|
62
|
-
}
|
|
63
|
-
export interface TurnMetricRecord extends TurnMetricInput {
|
|
64
|
-
readonly schemaVersion: typeof TURN_METRICS_SCHEMA_VERSION;
|
|
65
|
-
readonly durationMs: number;
|
|
66
|
-
readonly toolCallCount: number;
|
|
67
|
-
readonly toolFailureCount: number;
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Build the record written for one turn. Pure: no clock, no filesystem, so the
|
|
71
|
-
* derived fields can be asserted directly.
|
|
72
|
-
*/
|
|
73
|
-
export declare function buildTurnMetricRecord(input: TurnMetricInput): TurnMetricRecord;
|
|
74
22
|
export interface TurnMetricsSinkOptions {
|
|
75
23
|
readonly dir: string;
|
|
76
24
|
readonly maxBytes?: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"turn-metrics.d.ts","sourceRoot":"","sources":["../../src/core/turn-metrics.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Per-turn runtime metrics.\n *\n * Before this, `telemetry.ts` covered install opt-in and nothing else, and\n * `run-journal.ts` recorded a hash-chained *integrity* log (run started /\n * finished / tool timeout). Neither could answer the questions that actually\n * drive harness work: which tools fail, how long they take, what a turn costs,\n * and whether the context cache is doing anything.\n *\n * Shape: one JSON object per line under `<cwd>/.omk/metrics/turns.jsonl`.\n * Append-only, bounded by size with a single rotation, and never load-bearing —\n * a failed write is dropped, never surfaced as a turn error.\n *\n * Privacy: records counts, durations, ids, and error *classes*. It never\n * records prompt text, tool arguments, tool output, or file contents. Tool\n * error strings are truncated and kept only to distinguish failure modes.\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { parseRuntimeProvenance, type RuntimeProvenance } from \"./runtime-provenance.ts\";\n\nexport const TURN_METRICS_SCHEMA_VERSION = \"omk-turn-metrics-1\" as const;\n/** Rotate once the active file passes this size. */\nexport const DEFAULT_MAX_METRICS_BYTES = 8 * 1024 * 1024;\n/** Cap on a retained error string. Enough to classify, too short to carry a payload. */\nexport const MAX_ERROR_CHARS = 200;\n\nexport interface ToolCallMetric {\n\treadonly name: string;\n\treadonly durationMs: number;\n\treadonly ok: boolean;\n\t/** Truncated failure text, present only when `ok` is false. */\n\treadonly error?: string;\n}\n\nexport interface TurnUsageMetric {\n\treadonly input: number;\n\treadonly output: number;\n\treadonly cacheRead: number;\n\treadonly cacheWrite: number;\n\treadonly costUsd: number;\n}\n\nexport interface TurnMetricInput {\n\treadonly sessionId: string;\n\treadonly turnIndex: number;\n\treadonly provider?: string;\n\treadonly model?: string;\n\treadonly startedAtEpochMs: number;\n\treadonly endedAtEpochMs: number;\n\t/** Time to the first streamed assistant chunk, when observed. */\n\treadonly timeToFirstChunkMs?: number;\n\treadonly usage?: TurnUsageMetric;\n\treadonly stopReason?: string;\n\treadonly toolCalls?: readonly ToolCallMetric[];\n\t/** Whether compaction ran during this turn. */\n\treadonly compacted?: boolean;\n\t/** Provider failover attempts made during this turn. */\n\treadonly failovers?: number;\n\t/** Context-budget cache outcome for the turn's prompt build. */\n\treadonly contextCache?: { readonly planHit: boolean; readonly hits: number; readonly misses: number };\n\t/** Requested/selected/response model and thinking projection. Advisory metadata only. */\n\treadonly runtimeProvenance?: RuntimeProvenance;\n}\n\nexport interface TurnMetricRecord extends TurnMetricInput {\n\treadonly schemaVersion: typeof TURN_METRICS_SCHEMA_VERSION;\n\treadonly durationMs: number;\n\treadonly toolCallCount: number;\n\treadonly toolFailureCount: number;\n}\n\nfunction truncateError(value: string | undefined): string | undefined {\n\tif (value === undefined) return undefined;\n\tconst collapsed = value.replace(/\\s+/gu, \" \").trim();\n\tif (collapsed.length === 0) return undefined;\n\treturn collapsed.length > MAX_ERROR_CHARS ? `${collapsed.slice(0, MAX_ERROR_CHARS)}\\u2026` : collapsed;\n}\n\n/**\n * Build the record written for one turn. Pure: no clock, no filesystem, so the\n * derived fields can be asserted directly.\n */\nexport function buildTurnMetricRecord(input: TurnMetricInput): TurnMetricRecord {\n\tconst toolCalls = (input.toolCalls ?? []).map((call) => ({\n\t\tname: call.name,\n\t\tdurationMs: Math.max(0, Math.round(call.durationMs)),\n\t\tok: call.ok,\n\t\t...(call.ok ? {} : { error: truncateError(call.error) }),\n\t}));\n\tconst runtimeProvenance =\n\t\tinput.runtimeProvenance === undefined ? undefined : parseRuntimeProvenance(input.runtimeProvenance);\n\t// Spread the raw input minus its projection: a validated projection is re-added\n\t// below, an invalid one is dropped rather than persisted.\n\tconst { runtimeProvenance: _rawProvenance, ...rest } = input;\n\treturn {\n\t\t...rest,\n\t\tschemaVersion: TURN_METRICS_SCHEMA_VERSION,\n\t\tdurationMs: Math.max(0, input.endedAtEpochMs - input.startedAtEpochMs),\n\t\ttoolCalls,\n\t\ttoolCallCount: toolCalls.length,\n\t\ttoolFailureCount: toolCalls.filter((call) => !call.ok).length,\n\t\t...(runtimeProvenance === null || runtimeProvenance === undefined ? {} : { runtimeProvenance }),\n\t};\n}\n\nexport interface TurnMetricsSinkOptions {\n\treadonly dir: string;\n\treadonly maxBytes?: number;\n\treadonly fileName?: string;\n}\n\n/**\n * Append-only JSONL sink. Writes are synchronous and best-effort: metrics must\n * never be the reason a turn fails, so every filesystem error is swallowed and\n * counted instead of thrown.\n */\nexport class TurnMetricsSink {\n\tprivate readonly filePath: string;\n\tprivate readonly maxBytes: number;\n\tprivate dropped = 0;\n\tprivate written = 0;\n\n\tconstructor(options: TurnMetricsSinkOptions) {\n\t\tthis.filePath = path.join(options.dir, options.fileName ?? \"turns.jsonl\");\n\t\tthis.maxBytes = Math.max(1024, options.maxBytes ?? DEFAULT_MAX_METRICS_BYTES);\n\t}\n\n\tget path(): string {\n\t\treturn this.filePath;\n\t}\n\n\t/** Records written successfully this process. */\n\tget writtenCount(): number {\n\t\treturn this.written;\n\t}\n\n\t/** Records lost to a filesystem error this process. */\n\tget droppedCount(): number {\n\t\treturn this.dropped;\n\t}\n\n\t/** Append one turn. Returns `true` when the line reached disk. */\n\trecord(input: TurnMetricInput): boolean {\n\t\tlet line: string;\n\t\ttry {\n\t\t\tline = `${JSON.stringify(buildTurnMetricRecord(input))}\\n`;\n\t\t} catch {\n\t\t\tthis.dropped++;\n\t\t\treturn false;\n\t\t}\n\t\ttry {\n\t\t\tfs.mkdirSync(path.dirname(this.filePath), { recursive: true });\n\t\t\tthis.rotateIfNeeded(Buffer.byteLength(line, \"utf8\"));\n\t\t\tfs.appendFileSync(this.filePath, line, { encoding: \"utf8\", mode: 0o600 });\n\t\t\tthis.written++;\n\t\t\treturn true;\n\t\t} catch {\n\t\t\tthis.dropped++;\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tprivate rotateIfNeeded(incomingBytes: number): void {\n\t\tlet size: number;\n\t\ttry {\n\t\t\tsize = fs.statSync(this.filePath).size;\n\t\t} catch {\n\t\t\treturn; // No file yet.\n\t\t}\n\t\tif (size + incomingBytes <= this.maxBytes) return;\n\t\ttry {\n\t\t\t// Single generation: the previous file is replaced, so disk use stays bounded at 2x.\n\t\t\tfs.renameSync(this.filePath, `${this.filePath}.1`);\n\t\t} catch {\n\t\t\t// Rotation failure is not fatal; the next append still succeeds.\n\t\t}\n\t}\n}\n\nexport interface ToolAggregate {\n\treadonly name: string;\n\treadonly calls: number;\n\treadonly failures: number;\n\treadonly failureRate: number;\n\treadonly p50Ms: number;\n\treadonly p95Ms: number;\n\treadonly totalMs: number;\n}\n\nexport interface TurnMetricsSummary {\n\treadonly turns: number;\n\treadonly sessions: number;\n\treadonly models: readonly string[];\n\treadonly totalCostUsd: number;\n\treadonly totals: TurnUsageMetric;\n\treadonly turnDurationP50Ms: number;\n\treadonly turnDurationP95Ms: number;\n\treadonly timeToFirstChunkP50Ms?: number;\n\treadonly cacheReadShare: number;\n\treadonly compactions: number;\n\treadonly failovers: number;\n\treadonly contextCachePlanHitRate?: number;\n\t/** Turns where a valid projection's response model differs from the selected model. */\n\treadonly provenanceMismatchTurns: number;\n\treadonly tools: readonly ToolAggregate[];\n\t/** Lines that could not be parsed as a metric record. */\n\treadonly malformedLines: number;\n}\n\nfunction percentile(sorted: readonly number[], fraction: number): number {\n\tif (sorted.length === 0) return 0;\n\tconst index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(fraction * sorted.length) - 1));\n\treturn sorted[index];\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction parseRecord(line: string): TurnMetricRecord | undefined {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(line);\n\t} catch {\n\t\treturn undefined;\n\t}\n\tif (!isRecord(parsed) || parsed.schemaVersion !== TURN_METRICS_SCHEMA_VERSION) return undefined;\n\tif (typeof parsed.turnIndex !== \"number\" || typeof parsed.durationMs !== \"number\") return undefined;\n\treturn parsed as unknown as TurnMetricRecord;\n}\n\n/**\n * Aggregate JSONL lines into a summary. Malformed lines are counted rather than\n * discarded silently, so a truncated tail is visible instead of pretending the\n * data is complete.\n */\nexport function summarizeTurnMetrics(lines: readonly string[]): TurnMetricsSummary {\n\tconst records: TurnMetricRecord[] = [];\n\tlet malformedLines = 0;\n\tfor (const line of lines) {\n\t\tconst trimmed = line.trim();\n\t\tif (trimmed.length === 0) continue;\n\t\tconst record = parseRecord(trimmed);\n\t\tif (record) records.push(record);\n\t\telse malformedLines++;\n\t}\n\n\tconst sessions = new Set<string>();\n\tconst models = new Set<string>();\n\tconst totals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, costUsd: 0 };\n\tconst turnDurations: number[] = [];\n\tconst firstChunkTimes: number[] = [];\n\tconst toolStats = new Map<string, { durations: number[]; failures: number }>();\n\tlet compactions = 0;\n\tlet failovers = 0;\n\tlet planHits = 0;\n\tlet planObservations = 0;\n\tlet provenanceMismatchTurns = 0;\n\n\tfor (const record of records) {\n\t\tsessions.add(record.sessionId);\n\t\tif (record.model) models.add(record.provider ? `${record.provider}/${record.model}` : record.model);\n\t\tif (record.usage) {\n\t\t\ttotals.input += record.usage.input;\n\t\t\ttotals.output += record.usage.output;\n\t\t\ttotals.cacheRead += record.usage.cacheRead;\n\t\t\ttotals.cacheWrite += record.usage.cacheWrite;\n\t\t\ttotals.costUsd += record.usage.costUsd;\n\t\t}\n\t\tturnDurations.push(record.durationMs);\n\t\tif (typeof record.timeToFirstChunkMs === \"number\") firstChunkTimes.push(record.timeToFirstChunkMs);\n\t\tif (record.compacted) compactions++;\n\t\tfailovers += record.failovers ?? 0;\n\t\tif (record.contextCache) {\n\t\t\tplanObservations++;\n\t\t\tif (record.contextCache.planHit) planHits++;\n\t\t}\n\t\t// Re-validate provenance at read time: a tampered projection never counts.\n\t\tconst provenance =\n\t\t\trecord.runtimeProvenance === undefined ? undefined : parseRuntimeProvenance(record.runtimeProvenance);\n\t\tif (\n\t\t\tprovenance !== undefined &&\n\t\t\tprovenance !== null &&\n\t\t\tprovenance.responseModel !== null &&\n\t\t\tprovenance.responseModel !== provenance.selectedModel\n\t\t) {\n\t\t\tprovenanceMismatchTurns++;\n\t\t}\n\t\tfor (const call of record.toolCalls ?? []) {\n\t\t\tconst stats = toolStats.get(call.name) ?? { durations: [], failures: 0 };\n\t\t\tstats.durations.push(call.durationMs);\n\t\t\tif (!call.ok) stats.failures++;\n\t\t\ttoolStats.set(call.name, stats);\n\t\t}\n\t}\n\n\tturnDurations.sort((a, b) => a - b);\n\tfirstChunkTimes.sort((a, b) => a - b);\n\n\tconst tools: ToolAggregate[] = [...toolStats.entries()]\n\t\t.map(([name, stats]) => {\n\t\t\tconst sorted = [...stats.durations].sort((a, b) => a - b);\n\t\t\treturn {\n\t\t\t\tname,\n\t\t\t\tcalls: sorted.length,\n\t\t\t\tfailures: stats.failures,\n\t\t\t\tfailureRate: sorted.length === 0 ? 0 : stats.failures / sorted.length,\n\t\t\t\tp50Ms: percentile(sorted, 0.5),\n\t\t\t\tp95Ms: percentile(sorted, 0.95),\n\t\t\t\ttotalMs: sorted.reduce((sum, value) => sum + value, 0),\n\t\t\t};\n\t\t})\n\t\t.sort((a, b) => b.totalMs - a.totalMs || a.name.localeCompare(b.name));\n\n\tconst promptSideTotal = totals.input + totals.cacheRead;\n\treturn {\n\t\tturns: records.length,\n\t\tsessions: sessions.size,\n\t\tmodels: [...models].sort(),\n\t\ttotalCostUsd: totals.costUsd,\n\t\ttotals,\n\t\tturnDurationP50Ms: percentile(turnDurations, 0.5),\n\t\tturnDurationP95Ms: percentile(turnDurations, 0.95),\n\t\ttimeToFirstChunkP50Ms: firstChunkTimes.length > 0 ? percentile(firstChunkTimes, 0.5) : undefined,\n\t\tcacheReadShare: promptSideTotal === 0 ? 0 : totals.cacheRead / promptSideTotal,\n\t\tcompactions,\n\t\tfailovers,\n\t\tcontextCachePlanHitRate: planObservations === 0 ? undefined : planHits / planObservations,\n\t\tprovenanceMismatchTurns,\n\t\ttools,\n\t\tmalformedLines,\n\t};\n}\n\n/** Read and summarize a metrics file. A missing file summarizes as zero turns. */\nexport function summarizeTurnMetricsFile(filePath: string): TurnMetricsSummary {\n\tlet raw: string;\n\ttry {\n\t\traw = fs.readFileSync(filePath, \"utf8\");\n\t} catch {\n\t\treturn summarizeTurnMetrics([]);\n\t}\n\treturn summarizeTurnMetrics(raw.split(\"\\n\"));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"turn-metrics.d.ts","sourceRoot":"","sources":["../../src/core/turn-metrics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,OAAO,EAGN,KAAK,eAAe,EAEpB,KAAK,eAAe,EACpB,MAAM,0BAA0B,CAAC;AAElC,cAAc,0BAA0B,CAAC;AACzC,oDAAoD;AACpD,eAAO,MAAM,yBAAyB,QAAkB,CAAC;AACzD,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;GAIG;AACH,qBAAa,eAAe;IAC3B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,OAAO,CAAK;IAEpB,YAAY,OAAO,EAAE,sBAAsB,EAG1C;IAED,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,iDAAiD;IACjD,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,uDAAuD;IACvD,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,kEAAkE;IAClE,MAAM,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAuBtC;IAED,OAAO,CAAC,cAAc;CAetB;AAED,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;IACjC,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IACxC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,uBAAuB,CAAC,EAAE,MAAM,CAAC;IAC1C,uFAAuF;IACvF,QAAQ,CAAC,uBAAuB,EAAE,MAAM,CAAC;IACzC,QAAQ,CAAC,KAAK,EAAE,SAAS,aAAa,EAAE,CAAC;IACzC,yDAAyD;IACzD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CAChC;AAQD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,kBAAkB,CA8FjF;AAED,kFAAkF;AAClF,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,kBAAkB,CAQ7E","sourcesContent":["/**\n * Per-turn runtime metrics.\n *\n * Before this, `telemetry.ts` covered install opt-in and nothing else, and\n * `run-journal.ts` recorded a hash-chained *integrity* log (run started /\n * finished / tool timeout). Neither could answer the questions that actually\n * drive harness work: which tools fail, how long they take, what a turn costs,\n * and whether the context cache is doing anything.\n *\n * Shape: one JSON object per line under `<cwd>/.omk/metrics/turns.jsonl`.\n * Append-only, bounded by size with a single rotation, and never load-bearing —\n * a failed write is dropped, never surfaced as a turn error.\n *\n * Privacy: records counts, durations, ids, and error *classes*. It never\n * records prompt text, tool arguments, tool output, or file contents. New\n * records retain bounded error classes only; raw failure text is never serialized.\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport {\n\tbuildTurnMetricRecord,\n\tparseTurnMetricRecord,\n\ttype TurnMetricInput,\n\ttype TurnMetricRecord,\n\ttype TurnUsageMetric,\n} from \"./turn-metrics-record.ts\";\n\nexport * from \"./turn-metrics-record.ts\";\n/** Rotate once the active file passes this size. */\nexport const DEFAULT_MAX_METRICS_BYTES = 8 * 1024 * 1024;\nexport interface TurnMetricsSinkOptions {\n\treadonly dir: string;\n\treadonly maxBytes?: number;\n\treadonly fileName?: string;\n}\n\n/**\n * Append-only JSONL sink. Writes are synchronous and best-effort: metrics must\n * never be the reason a turn fails, so every filesystem error is swallowed and\n * counted instead of thrown.\n */\nexport class TurnMetricsSink {\n\tprivate readonly filePath: string;\n\tprivate readonly maxBytes: number;\n\tprivate dropped = 0;\n\tprivate written = 0;\n\n\tconstructor(options: TurnMetricsSinkOptions) {\n\t\tthis.filePath = path.join(options.dir, options.fileName ?? \"turns.jsonl\");\n\t\tthis.maxBytes = Math.max(1024, options.maxBytes ?? DEFAULT_MAX_METRICS_BYTES);\n\t}\n\n\tget path(): string {\n\t\treturn this.filePath;\n\t}\n\n\t/** Records written successfully this process. */\n\tget writtenCount(): number {\n\t\treturn this.written;\n\t}\n\n\t/** Records lost to a filesystem error this process. */\n\tget droppedCount(): number {\n\t\treturn this.dropped;\n\t}\n\n\t/** Append one turn. Returns `true` when the line reached disk. */\n\trecord(input: TurnMetricInput): boolean {\n\t\tlet line: string;\n\t\ttry {\n\t\t\tline = `${JSON.stringify(buildTurnMetricRecord(input))}\\n`;\n\t\t} catch {\n\t\t\tthis.dropped++;\n\t\t\treturn false;\n\t\t}\n\t\tconst bytes = Buffer.byteLength(line, \"utf8\");\n\t\tif (bytes > this.maxBytes) {\n\t\t\tthis.dropped++;\n\t\t\treturn false;\n\t\t}\n\t\ttry {\n\t\t\tfs.mkdirSync(path.dirname(this.filePath), { recursive: true });\n\t\t\tthis.rotateIfNeeded(bytes);\n\t\t\tfs.appendFileSync(this.filePath, line, { encoding: \"utf8\", mode: 0o600 });\n\t\t\tthis.written++;\n\t\t\treturn true;\n\t\t} catch {\n\t\t\tthis.dropped++;\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tprivate rotateIfNeeded(incomingBytes: number): void {\n\t\tlet size: number;\n\t\ttry {\n\t\t\tsize = fs.statSync(this.filePath).size;\n\t\t} catch {\n\t\t\treturn; // No file yet.\n\t\t}\n\t\tif (size + incomingBytes <= this.maxBytes) return;\n\t\ttry {\n\t\t\t// Single generation: the previous file is replaced, so disk use stays bounded at 2x.\n\t\t\tfs.renameSync(this.filePath, `${this.filePath}.1`);\n\t\t} catch {\n\t\t\t// Rotation failure is not fatal; the next append still succeeds.\n\t\t}\n\t}\n}\n\nexport interface ToolAggregate {\n\treadonly name: string;\n\treadonly calls: number;\n\treadonly failures: number;\n\treadonly failureRate: number;\n\treadonly p50Ms: number;\n\treadonly p95Ms: number;\n\treadonly totalMs: number;\n}\n\nexport interface TurnMetricsSummary {\n\treadonly turns: number;\n\treadonly sessions: number;\n\treadonly models: readonly string[];\n\treadonly totalCostUsd: number;\n\treadonly totals: TurnUsageMetric;\n\treadonly turnDurationP50Ms: number;\n\treadonly turnDurationP95Ms: number;\n\treadonly timeToFirstChunkP50Ms?: number;\n\treadonly cacheReadShare: number;\n\treadonly compactions: number;\n\treadonly failovers: number;\n\treadonly contextCachePlanHitRate?: number;\n\t/** Turns where a valid projection's response model differs from the selected model. */\n\treadonly provenanceMismatchTurns: number;\n\treadonly tools: readonly ToolAggregate[];\n\t/** Lines that could not be parsed as a metric record. */\n\treadonly malformedLines: number;\n}\n\nfunction percentile(sorted: readonly number[], fraction: number): number {\n\tif (sorted.length === 0) return 0;\n\tconst index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(fraction * sorted.length) - 1));\n\treturn sorted[index];\n}\n\n/**\n * Aggregate JSONL lines into a summary. Malformed lines are counted rather than\n * discarded silently, so a truncated tail is visible instead of pretending the\n * data is complete.\n */\nexport function summarizeTurnMetrics(lines: readonly string[]): TurnMetricsSummary {\n\tconst records: TurnMetricRecord[] = [];\n\tlet malformedLines = 0;\n\tfor (const line of lines) {\n\t\tconst trimmed = line.trim();\n\t\tif (trimmed.length === 0) continue;\n\t\tconst record = parseTurnMetricRecord(trimmed);\n\t\tif (record) records.push(record);\n\t\telse malformedLines++;\n\t}\n\n\tconst sessions = new Set<string>();\n\tconst models = new Set<string>();\n\tconst totals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, costUsd: 0 };\n\tconst turnDurations: number[] = [];\n\tconst firstChunkTimes: number[] = [];\n\tconst toolStats = new Map<string, { durations: number[]; failures: number }>();\n\tlet compactions = 0;\n\tlet failovers = 0;\n\tlet planHits = 0;\n\tlet planObservations = 0;\n\tlet provenanceMismatchTurns = 0;\n\n\tfor (const record of records) {\n\t\tsessions.add(record.sessionId);\n\t\tif (record.model) models.add(record.provider ? `${record.provider}/${record.model}` : record.model);\n\t\tif (record.usage) {\n\t\t\ttotals.input += record.usage.input;\n\t\t\ttotals.output += record.usage.output;\n\t\t\ttotals.cacheRead += record.usage.cacheRead;\n\t\t\ttotals.cacheWrite += record.usage.cacheWrite;\n\t\t\ttotals.costUsd += record.usage.costUsd;\n\t\t}\n\t\tturnDurations.push(record.durationMs);\n\t\tif (typeof record.timeToFirstChunkMs === \"number\") firstChunkTimes.push(record.timeToFirstChunkMs);\n\t\tif (record.compacted) compactions++;\n\t\tfailovers += record.failovers ?? 0;\n\t\tif (record.contextCache) {\n\t\t\tplanObservations++;\n\t\t\tif (record.contextCache.planHit) planHits++;\n\t\t}\n\t\tconst provenance = record.runtimeProvenance;\n\t\tif (\n\t\t\tprovenance !== undefined &&\n\t\t\tprovenance !== null &&\n\t\t\tprovenance.responseModel !== null &&\n\t\t\tprovenance.responseModel !== provenance.selectedModel\n\t\t) {\n\t\t\tprovenanceMismatchTurns++;\n\t\t}\n\t\tfor (const call of record.toolCalls ?? []) {\n\t\t\tconst stats = toolStats.get(call.name) ?? { durations: [], failures: 0 };\n\t\t\tstats.durations.push(call.durationMs);\n\t\t\tif (!call.ok) stats.failures++;\n\t\t\ttoolStats.set(call.name, stats);\n\t\t}\n\t}\n\n\tturnDurations.sort((a, b) => a - b);\n\tfirstChunkTimes.sort((a, b) => a - b);\n\n\tconst tools: ToolAggregate[] = [...toolStats.entries()]\n\t\t.map(([name, stats]) => {\n\t\t\tconst sorted = [...stats.durations].sort((a, b) => a - b);\n\t\t\treturn {\n\t\t\t\tname,\n\t\t\t\tcalls: sorted.length,\n\t\t\t\tfailures: stats.failures,\n\t\t\t\tfailureRate: sorted.length === 0 ? 0 : stats.failures / sorted.length,\n\t\t\t\tp50Ms: percentile(sorted, 0.5),\n\t\t\t\tp95Ms: percentile(sorted, 0.95),\n\t\t\t\ttotalMs: sorted.reduce((sum, value) => sum + value, 0),\n\t\t\t};\n\t\t})\n\t\t.sort((a, b) => b.totalMs - a.totalMs || a.name.localeCompare(b.name));\n\n\tconst promptSideTotal = totals.input + totals.cacheRead;\n\treturn {\n\t\tturns: records.length,\n\t\tsessions: sessions.size,\n\t\tmodels: [...models].sort(),\n\t\ttotalCostUsd: totals.costUsd,\n\t\ttotals,\n\t\tturnDurationP50Ms: percentile(turnDurations, 0.5),\n\t\tturnDurationP95Ms: percentile(turnDurations, 0.95),\n\t\ttimeToFirstChunkP50Ms: firstChunkTimes.length > 0 ? percentile(firstChunkTimes, 0.5) : undefined,\n\t\tcacheReadShare: promptSideTotal === 0 ? 0 : totals.cacheRead / promptSideTotal,\n\t\tcompactions,\n\t\tfailovers,\n\t\tcontextCachePlanHitRate: planObservations === 0 ? undefined : planHits / planObservations,\n\t\tprovenanceMismatchTurns,\n\t\ttools,\n\t\tmalformedLines,\n\t};\n}\n\n/** Read and summarize a metrics file. A missing file summarizes as zero turns. */\nexport function summarizeTurnMetricsFile(filePath: string): TurnMetricsSummary {\n\tlet raw: string;\n\ttry {\n\t\traw = fs.readFileSync(filePath, \"utf8\");\n\t} catch {\n\t\treturn summarizeTurnMetrics([]);\n\t}\n\treturn summarizeTurnMetrics(raw.split(\"\\n\"));\n}\n"]}
|