skillpool 4.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. skillpool/__init__.py +74 -0
  2. skillpool/__main__.py +6 -0
  3. skillpool/adapters/__init__.py +8 -0
  4. skillpool/adapters/base.py +41 -0
  5. skillpool/adapters/claude_adapter.py +36 -0
  6. skillpool/adapters/codex_adapter.py +92 -0
  7. skillpool/adapters/hermes_adapter.py +38 -0
  8. skillpool/audit/__init__.py +651 -0
  9. skillpool/bridge/__init__.py +16 -0
  10. skillpool/bridge/freeze_detector.py +134 -0
  11. skillpool/bridge/maintenance.py +119 -0
  12. skillpool/bridge/wal_manager.py +136 -0
  13. skillpool/clawmem_client.py +176 -0
  14. skillpool/cli.py +700 -0
  15. skillpool/combiner/__init__.py +31 -0
  16. skillpool/combiner/lifecycle.py +453 -0
  17. skillpool/combiner/models.py +99 -0
  18. skillpool/config.py +34 -0
  19. skillpool/cost/__init__.py +111 -0
  20. skillpool/cost/audit_hash.py +51 -0
  21. skillpool/cost/budget_tracker.py +66 -0
  22. skillpool/cost/dashboard.py +189 -0
  23. skillpool/cost/models.py +129 -0
  24. skillpool/cost/token_governor.py +264 -0
  25. skillpool/cost/trace_ceiling.py +38 -0
  26. skillpool/csdf.py +126 -0
  27. skillpool/evolver/__init__.py +978 -0
  28. skillpool/gain/__init__.py +285 -0
  29. skillpool/gate.py +282 -0
  30. skillpool/gate_policy/__init__.py +31 -0
  31. skillpool/gate_policy/incremental.py +157 -0
  32. skillpool/gate_policy/parser.py +258 -0
  33. skillpool/gate_policy/state_machine.py +432 -0
  34. skillpool/graph/__init__.py +14 -0
  35. skillpool/graph/ppr.py +279 -0
  36. skillpool/health/__init__.py +73 -0
  37. skillpool/health/check.py +85 -0
  38. skillpool/health/degradation.py +90 -0
  39. skillpool/health/models.py +43 -0
  40. skillpool/hooks/__init__.py +4 -0
  41. skillpool/hooks/security_scanner.py +288 -0
  42. skillpool/lifecycle.py +150 -0
  43. skillpool/materializer/__init__.py +124 -0
  44. skillpool/materializer/budget_cropper.py +178 -0
  45. skillpool/materializer/csdf_loader.py +114 -0
  46. skillpool/materializer/lazy_loader.py +265 -0
  47. skillpool/materializer/lifecycle_filter.py +93 -0
  48. skillpool/materializer/mapper.py +178 -0
  49. skillpool/materializer/models.py +66 -0
  50. skillpool/mcp_server.py +2005 -0
  51. skillpool/monitor/__init__.py +576 -0
  52. skillpool/monitor/bug_collector.py +392 -0
  53. skillpool/monitor/defect_classifier.py +218 -0
  54. skillpool/monitor/self_healing.py +530 -0
  55. skillpool/monitor/telemetry_bridge.py +197 -0
  56. skillpool/paradigm/__init__.py +312 -0
  57. skillpool/paradigm/override.py +285 -0
  58. skillpool/profile.py +94 -0
  59. skillpool/quality.py +254 -0
  60. skillpool/registry/__init__.py +509 -0
  61. skillpool/registry/models.py +98 -0
  62. skillpool/resolver/__init__.py +320 -0
  63. skillpool/resolver/cache.py +103 -0
  64. skillpool/resolver/circuit_breaker.py +103 -0
  65. skillpool/resolver/conflict_detector.py +111 -0
  66. skillpool/resolver/health_filter.py +38 -0
  67. skillpool/resolver/models.py +154 -0
  68. skillpool/resolver/rate_limiter.py +48 -0
  69. skillpool/resolver/skill_graph.py +183 -0
  70. skillpool/review/__init__.py +242 -0
  71. skillpool/review/async_queue.py +96 -0
  72. skillpool/review/checkpoint_runner.py +345 -0
  73. skillpool/review/models.py +164 -0
  74. skillpool/review/suspect_marker.py +39 -0
  75. skillpool/review/veto_evaluator.py +94 -0
  76. skillpool/router/__init__.py +481 -0
  77. skillpool/schemas.py +119 -0
  78. skillpool/synergy/__init__.py +240 -0
  79. skillpool/synergy/detector.py +5 -0
  80. skillpool/telemetry.py +126 -0
  81. skillpool/utils/__init__.py +21 -0
  82. skillpool/utils/changelog.py +218 -0
  83. skillpool/utils/logger.py +273 -0
  84. skillpool/utils/runtime_audit.py +163 -0
  85. skillpool/utils/time_utils.py +13 -0
  86. skillpool-4.3.0.dist-info/METADATA +21 -0
  87. skillpool-4.3.0.dist-info/RECORD +90 -0
  88. skillpool-4.3.0.dist-info/WHEEL +5 -0
  89. skillpool-4.3.0.dist-info/entry_points.txt +3 -0
  90. skillpool-4.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,111 @@
