handcode 0.3.0rc1__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 (67) hide show
  1. agentctl/__init__.py +0 -0
  2. agentctl/adapters/__init__.py +0 -0
  3. agentctl/adapters/litellm/__init__.py +9 -0
  4. agentctl/adapters/litellm/hook.py +49 -0
  5. agentctl/adapters/litellm/recorder.py +187 -0
  6. agentctl/adapters/openhands/__init__.py +169 -0
  7. agentctl/adapters/openhands/handoff.py +155 -0
  8. agentctl/adapters/openhands/seam_b.py +259 -0
  9. agentctl/adapters/openhands/seam_c.py +209 -0
  10. agentctl/cli.py +1450 -0
  11. agentctl/control/__init__.py +0 -0
  12. agentctl/control/cost/__init__.py +4 -0
  13. agentctl/control/cost/ledger.py +210 -0
  14. agentctl/control/dash.py +697 -0
  15. agentctl/control/keys.py +440 -0
  16. agentctl/control/matrix/__init__.py +0 -0
  17. agentctl/control/matrix/data/tools.yaml +149 -0
  18. agentctl/control/policy/__init__.py +10 -0
  19. agentctl/control/policy/compile.py +258 -0
  20. agentctl/control/policy/data/policy.compiled.json +38 -0
  21. agentctl/control/policy/data/policy.yaml +46 -0
  22. agentctl/control/probe.py +399 -0
  23. agentctl/control/providers.py +293 -0
  24. agentctl/control/proxy.py +536 -0
  25. agentctl/control/proxyenv.py +309 -0
  26. agentctl/control/replay/__init__.py +14 -0
  27. agentctl/control/replay/cassette.py +281 -0
  28. agentctl/control/replay/server.py +109 -0
  29. agentctl/demo/__init__.py +214 -0
  30. agentctl/demo/child.py +84 -0
  31. agentctl/demo/mock.py +79 -0
  32. agentctl/demo/tool.py +62 -0
  33. agentctl/gha.py +488 -0
  34. agentctl/kernel/__init__.py +0 -0
  35. agentctl/kernel/classify.py +170 -0
  36. agentctl/kernel/gate.py +391 -0
  37. agentctl/kernel/hook.py +229 -0
  38. agentctl/kernel/ledger/__init__.py +0 -0
  39. agentctl/kernel/ledger/models.py +160 -0
  40. agentctl/kernel/ledger/schema.sql +62 -0
  41. agentctl/kernel/ledger/store.py +596 -0
  42. agentctl/kernel/paths.py +203 -0
  43. agentctl/kernel/policy.py +160 -0
  44. agentctl/kernel/reconcile/__init__.py +31 -0
  45. agentctl/kernel/reconcile/base.py +106 -0
  46. agentctl/kernel/reconcile/external.py +137 -0
  47. agentctl/kernel/reconcile/filesystem.py +162 -0
  48. agentctl/kernel/reconcile/git.py +162 -0
  49. agentctl/runtime/__init__.py +20 -0
  50. agentctl/runtime/citations.py +179 -0
  51. agentctl/runtime/config.py +97 -0
  52. agentctl/runtime/doctor.py +335 -0
  53. agentctl/runtime/init.py +148 -0
  54. agentctl/runtime/lease.py +143 -0
  55. agentctl/runtime/orchestrate.py +187 -0
  56. agentctl/runtime/plugins.py +130 -0
  57. agentctl/runtime/report.py +361 -0
  58. agentctl/runtime/runner.py +787 -0
  59. agentctl/runtime/runs.py +191 -0
  60. agentctl/runtime/subagent.py +274 -0
  61. agentctl/runtime/tools.py +350 -0
  62. handcode-0.3.0rc1.dist-info/METADATA +659 -0
  63. handcode-0.3.0rc1.dist-info/RECORD +67 -0
  64. handcode-0.3.0rc1.dist-info/WHEEL +5 -0
  65. handcode-0.3.0rc1.dist-info/entry_points.txt +3 -0
  66. handcode-0.3.0rc1.dist-info/licenses/LICENSE +21 -0
  67. handcode-0.3.0rc1.dist-info/top_level.txt +1 -0
