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.
Files changed (131) hide show
  1. {skillware-0.3.9/skillware.egg-info → skillware-0.4.0}/PKG-INFO +14 -2
  2. {skillware-0.3.9 → skillware-0.4.0}/README.md +13 -1
  3. {skillware-0.3.9 → skillware-0.4.0}/pyproject.toml +1 -1
  4. skillware-0.4.0/skills/monitoring/__init__.py +1 -0
  5. skillware-0.4.0/skills/monitoring/token_limiter/__init__.py +1 -0
  6. skillware-0.4.0/skills/monitoring/token_limiter/budget.py +286 -0
  7. skillware-0.4.0/skills/monitoring/token_limiter/card.json +22 -0
  8. skillware-0.4.0/skills/monitoring/token_limiter/data/model_pricing.json +39 -0
  9. skillware-0.4.0/skills/monitoring/token_limiter/instructions.md +42 -0
  10. skillware-0.4.0/skills/monitoring/token_limiter/manifest.yaml +74 -0
  11. skillware-0.4.0/skills/monitoring/token_limiter/skill.py +60 -0
  12. skillware-0.4.0/skills/monitoring/token_limiter/test_skill.py +174 -0
  13. {skillware-0.3.9 → skillware-0.4.0}/skillware/cli.py +7 -2
  14. {skillware-0.3.9 → skillware-0.4.0/skillware.egg-info}/PKG-INFO +14 -2
  15. {skillware-0.3.9 → skillware-0.4.0}/skillware.egg-info/SOURCES.txt +10 -0
  16. skillware-0.4.0/tests/test_registry_docs.py +166 -0
  17. {skillware-0.3.9 → skillware-0.4.0}/LICENSE +0 -0
  18. {skillware-0.3.9 → skillware-0.4.0}/MANIFEST.in +0 -0
  19. {skillware-0.3.9 → skillware-0.4.0}/setup.cfg +0 -0
  20. {skillware-0.3.9 → skillware-0.4.0}/skills/__init__.py +0 -0
  21. {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/__init__.py +0 -0
  22. {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/mica_module/__init__.py +0 -0
  23. {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/mica_module/card.json +0 -0
  24. {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/mica_module/instructions.md +0 -0
  25. {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/mica_module/manifest.yaml +0 -0
  26. {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/mica_module/mica_corpus.json +0 -0
  27. {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/mica_module/skill.py +0 -0
  28. {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/mica_module/test_skill.py +0 -0
  29. {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/pii_masker/__init__.py +0 -0
  30. {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/pii_masker/card.json +0 -0
  31. {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/pii_masker/instructions.md +0 -0
  32. {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/pii_masker/manifest.yaml +0 -0
  33. {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/pii_masker/skill.py +0 -0
  34. {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/pii_masker/test_skill.py +0 -0
  35. {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/tos_evaluator/__init__.py +0 -0
  36. {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/tos_evaluator/card.json +0 -0
  37. {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/tos_evaluator/instructions.md +0 -0
  38. {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/tos_evaluator/manifest.yaml +0 -0
  39. {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/tos_evaluator/skill.py +0 -0
  40. {skillware-0.3.9 → skillware-0.4.0}/skills/compliance/tos_evaluator/test_skill.py +0 -0
  41. {skillware-0.3.9 → skillware-0.4.0}/skills/data_engineering/__init__.py +0 -0
  42. {skillware-0.3.9 → skillware-0.4.0}/skills/data_engineering/novelty_extractor/__init__.py +0 -0
  43. {skillware-0.3.9 → skillware-0.4.0}/skills/data_engineering/novelty_extractor/card.json +0 -0
  44. {skillware-0.3.9 → skillware-0.4.0}/skills/data_engineering/novelty_extractor/instructions.md +0 -0
  45. {skillware-0.3.9 → skillware-0.4.0}/skills/data_engineering/novelty_extractor/manifest.yaml +0 -0
  46. {skillware-0.3.9 → skillware-0.4.0}/skills/data_engineering/novelty_extractor/skill.py +0 -0
  47. {skillware-0.3.9 → skillware-0.4.0}/skills/data_engineering/novelty_extractor/test_skill.py +0 -0
  48. {skillware-0.3.9 → skillware-0.4.0}/skills/data_engineering/synthetic_generator/__init__.py +0 -0
  49. {skillware-0.3.9 → skillware-0.4.0}/skills/data_engineering/synthetic_generator/card.json +0 -0
  50. {skillware-0.3.9 → skillware-0.4.0}/skills/data_engineering/synthetic_generator/instructions.md +0 -0
  51. {skillware-0.3.9 → skillware-0.4.0}/skills/data_engineering/synthetic_generator/manifest.yaml +0 -0
  52. {skillware-0.3.9 → skillware-0.4.0}/skills/data_engineering/synthetic_generator/skill.py +0 -0
  53. {skillware-0.3.9 → skillware-0.4.0}/skills/data_engineering/synthetic_generator/test_skill.py +0 -0
  54. {skillware-0.3.9 → skillware-0.4.0}/skills/defi/__init__.py +0 -0
  55. {skillware-0.3.9 → skillware-0.4.0}/skills/defi/evm_tx_handler/__init__.py +0 -0
  56. {skillware-0.3.9 → skillware-0.4.0}/skills/defi/evm_tx_handler/abis.py +0 -0
  57. {skillware-0.3.9 → skillware-0.4.0}/skills/defi/evm_tx_handler/card.json +0 -0
  58. {skillware-0.3.9 → skillware-0.4.0}/skills/defi/evm_tx_handler/config.yaml.example +0 -0
  59. {skillware-0.3.9 → skillware-0.4.0}/skills/defi/evm_tx_handler/data/addressbook.yaml +0 -0
  60. {skillware-0.3.9 → skillware-0.4.0}/skills/defi/evm_tx_handler/data/chains.yaml +0 -0
  61. {skillware-0.3.9 → skillware-0.4.0}/skills/defi/evm_tx_handler/data/tokens.yaml +0 -0
  62. {skillware-0.3.9 → skillware-0.4.0}/skills/defi/evm_tx_handler/instructions.md +0 -0
  63. {skillware-0.3.9 → skillware-0.4.0}/skills/defi/evm_tx_handler/manifest.yaml +0 -0
  64. {skillware-0.3.9 → skillware-0.4.0}/skills/defi/evm_tx_handler/skill.py +0 -0
  65. {skillware-0.3.9 → skillware-0.4.0}/skills/defi/evm_tx_handler/test_skill.py +0 -0
  66. {skillware-0.3.9 → skillware-0.4.0}/skills/dev_tools/__init__.py +0 -0
  67. {skillware-0.3.9 → skillware-0.4.0}/skills/dev_tools/issue_resolver/__init__.py +0 -0
  68. {skillware-0.3.9 → skillware-0.4.0}/skills/dev_tools/issue_resolver/card.json +0 -0
  69. {skillware-0.3.9 → skillware-0.4.0}/skills/dev_tools/issue_resolver/instructions.md +0 -0
  70. {skillware-0.3.9 → skillware-0.4.0}/skills/dev_tools/issue_resolver/manifest.yaml +0 -0
  71. {skillware-0.3.9 → skillware-0.4.0}/skills/dev_tools/issue_resolver/skill.py +0 -0
  72. {skillware-0.3.9 → skillware-0.4.0}/skills/dev_tools/issue_resolver/test_skill.py +0 -0
  73. {skillware-0.3.9 → skillware-0.4.0}/skills/dev_tools/issue_resolver/workflow.py +0 -0
  74. {skillware-0.3.9 → skillware-0.4.0}/skills/finance/__init__.py +0 -0
  75. {skillware-0.3.9 → skillware-0.4.0}/skills/finance/wallet_screening/__init__.py +0 -0
  76. {skillware-0.3.9 → skillware-0.4.0}/skills/finance/wallet_screening/card.json +0 -0
  77. {skillware-0.3.9 → skillware-0.4.0}/skills/finance/wallet_screening/data/entities.ftm.json +0 -0
  78. {skillware-0.3.9 → skillware-0.4.0}/skills/finance/wallet_screening/data/malicious_scs_2025.json +0 -0
  79. {skillware-0.3.9 → skillware-0.4.0}/skills/finance/wallet_screening/data/normalized_fbi_lazarus_20250722_180019.json +0 -0
  80. {skillware-0.3.9 → skillware-0.4.0}/skills/finance/wallet_screening/data/normalized_israel_nbctf_20250722_180019.json +0 -0
  81. {skillware-0.3.9 → skillware-0.4.0}/skills/finance/wallet_screening/data/normalized_uniswap_trm.json +0 -0
  82. {skillware-0.3.9 → skillware-0.4.0}/skills/finance/wallet_screening/instructions.md +0 -0
  83. {skillware-0.3.9 → skillware-0.4.0}/skills/finance/wallet_screening/maintenance/normalization_tool.py +0 -0
  84. {skillware-0.3.9 → skillware-0.4.0}/skills/finance/wallet_screening/maintenance/normalize_uniswap_trm.py +0 -0
  85. {skillware-0.3.9 → skillware-0.4.0}/skills/finance/wallet_screening/manifest.yaml +0 -0
  86. {skillware-0.3.9 → skillware-0.4.0}/skills/finance/wallet_screening/skill.py +0 -0
  87. {skillware-0.3.9 → skillware-0.4.0}/skills/finance/wallet_screening/test_skill.py +0 -0
  88. {skillware-0.3.9 → skillware-0.4.0}/skills/office/__init__.py +0 -0
  89. {skillware-0.3.9 → skillware-0.4.0}/skills/office/pdf_form_filler/__init__.py +0 -0
  90. {skillware-0.3.9 → skillware-0.4.0}/skills/office/pdf_form_filler/card.json +0 -0
  91. {skillware-0.3.9 → skillware-0.4.0}/skills/office/pdf_form_filler/instructions.md +0 -0
  92. {skillware-0.3.9 → skillware-0.4.0}/skills/office/pdf_form_filler/manifest.yaml +0 -0
  93. {skillware-0.3.9 → skillware-0.4.0}/skills/office/pdf_form_filler/skill.py +0 -0
  94. {skillware-0.3.9 → skillware-0.4.0}/skills/office/pdf_form_filler/test_skill.py +0 -0
  95. {skillware-0.3.9 → skillware-0.4.0}/skills/office/pdf_form_filler/utils.py +0 -0
  96. {skillware-0.3.9 → skillware-0.4.0}/skills/optimization/__init__.py +0 -0
  97. {skillware-0.3.9 → skillware-0.4.0}/skills/optimization/prompt_rewriter/__init__.py +0 -0
  98. {skillware-0.3.9 → skillware-0.4.0}/skills/optimization/prompt_rewriter/card.json +0 -0
  99. {skillware-0.3.9 → skillware-0.4.0}/skills/optimization/prompt_rewriter/instructions.md +0 -0
  100. {skillware-0.3.9 → skillware-0.4.0}/skills/optimization/prompt_rewriter/manifest.yaml +0 -0
  101. {skillware-0.3.9 → skillware-0.4.0}/skills/optimization/prompt_rewriter/skill.py +0 -0
  102. {skillware-0.3.9 → skillware-0.4.0}/skills/optimization/prompt_rewriter/test_skill.py +0 -0
  103. {skillware-0.3.9 → skillware-0.4.0}/skills/wellness/__init__.py +0 -0
  104. {skillware-0.3.9 → skillware-0.4.0}/skills/wellness/mental_coach/__init__.py +0 -0
  105. {skillware-0.3.9 → skillware-0.4.0}/skills/wellness/mental_coach/card.json +0 -0
  106. {skillware-0.3.9 → skillware-0.4.0}/skills/wellness/mental_coach/constraints.py +0 -0
  107. {skillware-0.3.9 → skillware-0.4.0}/skills/wellness/mental_coach/crisis_gate.py +0 -0
  108. {skillware-0.3.9 → skillware-0.4.0}/skills/wellness/mental_coach/instructions.md +0 -0
  109. {skillware-0.3.9 → skillware-0.4.0}/skills/wellness/mental_coach/kb/corpus.json +0 -0
  110. {skillware-0.3.9 → skillware-0.4.0}/skills/wellness/mental_coach/kb/crisis_resources.json +0 -0
  111. {skillware-0.3.9 → skillware-0.4.0}/skills/wellness/mental_coach/kb/hard_constraints.yaml +0 -0
  112. {skillware-0.3.9 → skillware-0.4.0}/skills/wellness/mental_coach/kb_provider.py +0 -0
  113. {skillware-0.3.9 → skillware-0.4.0}/skills/wellness/mental_coach/manifest.yaml +0 -0
  114. {skillware-0.3.9 → skillware-0.4.0}/skills/wellness/mental_coach/resources.py +0 -0
  115. {skillware-0.3.9 → skillware-0.4.0}/skills/wellness/mental_coach/skill.py +0 -0
  116. {skillware-0.3.9 → skillware-0.4.0}/skills/wellness/mental_coach/test_skill.py +0 -0
  117. {skillware-0.3.9 → skillware-0.4.0}/skillware/__init__.py +0 -0
  118. {skillware-0.3.9 → skillware-0.4.0}/skillware/__main__.py +0 -0
  119. {skillware-0.3.9 → skillware-0.4.0}/skillware/core/__init__.py +0 -0
  120. {skillware-0.3.9 → skillware-0.4.0}/skillware/core/base_skill.py +0 -0
  121. {skillware-0.3.9 → skillware-0.4.0}/skillware/core/env.py +0 -0
  122. {skillware-0.3.9 → skillware-0.4.0}/skillware/core/loader.py +0 -0
  123. {skillware-0.3.9 → skillware-0.4.0}/skillware/version_policy.py +0 -0
  124. {skillware-0.3.9 → skillware-0.4.0}/skillware.egg-info/dependency_links.txt +0 -0
  125. {skillware-0.3.9 → skillware-0.4.0}/skillware.egg-info/entry_points.txt +0 -0
  126. {skillware-0.3.9 → skillware-0.4.0}/skillware.egg-info/requires.txt +0 -0
  127. {skillware-0.3.9 → skillware-0.4.0}/skillware.egg-info/top_level.txt +0 -0
  128. {skillware-0.3.9 → skillware-0.4.0}/tests/test_cli.py +0 -0
  129. {skillware-0.3.9 → skillware-0.4.0}/tests/test_loader.py +0 -0
  130. {skillware-0.3.9 → skillware-0.4.0}/tests/test_skill_issuer.py +0 -0
  131. {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.9
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:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "skillware"
7
- version = "0.3.9"
7
+ version = "0.4.0"
8
8
  description = "A framework for modular, self-contained AI skills."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -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
+ }