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.
Files changed (77) hide show
  1. codenaut-0.1.0/LICENSE +21 -0
  2. codenaut-0.1.0/PKG-INFO +199 -0
  3. codenaut-0.1.0/README.md +161 -0
  4. codenaut-0.1.0/agent/__init__.py +1 -0
  5. codenaut-0.1.0/agent/agent.py +236 -0
  6. codenaut-0.1.0/agent/events.py +145 -0
  7. codenaut-0.1.0/agent/persistence.py +123 -0
  8. codenaut-0.1.0/agent/session.py +99 -0
  9. codenaut-0.1.0/client/__init__.py +1 -0
  10. codenaut-0.1.0/client/llm_client.py +266 -0
  11. codenaut-0.1.0/client/response.py +88 -0
  12. codenaut-0.1.0/codenaut.egg-info/PKG-INFO +199 -0
  13. codenaut-0.1.0/codenaut.egg-info/SOURCES.txt +75 -0
  14. codenaut-0.1.0/codenaut.egg-info/dependency_links.txt +1 -0
  15. codenaut-0.1.0/codenaut.egg-info/entry_points.txt +3 -0
  16. codenaut-0.1.0/codenaut.egg-info/requires.txt +19 -0
  17. codenaut-0.1.0/codenaut.egg-info/top_level.txt +12 -0
  18. codenaut-0.1.0/config/__init__.py +1 -0
  19. codenaut-0.1.0/config/config.py +173 -0
  20. codenaut-0.1.0/config/loader.py +173 -0
  21. codenaut-0.1.0/context/__init__.py +1 -0
  22. codenaut-0.1.0/context/compaction.py +96 -0
  23. codenaut-0.1.0/context/loop_detector.py +49 -0
  24. codenaut-0.1.0/context/manager.py +257 -0
  25. codenaut-0.1.0/hooks/__init__.py +1 -0
  26. codenaut-0.1.0/hooks/hook_system.py +140 -0
  27. codenaut-0.1.0/main.py +422 -0
  28. codenaut-0.1.0/prompts/__init__.py +1 -0
  29. codenaut-0.1.0/prompts/system.py +380 -0
  30. codenaut-0.1.0/pyproject.toml +72 -0
  31. codenaut-0.1.0/safety/__init__.py +1 -0
  32. codenaut-0.1.0/safety/approval.py +157 -0
  33. codenaut-0.1.0/setup.cfg +4 -0
  34. codenaut-0.1.0/skills/__init__.py +9 -0
  35. codenaut-0.1.0/skills/discovery.py +97 -0
  36. codenaut-0.1.0/skills/models.py +14 -0
  37. codenaut-0.1.0/skills/registry.py +56 -0
  38. codenaut-0.1.0/tests/test_agent_loop.py +103 -0
  39. codenaut-0.1.0/tests/test_apply_patch.py +193 -0
  40. codenaut-0.1.0/tests/test_config_loader.py +47 -0
  41. codenaut-0.1.0/tests/test_context_manager.py +63 -0
  42. codenaut-0.1.0/tests/test_file_tags.py +110 -0
  43. codenaut-0.1.0/tests/test_llm_client.py +73 -0
  44. codenaut-0.1.0/tests/test_persistence.py +46 -0
  45. codenaut-0.1.0/tests/test_skills.py +123 -0
  46. codenaut-0.1.0/tests/test_tasks.py +101 -0
  47. codenaut-0.1.0/tools/__init__.py +1 -0
  48. codenaut-0.1.0/tools/base.py +200 -0
  49. codenaut-0.1.0/tools/builtin/__init__.py +67 -0
  50. codenaut-0.1.0/tools/builtin/apply_patch.py +388 -0
  51. codenaut-0.1.0/tools/builtin/edit_file.py +211 -0
  52. codenaut-0.1.0/tools/builtin/glob.py +80 -0
  53. codenaut-0.1.0/tools/builtin/grep.py +117 -0
  54. codenaut-0.1.0/tools/builtin/list_dir.py +60 -0
  55. codenaut-0.1.0/tools/builtin/memory.py +115 -0
  56. codenaut-0.1.0/tools/builtin/read_file.py +133 -0
  57. codenaut-0.1.0/tools/builtin/shell.py +160 -0
  58. codenaut-0.1.0/tools/builtin/tasks.py +336 -0
  59. codenaut-0.1.0/tools/builtin/web_fetch.py +55 -0
  60. codenaut-0.1.0/tools/builtin/web_search.py +60 -0
  61. codenaut-0.1.0/tools/builtin/write_file.py +111 -0
  62. codenaut-0.1.0/tools/discovery.py +70 -0
  63. codenaut-0.1.0/tools/mcp/__init__.py +1 -0
  64. codenaut-0.1.0/tools/mcp/client.py +127 -0
  65. codenaut-0.1.0/tools/mcp/mcp_manager.py +83 -0
  66. codenaut-0.1.0/tools/mcp/mcp_tool.py +51 -0
  67. codenaut-0.1.0/tools/registry.py +156 -0
  68. codenaut-0.1.0/tools/subagents.py +148 -0
  69. codenaut-0.1.0/ui/__init__.py +12 -0
  70. codenaut-0.1.0/ui/completer.py +160 -0
  71. codenaut-0.1.0/ui/prompt.py +65 -0
  72. codenaut-0.1.0/ui/tui.py +669 -0
  73. codenaut-0.1.0/utils/__init__.py +1 -0
  74. codenaut-0.1.0/utils/errors.py +49 -0
  75. codenaut-0.1.0/utils/file_tags.py +83 -0
  76. codenaut-0.1.0/utils/paths.py +40 -0
  77. 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.
@@ -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).
@@ -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