File without changes
@@ -0,0 +1,4 @@
1
+ """Cost attribution. Control plane -- may fail, never on the request path."""
2
+ from .ledger import CostLedger, Totals
3
+
4
+ __all__ = ["CostLedger", "Totals"]
@@ -0,0 +1,210 @@
1
+ """Cost ledger — spend attributed to logical work. `docs/0008` §8, M5.
2
+
3
+ Control plane, so it may fail: nothing here is on the request path. It consumes
4
+ Seam A telemetry after the fact.
5
+
6
+ **Pricing coverage is a first-class column, not a nicety.** `docs/0021` §5 found
7
+ that LiteLLM reports `response_cost: 0.0` for an endpoint it cannot price —
8
+ not `None`, *zero*. So an unpriced call and a genuinely free one are
9
+ indistinguishable in the raw data, and any budget built on it silently
10
+ under-counts exactly where a free-tier pool lives.
11
+
12
+ The ledger therefore records `priced` separately from `cost_usd`, and every
13
+ total is reported alongside the share of calls it actually covers. "I spent
14
+ $0.00" and "I do not know what I spent" must never render the same.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import sqlite3
20
+ import time
21
+ from dataclasses import dataclass
22
+ from pathlib import Path
23
+
24
+ SCHEMA = """
25
+ PRAGMA journal_mode = WAL;
26
+
27
+ CREATE TABLE IF NOT EXISTS usage_record (
28
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
29
+ trace_id TEXT,
30
+ conversation_id TEXT,
31
+ turn_id TEXT,
32
+ model TEXT,
33
+ deployment TEXT,
34
+ prompt_tokens INTEGER,
35
+ completion_tokens INTEGER,
36
+ cached_tokens INTEGER,
37
+ cost_usd REAL,
38
+ -- The Q18 column. 0 means litellm could not price this call, so cost_usd
39
+ -- is meaningless rather than zero. docs/0021 §5.
40
+ priced INTEGER NOT NULL DEFAULT 0,
41
+ -- The third state (docs/0042 I-10): the proxy config says this
42
+ -- deployment is a free tier, so its cost is a known zero -- whatever list
43
+ -- price litellm attached, and even when litellm attached none.
44
+ free INTEGER NOT NULL DEFAULT 0,
45
+ latency_s REAL,
46
+ ts REAL NOT NULL,
47
+ UNIQUE(trace_id, ts)
48
+ );
49
+
50
+ CREATE INDEX IF NOT EXISTS ix_usage_conv ON usage_record(conversation_id);
51
+ CREATE INDEX IF NOT EXISTS ix_usage_ts ON usage_record(ts);
52
+ CREATE INDEX IF NOT EXISTS ix_usage_dep ON usage_record(deployment);
53
+ """
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class Totals:
58
+ calls: int = 0
59
+ priced_calls: int = 0
60
+ cost_usd: float = 0.0
61
+ prompt_tokens: int = 0
62
+ completion_tokens: int = 0
63
+ cached_tokens: int = 0
64
+ free_calls: int = 0
65
+
66
+ @property
67
+ def known_calls(self) -> int:
68
+ """Calls whose cost is known: priced, or free per the proxy config."""
69
+ return self.priced_calls + self.free_calls
70
+
71
+ @property
72
+ def coverage(self) -> float:
73
+ """Share of calls whose cost is known. 1.0 means trustworthy."""
74
+ return (self.known_calls / self.calls) if self.calls else 0.0
75
+
76
+ @property
77
+ def trustworthy(self) -> bool:
78
+ return self.calls > 0 and self.known_calls == self.calls
79
+
80
+ @property
81
+ def cache_hit_ratio(self) -> float:
82
+ return (self.cached_tokens / self.prompt_tokens) if self.prompt_tokens else 0.0
83
+
84
+ def describe_cost(self) -> str:
85
+ """Never render an unpriced total as though it were a real zero."""
86
+ if self.calls == 0:
87
+ return "no calls"
88
+ free = (f" ({self.free_calls} call(s) free tier, per proxy config)"
89
+ if self.free_calls else "")
90
+ if self.known_calls == 0:
91
+ return f"unknown (0/{self.calls} calls priced)"
92
+ if not self.trustworthy:
93
+ return (f"${self.cost_usd:.4f} + unknown "
94
+ f"({self.known_calls}/{self.calls} known){free}")
95
+ return f"${self.cost_usd:.4f}{free}"
96
+
97
+
98
+ class CostLedger:
99
+ """Ingests Seam A telemetry and answers what work cost."""
100
+
101
+ def __init__(self, path: str | Path):
102
+ self.path = Path(path)
103
+ self.path.parent.mkdir(parents=True, exist_ok=True)
104
+ self._db = sqlite3.connect(str(self.path), isolation_level=None)
105
+ self._db.row_factory = sqlite3.Row
106
+ self._db.executescript(SCHEMA)
107
+ # A ledger written before the `free` column existed. CREATE TABLE IF
108
+ # NOT EXISTS does not add columns, so add it, once.
109
+ cols = {r["name"] for r in self._db.execute("PRAGMA table_info(usage_record)")}
110
+ if "free" not in cols:
111
+ self._db.execute("ALTER TABLE usage_record ADD COLUMN "
112
+ "free INTEGER NOT NULL DEFAULT 0")
113
+
114
+ def close(self) -> None:
115
+ self._db.close()
116
+
117
+ def __enter__(self) -> "CostLedger":
118
+ return self
119
+
120
+ def __exit__(self, *_) -> None:
121
+ self.close()
122
+
123
+ # ── ingest ─────────────────────────────────────────────────────────
124
+ def ingest_record(self, rec: dict) -> bool:
125
+ """Store one Seam A telemetry record. Idempotent on (trace_id, ts)."""
126
+ trace = rec.get("trace_id") or ""
127
+ conv, _, turn = trace.partition(":")
128
+ cost = rec.get("cost")
129
+ # Free per the proxy config: a known zero, whatever litellm said. Its
130
+ # list price is NOT spend -- counting it put phantom dollars against
131
+ # `daily_usd` on a pool that bills nothing (docs/0042 I-10).
132
+ free = 1 if rec.get("free") is True else 0
133
+ # THE Q18 DISTINCTION: a falsy cost means litellm could not price it.
134
+ # Storing 0.0 as though it were a measured zero is the whole hazard.
135
+ priced = 1 if (cost and not free) else 0
136
+ try:
137
+ self._db.execute(
138
+ "INSERT OR IGNORE INTO usage_record(trace_id, conversation_id, "
139
+ "turn_id, model, deployment, prompt_tokens, completion_tokens, "
140
+ "cached_tokens, cost_usd, priced, free, latency_s, ts) "
141
+ "VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)",
142
+ (trace or None, conv or None, turn or None,
143
+ rec.get("model"), rec.get("deployment"),
144
+ rec.get("prompt_tokens") or 0, rec.get("completion_tokens") or 0,
145
+ rec.get("cached_tokens") or 0,
146
+ float(cost) if priced else 0.0, priced, free,
147
+ rec.get("latency_s"), rec.get("ts") or time.time()))
148
+ return True
149
+ except sqlite3.Error:
150
+ return False
151
+
152
+ def ingest_telemetry(self, path: str | Path) -> int:
153
+ """Load a Seam A telemetry file. Returns the number of records stored."""
154
+ try:
155
+ data = json.loads(Path(path).read_text(encoding="utf-8"))
156
+ except Exception: # noqa: BLE001
157
+ return 0
158
+ return sum(1 for r in data.get("records", []) if self.ingest_record(r))
159
+
160
+ # ── queries ────────────────────────────────────────────────────────
161
+ def totals(self, conversation_id: str | None = None,
162
+ since: float | None = None) -> Totals:
163
+ where, args = [], []
164
+ if conversation_id:
165
+ where.append("conversation_id=?")
166
+ args.append(conversation_id)
167
+ if since is not None:
168
+ where.append("ts>=?")
169
+ args.append(since)
170
+ clause = f"WHERE {' AND '.join(where)}" if where else ""
171
+ row = self._db.execute(
172
+ "SELECT COUNT(*) calls, COALESCE(SUM(priced),0) priced_calls, "
173
+ "COALESCE(SUM(free),0) free_calls, "
174
+ "COALESCE(SUM(cost_usd),0) cost, "
175
+ "COALESCE(SUM(prompt_tokens),0) pt, "
176
+ "COALESCE(SUM(completion_tokens),0) ct, "
177
+ "COALESCE(SUM(cached_tokens),0) cached "
178
+ f"FROM usage_record {clause}", args).fetchone()
179
+ return Totals(row["calls"], row["priced_calls"], row["cost"],
180
+ row["pt"], row["ct"], row["cached"], row["free_calls"])
181
+
182
+ def by_conversation(self, limit: int = 20) -> list[dict]:
183
+ rows = self._db.execute(
184
+ "SELECT conversation_id, COUNT(*) calls, SUM(priced) priced_calls, "
185
+ "SUM(free) free_calls, "
186
+ "SUM(cost_usd) cost, SUM(prompt_tokens) pt, MAX(ts) last_ts "
187
+ "FROM usage_record WHERE conversation_id IS NOT NULL "
188
+ "GROUP BY conversation_id ORDER BY last_ts DESC LIMIT ?",
189
+ (limit,)).fetchall()
190
+ return [dict(r) for r in rows]
191
+
192
+ def by_deployment(self) -> list[dict]:
193
+ rows = self._db.execute(
194
+ "SELECT deployment, COUNT(*) calls, SUM(priced) priced_calls, "
195
+ "SUM(free) free_calls, "
196
+ "SUM(cost_usd) cost, SUM(cached_tokens) cached, SUM(prompt_tokens) pt "
197
+ "FROM usage_record WHERE deployment IS NOT NULL "
198
+ "GROUP BY deployment ORDER BY calls DESC").fetchall()
199
+ return [dict(r) for r in rows]
200
+
201
+ def unpriced_deployments(self) -> list[str]:
202
+ """Endpoints whose spend is invisible. The budget guard's blind spot."""
203
+ rows = self._db.execute(
204
+ "SELECT deployment FROM usage_record WHERE deployment IS NOT NULL "
205
+ "GROUP BY deployment HAVING SUM(priced)=0 AND SUM(free)=0").fetchall()
206
+ return [r["deployment"] for r in rows]
207
+
208
+ def cost_per_task(self, conversation_id: str) -> Totals:
209
+ """The metric that matters: what one completed task cost."""
210
+ return self.totals(conversation_id=conversation_id)