paimon 0.2.4__tar.gz → 0.2.6__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.
- {paimon-0.2.4/paimon.egg-info → paimon-0.2.6}/PKG-INFO +104 -2
- {paimon-0.2.4 → paimon-0.2.6}/README.md +101 -1
- {paimon-0.2.4 → paimon-0.2.6}/README.zh-CN.md +101 -1
- {paimon-0.2.4 → paimon-0.2.6}/paimon/agent.py +124 -24
- paimon-0.2.6/paimon/agents.py +259 -0
- {paimon-0.2.4 → paimon-0.2.6}/paimon/app.py +93 -26
- {paimon-0.2.4 → paimon-0.2.6}/paimon/app.tcss +13 -3
- {paimon-0.2.4 → paimon-0.2.6}/paimon/cli.py +16 -0
- paimon-0.2.4/paimon/taskpane.py → paimon-0.2.6/paimon/commandpane.py +6 -6
- {paimon-0.2.4 → paimon-0.2.6}/paimon/compaction.py +13 -4
- {paimon-0.2.4 → paimon-0.2.6}/paimon/config.py +13 -0
- {paimon-0.2.4 → paimon-0.2.6}/paimon/headless.py +14 -7
- {paimon-0.2.4 → paimon-0.2.6}/paimon/jobs.py +35 -13
- {paimon-0.2.4 → paimon-0.2.6}/paimon/pane.py +176 -22
- {paimon-0.2.4 → paimon-0.2.6}/paimon/prompt.py +8 -1
- {paimon-0.2.4 → paimon-0.2.6}/paimon/retry.py +9 -3
- {paimon-0.2.4 → paimon-0.2.6}/paimon/session.py +60 -17
- paimon-0.2.6/paimon/skills.py +399 -0
- {paimon-0.2.4 → paimon-0.2.6}/paimon/supervisor.py +101 -23
- {paimon-0.2.4 → paimon-0.2.6}/paimon/tools.py +149 -14
- {paimon-0.2.4 → paimon-0.2.6}/paimon/ui.py +9 -1
- {paimon-0.2.4 → paimon-0.2.6/paimon.egg-info}/PKG-INFO +104 -2
- {paimon-0.2.4 → paimon-0.2.6}/paimon.egg-info/SOURCES.txt +3 -1
- {paimon-0.2.4 → paimon-0.2.6}/paimon.egg-info/requires.txt +2 -0
- {paimon-0.2.4 → paimon-0.2.6}/paimon.egg-info/scm_file_list.json +5 -1
- paimon-0.2.6/paimon.egg-info/scm_version.json +8 -0
- {paimon-0.2.4 → paimon-0.2.6}/pyproject.toml +2 -0
- paimon-0.2.4/paimon.egg-info/scm_version.json +0 -8
- {paimon-0.2.4 → paimon-0.2.6}/LICENSE +0 -0
- {paimon-0.2.4 → paimon-0.2.6}/MANIFEST.in +0 -0
- {paimon-0.2.4 → paimon-0.2.6}/paimon/__init__.py +0 -0
- {paimon-0.2.4 → paimon-0.2.6}/paimon/__main__.py +0 -0
- {paimon-0.2.4 → paimon-0.2.6}/paimon/aside.py +0 -0
- {paimon-0.2.4 → paimon-0.2.6}/paimon/commands.py +0 -0
- {paimon-0.2.4 → paimon-0.2.6}/paimon/diff.py +0 -0
- {paimon-0.2.4 → paimon-0.2.6}/paimon/errors.py +0 -0
- {paimon-0.2.4 → paimon-0.2.6}/paimon/llm.py +0 -0
- {paimon-0.2.4 → paimon-0.2.6}/paimon/lockfile.py +0 -0
- {paimon-0.2.4 → paimon-0.2.6}/paimon/login.py +0 -0
- {paimon-0.2.4 → paimon-0.2.6}/paimon/mentions.py +0 -0
- {paimon-0.2.4 → paimon-0.2.6}/paimon/model_windows.py +0 -0
- {paimon-0.2.4 → paimon-0.2.6}/paimon/skill/SKILL.md +0 -0
- {paimon-0.2.4 → paimon-0.2.6}/paimon/tabs.py +0 -0
- {paimon-0.2.4 → paimon-0.2.6}/paimon/telemetry.py +0 -0
- {paimon-0.2.4 → paimon-0.2.6}/paimon.egg-info/dependency_links.txt +0 -0
- {paimon-0.2.4 → paimon-0.2.6}/paimon.egg-info/entry_points.txt +0 -0
- {paimon-0.2.4 → paimon-0.2.6}/paimon.egg-info/top_level.txt +0 -0
- {paimon-0.2.4 → paimon-0.2.6}/setup.cfg +0 -0
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: paimon
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: A minimal code agent built on pydantic-ai + textual
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
7
|
License-File: LICENSE
|
|
8
|
+
Requires-Dist: httpx>=0.27
|
|
8
9
|
Requires-Dist: pydantic-ai-slim[anthropic,openai]>=2.31.0
|
|
9
10
|
Requires-Dist: textual>=8.2.7
|
|
10
11
|
Requires-Dist: textual-serve>=1.1.3
|
|
12
|
+
Requires-Dist: pyyaml>=6
|
|
11
13
|
Dynamic: license-file
|
|
12
14
|
|
|
13
15
|
# Paimon
|
|
@@ -38,10 +40,14 @@ uvx paimon
|
|
|
38
40
|
|
|
39
41
|
The first launch asks for a provider, model, API base and key. Then just type what you want done. Write `@path/to/file` in a prompt to hand a file to the agent.
|
|
40
42
|
|
|
41
|
-
While it runs: `Shift+Tab` switches how much the agent may do on its own (**read** asks before writing files or running commands, **edit** lets edits inside the working directory through, **yolo** never asks and is the default), `Esc` interrupts the current turn, `Ctrl+P` opens the command palette, `Ctrl+C` quits.
|
|
43
|
+
While it runs: `Shift+Tab` switches how much the agent may do on its own (**read** asks before writing files or running commands, **edit** lets edits inside the working directory through, **yolo** never asks and is the default), `Esc` interrupts the current turn, `Ctrl+P` opens the command palette, `Ctrl+C` quits. A line starting with `!` runs in a shell instead of being sent, and Paimon sees what it printed. `!!` keeps it to yourself.
|
|
42
44
|
|
|
43
45
|
`Ctrl+T` opens another session in a pane of its own, `Ctrl+W` closes one, `Ctrl+PageUp` and `Ctrl+PageDown` move between them, and `Ctrl+G` jumps to a pane waiting for permission. Paimon can open panes itself: ask for two independent things and it starts a second agent in its own tab. It can also leave a command running in a tab of its own, a dev server or a watcher, instead of holding up a turn.
|
|
44
46
|
|
|
47
|
+
## Skills
|
|
48
|
+
|
|
49
|
+
Paimon loads [Agent Skills](https://agentskills.io) from `~/.config/paimon/skills`, `~/.agents/skills` and every `.agents/skills` from the working directory up to the repository root. Only each skill's name and description go into the system prompt; the model reads the `SKILL.md` when a task matches, and `/skill:name args` sends it explicitly (the `/` command palette lists them). More locations go in `config.json` as `"skills": ["~/.claude/skills"]` or on the command line with `--skill PATH`; `--no-skills` skips the default locations. When two skills share a name, explicit paths beat the project's, which beat the global ones.
|
|
50
|
+
|
|
45
51
|
## Using Paimon as a subagent
|
|
46
52
|
|
|
47
53
|
Frontier models are good at planning and reviewing; the steps in between are often mechanical. Point Paimon at a cheaper model and let Claude Code or Codex write the plan and check the result. A profile keeps that model's account separate:
|
|
@@ -111,6 +117,102 @@ Each profile keeps its model settings in `~/.config/paimon/<name>/config.json`,
|
|
|
111
117
|
|
|
112
118
|
Read and edit modes run a small set of clearly read-only commands (`ls`, `cat`, `git status`, …) without asking; `--strict` turns that off. **This is a guardrail against agent mistakes, not a security boundary.** For real isolation, run Paimon inside a container or VM.
|
|
113
119
|
|
|
120
|
+
## Architecture
|
|
121
|
+
|
|
122
|
+
`Agent.run` is a UI-agnostic stream of events; the TUI, `--web` and headless mode are three renderers over that one stream. `Supervisor` sits between an agent and the subagents or background commands it starts, and is the permission boundary for both.
|
|
123
|
+
|
|
124
|
+
```mermaid
|
|
125
|
+
flowchart TD
|
|
126
|
+
subgraph entry["Entry points"]
|
|
127
|
+
CLI["cli.py"]
|
|
128
|
+
Commands["commands.py<br/>status / login / sessions"]
|
|
129
|
+
Headless["headless.py<br/>-p, one-shot"]
|
|
130
|
+
App["app.py<br/>Textual TUI / --web"]
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
subgraph tui["TUI widgets"]
|
|
134
|
+
Pane["pane.py<br/>SessionPane"]
|
|
135
|
+
CommandPane["commandpane.py<br/>background command pane"]
|
|
136
|
+
Tabs["tabs.py<br/>pane strip"]
|
|
137
|
+
Login["login.py<br/>provider / model / key"]
|
|
138
|
+
UIWidgets["ui.py<br/>prompt input, confirmations"]
|
|
139
|
+
Diff["diff.py<br/>side-by-side diff rendering"]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
subgraph core["Agent loop"]
|
|
143
|
+
AgentLoop["agent.py<br/>Agent.run()"]
|
|
144
|
+
LLM["llm.py<br/>build_model()"]
|
|
145
|
+
PromptMod["prompt.py<br/>system prompt"]
|
|
146
|
+
ToolsMod["tools.py<br/>tool REGISTRY"]
|
|
147
|
+
SessionMod["session.py<br/>JSONL persistence"]
|
|
148
|
+
Compaction["compaction.py"]
|
|
149
|
+
ModelWindows["model_windows.py<br/>context window sizes"]
|
|
150
|
+
Retry["retry.py"]
|
|
151
|
+
Mentions["mentions.py<br/>@path expansion"]
|
|
152
|
+
Aside["aside.py<br/>off-turn question, unrecorded"]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
subgraph concurrency["Jobs & subagents"]
|
|
156
|
+
Supervisor["supervisor.py<br/>job pool, permissions"]
|
|
157
|
+
Jobs["jobs.py<br/>AgentJob / CommandJob"]
|
|
158
|
+
AgentTypes["agents.py<br/>subagent types"]
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
subgraph support["Config & skills"]
|
|
162
|
+
Config["config.py<br/>profiles, credentials"]
|
|
163
|
+
Skills["skills.py<br/>Agent Skills discovery"]
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
CLI --> Commands
|
|
167
|
+
CLI --> Headless
|
|
168
|
+
CLI --> App
|
|
169
|
+
CLI --> Config
|
|
170
|
+
|
|
171
|
+
Headless --> AgentLoop
|
|
172
|
+
Headless --> Mentions
|
|
173
|
+
|
|
174
|
+
App --> Pane
|
|
175
|
+
App --> CommandPane
|
|
176
|
+
App --> Tabs
|
|
177
|
+
App --> Login
|
|
178
|
+
App --> Supervisor
|
|
179
|
+
App --> Config
|
|
180
|
+
|
|
181
|
+
Pane --> AgentLoop
|
|
182
|
+
Pane --> Aside
|
|
183
|
+
Pane --> Diff
|
|
184
|
+
Pane --> UIWidgets
|
|
185
|
+
Pane --> LLM
|
|
186
|
+
CommandPane --> Pane
|
|
187
|
+
UIWidgets --> Diff
|
|
188
|
+
|
|
189
|
+
AgentLoop --> LLM
|
|
190
|
+
AgentLoop --> PromptMod
|
|
191
|
+
AgentLoop --> ToolsMod
|
|
192
|
+
AgentLoop --> SessionMod
|
|
193
|
+
AgentLoop --> Compaction
|
|
194
|
+
AgentLoop --> Retry
|
|
195
|
+
AgentLoop --> Mentions
|
|
196
|
+
AgentLoop -. "spawn_agent / run_background" .-> Supervisor
|
|
197
|
+
|
|
198
|
+
Aside --> Retry
|
|
199
|
+
Aside --> SessionMod
|
|
200
|
+
|
|
201
|
+
PromptMod --> Skills
|
|
202
|
+
Compaction --> ModelWindows
|
|
203
|
+
|
|
204
|
+
Supervisor --> Jobs
|
|
205
|
+
Supervisor --> AgentTypes
|
|
206
|
+
Supervisor --> ToolsMod
|
|
207
|
+
Supervisor -. "launch callback" .-> App
|
|
208
|
+
Jobs --> AgentLoop
|
|
209
|
+
|
|
210
|
+
AgentTypes --> ToolsMod
|
|
211
|
+
AgentTypes --> Config
|
|
212
|
+
Skills --> Config
|
|
213
|
+
Config --> LLM
|
|
214
|
+
```
|
|
215
|
+
|
|
114
216
|
## Telemetry
|
|
115
217
|
|
|
116
218
|
Each launch sends one anonymous event to Google Analytics: a random install id, the launch mode, the version, the OS name and the configured provider and model name. Nothing from your sessions, prompts, files or credentials is included. Set `PAIMON_NO_TELEMETRY=1` or `DO_NOT_TRACK=1` to turn it off.
|
|
@@ -26,10 +26,14 @@ uvx paimon
|
|
|
26
26
|
|
|
27
27
|
The first launch asks for a provider, model, API base and key. Then just type what you want done. Write `@path/to/file` in a prompt to hand a file to the agent.
|
|
28
28
|
|
|
29
|
-
While it runs: `Shift+Tab` switches how much the agent may do on its own (**read** asks before writing files or running commands, **edit** lets edits inside the working directory through, **yolo** never asks and is the default), `Esc` interrupts the current turn, `Ctrl+P` opens the command palette, `Ctrl+C` quits.
|
|
29
|
+
While it runs: `Shift+Tab` switches how much the agent may do on its own (**read** asks before writing files or running commands, **edit** lets edits inside the working directory through, **yolo** never asks and is the default), `Esc` interrupts the current turn, `Ctrl+P` opens the command palette, `Ctrl+C` quits. A line starting with `!` runs in a shell instead of being sent, and Paimon sees what it printed. `!!` keeps it to yourself.
|
|
30
30
|
|
|
31
31
|
`Ctrl+T` opens another session in a pane of its own, `Ctrl+W` closes one, `Ctrl+PageUp` and `Ctrl+PageDown` move between them, and `Ctrl+G` jumps to a pane waiting for permission. Paimon can open panes itself: ask for two independent things and it starts a second agent in its own tab. It can also leave a command running in a tab of its own, a dev server or a watcher, instead of holding up a turn.
|
|
32
32
|
|
|
33
|
+
## Skills
|
|
34
|
+
|
|
35
|
+
Paimon loads [Agent Skills](https://agentskills.io) from `~/.config/paimon/skills`, `~/.agents/skills` and every `.agents/skills` from the working directory up to the repository root. Only each skill's name and description go into the system prompt; the model reads the `SKILL.md` when a task matches, and `/skill:name args` sends it explicitly (the `/` command palette lists them). More locations go in `config.json` as `"skills": ["~/.claude/skills"]` or on the command line with `--skill PATH`; `--no-skills` skips the default locations. When two skills share a name, explicit paths beat the project's, which beat the global ones.
|
|
36
|
+
|
|
33
37
|
## Using Paimon as a subagent
|
|
34
38
|
|
|
35
39
|
Frontier models are good at planning and reviewing; the steps in between are often mechanical. Point Paimon at a cheaper model and let Claude Code or Codex write the plan and check the result. A profile keeps that model's account separate:
|
|
@@ -99,6 +103,102 @@ Each profile keeps its model settings in `~/.config/paimon/<name>/config.json`,
|
|
|
99
103
|
|
|
100
104
|
Read and edit modes run a small set of clearly read-only commands (`ls`, `cat`, `git status`, …) without asking; `--strict` turns that off. **This is a guardrail against agent mistakes, not a security boundary.** For real isolation, run Paimon inside a container or VM.
|
|
101
105
|
|
|
106
|
+
## Architecture
|
|
107
|
+
|
|
108
|
+
`Agent.run` is a UI-agnostic stream of events; the TUI, `--web` and headless mode are three renderers over that one stream. `Supervisor` sits between an agent and the subagents or background commands it starts, and is the permission boundary for both.
|
|
109
|
+
|
|
110
|
+
```mermaid
|
|
111
|
+
flowchart TD
|
|
112
|
+
subgraph entry["Entry points"]
|
|
113
|
+
CLI["cli.py"]
|
|
114
|
+
Commands["commands.py<br/>status / login / sessions"]
|
|
115
|
+
Headless["headless.py<br/>-p, one-shot"]
|
|
116
|
+
App["app.py<br/>Textual TUI / --web"]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
subgraph tui["TUI widgets"]
|
|
120
|
+
Pane["pane.py<br/>SessionPane"]
|
|
121
|
+
CommandPane["commandpane.py<br/>background command pane"]
|
|
122
|
+
Tabs["tabs.py<br/>pane strip"]
|
|
123
|
+
Login["login.py<br/>provider / model / key"]
|
|
124
|
+
UIWidgets["ui.py<br/>prompt input, confirmations"]
|
|
125
|
+
Diff["diff.py<br/>side-by-side diff rendering"]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
subgraph core["Agent loop"]
|
|
129
|
+
AgentLoop["agent.py<br/>Agent.run()"]
|
|
130
|
+
LLM["llm.py<br/>build_model()"]
|
|
131
|
+
PromptMod["prompt.py<br/>system prompt"]
|
|
132
|
+
ToolsMod["tools.py<br/>tool REGISTRY"]
|
|
133
|
+
SessionMod["session.py<br/>JSONL persistence"]
|
|
134
|
+
Compaction["compaction.py"]
|
|
135
|
+
ModelWindows["model_windows.py<br/>context window sizes"]
|
|
136
|
+
Retry["retry.py"]
|
|
137
|
+
Mentions["mentions.py<br/>@path expansion"]
|
|
138
|
+
Aside["aside.py<br/>off-turn question, unrecorded"]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
subgraph concurrency["Jobs & subagents"]
|
|
142
|
+
Supervisor["supervisor.py<br/>job pool, permissions"]
|
|
143
|
+
Jobs["jobs.py<br/>AgentJob / CommandJob"]
|
|
144
|
+
AgentTypes["agents.py<br/>subagent types"]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
subgraph support["Config & skills"]
|
|
148
|
+
Config["config.py<br/>profiles, credentials"]
|
|
149
|
+
Skills["skills.py<br/>Agent Skills discovery"]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
CLI --> Commands
|
|
153
|
+
CLI --> Headless
|
|
154
|
+
CLI --> App
|
|
155
|
+
CLI --> Config
|
|
156
|
+
|
|
157
|
+
Headless --> AgentLoop
|
|
158
|
+
Headless --> Mentions
|
|
159
|
+
|
|
160
|
+
App --> Pane
|
|
161
|
+
App --> CommandPane
|
|
162
|
+
App --> Tabs
|
|
163
|
+
App --> Login
|
|
164
|
+
App --> Supervisor
|
|
165
|
+
App --> Config
|
|
166
|
+
|
|
167
|
+
Pane --> AgentLoop
|
|
168
|
+
Pane --> Aside
|
|
169
|
+
Pane --> Diff
|
|
170
|
+
Pane --> UIWidgets
|
|
171
|
+
Pane --> LLM
|
|
172
|
+
CommandPane --> Pane
|
|
173
|
+
UIWidgets --> Diff
|
|
174
|
+
|
|
175
|
+
AgentLoop --> LLM
|
|
176
|
+
AgentLoop --> PromptMod
|
|
177
|
+
AgentLoop --> ToolsMod
|
|
178
|
+
AgentLoop --> SessionMod
|
|
179
|
+
AgentLoop --> Compaction
|
|
180
|
+
AgentLoop --> Retry
|
|
181
|
+
AgentLoop --> Mentions
|
|
182
|
+
AgentLoop -. "spawn_agent / run_background" .-> Supervisor
|
|
183
|
+
|
|
184
|
+
Aside --> Retry
|
|
185
|
+
Aside --> SessionMod
|
|
186
|
+
|
|
187
|
+
PromptMod --> Skills
|
|
188
|
+
Compaction --> ModelWindows
|
|
189
|
+
|
|
190
|
+
Supervisor --> Jobs
|
|
191
|
+
Supervisor --> AgentTypes
|
|
192
|
+
Supervisor --> ToolsMod
|
|
193
|
+
Supervisor -. "launch callback" .-> App
|
|
194
|
+
Jobs --> AgentLoop
|
|
195
|
+
|
|
196
|
+
AgentTypes --> ToolsMod
|
|
197
|
+
AgentTypes --> Config
|
|
198
|
+
Skills --> Config
|
|
199
|
+
Config --> LLM
|
|
200
|
+
```
|
|
201
|
+
|
|
102
202
|
## Telemetry
|
|
103
203
|
|
|
104
204
|
Each launch sends one anonymous event to Google Analytics: a random install id, the launch mode, the version, the OS name and the configured provider and model name. Nothing from your sessions, prompts, files or credentials is included. Set `PAIMON_NO_TELEMETRY=1` or `DO_NOT_TRACK=1` to turn it off.
|
|
@@ -26,10 +26,14 @@ uvx paimon
|
|
|
26
26
|
|
|
27
27
|
首次启动会询问 provider、模型、API base 和 key。之后输入要完成的任务即可。在提示中写 `@path/to/file` 可以把文件提供给 agent。
|
|
28
28
|
|
|
29
|
-
运行时:`Shift+Tab` 切换 agent 的自主程度(**read** 写文件或执行命令前先询问,**edit** 放行工作目录内的编辑,**yolo** 从不询问,也是默认值),`Esc` 打断当前回合,`Ctrl+P` 打开命令面板,`Ctrl+C`
|
|
29
|
+
运行时:`Shift+Tab` 切换 agent 的自主程度(**read** 写文件或执行命令前先询问,**edit** 放行工作目录内的编辑,**yolo** 从不询问,也是默认值),`Esc` 打断当前回合,`Ctrl+P` 打开命令面板,`Ctrl+C` 退出。以 `!` 开头的一行不发给模型,而是直接在 shell 里执行,输出 Paimon 也能看到。`!!` 则不告诉它。
|
|
30
30
|
|
|
31
31
|
`Ctrl+T` 在新 pane 里打开另一个会话,`Ctrl+W` 关闭当前 pane,`Ctrl+PageUp` 和 `Ctrl+PageDown` 在 pane 之间切换,`Ctrl+G` 跳到正在等待授权的 pane。Paimon 自己也能开 pane:让它同时做两件互不相干的事,它会在新 tab 里起第二个 agent。它也能把一条命令留在单独的 tab 里跑,比如开发服务器或者文件监视,不占着当前回合。
|
|
32
32
|
|
|
33
|
+
## Skills
|
|
34
|
+
|
|
35
|
+
Paimon 会从 `~/.config/paimon/skills`、`~/.agents/skills` 以及工作目录到仓库根之间每一层的 `.agents/skills` 加载 [Agent Skills](https://agentskills.io)。system prompt 里只放每个 skill 的名字和描述,任务匹配时模型自己去读 `SKILL.md`,也可以用 `/skill:name 参数` 显式发送(`/` 命令面板里列出了它们)。其他位置可以写进 `config.json` 的 `"skills": ["~/.claude/skills"]`,或者用命令行参数 `--skill PATH`;`--no-skills` 跳过默认位置。同名时显式指定的优先于项目的,项目的优先于全局的。
|
|
36
|
+
|
|
33
37
|
## 当作 subagent 使用
|
|
34
38
|
|
|
35
39
|
前沿模型擅长制定计划和验收结果,中间的执行步骤往往比较机械。让 Paimon 使用成本较低的模型执行,由 Claude Code 或 Codex 制定计划并检查结果。用一个 profile 单独保存该模型的账号:
|
|
@@ -99,6 +103,102 @@ paimon --profile work # 单独配置的另一个账号
|
|
|
99
103
|
|
|
100
104
|
read 和 edit 模式会不经询问执行一小组明确只读的命令(`ls`、`cat`、`git status` 等),`--strict` 可以关掉。**这是防止 agent 失误的护栏,不是安全边界。** 需要真正的隔离时,请在容器或虚拟机中运行 Paimon。
|
|
101
105
|
|
|
106
|
+
## 架构
|
|
107
|
+
|
|
108
|
+
`Agent.run` 产出一串与 UI 无关的事件流,TUI、`--web` 和无头模式都是这条事件流的渲染器。`Supervisor` 位于 agent 和它启动的子 agent、后台命令之间,是两者共同的权限边界。
|
|
109
|
+
|
|
110
|
+
```mermaid
|
|
111
|
+
flowchart TD
|
|
112
|
+
subgraph entry["入口"]
|
|
113
|
+
CLI["cli.py"]
|
|
114
|
+
Commands["commands.py<br/>status / login / sessions"]
|
|
115
|
+
Headless["headless.py<br/>-p,单次运行"]
|
|
116
|
+
App["app.py<br/>Textual TUI / --web"]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
subgraph tui["TUI 组件"]
|
|
120
|
+
Pane["pane.py<br/>SessionPane"]
|
|
121
|
+
CommandPane["commandpane.py<br/>后台命令 pane"]
|
|
122
|
+
Tabs["tabs.py<br/>pane 标签栏"]
|
|
123
|
+
Login["login.py<br/>provider / 模型 / key"]
|
|
124
|
+
UIWidgets["ui.py<br/>输入框、确认面板"]
|
|
125
|
+
Diff["diff.py<br/>并排 diff 渲染"]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
subgraph core["Agent 主循环"]
|
|
129
|
+
AgentLoop["agent.py<br/>Agent.run()"]
|
|
130
|
+
LLM["llm.py<br/>build_model()"]
|
|
131
|
+
PromptMod["prompt.py<br/>system prompt"]
|
|
132
|
+
ToolsMod["tools.py<br/>工具注册表"]
|
|
133
|
+
SessionMod["session.py<br/>JSONL 持久化"]
|
|
134
|
+
Compaction["compaction.py"]
|
|
135
|
+
ModelWindows["model_windows.py<br/>各模型上下文窗口大小"]
|
|
136
|
+
Retry["retry.py"]
|
|
137
|
+
Mentions["mentions.py<br/>@path 展开"]
|
|
138
|
+
Aside["aside.py<br/>回合外提问,不落盘"]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
subgraph concurrency["任务与子 agent"]
|
|
142
|
+
Supervisor["supervisor.py<br/>任务池、权限"]
|
|
143
|
+
Jobs["jobs.py<br/>AgentJob / CommandJob"]
|
|
144
|
+
AgentTypes["agents.py<br/>子 agent 类型"]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
subgraph support["配置与 skills"]
|
|
148
|
+
Config["config.py<br/>profile、凭证"]
|
|
149
|
+
Skills["skills.py<br/>Agent Skills 发现"]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
CLI --> Commands
|
|
153
|
+
CLI --> Headless
|
|
154
|
+
CLI --> App
|
|
155
|
+
CLI --> Config
|
|
156
|
+
|
|
157
|
+
Headless --> AgentLoop
|
|
158
|
+
Headless --> Mentions
|
|
159
|
+
|
|
160
|
+
App --> Pane
|
|
161
|
+
App --> CommandPane
|
|
162
|
+
App --> Tabs
|
|
163
|
+
App --> Login
|
|
164
|
+
App --> Supervisor
|
|
165
|
+
App --> Config
|
|
166
|
+
|
|
167
|
+
Pane --> AgentLoop
|
|
168
|
+
Pane --> Aside
|
|
169
|
+
Pane --> Diff
|
|
170
|
+
Pane --> UIWidgets
|
|
171
|
+
Pane --> LLM
|
|
172
|
+
CommandPane --> Pane
|
|
173
|
+
UIWidgets --> Diff
|
|
174
|
+
|
|
175
|
+
AgentLoop --> LLM
|
|
176
|
+
AgentLoop --> PromptMod
|
|
177
|
+
AgentLoop --> ToolsMod
|
|
178
|
+
AgentLoop --> SessionMod
|
|
179
|
+
AgentLoop --> Compaction
|
|
180
|
+
AgentLoop --> Retry
|
|
181
|
+
AgentLoop --> Mentions
|
|
182
|
+
AgentLoop -. "spawn_agent / run_background" .-> Supervisor
|
|
183
|
+
|
|
184
|
+
Aside --> Retry
|
|
185
|
+
Aside --> SessionMod
|
|
186
|
+
|
|
187
|
+
PromptMod --> Skills
|
|
188
|
+
Compaction --> ModelWindows
|
|
189
|
+
|
|
190
|
+
Supervisor --> Jobs
|
|
191
|
+
Supervisor --> AgentTypes
|
|
192
|
+
Supervisor --> ToolsMod
|
|
193
|
+
Supervisor -. "启动回调" .-> App
|
|
194
|
+
Jobs --> AgentLoop
|
|
195
|
+
|
|
196
|
+
AgentTypes --> ToolsMod
|
|
197
|
+
AgentTypes --> Config
|
|
198
|
+
Skills --> Config
|
|
199
|
+
Config --> LLM
|
|
200
|
+
```
|
|
201
|
+
|
|
102
202
|
## 遥测
|
|
103
203
|
|
|
104
204
|
每次启动会向 Google Analytics 发送一条匿名事件:随机生成的安装 id、启动方式、版本号、操作系统名称,以及配置的 provider 和模型名。会话、提示词、文件和密钥一概不上报。设置 `PAIMON_NO_TELEMETRY=1` 或 `DO_NOT_TRACK=1` 可以关闭。
|
|
@@ -10,7 +10,7 @@ import json
|
|
|
10
10
|
import time
|
|
11
11
|
from dataclasses import dataclass
|
|
12
12
|
from pathlib import Path
|
|
13
|
-
from typing import AsyncIterator, Callable, Optional
|
|
13
|
+
from typing import AsyncIterator, Callable, Optional, Sequence
|
|
14
14
|
|
|
15
15
|
from pydantic_ai.direct import model_request_stream
|
|
16
16
|
from pydantic_ai.messages import (
|
|
@@ -30,18 +30,22 @@ from pydantic_ai.messages import (
|
|
|
30
30
|
)
|
|
31
31
|
from pydantic_ai.models import Model, ModelRequestParameters
|
|
32
32
|
|
|
33
|
-
from . import aside, compaction, retry, tools
|
|
33
|
+
from . import agents, aside, compaction, retry, tools
|
|
34
34
|
from .config import Config
|
|
35
35
|
from .llm import NoModelError, build_model, user_agent
|
|
36
36
|
from .mentions import expand_mentions
|
|
37
37
|
from .prompt import build_system_prompt
|
|
38
|
+
from .skills import Skill, SkillDiagnostic, discover_skills, expand_skill_command
|
|
38
39
|
from .session import (
|
|
39
40
|
Session,
|
|
40
41
|
SessionIncompleteError,
|
|
41
42
|
agents_message,
|
|
42
43
|
agents_text,
|
|
43
44
|
is_agents_message,
|
|
45
|
+
is_shell_message,
|
|
44
46
|
is_summary_message,
|
|
47
|
+
shell_message,
|
|
48
|
+
shell_text,
|
|
45
49
|
)
|
|
46
50
|
|
|
47
51
|
# Transient compaction failures tolerated in one turn before it is left off.
|
|
@@ -166,13 +170,26 @@ class AgentsNotice:
|
|
|
166
170
|
text: str
|
|
167
171
|
|
|
168
172
|
|
|
173
|
+
@dataclass
|
|
174
|
+
class ShellRun:
|
|
175
|
+
"""A command the user ran themselves with the "!" prefix.
|
|
176
|
+
|
|
177
|
+
Like AgentsNotice this is not replay-only: the run is persisted as a
|
|
178
|
+
synthetic user message, so it is both yielded when it happens and rebuilt
|
|
179
|
+
when the session is resumed.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
command: str
|
|
183
|
+
output: str
|
|
184
|
+
|
|
185
|
+
|
|
169
186
|
# Everything ``Agent.run`` and ``replay_events`` can yield. Renderers dispatch
|
|
170
187
|
# on isinstance; the alias exists so a type checker can flag an unhandled one.
|
|
171
188
|
AgentEvent = (
|
|
172
189
|
TextDelta | ReasoningDelta | ToolStart | ToolEnd | TodosUpdate
|
|
173
190
|
| SessionHandoff | RequestStats | ToolBudgetExhausted | TurnEnd
|
|
174
191
|
| ContextCompacted | ContextCompactionFailed | ModelRetry | UserInput
|
|
175
|
-
| CompactionNotice | AgentsNotice
|
|
192
|
+
| CompactionNotice | AgentsNotice | ShellRun
|
|
176
193
|
)
|
|
177
194
|
|
|
178
195
|
|
|
@@ -207,6 +224,9 @@ def replay_events(messages: list[ModelMessage]) -> list[AgentEvent]:
|
|
|
207
224
|
if is_agents_message(message):
|
|
208
225
|
events.append(AgentsNotice(agents_text(message)))
|
|
209
226
|
continue
|
|
227
|
+
if is_shell_message(message):
|
|
228
|
+
events.append(ShellRun(*shell_text(message)))
|
|
229
|
+
continue
|
|
210
230
|
if isinstance(message, ModelRequest):
|
|
211
231
|
for part in message.parts:
|
|
212
232
|
if isinstance(part, UserPromptPart) and isinstance(part.content, str) and part.content:
|
|
@@ -287,10 +307,13 @@ def _strip_foreign_thinking(history: list[ModelMessage], model: Model) -> list[M
|
|
|
287
307
|
# Re-exported so UI code can keep importing it from here.
|
|
288
308
|
ConfirmFn = tools.ConfirmFn
|
|
289
309
|
|
|
290
|
-
# Takes the
|
|
291
|
-
#
|
|
292
|
-
#
|
|
293
|
-
|
|
310
|
+
# Takes what the user queued while a turn was already running, clearing the
|
|
311
|
+
# queue as it goes. The loop calls it before every model request, so what it
|
|
312
|
+
# returns reaches the model at the next step rather than the next turn. A
|
|
313
|
+
# plain string is a typed prompt and goes through the usual expansion; a
|
|
314
|
+
# ready-made request is one paimon assembled (a "!" shell run) and is appended
|
|
315
|
+
# verbatim, since @path in a command's output is output, not a mention.
|
|
316
|
+
PendingFn = Callable[[], list[str | ModelRequest]]
|
|
294
317
|
|
|
295
318
|
|
|
296
319
|
class Agent:
|
|
@@ -312,8 +335,20 @@ class Agent:
|
|
|
312
335
|
confirm: Optional[ConfirmFn] = None, mode: str = "yolo",
|
|
313
336
|
config: Optional[Config] = None,
|
|
314
337
|
toolset: Optional[dict[str, tools.Tool]] = None,
|
|
315
|
-
model_override: Optional[str] = None
|
|
338
|
+
model_override: Optional[str] = None,
|
|
339
|
+
skills: Sequence[Skill] = (),
|
|
340
|
+
skill_diagnostics: Sequence[SkillDiagnostic] = (),
|
|
341
|
+
agent_types: Sequence[agents.AgentType] = (),
|
|
342
|
+
agent_type_diagnostics: Sequence[SkillDiagnostic] = ()):
|
|
316
343
|
self.cwd = Path(cwd or Path.cwd())
|
|
344
|
+
# The skills listed in the system prompt and expandable by /skill:name,
|
|
345
|
+
# with whatever discovery had to complain about (for the UI to show).
|
|
346
|
+
self.skills = list(skills)
|
|
347
|
+
self.skill_diagnostics = list(skill_diagnostics)
|
|
348
|
+
# The agent types spawn_agent may name, discovered like skills; the
|
|
349
|
+
# launcher reads them off whoever called spawn_agent.
|
|
350
|
+
self.agent_types = list(agent_types)
|
|
351
|
+
self.agent_type_diagnostics = list(agent_type_diagnostics)
|
|
317
352
|
self.confirm = confirm
|
|
318
353
|
self.mode = mode
|
|
319
354
|
self.config = config or Config.load()
|
|
@@ -339,6 +374,12 @@ class Agent:
|
|
|
339
374
|
self.history: list[ModelMessage] = session.messages()
|
|
340
375
|
# This agent's tool set; None means everything in tools.REGISTRY.
|
|
341
376
|
self.toolset = dict(tools.REGISTRY if toolset is None else toolset)
|
|
377
|
+
# The registry's spawn_agent knows no types; this agent's copy lists
|
|
378
|
+
# the ones it discovered, so the schema below advertises them. A
|
|
379
|
+
# toolset without spawn_agent (subagents, headless) skips it whole.
|
|
380
|
+
if "spawn_agent" in self.toolset and self.agent_types:
|
|
381
|
+
self.toolset["spawn_agent"] = agents.spawn_tool_with_types(
|
|
382
|
+
self.toolset["spawn_agent"], self.agent_types)
|
|
342
383
|
self.tool_schemas = tools.schemas(self.toolset)
|
|
343
384
|
self._tool_definitions = tools.definitions(self.toolset)
|
|
344
385
|
self._cached_model: Optional[tuple[tuple, Model]] = None
|
|
@@ -356,19 +397,23 @@ class Agent:
|
|
|
356
397
|
append_system_prompt: Optional[str] = None,
|
|
357
398
|
toolset: Optional[dict[str, tools.Tool]] = None,
|
|
358
399
|
model_override: Optional[str] = None,
|
|
359
|
-
|
|
400
|
+
parent_session_id: Optional[str] = None,
|
|
401
|
+
agent_type: Optional[str] = None) -> "Agent":
|
|
360
402
|
"""Start a new session, or resume ``session``, and take its lock.
|
|
361
403
|
|
|
362
404
|
``append_system_prompt`` is added to the end of a new session's system
|
|
363
405
|
prompt and persisted with it, so a resumed session keeps it. Resuming
|
|
364
406
|
with it set raises ``ValueError``: the persisted role is immutable.
|
|
365
|
-
The dynamic parts of the prompt (date, environment, AGENTS.md
|
|
366
|
-
rebuilt on resume — a session picked up months later
|
|
367
|
-
telling the model the old date or stale project rules —
|
|
368
|
-
changed, the rebuilt snapshot is appended to the log, so
|
|
369
|
-
records the prompt each turn actually ran with.
|
|
370
|
-
``
|
|
371
|
-
the session listings its parent shows up in
|
|
407
|
+
The dynamic parts of the prompt (date, environment, AGENTS.md, the
|
|
408
|
+
skill list) are rebuilt on resume — a session picked up months later
|
|
409
|
+
must not keep telling the model the old date or stale project rules —
|
|
410
|
+
and when they changed, the rebuilt snapshot is appended to the log, so
|
|
411
|
+
it always records the prompt each turn actually ran with.
|
|
412
|
+
``parent_session_id`` marks the new session as a subagent's, which
|
|
413
|
+
keeps it out of the session listings its parent shows up in;
|
|
414
|
+
``agent_type`` records what type it was spawned as, so resuming the
|
|
415
|
+
session can restore the type's narrowing. Both apply to new sessions
|
|
416
|
+
only and are ignored on resume, where the header already says.
|
|
372
417
|
|
|
373
418
|
Raises ``SessionBusyError`` when the session is already open (here or
|
|
374
419
|
in another process) and ``SessionIncompleteError`` when a resumed log
|
|
@@ -378,9 +423,14 @@ class Agent:
|
|
|
378
423
|
cwd = Path(cwd or Path.cwd())
|
|
379
424
|
if session is not None and append_system_prompt:
|
|
380
425
|
raise ValueError("append_system_prompt only applies to a new session")
|
|
426
|
+
config = config or Config.load()
|
|
427
|
+
skills, skill_diagnostics = discover_skills(
|
|
428
|
+
cwd, extra_paths=config.skills, include_defaults=config.include_default_skills)
|
|
429
|
+
agent_types, agent_type_diagnostics = agents.discover_agent_types(
|
|
430
|
+
cwd, extra_paths=config.agents, include_defaults=config.include_default_agents)
|
|
381
431
|
is_new = session is None
|
|
382
432
|
if session is None:
|
|
383
|
-
session = Session.create(cwd,
|
|
433
|
+
session = Session.create(cwd, parent_session_id, agent_type)
|
|
384
434
|
session.lock()
|
|
385
435
|
# Everything after the lock can fail — a full disk while writing the
|
|
386
436
|
# prompt, a message the current pydantic-ai cannot parse — and no
|
|
@@ -388,7 +438,7 @@ class Agent:
|
|
|
388
438
|
try:
|
|
389
439
|
if is_new:
|
|
390
440
|
appended = append_system_prompt.strip() if append_system_prompt else None
|
|
391
|
-
system_prompt = build_system_prompt(cwd)
|
|
441
|
+
system_prompt = build_system_prompt(cwd, skills)
|
|
392
442
|
if appended:
|
|
393
443
|
system_prompt += f"\n\n{appended}"
|
|
394
444
|
session.append_system_prompt(system_prompt, appended=appended)
|
|
@@ -397,13 +447,16 @@ class Agent:
|
|
|
397
447
|
stored, appended = session.system_prompt_parts()
|
|
398
448
|
if stored is None:
|
|
399
449
|
raise SessionIncompleteError("Session does not contain a persisted system prompt")
|
|
400
|
-
system_prompt = build_system_prompt(cwd)
|
|
450
|
+
system_prompt = build_system_prompt(cwd, skills)
|
|
401
451
|
if appended:
|
|
402
452
|
system_prompt += f"\n\n{appended}"
|
|
403
453
|
if system_prompt != stored:
|
|
404
454
|
session.append_system_prompt(system_prompt, appended=appended)
|
|
405
455
|
return cls(session, system_prompt, cwd=cwd, confirm=confirm, mode=mode,
|
|
406
|
-
config=config, toolset=toolset, model_override=model_override
|
|
456
|
+
config=config, toolset=toolset, model_override=model_override,
|
|
457
|
+
skills=skills, skill_diagnostics=skill_diagnostics,
|
|
458
|
+
agent_types=agent_types,
|
|
459
|
+
agent_type_diagnostics=agent_type_diagnostics)
|
|
407
460
|
except BaseException:
|
|
408
461
|
session.unlock()
|
|
409
462
|
raise
|
|
@@ -554,6 +607,27 @@ class Agent:
|
|
|
554
607
|
max_tokens=max_tokens):
|
|
555
608
|
yield delta
|
|
556
609
|
|
|
610
|
+
# ---- Commands the user runs themselves ---------------------------------
|
|
611
|
+
|
|
612
|
+
async def run_shell(self, command: str) -> str:
|
|
613
|
+
"""Run a shell command on the user's behalf and return what it printed.
|
|
614
|
+
|
|
615
|
+
The TUI's "!" prefix. No permission gate: the gate exists so the model
|
|
616
|
+
cannot run something the user did not ask for, and here the user *is*
|
|
617
|
+
the one asking. Nothing is recorded — the caller decides whether the
|
|
618
|
+
run reaches the model now or at the running turn's next step.
|
|
619
|
+
"""
|
|
620
|
+
return await tools.user_shell(command, self.cwd, self.tool_context)
|
|
621
|
+
|
|
622
|
+
def record_shell(self, command: str, output: str) -> None:
|
|
623
|
+
"""Persist a "!" run so the model sees what the user just did.
|
|
624
|
+
|
|
625
|
+
Only between turns. While one is running the message belongs in the
|
|
626
|
+
pending queue instead, which the loop drains between steps — appended
|
|
627
|
+
here it would land between a tool call and its result.
|
|
628
|
+
"""
|
|
629
|
+
self._append_message(shell_message(command, output))
|
|
630
|
+
|
|
557
631
|
# ---- Tools the agent loop runs itself ----------------------------------
|
|
558
632
|
# These mutate agent-held state or end the turn, so they cannot go through
|
|
559
633
|
# the stateless tools.run_tool. Each handler fills ``slot`` with the tool
|
|
@@ -659,10 +733,25 @@ class Agent:
|
|
|
659
733
|
"stop_job": _run_supervised,
|
|
660
734
|
}
|
|
661
735
|
|
|
662
|
-
|
|
736
|
+
def expand_input(self, text: str) -> str:
|
|
737
|
+
"""What a typed message becomes: a /skill:name command expands to the
|
|
738
|
+
skill's content, then @path mentions are inlined."""
|
|
739
|
+
def mentions(part: str) -> str:
|
|
740
|
+
return expand_mentions(part, self.cwd)
|
|
741
|
+
|
|
742
|
+
expanded = expand_skill_command(text, self.skills, expand_args=mentions)
|
|
743
|
+
# Unchanged (the same object back) means it was not a skill command.
|
|
744
|
+
return mentions(text) if expanded is text else expanded
|
|
745
|
+
|
|
746
|
+
async def run(self, user_input: Optional[str], *, expand: bool = True,
|
|
663
747
|
max_tool_calls: Optional[int] = None) -> AsyncIterator[AgentEvent]:
|
|
664
748
|
"""Run one user turn to completion, yielding events along the way.
|
|
665
749
|
|
|
750
|
+
``user_input`` may be None: a wake-up turn, run so the model reacts to
|
|
751
|
+
agents that finished — the status line injected below is its only new
|
|
752
|
+
input. When there is nothing to report either, the turn never happened:
|
|
753
|
+
no events, nothing persisted, no model request.
|
|
754
|
+
|
|
666
755
|
``expand=False`` skips @path expansion, for callers that assembled the
|
|
667
756
|
prompt themselves and must not have unrelated text rewritten (piped
|
|
668
757
|
stdin, where a line like ``@foo.py`` is data rather than a mention).
|
|
@@ -674,16 +763,19 @@ class Agent:
|
|
|
674
763
|
an explicit refusal persisted as its result, ToolBudgetExhausted is
|
|
675
764
|
yielded and the turn ends.
|
|
676
765
|
"""
|
|
677
|
-
prompt = expand_mentions(user_input, self.cwd) if expand else user_input
|
|
678
766
|
# Agents this session started report in here, at the top of the next
|
|
679
767
|
# turn, rather than by interrupting whatever the user is typing. It has
|
|
680
768
|
# to be a persisted message: an event the model never sees would defeat
|
|
681
769
|
# the point, which is to get it to call read_job.
|
|
682
770
|
summary = self.supervisor.status_summary(self) if self.supervisor is not None else None
|
|
771
|
+
if user_input is None and not summary:
|
|
772
|
+
return
|
|
683
773
|
if summary:
|
|
684
774
|
self._append_message(agents_message(summary))
|
|
685
775
|
yield AgentsNotice(summary)
|
|
686
|
-
|
|
776
|
+
if user_input is not None:
|
|
777
|
+
prompt = self.expand_input(user_input) if expand else user_input
|
|
778
|
+
self._append_message(ModelRequest(parts=[UserPromptPart(content=prompt)]))
|
|
687
779
|
|
|
688
780
|
# Every turn leaves exactly one terminal ``turn_end`` record in the
|
|
689
781
|
# session: success, error (with the exception), max_tool_calls or
|
|
@@ -742,8 +834,16 @@ class Agent:
|
|
|
742
834
|
# results already recorded. Before compaction, so they count
|
|
743
835
|
# towards the context check and survive a history replacement.
|
|
744
836
|
for queued in (self.pending() if self.pending is not None else []):
|
|
837
|
+
# A ready-made request is one the caller already showed the
|
|
838
|
+
# user (a "!" run, whose output was on screen the moment the
|
|
839
|
+
# command exited), so it is appended without an event of its
|
|
840
|
+
# own — and without expansion, since @path in a command's
|
|
841
|
+
# output is output rather than a mention.
|
|
842
|
+
if isinstance(queued, ModelRequest):
|
|
843
|
+
self._append_message(queued)
|
|
844
|
+
continue
|
|
745
845
|
self._append_message(ModelRequest(
|
|
746
|
-
parts=[UserPromptPart(content=
|
|
846
|
+
parts=[UserPromptPart(content=self.expand_input(queued))]))
|
|
747
847
|
yield UserInput(queued)
|
|
748
848
|
|
|
749
849
|
if not compaction_off:
|