generated from coulomb/repo-seed
feat: WP-0001 foundation + WP-0002 core extensions
WP-0001 — Foundation & GAAF Baseline - SCOPE.md, ARCHITECTURE-LAYERS.md, contracts/ tree - .claude/rules/ stubs filled (architecture, stack, boundary) - 57 tests (pytest), pyproject.toml with ruff+mypy, CI workflow WP-0002 — Core Extensions (FR-4 + FR-3) - FR-4: BudgetTracker (thread-safe) + LLMBudgetExceededError + optional RunConfig.budget_tracker + enforcement in all adapters - FR-3: async_execute_prompt on LLMAdapter ABC (asyncio.to_thread fallback) + native asyncio.create_subprocess_exec in ClaudeCodeAdapter 81 tests passing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,8 +5,53 @@ These classes are the canonical definitions; they are re-exported by
|
||||
markitect.prompts.execution.models for backward compatibility.
|
||||
"""
|
||||
|
||||
import threading
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, Any
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
|
||||
class BudgetTracker:
|
||||
"""Shared token budget for a call or delegation chain.
|
||||
|
||||
Thread-safe. Tracks cumulative token spend across multiple adapter
|
||||
calls. Raises ``LLMBudgetExceededError`` when the cap is exceeded.
|
||||
|
||||
Example::
|
||||
|
||||
tracker = BudgetTracker(total=4000)
|
||||
config = RunConfig(budget_tracker=tracker)
|
||||
# All adapter calls sharing this config will consume from the same cap.
|
||||
"""
|
||||
|
||||
def __init__(self, total: int) -> None:
|
||||
if total <= 0:
|
||||
raise ValueError(f"BudgetTracker total must be positive, got {total}")
|
||||
self.total = total
|
||||
self.spent = 0
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def remaining(self) -> int:
|
||||
"""Return tokens remaining in the budget."""
|
||||
return max(0, self.total - self.spent)
|
||||
|
||||
def consume(self, tokens: int) -> None:
|
||||
"""Record *tokens* as spent. Raises ``LLMBudgetExceededError`` if cap exceeded."""
|
||||
from llm_connect.exceptions import LLMBudgetExceededError # avoid circular at module load
|
||||
|
||||
with self._lock:
|
||||
new_spent = self.spent + tokens
|
||||
if new_spent > self.total:
|
||||
raise LLMBudgetExceededError(
|
||||
f"Token budget exceeded: {new_spent} tokens used, cap is {self.total}",
|
||||
total=self.total,
|
||||
spent=self.spent,
|
||||
requested=tokens,
|
||||
context={"total": self.total, "spent": self.spent, "requested": tokens},
|
||||
)
|
||||
self.spent = new_spent
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"BudgetTracker(total={self.total}, spent={self.spent}, remaining={self.remaining()})"
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -30,9 +75,10 @@ class RunConfig:
|
||||
max_depth: int = 3
|
||||
skip_if_exists: bool = True
|
||||
timeout_seconds: int = 300
|
||||
budget_tracker: Optional["BudgetTracker"] = field(default=None, repr=False)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
"""Convert to dictionary. ``budget_tracker`` is excluded (runtime object)."""
|
||||
return {
|
||||
"model_name": self.model_name,
|
||||
"temperature": self.temperature,
|
||||
|
||||
Reference in New Issue
Block a user