skillware 0.3.9__tar.gz → 0.4.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.
- {skillware-0.3.9/skillware.egg-info → skillware-0.4.0}/PKG-INFO +14 -2
- {skillware-0.3.9 → skillware-0.4.0}/README.md +13 -1
- {skillware-0.3.9 → skillware-0.4.0}/pyproject.toml +1 -1
- skillware-0.4.0/skills/monitoring/__init__.py +1 -0
- skillware-0.4.0/skills/monitoring/token_limiter/__init__.py +1 -0
- skillware-0.4.0/skills/monitoring/token_limiter/budget.py +286 -0
- skillware-0.4.0/skills/monitoring/token_limiter/card.json +22 -0
- skillware-0.4.0/skills/monitoring/token_limiter/data/model_pricing.json +39 -0
- skillware-0.4.0/skills/monitoring/token_limiter/instructions.md +42 -0
- skillware-0.4.0/skills/monitoring/token_limiter/manifest.yaml +74 -0
- skillware-0.4.0/skills/monitoring/token_limiter/skill.py +60 -0
- skillware-0.4.0/skills/monitoring/token_limiter/test_skill.py +174 -0
- {skillware-0.3.9 → skillware-0.4.0}/skillware/cli.py +7 -2
- {skillware-0.3.9 → skillware-0.4.0/skillware.egg-info}/PKG-INFO +14 -2
- {skillware-0.3.9 → skillware-0.4.0}/skillware.egg-info/SOURCES.txt +10 -0
- skillware-0.4.0/tests/test_registry_docs.py +166 -0
- {skillware-0.3.9 → skillware-0.4.0}/LICENSE +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/MANIFEST.in +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/setup.cfg +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/__init__.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/__init__.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/mica_module/__init__.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/mica_module/card.json +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/mica_module/instructions.md +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/mica_module/manifest.yaml +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/mica_module/mica_corpus.json +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/mica_module/skill.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/mica_module/test_skill.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/pii_masker/__init__.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/pii_masker/card.json +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/pii_masker/instructions.md +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/pii_masker/manifest.yaml +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/pii_masker/skill.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/pii_masker/test_skill.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/tos_evaluator/__init__.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/tos_evaluator/card.json +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/tos_evaluator/instructions.md +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/tos_evaluator/manifest.yaml +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/tos_evaluator/skill.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/tos_evaluator/test_skill.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/data_engineering/__init__.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/data_engineering/novelty_extractor/__init__.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/data_engineering/novelty_extractor/card.json +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/data_engineering/novelty_extractor/instructions.md +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/data_engineering/novelty_extractor/manifest.yaml +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/data_engineering/novelty_extractor/skill.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/data_engineering/novelty_extractor/test_skill.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/data_engineering/synthetic_generator/__init__.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/data_engineering/synthetic_generator/card.json +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/data_engineering/synthetic_generator/instructions.md +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/data_engineering/synthetic_generator/manifest.yaml +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/data_engineering/synthetic_generator/skill.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/data_engineering/synthetic_generator/test_skill.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/defi/__init__.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/defi/evm_tx_handler/__init__.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/defi/evm_tx_handler/abis.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/defi/evm_tx_handler/card.json +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/defi/evm_tx_handler/config.yaml.example +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/defi/evm_tx_handler/data/addressbook.yaml +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/defi/evm_tx_handler/data/chains.yaml +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/defi/evm_tx_handler/data/tokens.yaml +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/defi/evm_tx_handler/instructions.md +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/defi/evm_tx_handler/manifest.yaml +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/defi/evm_tx_handler/skill.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/defi/evm_tx_handler/test_skill.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/dev_tools/__init__.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/dev_tools/issue_resolver/__init__.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/dev_tools/issue_resolver/card.json +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/dev_tools/issue_resolver/instructions.md +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/dev_tools/issue_resolver/manifest.yaml +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/dev_tools/issue_resolver/skill.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/dev_tools/issue_resolver/test_skill.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/dev_tools/issue_resolver/workflow.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/finance/__init__.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/finance/wallet_screening/__init__.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/finance/wallet_screening/card.json +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/finance/wallet_screening/data/entities.ftm.json +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/finance/wallet_screening/data/malicious_scs_2025.json +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/finance/wallet_screening/data/normalized_fbi_lazarus_20250722_180019.json +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/finance/wallet_screening/data/normalized_israel_nbctf_20250722_180019.json +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/finance/wallet_screening/data/normalized_uniswap_trm.json +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/finance/wallet_screening/instructions.md +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/finance/wallet_screening/maintenance/normalization_tool.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/finance/wallet_screening/maintenance/normalize_uniswap_trm.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/finance/wallet_screening/manifest.yaml +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/finance/wallet_screening/skill.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/finance/wallet_screening/test_skill.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/office/__init__.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/office/pdf_form_filler/__init__.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/office/pdf_form_filler/card.json +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/office/pdf_form_filler/instructions.md +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/office/pdf_form_filler/manifest.yaml +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/office/pdf_form_filler/skill.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/office/pdf_form_filler/test_skill.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/office/pdf_form_filler/utils.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/optimization/__init__.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/optimization/prompt_rewriter/__init__.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/optimization/prompt_rewriter/card.json +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/optimization/prompt_rewriter/instructions.md +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/optimization/prompt_rewriter/manifest.yaml +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/optimization/prompt_rewriter/skill.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/optimization/prompt_rewriter/test_skill.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/wellness/__init__.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/wellness/mental_coach/__init__.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/wellness/mental_coach/card.json +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/wellness/mental_coach/constraints.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/wellness/mental_coach/crisis_gate.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/wellness/mental_coach/instructions.md +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/wellness/mental_coach/kb/corpus.json +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/wellness/mental_coach/kb/crisis_resources.json +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/wellness/mental_coach/kb/hard_constraints.yaml +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/wellness/mental_coach/kb_provider.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/wellness/mental_coach/manifest.yaml +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/wellness/mental_coach/resources.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/wellness/mental_coach/skill.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skills/wellness/mental_coach/test_skill.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skillware/__init__.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skillware/__main__.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skillware/core/__init__.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skillware/core/base_skill.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skillware/core/env.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skillware/core/loader.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skillware/version_policy.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skillware.egg-info/dependency_links.txt +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skillware.egg-info/entry_points.txt +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skillware.egg-info/requires.txt +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/skillware.egg-info/top_level.txt +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/tests/test_cli.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/tests/test_loader.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/tests/test_skill_issuer.py +0 -0
- {skillware-0.3.9 → skillware-0.4.0}/tests/test_version_policy.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: skillware
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: A framework for modular, self-contained AI skills.
|
|
5
5
|
Author-email: ARPA Hellenic Logical Systems <skillware-os@arpacorp.net>
|
|
6
6
|
License: MIT License
|
|
@@ -81,6 +81,7 @@ Dynamic: license-file
|
|
|
81
81
|
<img src="https://img.shields.io/badge/License-MIT-efcefa?style=flat-square" alt="License">
|
|
82
82
|
<img src="https://img.shields.io/badge/Python-3.10+-bae6fd?style=flat-square" alt="Python Version">
|
|
83
83
|
<a href="https://pypi.org/project/skillware/"><img src="https://img.shields.io/pypi/v/skillware?style=flat-square&color=bbf7d0" alt="PyPI Version"></a>
|
|
84
|
+
<a href="https://pepy.tech/projects/skillware"><img src="https://img.shields.io/pepy/dt/skillware?style=flat-square&label=DLs%20%E2%86%93&color=ffdac1" alt="Total PyPI downloads"></a>
|
|
84
85
|
</div>
|
|
85
86
|
|
|
86
87
|
<br/>
|
|
@@ -92,6 +93,7 @@ Dynamic: license-file
|
|
|
92
93
|
<a href="#documentation">Documentation</a> •
|
|
93
94
|
<a href="#contributing">Contributing</a> •
|
|
94
95
|
<a href="#comparison">Comparison</a> •
|
|
96
|
+
<a href="#stats">Stats</a> •
|
|
95
97
|
<a href="CHANGELOG.md">Changelog</a> •
|
|
96
98
|
<a href="#contact">Contact</a>
|
|
97
99
|
</div>
|
|
@@ -180,7 +182,9 @@ skillware list
|
|
|
180
182
|
|
|
181
183
|
This prints a table of all locally available skills and confirms the install
|
|
182
184
|
and path resolution are working. Running `skillware` with no arguments opens
|
|
183
|
-
the interactive menu.
|
|
185
|
+
the interactive menu. As optional checks you can also run `skillware test` to
|
|
186
|
+
execute bundle tests, and `skillware examples` to browse the runnable example
|
|
187
|
+
index (fetched from GitHub when no local `examples/README.md` is present).
|
|
184
188
|
|
|
185
189
|
If `skillware` is not recognized, Python's `Scripts` directory may not be on
|
|
186
190
|
your PATH — use `python -m skillware list` as a fallback. See
|
|
@@ -292,6 +296,14 @@ Skillware differs from the Model Context Protocol (MCP), and Agent Skills (SKILL
|
|
|
292
296
|
|
|
293
297
|
[Read the full comparison here](COMPARISON.md).
|
|
294
298
|
|
|
299
|
+
## Stats
|
|
300
|
+
|
|
301
|
+
<a href="https://pepy.tech/projects/skillware"><img src="https://img.shields.io/badge/Stats-PePyTech-ffdac1?style=flat-square" alt="PePy.tech dashboard"></a>
|
|
302
|
+
<a href="https://pypistats.org/packages/skillware"><img src="https://img.shields.io/badge/Stats-PyPiStats-ffdac1?style=flat-square" alt="PyPI Stats dashboard"></a>
|
|
303
|
+
<a href="https://pepy.tech/projects/skillware"><img src="https://img.shields.io/pepy/dt/skillware?style=flat-square&label=DLs%20%E2%86%93%20total&color=bbf7d0" alt="Total downloads (PePy)"></a>
|
|
304
|
+
|
|
305
|
+
PyPI download counts show how often `skillware` is installed from the package index. Numbers come from public third-party aggregators, and they include CI and mirror traffic, so they measure **install activity**, not unique users. Totals may differ slightly between services because each source uses its own window and methodology. Use the stats dashboards in the above badges for charts, version breakdowns, and longer history.
|
|
306
|
+
|
|
295
307
|
## Contact
|
|
296
308
|
|
|
297
309
|
For questions, suggestions, or contributions, please open an issue or reach out to us:
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
<img src="https://img.shields.io/badge/License-MIT-efcefa?style=flat-square" alt="License">
|
|
11
11
|
<img src="https://img.shields.io/badge/Python-3.10+-bae6fd?style=flat-square" alt="Python Version">
|
|
12
12
|
<a href="https://pypi.org/project/skillware/"><img src="https://img.shields.io/pypi/v/skillware?style=flat-square&color=bbf7d0" alt="PyPI Version"></a>
|
|
13
|
+
<a href="https://pepy.tech/projects/skillware"><img src="https://img.shields.io/pepy/dt/skillware?style=flat-square&label=DLs%20%E2%86%93&color=ffdac1" alt="Total PyPI downloads"></a>
|
|
13
14
|
</div>
|
|
14
15
|
|
|
15
16
|
<br/>
|
|
@@ -21,6 +22,7 @@
|
|
|
21
22
|
<a href="#documentation">Documentation</a> •
|
|
22
23
|
<a href="#contributing">Contributing</a> •
|
|
23
24
|
<a href="#comparison">Comparison</a> •
|
|
25
|
+
<a href="#stats">Stats</a> •
|
|
24
26
|
<a href="CHANGELOG.md">Changelog</a> •
|
|
25
27
|
<a href="#contact">Contact</a>
|
|
26
28
|
</div>
|
|
@@ -109,7 +111,9 @@ skillware list
|
|
|
109
111
|
|
|
110
112
|
This prints a table of all locally available skills and confirms the install
|
|
111
113
|
and path resolution are working. Running `skillware` with no arguments opens
|
|
112
|
-
the interactive menu.
|
|
114
|
+
the interactive menu. As optional checks you can also run `skillware test` to
|
|
115
|
+
execute bundle tests, and `skillware examples` to browse the runnable example
|
|
116
|
+
index (fetched from GitHub when no local `examples/README.md` is present).
|
|
113
117
|
|
|
114
118
|
If `skillware` is not recognized, Python's `Scripts` directory may not be on
|
|
115
119
|
your PATH — use `python -m skillware list` as a fallback. See
|
|
@@ -221,6 +225,14 @@ Skillware differs from the Model Context Protocol (MCP), and Agent Skills (SKILL
|
|
|
221
225
|
|
|
222
226
|
[Read the full comparison here](COMPARISON.md).
|
|
223
227
|
|
|
228
|
+
## Stats
|
|
229
|
+
|
|
230
|
+
<a href="https://pepy.tech/projects/skillware"><img src="https://img.shields.io/badge/Stats-PePyTech-ffdac1?style=flat-square" alt="PePy.tech dashboard"></a>
|
|
231
|
+
<a href="https://pypistats.org/packages/skillware"><img src="https://img.shields.io/badge/Stats-PyPiStats-ffdac1?style=flat-square" alt="PyPI Stats dashboard"></a>
|
|
232
|
+
<a href="https://pepy.tech/projects/skillware"><img src="https://img.shields.io/pepy/dt/skillware?style=flat-square&label=DLs%20%E2%86%93%20total&color=bbf7d0" alt="Total downloads (PePy)"></a>
|
|
233
|
+
|
|
234
|
+
PyPI download counts show how often `skillware` is installed from the package index. Numbers come from public third-party aggregators, and they include CI and mirror traffic, so they measure **install activity**, not unique users. Totals may differ slightly between services because each source uses its own window and methodology. Use the stats dashboards in the above badges for charts, version breakdowns, and longer history.
|
|
235
|
+
|
|
224
236
|
## Contact
|
|
225
237
|
|
|
226
238
|
For questions, suggestions, or contributions, please open an issue or reach out to us:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Monitoring skills package.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Token hard limit skill package.
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""Deterministic token budget evaluation for agent loops."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
9
|
+
|
|
10
|
+
SCHEMA_VERSION = "1.0"
|
|
11
|
+
DEFAULT_SOFT_THRESHOLD_PCT = 80.0
|
|
12
|
+
VALID_ACTIONS = frozenset({"check", "reset"})
|
|
13
|
+
VALID_DECISIONS = frozenset({"CONTINUE", "WARN", "FORCE_TERMINATE"})
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def utc_now_iso() -> str:
|
|
17
|
+
return (
|
|
18
|
+
datetime.now(timezone.utc)
|
|
19
|
+
.replace(microsecond=0)
|
|
20
|
+
.isoformat()
|
|
21
|
+
.replace("+00:00", "Z")
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load_pricing(data_dir: str) -> Dict[str, Any]:
|
|
26
|
+
pricing_path = os.path.join(data_dir, "model_pricing.json")
|
|
27
|
+
with open(pricing_path, "r", encoding="utf-8") as handle:
|
|
28
|
+
return json.load(handle)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _positive_int(value: Any, field: str) -> Tuple[Optional[int], Optional[str]]:
|
|
32
|
+
if value is None:
|
|
33
|
+
return None, f"{field} is required."
|
|
34
|
+
if isinstance(value, bool):
|
|
35
|
+
return None, f"{field} must be an integer."
|
|
36
|
+
try:
|
|
37
|
+
parsed = int(value)
|
|
38
|
+
except (TypeError, ValueError):
|
|
39
|
+
return None, f"{field} must be an integer."
|
|
40
|
+
if parsed < 0:
|
|
41
|
+
return None, f"{field} must be zero or greater."
|
|
42
|
+
return parsed, None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _optional_non_negative_int(
|
|
46
|
+
value: Any, field: str
|
|
47
|
+
) -> Tuple[Optional[int], Optional[str]]:
|
|
48
|
+
if value is None:
|
|
49
|
+
return None, None
|
|
50
|
+
return _positive_int(value, field)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _soft_threshold_pct(value: Any) -> Tuple[float, Optional[str]]:
|
|
54
|
+
if value is None:
|
|
55
|
+
return DEFAULT_SOFT_THRESHOLD_PCT, None
|
|
56
|
+
try:
|
|
57
|
+
parsed = float(value)
|
|
58
|
+
except (TypeError, ValueError):
|
|
59
|
+
return DEFAULT_SOFT_THRESHOLD_PCT, "soft_threshold_pct must be a number."
|
|
60
|
+
if parsed <= 0 or parsed > 100:
|
|
61
|
+
return (
|
|
62
|
+
DEFAULT_SOFT_THRESHOLD_PCT,
|
|
63
|
+
"soft_threshold_pct must be between 0 and 100.",
|
|
64
|
+
)
|
|
65
|
+
return parsed, None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def estimate_cost_usd(
|
|
69
|
+
pricing: Dict[str, Any],
|
|
70
|
+
model_id: Optional[str],
|
|
71
|
+
current_token_count: int,
|
|
72
|
+
input_tokens: Optional[int],
|
|
73
|
+
output_tokens: Optional[int],
|
|
74
|
+
) -> Tuple[Optional[float], List[str]]:
|
|
75
|
+
warnings: List[str] = []
|
|
76
|
+
if not model_id:
|
|
77
|
+
return None, warnings
|
|
78
|
+
|
|
79
|
+
models = pricing.get("models", {})
|
|
80
|
+
model_key = str(model_id).strip()
|
|
81
|
+
rates = models.get(model_key)
|
|
82
|
+
|
|
83
|
+
if rates is None:
|
|
84
|
+
fallback = pricing.get("fallback", {})
|
|
85
|
+
blended = float(fallback.get("blended_usd_per_1m", 5.0))
|
|
86
|
+
warnings.append(
|
|
87
|
+
f"Unknown model_id {model_key!r}; using fallback blended rate "
|
|
88
|
+
f"{blended} USD per 1M tokens."
|
|
89
|
+
)
|
|
90
|
+
cost = (current_token_count / 1_000_000.0) * blended
|
|
91
|
+
return round(cost, 6), warnings
|
|
92
|
+
|
|
93
|
+
input_rate = float(rates.get("input_usd_per_1m", 0.0))
|
|
94
|
+
output_rate = float(rates.get("output_usd_per_1m", 0.0))
|
|
95
|
+
|
|
96
|
+
if input_tokens is None and output_tokens is None:
|
|
97
|
+
input_tokens = current_token_count // 2
|
|
98
|
+
output_tokens = current_token_count - input_tokens
|
|
99
|
+
warnings.append(
|
|
100
|
+
"input_tokens and output_tokens were not provided; "
|
|
101
|
+
"assuming a 50/50 split for cost estimation."
|
|
102
|
+
)
|
|
103
|
+
elif input_tokens is None:
|
|
104
|
+
input_tokens = max(0, current_token_count - int(output_tokens))
|
|
105
|
+
elif output_tokens is None:
|
|
106
|
+
output_tokens = max(0, current_token_count - int(input_tokens))
|
|
107
|
+
|
|
108
|
+
cost = (input_tokens / 1_000_000.0) * input_rate + (
|
|
109
|
+
output_tokens / 1_000_000.0
|
|
110
|
+
) * output_rate
|
|
111
|
+
return round(cost, 6), warnings
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def build_roi_scaffold(
|
|
115
|
+
params: Dict[str, Any], cost_usd: Optional[float]
|
|
116
|
+
) -> Dict[str, Any]:
|
|
117
|
+
roi_value = params.get("roi_value_usd")
|
|
118
|
+
expected_outcome = params.get("expected_outcome")
|
|
119
|
+
outcome_delivered = params.get("outcome_delivered")
|
|
120
|
+
|
|
121
|
+
scaffold: Dict[str, Any] = {
|
|
122
|
+
"enabled": False,
|
|
123
|
+
"status": "not_evaluated",
|
|
124
|
+
"notes": (
|
|
125
|
+
"ROI enforcement is not active in v1. Token limits are the sole "
|
|
126
|
+
"termination trigger. Future versions may compare expected_outcome "
|
|
127
|
+
"and outcome_delivered against token spend."
|
|
128
|
+
),
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if roi_value is not None or expected_outcome or outcome_delivered is not None:
|
|
132
|
+
scaffold["enabled"] = True
|
|
133
|
+
scaffold["roi_value_usd"] = roi_value
|
|
134
|
+
scaffold["expected_outcome"] = expected_outcome
|
|
135
|
+
scaffold["outcome_delivered"] = outcome_delivered
|
|
136
|
+
scaffold["cost_incurred_usd"] = cost_usd
|
|
137
|
+
scaffold["status"] = "scaffold_only"
|
|
138
|
+
|
|
139
|
+
return scaffold
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def evaluate_budget(
|
|
143
|
+
params: Dict[str, Any],
|
|
144
|
+
pricing: Dict[str, Any],
|
|
145
|
+
turn_cache: Dict[str, Dict[str, Any]],
|
|
146
|
+
) -> Dict[str, Any]:
|
|
147
|
+
action = (params.get("action") or "check").strip().lower()
|
|
148
|
+
if action not in VALID_ACTIONS:
|
|
149
|
+
return {
|
|
150
|
+
"status": "error",
|
|
151
|
+
"message": f"Invalid action {action!r}. Expected one of: check, reset.",
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
task_id = (params.get("task_id") or "").strip()
|
|
155
|
+
if not task_id:
|
|
156
|
+
return {"status": "error", "message": "task_id is required."}
|
|
157
|
+
|
|
158
|
+
if action == "reset":
|
|
159
|
+
prefix = f"{task_id}|"
|
|
160
|
+
keys_to_delete = [key for key in turn_cache if key.startswith(prefix)]
|
|
161
|
+
for key in keys_to_delete:
|
|
162
|
+
del turn_cache[key]
|
|
163
|
+
return {
|
|
164
|
+
"status": "ready",
|
|
165
|
+
"schema_version": SCHEMA_VERSION,
|
|
166
|
+
"action": "RESET",
|
|
167
|
+
"task_id": task_id,
|
|
168
|
+
"message": f"Cleared {len(keys_to_delete)} cached turn result(s) for task.",
|
|
169
|
+
"timestamp": utc_now_iso(),
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
current_token_count, err = _positive_int(
|
|
173
|
+
params.get("current_token_count"), "current_token_count"
|
|
174
|
+
)
|
|
175
|
+
if err:
|
|
176
|
+
return {"status": "error", "message": err}
|
|
177
|
+
|
|
178
|
+
max_allowed_tokens, err = _positive_int(
|
|
179
|
+
params.get("max_allowed_tokens"), "max_allowed_tokens"
|
|
180
|
+
)
|
|
181
|
+
if err:
|
|
182
|
+
return {"status": "error", "message": err}
|
|
183
|
+
|
|
184
|
+
if max_allowed_tokens == 0:
|
|
185
|
+
return {
|
|
186
|
+
"status": "error",
|
|
187
|
+
"message": "max_allowed_tokens must be greater than zero.",
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
soft_threshold_pct, soft_err = _soft_threshold_pct(params.get("soft_threshold_pct"))
|
|
191
|
+
turn_id = (params.get("turn_id") or "").strip()
|
|
192
|
+
cache_key = f"{task_id}|{turn_id}|{current_token_count}|{max_allowed_tokens}"
|
|
193
|
+
|
|
194
|
+
if turn_id and cache_key in turn_cache:
|
|
195
|
+
cached = dict(turn_cache[cache_key])
|
|
196
|
+
cached["metadata"] = dict(cached.get("metadata", {}))
|
|
197
|
+
cached["metadata"]["cache_hit"] = True
|
|
198
|
+
return cached
|
|
199
|
+
|
|
200
|
+
input_tokens, input_err = _optional_non_negative_int(
|
|
201
|
+
params.get("input_tokens"), "input_tokens"
|
|
202
|
+
)
|
|
203
|
+
if input_err:
|
|
204
|
+
return {"status": "error", "message": input_err}
|
|
205
|
+
|
|
206
|
+
output_tokens, output_err = _optional_non_negative_int(
|
|
207
|
+
params.get("output_tokens"), "output_tokens"
|
|
208
|
+
)
|
|
209
|
+
if output_err:
|
|
210
|
+
return {"status": "error", "message": output_err}
|
|
211
|
+
|
|
212
|
+
model_id = (params.get("model_id") or "").strip() or None
|
|
213
|
+
cost_usd, cost_warnings = estimate_cost_usd(
|
|
214
|
+
pricing,
|
|
215
|
+
model_id,
|
|
216
|
+
current_token_count,
|
|
217
|
+
input_tokens,
|
|
218
|
+
output_tokens,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
utilization_pct = round((current_token_count / max_allowed_tokens) * 100.0, 2)
|
|
222
|
+
tokens_over_budget = max(0, current_token_count - max_allowed_tokens)
|
|
223
|
+
soft_limit = int(max_allowed_tokens * (soft_threshold_pct / 100.0))
|
|
224
|
+
|
|
225
|
+
warnings: List[str] = []
|
|
226
|
+
if soft_err:
|
|
227
|
+
warnings.append(soft_err)
|
|
228
|
+
warnings.extend(cost_warnings)
|
|
229
|
+
|
|
230
|
+
if current_token_count >= max_allowed_tokens:
|
|
231
|
+
decision = "FORCE_TERMINATE"
|
|
232
|
+
reason = (
|
|
233
|
+
f"Token budget exceeded by {tokens_over_budget} "
|
|
234
|
+
f"({utilization_pct}% of limit)."
|
|
235
|
+
)
|
|
236
|
+
elif current_token_count >= soft_limit:
|
|
237
|
+
decision = "WARN"
|
|
238
|
+
reason = (
|
|
239
|
+
f"Token utilization at {utilization_pct}% "
|
|
240
|
+
f"(soft threshold {soft_threshold_pct}%)."
|
|
241
|
+
)
|
|
242
|
+
else:
|
|
243
|
+
decision = "CONTINUE"
|
|
244
|
+
reason = (
|
|
245
|
+
f"Token utilization at {utilization_pct}% "
|
|
246
|
+
f"({current_token_count}/{max_allowed_tokens})."
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
roi = build_roi_scaffold(params, cost_usd)
|
|
250
|
+
|
|
251
|
+
result: Dict[str, Any] = {
|
|
252
|
+
"status": "ready",
|
|
253
|
+
"schema_version": SCHEMA_VERSION,
|
|
254
|
+
"action": decision,
|
|
255
|
+
"task_id": task_id,
|
|
256
|
+
"reason": reason,
|
|
257
|
+
"budget": {
|
|
258
|
+
"current_token_count": current_token_count,
|
|
259
|
+
"max_allowed_tokens": max_allowed_tokens,
|
|
260
|
+
"utilization_pct": utilization_pct,
|
|
261
|
+
"tokens_over_budget": tokens_over_budget,
|
|
262
|
+
"soft_threshold_pct": soft_threshold_pct,
|
|
263
|
+
"soft_limit_tokens": soft_limit,
|
|
264
|
+
},
|
|
265
|
+
"cost": {
|
|
266
|
+
"incurred_usd": cost_usd,
|
|
267
|
+
"model_id": model_id,
|
|
268
|
+
"pricing_source": "data/model_pricing.json",
|
|
269
|
+
"pricing_last_updated": pricing.get("last_updated"),
|
|
270
|
+
},
|
|
271
|
+
"roi": roi,
|
|
272
|
+
"metadata": {
|
|
273
|
+
"warnings": warnings,
|
|
274
|
+
"turn_id": turn_id or None,
|
|
275
|
+
"cache_hit": False,
|
|
276
|
+
"host_responsibility": (
|
|
277
|
+
"The host loop must stop when action is FORCE_TERMINATE."
|
|
278
|
+
),
|
|
279
|
+
},
|
|
280
|
+
"timestamp": utc_now_iso(),
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if turn_id:
|
|
284
|
+
turn_cache[cache_key] = dict(result)
|
|
285
|
+
|
|
286
|
+
return result
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Token Limiter",
|
|
3
|
+
"description": "Budget gate for autonomous agent loops. Returns CONTINUE, WARN, or FORCE_TERMINATE.",
|
|
4
|
+
"issuer": {
|
|
5
|
+
"name": "Ross Peili",
|
|
6
|
+
"email": "vpeilivanidis@gmail.com",
|
|
7
|
+
"github": "rosspeili",
|
|
8
|
+
"org": "ARPAHLS"
|
|
9
|
+
},
|
|
10
|
+
"icon": "activity",
|
|
11
|
+
"color": "orange",
|
|
12
|
+
"ui_schema": {
|
|
13
|
+
"type": "card",
|
|
14
|
+
"fields": [
|
|
15
|
+
{"key": "task_id", "label": "Task"},
|
|
16
|
+
{"key": "action", "label": "Decision", "type": "status_badge"},
|
|
17
|
+
{"key": "budget.utilization_pct", "label": "Utilization %", "type": "percent"},
|
|
18
|
+
{"key": "budget.current_token_count", "label": "Tokens Used", "type": "count"},
|
|
19
|
+
{"key": "cost.incurred_usd", "label": "Est. Cost (USD)", "type": "currency"}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "1.0",
|
|
3
|
+
"last_updated": "2026-05-24",
|
|
4
|
+
"currency": "USD",
|
|
5
|
+
"notes": "Indicative public list prices for cost estimates only. Not billing-grade.",
|
|
6
|
+
"models": {
|
|
7
|
+
"gpt-4o": {
|
|
8
|
+
"input_usd_per_1m": 2.5,
|
|
9
|
+
"output_usd_per_1m": 10.0
|
|
10
|
+
},
|
|
11
|
+
"gpt-4o-mini": {
|
|
12
|
+
"input_usd_per_1m": 0.15,
|
|
13
|
+
"output_usd_per_1m": 0.6
|
|
14
|
+
},
|
|
15
|
+
"claude-3-5-sonnet-latest": {
|
|
16
|
+
"input_usd_per_1m": 3.0,
|
|
17
|
+
"output_usd_per_1m": 15.0
|
|
18
|
+
},
|
|
19
|
+
"claude-3-5-haiku-latest": {
|
|
20
|
+
"input_usd_per_1m": 0.8,
|
|
21
|
+
"output_usd_per_1m": 4.0
|
|
22
|
+
},
|
|
23
|
+
"gemini-2.5-flash": {
|
|
24
|
+
"input_usd_per_1m": 0.15,
|
|
25
|
+
"output_usd_per_1m": 0.6
|
|
26
|
+
},
|
|
27
|
+
"gemini-2.5-flash-lite": {
|
|
28
|
+
"input_usd_per_1m": 0.075,
|
|
29
|
+
"output_usd_per_1m": 0.3
|
|
30
|
+
},
|
|
31
|
+
"deepseek-chat": {
|
|
32
|
+
"input_usd_per_1m": 0.27,
|
|
33
|
+
"output_usd_per_1m": 1.1
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"fallback": {
|
|
37
|
+
"blended_usd_per_1m": 5.0
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Cognition Instructions: Token Limiter
|
|
2
|
+
|
|
3
|
+
You have access to the `monitoring/token_limiter` tool.
|
|
4
|
+
|
|
5
|
+
This skill is a **budget gate** for long-running autonomous loops. It does **not** stop the loop itself. After each model turn (or before the next one), call it with the **cumulative** token count for the task. If the result is `FORCE_TERMINATE`, you must **stop the loop immediately** and report the reason to the operator.
|
|
6
|
+
|
|
7
|
+
## When to use this skill
|
|
8
|
+
|
|
9
|
+
- Before starting a high-risk autonomous task (scraping, multi-step research, codegen loops).
|
|
10
|
+
- After **every** model turn in a loop where token spend must stay bounded.
|
|
11
|
+
- When the operator sets a maximum token budget for a task.
|
|
12
|
+
|
|
13
|
+
## Required host behavior
|
|
14
|
+
|
|
15
|
+
1. Track `task_id` for the session and pass **cumulative** `current_token_count` (not just the last turn delta unless you maintain the running total yourself).
|
|
16
|
+
2. Set `max_allowed_tokens` from operator policy or task metadata.
|
|
17
|
+
3. On `action: WARN`, consider narrowing scope, compressing context, or asking the operator before continuing.
|
|
18
|
+
4. On `action: FORCE_TERMINATE`, **do not** call the main model again for this task. Return the skill payload to the dashboard or user.
|
|
19
|
+
|
|
20
|
+
## Parameters
|
|
21
|
+
|
|
22
|
+
| Field | Required | Notes |
|
|
23
|
+
| :--- | :--- | :--- |
|
|
24
|
+
| `task_id` | Yes | Stable task identifier |
|
|
25
|
+
| `current_token_count` | Yes (check) | Cumulative tokens used so far |
|
|
26
|
+
| `max_allowed_tokens` | Yes (check) | Hard ceiling |
|
|
27
|
+
| `turn_id` | No | Use for idempotent retries of the same turn |
|
|
28
|
+
| `model_id` | No | Enables indicative USD cost in the response |
|
|
29
|
+
| `soft_threshold_pct` | No | Default 80; triggers WARN before hard limit |
|
|
30
|
+
|
|
31
|
+
## ROI scaffold (v2, not enforced)
|
|
32
|
+
|
|
33
|
+
Optional fields `roi_value_usd`, `expected_outcome`, and `outcome_delivered` are accepted for future ROI logic. **v1 never terminates on ROI alone.** Token limits are the only termination trigger today.
|
|
34
|
+
|
|
35
|
+
## Interpreting the response
|
|
36
|
+
|
|
37
|
+
- `CONTINUE`: Under the soft threshold. Proceed with the next turn.
|
|
38
|
+
- `WARN`: Approaching the limit. Tighten scope or warn the operator.
|
|
39
|
+
- `FORCE_TERMINATE`: Hard limit reached. Stop the loop and surface `reason`.
|
|
40
|
+
- `RESET`: Administrative action clearing cached turn results for a `task_id`.
|
|
41
|
+
|
|
42
|
+
Cost figures in `cost.incurred_usd` are **indicative** estimates from bundled list prices, not invoice amounts.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
name: "monitoring/token_limiter"
|
|
2
|
+
version: "1.0.0"
|
|
3
|
+
description: "Deterministic token budget gate for autonomous agent loops. Returns CONTINUE, WARN, or FORCE_TERMINATE so the host loop can stop runaway tasks before they exhaust context or budget."
|
|
4
|
+
short_description: "Token budget gate: CONTINUE, WARN, or FORCE_TERMINATE for agent loops."
|
|
5
|
+
issuer:
|
|
6
|
+
name: Ross Peili
|
|
7
|
+
email: vpeilivanidis@gmail.com
|
|
8
|
+
github: rosspeili
|
|
9
|
+
org: ARPAHLS
|
|
10
|
+
category: "monitoring"
|
|
11
|
+
requirements: []
|
|
12
|
+
parameters:
|
|
13
|
+
type: object
|
|
14
|
+
properties:
|
|
15
|
+
action:
|
|
16
|
+
type: string
|
|
17
|
+
description: "Workflow action: check (default) evaluates the budget; reset clears cached turn results for task_id."
|
|
18
|
+
enum:
|
|
19
|
+
- check
|
|
20
|
+
- reset
|
|
21
|
+
task_id:
|
|
22
|
+
type: string
|
|
23
|
+
description: "Stable identifier for the autonomous task or agent session."
|
|
24
|
+
current_token_count:
|
|
25
|
+
type: integer
|
|
26
|
+
description: "Cumulative token count for the task so far. The host loop must track and pass the running total."
|
|
27
|
+
max_allowed_tokens:
|
|
28
|
+
type: integer
|
|
29
|
+
description: "Hard token ceiling for the task. At or above this value the skill returns FORCE_TERMINATE."
|
|
30
|
+
soft_threshold_pct:
|
|
31
|
+
type: number
|
|
32
|
+
description: "Optional warning threshold as a percentage of max_allowed_tokens. Default 80."
|
|
33
|
+
turn_id:
|
|
34
|
+
type: string
|
|
35
|
+
description: "Optional turn identifier for idempotent retries. Same task_id, turn_id, and counts return the cached decision."
|
|
36
|
+
model_id:
|
|
37
|
+
type: string
|
|
38
|
+
description: "Optional model name for indicative USD cost estimates (for example gpt-4o, gemini-2.5-flash)."
|
|
39
|
+
input_tokens:
|
|
40
|
+
type: integer
|
|
41
|
+
description: "Optional input tokens for this turn or cumulative split when estimating cost."
|
|
42
|
+
output_tokens:
|
|
43
|
+
type: integer
|
|
44
|
+
description: "Optional output tokens for this turn or cumulative split when estimating cost."
|
|
45
|
+
roi_value_usd:
|
|
46
|
+
type: number
|
|
47
|
+
description: "Optional scaffold for future ROI checks. Not enforced in v1."
|
|
48
|
+
expected_outcome:
|
|
49
|
+
type: string
|
|
50
|
+
description: "Optional scaffold describing the deliverable expected from the task (v2 ROI)."
|
|
51
|
+
outcome_delivered:
|
|
52
|
+
type: boolean
|
|
53
|
+
description: "Optional scaffold indicating whether the expected outcome was delivered (v2 ROI)."
|
|
54
|
+
required: []
|
|
55
|
+
outputs:
|
|
56
|
+
status:
|
|
57
|
+
type: string
|
|
58
|
+
description: "ready or error."
|
|
59
|
+
action:
|
|
60
|
+
type: string
|
|
61
|
+
description: "CONTINUE, WARN, FORCE_TERMINATE, or RESET."
|
|
62
|
+
reason:
|
|
63
|
+
type: string
|
|
64
|
+
description: "Human-readable explanation of the decision."
|
|
65
|
+
constitution: |
|
|
66
|
+
1. INFORMATIONAL ONLY: Output is a budget signal, not billing authority or legal clearance.
|
|
67
|
+
2. HOST RESPONSIBILITY: The calling loop must stop when action is FORCE_TERMINATE.
|
|
68
|
+
3. DETERMINISM: Same inputs produce the same decision (except timestamp).
|
|
69
|
+
4. PRIVACY: Do not accept or store prompt content; only metrics and task identifiers.
|
|
70
|
+
5. TOKEN FIRST: v1 terminates only on token hard limits. ROI fields are scaffold-only.
|
|
71
|
+
6. NO SIDE EFFECTS: Do not spawn background tasks, network calls, or process kills.
|
|
72
|
+
presentation:
|
|
73
|
+
icon: "activity"
|
|
74
|
+
color: "#E67E22"
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
import os
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
from skillware.core.base_skill import BaseSkill
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _import_budget():
|
|
9
|
+
try:
|
|
10
|
+
from . import budget as budget_module # type: ignore[import-not-found]
|
|
11
|
+
except ImportError:
|
|
12
|
+
budget_path = os.path.join(os.path.dirname(__file__), "budget.py")
|
|
13
|
+
spec = importlib.util.spec_from_file_location(
|
|
14
|
+
"token_limiter_budget", budget_path
|
|
15
|
+
)
|
|
16
|
+
if spec is None or spec.loader is None:
|
|
17
|
+
raise ImportError(f"Cannot load budget module from {budget_path}")
|
|
18
|
+
budget_module = importlib.util.module_from_spec(spec)
|
|
19
|
+
spec.loader.exec_module(budget_module)
|
|
20
|
+
return budget_module
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_budget = _import_budget()
|
|
24
|
+
evaluate_budget = _budget.evaluate_budget
|
|
25
|
+
load_pricing = _budget.load_pricing
|
|
26
|
+
SCHEMA_VERSION = _budget.SCHEMA_VERSION
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TokenLimiterSkill(BaseSkill):
|
|
30
|
+
"""
|
|
31
|
+
Deterministic token budget gate for autonomous agent loops.
|
|
32
|
+
|
|
33
|
+
The skill does not terminate host processes or provider sessions itself.
|
|
34
|
+
It returns CONTINUE, WARN, or FORCE_TERMINATE and the host loop must act.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
|
38
|
+
super().__init__(config)
|
|
39
|
+
self._data_dir = os.path.join(os.path.dirname(__file__), "data")
|
|
40
|
+
self._pricing = load_pricing(self._data_dir)
|
|
41
|
+
self._turn_cache: Dict[str, Dict[str, Any]] = {}
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def manifest(self) -> Dict[str, Any]:
|
|
45
|
+
manifest_path = os.path.join(os.path.dirname(__file__), "manifest.yaml")
|
|
46
|
+
if os.path.exists(manifest_path):
|
|
47
|
+
import yaml
|
|
48
|
+
|
|
49
|
+
with open(manifest_path, "r", encoding="utf-8") as handle:
|
|
50
|
+
return yaml.safe_load(handle)
|
|
51
|
+
return {}
|
|
52
|
+
|
|
53
|
+
def execute(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
54
|
+
try:
|
|
55
|
+
return evaluate_budget(params, self._pricing, self._turn_cache)
|
|
56
|
+
except Exception as exc:
|
|
57
|
+
return {
|
|
58
|
+
"status": "error",
|
|
59
|
+
"message": f"Token budget evaluation failed: {exc}",
|
|
60
|
+
}
|