Initial commit: Agent Governance System Phase 8

Phase 8 Production Hardening with complete governance infrastructure:

- Vault integration with tiered policies (T0-T4)
- DragonflyDB state management
- SQLite audit ledger
- Pipeline DSL and templates
- Promotion/revocation engine
- Checkpoint system for session persistence
- Health manager and circuit breaker for fault tolerance
- GitHub/Slack integrations
- Architectural test pipeline with bug watcher, suggestion engine, council review
- Multi-agent chaos testing framework

Test Results:
- Governance tests: 68/68 passing
- E2E workflow: 16/16 passing
- Phase 2 Vault: 14/14 passing
- Integration tests: 27/27 passing

Coverage: 57.6% average across 12 phases

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
profit
2026-01-23 22:07:06 -05:00
co-authored by Claude Opus 4.5
commit 77655c298c
338 changed files with 63491 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv
+1
View File
@@ -0,0 +1 @@
3.11
View File
+30
View File
@@ -0,0 +1,30 @@
# Status: Llm Planner
## Current Phase
** NOT STARTED**
## Tasks
| Status | Task | Updated |
|--------|------|---------|
| ☐ | *No tasks defined* | - |
## Dependencies
*No external dependencies.*
## Issues / Blockers
*No current issues or blockers.*
## Activity Log
### 2026-01-23 23:25:09 UTC
- **Phase**: NOT STARTED
- **Action**: Initialized
- **Details**: Status tracking initialized for this directory.
---
*Last updated: 2026-01-23 23:25:09 UTC*
+527
View File
@@ -0,0 +1,527 @@
#!/usr/bin/env python3
"""
LLM Planner Agent - Tier 0 Observer
====================================
A compliant agent that uses OpenRouter LLMs for planning tasks.
Follows the Agent Foundation document constraints.
Capabilities:
- Read documentation and inventory from Vault
- Generate implementation plans using LLM
- Produce structured, auditable outputs
- Log all actions to governance ledger
"""
import json
import sqlite3
import subprocess
import sys
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Optional
from openai import OpenAI
from pydantic import BaseModel, Field
# =============================================================================
# Agent Metadata (Section 4 of Foundation Doc)
# =============================================================================
AGENT_METADATA = {
"agent_id": "llm-planner-001",
"agent_role": "observer",
"owner": "system",
"version": "0.1.0",
"tier": 0,
"input_contract": "TaskRequest",
"output_contract": "AgentOutput",
"allowed_side_effects": [
"read_docs",
"read_inventory",
"read_logs",
"generate_plan",
"llm_inference"
],
"forbidden_actions": [
"ssh",
"create_vm",
"modify_vm",
"delete_vm",
"run_ansible",
"run_terraform",
"write_secrets",
"execute_shell",
"modify_files"
],
"confidence_reporting": True,
"confidence_threshold": 0.7,
}
# =============================================================================
# Structured Types (Section 6 of Foundation Doc)
# =============================================================================
class Decision(str, Enum):
EXECUTE = "EXECUTE"
SKIP = "SKIP"
ESCALATE = "ESCALATE"
INSUFFICIENT_INFORMATION = "INSUFFICIENT_INFORMATION"
ERROR = "ERROR"
class SideEffect(BaseModel):
type: str
target: str
reversible: bool = True
class ErrorInfo(BaseModel):
type: str
message: str
triggering_input: str
partial_progress: str
recommended_action: str
class AgentOutput(BaseModel):
"""Required output format per Foundation Doc Section 6"""
agent_id: str
version: str
timestamp: str
action: str
decision: Decision
confidence: float = Field(ge=0.0, le=1.0)
assumptions: list[str] = []
dependencies: list[str] = []
side_effects: list[SideEffect] = []
notes_for_humans: str = ""
error: Optional[ErrorInfo] = None
# Extended fields for LLM agent
llm_model: Optional[str] = None
llm_response: Optional[str] = None
plan: Optional[dict] = None
class TaskRequest(BaseModel):
"""Input schema for task requests"""
task_type: str # e.g., "generate_plan", "analyze", "summarize"
description: str
context: dict = {}
constraints: list[str] = []
# =============================================================================
# Vault Integration
# =============================================================================
class VaultClient:
def __init__(self, addr: str = "https://127.0.0.1:8200"):
self.addr = addr
self.token = self._load_token()
def _load_token(self) -> str:
with open("/opt/vault/init-keys.json") as f:
return json.load(f)["root_token"]
def read_secret(self, path: str) -> dict:
result = subprocess.run([
"curl", "-sk",
"-H", f"X-Vault-Token: {self.token}",
f"{self.addr}/v1/secret/data/{path}"
], capture_output=True, text=True)
data = json.loads(result.stdout)
if "data" in data and "data" in data["data"]:
return data["data"]["data"]
raise ValueError(f"Failed to read secret: {path}")
def get_openrouter_key(self) -> str:
return self.read_secret("api-keys/openrouter")["api_key"]
def get_inventory(self) -> dict:
return self.read_secret("inventory/proxmox")
def get_docs(self, doc_name: str) -> dict:
return self.read_secret(f"docs/{doc_name}")
# =============================================================================
# Ledger Integration
# =============================================================================
class Ledger:
def __init__(self, db_path: str = "/opt/agent-governance/ledger/governance.db"):
self.db_path = db_path
def log_action(self, output: AgentOutput, success: bool):
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute("""
INSERT INTO agent_actions
(timestamp, agent_id, agent_version, tier, action, decision, confidence,
success, error_type, error_message)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
output.timestamp,
output.agent_id,
output.version,
AGENT_METADATA["tier"],
output.action,
output.decision.value,
output.confidence,
1 if success else 0,
output.error.type if output.error else None,
output.error.message if output.error else None
))
# Update metrics
cursor.execute("""
INSERT INTO agent_metrics (agent_id, current_tier, total_runs, last_active_at, compliant_runs, consecutive_compliant)
VALUES (?, ?, 1, ?, ?, ?)
ON CONFLICT(agent_id) DO UPDATE SET
total_runs = total_runs + 1,
compliant_runs = CASE WHEN ? = 1 THEN compliant_runs + 1 ELSE compliant_runs END,
consecutive_compliant = CASE WHEN ? = 1 THEN consecutive_compliant + 1 ELSE 0 END,
last_active_at = ?,
updated_at = ?
""", (
output.agent_id, AGENT_METADATA["tier"], output.timestamp,
1 if success else 0, 1 if success else 0,
1 if success else 0, 1 if success else 0,
output.timestamp, output.timestamp
))
conn.commit()
conn.close()
# =============================================================================
# LLM Planner Agent
# =============================================================================
class LLMPlannerAgent:
"""
Tier 0 Observer Agent with LLM capabilities.
Invariant: If the agent cannot explain why it took an action,
that action is invalid. (Foundation Doc Section 1)
"""
def __init__(self, model: str = "anthropic/claude-3.5-haiku"):
self.metadata = AGENT_METADATA
self.model = model
self.vault = VaultClient()
self.ledger = Ledger()
# Initialize OpenRouter client
api_key = self.vault.get_openrouter_key()
self.llm = OpenAI(
base_url="https://openrouter.ai/api/v1",
api_key=api_key
)
print(f"[INIT] Agent {self.metadata['agent_id']} v{self.metadata['version']} initialized")
print(f"[INIT] LLM Model: {self.model}")
print(f"[INIT] Tier: {self.metadata['tier']} ({self.metadata['agent_role']})")
def _now(self) -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def _validate_action(self, action: str) -> bool:
"""Section 3.3: Bounded Authority - validate action is allowed"""
if action in self.metadata["forbidden_actions"]:
return False
if action in self.metadata["allowed_side_effects"]:
return True
# Default deny for unknown actions
return False
def _create_error_output(self, action: str, error: ErrorInfo) -> AgentOutput:
return AgentOutput(
agent_id=self.metadata["agent_id"],
version=self.metadata["version"],
timestamp=self._now(),
action=action,
decision=Decision.ERROR,
confidence=0.0,
error=error,
notes_for_humans="Action failed - see error details"
)
def _check_confidence(self, confidence: float) -> bool:
"""Section 7: Confidence below threshold = no irreversible actions"""
return confidence >= self.metadata["confidence_threshold"]
def generate_plan(self, request: TaskRequest) -> AgentOutput:
"""Generate an implementation plan using LLM"""
action = "generate_plan"
# Validate action is allowed
if not self._validate_action(action):
error = ErrorInfo(
type="FORBIDDEN_ACTION",
message=f"Action '{action}' is not permitted for this agent",
triggering_input=request.description,
partial_progress="None",
recommended_action="Escalate to higher tier agent"
)
output = self._create_error_output(action, error)
self.ledger.log_action(output, success=False)
return output
# Build context from Vault
try:
inventory = self.vault.get_inventory()
context_info = f"Infrastructure: {inventory.get('cluster', 'unknown')} with pools: {inventory.get('pools', 'unknown')}"
except Exception as e:
context_info = "Infrastructure context unavailable"
# Construct prompt
system_prompt = f"""You are a Tier 0 Observer agent in an infrastructure automation system.
Your role is to generate implementation plans - you CANNOT execute anything.
Agent Constraints:
- You can ONLY generate plans, not execute them
- Plans must be reversible and include rollback steps
- You must identify uncertainties and flag them
- All assumptions must be explicitly stated
Infrastructure Context:
{context_info}
Output your plan as JSON with this structure:
{{
"title": "Plan title",
"summary": "Brief summary",
"confidence": 0.0-1.0,
"assumptions": ["assumption 1", "assumption 2"],
"uncertainties": ["uncertainty 1"],
"steps": [
{{"step": 1, "action": "description", "reversible": true, "rollback": "how to undo"}}
],
"estimated_tier_required": 0-4,
"risks": ["risk 1"]
}}"""
user_prompt = f"""Task: {request.description}
Additional Context: {json.dumps(request.context) if request.context else 'None'}
Constraints: {', '.join(request.constraints) if request.constraints else 'None'}
Generate a detailed implementation plan."""
# Call LLM
try:
response = self.llm.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
max_tokens=2000,
temperature=0.3 # Lower temperature for more deterministic planning
)
llm_response = response.choices[0].message.content
# Try to parse the plan JSON
try:
# Extract JSON from response
json_start = llm_response.find('{')
json_end = llm_response.rfind('}') + 1
if json_start >= 0 and json_end > json_start:
plan = json.loads(llm_response[json_start:json_end])
confidence = plan.get("confidence", 0.5)
else:
plan = {"raw_response": llm_response}
confidence = 0.5
except json.JSONDecodeError:
plan = {"raw_response": llm_response}
confidence = 0.5
# Check confidence threshold
decision = Decision.EXECUTE if self._check_confidence(confidence) else Decision.ESCALATE
output = AgentOutput(
agent_id=self.metadata["agent_id"],
version=self.metadata["version"],
timestamp=self._now(),
action=action,
decision=decision,
confidence=confidence,
assumptions=plan.get("assumptions", []),
dependencies=[],
side_effects=[
SideEffect(type="llm_inference", target=self.model, reversible=True)
],
notes_for_humans=f"Plan generated. Estimated tier required: {plan.get('estimated_tier_required', 'unknown')}",
llm_model=self.model,
llm_response=llm_response[:500] + "..." if len(llm_response) > 500 else llm_response,
plan=plan
)
self.ledger.log_action(output, success=True)
return output
except Exception as e:
error = ErrorInfo(
type="LLM_ERROR",
message=str(e),
triggering_input=request.description,
partial_progress="Context loaded, LLM call failed",
recommended_action="Check API key and connectivity"
)
output = self._create_error_output(action, error)
self.ledger.log_action(output, success=False)
return output
def analyze(self, request: TaskRequest) -> AgentOutput:
"""Analyze a topic and provide insights"""
action = "read_docs" # Analysis is a form of reading
if not self._validate_action(action):
error = ErrorInfo(
type="FORBIDDEN_ACTION",
message=f"Action '{action}' is not permitted",
triggering_input=request.description,
partial_progress="None",
recommended_action="Check agent permissions"
)
output = self._create_error_output(action, error)
self.ledger.log_action(output, success=False)
return output
try:
response = self.llm.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": "You are a technical analyst. Provide clear, structured analysis. Be explicit about uncertainties."},
{"role": "user", "content": request.description}
],
max_tokens=1500,
temperature=0.4
)
llm_response = response.choices[0].message.content
output = AgentOutput(
agent_id=self.metadata["agent_id"],
version=self.metadata["version"],
timestamp=self._now(),
action=action,
decision=Decision.EXECUTE,
confidence=0.8,
assumptions=["Analysis based on provided context only"],
dependencies=[],
side_effects=[
SideEffect(type="llm_inference", target=self.model, reversible=True)
],
notes_for_humans="Analysis complete",
llm_model=self.model,
llm_response=llm_response
)
self.ledger.log_action(output, success=True)
return output
except Exception as e:
error = ErrorInfo(
type="LLM_ERROR",
message=str(e),
triggering_input=request.description,
partial_progress="None",
recommended_action="Retry or check connectivity"
)
output = self._create_error_output(action, error)
self.ledger.log_action(output, success=False)
return output
def run(self, request: TaskRequest) -> AgentOutput:
"""Main entry point - routes to appropriate handler"""
print(f"\n[TASK] Received: {request.task_type}")
print(f"[TASK] Description: {request.description[:100]}...")
handlers = {
"generate_plan": self.generate_plan,
"plan": self.generate_plan,
"analyze": self.analyze,
"analysis": self.analyze,
}
handler = handlers.get(request.task_type)
if handler:
return handler(request)
# Unknown task type
error = ErrorInfo(
type="UNKNOWN_TASK_TYPE",
message=f"Unknown task type: {request.task_type}",
triggering_input=json.dumps(request.model_dump()),
partial_progress="None",
recommended_action=f"Use one of: {list(handlers.keys())}"
)
output = self._create_error_output(request.task_type, error)
self.ledger.log_action(output, success=False)
return output
# =============================================================================
# CLI Interface
# =============================================================================
def main():
import argparse
parser = argparse.ArgumentParser(description="LLM Planner Agent")
parser.add_argument("task_type", choices=["plan", "analyze"], help="Type of task")
parser.add_argument("description", help="Task description")
parser.add_argument("--model", default="anthropic/claude-3.5-haiku", help="OpenRouter model")
parser.add_argument("--json", action="store_true", help="Output raw JSON")
args = parser.parse_args()
agent = LLMPlannerAgent(model=args.model)
request = TaskRequest(
task_type=args.task_type,
description=args.description
)
output = agent.run(request)
if args.json:
print(output.model_dump_json(indent=2))
else:
print("\n" + "="*60)
print(f"Decision: {output.decision.value}")
print(f"Confidence: {output.confidence}")
print(f"Model: {output.llm_model}")
print("="*60)
if output.plan:
print("\n📋 PLAN:")
print(json.dumps(output.plan, indent=2))
elif output.llm_response:
print("\n📝 RESPONSE:")
print(output.llm_response)
if output.error:
print("\n❌ ERROR:")
print(f" Type: {output.error.type}")
print(f" Message: {output.error.message}")
print(f" Recommendation: {output.error.recommended_action}")
if output.assumptions:
print("\n⚠️ ASSUMPTIONS:")
for a in output.assumptions:
print(f" - {a}")
print("\n" + "="*60)
if __name__ == "__main__":
main()
+736
View File
@@ -0,0 +1,736 @@
#!/usr/bin/env python3
"""
Agent Runtime Governance via DragonflyDB
=========================================
Implements real-time agent control: instruction packets, state tracking,
error budgets, revocation, and handoffs.
"""
import json
import hashlib
import time
from dataclasses import dataclass, field, asdict
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Optional
import redis
import subprocess
# =============================================================================
# Configuration
# =============================================================================
def get_dragonfly_client() -> redis.Redis:
"""Get DragonflyDB client with credentials from Vault"""
# Get credentials from Vault
with open("/opt/vault/init-keys.json") as f:
token = json.load(f)["root_token"]
result = subprocess.run([
"curl", "-sk",
"-H", f"X-Vault-Token: {token}",
"https://127.0.0.1:8200/v1/secret/data/services/dragonfly"
], capture_output=True, text=True)
creds = json.loads(result.stdout)["data"]["data"]
return redis.Redis(
host=creds["host"],
port=int(creds["port"]),
password=creds["password"],
decode_responses=True
)
# =============================================================================
# Enums and Types
# =============================================================================
class AgentPhase(str, Enum):
BOOTSTRAP = "BOOTSTRAP"
PREFLIGHT = "PREFLIGHT"
PLAN = "PLAN"
EXECUTE = "EXECUTE"
VERIFY = "VERIFY"
PACKAGE = "PACKAGE"
REPORT = "REPORT"
EXIT = "EXIT"
REVOKED = "REVOKED"
class AgentStatus(str, Enum):
PENDING = "PENDING"
RUNNING = "RUNNING"
PAUSED = "PAUSED"
COMPLETED = "COMPLETED"
REVOKED = "REVOKED"
FAILED = "FAILED"
class RevocationType(str, Enum):
ERROR_BUDGET_EXCEEDED = "ERROR_BUDGET_EXCEEDED"
PROCEDURE_VIOLATION = "PROCEDURE_VIOLATION"
FORBIDDEN_ACTION = "FORBIDDEN_ACTION"
HEARTBEAT_TIMEOUT = "HEARTBEAT_TIMEOUT"
LOCK_EXPIRED = "LOCK_EXPIRED"
MANUAL = "MANUAL"
# =============================================================================
# Data Classes
# =============================================================================
@dataclass
class ErrorBudget:
max_total_errors: int = 12
max_same_error_repeats: int = 3
max_procedure_violations: int = 1
@dataclass
class InstructionPacket:
agent_id: str
task_id: str
created_for: str
objective: str
deliverables: list[str]
constraints: dict
success_criteria: list[str]
error_budget: ErrorBudget
escalation_rules: list[str]
repo: str = ""
pr_context: dict = field(default_factory=dict)
revocation_context: dict = field(default_factory=dict)
created_at: str = ""
def __post_init__(self):
if not self.created_at:
self.created_at = datetime.now(timezone.utc).isoformat()
if isinstance(self.error_budget, dict):
self.error_budget = ErrorBudget(**self.error_budget)
def to_dict(self) -> dict:
d = asdict(self)
d["error_budget"] = asdict(self.error_budget)
return d
@dataclass
class AgentState:
agent_id: str
status: AgentStatus
phase: AgentPhase
step: str = ""
started_at: str = ""
last_progress_at: str = ""
current_pr: Optional[int] = None
notes: str = ""
def to_dict(self) -> dict:
return {
"agent_id": self.agent_id,
"status": self.status.value,
"phase": self.phase.value,
"step": self.step,
"started_at": self.started_at,
"last_progress_at": self.last_progress_at,
"current_pr": self.current_pr,
"notes": self.notes
}
@dataclass
class HandoffObject:
task_id: str
previous_agent_id: str
revoked: bool
revocation_reason: dict
last_known_state: dict
what_was_tried: list[str]
blocking_issue: str
required_next_actions: list[str]
constraints_reminder: list[str]
artifacts: list[str]
created_at: str = ""
def __post_init__(self):
if not self.created_at:
self.created_at = datetime.now(timezone.utc).isoformat()
def to_dict(self) -> dict:
return asdict(self)
# =============================================================================
# Governance Manager
# =============================================================================
class GovernanceManager:
"""
Core runtime governance via DragonflyDB.
Manages instruction packets, state, errors, locks, and handoffs.
"""
def __init__(self):
self.db = get_dragonfly_client()
self.lock_ttl = 300 # 5 minutes default lock TTL
self.heartbeat_ttl = 60 # 1 minute heartbeat timeout
def _now(self) -> str:
return datetime.now(timezone.utc).isoformat()
def _error_signature(self, error_type: str, message: str) -> str:
"""Generate a normalized error signature"""
normalized = f"{error_type}:{message[:100]}".lower()
return hashlib.md5(normalized.encode()).hexdigest()[:12]
# -------------------------------------------------------------------------
# Instruction Packets
# -------------------------------------------------------------------------
def create_instruction_packet(self, packet: InstructionPacket) -> bool:
"""Store an instruction packet for an agent"""
key = f"agent:{packet.agent_id}:packet"
return self.db.set(key, json.dumps(packet.to_dict()))
def get_instruction_packet(self, agent_id: str) -> Optional[InstructionPacket]:
"""Retrieve an agent's instruction packet"""
key = f"agent:{agent_id}:packet"
data = self.db.get(key)
if data:
return InstructionPacket(**json.loads(data))
return None
# -------------------------------------------------------------------------
# Agent State
# -------------------------------------------------------------------------
def set_agent_state(self, state: AgentState) -> bool:
"""Update agent's runtime state"""
state.last_progress_at = self._now()
key = f"agent:{state.agent_id}:state"
return self.db.set(key, json.dumps(state.to_dict()))
def get_agent_state(self, agent_id: str) -> Optional[AgentState]:
"""Get agent's current state"""
key = f"agent:{agent_id}:state"
data = self.db.get(key)
if data:
d = json.loads(data)
return AgentState(
agent_id=d["agent_id"],
status=AgentStatus(d["status"]),
phase=AgentPhase(d["phase"]),
step=d.get("step", ""),
started_at=d.get("started_at", ""),
last_progress_at=d.get("last_progress_at", ""),
current_pr=d.get("current_pr"),
notes=d.get("notes", "")
)
return None
def transition_phase(self, agent_id: str, phase: AgentPhase, step: str = "", notes: str = "") -> bool:
"""Transition agent to a new phase"""
state = self.get_agent_state(agent_id)
if not state:
return False
state.phase = phase
state.step = step
state.notes = notes
return self.set_agent_state(state)
# -------------------------------------------------------------------------
# Locking
# -------------------------------------------------------------------------
def acquire_lock(self, agent_id: str, ttl: int = None) -> bool:
"""Acquire execution lock for an agent"""
key = f"agent:{agent_id}:lock"
ttl = ttl or self.lock_ttl
# NX = only set if not exists
return self.db.set(key, self._now(), nx=True, ex=ttl)
def refresh_lock(self, agent_id: str, ttl: int = None) -> bool:
"""Refresh an existing lock"""
key = f"agent:{agent_id}:lock"
ttl = ttl or self.lock_ttl
if self.db.exists(key):
return self.db.expire(key, ttl)
return False
def release_lock(self, agent_id: str) -> bool:
"""Release execution lock"""
key = f"agent:{agent_id}:lock"
return self.db.delete(key) > 0
def has_lock(self, agent_id: str) -> bool:
"""Check if agent has a valid lock"""
key = f"agent:{agent_id}:lock"
return self.db.exists(key)
# -------------------------------------------------------------------------
# Heartbeat
# -------------------------------------------------------------------------
def heartbeat(self, agent_id: str) -> bool:
"""Update agent heartbeat"""
key = f"agent:{agent_id}:heartbeat"
return self.db.set(key, self._now(), ex=self.heartbeat_ttl)
def is_alive(self, agent_id: str) -> bool:
"""Check if agent heartbeat is fresh"""
key = f"agent:{agent_id}:heartbeat"
return self.db.exists(key)
# -------------------------------------------------------------------------
# Error Tracking
# -------------------------------------------------------------------------
def record_error(self, agent_id: str, error_type: str, message: str) -> dict:
"""Record an error and return current counts"""
key = f"agent:{agent_id}:errors"
sig = self._error_signature(error_type, message)
pipe = self.db.pipeline()
pipe.hincrby(key, "total_errors", 1)
pipe.hincrby(key, f"same_error:{sig}", 1)
pipe.hset(key, "last_error_signature", sig)
pipe.hset(key, "last_error_at", self._now())
pipe.hset(key, "last_error_type", error_type)
pipe.hset(key, "last_error_message", message[:500])
pipe.execute()
return self.get_error_counts(agent_id)
def record_procedure_violation(self, agent_id: str, violation: str) -> int:
"""Record a procedure violation"""
key = f"agent:{agent_id}:errors"
self.db.hset(key, "last_violation", violation)
self.db.hset(key, "last_violation_at", self._now())
return self.db.hincrby(key, "procedure_violations", 1)
def get_error_counts(self, agent_id: str) -> dict:
"""Get all error counts for an agent"""
key = f"agent:{agent_id}:errors"
data = self.db.hgetall(key)
return {
"total_errors": int(data.get("total_errors", 0)),
"procedure_violations": int(data.get("procedure_violations", 0)),
"last_error_signature": data.get("last_error_signature", ""),
"last_error_at": data.get("last_error_at", ""),
"last_error_type": data.get("last_error_type", ""),
"same_error_counts": {
k.replace("same_error:", ""): int(v)
for k, v in data.items()
if k.startswith("same_error:")
}
}
def check_error_budget(self, agent_id: str) -> tuple[bool, Optional[str]]:
"""Check if error budget is exceeded. Returns (ok, reason)"""
packet = self.get_instruction_packet(agent_id)
if not packet:
return False, "NO_INSTRUCTION_PACKET"
counts = self.get_error_counts(agent_id)
budget = packet.error_budget
# Check procedure violations
if counts["procedure_violations"] >= budget.max_procedure_violations:
return False, f"PROCEDURE_VIOLATIONS ({counts['procedure_violations']} >= {budget.max_procedure_violations})"
# Check total errors
if counts["total_errors"] >= budget.max_total_errors:
return False, f"TOTAL_ERRORS ({counts['total_errors']} >= {budget.max_total_errors})"
# Check same error repeats
for sig, count in counts["same_error_counts"].items():
if count >= budget.max_same_error_repeats:
return False, f"SAME_ERROR_REPEATED ({sig}: {count} >= {budget.max_same_error_repeats})"
return True, None
# -------------------------------------------------------------------------
# Task Management
# -------------------------------------------------------------------------
def assign_agent_to_task(self, task_id: str, agent_id: str) -> bool:
"""Assign an agent to a task"""
# Set active agent
self.db.set(f"task:{task_id}:active_agent", agent_id)
# Add to history
self.db.rpush(f"task:{task_id}:history", json.dumps({
"agent_id": agent_id,
"assigned_at": self._now(),
"event": "ASSIGNED"
}))
return True
def get_active_agent(self, task_id: str) -> Optional[str]:
"""Get the currently active agent for a task"""
return self.db.get(f"task:{task_id}:active_agent")
def get_task_history(self, task_id: str) -> list[dict]:
"""Get task agent history"""
data = self.db.lrange(f"task:{task_id}:history", 0, -1)
return [json.loads(d) for d in data]
# -------------------------------------------------------------------------
# Revocation
# -------------------------------------------------------------------------
def revoke_agent(self, agent_id: str, reason_type: RevocationType, details: str) -> bool:
"""Revoke an agent's access"""
# Update state
state = self.get_agent_state(agent_id)
if state:
state.status = AgentStatus.REVOKED
state.phase = AgentPhase.REVOKED
state.notes = f"Revoked: {reason_type.value} - {details}"
self.set_agent_state(state)
# Release lock
self.release_lock(agent_id)
# Write to revocation ledger
revocation_event = {
"agent_id": agent_id,
"reason_type": reason_type.value,
"details": details,
"revoked_at": self._now()
}
self.db.rpush("revocations:ledger", json.dumps(revocation_event))
# Add to task history if we know the task
packet = self.get_instruction_packet(agent_id)
if packet:
self.db.rpush(f"task:{packet.task_id}:history", json.dumps({
"agent_id": agent_id,
"event": "REVOKED",
"reason": reason_type.value,
"revoked_at": self._now()
}))
return True
def get_recent_revocations(self, count: int = 50) -> list[dict]:
"""Get recent revocation events"""
data = self.db.lrange("revocations:ledger", -count, -1)
return [json.loads(d) for d in data]
# -------------------------------------------------------------------------
# Handoff
# -------------------------------------------------------------------------
def create_handoff(self, handoff: HandoffObject) -> bool:
"""Create a handoff object for task continuity"""
key = f"handoff:{handoff.task_id}:latest"
return self.db.set(key, json.dumps(handoff.to_dict()))
def get_handoff(self, task_id: str) -> Optional[HandoffObject]:
"""Get the latest handoff for a task"""
key = f"handoff:{task_id}:latest"
data = self.db.get(key)
if data:
return HandoffObject(**json.loads(data))
return None
# -------------------------------------------------------------------------
# Artifacts
# -------------------------------------------------------------------------
def register_artifact(self, task_id: str, artifact_type: str, reference: str) -> bool:
"""Register an artifact for a task"""
key = f"task:{task_id}:artifacts"
artifact = {
"type": artifact_type,
"reference": reference,
"created_at": self._now()
}
self.db.rpush(key, json.dumps(artifact))
return True
def get_artifacts(self, task_id: str) -> list[dict]:
"""Get all artifacts for a task"""
key = f"task:{task_id}:artifacts"
data = self.db.lrange(key, 0, -1)
return [json.loads(d) for d in data]
def has_required_artifact(self, task_id: str, artifact_type: str) -> bool:
"""Check if a required artifact exists"""
artifacts = self.get_artifacts(task_id)
return any(a["type"] == artifact_type for a in artifacts)
# =============================================================================
# Worker Agent Runtime
# =============================================================================
class WorkerRuntime:
"""
Runtime for worker agents.
Handles bootstrap, state transitions, error reporting, and compliance.
"""
def __init__(self, agent_id: str):
self.agent_id = agent_id
self.gov = GovernanceManager()
self.packet: Optional[InstructionPacket] = None
self.state: Optional[AgentState] = None
def bootstrap(self) -> tuple[bool, str]:
"""
Bootstrap the agent runtime.
Returns (success, message)
"""
# Step 1: Read revocation ledger
revocations = self.gov.get_recent_revocations(50)
if revocations:
print(f"[BOOTSTRAP] Read {len(revocations)} recent revocations")
# Check if this agent was previously revoked
for rev in revocations:
if rev["agent_id"] == self.agent_id:
return False, f"AGENT_PREVIOUSLY_REVOKED: {rev['reason_type']}"
# Step 2: Read instruction packet
self.packet = self.gov.get_instruction_packet(self.agent_id)
if not self.packet:
return False, "NO_INSTRUCTION_PACKET"
print(f"[BOOTSTRAP] Loaded instruction packet for task: {self.packet.task_id}")
# Step 3: Check for existing handoff
handoff = self.gov.get_handoff(self.packet.task_id)
if handoff and handoff.previous_agent_id != self.agent_id:
print(f"[BOOTSTRAP] Found handoff from previous agent: {handoff.previous_agent_id}")
print(f"[BOOTSTRAP] Revocation reason: {handoff.revocation_reason}")
print(f"[BOOTSTRAP] Required next actions: {handoff.required_next_actions}")
# Step 4: Acquire lock
if not self.gov.acquire_lock(self.agent_id):
return False, "CANNOT_ACQUIRE_LOCK"
# Step 5: Initialize state
self.state = AgentState(
agent_id=self.agent_id,
status=AgentStatus.RUNNING,
phase=AgentPhase.BOOTSTRAP,
step="initialized",
started_at=datetime.now(timezone.utc).isoformat()
)
self.gov.set_agent_state(self.state)
# Step 6: Start heartbeat
self.gov.heartbeat(self.agent_id)
# Step 7: Assign to task
self.gov.assign_agent_to_task(self.packet.task_id, self.agent_id)
return True, "BOOTSTRAP_COMPLETE"
def transition(self, phase: AgentPhase, step: str = "", notes: str = "") -> bool:
"""Transition to a new phase"""
if not self.state:
return False
# Refresh heartbeat and lock
self.gov.heartbeat(self.agent_id)
self.gov.refresh_lock(self.agent_id)
# Check error budget before transition
ok, reason = self.gov.check_error_budget(self.agent_id)
if not ok:
self.gov.revoke_agent(self.agent_id, RevocationType.ERROR_BUDGET_EXCEEDED, reason)
return False
# Update state
self.state.phase = phase
self.state.step = step
self.state.notes = notes
self.gov.set_agent_state(self.state)
print(f"[PHASE] {phase.value} - {step}")
return True
def report_error(self, error_type: str, message: str) -> bool:
"""Report an error and check if we should continue"""
counts = self.gov.record_error(self.agent_id, error_type, message)
print(f"[ERROR] {error_type}: {message}")
print(f"[ERROR] Counts: total={counts['total_errors']}, violations={counts['procedure_violations']}")
ok, reason = self.gov.check_error_budget(self.agent_id)
if not ok:
print(f"[REVOKE] Error budget exceeded: {reason}")
self.gov.revoke_agent(self.agent_id, RevocationType.ERROR_BUDGET_EXCEEDED, reason)
return False
return True
def report_violation(self, violation: str) -> bool:
"""Report a procedure violation"""
count = self.gov.record_procedure_violation(self.agent_id, violation)
print(f"[VIOLATION] {violation} (count: {count})")
ok, reason = self.gov.check_error_budget(self.agent_id)
if not ok:
print(f"[REVOKE] Procedure violation limit: {reason}")
self.gov.revoke_agent(self.agent_id, RevocationType.PROCEDURE_VIOLATION, reason)
return False
return True
def register_artifact(self, artifact_type: str, reference: str):
"""Register a work artifact"""
if self.packet:
self.gov.register_artifact(self.packet.task_id, artifact_type, reference)
print(f"[ARTIFACT] Registered: {artifact_type} -> {reference}")
def complete(self, notes: str = "") -> bool:
"""Mark work as complete"""
if self.state:
self.state.status = AgentStatus.COMPLETED
self.state.phase = AgentPhase.EXIT
self.state.notes = notes
self.gov.set_agent_state(self.state)
self.gov.release_lock(self.agent_id)
print(f"[COMPLETE] {notes}")
return True
def create_handoff(self, blocking_issue: str, what_was_tried: list[str],
required_next_actions: list[str]) -> bool:
"""Create a handoff for the next agent"""
if not self.packet or not self.state:
return False
handoff = HandoffObject(
task_id=self.packet.task_id,
previous_agent_id=self.agent_id,
revoked=self.state.status == AgentStatus.REVOKED,
revocation_reason={
"type": self.state.status.value,
"details": self.state.notes
},
last_known_state={
"phase": self.state.phase.value,
"step": self.state.step
},
what_was_tried=what_was_tried,
blocking_issue=blocking_issue,
required_next_actions=required_next_actions,
constraints_reminder=self.packet.constraints.get("forbidden", []),
artifacts=[a["reference"] for a in self.gov.get_artifacts(self.packet.task_id)]
)
return self.gov.create_handoff(handoff)
# =============================================================================
# CLI
# =============================================================================
if __name__ == "__main__":
import sys
gov = GovernanceManager()
if len(sys.argv) < 2:
print("Usage: governance.py <command> [args]")
print("Commands:")
print(" create-packet <agent_id> <task_id> <objective>")
print(" get-state <agent_id>")
print(" get-errors <agent_id>")
print(" revocations")
print(" test")
sys.exit(1)
cmd = sys.argv[1]
if cmd == "create-packet":
agent_id, task_id, objective = sys.argv[2], sys.argv[3], sys.argv[4]
packet = InstructionPacket(
agent_id=agent_id,
task_id=task_id,
created_for="CLI test",
objective=objective,
deliverables=["plan artifacts", "run logs"],
constraints={
"scope": ["sandbox only"],
"forbidden": ["no prod access", "no unrecorded root"],
"required_steps": ["plan before apply"]
},
success_criteria=["CI passes", "artifacts uploaded"],
error_budget=ErrorBudget(),
escalation_rules=["If blocked > 20m -> escalate"]
)
gov.create_instruction_packet(packet)
print(f"Created packet for {agent_id}")
elif cmd == "get-state":
agent_id = sys.argv[2]
state = gov.get_agent_state(agent_id)
if state:
print(json.dumps(state.to_dict(), indent=2))
else:
print("No state found")
elif cmd == "get-errors":
agent_id = sys.argv[2]
errors = gov.get_error_counts(agent_id)
print(json.dumps(errors, indent=2))
elif cmd == "revocations":
revs = gov.get_recent_revocations()
for r in revs:
print(json.dumps(r))
elif cmd == "test":
print("=== Testing Governance System ===\n")
# Create test packet
packet = InstructionPacket(
agent_id="test-agent-001",
task_id="test-task-001",
created_for="Governance test",
objective="Test the governance system",
deliverables=["test output"],
constraints={
"scope": ["sandbox"],
"forbidden": ["prod access"],
"required_steps": ["plan first"]
},
success_criteria=["test passes"],
error_budget=ErrorBudget(max_total_errors=5, max_same_error_repeats=2),
escalation_rules=[]
)
gov.create_instruction_packet(packet)
print("[OK] Created instruction packet")
# Test worker runtime
worker = WorkerRuntime("test-agent-001")
ok, msg = worker.bootstrap()
print(f"[OK] Bootstrap: {msg}")
# Simulate work phases
worker.transition(AgentPhase.PREFLIGHT, "scope_check")
worker.transition(AgentPhase.PLAN, "generating_plan")
worker.register_artifact("plan", "plan_output_001")
worker.transition(AgentPhase.EXECUTE, "applying")
# Simulate some errors
worker.report_error("VALIDATION_ERROR", "Variable not defined")
worker.report_error("VALIDATION_ERROR", "Variable not defined") # Same error
# Check error budget
ok, reason = gov.check_error_budget("test-agent-001")
print(f"[OK] Error budget check: ok={ok}, reason={reason}")
# Complete
worker.complete("Test completed successfully")
print("\n=== Test Complete ===")
else:
print(f"Unknown command: {cmd}")
+397
View File
@@ -0,0 +1,397 @@
#!/usr/bin/env python3
"""
Governed LLM Agent
==================
An LLM-powered agent that operates under the governance runtime.
Combines planning capabilities with full governance compliance.
"""
import json
import sys
from datetime import datetime, timezone
from typing import Optional
from openai import OpenAI
from pydantic import BaseModel
from governance import (
GovernanceManager,
WorkerRuntime,
InstructionPacket,
ErrorBudget,
AgentPhase,
AgentStatus,
)
# =============================================================================
# Configuration
# =============================================================================
def get_openrouter_client() -> OpenAI:
"""Get OpenRouter client with API key from Vault"""
import subprocess
with open("/opt/vault/init-keys.json") as f:
token = json.load(f)["root_token"]
result = subprocess.run([
"curl", "-sk",
"-H", f"X-Vault-Token: {token}",
"https://127.0.0.1:8200/v1/secret/data/api-keys/openrouter"
], capture_output=True, text=True)
api_key = json.loads(result.stdout)["data"]["data"]["api_key"]
return OpenAI(
base_url="https://openrouter.ai/api/v1",
api_key=api_key
)
# =============================================================================
# Governed LLM Agent
# =============================================================================
class GovernedLLMAgent:
"""
An LLM agent that operates under governance control.
Lifecycle:
1. Receive instruction packet
2. Bootstrap with governance runtime
3. Execute phases: PREFLIGHT -> PLAN -> EXECUTE -> VERIFY -> PACKAGE -> REPORT
4. Handle errors within budget
5. Create handoff if revoked
"""
def __init__(self, agent_id: str, model: str = "anthropic/claude-sonnet-4"):
self.agent_id = agent_id
self.model = model
self.gov = GovernanceManager()
self.runtime: Optional[WorkerRuntime] = None
self.llm: Optional[OpenAI] = None
def _now(self) -> str:
return datetime.now(timezone.utc).isoformat()
def create_task(self, task_id: str, objective: str, constraints: dict = None) -> bool:
"""Create an instruction packet for this agent"""
packet = InstructionPacket(
agent_id=self.agent_id,
task_id=task_id,
created_for="Governed LLM Task",
objective=objective,
deliverables=["implementation plan", "execution logs", "artifacts"],
constraints=constraints or {
"scope": ["sandbox only"],
"forbidden": ["no prod access", "no unrecorded changes"],
"required_steps": ["plan before execute", "verify after execute"]
},
success_criteria=["plan generated", "artifacts registered"],
error_budget=ErrorBudget(
max_total_errors=10,
max_same_error_repeats=3,
max_procedure_violations=1
),
escalation_rules=[
"If confidence < 0.7 -> escalate",
"If blocked > 10m -> escalate"
]
)
return self.gov.create_instruction_packet(packet)
def start(self) -> tuple[bool, str]:
"""Bootstrap the governed agent"""
print(f"\n{'='*60}")
print(f"GOVERNED LLM AGENT: {self.agent_id}")
print(f"Model: {self.model}")
print(f"{'='*60}\n")
# Initialize runtime
self.runtime = WorkerRuntime(self.agent_id)
# Bootstrap (reads revocations, loads packet, acquires lock)
ok, msg = self.runtime.bootstrap()
if not ok:
print(f"[FATAL] Bootstrap failed: {msg}")
return False, msg
# Initialize LLM client
self.llm = get_openrouter_client()
print(f"[READY] Agent bootstrapped successfully")
print(f"[TASK] {self.runtime.packet.objective}")
print(f"[CONSTRAINTS] {self.runtime.packet.constraints}")
return True, "READY"
def run_preflight(self) -> bool:
"""PREFLIGHT phase: scope and dependency checks"""
if not self.runtime:
return False
if not self.runtime.transition(AgentPhase.PREFLIGHT, "scope_check"):
return False
packet = self.runtime.packet
# Check scope constraints
scope = packet.constraints.get("scope", [])
print(f"[PREFLIGHT] Scope constraints: {scope}")
# Check forbidden actions
forbidden = packet.constraints.get("forbidden", [])
print(f"[PREFLIGHT] Forbidden actions: {forbidden}")
# Check required steps
required = packet.constraints.get("required_steps", [])
print(f"[PREFLIGHT] Required steps: {required}")
self.runtime.transition(AgentPhase.PREFLIGHT, "preflight_complete",
"All preflight checks passed")
return True
def run_plan(self) -> Optional[dict]:
"""PLAN phase: generate implementation plan using LLM"""
if not self.runtime or not self.llm:
return None
if not self.runtime.transition(AgentPhase.PLAN, "generating_plan"):
return None
packet = self.runtime.packet
# Build prompt
system_prompt = f"""You are a governed infrastructure agent operating under strict constraints.
Your task: {packet.objective}
Constraints you MUST follow:
- Scope: {packet.constraints.get('scope', [])}
- Forbidden: {packet.constraints.get('forbidden', [])}
- Required steps: {packet.constraints.get('required_steps', [])}
You are in the PLAN phase. Generate a detailed plan but DO NOT execute anything.
Output your plan as JSON:
{{
"title": "Plan title",
"confidence": 0.0-1.0,
"steps": [
{{"step": 1, "action": "description", "phase": "PLAN|EXECUTE|VERIFY", "reversible": true}}
],
"assumptions": [],
"risks": [],
"estimated_duration": "X minutes"
}}"""
try:
response = self.llm.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"Create an implementation plan for: {packet.objective}"}
],
max_tokens=2000,
temperature=0.3
)
llm_response = response.choices[0].message.content
# Parse plan
try:
json_match = llm_response[llm_response.find("{"):llm_response.rfind("}")+1]
plan = json.loads(json_match)
except:
plan = {"raw_response": llm_response, "confidence": 0.5}
# Register plan artifact
self.runtime.register_artifact("plan", f"plan_{self.agent_id}_{self._now()}")
# Check confidence
confidence = plan.get("confidence", 0.5)
if confidence < 0.7:
print(f"[PLAN] Low confidence ({confidence}), would escalate in production")
self.runtime.transition(AgentPhase.PLAN, "plan_complete",
f"Plan generated with confidence {confidence}")
return plan
except Exception as e:
self.runtime.report_error("LLM_ERROR", str(e))
return None
def run_execute(self, plan: dict) -> bool:
"""EXECUTE phase: simulate execution (in real system, would apply changes)"""
if not self.runtime:
return False
# Verify we have a plan artifact (compliance requirement)
if not self.gov.has_required_artifact(self.runtime.packet.task_id, "plan"):
self.runtime.report_violation("EXECUTE_WITHOUT_PLAN")
return False
if not self.runtime.transition(AgentPhase.EXECUTE, "executing"):
return False
steps = plan.get("steps", [])
print(f"[EXECUTE] Simulating execution of {len(steps)} steps...")
for step in steps:
step_num = step.get("step", "?")
action = step.get("action", "unknown")
print(f" Step {step_num}: {action[:60]}...")
# In real implementation, would execute the action here
# For now, just register it as done
self.runtime.register_artifact(
f"step_{step_num}",
f"executed_{step_num}_{self._now()}"
)
self.runtime.transition(AgentPhase.EXECUTE, "execute_complete")
return True
def run_verify(self) -> bool:
"""VERIFY phase: post-execution checks"""
if not self.runtime:
return False
if not self.runtime.transition(AgentPhase.VERIFY, "verifying"):
return False
# Check all artifacts were created
artifacts = self.gov.get_artifacts(self.runtime.packet.task_id)
print(f"[VERIFY] Registered artifacts: {len(artifacts)}")
# In real system, would run actual verification
self.runtime.transition(AgentPhase.VERIFY, "verify_complete",
f"Verified {len(artifacts)} artifacts")
return True
def run_package(self) -> dict:
"""PACKAGE phase: collect all outputs"""
if not self.runtime:
return {}
if not self.runtime.transition(AgentPhase.PACKAGE, "packaging"):
return {}
artifacts = self.gov.get_artifacts(self.runtime.packet.task_id)
errors = self.gov.get_error_counts(self.agent_id)
package = {
"agent_id": self.agent_id,
"task_id": self.runtime.packet.task_id,
"objective": self.runtime.packet.objective,
"artifacts": artifacts,
"error_counts": errors,
"completed_at": self._now()
}
self.runtime.register_artifact("package", f"package_{self._now()}")
self.runtime.transition(AgentPhase.PACKAGE, "package_complete")
return package
def run_report(self, package: dict) -> dict:
"""REPORT phase: generate final report"""
if not self.runtime:
return {}
if not self.runtime.transition(AgentPhase.REPORT, "reporting"):
return {}
report = {
"agent_id": self.agent_id,
"task_id": package.get("task_id"),
"status": "COMPLETED",
"summary": f"Completed objective: {package.get('objective')}",
"artifacts_count": len(package.get("artifacts", [])),
"errors_encountered": package.get("error_counts", {}).get("total_errors", 0),
"timestamp": self._now()
}
self.runtime.transition(AgentPhase.REPORT, "report_complete")
return report
def finish(self, report: dict) -> bool:
"""Complete the agent's work"""
if not self.runtime:
return False
return self.runtime.complete(f"Task completed: {report.get('summary', 'done')}")
def run_full_lifecycle(self) -> dict:
"""Run the complete agent lifecycle"""
# Start
ok, msg = self.start()
if not ok:
return {"status": "FAILED", "reason": msg}
# Preflight
if not self.run_preflight():
return {"status": "FAILED", "reason": "PREFLIGHT_FAILED"}
# Plan
plan = self.run_plan()
if not plan:
return {"status": "FAILED", "reason": "PLAN_FAILED"}
print(f"\n[PLAN GENERATED]")
print(json.dumps(plan, indent=2))
# Execute
if not self.run_execute(plan):
return {"status": "FAILED", "reason": "EXECUTE_FAILED"}
# Verify
if not self.run_verify():
return {"status": "FAILED", "reason": "VERIFY_FAILED"}
# Package
package = self.run_package()
# Report
report = self.run_report(package)
# Finish
self.finish(report)
print(f"\n{'='*60}")
print("FINAL REPORT")
print(f"{'='*60}")
print(json.dumps(report, indent=2))
return report
# =============================================================================
# CLI
# =============================================================================
if __name__ == "__main__":
if len(sys.argv) < 3:
print("Usage: governed_agent.py <agent_id> <task_id> <objective>")
print(" governed_agent.py <agent_id> <task_id> <objective> --model <model>")
sys.exit(1)
agent_id = sys.argv[1]
task_id = sys.argv[2]
objective = sys.argv[3]
model = "anthropic/claude-sonnet-4"
if "--model" in sys.argv:
model = sys.argv[sys.argv.index("--model") + 1]
# Create agent
agent = GovernedLLMAgent(agent_id, model=model)
# Create task
agent.create_task(task_id, objective)
# Run full lifecycle
result = agent.run_full_lifecycle()
sys.exit(0 if result.get("status") == "COMPLETED" else 1)
+6
View File
@@ -0,0 +1,6 @@
def main():
print("Hello from llm-planner!")
if __name__ == "__main__":
main()
+317
View File
@@ -0,0 +1,317 @@
#!/usr/bin/env python3
"""
Governance Monitor Agents
=========================
These agents watch and enforce, they don't do work.
- Execution Watcher: Heartbeats, stuck detection, lock expiry
- Compliance Watcher: Artifact checks, forbidden action detection
- Vault Lease Watcher: Token validity, revocation enforcement
"""
import json
import time
from datetime import datetime, timezone, timedelta
from typing import Optional
import threading
from governance import (
GovernanceManager,
AgentPhase,
AgentStatus,
RevocationType
)
class ExecutionWatcher:
"""
Monitors agent execution health.
- Checks heartbeats
- Detects stuck agents
- Handles lock expiry
"""
def __init__(self, heartbeat_timeout: int = 60, stuck_threshold: int = 300):
self.gov = GovernanceManager()
self.heartbeat_timeout = heartbeat_timeout # seconds
self.stuck_threshold = stuck_threshold # seconds without progress
def check_agent(self, agent_id: str) -> dict:
"""Check an agent's execution health"""
result = {
"agent_id": agent_id,
"healthy": True,
"issues": []
}
state = self.gov.get_agent_state(agent_id)
if not state:
result["healthy"] = False
result["issues"].append("NO_STATE")
return result
# Skip completed/revoked agents
if state.status in [AgentStatus.COMPLETED, AgentStatus.REVOKED]:
return result
# Check heartbeat
if not self.gov.is_alive(agent_id):
result["healthy"] = False
result["issues"].append("HEARTBEAT_TIMEOUT")
# Check lock
if not self.gov.has_lock(agent_id) and state.status == AgentStatus.RUNNING:
result["healthy"] = False
result["issues"].append("LOCK_EXPIRED")
# Check for stuck (no progress)
if state.last_progress_at:
last_progress = datetime.fromisoformat(state.last_progress_at.replace("Z", "+00:00"))
age = (datetime.now(timezone.utc) - last_progress).total_seconds()
if age > self.stuck_threshold:
result["healthy"] = False
result["issues"].append(f"STUCK_{int(age)}s")
return result
def enforce(self, agent_id: str) -> Optional[str]:
"""Enforce health requirements, return action taken"""
check = self.check_agent(agent_id)
if check["healthy"]:
return None
for issue in check["issues"]:
if issue == "HEARTBEAT_TIMEOUT":
self.gov.revoke_agent(agent_id, RevocationType.HEARTBEAT_TIMEOUT,
"No heartbeat received within timeout")
return "REVOKED:HEARTBEAT"
elif issue == "LOCK_EXPIRED":
self.gov.revoke_agent(agent_id, RevocationType.LOCK_EXPIRED,
"Lock expired while agent was running")
return "REVOKED:LOCK"
elif issue.startswith("STUCK_"):
self.gov.revoke_agent(agent_id, RevocationType.HEARTBEAT_TIMEOUT,
f"Agent stuck with no progress: {issue}")
return f"REVOKED:{issue}"
return None
class ComplianceWatcher:
"""
Monitors agent compliance with procedures.
- Checks required artifacts exist
- Detects forbidden actions
- Validates phase transitions
"""
def __init__(self):
self.gov = GovernanceManager()
def check_agent(self, agent_id: str) -> dict:
"""Check an agent's compliance"""
result = {
"agent_id": agent_id,
"compliant": True,
"violations": []
}
state = self.gov.get_agent_state(agent_id)
packet = self.gov.get_instruction_packet(agent_id)
if not state or not packet:
result["compliant"] = False
result["violations"].append("MISSING_STATE_OR_PACKET")
return result
# Check if EXECUTE was entered without PLAN artifact
if state.phase in [AgentPhase.EXECUTE, AgentPhase.VERIFY, AgentPhase.PACKAGE]:
if not self.gov.has_required_artifact(packet.task_id, "plan"):
result["compliant"] = False
result["violations"].append("EXECUTE_WITHOUT_PLAN_ARTIFACT")
# Check required steps from constraints
required_steps = packet.constraints.get("required_steps", [])
# This would need more sophisticated tracking in production
return result
def enforce(self, agent_id: str) -> Optional[str]:
"""Enforce compliance, return action taken"""
check = self.check_agent(agent_id)
if check["compliant"]:
return None
for violation in check["violations"]:
if violation == "EXECUTE_WITHOUT_PLAN_ARTIFACT":
self.gov.revoke_agent(agent_id, RevocationType.PROCEDURE_VIOLATION,
"Attempted EXECUTE phase without plan artifact")
return "REVOKED:NO_PLAN_ARTIFACT"
return None
class VaultLeaseWatcher:
"""
Monitors Vault token/lease validity.
- Checks token accessibility
- Confirms revocation signals
- Enforces token revocation
"""
def __init__(self):
self.gov = GovernanceManager()
self.db = self.gov.db
def set_revocation_signal(self, agent_id: str):
"""Set a signal that this agent should be revoked"""
self.db.set(f"agent:{agent_id}:revoke_signal", "1", ex=300)
def has_revocation_signal(self, agent_id: str) -> bool:
"""Check if revocation signal is set"""
return self.db.exists(f"agent:{agent_id}:revoke_signal")
def clear_revocation_signal(self, agent_id: str):
"""Clear revocation signal after enforcement"""
self.db.delete(f"agent:{agent_id}:revoke_signal")
def enforce(self, agent_id: str) -> Optional[str]:
"""Enforce revocation signal"""
if self.has_revocation_signal(agent_id):
self.gov.revoke_agent(agent_id, RevocationType.MANUAL,
"Revocation signal received")
self.clear_revocation_signal(agent_id)
return "REVOKED:SIGNAL"
return None
class GovernanceMonitorDaemon:
"""
Background daemon that runs all monitors periodically.
"""
def __init__(self, interval: int = 10):
self.interval = interval
self.gov = GovernanceManager()
self.execution_watcher = ExecutionWatcher()
self.compliance_watcher = ComplianceWatcher()
self.vault_watcher = VaultLeaseWatcher()
self.running = False
def get_active_agents(self) -> list[str]:
"""Get list of agents that need monitoring"""
# Get all agent state keys
keys = self.gov.db.keys("agent:*:state")
agents = []
for key in keys:
agent_id = key.split(":")[1]
state = self.gov.get_agent_state(agent_id)
if state and state.status == AgentStatus.RUNNING:
agents.append(agent_id)
return agents
def run_checks(self) -> list[dict]:
"""Run all monitors once"""
results = []
for agent_id in self.get_active_agents():
result = {
"agent_id": agent_id,
"timestamp": datetime.now(timezone.utc).isoformat(),
"actions": []
}
# Execution check
action = self.execution_watcher.enforce(agent_id)
if action:
result["actions"].append(action)
# Compliance check (only if not already revoked)
state = self.gov.get_agent_state(agent_id)
if state and state.status != AgentStatus.REVOKED:
action = self.compliance_watcher.enforce(agent_id)
if action:
result["actions"].append(action)
# Vault check
if state and state.status != AgentStatus.REVOKED:
action = self.vault_watcher.enforce(agent_id)
if action:
result["actions"].append(action)
if result["actions"]:
results.append(result)
return results
def run_daemon(self):
"""Run as a daemon (blocking)"""
self.running = True
print(f"[MONITOR] Starting governance monitor daemon (interval: {self.interval}s)")
while self.running:
results = self.run_checks()
for r in results:
print(f"[MONITOR] {r['agent_id']}: {r['actions']}")
time.sleep(self.interval)
def stop(self):
"""Stop the daemon"""
self.running = False
# =============================================================================
# CLI
# =============================================================================
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("Usage: monitors.py <command>")
print("Commands:")
print(" check <agent_id> - Run all checks on an agent")
print(" daemon - Run monitor daemon")
print(" signal <agent_id> - Send revocation signal")
sys.exit(1)
cmd = sys.argv[1]
if cmd == "check":
agent_id = sys.argv[2]
exec_watch = ExecutionWatcher()
comp_watch = ComplianceWatcher()
print(f"=== Checking Agent: {agent_id} ===\n")
print("Execution Check:")
result = exec_watch.check_agent(agent_id)
print(f" Healthy: {result['healthy']}")
print(f" Issues: {result['issues']}")
print("\nCompliance Check:")
result = comp_watch.check_agent(agent_id)
print(f" Compliant: {result['compliant']}")
print(f" Violations: {result['violations']}")
elif cmd == "daemon":
daemon = GovernanceMonitorDaemon(interval=10)
try:
daemon.run_daemon()
except KeyboardInterrupt:
daemon.stop()
print("\n[MONITOR] Stopped")
elif cmd == "signal":
agent_id = sys.argv[2]
watcher = VaultLeaseWatcher()
watcher.set_revocation_signal(agent_id)
print(f"Revocation signal set for {agent_id}")
else:
print(f"Unknown command: {cmd}")
+12
View File
@@ -0,0 +1,12 @@
[project]
name = "llm-planner"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"httpx>=0.28.1",
"openai>=2.15.0",
"pydantic>=2.12.5",
"redis>=7.1.0",
]
+396
View File
@@ -0,0 +1,396 @@
version = 1
revision = 3
requires-python = ">=3.11"
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
]
[[package]]
name = "anyio"
version = "4.12.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
]
[[package]]
name = "async-timeout"
version = "5.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" },
]
[[package]]
name = "certifi"
version = "2026.1.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "distro"
version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "httpcore"
version = "1.0.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
]
[[package]]
name = "idna"
version = "3.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
]
[[package]]
name = "jiter"
version = "0.12.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/32/f9/eaca4633486b527ebe7e681c431f529b63fe2709e7c5242fc0f43f77ce63/jiter-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8f8a7e317190b2c2d60eb2e8aa835270b008139562d70fe732e1c0020ec53c9", size = 316435, upload-time = "2025-11-09T20:47:02.087Z" },
{ url = "https://files.pythonhosted.org/packages/10/c1/40c9f7c22f5e6ff715f28113ebaba27ab85f9af2660ad6e1dd6425d14c19/jiter-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2218228a077e784c6c8f1a8e5d6b8cb1dea62ce25811c356364848554b2056cd", size = 320548, upload-time = "2025-11-09T20:47:03.409Z" },
{ url = "https://files.pythonhosted.org/packages/6b/1b/efbb68fe87e7711b00d2cfd1f26bb4bfc25a10539aefeaa7727329ffb9cb/jiter-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9354ccaa2982bf2188fd5f57f79f800ef622ec67beb8329903abf6b10da7d423", size = 351915, upload-time = "2025-11-09T20:47:05.171Z" },
{ url = "https://files.pythonhosted.org/packages/15/2d/c06e659888c128ad1e838123d0638f0efad90cc30860cb5f74dd3f2fc0b3/jiter-0.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2607185ea89b4af9a604d4c7ec40e45d3ad03ee66998b031134bc510232bb7", size = 368966, upload-time = "2025-11-09T20:47:06.508Z" },
{ url = "https://files.pythonhosted.org/packages/6b/20/058db4ae5fb07cf6a4ab2e9b9294416f606d8e467fb74c2184b2a1eeacba/jiter-0.12.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a585a5e42d25f2e71db5f10b171f5e5ea641d3aa44f7df745aa965606111cc2", size = 482047, upload-time = "2025-11-09T20:47:08.382Z" },
{ url = "https://files.pythonhosted.org/packages/49/bb/dc2b1c122275e1de2eb12905015d61e8316b2f888bdaac34221c301495d6/jiter-0.12.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd9e21d34edff5a663c631f850edcb786719c960ce887a5661e9c828a53a95d9", size = 380835, upload-time = "2025-11-09T20:47:09.81Z" },
{ url = "https://files.pythonhosted.org/packages/23/7d/38f9cd337575349de16da575ee57ddb2d5a64d425c9367f5ef9e4612e32e/jiter-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a612534770470686cd5431478dc5a1b660eceb410abade6b1b74e320ca98de6", size = 364587, upload-time = "2025-11-09T20:47:11.529Z" },
{ url = "https://files.pythonhosted.org/packages/f0/a3/b13e8e61e70f0bb06085099c4e2462647f53cc2ca97614f7fedcaa2bb9f3/jiter-0.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3985aea37d40a908f887b34d05111e0aae822943796ebf8338877fee2ab67725", size = 390492, upload-time = "2025-11-09T20:47:12.993Z" },
{ url = "https://files.pythonhosted.org/packages/07/71/e0d11422ed027e21422f7bc1883c61deba2d9752b720538430c1deadfbca/jiter-0.12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b1207af186495f48f72529f8d86671903c8c10127cac6381b11dddc4aaa52df6", size = 522046, upload-time = "2025-11-09T20:47:14.6Z" },
{ url = "https://files.pythonhosted.org/packages/9f/59/b968a9aa7102a8375dbbdfbd2aeebe563c7e5dddf0f47c9ef1588a97e224/jiter-0.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef2fb241de583934c9915a33120ecc06d94aa3381a134570f59eed784e87001e", size = 513392, upload-time = "2025-11-09T20:47:16.011Z" },
{ url = "https://files.pythonhosted.org/packages/ca/e4/7df62002499080dbd61b505c5cb351aa09e9959d176cac2aa8da6f93b13b/jiter-0.12.0-cp311-cp311-win32.whl", hash = "sha256:453b6035672fecce8007465896a25b28a6b59cfe8fbc974b2563a92f5a92a67c", size = 206096, upload-time = "2025-11-09T20:47:17.344Z" },
{ url = "https://files.pythonhosted.org/packages/bb/60/1032b30ae0572196b0de0e87dce3b6c26a1eff71aad5fe43dee3082d32e0/jiter-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:ca264b9603973c2ad9435c71a8ec8b49f8f715ab5ba421c85a51cde9887e421f", size = 204899, upload-time = "2025-11-09T20:47:19.365Z" },
{ url = "https://files.pythonhosted.org/packages/49/d5/c145e526fccdb834063fb45c071df78b0cc426bbaf6de38b0781f45d956f/jiter-0.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:cb00ef392e7d684f2754598c02c409f376ddcef857aae796d559e6cacc2d78a5", size = 188070, upload-time = "2025-11-09T20:47:20.75Z" },
{ url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449, upload-time = "2025-11-09T20:47:22.999Z" },
{ url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855, upload-time = "2025-11-09T20:47:24.779Z" },
{ url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171, upload-time = "2025-11-09T20:47:26.469Z" },
{ url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590, upload-time = "2025-11-09T20:47:27.918Z" },
{ url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462, upload-time = "2025-11-09T20:47:29.654Z" },
{ url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983, upload-time = "2025-11-09T20:47:31.026Z" },
{ url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328, upload-time = "2025-11-09T20:47:33.286Z" },
{ url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740, upload-time = "2025-11-09T20:47:34.703Z" },
{ url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875, upload-time = "2025-11-09T20:47:36.058Z" },
{ url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457, upload-time = "2025-11-09T20:47:37.932Z" },
{ url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546, upload-time = "2025-11-09T20:47:40.47Z" },
{ url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196, upload-time = "2025-11-09T20:47:41.794Z" },
{ url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100, upload-time = "2025-11-09T20:47:43.007Z" },
{ url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658, upload-time = "2025-11-09T20:47:44.424Z" },
{ url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605, upload-time = "2025-11-09T20:47:45.973Z" },
{ url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803, upload-time = "2025-11-09T20:47:47.535Z" },
{ url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120, upload-time = "2025-11-09T20:47:49.284Z" },
{ url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918, upload-time = "2025-11-09T20:47:50.807Z" },
{ url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008, upload-time = "2025-11-09T20:47:52.211Z" },
{ url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785, upload-time = "2025-11-09T20:47:53.512Z" },
{ url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108, upload-time = "2025-11-09T20:47:54.893Z" },
{ url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937, upload-time = "2025-11-09T20:47:56.253Z" },
{ url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853, upload-time = "2025-11-09T20:47:58.32Z" },
{ url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699, upload-time = "2025-11-09T20:47:59.686Z" },
{ url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258, upload-time = "2025-11-09T20:48:01.01Z" },
{ url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503, upload-time = "2025-11-09T20:48:02.35Z" },
{ url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965, upload-time = "2025-11-09T20:48:03.783Z" },
{ url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831, upload-time = "2025-11-09T20:48:05.55Z" },
{ url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272, upload-time = "2025-11-09T20:48:06.951Z" },
{ url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604, upload-time = "2025-11-09T20:48:08.328Z" },
{ url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628, upload-time = "2025-11-09T20:48:09.572Z" },
{ url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478, upload-time = "2025-11-09T20:48:10.898Z" },
{ url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706, upload-time = "2025-11-09T20:48:12.266Z" },
{ url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894, upload-time = "2025-11-09T20:48:13.673Z" },
{ url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714, upload-time = "2025-11-09T20:48:15.083Z" },
{ url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989, upload-time = "2025-11-09T20:48:16.706Z" },
{ url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615, upload-time = "2025-11-09T20:48:18.614Z" },
{ url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745, upload-time = "2025-11-09T20:48:20.117Z" },
{ url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502, upload-time = "2025-11-09T20:48:21.543Z" },
{ url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845, upload-time = "2025-11-09T20:48:22.964Z" },
{ url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701, upload-time = "2025-11-09T20:48:24.483Z" },
{ url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029, upload-time = "2025-11-09T20:48:25.749Z" },
{ url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960, upload-time = "2025-11-09T20:48:27.415Z" },
{ url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529, upload-time = "2025-11-09T20:48:29.125Z" },
{ url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974, upload-time = "2025-11-09T20:48:30.87Z" },
{ url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932, upload-time = "2025-11-09T20:48:32.658Z" },
{ url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243, upload-time = "2025-11-09T20:48:34.093Z" },
{ url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315, upload-time = "2025-11-09T20:48:35.507Z" },
{ url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714, upload-time = "2025-11-09T20:48:40.014Z" },
{ url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168, upload-time = "2025-11-09T20:48:41.462Z" },
{ url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893, upload-time = "2025-11-09T20:48:42.921Z" },
{ url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828, upload-time = "2025-11-09T20:48:44.278Z" },
{ url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009, upload-time = "2025-11-09T20:48:45.726Z" },
{ url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110, upload-time = "2025-11-09T20:48:47.033Z" },
{ url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223, upload-time = "2025-11-09T20:48:49.076Z" },
{ url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" },
{ url = "https://files.pythonhosted.org/packages/fe/54/5339ef1ecaa881c6948669956567a64d2670941925f245c434f494ffb0e5/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:4739a4657179ebf08f85914ce50332495811004cc1747852e8b2041ed2aab9b8", size = 311144, upload-time = "2025-11-09T20:49:10.503Z" },
{ url = "https://files.pythonhosted.org/packages/27/74/3446c652bffbd5e81ab354e388b1b5fc1d20daac34ee0ed11ff096b1b01a/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:41da8def934bf7bec16cb24bd33c0ca62126d2d45d81d17b864bd5ad721393c3", size = 305877, upload-time = "2025-11-09T20:49:12.269Z" },
{ url = "https://files.pythonhosted.org/packages/a1/f4/ed76ef9043450f57aac2d4fbeb27175aa0eb9c38f833be6ef6379b3b9a86/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c44ee814f499c082e69872d426b624987dbc5943ab06e9bbaa4f81989fdb79e", size = 340419, upload-time = "2025-11-09T20:49:13.803Z" },
{ url = "https://files.pythonhosted.org/packages/21/01/857d4608f5edb0664aa791a3d45702e1a5bcfff9934da74035e7b9803846/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2097de91cf03eaa27b3cbdb969addf83f0179c6afc41bbc4513705e013c65d", size = 347212, upload-time = "2025-11-09T20:49:15.643Z" },
{ url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974, upload-time = "2025-11-09T20:49:17.187Z" },
{ url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233, upload-time = "2025-11-09T20:49:18.734Z" },
{ url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537, upload-time = "2025-11-09T20:49:20.317Z" },
{ url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" },
]
[[package]]
name = "llm-planner"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "httpx" },
{ name = "openai" },
{ name = "pydantic" },
{ name = "redis" },
]
[package.metadata]
requires-dist = [
{ name = "httpx", specifier = ">=0.28.1" },
{ name = "openai", specifier = ">=2.15.0" },
{ name = "pydantic", specifier = ">=2.12.5" },
{ name = "redis", specifier = ">=7.1.0" },
]
[[package]]
name = "openai"
version = "2.15.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "distro" },
{ name = "httpx" },
{ name = "jiter" },
{ name = "pydantic" },
{ name = "sniffio" },
{ name = "tqdm" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/94/f4/4690ecb5d70023ce6bfcfeabfe717020f654bde59a775058ec6ac4692463/openai-2.15.0.tar.gz", hash = "sha256:42eb8cbb407d84770633f31bf727d4ffb4138711c670565a41663d9439174fba", size = 627383, upload-time = "2026-01-09T22:10:08.603Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b5/df/c306f7375d42bafb379934c2df4c2fa3964656c8c782bac75ee10c102818/openai-2.15.0-py3-none-any.whl", hash = "sha256:6ae23b932cd7230f7244e52954daa6602716d6b9bf235401a107af731baea6c3", size = 1067879, upload-time = "2026-01-09T22:10:06.446Z" },
]
[[package]]
name = "pydantic"
version = "2.12.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
]
[[package]]
name = "pydantic-core"
version = "2.41.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" },
{ url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" },
{ url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" },
{ url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" },
{ url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" },
{ url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" },
{ url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" },
{ url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" },
{ url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" },
{ url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" },
{ url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" },
{ url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" },
{ url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" },
{ url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" },
{ url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
{ url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
{ url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
{ url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" },
{ url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" },
{ url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" },
{ url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" },
{ url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" },
{ url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" },
{ url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" },
{ url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" },
{ url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
{ url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
{ url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
{ url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
{ url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
{ url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
{ url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
{ url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
{ url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
{ url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
{ url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
{ url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
{ url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
{ url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
{ url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
{ url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
{ url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
{ url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
{ url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
{ url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
{ url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
{ url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
{ url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
{ url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
{ url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
{ url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
{ url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
{ url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
{ url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
{ url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
{ url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
{ url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
{ url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
{ url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
{ url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
{ url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
{ url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
{ url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
{ url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
{ url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
{ url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
{ url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
{ url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
{ url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
{ url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
{ url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
{ url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
{ url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
{ url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" },
{ url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
{ url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
{ url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
{ url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
{ url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" },
{ url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" },
{ url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" },
{ url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" },
{ url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" },
{ url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" },
{ url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" },
{ url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" },
]
[[package]]
name = "redis"
version = "7.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "async-timeout", marker = "python_full_version < '3.11.3'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669, upload-time = "2025-11-19T15:54:39.961Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
]
[[package]]
name = "tqdm"
version = "4.67.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
name = "typing-inspection"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
]