kittycode 0.1.0__tar.gz → 0.1.2__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.
- {kittycode-0.1.0 → kittycode-0.1.2}/.gitignore +4 -1
- kittycode-0.1.2/PKG-INFO +249 -0
- {kittycode-0.1.0 → kittycode-0.1.2}/README.md +45 -1
- {kittycode-0.1.0 → kittycode-0.1.2}/README_CN.md +45 -1
- {kittycode-0.1.0 → kittycode-0.1.2}/kittycode/__init__.py +1 -1
- kittycode-0.1.2/kittycode/agent.py +166 -0
- kittycode-0.1.2/kittycode/cli.py +733 -0
- kittycode-0.1.2/kittycode/interrupts.py +5 -0
- {kittycode-0.1.0 → kittycode-0.1.2}/kittycode/llm.py +46 -12
- kittycode-0.1.2/kittycode/tools/__init__.py +33 -0
- {kittycode-0.1.0 → kittycode-0.1.2}/kittycode/tools/agent.py +5 -5
- kittycode-0.1.2/kittycode/tools/bash.py +184 -0
- kittycode-0.1.2/pyproject.toml +34 -0
- kittycode-0.1.2/tests/test_bash_streaming.py +96 -0
- kittycode-0.1.2/tests/test_cli.py +136 -0
- {kittycode-0.1.0 → kittycode-0.1.2}/tests/test_core.py +102 -7
- kittycode-0.1.2/tests/test_interrupt_latency.py +159 -0
- {kittycode-0.1.0 → kittycode-0.1.2}/tests/test_llm.py +14 -1
- kittycode-0.1.0/PKG-INFO +0 -13
- kittycode-0.1.0/kittycode/agent.py +0 -125
- kittycode-0.1.0/kittycode/cli.py +0 -367
- kittycode-0.1.0/kittycode/tools/__init__.py +0 -27
- kittycode-0.1.0/kittycode/tools/bash.py +0 -100
- kittycode-0.1.0/pyproject.toml +0 -25
- {kittycode-0.1.0 → kittycode-0.1.2}/LICENSE +0 -0
- {kittycode-0.1.0 → kittycode-0.1.2}/kittycode/__main__.py +0 -0
- {kittycode-0.1.0 → kittycode-0.1.2}/kittycode/config.py +0 -0
- {kittycode-0.1.0 → kittycode-0.1.2}/kittycode/context.py +0 -0
- {kittycode-0.1.0 → kittycode-0.1.2}/kittycode/prompt.py +0 -0
- {kittycode-0.1.0 → kittycode-0.1.2}/kittycode/session.py +0 -0
- {kittycode-0.1.0 → kittycode-0.1.2}/kittycode/skills.py +0 -0
- {kittycode-0.1.0 → kittycode-0.1.2}/kittycode/tools/base.py +0 -0
- {kittycode-0.1.0 → kittycode-0.1.2}/kittycode/tools/edit.py +0 -0
- {kittycode-0.1.0 → kittycode-0.1.2}/kittycode/tools/glob_tool.py +0 -0
- {kittycode-0.1.0 → kittycode-0.1.2}/kittycode/tools/grep.py +0 -0
- {kittycode-0.1.0 → kittycode-0.1.2}/kittycode/tools/read.py +0 -0
- {kittycode-0.1.0 → kittycode-0.1.2}/kittycode/tools/write.py +0 -0
- {kittycode-0.1.0 → kittycode-0.1.2}/tests/test_skills.py +0 -0
- {kittycode-0.1.0 → kittycode-0.1.2}/tests/test_tools.py +0 -0
kittycode-0.1.2/PKG-INFO
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kittycode
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Minimal AI coding agent with a simple tool loop and skill support.
|
|
5
|
+
Project-URL: Homepage, https://github.com/yejiming/KittyCode
|
|
6
|
+
Project-URL: Repository, https://github.com/yejiming/KittyCode
|
|
7
|
+
Author-email: Jimmy Ye <jiming_ye@163.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agent,ai,cli,coding,llm
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Requires-Dist: anthropic>=0.84.0
|
|
13
|
+
Requires-Dist: openai>=1.0
|
|
14
|
+
Requires-Dist: prompt-toolkit>=3.0
|
|
15
|
+
Requires-Dist: rich>=13.0
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# KittyCode
|
|
21
|
+
|
|
22
|
+
KittyCode is a minimal terminal coding agent focused on a compact, readable implementation. It keeps the core runtime straightforward, with an agent loop, local tools, context compression, a small command-line interface, and support for both OpenAI-compatible and Anthropic APIs.
|
|
23
|
+
|
|
24
|
+
## Background
|
|
25
|
+
|
|
26
|
+
KittyCode follows a simple terminal-agent runtime model:
|
|
27
|
+
|
|
28
|
+
- A user message is sent through the configured model interface.
|
|
29
|
+
- The model can either answer directly or call tools.
|
|
30
|
+
- Tool calls are executed locally and the results are fed back into the conversation.
|
|
31
|
+
- The loop continues until the model returns plain text.
|
|
32
|
+
|
|
33
|
+
The project is intentionally small. It includes the core agent runtime, a compact CLI, session persistence, context compression, and a default built-in tool set.
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- Minimal agent loop with optional parallel execution for multiple tool calls.
|
|
38
|
+
- LLM adapter that supports both OpenAI-compatible and Anthropic interfaces.
|
|
39
|
+
- Built-in tools for shell commands, file reading, file writing, targeted editing, glob search, regex search, and sub-agents.
|
|
40
|
+
- Startup skill discovery from `~/.kittycode/skills`, with skill metadata injected into the system prompt each round.
|
|
41
|
+
- Interactive REPL and one-shot command mode.
|
|
42
|
+
- ANSI pixel-cat startup banner.
|
|
43
|
+
- `Esc` interrupt support for stopping the current agent run at the next safe checkpoint.
|
|
44
|
+
- Context compression to keep long sessions manageable.
|
|
45
|
+
- Session save and resume support.
|
|
46
|
+
|
|
47
|
+
## Requirements
|
|
48
|
+
|
|
49
|
+
- Python 3.10 or newer
|
|
50
|
+
- An API key for either an OpenAI-compatible endpoint or an Anthropic-compatible endpoint
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
Clone the repository and install it in editable mode:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
cd KittyCode
|
|
58
|
+
python -m pip install -e .
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
If you also want the development test dependency:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
python -m pip install -e .[dev]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Configuration
|
|
68
|
+
|
|
69
|
+
KittyCode reads startup configuration from `~/.kittycode/config.json`.
|
|
70
|
+
|
|
71
|
+
Supported fields:
|
|
72
|
+
|
|
73
|
+
- `interface`: `openai` or `anthropic`
|
|
74
|
+
- `api_key`
|
|
75
|
+
- `model`
|
|
76
|
+
- `base_url`
|
|
77
|
+
- `max_tokens`
|
|
78
|
+
- `temperature`
|
|
79
|
+
- `max_context`
|
|
80
|
+
|
|
81
|
+
OpenAI-compatible example:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"interface": "openai",
|
|
86
|
+
"api_key": "sk-...",
|
|
87
|
+
"model": "gpt-4o",
|
|
88
|
+
"base_url": "https://api.openai.com/v1",
|
|
89
|
+
"max_tokens": 4096,
|
|
90
|
+
"temperature": 0,
|
|
91
|
+
"max_context": 128000
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Anthropic example:
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"interface": "anthropic",
|
|
100
|
+
"api_key": "sk-ant-...",
|
|
101
|
+
"model": "claude-3-7-sonnet-latest",
|
|
102
|
+
"base_url": "https://api.anthropic.com",
|
|
103
|
+
"max_tokens": 4096,
|
|
104
|
+
"temperature": 0,
|
|
105
|
+
"max_context": 128000
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The CLI still allows explicit overrides such as `--model`, `--interface`, `--base-url`, and `--api-key`.
|
|
110
|
+
|
|
111
|
+
## Skills
|
|
112
|
+
|
|
113
|
+
At startup, KittyCode scans `~/.kittycode/skills` for skill folders. Each skill should live in its own directory and include a `SKILL.md` file at the top level.
|
|
114
|
+
|
|
115
|
+
Expected layout:
|
|
116
|
+
|
|
117
|
+
```text
|
|
118
|
+
~/.kittycode/skills/
|
|
119
|
+
example-skill/
|
|
120
|
+
SKILL.md
|
|
121
|
+
other-files...
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
KittyCode reads the leading `name` and `description` fields from each `SKILL.md`, keeps the resulting skill list in memory, and inserts the list into the system prompt at the start of every round. The prompt includes:
|
|
125
|
+
|
|
126
|
+
- `name`
|
|
127
|
+
- `description`
|
|
128
|
+
- `path`
|
|
129
|
+
|
|
130
|
+
This allows the model to see which local skills are available and decide when to read and use them.
|
|
131
|
+
|
|
132
|
+
Before each round, KittyCode checks whether the skill directory changed and reloads the cached skill metadata when needed, so adding or editing skills does not require restarting the process.
|
|
133
|
+
|
|
134
|
+
You can also invoke a loaded skill directly from the CLI with `/<skill name>`.
|
|
135
|
+
|
|
136
|
+
- `/<skill name>` selects that skill for your next non-command message.
|
|
137
|
+
- `/<skill name> <task>` runs the next request immediately with that skill.
|
|
138
|
+
|
|
139
|
+
## Usage
|
|
140
|
+
|
|
141
|
+
Run the interactive terminal UI:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
kittycode
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
When the CLI is busy, press `Esc` to interrupt the current run. The stop is cooperative: KittyCode cancels at the next safe checkpoint between LLM streaming, tool dispatch, and loop rounds. A blocking external call may still need to return before the run fully stops.
|
|
148
|
+
|
|
149
|
+
You can also use the module entry point:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
python -m kittycode
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Run a one-shot prompt and exit:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
kittycode -p "Explain the project structure"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Resume a saved session:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
kittycode -r session_1234567890
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Override model, interface, or endpoint from the command line:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
kittycode --interface anthropic --model claude-3-7-sonnet-latest
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Interactive Commands
|
|
174
|
+
|
|
175
|
+
Inside the REPL, KittyCode supports:
|
|
176
|
+
|
|
177
|
+
- `/help`
|
|
178
|
+
- `/reset`
|
|
179
|
+
- `/skills`
|
|
180
|
+
- `/<skill name>`
|
|
181
|
+
- `/model <name>`
|
|
182
|
+
- `/tokens`
|
|
183
|
+
- `/compact`
|
|
184
|
+
- `/save`
|
|
185
|
+
- `/sessions`
|
|
186
|
+
- `/quit`
|
|
187
|
+
|
|
188
|
+
The `/skills` command refreshes the local skill cache if the skill directory has changed and then prints the currently loaded skills.
|
|
189
|
+
Slash commands also support prefix matching while typing, so entering `/` shows matching commands and skills through completion suggestions.
|
|
190
|
+
|
|
191
|
+
## If `kittycode` Is Still Not Found After `pip install -e .`
|
|
192
|
+
|
|
193
|
+
The project already declares the console entry point in `pyproject.toml`:
|
|
194
|
+
|
|
195
|
+
```toml
|
|
196
|
+
[project.scripts]
|
|
197
|
+
kittycode = "kittycode.cli:main"
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
So if `kittycode` is still unavailable, the usual cause is the local Python install location rather than missing packaging metadata.
|
|
201
|
+
|
|
202
|
+
Check which Python/pip you used:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
python3 -m pip --version
|
|
206
|
+
python3 -m site --user-base
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
On macOS, a user install commonly places scripts under:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
~/Library/Python/3.11/bin
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
If that directory is not in `PATH`, the editable install may succeed but the shell still will not find `kittycode`.
|
|
216
|
+
|
|
217
|
+
For `zsh`, add the corresponding bin directory to your shell profile:
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
export PATH="$HOME/Library/Python/3.11/bin:$PATH"
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Then reload the shell:
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
source ~/.zshrc
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
If the install itself fails with a permissions error while writing a `.pth` file under `~/Library/Python/.../site-packages`, fix that environment issue first or install into a virtual environment before retrying.
|
|
230
|
+
|
|
231
|
+
## Project Layout
|
|
232
|
+
|
|
233
|
+
- `kittycode/agent.py`: core agent loop
|
|
234
|
+
- `kittycode/llm.py`: streaming LLM wrapper
|
|
235
|
+
- `kittycode/context.py`: context compression
|
|
236
|
+
- `kittycode/cli.py`: interactive and one-shot CLI
|
|
237
|
+
- `kittycode/session.py`: session persistence
|
|
238
|
+
- `kittycode/tools/`: built-in tools
|
|
239
|
+
- `tests/`: focused runtime and tool tests
|
|
240
|
+
|
|
241
|
+
## Development
|
|
242
|
+
|
|
243
|
+
Run the test suite:
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
python -m pytest -q
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The current test suite covers the exported API, config-file behavior, provider conversion helpers, context compression, session helpers, the default tool registry, and skill discovery/prompt injection.
|
|
@@ -20,6 +20,8 @@ The project is intentionally small. It includes the core agent runtime, a compac
|
|
|
20
20
|
- Built-in tools for shell commands, file reading, file writing, targeted editing, glob search, regex search, and sub-agents.
|
|
21
21
|
- Startup skill discovery from `~/.kittycode/skills`, with skill metadata injected into the system prompt each round.
|
|
22
22
|
- Interactive REPL and one-shot command mode.
|
|
23
|
+
- ANSI pixel-cat startup banner.
|
|
24
|
+
- `Esc` interrupt support for stopping the current agent run at the next safe checkpoint.
|
|
23
25
|
- Context compression to keep long sessions manageable.
|
|
24
26
|
- Session save and resume support.
|
|
25
27
|
|
|
@@ -123,6 +125,8 @@ Run the interactive terminal UI:
|
|
|
123
125
|
kittycode
|
|
124
126
|
```
|
|
125
127
|
|
|
128
|
+
When the CLI is busy, press `Esc` to interrupt the current run. The stop is cooperative: KittyCode cancels at the next safe checkpoint between LLM streaming, tool dispatch, and loop rounds. A blocking external call may still need to return before the run fully stops.
|
|
129
|
+
|
|
126
130
|
You can also use the module entry point:
|
|
127
131
|
|
|
128
132
|
```bash
|
|
@@ -165,6 +169,46 @@ Inside the REPL, KittyCode supports:
|
|
|
165
169
|
The `/skills` command refreshes the local skill cache if the skill directory has changed and then prints the currently loaded skills.
|
|
166
170
|
Slash commands also support prefix matching while typing, so entering `/` shows matching commands and skills through completion suggestions.
|
|
167
171
|
|
|
172
|
+
## If `kittycode` Is Still Not Found After `pip install -e .`
|
|
173
|
+
|
|
174
|
+
The project already declares the console entry point in `pyproject.toml`:
|
|
175
|
+
|
|
176
|
+
```toml
|
|
177
|
+
[project.scripts]
|
|
178
|
+
kittycode = "kittycode.cli:main"
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
So if `kittycode` is still unavailable, the usual cause is the local Python install location rather than missing packaging metadata.
|
|
182
|
+
|
|
183
|
+
Check which Python/pip you used:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
python3 -m pip --version
|
|
187
|
+
python3 -m site --user-base
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
On macOS, a user install commonly places scripts under:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
~/Library/Python/3.11/bin
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
If that directory is not in `PATH`, the editable install may succeed but the shell still will not find `kittycode`.
|
|
197
|
+
|
|
198
|
+
For `zsh`, add the corresponding bin directory to your shell profile:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
export PATH="$HOME/Library/Python/3.11/bin:$PATH"
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Then reload the shell:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
source ~/.zshrc
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
If the install itself fails with a permissions error while writing a `.pth` file under `~/Library/Python/.../site-packages`, fix that environment issue first or install into a virtual environment before retrying.
|
|
211
|
+
|
|
168
212
|
## Project Layout
|
|
169
213
|
|
|
170
214
|
- `kittycode/agent.py`: core agent loop
|
|
@@ -183,4 +227,4 @@ Run the test suite:
|
|
|
183
227
|
python -m pytest -q
|
|
184
228
|
```
|
|
185
229
|
|
|
186
|
-
The current test suite covers the exported API, config-file behavior, provider conversion helpers, context compression, session helpers, the default tool registry, and skill discovery/prompt injection.
|
|
230
|
+
The current test suite covers the exported API, config-file behavior, provider conversion helpers, context compression, session helpers, the default tool registry, and skill discovery/prompt injection.
|
|
@@ -20,6 +20,8 @@ KittyCode 的基本工作方式如下:
|
|
|
20
20
|
- 内置 Bash、读文件、写文件、精确替换编辑、Glob 搜索、Grep 搜索、子代理等工具。
|
|
21
21
|
- 启动时自动扫描 `~/.kittycode/skills`,并在每轮系统 prompt 开头注入可用 skill 列表。
|
|
22
22
|
- 支持交互式 REPL 和单次命令模式。
|
|
23
|
+
- 启动时展示 ANSI 彩色像素猫。
|
|
24
|
+
- 支持在执行过程中按 `Esc` 中断当前任务。
|
|
23
25
|
- 支持长会话上下文压缩。
|
|
24
26
|
- 支持保存和恢复历史会话。
|
|
25
27
|
|
|
@@ -123,6 +125,8 @@ KittyCode 启动时会扫描 `~/.kittycode/skills` 目录。每个 skill 使用
|
|
|
123
125
|
kittycode
|
|
124
126
|
```
|
|
125
127
|
|
|
128
|
+
当 CLI 正在执行时,可以按 `Esc` 中断当前任务。这个中断是协作式的:KittyCode 会在下一处安全检查点停止,例如 LLM 流式输出过程中、工具调度前后或下一轮循环开始前。如果某个底层外部调用本身是阻塞的,仍然可能要等该调用返回后才会完全结束。
|
|
129
|
+
|
|
126
130
|
也可以直接通过模块启动:
|
|
127
131
|
|
|
128
132
|
```bash
|
|
@@ -165,6 +169,46 @@ kittycode --interface anthropic --model claude-3-7-sonnet-latest
|
|
|
165
169
|
`/skills` 命令会在检测到 skill 目录变化时刷新本地缓存,并输出当前已加载的 skill 列表。
|
|
166
170
|
当输入以 `/` 开头时,CLI 还会根据前缀自动补全可用命令和 skill。
|
|
167
171
|
|
|
172
|
+
## 如果执行 `pip install -e .` 后仍然找不到 `kittycode`
|
|
173
|
+
|
|
174
|
+
项目本身已经在 `pyproject.toml` 中声明了控制台入口:
|
|
175
|
+
|
|
176
|
+
```toml
|
|
177
|
+
[project.scripts]
|
|
178
|
+
kittycode = "kittycode.cli:main"
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
所以如果仍然不能直接执行 `kittycode`,通常不是因为项目没有声明入口,而是 Python 本地安装路径或 shell `PATH` 的问题。
|
|
182
|
+
|
|
183
|
+
先确认你实际使用的是哪个 Python/pip:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
python3 -m pip --version
|
|
187
|
+
python3 -m site --user-base
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
在 macOS 上,用户级安装通常会把脚本放到类似下面的目录:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
~/Library/Python/3.11/bin
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
如果这个目录不在 `PATH` 中,即使 editable install 成功,shell 也依然找不到 `kittycode`。
|
|
197
|
+
|
|
198
|
+
对于 `zsh`,可以把对应目录加入配置:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
export PATH="$HOME/Library/Python/3.11/bin:$PATH"
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
然后重新加载 shell:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
source ~/.zshrc
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
如果安装过程本身在往 `~/Library/Python/.../site-packages` 写 `.pth` 文件时就报权限错误,那要先修复本地 Python 环境权限,或者改用虚拟环境后再重新安装。
|
|
211
|
+
|
|
168
212
|
## 目录结构
|
|
169
213
|
|
|
170
214
|
- `kittycode/agent.py`:核心代理循环
|
|
@@ -183,4 +227,4 @@ kittycode --interface anthropic --model claude-3-7-sonnet-latest
|
|
|
183
227
|
python -m pytest -q
|
|
184
228
|
```
|
|
185
229
|
|
|
186
|
-
当前测试主要覆盖导出 API、config.json 读取、provider 转换辅助逻辑、上下文压缩、会话辅助函数、默认工具注册表,以及 skill 发现与 prompt 注入逻辑。
|
|
230
|
+
当前测试主要覆盖导出 API、config.json 读取、provider 转换辅助逻辑、上下文压缩、会话辅助函数、默认工具注册表,以及 skill 发现与 prompt 注入逻辑。
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Core agent loop.
|
|
2
|
+
|
|
3
|
+
This is the heart of KittyCode.
|
|
4
|
+
|
|
5
|
+
user message -> LLM (with tools) -> tool calls? -> execute -> loop
|
|
6
|
+
-> text reply? -> return to user
|
|
7
|
+
|
|
8
|
+
It keeps looping until the LLM responds with plain text, which means it is
|
|
9
|
+
done working and ready to report back.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import concurrent.futures
|
|
13
|
+
import copy
|
|
14
|
+
import inspect
|
|
15
|
+
|
|
16
|
+
from .context import ContextManager
|
|
17
|
+
from .interrupts import CancellationRequested
|
|
18
|
+
from .llm import LLM
|
|
19
|
+
from .prompt import system_prompt
|
|
20
|
+
from .skills import load_skills
|
|
21
|
+
from .tools import create_tool_instances, get_tool
|
|
22
|
+
from .tools.agent import AgentTool
|
|
23
|
+
from .tools.base import Tool
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Agent:
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
llm: LLM,
|
|
30
|
+
tools: list[Tool] | None = None,
|
|
31
|
+
max_context_tokens: int = 128_000,
|
|
32
|
+
max_rounds: int = 50,
|
|
33
|
+
):
|
|
34
|
+
self.llm = llm
|
|
35
|
+
self.tools = list(tools) if tools is not None else create_tool_instances()
|
|
36
|
+
self.messages: list[dict] = []
|
|
37
|
+
self.context = ContextManager(max_tokens=max_context_tokens)
|
|
38
|
+
self.max_rounds = max_rounds
|
|
39
|
+
self.skills = []
|
|
40
|
+
self._system = ""
|
|
41
|
+
self.refresh_skills(force_reload=True)
|
|
42
|
+
|
|
43
|
+
for tool in self.tools:
|
|
44
|
+
if isinstance(tool, AgentTool):
|
|
45
|
+
tool._parent_agent = self
|
|
46
|
+
|
|
47
|
+
def _full_messages(self) -> list[dict]:
|
|
48
|
+
self.refresh_skills()
|
|
49
|
+
return [{"role": "system", "content": self._system}] + self.messages
|
|
50
|
+
|
|
51
|
+
def _tool_schemas(self) -> list[dict]:
|
|
52
|
+
return [tool.schema() for tool in self.tools]
|
|
53
|
+
|
|
54
|
+
def fork(self):
|
|
55
|
+
worker = Agent(
|
|
56
|
+
llm=self.llm.clone() if hasattr(self.llm, "clone") else self.llm,
|
|
57
|
+
tools=[type(tool)() for tool in self.tools],
|
|
58
|
+
max_context_tokens=self.context.max_tokens,
|
|
59
|
+
max_rounds=self.max_rounds,
|
|
60
|
+
)
|
|
61
|
+
worker.messages = copy.deepcopy(self.messages)
|
|
62
|
+
worker.skills = list(self.skills)
|
|
63
|
+
worker._system = self._system
|
|
64
|
+
return worker
|
|
65
|
+
|
|
66
|
+
def chat(self, user_input: str, on_token=None, on_tool=None, on_tool_output=None, cancel_event=None) -> str:
|
|
67
|
+
"""Process one user message. May involve multiple LLM/tool rounds."""
|
|
68
|
+
self.messages.append({"role": "user", "content": user_input})
|
|
69
|
+
self.context.maybe_compress(self.messages, self.llm)
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
self._raise_if_cancelled(cancel_event)
|
|
73
|
+
|
|
74
|
+
for _ in range(self.max_rounds):
|
|
75
|
+
self._raise_if_cancelled(cancel_event)
|
|
76
|
+
response = self.llm.chat(
|
|
77
|
+
messages=self._full_messages(),
|
|
78
|
+
tools=self._tool_schemas(),
|
|
79
|
+
on_token=on_token,
|
|
80
|
+
cancel_event=cancel_event,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if not response.tool_calls:
|
|
84
|
+
self.messages.append(response.message)
|
|
85
|
+
return response.content
|
|
86
|
+
|
|
87
|
+
self.messages.append(response.message)
|
|
88
|
+
|
|
89
|
+
if len(response.tool_calls) == 1:
|
|
90
|
+
tool_call = response.tool_calls[0]
|
|
91
|
+
if on_tool:
|
|
92
|
+
on_tool(tool_call.name, tool_call.arguments)
|
|
93
|
+
self._raise_if_cancelled(cancel_event)
|
|
94
|
+
result = self._exec_tool(tool_call, cancel_event=cancel_event, on_output=on_tool_output)
|
|
95
|
+
self.messages.append(
|
|
96
|
+
{
|
|
97
|
+
"role": "tool",
|
|
98
|
+
"tool_call_id": tool_call.id,
|
|
99
|
+
"content": result,
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
else:
|
|
103
|
+
results = self._exec_tools_parallel(
|
|
104
|
+
response.tool_calls,
|
|
105
|
+
on_tool=on_tool,
|
|
106
|
+
on_tool_output=on_tool_output,
|
|
107
|
+
cancel_event=cancel_event,
|
|
108
|
+
)
|
|
109
|
+
for tool_call, result in zip(response.tool_calls, results):
|
|
110
|
+
self.messages.append(
|
|
111
|
+
{
|
|
112
|
+
"role": "tool",
|
|
113
|
+
"tool_call_id": tool_call.id,
|
|
114
|
+
"content": result,
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
self.context.maybe_compress(self.messages, self.llm)
|
|
119
|
+
|
|
120
|
+
return "(reached maximum tool-call rounds)"
|
|
121
|
+
except CancellationRequested:
|
|
122
|
+
return "(interrupted)"
|
|
123
|
+
|
|
124
|
+
def _exec_tool(self, tool_call, cancel_event=None, on_output=None) -> str:
|
|
125
|
+
"""Execute a single tool call and return the result string."""
|
|
126
|
+
self._raise_if_cancelled(cancel_event)
|
|
127
|
+
tool = get_tool(tool_call.name, self.tools)
|
|
128
|
+
if tool is None:
|
|
129
|
+
return f"Error: unknown tool '{tool_call.name}'"
|
|
130
|
+
try:
|
|
131
|
+
execute_kwargs = dict(tool_call.arguments)
|
|
132
|
+
parameters = inspect.signature(tool.execute).parameters
|
|
133
|
+
if on_output is not None and "stream_callback" in parameters:
|
|
134
|
+
execute_kwargs["stream_callback"] = lambda text: on_output(tool_call.name, text)
|
|
135
|
+
if cancel_event is not None and "cancel_event" in parameters:
|
|
136
|
+
execute_kwargs["cancel_event"] = cancel_event
|
|
137
|
+
return tool.execute(**execute_kwargs)
|
|
138
|
+
except TypeError as exc:
|
|
139
|
+
return f"Error: bad arguments for {tool_call.name}: {exc}"
|
|
140
|
+
except Exception as exc:
|
|
141
|
+
return f"Error executing {tool_call.name}: {exc}"
|
|
142
|
+
|
|
143
|
+
def _exec_tools_parallel(self, tool_calls, on_tool=None, on_tool_output=None, cancel_event=None) -> list[str]:
|
|
144
|
+
"""Run multiple tool calls concurrently using threads."""
|
|
145
|
+
for tool_call in tool_calls:
|
|
146
|
+
if on_tool:
|
|
147
|
+
on_tool(tool_call.name, tool_call.arguments)
|
|
148
|
+
self._raise_if_cancelled(cancel_event)
|
|
149
|
+
|
|
150
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as pool:
|
|
151
|
+
futures = [pool.submit(self._exec_tool, tool_call, cancel_event, on_tool_output) for tool_call in tool_calls]
|
|
152
|
+
return [future.result() for future in futures]
|
|
153
|
+
|
|
154
|
+
def reset(self):
|
|
155
|
+
"""Clear conversation history."""
|
|
156
|
+
self.messages.clear()
|
|
157
|
+
|
|
158
|
+
def refresh_skills(self, force_reload: bool = False):
|
|
159
|
+
"""Refresh cached skill metadata and rebuild the system prompt."""
|
|
160
|
+
self.skills = load_skills(force_reload=force_reload)
|
|
161
|
+
self._system = system_prompt(self.tools, self.skills)
|
|
162
|
+
|
|
163
|
+
@staticmethod
|
|
164
|
+
def _raise_if_cancelled(cancel_event):
|
|
165
|
+
if cancel_event is not None and cancel_event.is_set():
|
|
166
|
+
raise CancellationRequested()
|