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