cc7993e53d
TIER-3 READ AGENTS.md + conductor/code_styleguides/error_handling.md + src/provider_state.py + src/ai_client.py:2148-2220 before provider-state-rlock-fix.
Tier 2's 25a22057 commit re-bound the 14 module globals in src/ai_client.py as
aliases to provider_state.get_history(...) instances. The ProviderHistory dunder
methods (__bool__, __len__, __iter__, __getitem__) all use \with self.lock:\.
The dunders are non-reentrant: \ hreading.Lock\ blocks if the lock is already
held. The call site in src/ai_client.py:2210-2217 acquires the lock via
\with _deepseek_history_lock:\ (alias to ProviderHistory.lock), then calls
_rerepair_deepseek_history(_deepseek_history) which does \history[-1]\
(acquires the lock again -> DEADLOCK). This caused
tests/test_deepseek_provider.py::test_deepseek_completion_logic to hang
with a 30s timeout.
Fix: change \ hreading.Lock\ to \ hreading.RLock\ in ProviderHistory.
The dunders can now be safely called while the lock is already held.
Also removed:
- Duplicate @dataclass decorator on ProviderHistory (line 25-26)
- Duplicate _PROVIDER_HISTORIES dict declaration (lines 64-71 and 74-81)
Acceptance: test_deepseek_provider (7/7) + test_provider_state + test_ai_client_result + test_ai_client_tool_loop all pass.
85 lines
2.2 KiB
Python
85 lines
2.2 KiB
Python
"""Per-provider history state for the AI client layer.
|
|
|
|
Promotes 14 module globals in src/ai_client.py:
|
|
- 7x `_<provider>_history: list[Metadata]` (anthropic/deepseek/minimax/qwen/grok/llama)
|
|
- 7x `_<provider>_history_lock: threading.Lock`
|
|
|
|
To a single `_PROVIDER_HISTORIES: dict[str, ProviderHistory]` keyed by
|
|
provider name. Each `ProviderHistory` owns its own lock and message list;
|
|
the cross-provider pattern is encapsulated behind a 4-method interface.
|
|
|
|
SDK client holders (`_gemini_chat`, `_deepseek_client`, etc.) stay as
|
|
module-level `Any` variables per Pattern 3 (heterogeneous SDK types,
|
|
lazy-initialized). Only the homogeneous history aspect is unified.
|
|
|
|
CONVENTION: 1-space indentation. NO COMMENTS.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import threading
|
|
from dataclasses import dataclass, field
|
|
|
|
from src.type_aliases import HistoryMessage, Metadata
|
|
|
|
|
|
@dataclass
|
|
class ProviderHistory:
|
|
messages: list[HistoryMessage] = field(default_factory=list)
|
|
lock: threading.RLock = field(default_factory=threading.RLock)
|
|
|
|
def __bool__(self) -> bool:
|
|
with self.lock:
|
|
return bool(self.messages)
|
|
|
|
def __len__(self) -> int:
|
|
with self.lock:
|
|
return len(self.messages)
|
|
|
|
def __iter__(self):
|
|
with self.lock:
|
|
return iter(list(self.messages))
|
|
|
|
def __getitem__(self, idx):
|
|
with self.lock:
|
|
return self.messages[idx]
|
|
|
|
def append(self, message: HistoryMessage) -> None:
|
|
with self.lock:
|
|
self.messages.append(message)
|
|
|
|
def get_all(self) -> list[HistoryMessage]:
|
|
with self.lock:
|
|
return list(self.messages)
|
|
|
|
def replace_all(self, messages: list[HistoryMessage]) -> None:
|
|
with self.lock:
|
|
self.messages = list(messages)
|
|
|
|
def clear(self) -> None:
|
|
with self.lock:
|
|
self.messages = []
|
|
|
|
|
|
_PROVIDER_HISTORIES: dict[str, ProviderHistory] = {
|
|
"anthropic": ProviderHistory(),
|
|
"deepseek": ProviderHistory(),
|
|
"minimax": ProviderHistory(),
|
|
"qwen": ProviderHistory(),
|
|
"grok": ProviderHistory(),
|
|
"llama": ProviderHistory(),
|
|
}
|
|
|
|
|
|
def get_history(provider: str) -> ProviderHistory:
|
|
if provider not in _PROVIDER_HISTORIES:
|
|
raise KeyError(f"Unknown provider: {provider!r}")
|
|
return _PROVIDER_HISTORIES[provider]
|
|
|
|
|
|
def clear_all() -> None:
|
|
for h in _PROVIDER_HISTORIES.values():
|
|
h.clear()
|
|
|
|
|
|
def providers() -> tuple[str, ...]:
|
|
return tuple(_PROVIDER_HISTORIES.keys()) |