axor-claude 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.
- axor_claude-0.1.0/.github/workflows/ci.yml +120 -0
- axor_claude-0.1.0/.gitignore +12 -0
- axor_claude-0.1.0/CHANGELOG.md +17 -0
- axor_claude-0.1.0/CONTRIBUTING.md +56 -0
- axor_claude-0.1.0/LICENSE +21 -0
- axor_claude-0.1.0/PKG-INFO +400 -0
- axor_claude-0.1.0/README.md +375 -0
- axor_claude-0.1.0/axor_claude/__init__.py +81 -0
- axor_claude-0.1.0/axor_claude/events.py +215 -0
- axor_claude-0.1.0/axor_claude/executor.py +349 -0
- axor_claude-0.1.0/axor_claude/extensions/__init__.py +0 -0
- axor_claude-0.1.0/axor_claude/extensions/plugin_loader.py +155 -0
- axor_claude-0.1.0/axor_claude/extensions/skill_loader.py +88 -0
- axor_claude-0.1.0/axor_claude/normalizer.py +111 -0
- axor_claude-0.1.0/axor_claude/tool_definitions.py +204 -0
- axor_claude-0.1.0/axor_claude/tools/__init__.py +0 -0
- axor_claude-0.1.0/axor_claude/tools/bash.py +123 -0
- axor_claude-0.1.0/axor_claude/tools/glob.py +102 -0
- axor_claude-0.1.0/axor_claude/tools/read.py +99 -0
- axor_claude-0.1.0/axor_claude/tools/search.py +187 -0
- axor_claude-0.1.0/axor_claude/tools/write.py +81 -0
- axor_claude-0.1.0/pyproject.toml +51 -0
- axor_claude-0.1.0/tests/__init__.py +0 -0
- axor_claude-0.1.0/tests/conftest.py +43 -0
- axor_claude-0.1.0/tests/integration/__init__.py +0 -0
- axor_claude-0.1.0/tests/integration/test_integration.py +103 -0
- axor_claude-0.1.0/tests/unit/__init__.py +0 -0
- axor_claude-0.1.0/tests/unit/test_events.py +260 -0
- axor_claude-0.1.0/tests/unit/test_normalizer_and_defs.py +166 -0
- axor_claude-0.1.0/tests/unit/tools/__init__.py +0 -0
- axor_claude-0.1.0/tests/unit/tools/test_tools.py +324 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
name: CI/CD
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
tags: ["v*.*.*"]
|
|
7
|
+
pull_request:
|
|
8
|
+
branches: [main]
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
test:
|
|
12
|
+
name: Test (Python ${{ matrix.python-version }})
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
strategy:
|
|
15
|
+
matrix:
|
|
16
|
+
python-version: ["3.11", "3.12"]
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Checkout axor-core
|
|
22
|
+
uses: actions/checkout@v4
|
|
23
|
+
with:
|
|
24
|
+
repository: ${{ github.repository_owner }}/axor-core
|
|
25
|
+
path: axor-core
|
|
26
|
+
|
|
27
|
+
- uses: actions/setup-python@v5
|
|
28
|
+
with:
|
|
29
|
+
python-version: ${{ matrix.python-version }}
|
|
30
|
+
cache: pip
|
|
31
|
+
|
|
32
|
+
- name: Install
|
|
33
|
+
run: |
|
|
34
|
+
pip install -e axor-core/
|
|
35
|
+
pip install -e ".[dev]"
|
|
36
|
+
|
|
37
|
+
- name: Static analysis — no governance in adapter
|
|
38
|
+
run: |
|
|
39
|
+
python - << 'EOF'
|
|
40
|
+
import re, os, sys
|
|
41
|
+
gov_patterns = [r'\bPolicySelector\b', r'\bCapabilityResolver\b', r'\bIntentLoop\b']
|
|
42
|
+
anthropic_allowed = {"axor_claude/executor.py", "axor_claude/events.py"}
|
|
43
|
+
issues = []
|
|
44
|
+
for root, dirs, files in os.walk("axor_claude"):
|
|
45
|
+
dirs[:] = [d for d in dirs if not d.startswith("__")]
|
|
46
|
+
for f in files:
|
|
47
|
+
if not f.endswith(".py"):
|
|
48
|
+
continue
|
|
49
|
+
path = os.path.join(root, f)
|
|
50
|
+
src = open(path).read()
|
|
51
|
+
rel = path.replace("\\", "/")
|
|
52
|
+
for line in src.splitlines():
|
|
53
|
+
stripped = line.strip()
|
|
54
|
+
if stripped.startswith("#"):
|
|
55
|
+
continue
|
|
56
|
+
for pat in gov_patterns:
|
|
57
|
+
if re.search(pat, stripped):
|
|
58
|
+
issues.append(f"GOV_LOGIC: {rel}: {stripped[:60]}")
|
|
59
|
+
if rel not in anthropic_allowed:
|
|
60
|
+
for line in src.splitlines():
|
|
61
|
+
if re.match(r"\s*(import anthropic|from anthropic)", line):
|
|
62
|
+
issues.append(f"ANTHROPIC_OUTSIDE_EXECUTOR: {rel}")
|
|
63
|
+
if issues:
|
|
64
|
+
print("\n".join(issues))
|
|
65
|
+
sys.exit(1)
|
|
66
|
+
print("✓ adapter architecture clean")
|
|
67
|
+
EOF
|
|
68
|
+
|
|
69
|
+
- name: Run unit tests
|
|
70
|
+
run: pytest tests/unit/ -v --tb=short
|
|
71
|
+
|
|
72
|
+
publish:
|
|
73
|
+
name: Publish to PyPI
|
|
74
|
+
needs: test
|
|
75
|
+
runs-on: ubuntu-latest
|
|
76
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
77
|
+
environment: pypi
|
|
78
|
+
|
|
79
|
+
permissions:
|
|
80
|
+
id-token: write
|
|
81
|
+
|
|
82
|
+
steps:
|
|
83
|
+
- uses: actions/checkout@v4
|
|
84
|
+
|
|
85
|
+
- uses: actions/setup-python@v5
|
|
86
|
+
with:
|
|
87
|
+
python-version: "3.12"
|
|
88
|
+
|
|
89
|
+
- name: Verify tag matches package version
|
|
90
|
+
run: |
|
|
91
|
+
python - << 'EOF'
|
|
92
|
+
import pathlib
|
|
93
|
+
import re
|
|
94
|
+
import sys
|
|
95
|
+
import tomllib
|
|
96
|
+
|
|
97
|
+
ref = "${{ github.ref_name }}"
|
|
98
|
+
m = re.fullmatch(r"v(\d+\.\d+\.\d+)", ref)
|
|
99
|
+
if not m:
|
|
100
|
+
print(f"Tag {ref!r} must match vX.Y.Z")
|
|
101
|
+
sys.exit(1)
|
|
102
|
+
|
|
103
|
+
tag_version = m.group(1)
|
|
104
|
+
data = tomllib.loads(pathlib.Path("pyproject.toml").read_text(encoding="utf-8"))
|
|
105
|
+
pkg_version = data["project"]["version"]
|
|
106
|
+
|
|
107
|
+
if tag_version != pkg_version:
|
|
108
|
+
print(f"Version mismatch: tag={tag_version}, pyproject={pkg_version}")
|
|
109
|
+
sys.exit(1)
|
|
110
|
+
|
|
111
|
+
print(f"Version check passed: {pkg_version}")
|
|
112
|
+
EOF
|
|
113
|
+
|
|
114
|
+
- name: Build
|
|
115
|
+
run: |
|
|
116
|
+
pip install hatchling build
|
|
117
|
+
python -m build
|
|
118
|
+
|
|
119
|
+
- name: Publish to PyPI
|
|
120
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — 2025-04-12
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Core governance kernel (axor-core)
|
|
9
|
+
- Claude Code adapter (axor-claude)
|
|
10
|
+
- CLI with interactive REPL (axor-cli)
|
|
11
|
+
- Dynamic policy selection (7-policy matrix)
|
|
12
|
+
- ContextManager with 11 waste categories
|
|
13
|
+
- ToolResultBus for async tool loop
|
|
14
|
+
- Federation via spawn_child with child_executor
|
|
15
|
+
- CancelToken cooperative cancellation
|
|
16
|
+
- BudgetPolicyEngine (60/80/90/95% thresholds)
|
|
17
|
+
- TraceCollector with lineage (17 event kinds)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Contributing to axor-claude
|
|
2
|
+
|
|
3
|
+
## Architecture principles
|
|
4
|
+
|
|
5
|
+
axor-claude is an **adapter** — it translates between axor-core contracts
|
|
6
|
+
and the Anthropic SDK. It must not contain governance logic.
|
|
7
|
+
|
|
8
|
+
Key rules:
|
|
9
|
+
- `axor_claude/` never defines policy semantics
|
|
10
|
+
- `axor_claude/` never imports axor-core internals beyond `contracts/`
|
|
11
|
+
and `capability.executor.ToolHandler`
|
|
12
|
+
- Tool handlers do only I/O — no business logic
|
|
13
|
+
- `StreamNormalizer` is the only place that knows Anthropic SDK event structure
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
git clone https://github.com/your-org/axor-claude
|
|
19
|
+
cd axor-claude
|
|
20
|
+
python -m venv .venv && source .venv/bin/activate
|
|
21
|
+
pip install -e ".[dev]"
|
|
22
|
+
pip install axor-core # or: pip install -e ../axor-core
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Running tests
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pytest tests/unit/ # no API key needed
|
|
29
|
+
pytest tests/integration/ -m integration # requires ANTHROPIC_API_KEY
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Adding a tool
|
|
33
|
+
|
|
34
|
+
1. Create `axor_claude/tools/mytool.py` extending `ToolHandler`
|
|
35
|
+
2. Add Anthropic tool definition to `tool_definitions.py`
|
|
36
|
+
3. Register in `__init__.py` and `make_session()`
|
|
37
|
+
4. Add unit tests in `tests/unit/tools/test_mytool.py`
|
|
38
|
+
|
|
39
|
+
Tool handlers must:
|
|
40
|
+
- Be async
|
|
41
|
+
- Raise `ValueError` for missing required args
|
|
42
|
+
- Never catch all exceptions — let callers handle
|
|
43
|
+
|
|
44
|
+
## Adding an extension loader
|
|
45
|
+
|
|
46
|
+
Implement `ExtensionLoader.load() -> ExtensionBundle`.
|
|
47
|
+
Return `ExtensionFragment` objects — never raw file contents
|
|
48
|
+
without going through the loader abstraction.
|
|
49
|
+
|
|
50
|
+
## Pull request checklist
|
|
51
|
+
|
|
52
|
+
- [ ] No governance logic in adapter code
|
|
53
|
+
- [ ] No Anthropic SDK imports outside `executor.py` and `events.py`
|
|
54
|
+
- [ ] Unit tests don't require `ANTHROPIC_API_KEY`
|
|
55
|
+
- [ ] Tool handlers raise `ValueError` for empty/missing args
|
|
56
|
+
- [ ] `PYTHONPATH=../axor-core python -c "from axor_claude import make_session"` passes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Axor Contributors
|
|
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,400 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: axor-claude
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Claude Code adapter for axor-core governance kernel
|
|
5
|
+
Project-URL: Repository, https://github.com/Bucha11/axor-claude
|
|
6
|
+
Project-URL: Bug Tracker, https://github.com/Bucha11/axor-claude/issues
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: agents,anthropic,axor,claude,claude-code,governance
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Requires-Dist: anthropic>=0.40.0
|
|
19
|
+
Requires-Dist: axor-core>=0.1.0
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# axor-claude
|
|
27
|
+
|
|
28
|
+
[](https://github.com/Bucha11/axor-claude/actions/workflows/ci.yml)
|
|
29
|
+
[](https://pypi.org/project/axor-claude/)
|
|
30
|
+
[](https://pypi.org/project/axor-claude/)
|
|
31
|
+
[](LICENSE)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
**Claude Code adapter for [axor-core](https://github.com/Bucha11/axor-core).**
|
|
35
|
+
|
|
36
|
+
Wraps Claude via the Anthropic SDK and runs it as a governed agent under axor-core's governance kernel — controlled context, explicit tool permissions, token optimization, and full audit trail.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install axor-claude
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Requires an [Anthropic API key](https://console.anthropic.com/).
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Quick Start
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import asyncio
|
|
54
|
+
import axor_claude
|
|
55
|
+
|
|
56
|
+
async def main():
|
|
57
|
+
session = axor_claude.make_session(
|
|
58
|
+
api_key="sk-ant-...", # or set ANTHROPIC_API_KEY env var
|
|
59
|
+
)
|
|
60
|
+
result = await session.run("refactor the auth module to add rate limiting")
|
|
61
|
+
print(result.output)
|
|
62
|
+
print(f"policy: {result.metadata['policy']}") # e.g. moderate_mutative
|
|
63
|
+
print(f"tokens: {result.token_usage.total}")
|
|
64
|
+
|
|
65
|
+
asyncio.run(main())
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
No API key in code:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
72
|
+
python your_script.py
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## What axor-claude provides
|
|
78
|
+
|
|
79
|
+
| Component | What it does |
|
|
80
|
+
|-----------|-------------|
|
|
81
|
+
| `ClaudeCodeExecutor` | Streams Claude API responses, drives multi-turn tool loop via `ToolResultBus` |
|
|
82
|
+
| `ReadHandler` | Read files — line ranges, encoding fallback (utf-8 → latin-1), 1MB cap |
|
|
83
|
+
| `WriteHandler` | Atomic writes (tmpfile → rename), append mode, creates parent dirs |
|
|
84
|
+
| `BashHandler` | Async subprocess — timeout, process group SIGTERM, 512KB output cap |
|
|
85
|
+
| `SearchHandler` | ripgrep if available, pure-Python fallback, regex, context lines |
|
|
86
|
+
| `GlobHandler` | File pattern matching with `**` support, smart ignore list |
|
|
87
|
+
| `ClaudeSkillLoader` | Loads `CLAUDE.md` and `.claude/skills/*.md` as context fragments |
|
|
88
|
+
| `ClaudePluginLoader` | Loads `.claude/plugins/*/plugin.json` — tools, commands, hooks |
|
|
89
|
+
| `normalizer` | Extracts token usage, stop reason, tool calls from API responses |
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Configuration
|
|
94
|
+
|
|
95
|
+
### `make_session()` — all options
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
session = axor_claude.make_session(
|
|
99
|
+
api_key="sk-ant-...", # None → reads ANTHROPIC_API_KEY
|
|
100
|
+
model="claude-sonnet-4-5", # default model
|
|
101
|
+
system_prompt="You are...", # override default system prompt
|
|
102
|
+
tools=("read", "write", "bash", "search", "glob"), # default: all
|
|
103
|
+
load_skills=True, # load CLAUDE.md + .claude/skills/
|
|
104
|
+
load_plugins=True, # load .claude/plugins/
|
|
105
|
+
soft_token_limit=100_000, # budget optimization signals
|
|
106
|
+
trace_config=TraceConfig(...), # privacy / persistence settings
|
|
107
|
+
)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Tools
|
|
111
|
+
|
|
112
|
+
Register only what you need — policy enforces what Claude can actually use:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from axor_core import GovernedSession, CapabilityExecutor
|
|
116
|
+
from axor_claude import ClaudeCodeExecutor, ReadHandler, WriteHandler
|
|
117
|
+
|
|
118
|
+
cap = CapabilityExecutor()
|
|
119
|
+
cap.register(ReadHandler())
|
|
120
|
+
cap.register(WriteHandler())
|
|
121
|
+
|
|
122
|
+
session = GovernedSession(
|
|
123
|
+
executor=ClaudeCodeExecutor(),
|
|
124
|
+
capability_executor=cap,
|
|
125
|
+
)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Extensions
|
|
129
|
+
|
|
130
|
+
`CLAUDE.md` and `.claude/skills/` are loaded automatically by `make_session()`:
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
your-project/
|
|
134
|
+
├── CLAUDE.md ← project-level context → ContextView
|
|
135
|
+
└── .claude/
|
|
136
|
+
├── skills/
|
|
137
|
+
│ ├── testing.md ← "always write pytest tests"
|
|
138
|
+
│ └── style.md ← "use type annotations"
|
|
139
|
+
└── plugins/
|
|
140
|
+
└── my-plugin/
|
|
141
|
+
├── plugin.json ← tool definitions, commands, hooks
|
|
142
|
+
└── README.md ← context → ContextView
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Custom extension loader:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from axor_core.contracts.extension import ExtensionLoader, ExtensionBundle, ExtensionFragment
|
|
149
|
+
|
|
150
|
+
class MyLoader(ExtensionLoader):
|
|
151
|
+
async def load(self) -> ExtensionBundle:
|
|
152
|
+
return ExtensionBundle(fragments=(
|
|
153
|
+
ExtensionFragment(
|
|
154
|
+
name="domain_context",
|
|
155
|
+
context_fragment="This project uses FastAPI with async SQLAlchemy.",
|
|
156
|
+
required_tools=("read",),
|
|
157
|
+
policy_overrides={},
|
|
158
|
+
source="my_loader",
|
|
159
|
+
),
|
|
160
|
+
))
|
|
161
|
+
|
|
162
|
+
session = axor_claude.make_session(
|
|
163
|
+
extension_loaders=[MyLoader()],
|
|
164
|
+
)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Budget tracking
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
session = axor_claude.make_session(
|
|
171
|
+
soft_token_limit=100_000,
|
|
172
|
+
)
|
|
173
|
+
# At 60% → suggest context compression
|
|
174
|
+
# At 80% → deny new child nodes
|
|
175
|
+
# At 90% → restrict export mode
|
|
176
|
+
# At 95% → hard stop via CancelToken
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Custom model and system prompt
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
executor = ClaudeCodeExecutor(
|
|
183
|
+
api_key="sk-ant-...",
|
|
184
|
+
model="claude-opus-4-5",
|
|
185
|
+
system_prompt="You are an expert in Go and distributed systems.",
|
|
186
|
+
)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## How the tool loop works
|
|
192
|
+
|
|
193
|
+
`ClaudeCodeExecutor` drives a multi-turn conversation with Claude. Tool calls are intercepted by axor-core's `IntentLoop` before they execute:
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
Claude API stream → tool_use event
|
|
197
|
+
→ IntentLoop intercepts → Intent
|
|
198
|
+
→ policy check: is tool in capabilities.allowed_tools?
|
|
199
|
+
→ approved → CapabilityExecutor.execute() → ToolResultBus.push()
|
|
200
|
+
→ denied → denial result → ToolResultBus.push()
|
|
201
|
+
→ executor drains bus → continues conversation with tool_result
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The `ToolResultBus` is the handoff mechanism:
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
# Inside ClaudeCodeExecutor.stream():
|
|
208
|
+
bus.expect(1)
|
|
209
|
+
yield ExecutorEvent(kind=TOOL_USE, ...) # IntentLoop intercepts here
|
|
210
|
+
# executes tool, pushes to bus
|
|
211
|
+
results = await bus.drain() # result already in queue
|
|
212
|
+
# next API round uses the result
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Streaming to terminal
|
|
216
|
+
|
|
217
|
+
`ClaudeCodeExecutor` supports a text callback for real-time streaming (used by axor-cli):
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
executor = ClaudeCodeExecutor()
|
|
221
|
+
|
|
222
|
+
def on_text(chunk: str) -> None:
|
|
223
|
+
print(chunk, end="", flush=True)
|
|
224
|
+
|
|
225
|
+
executor.set_text_callback(on_text)
|
|
226
|
+
# text chunks are printed as they arrive from Claude
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Error handling
|
|
230
|
+
|
|
231
|
+
Transient errors are distinguished from fatal ones in the ERROR event payload:
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
# ERROR event payload for rate limit:
|
|
235
|
+
{
|
|
236
|
+
"type": "RateLimitError",
|
|
237
|
+
"message": "...",
|
|
238
|
+
"transient": True, # retry is appropriate
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
# ERROR event payload for auth failure:
|
|
242
|
+
{
|
|
243
|
+
"type": "AuthenticationError",
|
|
244
|
+
"message": "...",
|
|
245
|
+
"transient": False, # fix the key, don't retry
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Transient types: `RateLimitError`, `InternalServerError`, `APIConnectionError`, `APITimeoutError`.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Policy-driven context
|
|
254
|
+
|
|
255
|
+
The fragment limit passed to Claude scales with the policy's context mode:
|
|
256
|
+
|
|
257
|
+
| context_mode | max fragments to Claude |
|
|
258
|
+
|-------------|------------------------|
|
|
259
|
+
| `minimal` | 5 |
|
|
260
|
+
| `moderate` | 15 |
|
|
261
|
+
| `broad` | 40 |
|
|
262
|
+
|
|
263
|
+
This means a "write a test" task (focused_generative → minimal) sends at most 5 context fragments. A "rewrite repo" task (expansive → broad) sends up to 40.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Custom tools via plugins
|
|
268
|
+
|
|
269
|
+
Register a tool handler and its Anthropic schema:
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
from axor_core.capability.executor import ToolHandler
|
|
273
|
+
from axor_claude import register_tool_definition
|
|
274
|
+
|
|
275
|
+
class GitBlameHandler(ToolHandler):
|
|
276
|
+
@property
|
|
277
|
+
def name(self) -> str:
|
|
278
|
+
return "git_blame"
|
|
279
|
+
|
|
280
|
+
async def execute(self, args: dict) -> str:
|
|
281
|
+
import asyncio
|
|
282
|
+
proc = await asyncio.create_subprocess_exec(
|
|
283
|
+
"git", "blame", args["file"],
|
|
284
|
+
stdout=asyncio.subprocess.PIPE,
|
|
285
|
+
)
|
|
286
|
+
out, _ = await proc.communicate()
|
|
287
|
+
return out.decode()
|
|
288
|
+
|
|
289
|
+
register_tool_definition("git_blame", {
|
|
290
|
+
"name": "git_blame",
|
|
291
|
+
"description": "Show who last modified each line of a file",
|
|
292
|
+
"input_schema": {
|
|
293
|
+
"type": "object",
|
|
294
|
+
"properties": {"file": {"type": "string"}},
|
|
295
|
+
"required": ["file"],
|
|
296
|
+
},
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
cap.register(GitBlameHandler())
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Or via `.claude/plugins/git-tools/plugin.json`:
|
|
303
|
+
|
|
304
|
+
```json
|
|
305
|
+
{
|
|
306
|
+
"name": "git-tools",
|
|
307
|
+
"version": "1.0.0",
|
|
308
|
+
"tools": [{
|
|
309
|
+
"name": "git_blame",
|
|
310
|
+
"description": "Show who last modified each line of a file",
|
|
311
|
+
"input_schema": {
|
|
312
|
+
"type": "object",
|
|
313
|
+
"properties": { "file": { "type": "string" } },
|
|
314
|
+
"required": ["file"]
|
|
315
|
+
}
|
|
316
|
+
}]
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## normalizer module
|
|
323
|
+
|
|
324
|
+
Utilities for working with Anthropic API response objects:
|
|
325
|
+
|
|
326
|
+
```python
|
|
327
|
+
from axor_claude import normalizer
|
|
328
|
+
|
|
329
|
+
usage = normalizer.extract_usage(message)
|
|
330
|
+
# {"input_tokens": 500, "output_tokens": 120, "tool_tokens": 0}
|
|
331
|
+
|
|
332
|
+
stop = normalizer.extract_stop_reason(message)
|
|
333
|
+
# "end_turn" | "tool_use" | "max_tokens" | "stop_sequence"
|
|
334
|
+
|
|
335
|
+
text = normalizer.extract_text_content(message)
|
|
336
|
+
# text content only, skips tool_use blocks
|
|
337
|
+
|
|
338
|
+
tools = normalizer.extract_tool_uses(message)
|
|
339
|
+
# [{"tool_use_id": "tu_1", "tool": "bash", "args": {"command": "ls"}}]
|
|
340
|
+
|
|
341
|
+
meta = normalizer.build_response_metadata(message, "focused_generative", "node_001", depth=0)
|
|
342
|
+
# {"policy": "focused_generative", "model": "claude-sonnet-4-5", "stop_reason": "end_turn", ...}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## Repository structure
|
|
348
|
+
|
|
349
|
+
```
|
|
350
|
+
axor-claude/
|
|
351
|
+
├── axor_claude/
|
|
352
|
+
│ ├── __init__.py Public API + make_session() factory
|
|
353
|
+
│ ├── executor.py ClaudeCodeExecutor — streaming, ToolResultBus, text callback
|
|
354
|
+
│ ├── events.py StreamNormalizer — Anthropic SDK events → ExecutorEvent
|
|
355
|
+
│ ├── normalizer.py Response metadata extraction utilities
|
|
356
|
+
│ ├── tool_definitions.py Anthropic API tool schemas (read/write/bash/search/glob/spawn_child)
|
|
357
|
+
│ ├── tools/
|
|
358
|
+
│ │ ├── read.py ReadHandler — line ranges, encoding fallback, size cap
|
|
359
|
+
│ │ ├── write.py WriteHandler — atomic write (tmpfile → rename), append
|
|
360
|
+
│ │ ├── bash.py BashHandler — async subprocess, process group, timeout
|
|
361
|
+
│ │ ├── search.py SearchHandler — ripgrep + Python fallback, context lines
|
|
362
|
+
│ │ └── glob.py GlobHandler — pattern matching, smart ignores
|
|
363
|
+
│ └── extensions/
|
|
364
|
+
│ ├── skill_loader.py ClaudeSkillLoader — CLAUDE.md + .claude/skills/
|
|
365
|
+
│ └── plugin_loader.py ClaudePluginLoader — .claude/plugins/ JSON manifests
|
|
366
|
+
└── tests/
|
|
367
|
+
├── unit/
|
|
368
|
+
│ ├── test_events.py StreamNormalizer — 30 tests
|
|
369
|
+
│ ├── test_normalizer_and_defs.py normalizer + tool_definitions
|
|
370
|
+
│ └── tools/test_tools.py all handlers — 35 tests
|
|
371
|
+
└── integration/ requires ANTHROPIC_API_KEY
|
|
372
|
+
└── test_integration.py
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
## Running tests
|
|
378
|
+
|
|
379
|
+
```bash
|
|
380
|
+
# unit tests — no API key needed
|
|
381
|
+
pytest tests/unit/
|
|
382
|
+
|
|
383
|
+
# integration tests — requires ANTHROPIC_API_KEY
|
|
384
|
+
ANTHROPIC_API_KEY=sk-ant-... pytest tests/integration/ -m integration
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
## Requirements
|
|
390
|
+
|
|
391
|
+
- Python 3.11+
|
|
392
|
+
- `axor-core >= 0.1.0`
|
|
393
|
+
- `anthropic >= 0.40.0`
|
|
394
|
+
- `ripgrep` (optional — faster search, falls back to Python grep)
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## License
|
|
399
|
+
|
|
400
|
+
MIT
|