kevlar-agent 0.1.0__tar.gz
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.
- kevlar_agent-0.1.0/LICENSE +21 -0
- kevlar_agent-0.1.0/PACKAGE_README.md +101 -0
- kevlar_agent-0.1.0/PKG-INFO +125 -0
- kevlar_agent-0.1.0/README.md +46 -0
- kevlar_agent-0.1.0/kevlar_agent/__init__.py +37 -0
- kevlar_agent-0.1.0/kevlar_agent/audit.py +94 -0
- kevlar_agent-0.1.0/kevlar_agent/budget.py +115 -0
- kevlar_agent-0.1.0/kevlar_agent/confirm.py +66 -0
- kevlar_agent-0.1.0/kevlar_agent/dedupe.py +69 -0
- kevlar_agent-0.1.0/kevlar_agent/silent.py +67 -0
- kevlar_agent-0.1.0/kevlar_agent/watchdog.py +102 -0
- kevlar_agent-0.1.0/kevlar_agent.egg-info/PKG-INFO +125 -0
- kevlar_agent-0.1.0/kevlar_agent.egg-info/SOURCES.txt +22 -0
- kevlar_agent-0.1.0/kevlar_agent.egg-info/dependency_links.txt +1 -0
- kevlar_agent-0.1.0/kevlar_agent.egg-info/requires.txt +7 -0
- kevlar_agent-0.1.0/kevlar_agent.egg-info/top_level.txt +1 -0
- kevlar_agent-0.1.0/pyproject.toml +32 -0
- kevlar_agent-0.1.0/setup.cfg +4 -0
- kevlar_agent-0.1.0/tests/test_audit.py +40 -0
- kevlar_agent-0.1.0/tests/test_budget.py +64 -0
- kevlar_agent-0.1.0/tests/test_confirm.py +52 -0
- kevlar_agent-0.1.0/tests/test_dedupe.py +52 -0
- kevlar_agent-0.1.0/tests/test_silent.py +30 -0
- kevlar_agent-0.1.0/tests/test_watchdog.py +71 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 KiitaInternet
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# kevlar-agent
|
|
2
|
+
|
|
3
|
+
Reliability primitives for AI agents that have to survive real use — extracted
|
|
4
|
+
from a voice AI assistant that has run continuously, unattended, in production.
|
|
5
|
+
Zero dependencies (an optional one for the watchdog). MIT licensed.
|
|
6
|
+
|
|
7
|
+
**[Why this exists / the full case study →](https://kiitainternet.github.io/kevlar/)**
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install kevlar-agent
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## The six primitives
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from kevlar_agent import dedupe, budget_guard, require_confirmation, silent, AuditLog, Watchdog
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### `dedupe` — suppress duplicate calls
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
@dedupe(window_seconds=5)
|
|
23
|
+
def launch_session(device_id: str) -> str:
|
|
24
|
+
return f"launched {device_id}"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
An identical call within the window is skipped instead of running the side
|
|
28
|
+
effect twice — fixes the "model emitted the same tool call twice" class of bug.
|
|
29
|
+
|
|
30
|
+
### `budget_guard` — independent per-feature spend ceilings
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
@budget_guard("image_gen", monthly_limit_usd=3.0, cost_usd=0.04)
|
|
34
|
+
def generate_clip(prompt: str) -> str:
|
|
35
|
+
...
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Raises `BudgetExceeded` before the call runs if this *specific feature* would
|
|
39
|
+
go over its own monthly ceiling — even if ten other features share the same
|
|
40
|
+
underlying API key.
|
|
41
|
+
|
|
42
|
+
### `require_confirmation` — code-enforced consent for irreversible actions
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
@require_confirmation(lambda amount: f"transfer ${amount}")
|
|
46
|
+
def transfer(amount: float) -> str:
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
transfer(50) # raises ConfirmationRequired
|
|
50
|
+
transfer(50, confirmed=True) # runs
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
A prompt-only guardrail is a suggestion the model can miss. This one is a gate.
|
|
54
|
+
|
|
55
|
+
### `silent` — call anything with a hard timeout, from any thread
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
try:
|
|
59
|
+
token = silent(refresh_oauth_token, timeout_seconds=15)
|
|
60
|
+
except SilentTimeout:
|
|
61
|
+
token = None # fail fast instead of hanging forever
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Fixes the "OAuth helper opened a browser and waited for a click that never
|
|
65
|
+
came, because nobody was watching" class of bug.
|
|
66
|
+
|
|
67
|
+
### `AuditLog` — structured, append-only call log
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
log = AuditLog("logs/audit")
|
|
71
|
+
log.record("send_email", {"to": "x@example.com"}, result="sent", source="voice")
|
|
72
|
+
log.tail(limit=20)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
One JSON object per line, one file per day. Never raises.
|
|
76
|
+
|
|
77
|
+
### `Watchdog` — restart what dies
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from kevlar_agent.watchdog import Watched
|
|
81
|
+
|
|
82
|
+
wd = Watchdog([
|
|
83
|
+
Watched("telegram_bot.py", start=["python", "telegram_bot.py"]),
|
|
84
|
+
Watched("mobile_relay.py", start=["python", "mobile_relay.py"]),
|
|
85
|
+
])
|
|
86
|
+
wd.run_forever(interval_seconds=300)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Liveness checking uses `psutil` by default (`pip install kevlar-agent[watchdog]`),
|
|
90
|
+
or pass your own `is_alive` callable.
|
|
91
|
+
|
|
92
|
+
## Development
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pip install -e ".[dev]"
|
|
96
|
+
pytest
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kevlar-agent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reliability primitives for AI agents that have to survive real use: dedupe, budget guards, silent-timeout, audit trails, confirmation gates, and a self-supervising watchdog.
|
|
5
|
+
Author: KiitaInternet
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://kiitainternet.github.io/kevlar/
|
|
8
|
+
Project-URL: Repository, https://github.com/KiitaInternet/kevlar
|
|
9
|
+
Keywords: ai,agent,reliability,llm,tool-calling,watchdog,production
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Provides-Extra: watchdog
|
|
19
|
+
Requires-Dist: psutil>=5.9; extra == "watchdog"
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
22
|
+
Requires-Dist: psutil>=5.9; extra == "dev"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# kevlar-agent
|
|
26
|
+
|
|
27
|
+
Reliability primitives for AI agents that have to survive real use — extracted
|
|
28
|
+
from a voice AI assistant that has run continuously, unattended, in production.
|
|
29
|
+
Zero dependencies (an optional one for the watchdog). MIT licensed.
|
|
30
|
+
|
|
31
|
+
**[Why this exists / the full case study →](https://kiitainternet.github.io/kevlar/)**
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install kevlar-agent
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## The six primitives
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from kevlar_agent import dedupe, budget_guard, require_confirmation, silent, AuditLog, Watchdog
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### `dedupe` — suppress duplicate calls
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
@dedupe(window_seconds=5)
|
|
47
|
+
def launch_session(device_id: str) -> str:
|
|
48
|
+
return f"launched {device_id}"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
An identical call within the window is skipped instead of running the side
|
|
52
|
+
effect twice — fixes the "model emitted the same tool call twice" class of bug.
|
|
53
|
+
|
|
54
|
+
### `budget_guard` — independent per-feature spend ceilings
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
@budget_guard("image_gen", monthly_limit_usd=3.0, cost_usd=0.04)
|
|
58
|
+
def generate_clip(prompt: str) -> str:
|
|
59
|
+
...
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Raises `BudgetExceeded` before the call runs if this *specific feature* would
|
|
63
|
+
go over its own monthly ceiling — even if ten other features share the same
|
|
64
|
+
underlying API key.
|
|
65
|
+
|
|
66
|
+
### `require_confirmation` — code-enforced consent for irreversible actions
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
@require_confirmation(lambda amount: f"transfer ${amount}")
|
|
70
|
+
def transfer(amount: float) -> str:
|
|
71
|
+
...
|
|
72
|
+
|
|
73
|
+
transfer(50) # raises ConfirmationRequired
|
|
74
|
+
transfer(50, confirmed=True) # runs
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
A prompt-only guardrail is a suggestion the model can miss. This one is a gate.
|
|
78
|
+
|
|
79
|
+
### `silent` — call anything with a hard timeout, from any thread
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
try:
|
|
83
|
+
token = silent(refresh_oauth_token, timeout_seconds=15)
|
|
84
|
+
except SilentTimeout:
|
|
85
|
+
token = None # fail fast instead of hanging forever
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Fixes the "OAuth helper opened a browser and waited for a click that never
|
|
89
|
+
came, because nobody was watching" class of bug.
|
|
90
|
+
|
|
91
|
+
### `AuditLog` — structured, append-only call log
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
log = AuditLog("logs/audit")
|
|
95
|
+
log.record("send_email", {"to": "x@example.com"}, result="sent", source="voice")
|
|
96
|
+
log.tail(limit=20)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
One JSON object per line, one file per day. Never raises.
|
|
100
|
+
|
|
101
|
+
### `Watchdog` — restart what dies
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from kevlar_agent.watchdog import Watched
|
|
105
|
+
|
|
106
|
+
wd = Watchdog([
|
|
107
|
+
Watched("telegram_bot.py", start=["python", "telegram_bot.py"]),
|
|
108
|
+
Watched("mobile_relay.py", start=["python", "mobile_relay.py"]),
|
|
109
|
+
])
|
|
110
|
+
wd.run_forever(interval_seconds=300)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Liveness checking uses `psutil` by default (`pip install kevlar-agent[watchdog]`),
|
|
114
|
+
or pass your own `is_alive` callable.
|
|
115
|
+
|
|
116
|
+
## Development
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
pip install -e ".[dev]"
|
|
120
|
+
pytest
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
MIT
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# KEVLAR
|
|
2
|
+
|
|
3
|
+
**A case study in what it actually takes to keep a voice-driven AI agent running, unattended, for months.**
|
|
4
|
+
|
|
5
|
+
Most agent frameworks optimize for the demo: wire up a tool, watch it call an API, ship it. KEVLAR is the opposite kind of artifact — a single-page write-up of the reliability patterns that only show up after an agent has been left running against real usage, real API quotas, and real unattended background jobs long enough for the unglamorous failure modes to surface.
|
|
6
|
+
|
|
7
|
+
**[Live case study →](https://kiitainternet.github.io/kevlar/)** · **[`pip install kevlar-agent`](./PACKAGE_README.md)** — the six patterns below, as a real, tested, installable library.
|
|
8
|
+
|
|
9
|
+
## Why this exists
|
|
10
|
+
|
|
11
|
+
Every pattern documented on the page traces back to a real failure, not a hypothetical one:
|
|
12
|
+
|
|
13
|
+
- An OAuth helper that opened a browser and waited for a human click — called from a background thread with no human anywhere near it. It hung for hours.
|
|
14
|
+
- Two rapid duplicate tool calls in the same turn that spun up two concurrent live sessions on the same device.
|
|
15
|
+
- One shared API key silently starving a second feature of quota mid-conversation because nothing isolated their spend.
|
|
16
|
+
|
|
17
|
+
None of that shows up in a framework's quickstart. It only shows up in production, after enough real hours logged. This page is what that experience distilled into reusable patterns looks like.
|
|
18
|
+
|
|
19
|
+
## What's here
|
|
20
|
+
|
|
21
|
+
A single self-contained `index.html` — no build step, no dependencies, no framework. Open it in a browser or drop it on any static host.
|
|
22
|
+
|
|
23
|
+
- An original, hand-authored animated HUD visual (Canvas 2D — no external asset, no template)
|
|
24
|
+
- Six documented reliability patterns, each with the failure mode that motivated it
|
|
25
|
+
- A system-shape diagram showing how one audited dispatch core serves multiple front-end channels
|
|
26
|
+
- A simulated self-check terminal sequence
|
|
27
|
+
|
|
28
|
+
## Stack
|
|
29
|
+
|
|
30
|
+
**The case study page**: plain HTML/CSS/JS, Google Fonts (Orbitron, Rajdhani, IBM Plex Sans, JetBrains Mono). No frameworks, no bundler, no build step — intentionally, so anyone can read the entire implementation top to bottom in one file.
|
|
31
|
+
|
|
32
|
+
**The library** (`kevlar_agent/`): zero-dependency Python (one optional dependency, `psutil`, for the watchdog's default liveness check). 24 tests, all passing — see [PACKAGE_README.md](./PACKAGE_README.md) for usage.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install kevlar-agent
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# to run the test suite yourself
|
|
40
|
+
pip install -e ".[dev]"
|
|
41
|
+
pytest
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## License
|
|
45
|
+
|
|
46
|
+
MIT — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
KEVLAR — reliability primitives for AI agents that have to survive real use.
|
|
3
|
+
|
|
4
|
+
Six small, dependency-free utilities, each extracted from a real production
|
|
5
|
+
failure in an always-on voice AI assistant:
|
|
6
|
+
|
|
7
|
+
dedupe suppress duplicate calls within a time window
|
|
8
|
+
budget_guard per-feature monthly spend ceiling
|
|
9
|
+
require_confirmation enforce an explicit confirm flag for risky actions
|
|
10
|
+
silent(...) run a blocking/interactive call with a hard timeout
|
|
11
|
+
AuditLog structured, append-only call log (JSONL)
|
|
12
|
+
Watchdog supervise external processes, restart what dies
|
|
13
|
+
|
|
14
|
+
None of this is clever. All of it is necessary once something runs
|
|
15
|
+
unattended long enough for the boring failure modes to show up.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from .dedupe import dedupe
|
|
19
|
+
from .budget import BudgetExceeded, budget_guard
|
|
20
|
+
from .confirm import ConfirmationRequired, require_confirmation
|
|
21
|
+
from .silent import SilentTimeout, silent
|
|
22
|
+
from .audit import AuditLog
|
|
23
|
+
from .watchdog import Watchdog
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"dedupe",
|
|
27
|
+
"budget_guard",
|
|
28
|
+
"BudgetExceeded",
|
|
29
|
+
"require_confirmation",
|
|
30
|
+
"ConfirmationRequired",
|
|
31
|
+
"silent",
|
|
32
|
+
"SilentTimeout",
|
|
33
|
+
"AuditLog",
|
|
34
|
+
"Watchdog",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Full-fidelity audit trail for tool/agent calls.
|
|
3
|
+
|
|
4
|
+
Real failure this fixes: without a record of what was actually called,
|
|
5
|
+
with what arguments, from where, and whether it errored, "it worked when
|
|
6
|
+
I tested it" is the only signal available — and that signal lies. The
|
|
7
|
+
audit log is what revealed, after the fact, that a background scheduler
|
|
8
|
+
was silently calling the same five "safe" tools every day while a dozen
|
|
9
|
+
others had never been exercised outside manual testing.
|
|
10
|
+
|
|
11
|
+
Deliberately boring: append-only JSONL, one file per day, no external
|
|
12
|
+
dependencies. Never raises — a logging failure must not take down the
|
|
13
|
+
call it's trying to record.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import time
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AuditLog:
|
|
25
|
+
"""Append-only, structured call log — one JSON object per line, per day.
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
>>> log = AuditLog("logs/audit")
|
|
29
|
+
>>> log.record("send_email", {"to": "x@example.com"}, result="sent", source="voice")
|
|
30
|
+
>>> recent = log.tail(limit=10)
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, directory: str | Path):
|
|
34
|
+
self.directory = Path(directory)
|
|
35
|
+
|
|
36
|
+
def _file_for_today(self) -> Path:
|
|
37
|
+
return self.directory / f"{time.strftime('%Y-%m-%d')}.jsonl"
|
|
38
|
+
|
|
39
|
+
def record(
|
|
40
|
+
self,
|
|
41
|
+
tool: str,
|
|
42
|
+
args: Any = None,
|
|
43
|
+
*,
|
|
44
|
+
result: Any = None,
|
|
45
|
+
error: str | None = None,
|
|
46
|
+
duration_ms: float | None = None,
|
|
47
|
+
source: str = "default",
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Append one call record. Never raises, even if the write itself fails."""
|
|
50
|
+
try:
|
|
51
|
+
self.directory.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
row = {
|
|
53
|
+
"ts": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
54
|
+
"source": source,
|
|
55
|
+
"tool": tool,
|
|
56
|
+
"args": _safe_str(args),
|
|
57
|
+
"result": _safe_str(result),
|
|
58
|
+
"error": error,
|
|
59
|
+
"duration_ms": round(duration_ms, 1) if duration_ms is not None else None,
|
|
60
|
+
}
|
|
61
|
+
with open(self._file_for_today(), "a", encoding="utf-8") as f:
|
|
62
|
+
f.write(json.dumps(row, ensure_ascii=False) + "\n")
|
|
63
|
+
except Exception:
|
|
64
|
+
pass # a logging failure must never break the caller
|
|
65
|
+
|
|
66
|
+
def tail(self, limit: int = 50, tool: str | None = None) -> list[dict]:
|
|
67
|
+
"""Return the most recent records, newest first, optionally filtered by tool name."""
|
|
68
|
+
if not self.directory.exists():
|
|
69
|
+
return []
|
|
70
|
+
out: list[dict] = []
|
|
71
|
+
for path in sorted(self.directory.glob("*.jsonl"), reverse=True):
|
|
72
|
+
for line in reversed(path.read_text(encoding="utf-8").splitlines()):
|
|
73
|
+
try:
|
|
74
|
+
row = json.loads(line)
|
|
75
|
+
except Exception:
|
|
76
|
+
continue
|
|
77
|
+
if tool and row.get("tool") != tool:
|
|
78
|
+
continue
|
|
79
|
+
out.append(row)
|
|
80
|
+
if len(out) >= limit:
|
|
81
|
+
return out
|
|
82
|
+
return out
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _safe_str(value: Any, max_chars: int = 2000) -> str | None:
|
|
86
|
+
if value is None:
|
|
87
|
+
return None
|
|
88
|
+
try:
|
|
89
|
+
text = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False, default=str)
|
|
90
|
+
except Exception:
|
|
91
|
+
return "<unserializable>"
|
|
92
|
+
if len(text) > max_chars:
|
|
93
|
+
return text[:max_chars] + f"...(truncated {len(text) - max_chars} chars)"
|
|
94
|
+
return text
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Per-feature budget guards.
|
|
3
|
+
|
|
4
|
+
Real failure this fixes: two features shared one provider API key with
|
|
5
|
+
no spend isolation. A usage spike in one feature silently starved the
|
|
6
|
+
other of quota mid-conversation — nobody could tell which feature was
|
|
7
|
+
actually responsible until the audit log was cross-referenced by hand.
|
|
8
|
+
|
|
9
|
+
The fix: every paid call declares which "feature" it belongs to and how
|
|
10
|
+
much it costs, and gets checked against that feature's own monthly
|
|
11
|
+
ceiling — independent of every other feature, even if they share the
|
|
12
|
+
same underlying API key.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import functools
|
|
18
|
+
import json
|
|
19
|
+
import time
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, Callable, TypeVar
|
|
22
|
+
|
|
23
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BudgetExceeded(Exception):
|
|
27
|
+
"""Raised when a feature's monthly spend ceiling would be exceeded."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, feature: str, spent: float, limit: float):
|
|
30
|
+
self.feature = feature
|
|
31
|
+
self.spent = spent
|
|
32
|
+
self.limit = limit
|
|
33
|
+
super().__init__(
|
|
34
|
+
f"'{feature}' has spent ${spent:.2f} of its ${limit:.2f}/month budget"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _Ledger:
|
|
39
|
+
"""Tracks spend per feature per calendar month, persisted to a small JSON file."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, storage_path: str | Path):
|
|
42
|
+
self.path = Path(storage_path)
|
|
43
|
+
|
|
44
|
+
def _month_key(self) -> str:
|
|
45
|
+
return time.strftime("%Y-%m")
|
|
46
|
+
|
|
47
|
+
def _load(self) -> dict:
|
|
48
|
+
if not self.path.exists():
|
|
49
|
+
return {}
|
|
50
|
+
try:
|
|
51
|
+
return json.loads(self.path.read_text(encoding="utf-8"))
|
|
52
|
+
except Exception:
|
|
53
|
+
return {}
|
|
54
|
+
|
|
55
|
+
def _save(self, data: dict) -> None:
|
|
56
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
self.path.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
58
|
+
|
|
59
|
+
def spent(self, feature: str) -> float:
|
|
60
|
+
return self._load().get(feature, {}).get(self._month_key(), 0.0)
|
|
61
|
+
|
|
62
|
+
def add(self, feature: str, amount: float) -> float:
|
|
63
|
+
data = self._load()
|
|
64
|
+
month = self._month_key()
|
|
65
|
+
data.setdefault(feature, {})
|
|
66
|
+
data[feature][month] = data[feature].get(month, 0.0) + amount
|
|
67
|
+
self._save(data)
|
|
68
|
+
return data[feature][month]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def budget_guard(
|
|
72
|
+
feature: str,
|
|
73
|
+
*,
|
|
74
|
+
monthly_limit_usd: float,
|
|
75
|
+
cost_usd: float | Callable[..., float],
|
|
76
|
+
storage_path: str | Path = ".kevlar_budget.json",
|
|
77
|
+
) -> Callable[[F], F]:
|
|
78
|
+
"""Enforce an independent monthly spend ceiling for one named feature.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
feature: a stable name for the thing being metered (e.g. "image_gen").
|
|
82
|
+
Two decorators with different ``feature`` names never share a budget,
|
|
83
|
+
even if they hit the same paid API underneath.
|
|
84
|
+
monthly_limit_usd: the ceiling for this feature, this calendar month.
|
|
85
|
+
cost_usd: either a fixed cost per call, or a callable that receives the
|
|
86
|
+
same ``(*args, **kwargs)`` as the wrapped function and returns the
|
|
87
|
+
cost for that specific call (use this when cost varies by input,
|
|
88
|
+
e.g. token count or output length).
|
|
89
|
+
storage_path: where running spend is persisted between calls/restarts.
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
BudgetExceeded: if this call would push the feature over its ceiling.
|
|
93
|
+
The wrapped function is never invoked in that case.
|
|
94
|
+
|
|
95
|
+
Example:
|
|
96
|
+
>>> @budget_guard("image_gen", monthly_limit_usd=3.0, cost_usd=0.04)
|
|
97
|
+
... def generate_clip(prompt: str) -> str:
|
|
98
|
+
... return f"generated: {prompt}"
|
|
99
|
+
"""
|
|
100
|
+
ledger = _Ledger(storage_path)
|
|
101
|
+
|
|
102
|
+
def decorator(func: F) -> F:
|
|
103
|
+
@functools.wraps(func)
|
|
104
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
105
|
+
call_cost = cost_usd(*args, **kwargs) if callable(cost_usd) else cost_usd
|
|
106
|
+
spent = ledger.spent(feature)
|
|
107
|
+
if spent + call_cost > monthly_limit_usd:
|
|
108
|
+
raise BudgetExceeded(feature, spent, monthly_limit_usd)
|
|
109
|
+
result = func(*args, **kwargs)
|
|
110
|
+
ledger.add(feature, call_cost)
|
|
111
|
+
return result
|
|
112
|
+
|
|
113
|
+
return wrapper # type: ignore[return-value]
|
|
114
|
+
|
|
115
|
+
return decorator
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Confirmation enforced as code, not as a prompt.
|
|
3
|
+
|
|
4
|
+
Real failure this fixes: a system prompt politely asked the model to
|
|
5
|
+
"confirm before doing anything irreversible." The model occasionally
|
|
6
|
+
skipped that step anyway — not maliciously, just a misread of intent.
|
|
7
|
+
A polite instruction is a suggestion; it is not a gate.
|
|
8
|
+
|
|
9
|
+
The fix: wrap the risky function itself. It refuses to run unless the
|
|
10
|
+
caller passes ``confirmed=True`` explicitly. There is no code path that
|
|
11
|
+
reaches the real side effect without that flag being true — the model
|
|
12
|
+
(or any caller) cannot talk its way past it.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import functools
|
|
18
|
+
from typing import Any, Callable, TypeVar
|
|
19
|
+
|
|
20
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ConfirmationRequired(Exception):
|
|
24
|
+
"""Raised when a guarded action is called without confirmed=True."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, summary: str):
|
|
27
|
+
self.summary = summary
|
|
28
|
+
super().__init__(f"confirmation required: {summary}")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def require_confirmation(summary: str | Callable[..., str]) -> Callable[[F], F]:
|
|
32
|
+
"""Refuse to run the wrapped function unless called with ``confirmed=True``.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
summary: a human-readable description of what's about to happen, or a
|
|
36
|
+
callable that builds one from the call's ``(*args, **kwargs)``.
|
|
37
|
+
Surfaced on :class:`ConfirmationRequired` so the caller (e.g. an
|
|
38
|
+
agent loop) can relay it back to a human and ask for real consent.
|
|
39
|
+
|
|
40
|
+
The ``confirmed`` kwarg is consumed by this decorator and never forwarded
|
|
41
|
+
to the wrapped function.
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
>>> @require_confirmation(lambda amount: f"transfer ${amount}")
|
|
45
|
+
... def transfer(amount: float, confirmed: bool = False) -> str:
|
|
46
|
+
... return f"transferred ${amount}"
|
|
47
|
+
>>> transfer(50)
|
|
48
|
+
Traceback (most recent call last):
|
|
49
|
+
...
|
|
50
|
+
kevlar_agent.confirm.ConfirmationRequired: confirmation required: transfer $50
|
|
51
|
+
>>> transfer(50, confirmed=True)
|
|
52
|
+
'transferred $50'
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def decorator(func: F) -> F:
|
|
56
|
+
@functools.wraps(func)
|
|
57
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
58
|
+
confirmed = kwargs.pop("confirmed", False)
|
|
59
|
+
if not confirmed:
|
|
60
|
+
text = summary(*args, **kwargs) if callable(summary) else summary
|
|
61
|
+
raise ConfirmationRequired(text)
|
|
62
|
+
return func(*args, **kwargs)
|
|
63
|
+
|
|
64
|
+
return wrapper # type: ignore[return-value]
|
|
65
|
+
|
|
66
|
+
return decorator
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Duplicate-call suppression.
|
|
3
|
+
|
|
4
|
+
Real failure this fixes: an LLM emitted the same tool call twice in one
|
|
5
|
+
turn (an eager first pass, then a retry). Both calls launched a live
|
|
6
|
+
session on the same device — two overlapping audio streams, one user.
|
|
7
|
+
|
|
8
|
+
The fix is deliberately dumb: hash the function name + its arguments,
|
|
9
|
+
remember the last time that exact call happened, and skip the real work
|
|
10
|
+
if it happened again inside the window. The *caller* still gets a
|
|
11
|
+
return value (so the agent loop doesn't see an error) — it's just a
|
|
12
|
+
cached "already done" response instead of doing the side effect twice.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import functools
|
|
18
|
+
import json
|
|
19
|
+
import time
|
|
20
|
+
from typing import Any, Callable, TypeVar
|
|
21
|
+
|
|
22
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
23
|
+
|
|
24
|
+
_last_seen: dict[str, float] = {}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _call_key(func: Callable, args: tuple, kwargs: dict) -> str:
|
|
28
|
+
try:
|
|
29
|
+
payload = json.dumps({"args": args, "kwargs": kwargs}, sort_keys=True, default=str)
|
|
30
|
+
except TypeError:
|
|
31
|
+
payload = repr((args, kwargs))
|
|
32
|
+
return f"{func.__module__}.{func.__qualname__}:{payload}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def dedupe(window_seconds: float = 5.0, *, on_duplicate: Any = None) -> Callable[[F], F]:
|
|
36
|
+
"""Suppress a second call with identical args within ``window_seconds``.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
window_seconds: how long an identical call is considered a duplicate.
|
|
40
|
+
on_duplicate: value to return when a duplicate is caught. If it's
|
|
41
|
+
callable, it's invoked with the same ``(*args, **kwargs)`` the
|
|
42
|
+
original call received; otherwise it's returned as-is. Defaults
|
|
43
|
+
to ``None``.
|
|
44
|
+
|
|
45
|
+
Example:
|
|
46
|
+
>>> @dedupe(window_seconds=5)
|
|
47
|
+
... def launch_session(device_id: str) -> str:
|
|
48
|
+
... return f"launched {device_id}"
|
|
49
|
+
>>> launch_session("phone-1")
|
|
50
|
+
'launched phone-1'
|
|
51
|
+
>>> launch_session("phone-1") # within 5s: skipped, no second launch
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def decorator(func: F) -> F:
|
|
55
|
+
@functools.wraps(func)
|
|
56
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
57
|
+
key = _call_key(func, args, kwargs)
|
|
58
|
+
now = time.monotonic()
|
|
59
|
+
last = _last_seen.get(key)
|
|
60
|
+
if last is not None and (now - last) < window_seconds:
|
|
61
|
+
if callable(on_duplicate):
|
|
62
|
+
return on_duplicate(*args, **kwargs)
|
|
63
|
+
return on_duplicate
|
|
64
|
+
_last_seen[key] = now
|
|
65
|
+
return func(*args, **kwargs)
|
|
66
|
+
|
|
67
|
+
return wrapper # type: ignore[return-value]
|
|
68
|
+
|
|
69
|
+
return decorator
|