fix(claude_sdk,conversations,rotation): context size from the last api call, new-day text by rotation reason

This commit is contained in:
hh
2026-08-29 16:54:29 +02:00
parent 72639c4b90
commit 4a59c59bda
8 changed files with 120 additions and 16 deletions
+45 -8
View File
@@ -23,7 +23,7 @@ import logging
import re
import uuid
from dataclasses import dataclass, field
from datetime import UTC, datetime, timedelta
from datetime import UTC, date, datetime, timedelta
from typing import TYPE_CHECKING, Any, cast
from claude_agent_sdk import (
@@ -134,6 +134,19 @@ class SeedContext:
title: str | None
@dataclass(frozen=True, slots=True)
class NewDayContext:
"""What the new master hears first.
``reason`` is ``ночь`` / ``возраст`` / ``транскрипт`` - only the first
one is actually a new day.
"""
day: date
reason: str
moved: int
@dataclass(frozen=True, slots=True)
class ConversationTexts:
"""Texts the gateway cannot invent for a setup.
@@ -155,7 +168,10 @@ class ConversationTexts:
"Этот мастер закрывается ({reason}). Напиши хендаут за {day}: справку "
"на утро, не задание - прошедшее время, без повелительного наклонения."
)
new_day: str = "Новый день: мастер сменился, хендаут за {day} записан."
new_day: Callable[[NewDayContext], Awaitable[str] | str] | str = (
"Мастер сменился ({reason}), хендаут за {day} записан."
)
"""First inject of the new master; a callable sees the rotation reason."""
distill: Callable[[DistillContext], Awaitable[str] | str] | None = None
"""The distiller fork's first message (§8.4): which chat, what day, where
the digest goes; ``None`` uses a path-less default."""
@@ -553,9 +569,7 @@ class Conversations:
.limit(1)
)
).first()
if row is None:
return 0
return row.input_tokens + row.cache_read_tokens + row.cache_creation_tokens
return context_of(row)
async def usage_tokens(self, since: datetime) -> int:
async with self._db.session() as session:
@@ -1213,9 +1227,18 @@ class Conversations:
_log.exception("%s could not mark %s", fe.name, conv.external_id)
return marked
async def new_day(self, conv: Conversation, *, moved: int = 0) -> InjectQueueItem:
day = datetime.now(UTC).astimezone().date().isoformat()
text = self._texts.new_day.format(day=day)
async def new_day(
self, conv: Conversation, *, reason: str = "ночь", moved: int = 0
) -> InjectQueueItem:
ctx = NewDayContext(
day=datetime.now(UTC).astimezone().date(), reason=reason, moved=moved
)
source = self._texts.new_day
if isinstance(source, str):
text = source.format(day=ctx.day.isoformat(), reason=ctx.reason)
else:
produced: Any = source(ctx)
text = await produced if inspect.isawaitable(produced) else produced
if moved:
text += f" Инжектов переехало из старого мастера: {moved}."
return await self.inject(
@@ -1902,3 +1925,17 @@ def _usage_dict(capture: TurnCapture) -> dict[str, Any] | None:
"cost_usd": usage.cost_usd,
"duration_ms": usage.duration_ms,
}
def context_of(row: Usage | None) -> int:
"""Context size of a turn.
The last API call's input, or, for rows written before it was recorded,
the per-call average of the turn's input sums.
"""
if row is None:
return 0
if row.context_tokens:
return row.context_tokens
total = row.input_tokens + row.cache_read_tokens + row.cache_creation_tokens
return round(total / max(row.num_turns or 1, 1))