1
+ """CostManager — unified cost tracking, budgeting, and throttling."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from skillpool.cost.models import (
6
+ BudgetStatus,
7
+ CostDashboardResponse,
8
+ CostQuery,
9
+ CostRecord,
10
+ ThrottleAction,
11
+ )
12
+ from skillpool.cost.token_governor import TokenGovernor
13
+ from skillpool.cost.budget_tracker import BudgetTracker
14
+ from skillpool.cost.trace_ceiling import TraceCeiling
15
+ from skillpool.cost.audit_hash import AuditHashChain
16
+ from skillpool.cost.dashboard import CostDashboard
17
+
18
+
19
+ class CostManager:
20
+ """Central cost management: recording, throttling, budgeting, auditing.
21
+
22
+ Combines TokenGovernor, BudgetTracker, TraceCeiling, AuditHashChain/AuditLayer,
23
+ and CostDashboard into a single facade.
24
+
25
+ When an AuditLayer is provided via audit_layer parameter, it is used for
26
+ full 34-field OTel audit records. Otherwise the lightweight AuditHashChain
27
+ is used for basic hash chain integrity.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ monthly_budget_usd: float = 5000.0,
33
+ trace_ceiling_usd: float = 5.0,
34
+ audit_layer=None,
35
+ ) -> None:
36
+ self._governor = TokenGovernor()
37
+ self._budget = BudgetTracker(monthly_budget_usd=monthly_budget_usd)
38
+ self._trace_ceiling = TraceCeiling(ceiling_usd=trace_ceiling_usd)
39
+ # Use full AuditLayer if provided, otherwise lightweight AuditHashChain
40
+ if audit_layer is not None:
41
+ self._audit = audit_layer
42
+ self._audit_full = True
43
+ else:
44
+ self._audit = AuditHashChain()
45
+ self._audit_full = False
46
+ self._dashboard = CostDashboard(governor=self._governor, budget_tracker=self._budget)
47
+
48
+ def report_cost(self, record: CostRecord) -> bool:
49
+ """Process a cost record: validate, record, check throttles.
50
+
51
+ Returns True if the cost was accepted, False if rejected by
52
+ budget or trace ceiling.
53
+ """
54
+ total_tokens = record.tokens_input + record.tokens_output
55
+
56
+ # Check trace ceiling
57
+ allowed, reason = self._trace_ceiling.check(record.trace_id, record.cost_usd)
58
+ if not allowed:
59
+ return False
60
+
61
+ # Check budget threshold
62
+ threshold, action = self._budget.check_budget_threshold()
63
+ if action == "block":
64
+ return False
65
+
66
+ # Record usage
67
+ self._governor.record_usage(record.agent_id, total_tokens)
68
+ self._budget.record_cost(record.cost_usd)
69
+ self._trace_ceiling.record_trace_cost(record.trace_id, record.cost_usd)
70
+
71
+ # Audit record: full AuditLayer or lightweight AuditHashChain
72
+ if self._audit_full:
73
+ self._audit.append(
74
+ action="cost_record",
75
+ object_id=record.agent_id,
76
+ result="success",
77
+ metadata={
78
+ "cost_usd": record.cost_usd,
79
+ "tokens_input": record.tokens_input,
80
+ "tokens_output": record.tokens_output,
81
+ "trace_id": record.trace_id,
82
+ },
83
+ trace_id=record.trace_id,
84
+ )
85
+ else:
86
+ self._audit.append(record.model_dump(mode="json"))
87
+
88
+ self._dashboard.record(
89
+ agent_id=record.agent_id,
90
+ tokens_input=record.tokens_input,
91
+ tokens_output=record.tokens_output,
92
+ cost_usd=record.cost_usd,
93
+ model=record.model,
94
+ operation=record.operation,
95
+ skill_id=record.skill_id,
96
+ )
97
+
98
+ return True
99
+
100
+ def get_dashboard(self, query: CostQuery) -> CostDashboardResponse:
101
+ """Query the cost dashboard."""
102
+ return self._dashboard.query(query)
103
+
104
+ def get_budget(self) -> BudgetStatus:
105
+ """Return current monthly budget status."""
106
+ return self._dashboard.get_budget_status()
107
+
108
+ def check_throttle(self, agent_id: str, tokens: int) -> ThrottleAction:
109
+ """Check throttle status for an agent/token request."""
110
+ action, _ = self._governor.check(agent_id, tokens)
111
+ return action
@@ -0,0 +1,51 @@
1
+ """AuditHashChain — SHA-256 hash chain for cost record integrity."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+
8
+
9
+ class AuditHashChain:
10
+ """Maintain a tamper-evident hash chain over cost records.
11
+
12
+ Each record's hash is computed from the previous hash + current record data,
13
+ creating a linked chain where any modification to a prior record invalidates
14
+ all subsequent hashes.
15
+ """
16
+
17
+ def __init__(self) -> None:
18
+ self._hashes: list[str] = []
19
+ self._records: list[dict] = []
20
+
21
+ @staticmethod
22
+ def compute_hash(previous_hash: str, record_data: dict) -> str:
23
+ """Compute SHA-256 hash from previous hash and record data."""
24
+ payload = previous_hash + json.dumps(record_data, sort_keys=True, separators=(",", ":"))
25
+ return hashlib.sha256(payload.encode()).hexdigest()
26
+
27
+ def append(self, record_data: dict) -> str:
28
+ """Append a record to the chain and return its hash."""
29
+ previous = self._hashes[-1] if self._hashes else "0" * 64
30
+ new_hash = self.compute_hash(previous, record_data)
31
+ self._hashes.append(new_hash)
32
+ self._records.append(record_data)
33
+ return new_hash
34
+
35
+ def verify_chain(self) -> bool:
36
+ """Verify the entire hash chain is consistent.
37
+
38
+ Recomputes every hash from scratch and checks against stored values.
39
+ Returns True if the chain is intact.
40
+ """
41
+ previous = "0" * 64
42
+ for i, record in enumerate(self._records):
43
+ expected = self.compute_hash(previous, record)
44
+ if self._hashes[i] != expected:
45
+ return False
46
+ previous = expected
47
+ return True
48
+
49
+ def get_chain(self) -> list[str]:
50
+ """Return a copy of the hash chain."""
51
+ return list(self._hashes)
@@ -0,0 +1,66 @@
1
+ """BudgetTracker — monthly budget enforcement with threshold alerts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from skillpool.cost.models import AgentConfig, BudgetStatus
6
+
7
+
8
+ class BudgetTracker:
9
+ """Track monthly cost against a budget with threshold actions.
10
+
11
+ Thresholds:
12
+ - >=50% remaining: normal
13
+ - >=25% remaining: caution
14
+ - >=10% remaining: warning
15
+ - 0% remaining: critical (all spending blocked)
16
+ """
17
+
18
+ def __init__(self, monthly_budget_usd: float = 5000.0) -> None:
19
+ self.monthly_budget_usd = monthly_budget_usd
20
+ self._consumed_usd: float = 0.0
21
+
22
+ def record_cost(self, cost_usd: float) -> None:
23
+ """Record a cost event against the monthly budget."""
24
+ self._consumed_usd += cost_usd
25
+
26
+ def get_status(self, agent_configs: list[AgentConfig] | None = None) -> BudgetStatus:
27
+ """Return current monthly budget status."""
28
+ remaining = self.monthly_budget_usd - self._consumed_usd
29
+ consumed_pct = self.get_consumed_pct()
30
+ # Project: if we've used X% so far, assume linear burn to month end
31
+ projected = self._consumed_usd # simplified; real impl would use day-of-month
32
+ if consumed_pct > 0:
33
+ # Approximate projection assuming mid-month
34
+ projected = self._consumed_usd * 2.0
35
+ return BudgetStatus(
36
+ monthly_budget_usd=self.monthly_budget_usd,
37
+ consumed_usd=round(self._consumed_usd, 4),
38
+ remaining_usd=round(max(0.0, remaining), 4),
39
+ consumed_pct=round(consumed_pct, 4),
40
+ projected_month_end_usd=round(projected, 4),
41
+ per_agent_limits=agent_configs or [],
42
+ )
43
+
44
+ def get_consumed_pct(self) -> float:
45
+ """Return percentage of monthly budget consumed (0.0 - 1.0+)."""
46
+ if self.monthly_budget_usd <= 0:
47
+ return 1.0
48
+ return self._consumed_usd / self.monthly_budget_usd
49
+
50
+ def check_budget_threshold(self) -> tuple[str, str]:
51
+ """Check budget threshold and return (threshold_name, action).
52
+
53
+ Returns:
54
+ - ("normal", "continue") when >50% budget remaining
55
+ - ("caution", "monitor") when 25-50% remaining
56
+ - ("warning", "throttle") when 10-25% remaining
57
+ - ("critical", "block") when <10% remaining
58
+ """
59
+ remaining_pct = 1.0 - self.get_consumed_pct()
60
+ if remaining_pct < 0.10:
61
+ return "critical", "block"
62
+ if remaining_pct < 0.25:
63
+ return "warning", "throttle"
64
+ if remaining_pct < 0.50:
65
+ return "caution", "monitor"
66
+ return "normal", "continue"
@@ -0,0 +1,189 @@
1
+ """CostDashboard — aggregated cost query and reporting."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timedelta
6
+
7
+ from skillpool.cost.models import (
8
+ AgentCost,
9
+ CostDashboardResponse,
10
+ CostQuery,
11
+ BudgetStatus,
12
+ )
13
+ from skillpool.cost.token_governor import TokenGovernor
14
+ from skillpool.cost.budget_tracker import BudgetTracker
15
+ from skillpool.utils.time_utils import utc_now
16
+
17
+
18
+ class CostDashboard:
19
+ """Aggregate cost data across agents for dashboard queries.
20
+
21
+ Uses TokenGovernor for per-agent budget/throttle state and
22
+ BudgetTracker for monthly budget status.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ governor: TokenGovernor,
28
+ budget_tracker: BudgetTracker,
29
+ ) -> None:
30
+ self._governor = governor
31
+ self._budget = budget_tracker
32
+ # In-memory cost records for aggregation
33
+ self._records: list[dict] = []
34
+
35
+ def record(
36
+ self,
37
+ agent_id: str,
38
+ tokens_input: int,
39
+ tokens_output: int,
40
+ cost_usd: float,
41
+ model: str = "",
42
+ operation: str = "",
43
+ skill_id: str = "",
44
+ ) -> None:
45
+ """Store a cost record for later querying."""
46
+ self._records.append(
47
+ {
48
+ "agent_id": agent_id,
49
+ "tokens_input": tokens_input,
50
+ "tokens_output": tokens_output,
51
+ "cost_usd": cost_usd,
52
+ "model": model,
53
+ "operation": operation,
54
+ "skill_id": skill_id,
55
+ "timestamp": utc_now().isoformat(),
56
+ }
57
+ )
58
+
59
+ def query(self, request: CostQuery) -> CostDashboardResponse:
60
+ """Query aggregated cost data.
61
+
62
+ Supports multi-dimension grouping: agent_id, model, skill_id, operation.
63
+ """
64
+ # Filter by agent_id if specified
65
+ records = self._records
66
+ if request.agent_id:
67
+ records = [r for r in records if r["agent_id"] == request.agent_id]
68
+
69
+ # Aggregate by agent
70
+ agent_data: dict[str, dict] = {}
71
+ model_data: dict[str, dict] = {}
72
+ skill_data: dict[str, dict] = {}
73
+ op_data: dict[str, dict] = {}
74
+ total_cost = 0.0
75
+ total_tokens = 0
76
+
77
+ for r in records:
78
+ aid = r["agent_id"]
79
+ if aid not in agent_data:
80
+ agent_data[aid] = {"tokens": 0, "cost_usd": 0.0}
81
+ agent_data[aid]["tokens"] += r["tokens_input"] + r["tokens_output"]
82
+ agent_data[aid]["cost_usd"] += r["cost_usd"]
83
+
84
+ # Group by model
85
+ model = r.get("model", "")
86
+ if model:
87
+ if model not in model_data:
88
+ model_data[model] = {"tokens": 0, "cost_usd": 0.0}
89
+ model_data[model]["tokens"] += r["tokens_input"] + r["tokens_output"]
90
+ model_data[model]["cost_usd"] += r["cost_usd"]
91
+
92
+ # Group by skill_id
93
+ skill = r.get("skill_id", "")
94
+ if skill:
95
+ if skill not in skill_data:
96
+ skill_data[skill] = {"tokens": 0, "cost_usd": 0.0}
97
+ skill_data[skill]["tokens"] += r["tokens_input"] + r["tokens_output"]
98
+ skill_data[skill]["cost_usd"] += r["cost_usd"]
99
+
100
+ # Group by operation
101
+ op = r.get("operation", "")
102
+ if op:
103
+ if op not in op_data:
104
+ op_data[op] = {"tokens": 0, "cost_usd": 0.0}
105
+ op_data[op]["tokens"] += r["tokens_input"] + r["tokens_output"]
106
+ op_data[op]["cost_usd"] += r["cost_usd"]
107
+
108
+ total_cost += r["cost_usd"]
109
+ total_tokens += r["tokens_input"] + r["tokens_output"]
110
+
111
+ # Build per-agent summaries
112
+ by_agent: list[AgentCost] = []
113
+ for aid, data in agent_data.items():
114
+ cfg = self._governor.get_config(aid)
115
+ pct_of_total = (data["cost_usd"] / total_cost * 100) if total_cost > 0 else 0.0
116
+ budget_limit = cfg.daily_limit_tokens if cfg else 0
117
+ daily_usage = self._governor.get_daily_usage(aid)
118
+ budget_consumed_pct = (daily_usage / budget_limit) if budget_limit > 0 else 0.0
119
+
120
+ by_agent.append(
121
+ AgentCost(
122
+ agent_id=aid,
123
+ agent_type=cfg.agent_type if cfg else "",
124
+ tokens=data["tokens"],
125
+ cost_usd=round(data["cost_usd"], 4),
126
+ pct_of_total=round(pct_of_total, 2),
127
+ budget_limit=budget_limit,
128
+ budget_consumed_pct=round(budget_consumed_pct, 4),
129
+ throttled=self._governor.is_throttled(aid),
130
+ )
131
+ )
132
+
133
+ budget_pct = self._budget.get_consumed_pct() * 100
134
+
135
+ # Projected overspend
136
+ consumed_pct = self._budget.get_consumed_pct()
137
+ projected_overspend = max(0.0, (consumed_pct * 2.0 - 1.0) * 100) if consumed_pct > 0.5 else 0.0
138
+
139
+ # Time series generation
140
+ series = self._build_time_series(records, request.granularity)
141
+
142
+ return CostDashboardResponse(
143
+ window=request.window,
144
+ total_cost_usd=round(total_cost, 4),
145
+ total_tokens=total_tokens,
146
+ monthly_budget_usd=self._budget.monthly_budget_usd,
147
+ monthly_budget_consumed_pct=round(budget_pct, 2),
148
+ by_agent=by_agent,
149
+ by_model=model_data,
150
+ by_skill=skill_data,
151
+ by_operation=op_data,
152
+ series=series,
153
+ projected_overspend_pct=round(projected_overspend, 2),
154
+ )
155
+
156
+ @staticmethod
157
+ def _build_time_series(records: list[dict], granularity: str) -> list[dict]:
158
+ """Build time series buckets from cost records.
159
+
160
+ granularity: "5m" → 5-minute buckets, "1h" → hourly, "1d" → daily.
161
+ """
162
+ if not records:
163
+ return []
164
+
165
+ delta_map = {"5m": timedelta(minutes=5), "1h": timedelta(hours=1), "1d": timedelta(days=1)}
166
+ _delta = delta_map.get(granularity, timedelta(hours=1))
167
+
168
+ buckets: dict[str, dict] = {}
169
+ for r in records:
170
+ ts = datetime.fromisoformat(r["timestamp"])
171
+ # Floor to bucket boundary
172
+ if granularity == "5m":
173
+ bucket_key = ts.replace(minute=(ts.minute // 5) * 5, second=0, microsecond=0).isoformat()
174
+ elif granularity == "1d":
175
+ bucket_key = ts.replace(hour=0, minute=0, second=0, microsecond=0).isoformat()
176
+ else: # 1h
177
+ bucket_key = ts.replace(minute=0, second=0, microsecond=0).isoformat()
178
+
179
+ if bucket_key not in buckets:
180
+ buckets[bucket_key] = {"timestamp": bucket_key, "tokens": 0, "cost_usd": 0.0}
181
+ buckets[bucket_key]["tokens"] += r["tokens_input"] + r["tokens_output"]
182
+ buckets[bucket_key]["cost_usd"] += r["cost_usd"]
183
+
184
+ return sorted(buckets.values(), key=lambda b: b["timestamp"])
185
+
186
+ def get_budget_status(self) -> BudgetStatus:
187
+ """Return current monthly budget status."""
188
+ configs = list(self._governor._configs.values())
189
+ return self._budget.get_status(agent_configs=configs)
@@ -0,0 +1,129 @@
1
+ """Cost models — Pydantic schemas for cost tracking and budgeting."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+ from enum import StrEnum
7
+ from typing import Optional
8
+
9
+ from pydantic import BaseModel, Field
10
+
11
+ from skillpool.utils.time_utils import utc_now
12
+
13
+
14
+ class ThrottleAction(StrEnum):
15
+ ALLOW = "allow"
16
+ THROTTLE = "throttle"
17
+ REJECT = "reject"
18
+
19
+
20
+ class AgentConfig(BaseModel):
21
+ """Per-agent token budget configuration."""
22
+
23
+ agent_id: str
24
+ agent_type: str = "worker"
25
+ daily_limit_tokens: int = Field(default=0, description="0 means unlimited")
26
+ throttle_at_pct: float = Field(default=0.8, ge=0.0, le=1.0)
27
+ throttle_to_pct: float = Field(default=0.25, ge=0.0, le=1.0)
28
+ is_critical: bool = False
29
+
30
+
31
+ class CostRecord(BaseModel):
32
+ """A single cost event record."""
33
+
34
+ agent_id: str
35
+ tokens_input: int = 0
36
+ tokens_output: int = 0
37
+ cost_usd: float = Field(default=0.0, ge=0.0)
38
+ model: str = ""
39
+ operation: str = ""
40
+ trace_id: str = ""
41
+ skill_id: str = "" # V4.1: per-skill cost attribution
42
+ timestamp: datetime = Field(default_factory=utc_now)
43
+
44
+
45
+ class CostQuery(BaseModel):
46
+ """Query parameters for cost dashboard.
47
+
48
+ V4.1: Supports multi-dimension grouping and granularity.
49
+ """
50
+
51
+ window: str = Field(default="24h", pattern=r"^(1h|6h|24h|7d|30d)$")
52
+ group_by: list[str] = Field(default_factory=lambda: ["agent_id"])
53
+ agent_id: Optional[str] = None
54
+ granularity: str = Field(default="1d", pattern=r"^(5m|1h|1d)$", description="Time series granularity")
55
+
56
+
57
+ class AgentCost(BaseModel):
58
+ """Cost summary for a single agent."""
59
+
60
+ agent_id: str
61
+ agent_type: str = ""
62
+ tokens: int = 0
63
+ cost_usd: float = 0.0
64
+ pct_of_total: float = 0.0
65
+ budget_limit: int = 0
66
+ budget_consumed_pct: float = 0.0
67
+ throttled: bool = False
68
+
69
+
70
+ class CostDashboardResponse(BaseModel):
71
+ """Response from cost dashboard query.
72
+
73
+ V4.1: Multi-dimension grouping (by_agent, by_model, by_skill, by_operation)
74
+ and time series data.
75
+ """
76
+
77
+ window: str
78
+ total_cost_usd: float = 0.0
79
+ total_tokens: int = 0
80
+ monthly_budget_usd: float = 0.0
81
+ monthly_budget_consumed_pct: float = 0.0
82
+ by_agent: list[AgentCost] = Field(default_factory=list)
83
+ by_model: dict[str, dict] = Field(
84
+ default_factory=dict, description="Cost grouped by model: {model: {tokens, cost_usd}}"
85
+ )
86
+ by_skill: dict[str, dict] = Field(
87
+ default_factory=dict, description="Cost grouped by skill_id: {skill_id: {tokens, cost_usd}}"
88
+ )
89
+ by_operation: dict[str, dict] = Field(
90
+ default_factory=dict, description="Cost grouped by operation: {op: {tokens, cost_usd}}"
91
+ )
92
+ series: list[dict] = Field(
93
+ default_factory=list, description="Time series data [{timestamp, agent_id, tokens, cost_usd}]"
94
+ )
95
+ projected_overspend_pct: float = Field(
96
+ default=0.0, description="Projected overspend percentage if current burn rate continues"
97
+ )
98
+
99
+
100
+ class BudgetStatus(BaseModel):
101
+ """Monthly budget status."""
102
+
103
+ monthly_budget_usd: float
104
+ consumed_usd: float = 0.0
105
+ remaining_usd: float = 0.0
106
+ consumed_pct: float = 0.0
107
+ projected_month_end_usd: float = 0.0
108
+ per_agent_limits: list[AgentConfig] = Field(default_factory=list)
109
+
110
+
111
+ class CostEstimate(BaseModel):
112
+ """Session cost estimation result.
113
+
114
+ V4.2: P50 conservative pricing model for cost estimation.
115
+ Combines skill execution cost + review overhead + checkpoint overhead.
116
+ """
117
+
118
+ skill_id: str
119
+ skill_length: int = Field(description="Character count of skill definition")
120
+ token_count: int = Field(description="Estimated token count")
121
+ base_cost_usd: float = Field(description="Skill execution cost (P50 pricing)")
122
+ l2_review_overhead_usd: float = Field(default=0.0, description="L2 checkpoint review overhead")
123
+ l3_review_overhead_usd: float = Field(default=0.0, description="L3+L2+ checkpoint review overhead")
124
+ review_checkpoint_overhead_usd: float = Field(default=0.0, description="Review checkpoint overhead")
125
+ total_cost_usd: float = Field(description="Total estimated cost")
126
+ price_per_1k_tokens: float = Field(default=0.003, description="P50 pricing: $0.003/1K tokens")
127
+ gate_passed: bool = Field(default=True, description="Gate validation result")
128
+ gate_block_reason: str | None = Field(default=None, description="Gate block reason if failed")
129
+ emergency_bypass_active: bool = Field(default=False, description="Emergency bypass status")