mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 21:47:02 +01:00
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (mypy, mypy) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (pylint, pylint) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (rest, ) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (shellcheck, shellcheck) (push) Waiting to run
Python tests / test (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / lib-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JThjboXTeZ3yJPBzxMx5wS
92 lines
3.6 KiB
Python
92 lines
3.6 KiB
Python
"""Join hook context to the run that produced it; price forced retries; merge.
|
|
|
|
``hook_additional_context`` names no command, so it is matched to a
|
|
``hook_success`` run of the same tool-use id and hook name whose stdout
|
|
contains the context text. A ``hook_blocking_error`` costs the calls it
|
|
forces: a Stop bounce every call until the next prompt, any other block the
|
|
one call that reacts to it.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import replace
|
|
import json
|
|
from typing import TYPE_CHECKING
|
|
|
|
from python_pkg.token_dashboard.marks import SNIPPET
|
|
|
|
if TYPE_CHECKING:
|
|
from python_pkg.token_dashboard.detail_rows import MarkRow
|
|
from python_pkg.token_dashboard.marks import Found
|
|
|
|
_STOPS = frozenset({"Stop", "SubagentStop"})
|
|
|
|
|
|
class Hooks:
|
|
"""Pending joins and open retry windows for one file."""
|
|
|
|
def __init__(self) -> None:
|
|
"""Nothing seen yet."""
|
|
self.runs: dict[tuple[str, str], list[tuple[str, str]]] = {}
|
|
self.pending: list[tuple[MarkRow, tuple[str, str], str]] = []
|
|
self.until_prompt: list[MarkRow] = []
|
|
self.next_turn: list[MarkRow] = []
|
|
|
|
def track(self, row: MarkRow, found: Found) -> None:
|
|
"""Remember what later records or turns need to complete ``row``."""
|
|
if found.kind == "hook_run":
|
|
self.runs.setdefault(found.key, []).append((row.detail, found.text))
|
|
elif found.kind == "hook_context":
|
|
self.pending.append((row, found.key, found.text))
|
|
elif found.kind == "hook_block":
|
|
stop = row.name in _STOPS
|
|
(self.until_prompt if stop else self.next_turn).append(row)
|
|
|
|
def prompt_opened(self) -> None:
|
|
"""A new prompt ends every Stop-bounce retry."""
|
|
self.until_prompt.clear()
|
|
|
|
def turn(self, cost: float, ts: float) -> None:
|
|
"""Charge an API call made after a block to that block's retry.
|
|
|
|
Turns are released one message late, so a turn released after the
|
|
block record may still predate it: the timestamp decides.
|
|
"""
|
|
for row in [*self.until_prompt, *self.next_turn]:
|
|
if ts > row.where.ts:
|
|
row.retry_usd += cost
|
|
self.next_turn = [row for row in self.next_turn if ts <= row.where.ts]
|
|
|
|
def resolve(self) -> None:
|
|
"""Name the script behind each injected hook context."""
|
|
for row, key, text in self.pending:
|
|
snippet = text[:SNIPPET]
|
|
escaped = json.dumps(snippet)[1:-1]
|
|
for detail, stdout in self.runs.get(key, []):
|
|
if snippet and (snippet in stdout or escaped in stdout):
|
|
row.detail = detail
|
|
break
|
|
|
|
|
|
def merge(rows: list[MarkRow], first_model: str) -> list[MarkRow]:
|
|
"""Sum marks sharing prompt, kind, name, detail and model; oldest first."""
|
|
merged: dict[tuple[int, str, str, str, str], MarkRow] = {}
|
|
for row in rows:
|
|
row.where = replace(row.where, model=row.where.model or first_model)
|
|
where = row.where
|
|
key = (where.prompt_idx, row.kind, row.name, row.detail, where.model)
|
|
held = merged.get(key)
|
|
if held is None:
|
|
merged[key] = row
|
|
continue
|
|
held.where = replace(held.where, ts=min(held.where.ts, where.ts))
|
|
held.count += row.count
|
|
held.tokens += row.tokens
|
|
held.extra += row.extra
|
|
held.duration_ms += row.duration_ms
|
|
held.retry_usd += row.retry_usd
|
|
held.cost.create_usd += row.cost.create_usd
|
|
held.cost.carry_usd += row.cost.carry_usd
|
|
held.cost.carry_calls += row.cost.carry_calls
|
|
return sorted(merged.values(), key=lambda r: r.where.ts)
|