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:
2026-04-01 22:24:14 +00:00
parent 57b346bb8b
commit d71f4114d1
28 changed files with 1601 additions and 26 deletions

View File

@@ -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,