shortleash 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.
- shortleash-0.1.0/LICENSE +21 -0
- shortleash-0.1.0/PKG-INFO +179 -0
- shortleash-0.1.0/README.md +166 -0
- shortleash-0.1.0/pyproject.toml +25 -0
- shortleash-0.1.0/setup.cfg +4 -0
- shortleash-0.1.0/shortleash/__init__.py +3 -0
- shortleash-0.1.0/shortleash/__main__.py +6 -0
- shortleash-0.1.0/shortleash/cacheopt.py +143 -0
- shortleash-0.1.0/shortleash/cli.py +276 -0
- shortleash-0.1.0/shortleash/events.py +82 -0
- shortleash-0.1.0/shortleash/fleet.py +137 -0
- shortleash-0.1.0/shortleash/guard.py +307 -0
- shortleash-0.1.0/shortleash/history.py +79 -0
- shortleash-0.1.0/shortleash/pricing.py +59 -0
- shortleash-0.1.0/shortleash/receipt.py +140 -0
- shortleash-0.1.0/shortleash.egg-info/PKG-INFO +179 -0
- shortleash-0.1.0/shortleash.egg-info/SOURCES.txt +26 -0
- shortleash-0.1.0/shortleash.egg-info/dependency_links.txt +1 -0
- shortleash-0.1.0/shortleash.egg-info/entry_points.txt +2 -0
- shortleash-0.1.0/shortleash.egg-info/top_level.txt +1 -0
- shortleash-0.1.0/tests/test_cache.py +159 -0
- shortleash-0.1.0/tests/test_cli.py +199 -0
- shortleash-0.1.0/tests/test_events.py +81 -0
- shortleash-0.1.0/tests/test_fleet.py +90 -0
- shortleash-0.1.0/tests/test_guard.py +130 -0
- shortleash-0.1.0/tests/test_history.py +136 -0
- shortleash-0.1.0/tests/test_pricing.py +56 -0
- shortleash-0.1.0/tests/test_round2_fixes.py +171 -0
shortleash-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Calas Automations Inc.
|
|
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,179 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shortleash
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Circuit breaker for AI coding-agent runs.
|
|
5
|
+
Author: Calas Automations Inc.
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/calasdan-sketch/shortleash
|
|
8
|
+
Project-URL: Issues, https://github.com/calasdan-sketch/shortleash/issues
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
# ShortLeash — stop runaway agents
|
|
15
|
+
|
|
16
|
+
A circuit breaker for AI coding-agent runs. Wraps an agent command, tails its
|
|
17
|
+
telemetry in real time, and **kills the run the moment it breaches a guardrail**
|
|
18
|
+
— budget, subagent-spawn cap, token cap, or wall-clock timeout. Emits a
|
|
19
|
+
per-run receipt showing what it cost and why it stopped.
|
|
20
|
+
|
|
21
|
+
Vendors' "budgets" only warn while the meter keeps running. This one halts.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
Zero dependencies — Python 3.10+ stdlib only.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
git clone <repo> && cd shortleash
|
|
29
|
+
# run directly:
|
|
30
|
+
python3 -m shortleash run --budget 2.00 -- <command>
|
|
31
|
+
# or add an alias (from the repo root, no install):
|
|
32
|
+
alias shortleash="python3 -m shortleash"
|
|
33
|
+
# or install for the `shortleash` console script:
|
|
34
|
+
pip install .
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Guard a Claude Code run: kill it past $2 or 10 subagent spawns.
|
|
41
|
+
shortleash run --budget 2.00 --max-spawns 10 -- \
|
|
42
|
+
claude -p "refactor the auth module"
|
|
43
|
+
|
|
44
|
+
# All guardrails:
|
|
45
|
+
shortleash run --budget 5.00 --max-spawns 10 --max-tokens 500000 \
|
|
46
|
+
--timeout 600 --receipt ./receipt.json -- \
|
|
47
|
+
claude -p "migrate the database layer"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Anomaly mode (the smart default)
|
|
51
|
+
|
|
52
|
+
Static caps punish legitimate big tasks. Anomaly mode learns instead:
|
|
53
|
+
every receipt is appended to `~/.shortleash/history.jsonl`, and once an
|
|
54
|
+
agent has 5+ past runs, the guard trips only when the live run exceeds
|
|
55
|
+
**3× the historical median** cost or tokens for that agent.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
shortleash run -- claude -p "migrate the database layer"
|
|
59
|
+
# [shortleash] anomaly=3.0x (needs 5 runs)
|
|
60
|
+
# ...
|
|
61
|
+
# STOPPED: anomaly: estimated $4.21 is 5.3x the typical $0.79 across 23 past runs
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
A naturally large task establishes its own baseline over time; only true
|
|
65
|
+
runaways trip. Breached runs are excluded from baselines. Tune with
|
|
66
|
+
`--anomaly 5.0`, `--min-history 10`, `--profile big-refactor` (separate
|
|
67
|
+
baseline), or `--no-anomaly` to disable.
|
|
68
|
+
|
|
69
|
+
### Fleet mode (`--fleet`)
|
|
70
|
+
|
|
71
|
+
Running several agents on the same machine? `--fleet` holds a **fleet
|
|
72
|
+
claim** on paths for the run — a lockfile for agents, merged from the
|
|
73
|
+
retired claimlock:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
shortleash run --fleet --budget 3 -- claude -p "refactor the api layer"
|
|
77
|
+
# [shortleash] fleet claim ce63a732a87b: /home/dan/project
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
- Default claim: the current directory; override with
|
|
81
|
+
`--claim /path/one /path/two`.
|
|
82
|
+
- **Conflict = warning, never a block.** If another agent holds an
|
|
83
|
+
overlapping path, you get a loud stderr warning and the run proceeds
|
|
84
|
+
anyway — a lock must never be a deadlock.
|
|
85
|
+
- Released automatically on exit (normal, signal, or crash via the TTL
|
|
86
|
+
backstop). Tune with `--fleet-owner "dan"` and `--fleet-ttl 3600`.
|
|
87
|
+
|
|
88
|
+
### Prompt cache optimizer
|
|
89
|
+
|
|
90
|
+
Stop the bleed *and* shrink the bill. Prompt caching discounts repeated
|
|
91
|
+
prefix tokens (up to 90% off) — but only when stable content sits first
|
|
92
|
+
and per-request variables sit last:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
shortleash cache guide # provider docs + the core rule
|
|
96
|
+
shortleash cache check -i prompt.txt # how cacheable is this prompt?
|
|
97
|
+
shortleash cache make -i prompt.txt -o prompt.cached.txt --provider anthropic
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`cache make` restructures your `{{variable}}`-templated prompt into
|
|
101
|
+
cache-optimal layout and emits integration snippets for Anthropic (explicit
|
|
102
|
+
`cache_control` breakpoints), OpenAI/DeepSeek (automatic prefix caching),
|
|
103
|
+
and Gemini (explicit context caching).
|
|
104
|
+
|
|
105
|
+
When wrapping `claude`, `--output-format stream-json --verbose` are added
|
|
106
|
+
automatically (the guard needs the telemetry stream). The agent's stdout
|
|
107
|
+
passes through untouched, so you still watch it work.
|
|
108
|
+
|
|
109
|
+
On breach the whole process tree gets SIGTERM, then SIGKILL after
|
|
110
|
+
`--grace-period` (default 5s). Exit code is `124` on a guard kill, otherwise
|
|
111
|
+
the child's exit code.
|
|
112
|
+
|
|
113
|
+
## The receipt
|
|
114
|
+
|
|
115
|
+
Every run ends with a receipt on stderr and optionally as JSON
|
|
116
|
+
(`--receipt path.json`):
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
── shortleash receipt ──────────────────────
|
|
120
|
+
cost: $0.1271 (estimated)
|
|
121
|
+
tokens: 51,000 (in=15,000 out=4,500 cache_r=30,000 cache_w=1,500)
|
|
122
|
+
turns: 0 spawns: 1
|
|
123
|
+
duration: 0.16s exit: 124
|
|
124
|
+
STOPPED: budget exceeded: estimated $0.1271 > $0.05 cap
|
|
125
|
+
──────────────────────────────────────────────────
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Mid-run cost is estimated from a fallback pricing table (clearly marked);
|
|
129
|
+
the final receipt prefers the agent-reported `total_cost_usd` when the run
|
|
130
|
+
completes. Override pricing with `--pricing pricing.json`:
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{"sonnet": [3.0, 15.0], "opus": [15.0, 75.0], "haiku": [0.8, 4.0]}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## How it works
|
|
137
|
+
|
|
138
|
+
- Child runs in its own process group (`start_new_session=True`) — a kill
|
|
139
|
+
takes the whole tree, never your shell.
|
|
140
|
+
- A monitor thread parses each stream-json line (`system/init`,
|
|
141
|
+
`assistant` usage + `Task` tool_use spawns, `result` cost) and checks caps
|
|
142
|
+
after every observation.
|
|
143
|
+
- A watchdog thread enforces `--timeout` even if the child goes silent.
|
|
144
|
+
- Subagent spawns are counted from `Task` tool_use blocks in the stream.
|
|
145
|
+
|
|
146
|
+
## Trust boundary
|
|
147
|
+
|
|
148
|
+
Be honest about what the guard can and can't see:
|
|
149
|
+
|
|
150
|
+
- **Token, budget, spawn, and anomaly caps all depend on the child
|
|
151
|
+
agent's own telemetry.** Token counts come from its stream-json
|
|
152
|
+
`usage` events; mid-run cost is estimated from those counts; the final
|
|
153
|
+
receipt prefers the child's reported `total_cost_usd`.
|
|
154
|
+
- **A misbehaving or compromised child that under-reports (or reports
|
|
155
|
+
negative) usage can blind those caps.** The guard clamps every
|
|
156
|
+
child-reported usage and cost value to ≥ 0 so negatives can't drive
|
|
157
|
+
totals *down*, but it cannot see spend the child simply hides.
|
|
158
|
+
- **Only `--timeout` is enforced independently of the child** — a
|
|
159
|
+
wall-clock watchdog, immune to telemetry lies.
|
|
160
|
+
|
|
161
|
+
Treat this as a guardrail against runaway cost, not a security boundary
|
|
162
|
+
against an adversarial agent.
|
|
163
|
+
|
|
164
|
+
## Testing
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
python3 -m unittest discover -s tests -v
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`sim/runaway.py` is a fake agent emitting realistic stream-json — the suite
|
|
171
|
+
kills it for real (budget, spawn, token, and timeout breaches) without
|
|
172
|
+
spending a cent.
|
|
173
|
+
|
|
174
|
+
## Scope / limits (v0.1.0)
|
|
175
|
+
|
|
176
|
+
- Claude Code `-p` (non-interactive) is the supported path. Interactive
|
|
177
|
+
sessions and other agents (Codex, Cursor) are on the roadmap — the
|
|
178
|
+
event parser is isolated in `events.py` for exactly this.
|
|
179
|
+
- Cost mid-run is an estimate; treat `--budget` as a guardrail, not accounting.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# ShortLeash — stop runaway agents
|
|
2
|
+
|
|
3
|
+
A circuit breaker for AI coding-agent runs. Wraps an agent command, tails its
|
|
4
|
+
telemetry in real time, and **kills the run the moment it breaches a guardrail**
|
|
5
|
+
— budget, subagent-spawn cap, token cap, or wall-clock timeout. Emits a
|
|
6
|
+
per-run receipt showing what it cost and why it stopped.
|
|
7
|
+
|
|
8
|
+
Vendors' "budgets" only warn while the meter keeps running. This one halts.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
Zero dependencies — Python 3.10+ stdlib only.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
git clone <repo> && cd shortleash
|
|
16
|
+
# run directly:
|
|
17
|
+
python3 -m shortleash run --budget 2.00 -- <command>
|
|
18
|
+
# or add an alias (from the repo root, no install):
|
|
19
|
+
alias shortleash="python3 -m shortleash"
|
|
20
|
+
# or install for the `shortleash` console script:
|
|
21
|
+
pip install .
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Guard a Claude Code run: kill it past $2 or 10 subagent spawns.
|
|
28
|
+
shortleash run --budget 2.00 --max-spawns 10 -- \
|
|
29
|
+
claude -p "refactor the auth module"
|
|
30
|
+
|
|
31
|
+
# All guardrails:
|
|
32
|
+
shortleash run --budget 5.00 --max-spawns 10 --max-tokens 500000 \
|
|
33
|
+
--timeout 600 --receipt ./receipt.json -- \
|
|
34
|
+
claude -p "migrate the database layer"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Anomaly mode (the smart default)
|
|
38
|
+
|
|
39
|
+
Static caps punish legitimate big tasks. Anomaly mode learns instead:
|
|
40
|
+
every receipt is appended to `~/.shortleash/history.jsonl`, and once an
|
|
41
|
+
agent has 5+ past runs, the guard trips only when the live run exceeds
|
|
42
|
+
**3× the historical median** cost or tokens for that agent.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
shortleash run -- claude -p "migrate the database layer"
|
|
46
|
+
# [shortleash] anomaly=3.0x (needs 5 runs)
|
|
47
|
+
# ...
|
|
48
|
+
# STOPPED: anomaly: estimated $4.21 is 5.3x the typical $0.79 across 23 past runs
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
A naturally large task establishes its own baseline over time; only true
|
|
52
|
+
runaways trip. Breached runs are excluded from baselines. Tune with
|
|
53
|
+
`--anomaly 5.0`, `--min-history 10`, `--profile big-refactor` (separate
|
|
54
|
+
baseline), or `--no-anomaly` to disable.
|
|
55
|
+
|
|
56
|
+
### Fleet mode (`--fleet`)
|
|
57
|
+
|
|
58
|
+
Running several agents on the same machine? `--fleet` holds a **fleet
|
|
59
|
+
claim** on paths for the run — a lockfile for agents, merged from the
|
|
60
|
+
retired claimlock:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
shortleash run --fleet --budget 3 -- claude -p "refactor the api layer"
|
|
64
|
+
# [shortleash] fleet claim ce63a732a87b: /home/dan/project
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- Default claim: the current directory; override with
|
|
68
|
+
`--claim /path/one /path/two`.
|
|
69
|
+
- **Conflict = warning, never a block.** If another agent holds an
|
|
70
|
+
overlapping path, you get a loud stderr warning and the run proceeds
|
|
71
|
+
anyway — a lock must never be a deadlock.
|
|
72
|
+
- Released automatically on exit (normal, signal, or crash via the TTL
|
|
73
|
+
backstop). Tune with `--fleet-owner "dan"` and `--fleet-ttl 3600`.
|
|
74
|
+
|
|
75
|
+
### Prompt cache optimizer
|
|
76
|
+
|
|
77
|
+
Stop the bleed *and* shrink the bill. Prompt caching discounts repeated
|
|
78
|
+
prefix tokens (up to 90% off) — but only when stable content sits first
|
|
79
|
+
and per-request variables sit last:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
shortleash cache guide # provider docs + the core rule
|
|
83
|
+
shortleash cache check -i prompt.txt # how cacheable is this prompt?
|
|
84
|
+
shortleash cache make -i prompt.txt -o prompt.cached.txt --provider anthropic
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`cache make` restructures your `{{variable}}`-templated prompt into
|
|
88
|
+
cache-optimal layout and emits integration snippets for Anthropic (explicit
|
|
89
|
+
`cache_control` breakpoints), OpenAI/DeepSeek (automatic prefix caching),
|
|
90
|
+
and Gemini (explicit context caching).
|
|
91
|
+
|
|
92
|
+
When wrapping `claude`, `--output-format stream-json --verbose` are added
|
|
93
|
+
automatically (the guard needs the telemetry stream). The agent's stdout
|
|
94
|
+
passes through untouched, so you still watch it work.
|
|
95
|
+
|
|
96
|
+
On breach the whole process tree gets SIGTERM, then SIGKILL after
|
|
97
|
+
`--grace-period` (default 5s). Exit code is `124` on a guard kill, otherwise
|
|
98
|
+
the child's exit code.
|
|
99
|
+
|
|
100
|
+
## The receipt
|
|
101
|
+
|
|
102
|
+
Every run ends with a receipt on stderr and optionally as JSON
|
|
103
|
+
(`--receipt path.json`):
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
── shortleash receipt ──────────────────────
|
|
107
|
+
cost: $0.1271 (estimated)
|
|
108
|
+
tokens: 51,000 (in=15,000 out=4,500 cache_r=30,000 cache_w=1,500)
|
|
109
|
+
turns: 0 spawns: 1
|
|
110
|
+
duration: 0.16s exit: 124
|
|
111
|
+
STOPPED: budget exceeded: estimated $0.1271 > $0.05 cap
|
|
112
|
+
──────────────────────────────────────────────────
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Mid-run cost is estimated from a fallback pricing table (clearly marked);
|
|
116
|
+
the final receipt prefers the agent-reported `total_cost_usd` when the run
|
|
117
|
+
completes. Override pricing with `--pricing pricing.json`:
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{"sonnet": [3.0, 15.0], "opus": [15.0, 75.0], "haiku": [0.8, 4.0]}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## How it works
|
|
124
|
+
|
|
125
|
+
- Child runs in its own process group (`start_new_session=True`) — a kill
|
|
126
|
+
takes the whole tree, never your shell.
|
|
127
|
+
- A monitor thread parses each stream-json line (`system/init`,
|
|
128
|
+
`assistant` usage + `Task` tool_use spawns, `result` cost) and checks caps
|
|
129
|
+
after every observation.
|
|
130
|
+
- A watchdog thread enforces `--timeout` even if the child goes silent.
|
|
131
|
+
- Subagent spawns are counted from `Task` tool_use blocks in the stream.
|
|
132
|
+
|
|
133
|
+
## Trust boundary
|
|
134
|
+
|
|
135
|
+
Be honest about what the guard can and can't see:
|
|
136
|
+
|
|
137
|
+
- **Token, budget, spawn, and anomaly caps all depend on the child
|
|
138
|
+
agent's own telemetry.** Token counts come from its stream-json
|
|
139
|
+
`usage` events; mid-run cost is estimated from those counts; the final
|
|
140
|
+
receipt prefers the child's reported `total_cost_usd`.
|
|
141
|
+
- **A misbehaving or compromised child that under-reports (or reports
|
|
142
|
+
negative) usage can blind those caps.** The guard clamps every
|
|
143
|
+
child-reported usage and cost value to ≥ 0 so negatives can't drive
|
|
144
|
+
totals *down*, but it cannot see spend the child simply hides.
|
|
145
|
+
- **Only `--timeout` is enforced independently of the child** — a
|
|
146
|
+
wall-clock watchdog, immune to telemetry lies.
|
|
147
|
+
|
|
148
|
+
Treat this as a guardrail against runaway cost, not a security boundary
|
|
149
|
+
against an adversarial agent.
|
|
150
|
+
|
|
151
|
+
## Testing
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
python3 -m unittest discover -s tests -v
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`sim/runaway.py` is a fake agent emitting realistic stream-json — the suite
|
|
158
|
+
kills it for real (budget, spawn, token, and timeout breaches) without
|
|
159
|
+
spending a cent.
|
|
160
|
+
|
|
161
|
+
## Scope / limits (v0.1.0)
|
|
162
|
+
|
|
163
|
+
- Claude Code `-p` (non-interactive) is the supported path. Interactive
|
|
164
|
+
sessions and other agents (Codex, Cursor) are on the roadmap — the
|
|
165
|
+
event parser is isolated in `events.py` for exactly this.
|
|
166
|
+
- Cost mid-run is an estimate; treat `--budget` as a guardrail, not accounting.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
[project]
|
|
5
|
+
name = "shortleash"
|
|
6
|
+
# Single source of truth: shortleash/__init__.py defines __version__.
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Circuit breaker for AI coding-agent runs."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
authors = [{name = "Calas Automations Inc."}]
|
|
12
|
+
license = {text = "MIT"}
|
|
13
|
+
|
|
14
|
+
[tool.setuptools.dynamic]
|
|
15
|
+
version = {attr = "shortleash.__version__"}
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Homepage = "https://github.com/calasdan-sketch/shortleash"
|
|
19
|
+
Issues = "https://github.com/calasdan-sketch/shortleash/issues"
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
shortleash = "shortleash.cli:main"
|
|
23
|
+
|
|
24
|
+
[tool.setuptools]
|
|
25
|
+
packages = ["shortleash"]
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Cache optimizer: prompts structured for LLM prompt caching.
|
|
2
|
+
|
|
3
|
+
Prompt caching (Anthropic/OpenAI/Gemini/DeepSeek all do some form) discounts
|
|
4
|
+
repeated prefix tokens — up to 90% off on cache hits. But it only works if
|
|
5
|
+
the stable content sits FIRST and the per-request variables sit LAST.
|
|
6
|
+
Most hand-written prompts interleave them, silently killing the cache.
|
|
7
|
+
|
|
8
|
+
`cache make` restructures a {{variable}}-templated prompt into
|
|
9
|
+
cache-optimal layout and emits per-provider integration snippets.
|
|
10
|
+
`cache check` reports how cacheable a prompt is. `cache guide` prints
|
|
11
|
+
the verified provider docs.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
|
|
18
|
+
PLACEHOLDER_RE = re.compile(r"\{\{\s*(\w+)\s*\}\}")
|
|
19
|
+
# \w (not [a-zA-Z0-9_]) so unicode names like {{naïve}} are found too.
|
|
20
|
+
|
|
21
|
+
CACHE_DOCS = [
|
|
22
|
+
("Anthropic",
|
|
23
|
+
"https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching",
|
|
24
|
+
"Explicit breakpoints via cache_control; up to 90% off cached tokens. "
|
|
25
|
+
"Put static content in early blocks, mark the breakpoint."),
|
|
26
|
+
("OpenAI",
|
|
27
|
+
"https://platform.openai.com/docs/guides/prompt-caching",
|
|
28
|
+
"Automatic prefix caching (no code changes); 50% off. "
|
|
29
|
+
"Keep the stable prefix byte-identical across requests."),
|
|
30
|
+
("Google Gemini",
|
|
31
|
+
"https://ai.google.dev/gemini-api/docs/caching",
|
|
32
|
+
"Explicit context caching API (cached_content); 75% off. "
|
|
33
|
+
"Cache the stable context object, pass variables per request."),
|
|
34
|
+
("DeepSeek",
|
|
35
|
+
"https://api-docs.deepseek.com/guides/kv_cache",
|
|
36
|
+
"Automatic prefix caching on repeated prefixes; 50% off. "
|
|
37
|
+
"Stable prefix first, same as OpenAI."),
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def find_placeholders(text: str) -> list[str]:
|
|
42
|
+
seen: list[str] = []
|
|
43
|
+
for m in PLACEHOLDER_RE.finditer(text):
|
|
44
|
+
if m.group(1) not in seen:
|
|
45
|
+
seen.append(m.group(1))
|
|
46
|
+
return seen
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def analyze(text: str) -> dict:
|
|
50
|
+
placeholders = find_placeholders(text)
|
|
51
|
+
static = PLACEHOLDER_RE.sub("", text)
|
|
52
|
+
return {
|
|
53
|
+
"placeholders": placeholders,
|
|
54
|
+
"n_placeholders": len(placeholders),
|
|
55
|
+
"static_chars": len(static),
|
|
56
|
+
"total_chars": len(text),
|
|
57
|
+
"static_ratio": len(static) / len(text) if text else 1.0,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def restructure(text: str) -> tuple[str, list[str]]:
|
|
62
|
+
"""Return (optimized_text, notes). Static content first (cacheable
|
|
63
|
+
prefix), variables last (below the breakpoint)."""
|
|
64
|
+
placeholders = find_placeholders(text)
|
|
65
|
+
notes: list[str] = []
|
|
66
|
+
if not placeholders:
|
|
67
|
+
notes.append("no {{variables}} found — the whole prompt is static, "
|
|
68
|
+
"already fully cacheable.")
|
|
69
|
+
return text, notes
|
|
70
|
+
# Split into static segments, preserving order, dropping empties.
|
|
71
|
+
parts = PLACEHOLDER_RE.split(text)
|
|
72
|
+
statics = [p for i, p in enumerate(parts) if i % 2 == 0 and p.strip()]
|
|
73
|
+
static_block = "\n".join(s.rstrip() for s in statics).strip()
|
|
74
|
+
var_block = "\n".join("{{" + name + "}}" for name in placeholders)
|
|
75
|
+
optimized = (static_block
|
|
76
|
+
+ "\n\n---\n"
|
|
77
|
+
"Variables (per-request — keep below the cache breakpoint):\n"
|
|
78
|
+
+ var_block + "\n")
|
|
79
|
+
# Warn if a variable was buried mid-prompt (the thing we're fixing).
|
|
80
|
+
first_var = text.find("{{")
|
|
81
|
+
if first_var > len(text) // 2:
|
|
82
|
+
notes.append("variables were already near the end — good shape, "
|
|
83
|
+
"minor cleanup applied.")
|
|
84
|
+
else:
|
|
85
|
+
notes.append(f"moved {len(placeholders)} variable(s) below the "
|
|
86
|
+
"static prefix so the cache hits.")
|
|
87
|
+
notes.append("Review the rewrite: reordering can change emphasis. "
|
|
88
|
+
"The static text is preserved verbatim.")
|
|
89
|
+
return optimized, notes
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def provider_snippet(provider: str) -> str:
|
|
93
|
+
p = provider.lower()
|
|
94
|
+
if p == "anthropic":
|
|
95
|
+
return '''Anthropic (explicit breakpoint):
|
|
96
|
+
import anthropic
|
|
97
|
+
client = anthropic.Anthropic()
|
|
98
|
+
# Put the STATIC part in its own block with cache_control.
|
|
99
|
+
# Everything after the breakpoint (your {{variables}}) is fresh each call.
|
|
100
|
+
msg = client.messages.create(
|
|
101
|
+
model="claude-opus-4-6",
|
|
102
|
+
max_tokens=1024,
|
|
103
|
+
system=[{"type": "text", "text": STATIC_PROMPT,
|
|
104
|
+
"cache_control": {"type": "ephemeral"}}],
|
|
105
|
+
messages=[{"role": "user",
|
|
106
|
+
"content": VARIABLES_TEXT}])
|
|
107
|
+
# Cached input tokens are ~90% cheaper. Breakpoints: max 4, min 1024 tokens.'''
|
|
108
|
+
if p == "openai":
|
|
109
|
+
return '''OpenAI (automatic — no code changes):
|
|
110
|
+
# Prefix caching is automatic on repeated prefixes (50% off).
|
|
111
|
+
# Just keep the STATIC part byte-identical and FIRST in messages:
|
|
112
|
+
messages = [{"role": "system", "content": STATIC_PROMPT}, # stable prefix
|
|
113
|
+
{"role": "user", "content": VARIABLES_TEXT}] # changes per call'''
|
|
114
|
+
if p == "gemini":
|
|
115
|
+
return '''Gemini (explicit context caching, 75% off):
|
|
116
|
+
from google import genai
|
|
117
|
+
client = genai.Client()
|
|
118
|
+
# Cache the stable context once, reuse its name per request:
|
|
119
|
+
cache = client.caches.create(model="gemini-2.5-flash",
|
|
120
|
+
config={"contents": STATIC_PROMPT,
|
|
121
|
+
"ttl": "3600s"})
|
|
122
|
+
resp = client.models.generate_content(
|
|
123
|
+
model="gemini-2.5-flash",
|
|
124
|
+
config={"cached_content": cache.name},
|
|
125
|
+
contents=VARIABLES_TEXT)'''
|
|
126
|
+
if p == "deepseek":
|
|
127
|
+
return '''DeepSeek (automatic prefix caching, 50% off):
|
|
128
|
+
# Same rule as OpenAI: stable prefix first, byte-identical across calls.
|
|
129
|
+
# No API changes needed — the cache engages on prefix match.'''
|
|
130
|
+
raise ValueError(f"unknown provider: {provider!r}")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def guide_text() -> str:
|
|
134
|
+
lines = ["", " PROMPT CACHING — provider guide", "",
|
|
135
|
+
" Cache discounts repeated prefix tokens. The rule everywhere:"]
|
|
136
|
+
lines.append(" STATIC content first (system prompt, docs, examples),")
|
|
137
|
+
lines.append(" {{variables}} last. Byte-identical prefixes hit the cache.")
|
|
138
|
+
lines.append("")
|
|
139
|
+
for name, url, note in CACHE_DOCS:
|
|
140
|
+
lines.append(f" {name}\n {url}\n {note}\n")
|
|
141
|
+
lines.append(" `shortleash cache make -i prompt.txt` restructures")
|
|
142
|
+
lines.append(" your prompt for this automatically.")
|
|
143
|
+
return "\n".join(lines)
|