codenaut 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.
- codenaut-0.1.0/LICENSE +21 -0
- codenaut-0.1.0/PKG-INFO +199 -0
- codenaut-0.1.0/README.md +161 -0
- codenaut-0.1.0/agent/__init__.py +1 -0
- codenaut-0.1.0/agent/agent.py +236 -0
- codenaut-0.1.0/agent/events.py +145 -0
- codenaut-0.1.0/agent/persistence.py +123 -0
- codenaut-0.1.0/agent/session.py +99 -0
- codenaut-0.1.0/client/__init__.py +1 -0
- codenaut-0.1.0/client/llm_client.py +266 -0
- codenaut-0.1.0/client/response.py +88 -0
- codenaut-0.1.0/codenaut.egg-info/PKG-INFO +199 -0
- codenaut-0.1.0/codenaut.egg-info/SOURCES.txt +75 -0
- codenaut-0.1.0/codenaut.egg-info/dependency_links.txt +1 -0
- codenaut-0.1.0/codenaut.egg-info/entry_points.txt +3 -0
- codenaut-0.1.0/codenaut.egg-info/requires.txt +19 -0
- codenaut-0.1.0/codenaut.egg-info/top_level.txt +12 -0
- codenaut-0.1.0/config/__init__.py +1 -0
- codenaut-0.1.0/config/config.py +173 -0
- codenaut-0.1.0/config/loader.py +173 -0
- codenaut-0.1.0/context/__init__.py +1 -0
- codenaut-0.1.0/context/compaction.py +96 -0
- codenaut-0.1.0/context/loop_detector.py +49 -0
- codenaut-0.1.0/context/manager.py +257 -0
- codenaut-0.1.0/hooks/__init__.py +1 -0
- codenaut-0.1.0/hooks/hook_system.py +140 -0
- codenaut-0.1.0/main.py +422 -0
- codenaut-0.1.0/prompts/__init__.py +1 -0
- codenaut-0.1.0/prompts/system.py +380 -0
- codenaut-0.1.0/pyproject.toml +72 -0
- codenaut-0.1.0/safety/__init__.py +1 -0
- codenaut-0.1.0/safety/approval.py +157 -0
- codenaut-0.1.0/setup.cfg +4 -0
- codenaut-0.1.0/skills/__init__.py +9 -0
- codenaut-0.1.0/skills/discovery.py +97 -0
- codenaut-0.1.0/skills/models.py +14 -0
- codenaut-0.1.0/skills/registry.py +56 -0
- codenaut-0.1.0/tests/test_agent_loop.py +103 -0
- codenaut-0.1.0/tests/test_apply_patch.py +193 -0
- codenaut-0.1.0/tests/test_config_loader.py +47 -0
- codenaut-0.1.0/tests/test_context_manager.py +63 -0
- codenaut-0.1.0/tests/test_file_tags.py +110 -0
- codenaut-0.1.0/tests/test_llm_client.py +73 -0
- codenaut-0.1.0/tests/test_persistence.py +46 -0
- codenaut-0.1.0/tests/test_skills.py +123 -0
- codenaut-0.1.0/tests/test_tasks.py +101 -0
- codenaut-0.1.0/tools/__init__.py +1 -0
- codenaut-0.1.0/tools/base.py +200 -0
- codenaut-0.1.0/tools/builtin/__init__.py +67 -0
- codenaut-0.1.0/tools/builtin/apply_patch.py +388 -0
- codenaut-0.1.0/tools/builtin/edit_file.py +211 -0
- codenaut-0.1.0/tools/builtin/glob.py +80 -0
- codenaut-0.1.0/tools/builtin/grep.py +117 -0
- codenaut-0.1.0/tools/builtin/list_dir.py +60 -0
- codenaut-0.1.0/tools/builtin/memory.py +115 -0
- codenaut-0.1.0/tools/builtin/read_file.py +133 -0
- codenaut-0.1.0/tools/builtin/shell.py +160 -0
- codenaut-0.1.0/tools/builtin/tasks.py +336 -0
- codenaut-0.1.0/tools/builtin/web_fetch.py +55 -0
- codenaut-0.1.0/tools/builtin/web_search.py +60 -0
- codenaut-0.1.0/tools/builtin/write_file.py +111 -0
- codenaut-0.1.0/tools/discovery.py +70 -0
- codenaut-0.1.0/tools/mcp/__init__.py +1 -0
- codenaut-0.1.0/tools/mcp/client.py +127 -0
- codenaut-0.1.0/tools/mcp/mcp_manager.py +83 -0
- codenaut-0.1.0/tools/mcp/mcp_tool.py +51 -0
- codenaut-0.1.0/tools/registry.py +156 -0
- codenaut-0.1.0/tools/subagents.py +148 -0
- codenaut-0.1.0/ui/__init__.py +12 -0
- codenaut-0.1.0/ui/completer.py +160 -0
- codenaut-0.1.0/ui/prompt.py +65 -0
- codenaut-0.1.0/ui/tui.py +669 -0
- codenaut-0.1.0/utils/__init__.py +1 -0
- codenaut-0.1.0/utils/errors.py +49 -0
- codenaut-0.1.0/utils/file_tags.py +83 -0
- codenaut-0.1.0/utils/paths.py +40 -0
- codenaut-0.1.0/utils/text.py +79 -0
codenaut-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nick211908
|
|
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.
|
codenaut-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codenaut
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A terminal-based OpenAI-compatible coding agent with tools, approvals, MCP, and session persistence.
|
|
5
|
+
Author: nick211908
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/nick211908/coding-agent
|
|
8
|
+
Project-URL: Issues, https://github.com/nick211908/coding-agent/issues
|
|
9
|
+
Project-URL: Repository, https://github.com/nick211908/coding-agent
|
|
10
|
+
Keywords: agent,cli,coding,openai,mcp
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Software Development
|
|
17
|
+
Classifier: Topic :: Utilities
|
|
18
|
+
Requires-Python: >=3.13
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: click>=8.4.2
|
|
22
|
+
Requires-Dist: ddgs>=9.14.4
|
|
23
|
+
Requires-Dist: fastmcp>=3.4.2
|
|
24
|
+
Requires-Dist: httpx>=0.28.1
|
|
25
|
+
Requires-Dist: langsmith>=0.13.0
|
|
26
|
+
Requires-Dist: openai>=2.44.0
|
|
27
|
+
Requires-Dist: platformdirs>=4.10.0
|
|
28
|
+
Requires-Dist: prompt-toolkit>=3.0.0
|
|
29
|
+
Requires-Dist: pydantic>=2.13.4
|
|
30
|
+
Requires-Dist: python-dotenv>=1.1.1
|
|
31
|
+
Requires-Dist: rich>=14.2.0
|
|
32
|
+
Requires-Dist: tiktoken>=0.13.0
|
|
33
|
+
Requires-Dist: tomli>=2.4.1; python_version < "3.11"
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
36
|
+
Requires-Dist: pylint>=4.0.6; extra == "dev"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# code-it
|
|
40
|
+
|
|
41
|
+
A terminal-based, OpenAI-compatible coding agent that reads, edits, and runs code in your project through a ReAct-style tool-calling loop with approvals, hooks, MCP, and session persistence.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
Prerequisites: Python 3.13+ and `uv`.
|
|
46
|
+
|
|
47
|
+
Install dependencies into the project-managed environment:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
uv sync
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Build distributable artifacts:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
uv build
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This creates:
|
|
60
|
+
|
|
61
|
+
- `dist/code_it-<version>-py3-none-any.whl`
|
|
62
|
+
- `dist/code_it-<version>.tar.gz`
|
|
63
|
+
|
|
64
|
+
## First Run
|
|
65
|
+
|
|
66
|
+
After syncing the project environment, configure credentials:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
uv run code-it --configure
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
This prompts for:
|
|
73
|
+
|
|
74
|
+
- your API key
|
|
75
|
+
- an optional base URL for any OpenAI-compatible provider
|
|
76
|
+
|
|
77
|
+
The values are saved in your user config file at:
|
|
78
|
+
|
|
79
|
+
- Windows: `%APPDATA%\code-it\config.toml`
|
|
80
|
+
- macOS: `~/Library/Application Support/code-it/config.toml`
|
|
81
|
+
- Linux: `~/.config/code-it/config.toml`
|
|
82
|
+
|
|
83
|
+
You can still use a project-local `.env` file or `API_KEY` and `BASE_URL` environment variables, but they are no longer required for packaged installs.
|
|
84
|
+
|
|
85
|
+
## Usage
|
|
86
|
+
|
|
87
|
+
Run interactively:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
uv run code-it
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Run a single prompt:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
uv run code-it "Summarize what this repo does"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Target a specific working directory:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
uv run code-it --cwd /path/to/project "Find and fix the failing test"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Update saved credentials later:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
uv run code-it --configure
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Inside an interactive session:
|
|
112
|
+
|
|
113
|
+
```text
|
|
114
|
+
[user]> /setup # update saved API credentials
|
|
115
|
+
[user]> /tools # list available tools
|
|
116
|
+
[user]> /mcp # list connected MCP servers
|
|
117
|
+
[user]> /approval auto-edit # change the approval policy
|
|
118
|
+
[user]> /save # save the current session
|
|
119
|
+
[user]> /sessions # list saved sessions
|
|
120
|
+
[user]> /resume <session_id> # resume a saved session
|
|
121
|
+
[user]> /checkpoint # snapshot the current session
|
|
122
|
+
[user]> /stats # show token/turn usage stats
|
|
123
|
+
[user]> /exit # quit
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Project Config
|
|
127
|
+
|
|
128
|
+
Project-level overrides still live in `.ai-agent/config.toml`.
|
|
129
|
+
|
|
130
|
+
You can also add:
|
|
131
|
+
|
|
132
|
+
- `AGENT.MD` for project-specific developer instructions
|
|
133
|
+
- `.ai-agent/skills/<skill-name>/SKILL.md` for reusable skills
|
|
134
|
+
- `.ai-agent/tools/*.py` for custom tools
|
|
135
|
+
|
|
136
|
+
## Publish Checklist
|
|
137
|
+
|
|
138
|
+
1. Bump `version` in `pyproject.toml`.
|
|
139
|
+
2. Run tests.
|
|
140
|
+
3. Build the wheel and sdist with `uv build --no-sources`.
|
|
141
|
+
4. Test the CLI with `uv run code-it --configure`.
|
|
142
|
+
5. Upload to TestPyPI.
|
|
143
|
+
6. Verify both `uv run code-it --configure` and `uv run code-it` work.
|
|
144
|
+
7. Publish to PyPI.
|
|
145
|
+
|
|
146
|
+
## Publish With uv
|
|
147
|
+
|
|
148
|
+
Recommended local release flow:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
uv sync --frozen
|
|
152
|
+
uv run python -m unittest tests.test_config_loader tests.test_skills
|
|
153
|
+
uv build --no-sources
|
|
154
|
+
uv run --isolated --no-project --with dist/*.whl tests/smoke_test.py
|
|
155
|
+
uv run --isolated --no-project --with dist/*.tar.gz tests/smoke_test.py
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
TestPyPI publish:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
$env:UV_PUBLISH_TOKEN="pypi-***"
|
|
162
|
+
uv publish --publish-url https://test.pypi.org/legacy/
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
PyPI publish:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
$env:UV_PUBLISH_TOKEN="pypi-***"
|
|
169
|
+
uv publish
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
For the first release, verify the package name on PyPI before publishing. If `code-it` is already taken, rename `project.name` in `pyproject.toml` before uploading.
|
|
173
|
+
|
|
174
|
+
## Trusted Publishing
|
|
175
|
+
|
|
176
|
+
A GitHub Actions release workflow is included at `.github/workflows/publish.yml`.
|
|
177
|
+
|
|
178
|
+
To use it:
|
|
179
|
+
|
|
180
|
+
1. Create the project on PyPI.
|
|
181
|
+
2. In the PyPI project settings, add a Trusted Publisher for this GitHub repository.
|
|
182
|
+
3. In GitHub, create an environment named `pypi`.
|
|
183
|
+
4. Push a version tag like `v0.1.0`.
|
|
184
|
+
|
|
185
|
+
That workflow will:
|
|
186
|
+
|
|
187
|
+
- sync with `uv`
|
|
188
|
+
- run the unit tests
|
|
189
|
+
- build the wheel and source distribution
|
|
190
|
+
- smoke-test both artifacts
|
|
191
|
+
- publish with `uv publish`
|
|
192
|
+
|
|
193
|
+
## Why `uv` Instead of `pip install -e .`
|
|
194
|
+
|
|
195
|
+
This repository already carries a `uv.lock`, so `uv sync` is the safer default for development. It avoids polluting a shared Python installation and reduces resolver conflicts with unrelated packages already installed on your machine.
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
Distributed under the [MIT License](LICENSE).
|
codenaut-0.1.0/README.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# code-it
|
|
2
|
+
|
|
3
|
+
A terminal-based, OpenAI-compatible coding agent that reads, edits, and runs code in your project through a ReAct-style tool-calling loop with approvals, hooks, MCP, and session persistence.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Prerequisites: Python 3.13+ and `uv`.
|
|
8
|
+
|
|
9
|
+
Install dependencies into the project-managed environment:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
uv sync
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Build distributable artifacts:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uv build
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
This creates:
|
|
22
|
+
|
|
23
|
+
- `dist/code_it-<version>-py3-none-any.whl`
|
|
24
|
+
- `dist/code_it-<version>.tar.gz`
|
|
25
|
+
|
|
26
|
+
## First Run
|
|
27
|
+
|
|
28
|
+
After syncing the project environment, configure credentials:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
uv run code-it --configure
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
This prompts for:
|
|
35
|
+
|
|
36
|
+
- your API key
|
|
37
|
+
- an optional base URL for any OpenAI-compatible provider
|
|
38
|
+
|
|
39
|
+
The values are saved in your user config file at:
|
|
40
|
+
|
|
41
|
+
- Windows: `%APPDATA%\code-it\config.toml`
|
|
42
|
+
- macOS: `~/Library/Application Support/code-it/config.toml`
|
|
43
|
+
- Linux: `~/.config/code-it/config.toml`
|
|
44
|
+
|
|
45
|
+
You can still use a project-local `.env` file or `API_KEY` and `BASE_URL` environment variables, but they are no longer required for packaged installs.
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
Run interactively:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uv run code-it
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Run a single prompt:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
uv run code-it "Summarize what this repo does"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Target a specific working directory:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
uv run code-it --cwd /path/to/project "Find and fix the failing test"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Update saved credentials later:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
uv run code-it --configure
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Inside an interactive session:
|
|
74
|
+
|
|
75
|
+
```text
|
|
76
|
+
[user]> /setup # update saved API credentials
|
|
77
|
+
[user]> /tools # list available tools
|
|
78
|
+
[user]> /mcp # list connected MCP servers
|
|
79
|
+
[user]> /approval auto-edit # change the approval policy
|
|
80
|
+
[user]> /save # save the current session
|
|
81
|
+
[user]> /sessions # list saved sessions
|
|
82
|
+
[user]> /resume <session_id> # resume a saved session
|
|
83
|
+
[user]> /checkpoint # snapshot the current session
|
|
84
|
+
[user]> /stats # show token/turn usage stats
|
|
85
|
+
[user]> /exit # quit
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Project Config
|
|
89
|
+
|
|
90
|
+
Project-level overrides still live in `.ai-agent/config.toml`.
|
|
91
|
+
|
|
92
|
+
You can also add:
|
|
93
|
+
|
|
94
|
+
- `AGENT.MD` for project-specific developer instructions
|
|
95
|
+
- `.ai-agent/skills/<skill-name>/SKILL.md` for reusable skills
|
|
96
|
+
- `.ai-agent/tools/*.py` for custom tools
|
|
97
|
+
|
|
98
|
+
## Publish Checklist
|
|
99
|
+
|
|
100
|
+
1. Bump `version` in `pyproject.toml`.
|
|
101
|
+
2. Run tests.
|
|
102
|
+
3. Build the wheel and sdist with `uv build --no-sources`.
|
|
103
|
+
4. Test the CLI with `uv run code-it --configure`.
|
|
104
|
+
5. Upload to TestPyPI.
|
|
105
|
+
6. Verify both `uv run code-it --configure` and `uv run code-it` work.
|
|
106
|
+
7. Publish to PyPI.
|
|
107
|
+
|
|
108
|
+
## Publish With uv
|
|
109
|
+
|
|
110
|
+
Recommended local release flow:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
uv sync --frozen
|
|
114
|
+
uv run python -m unittest tests.test_config_loader tests.test_skills
|
|
115
|
+
uv build --no-sources
|
|
116
|
+
uv run --isolated --no-project --with dist/*.whl tests/smoke_test.py
|
|
117
|
+
uv run --isolated --no-project --with dist/*.tar.gz tests/smoke_test.py
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
TestPyPI publish:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
$env:UV_PUBLISH_TOKEN="pypi-***"
|
|
124
|
+
uv publish --publish-url https://test.pypi.org/legacy/
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
PyPI publish:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
$env:UV_PUBLISH_TOKEN="pypi-***"
|
|
131
|
+
uv publish
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
For the first release, verify the package name on PyPI before publishing. If `code-it` is already taken, rename `project.name` in `pyproject.toml` before uploading.
|
|
135
|
+
|
|
136
|
+
## Trusted Publishing
|
|
137
|
+
|
|
138
|
+
A GitHub Actions release workflow is included at `.github/workflows/publish.yml`.
|
|
139
|
+
|
|
140
|
+
To use it:
|
|
141
|
+
|
|
142
|
+
1. Create the project on PyPI.
|
|
143
|
+
2. In the PyPI project settings, add a Trusted Publisher for this GitHub repository.
|
|
144
|
+
3. In GitHub, create an environment named `pypi`.
|
|
145
|
+
4. Push a version tag like `v0.1.0`.
|
|
146
|
+
|
|
147
|
+
That workflow will:
|
|
148
|
+
|
|
149
|
+
- sync with `uv`
|
|
150
|
+
- run the unit tests
|
|
151
|
+
- build the wheel and source distribution
|
|
152
|
+
- smoke-test both artifacts
|
|
153
|
+
- publish with `uv publish`
|
|
154
|
+
|
|
155
|
+
## Why `uv` Instead of `pip install -e .`
|
|
156
|
+
|
|
157
|
+
This repository already carries a `uv.lock`, so `uv sync` is the safer default for development. It avoids polluting a shared Python installation and reduces resolver conflicts with unrelated packages already installed on your machine.
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
Distributed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core agent runtime package."""
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import json
|
|
3
|
+
from typing import AsyncGenerator, Awaitable, Callable
|
|
4
|
+
from agent.events import AgentEvent, AgentEventType
|
|
5
|
+
from agent.session import Session
|
|
6
|
+
from client.response import StreamEventType, TokenUsage, ToolCall, ToolResultMessage
|
|
7
|
+
from config.config import Config
|
|
8
|
+
from prompts.system import create_loop_breaker_prompt
|
|
9
|
+
from tools.base import ToolConfirmation
|
|
10
|
+
from langsmith import trace
|
|
11
|
+
|
|
12
|
+
class Agent:
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
config: Config,
|
|
16
|
+
confirmation_callback: Callable[[ToolConfirmation], bool] | None = None,
|
|
17
|
+
):
|
|
18
|
+
self.config = config
|
|
19
|
+
self.session: Session | None = Session(self.config)
|
|
20
|
+
self.session.approval_manager.confirmation_callback = confirmation_callback
|
|
21
|
+
|
|
22
|
+
async def run(self, message: str):
|
|
23
|
+
with trace(
|
|
24
|
+
name="agent.run",
|
|
25
|
+
run_type="chain",
|
|
26
|
+
inputs={"message": message},
|
|
27
|
+
metadata={"model": self.config.model_name},
|
|
28
|
+
) as run:
|
|
29
|
+
await self.session.hook_system.trigger_before_agent(message)
|
|
30
|
+
yield AgentEvent.agent_start(message)
|
|
31
|
+
self.session.context_manager.add_user_message(message)
|
|
32
|
+
|
|
33
|
+
final_response: str | None = None
|
|
34
|
+
|
|
35
|
+
async for event in self._agentic_loop(message):
|
|
36
|
+
yield event
|
|
37
|
+
|
|
38
|
+
if event.type == AgentEventType.TEXT_COMPLETE:
|
|
39
|
+
final_response = event.data.get("content")
|
|
40
|
+
|
|
41
|
+
# Patch the output onto the run before the span closes
|
|
42
|
+
run.end(outputs={"response": final_response})
|
|
43
|
+
|
|
44
|
+
await self.session.hook_system.trigger_after_agent(message, final_response)
|
|
45
|
+
yield AgentEvent.agent_end(final_response)
|
|
46
|
+
|
|
47
|
+
def _should_enable_tools(self, message: str) -> bool:
|
|
48
|
+
normalized = " ".join(message.lower().strip().split())
|
|
49
|
+
if not normalized:
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
simple_messages = {
|
|
53
|
+
"hi",
|
|
54
|
+
"hii",
|
|
55
|
+
"hiii",
|
|
56
|
+
"hello",
|
|
57
|
+
"hey",
|
|
58
|
+
"hlo",
|
|
59
|
+
"yo",
|
|
60
|
+
"sup",
|
|
61
|
+
"thanks",
|
|
62
|
+
"thank you",
|
|
63
|
+
"ok",
|
|
64
|
+
"okay",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return normalized not in simple_messages
|
|
68
|
+
|
|
69
|
+
async def _agentic_loop(self, latest_user_message: str) -> AsyncGenerator[AgentEvent, None]:
|
|
70
|
+
max_turns = self.config.max_turns
|
|
71
|
+
enable_tools = self._should_enable_tools(latest_user_message)
|
|
72
|
+
|
|
73
|
+
for turn_num in range(max_turns):
|
|
74
|
+
self.session.increment_turn()
|
|
75
|
+
response_text = ""
|
|
76
|
+
|
|
77
|
+
# check for context overflow
|
|
78
|
+
if self.session.context_manager.needs_compression():
|
|
79
|
+
yield AgentEvent.compaction_start(
|
|
80
|
+
self.session.context_manager.message_count
|
|
81
|
+
)
|
|
82
|
+
summary, usage = await self.session.chat_compactor.compress(
|
|
83
|
+
self.session.context_manager
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if summary:
|
|
87
|
+
self.session.context_manager.replace_with_summary(summary)
|
|
88
|
+
self.session.context_manager.set_latest_usage(usage)
|
|
89
|
+
self.session.context_manager.add_usage(usage)
|
|
90
|
+
yield AgentEvent.compaction_complete(
|
|
91
|
+
self.session.context_manager.message_count,
|
|
92
|
+
usage,
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
yield AgentEvent.compaction_error(
|
|
96
|
+
self.session.chat_compactor.last_error
|
|
97
|
+
or "Compaction failed."
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
tool_schemas = self.session.tool_registry.get_schemas() if enable_tools else []
|
|
101
|
+
|
|
102
|
+
tool_calls: list[ToolCall] = []
|
|
103
|
+
usage: TokenUsage | None = None
|
|
104
|
+
|
|
105
|
+
async for event in self.session.client.chat_completion(
|
|
106
|
+
self.session.context_manager.get_messages(),
|
|
107
|
+
tools=tool_schemas if tool_schemas else None,
|
|
108
|
+
):
|
|
109
|
+
if event.type == StreamEventType.TEXT_DELTA:
|
|
110
|
+
if event.text_delta:
|
|
111
|
+
content = event.text_delta.content
|
|
112
|
+
response_text += content
|
|
113
|
+
yield AgentEvent.text_delta(content)
|
|
114
|
+
elif event.type == StreamEventType.TOOL_CALL_COMPLETE:
|
|
115
|
+
if event.tool_call:
|
|
116
|
+
tool_calls.append(event.tool_call)
|
|
117
|
+
elif event.type == StreamEventType.ERROR:
|
|
118
|
+
yield AgentEvent.agent_error(
|
|
119
|
+
event.error or "Unknown error occurred.",
|
|
120
|
+
)
|
|
121
|
+
elif event.type == StreamEventType.MESSAGE_COMPLETE:
|
|
122
|
+
usage = event.usage
|
|
123
|
+
|
|
124
|
+
self.session.context_manager.add_assistant_message(
|
|
125
|
+
response_text or None,
|
|
126
|
+
(
|
|
127
|
+
[
|
|
128
|
+
{
|
|
129
|
+
"id": tc.call_id,
|
|
130
|
+
"type": "function",
|
|
131
|
+
"function": {
|
|
132
|
+
"name": tc.name,
|
|
133
|
+
"arguments": json.dumps(tc.arguments),
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
for tc in tool_calls
|
|
137
|
+
]
|
|
138
|
+
if tool_calls
|
|
139
|
+
else None
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
if response_text:
|
|
143
|
+
yield AgentEvent.text_complete(response_text)
|
|
144
|
+
self.session.loop_detector.record_action(
|
|
145
|
+
"response",
|
|
146
|
+
text=response_text,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if not tool_calls:
|
|
150
|
+
if usage:
|
|
151
|
+
self.session.context_manager.set_latest_usage(usage)
|
|
152
|
+
self.session.context_manager.add_usage(usage)
|
|
153
|
+
|
|
154
|
+
self.session.context_manager.prune_tool_outputs()
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
enable_tools = True
|
|
158
|
+
|
|
159
|
+
tool_call_results: list[ToolResultMessage] = []
|
|
160
|
+
|
|
161
|
+
for tool_call in tool_calls:
|
|
162
|
+
with trace(
|
|
163
|
+
name=f"tool.{tool_call.name}",
|
|
164
|
+
run_type="tool",
|
|
165
|
+
inputs={
|
|
166
|
+
"tool_name": tool_call.name,
|
|
167
|
+
"arguments": tool_call.arguments,
|
|
168
|
+
},
|
|
169
|
+
) as tool_run:
|
|
170
|
+
yield AgentEvent.tool_call_start(
|
|
171
|
+
tool_call.call_id,
|
|
172
|
+
tool_call.name,
|
|
173
|
+
tool_call.arguments,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
self.session.loop_detector.record_action(
|
|
177
|
+
"tool_call",
|
|
178
|
+
tool_name=tool_call.name,
|
|
179
|
+
args=tool_call.arguments,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
result = await self.session.tool_registry.invoke(
|
|
183
|
+
tool_call.name,
|
|
184
|
+
tool_call.arguments,
|
|
185
|
+
self.config.cwd,
|
|
186
|
+
self.session.hook_system,
|
|
187
|
+
self.session.approval_manager,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
yield AgentEvent.tool_call_complete(
|
|
191
|
+
tool_call.call_id,
|
|
192
|
+
tool_call.name,
|
|
193
|
+
result,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
tool_call_results.append(
|
|
197
|
+
ToolResultMessage(
|
|
198
|
+
tool_call_id=tool_call.call_id,
|
|
199
|
+
content=result.to_model_output(),
|
|
200
|
+
is_error=not result.success,
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
for tool_result in tool_call_results:
|
|
205
|
+
self.session.context_manager.add_tool_result(
|
|
206
|
+
tool_result.tool_call_id,
|
|
207
|
+
tool_result.content,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
loop_detection_error = self.session.loop_detector.check_for_loop()
|
|
211
|
+
if loop_detection_error:
|
|
212
|
+
loop_prompt = create_loop_breaker_prompt(loop_detection_error)
|
|
213
|
+
self.session.context_manager.add_user_message(loop_prompt)
|
|
214
|
+
|
|
215
|
+
if usage:
|
|
216
|
+
self.session.context_manager.set_latest_usage(usage)
|
|
217
|
+
self.session.context_manager.add_usage(usage)
|
|
218
|
+
|
|
219
|
+
self.session.context_manager.prune_tool_outputs()
|
|
220
|
+
|
|
221
|
+
yield AgentEvent.agent_error(f"Maximum turns ({max_turns}) reached")
|
|
222
|
+
|
|
223
|
+
async def __aenter__(self) -> Agent:
|
|
224
|
+
await self.session.initialize()
|
|
225
|
+
return self
|
|
226
|
+
|
|
227
|
+
async def __aexit__(
|
|
228
|
+
self,
|
|
229
|
+
exc_type,
|
|
230
|
+
exc_val,
|
|
231
|
+
exc_tb,
|
|
232
|
+
) -> None:
|
|
233
|
+
if self.session and self.session.client and self.session.mcp_manager:
|
|
234
|
+
await self.session.client.close()
|
|
235
|
+
await self.session.mcp_manager.shutdown()
|
|
236
|
+
self.session = None
|