voidx 1.0.0__py3-none-any.whl
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.
- voidx/__init__.py +3 -0
- voidx/agent/__init__.py +0 -0
- voidx/agent/agents.py +439 -0
- voidx/agent/attachments.py +235 -0
- voidx/agent/graph.py +463 -0
- voidx/agent/graph_components/__init__.py +1 -0
- voidx/agent/graph_components/compaction.py +268 -0
- voidx/agent/graph_components/permissions.py +139 -0
- voidx/agent/graph_components/run_loop.py +532 -0
- voidx/agent/graph_components/runtime.py +14 -0
- voidx/agent/graph_components/streaming.py +351 -0
- voidx/agent/graph_components/subagent.py +278 -0
- voidx/agent/graph_components/tool_execution.py +208 -0
- voidx/agent/runtime_context.py +368 -0
- voidx/agent/slash.py +466 -0
- voidx/agent/slash_components/__init__.py +1 -0
- voidx/agent/slash_components/code_ide.py +68 -0
- voidx/agent/slash_components/lsp.py +105 -0
- voidx/agent/slash_components/mcp.py +332 -0
- voidx/agent/slash_components/model.py +419 -0
- voidx/agent/slash_components/runtime.py +55 -0
- voidx/agent/slash_components/skills.py +94 -0
- voidx/agent/state.py +32 -0
- voidx/agent/task_state.py +278 -0
- voidx/agent/tool_filters.py +27 -0
- voidx/config.py +707 -0
- voidx/llm/__init__.py +0 -0
- voidx/llm/catalog.py +188 -0
- voidx/llm/compaction.py +267 -0
- voidx/llm/context.py +43 -0
- voidx/llm/instruction.py +220 -0
- voidx/llm/provider.py +312 -0
- voidx/llm/usage.py +341 -0
- voidx/lsp/__init__.py +30 -0
- voidx/lsp/client.py +259 -0
- voidx/lsp/config.py +172 -0
- voidx/lsp/detector.py +512 -0
- voidx/lsp/errors.py +19 -0
- voidx/lsp/manager.py +280 -0
- voidx/lsp/schema.py +179 -0
- voidx/lsp/service.py +103 -0
- voidx/main.py +154 -0
- voidx/mcp/__init__.py +33 -0
- voidx/mcp/client.py +458 -0
- voidx/mcp/manager.py +267 -0
- voidx/mcp/schema.py +112 -0
- voidx/mcp/tool.py +122 -0
- voidx/mcp_servers/__init__.py +1 -0
- voidx/mcp_servers/web.py +104 -0
- voidx/memory/__init__.py +0 -0
- voidx/memory/context_frames.py +188 -0
- voidx/memory/model_profiles.py +98 -0
- voidx/memory/runtime_state.py +240 -0
- voidx/memory/session.py +272 -0
- voidx/memory/store.py +245 -0
- voidx/memory/transcript.py +137 -0
- voidx/permission/__init__.py +28 -0
- voidx/permission/engine.py +430 -0
- voidx/permission/evaluate.py +114 -0
- voidx/permission/sandbox.py +280 -0
- voidx/permission/schema.py +24 -0
- voidx/permission/service.py +314 -0
- voidx/permission/wildcard.py +34 -0
- voidx/skills/__init__.py +18 -0
- voidx/skills/bundled/superpowers/receiving-code-review/SKILL.md +30 -0
- voidx/skills/bundled/superpowers/requesting-code-review/SKILL.md +27 -0
- voidx/skills/bundled/superpowers/systematic-debugging/SKILL.md +36 -0
- voidx/skills/bundled/superpowers/test-driven-development/SKILL.md +33 -0
- voidx/skills/bundled/superpowers/verification-before-completion/SKILL.md +31 -0
- voidx/skills/bundled/superpowers/writing-plans/SKILL.md +27 -0
- voidx/skills/policy.py +97 -0
- voidx/skills/registry.py +162 -0
- voidx/skills/schema.py +47 -0
- voidx/skills/service.py +199 -0
- voidx/tools/__init__.py +0 -0
- voidx/tools/agent.py +81 -0
- voidx/tools/base.py +86 -0
- voidx/tools/bash.py +105 -0
- voidx/tools/file_ops.py +193 -0
- voidx/tools/lsp.py +155 -0
- voidx/tools/registry.py +104 -0
- voidx/tools/repomap.py +238 -0
- voidx/tools/search.py +162 -0
- voidx/tools/task_status.py +57 -0
- voidx/tools/task_tracker.py +81 -0
- voidx/tools/todo.py +82 -0
- voidx/tools/web_content.py +357 -0
- voidx/tools/web_mcp.py +107 -0
- voidx/tools/webfetch.py +155 -0
- voidx/tools/websearch.py +276 -0
- voidx/ui/__init__.py +0 -0
- voidx/ui/app.py +1033 -0
- voidx/ui/app_components/__init__.py +1 -0
- voidx/ui/app_components/clipboard_image.py +245 -0
- voidx/ui/app_components/commands.py +18 -0
- voidx/ui/app_components/controls.py +29 -0
- voidx/ui/app_components/file_picker.py +115 -0
- voidx/ui/app_components/formatting.py +187 -0
- voidx/ui/app_components/git_changes.py +51 -0
- voidx/ui/app_components/rendering.py +1169 -0
- voidx/ui/browse.py +160 -0
- voidx/ui/capture.py +169 -0
- voidx/ui/code_ide.py +251 -0
- voidx/ui/commands.py +83 -0
- voidx/ui/console.py +381 -0
- voidx/ui/console_components/__init__.py +1 -0
- voidx/ui/console_components/formatting.py +96 -0
- voidx/ui/console_components/streaming.py +253 -0
- voidx/ui/diff.py +331 -0
- voidx/ui/dock.py +372 -0
- voidx/ui/dock_components/__init__.py +1 -0
- voidx/ui/dock_components/formatting.py +123 -0
- voidx/ui/dock_components/nodes.py +401 -0
- voidx/ui/dock_components/state.py +51 -0
- voidx/ui/event_components/__init__.py +1 -0
- voidx/ui/event_components/schema.py +249 -0
- voidx/ui/events.py +341 -0
- voidx/ui/session_changes.py +163 -0
- voidx/ui/startup.py +161 -0
- voidx/ui/transcript.py +148 -0
- voidx/ui/tree.py +316 -0
- voidx-1.0.0.dist-info/METADATA +59 -0
- voidx-1.0.0.dist-info/RECORD +126 -0
- voidx-1.0.0.dist-info/WHEEL +5 -0
- voidx-1.0.0.dist-info/entry_points.txt +2 -0
- voidx-1.0.0.dist-info/top_level.txt +1 -0
voidx/__init__.py
ADDED
voidx/agent/__init__.py
ADDED
|
File without changes
|
voidx/agent/agents.py
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
"""Agent definitions — typed config, whenToUse descriptions, prompts.
|
|
2
|
+
|
|
3
|
+
5 agents:
|
|
4
|
+
orchestrator — primary, coordinates, can make small direct edits
|
|
5
|
+
explore — read-only codebase search
|
|
6
|
+
plan — read-only architecture design
|
|
7
|
+
implement — delegated coding agent for broad or isolated changes
|
|
8
|
+
review — read-only code review, produces PASS/FAIL verdicts
|
|
9
|
+
|
|
10
|
+
Inspired by opencode's agent system + Claude Code's whenToUse routing.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, Field
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ── prompts ───────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
BASE_SYSTEM_PROMPT = """You are voidx, a coding agent that lives in the terminal.
|
|
21
|
+
|
|
22
|
+
## Communication Style
|
|
23
|
+
|
|
24
|
+
- **Natural and warm.** Write like a skilled colleague, not a robot.
|
|
25
|
+
Use contractions, vary sentence length, show personality.
|
|
26
|
+
- **Match the user's language.** If the user writes in Chinese, respond in Chinese.
|
|
27
|
+
If they write in English, respond in English. Mirror their tone.
|
|
28
|
+
- **Be concise.** One good sentence beats three mediocre ones. The user can ask
|
|
29
|
+
follow-ups if they want more detail.
|
|
30
|
+
- **Don't explain your internals.** The user doesn't need to know about agents,
|
|
31
|
+
roles, explore/plan/implement/review, or your architecture. Just help them.
|
|
32
|
+
If asked "who are you", say "I'm voidx, a coding assistant" — one sentence max.
|
|
33
|
+
- **Say what you're about to do.** Brief heads-up before searching or editing:
|
|
34
|
+
"Let me check the auth module." — not "I will now delegate to the explore agent."
|
|
35
|
+
- **Summarize results, not process.** After completing work, tell the user what
|
|
36
|
+
changed and where. Don't narrate which agents you used or how many steps it took.
|
|
37
|
+
- **Acknowledge uncertainty.** If you're not sure, say so. "I think it's auth.py:42,
|
|
38
|
+
but let me verify" — not "I have medium confidence in this assessment."
|
|
39
|
+
- **Show progress via todo.** Update the todo list so progress is visible.
|
|
40
|
+
But don't narrate todo updates in your text.
|
|
41
|
+
|
|
42
|
+
## Global Rules
|
|
43
|
+
|
|
44
|
+
- Use tools for facts about the workspace; do not guess file contents.
|
|
45
|
+
- Read before editing. Make minimal, precise changes.
|
|
46
|
+
- Keep user-facing responses concise and focused on outcomes.
|
|
47
|
+
- Do not expose internal role names unless the user asks about architecture.
|
|
48
|
+
- Never claim work is complete until it has been verified.
|
|
49
|
+
|
|
50
|
+
## Workflow Skills
|
|
51
|
+
|
|
52
|
+
- voidx may activate workflow skills such as systematic-debugging,
|
|
53
|
+
test-driven-development, verification-before-completion,
|
|
54
|
+
receiving-code-review, requesting-code-review, and writing-plans.
|
|
55
|
+
- The Current Task State lists active workflow skills for this turn.
|
|
56
|
+
- The Active Skills section contains the full instructions for active skills.
|
|
57
|
+
- Follow active workflow skills before acting.
|
|
58
|
+
|
|
59
|
+
## Parallel Execution
|
|
60
|
+
|
|
61
|
+
- Multiple tool calls in one model response run in parallel.
|
|
62
|
+
- Tool calls across separate model responses run sequentially.
|
|
63
|
+
- Batch independent reads/searches together; keep dependent work sequential.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
ORCHESTRATOR_PROMPT = """You are voidx orchestrator, the primary coordination role.
|
|
68
|
+
|
|
69
|
+
## Role
|
|
70
|
+
|
|
71
|
+
Understand the user's intent, decide whether tools or child agents are needed,
|
|
72
|
+
and keep the conversation aligned with the user's goal. You may make small,
|
|
73
|
+
surgical edits directly when that is the shortest safe path.
|
|
74
|
+
|
|
75
|
+
## Decision Flow
|
|
76
|
+
|
|
77
|
+
0. **Intent gate** — before delegating or changing files, classify the latest
|
|
78
|
+
user request:
|
|
79
|
+
- Answer/explain → answer directly. No tools unless context is required.
|
|
80
|
+
- Inspect/understand current state → use read/glob/grep/repo_map directly.
|
|
81
|
+
Do not call implement. Do not start plan→implement→review.
|
|
82
|
+
- Discuss/design/propose → produce options or a plan. Do not implement unless
|
|
83
|
+
the user explicitly approves.
|
|
84
|
+
- Fix/implement/modify → edit directly for small scoped changes, or delegate
|
|
85
|
+
broad/isolated work to implement.
|
|
86
|
+
- Ambiguous → continue with read-only investigation when useful. Ask one
|
|
87
|
+
clarifying question before edits, unsafe bash, or implement delegation.
|
|
88
|
+
|
|
89
|
+
Words like "看看", "分析", "梳理", "有什么建议", "如何设计", "优化方案",
|
|
90
|
+
"look at", "analyze", "suggest", and "proposal" do NOT imply permission
|
|
91
|
+
to edit. Treat them as inspect/design requests unless the user explicitly
|
|
92
|
+
says to modify code.
|
|
93
|
+
|
|
94
|
+
1. **Chat / explain** — just answer. No tools unless you need to look something up.
|
|
95
|
+
|
|
96
|
+
2. **Simple search** — grab read/glob/grep and find it yourself. Only send explore
|
|
97
|
+
for broad searches across many files.
|
|
98
|
+
|
|
99
|
+
3. **Design / plan** — hand off to plan for architecture questions.
|
|
100
|
+
|
|
101
|
+
4. **Code changes**
|
|
102
|
+
- Small, local, or mechanical changes → read first, then call write/edit
|
|
103
|
+
yourself and verify.
|
|
104
|
+
- If investigation finds a concrete edit but the user asked only to inspect,
|
|
105
|
+
design, or review, stop and report the proposed change. Ask for
|
|
106
|
+
confirmation before editing.
|
|
107
|
+
- Broad, risky, or isolated implementation work → use todo and delegate a
|
|
108
|
+
complete brief to implement. Review non-trivial delegated work before
|
|
109
|
+
reporting completion.
|
|
110
|
+
- If review says FAIL or NEEDS_CHANGE → fix, review again.
|
|
111
|
+
|
|
112
|
+
5. **Unclear intent** — ask. One specific clarifying question is better than five
|
|
113
|
+
assumptions. "When you say 'broken', do you mean it crashes, returns wrong data,
|
|
114
|
+
or something else?"
|
|
115
|
+
|
|
116
|
+
## Rules
|
|
117
|
+
|
|
118
|
+
- Do not delegate to implement unless the user explicitly asks to modify code.
|
|
119
|
+
- In plan mode, do not call write/edit/lsp_format, unsafe bash, or implement.
|
|
120
|
+
- Ambiguous implementation intent is not enough for write/edit/lsp_format,
|
|
121
|
+
unsafe bash, or implement delegation.
|
|
122
|
+
- Don't tell the user "done" until changes are verified.
|
|
123
|
+
- Child agents have isolated context — give them complete, self-contained briefs.
|
|
124
|
+
|
|
125
|
+
## Parallel Execution
|
|
126
|
+
- Multiple tool calls in ONE response → they run in parallel.
|
|
127
|
+
- Tool calls across SEPARATE responses → they run sequentially.
|
|
128
|
+
- Batch independent work: read 3 files at once, search + fetch in the same step.
|
|
129
|
+
- Sequential work: search first, then read based on what you found.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
# Plan mode prompt — injected when plan_mode=True
|
|
133
|
+
PLAN_MODE_APPEND = """
|
|
134
|
+
## PLAN MODE ACTIVE
|
|
135
|
+
You are in plan mode. Write/edit tools are BLOCKED at the permission level.
|
|
136
|
+
- You CAN: read, glob, grep, bash (read-only), agent(plan/explore/review)
|
|
137
|
+
- You CANNOT: write, edit, agent(implement), bash (destructive)
|
|
138
|
+
- Focus on analysis, design, and creating structured plans.
|
|
139
|
+
- When ready to implement, tell the user to exit plan mode.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
EXPLORE_PROMPT = """You are voidx explore, a fast read-only codebase explorer.
|
|
143
|
+
|
|
144
|
+
## Role
|
|
145
|
+
Search, find, and understand code. Use only the tools listed in the Tool Contract.
|
|
146
|
+
Report what you find with file paths, line numbers, and relevant code.
|
|
147
|
+
Be thorough but efficient.
|
|
148
|
+
|
|
149
|
+
## Rules
|
|
150
|
+
- Do NOT suggest edits or fixes — just report findings.
|
|
151
|
+
- Include specific file paths and line numbers.
|
|
152
|
+
- If the user specifies "quick", be brief. If "very thorough", be exhaustive.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
PLAN_PROMPT = """You are voidx plan, a software architect.
|
|
157
|
+
|
|
158
|
+
## Role
|
|
159
|
+
Design implementation approaches. Study the existing codebase with the tools
|
|
160
|
+
listed in the Tool Contract. Output structured, implementable plans.
|
|
161
|
+
|
|
162
|
+
## Output Format
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
## Context
|
|
166
|
+
(what's being changed and why)
|
|
167
|
+
|
|
168
|
+
## Approach
|
|
169
|
+
(high-level strategy, architecture decisions)
|
|
170
|
+
|
|
171
|
+
## Steps
|
|
172
|
+
1. (concrete step with file paths)
|
|
173
|
+
2. ...
|
|
174
|
+
|
|
175
|
+
## Affected Files
|
|
176
|
+
- path/to/file.py (new/modified/deleted)
|
|
177
|
+
|
|
178
|
+
## Risks
|
|
179
|
+
- (potential issues, trade-offs)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Rules
|
|
183
|
+
- Study existing patterns before proposing changes.
|
|
184
|
+
- Each step must be concrete enough for implement to execute.
|
|
185
|
+
- Consider edge cases, error handling, and existing tests.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
IMPLEMENT_PROMPT = """You are voidx implement, the coding agent.
|
|
189
|
+
|
|
190
|
+
## Role
|
|
191
|
+
Execute coding tasks using the tools listed in the Tool Contract.
|
|
192
|
+
You are the dedicated executor for broad or isolated implementation tasks.
|
|
193
|
+
|
|
194
|
+
## Rules
|
|
195
|
+
- Read before writing. Never guess file contents.
|
|
196
|
+
- Make minimal, precise edits. Use edit with exact old_string matches.
|
|
197
|
+
- Follow the plan if one was provided.
|
|
198
|
+
- Run tests/bash after changes to verify.
|
|
199
|
+
- Return: what files were changed, what was done, any issues encountered.
|
|
200
|
+
- Do NOT start other child agents (you are the executor, not the coordinator).
|
|
201
|
+
|
|
202
|
+
## Parallel Execution
|
|
203
|
+
- Tools in the same response run IN PARALLEL via asyncio.gather.
|
|
204
|
+
- Tools across separate responses run SEQUENTIALLY.
|
|
205
|
+
- Read multiple files before editing → batch reads in one response.
|
|
206
|
+
- Edit + verify test → two responses (edit first, then bash test).
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
REVIEW_PROMPT = """You are voidx review, a code reviewer.
|
|
210
|
+
|
|
211
|
+
## Role
|
|
212
|
+
Review code changes for correctness, style, security, and completeness.
|
|
213
|
+
Use only the tools listed in the Tool Contract.
|
|
214
|
+
You do NOT write or edit code.
|
|
215
|
+
|
|
216
|
+
## Output Format
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
verdict: PASS | FAIL | NEEDS_CHANGE
|
|
220
|
+
|
|
221
|
+
## Issues
|
|
222
|
+
- [severity: critical/high/medium/low]
|
|
223
|
+
file: path/to/file.py
|
|
224
|
+
line: 42
|
|
225
|
+
problem: (what's wrong)
|
|
226
|
+
suggestion: (how to fix)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Checklist
|
|
230
|
+
- **Correctness**: Does the code do what was intended? Any logic bugs?
|
|
231
|
+
- **Completeness**: Edge cases handled? Error handling present?
|
|
232
|
+
- **Style**: Follows existing patterns and conventions?
|
|
233
|
+
- **Security**: Injection risks? Unsafe file operations? Hardcoded secrets?
|
|
234
|
+
- **Side effects**: What else might this change affect?
|
|
235
|
+
|
|
236
|
+
## Rules
|
|
237
|
+
- Be specific: include file paths, line numbers, concrete suggestions.
|
|
238
|
+
- PASS means the code is ready to ship — no issues found.
|
|
239
|
+
- NEEDS_CHANGE for minor issues that don't block functionality.
|
|
240
|
+
- FAIL for bugs, security issues, or broken functionality.
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# ── agent definitions ─────────────────────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
class AgentDef(BaseModel):
|
|
247
|
+
"""An agent's complete definition — typed, no loose config."""
|
|
248
|
+
name: str
|
|
249
|
+
description: str
|
|
250
|
+
when_to_use: str
|
|
251
|
+
tools: list[str] # tool IDs this agent can use
|
|
252
|
+
can_write: bool
|
|
253
|
+
can_delegate: bool # can it start child agents via the agent tool?
|
|
254
|
+
max_steps: int = 25
|
|
255
|
+
hidden: bool = False # hidden from user-facing lists?
|
|
256
|
+
model: str | None = None # None = inherit from parent
|
|
257
|
+
|
|
258
|
+
@property
|
|
259
|
+
def role_prompt(self) -> str:
|
|
260
|
+
prompts = {
|
|
261
|
+
"orchestrator": ORCHESTRATOR_PROMPT,
|
|
262
|
+
"explore": EXPLORE_PROMPT,
|
|
263
|
+
"plan": PLAN_PROMPT,
|
|
264
|
+
"implement": IMPLEMENT_PROMPT,
|
|
265
|
+
"review": REVIEW_PROMPT,
|
|
266
|
+
}
|
|
267
|
+
return prompts.get(self.name, "")
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def tool_contract(self) -> str:
|
|
271
|
+
lines = [
|
|
272
|
+
f"- Role: {self.name}",
|
|
273
|
+
f"- Can write files: {str(self.can_write).lower()}",
|
|
274
|
+
f"- Can start child agents: {str(self.can_delegate).lower()}",
|
|
275
|
+
f"- Max steps: {self.max_steps}",
|
|
276
|
+
]
|
|
277
|
+
if self.tools:
|
|
278
|
+
lines.append(f"- Available tools: {', '.join(self.tools)}")
|
|
279
|
+
else:
|
|
280
|
+
lines.append("- Available tools: none")
|
|
281
|
+
if not self.can_write:
|
|
282
|
+
lines.append("- Constraint: this role must not write or edit files.")
|
|
283
|
+
if not self.can_delegate:
|
|
284
|
+
lines.append("- Constraint: this role must not start another child agent.")
|
|
285
|
+
return "\n".join(lines)
|
|
286
|
+
|
|
287
|
+
@property
|
|
288
|
+
def prompt(self) -> str:
|
|
289
|
+
return self.role_prompt
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
# ── built-in agents ────────────────────────────────────────────────────────
|
|
293
|
+
|
|
294
|
+
BUILTIN_AGENTS: dict[str, AgentDef] = {
|
|
295
|
+
"orchestrator": AgentDef(
|
|
296
|
+
name="orchestrator",
|
|
297
|
+
description="Coordinator agent. Understands intent, edits small scoped changes directly, "
|
|
298
|
+
"delegates broad work to specialists, reviews results.",
|
|
299
|
+
when_to_use="Default agent for all user interactions. Always use first.",
|
|
300
|
+
tools=[
|
|
301
|
+
"read", "glob", "grep", "bash", "agent", "task_status", "todo",
|
|
302
|
+
"webfetch", "websearch", "repo_map",
|
|
303
|
+
"lsp_diagnostics", "lsp_symbols", "lsp_definition", "lsp_references",
|
|
304
|
+
"write", "edit", "lsp_format",
|
|
305
|
+
],
|
|
306
|
+
can_write=True,
|
|
307
|
+
can_delegate=True,
|
|
308
|
+
max_steps=20,
|
|
309
|
+
hidden=False,
|
|
310
|
+
),
|
|
311
|
+
"explore": AgentDef(
|
|
312
|
+
name="explore",
|
|
313
|
+
description="Fast read-only agent for exploring codebases. Finds files by pattern, "
|
|
314
|
+
"searches for symbols, understands how things work.",
|
|
315
|
+
when_to_use="Use when you need to find files, search code, understand structure, "
|
|
316
|
+
"or answer 'how does X work' questions. Specify thoroughness: "
|
|
317
|
+
"'quick' for basic, 'medium' for moderate, 'very thorough' for exhaustive.",
|
|
318
|
+
tools=[
|
|
319
|
+
"read", "glob", "grep", "webfetch", "websearch", "repo_map",
|
|
320
|
+
"lsp_diagnostics", "lsp_symbols", "lsp_definition", "lsp_references",
|
|
321
|
+
],
|
|
322
|
+
can_write=False,
|
|
323
|
+
can_delegate=False,
|
|
324
|
+
max_steps=10,
|
|
325
|
+
hidden=False,
|
|
326
|
+
|
|
327
|
+
),
|
|
328
|
+
"plan": AgentDef(
|
|
329
|
+
name="plan",
|
|
330
|
+
description="Software architect for designing implementation plans. Analyzes codebase, "
|
|
331
|
+
"proposes approaches, identifies risks.",
|
|
332
|
+
when_to_use="Use for design/architecture questions, before complex implementations, "
|
|
333
|
+
"or when the user asks for a plan/approach/solution design.",
|
|
334
|
+
tools=[
|
|
335
|
+
"read", "glob", "grep", "webfetch", "websearch", "repo_map",
|
|
336
|
+
"lsp_diagnostics", "lsp_symbols", "lsp_definition", "lsp_references",
|
|
337
|
+
],
|
|
338
|
+
can_write=False,
|
|
339
|
+
can_delegate=False,
|
|
340
|
+
max_steps=15,
|
|
341
|
+
hidden=False,
|
|
342
|
+
),
|
|
343
|
+
"implement": AgentDef(
|
|
344
|
+
name="implement",
|
|
345
|
+
description="Delegated coding agent. Writes and edits files, runs bash commands.",
|
|
346
|
+
when_to_use="Use for all code writing, file editing, refactoring, bug fixing, "
|
|
347
|
+
"and bash execution. Give complete, self-contained task descriptions.",
|
|
348
|
+
tools=[
|
|
349
|
+
"read", "write", "edit", "glob", "grep", "bash", "todo", "repo_map",
|
|
350
|
+
"lsp_diagnostics", "lsp_symbols", "lsp_definition", "lsp_references",
|
|
351
|
+
"lsp_format",
|
|
352
|
+
],
|
|
353
|
+
can_write=True,
|
|
354
|
+
can_delegate=False,
|
|
355
|
+
max_steps=25,
|
|
356
|
+
hidden=False,
|
|
357
|
+
),
|
|
358
|
+
"review": AgentDef(
|
|
359
|
+
name="review",
|
|
360
|
+
description="Code reviewer. Checks implementations for correctness, style, security. "
|
|
361
|
+
"Returns PASS/FAIL/NEEDS_CHANGE verdicts with specific issues.",
|
|
362
|
+
when_to_use="ALWAYS invoke after implement finishes non-trivial work. "
|
|
363
|
+
"Use to verify correctness before reporting completion to the user.",
|
|
364
|
+
tools=[
|
|
365
|
+
"read", "glob", "grep", "bash", "webfetch", "websearch", "repo_map",
|
|
366
|
+
"lsp_diagnostics", "lsp_symbols", "lsp_definition", "lsp_references",
|
|
367
|
+
],
|
|
368
|
+
can_write=False,
|
|
369
|
+
can_delegate=False,
|
|
370
|
+
max_steps=10,
|
|
371
|
+
hidden=False,
|
|
372
|
+
),
|
|
373
|
+
# ── hidden agents (not user-visible, internal only) ───────────────
|
|
374
|
+
"compaction": AgentDef(
|
|
375
|
+
name="compaction",
|
|
376
|
+
description="Internal agent for generating context summaries when compaction is needed.",
|
|
377
|
+
when_to_use="INTERNAL ONLY. Invoked automatically when context overflow is detected.",
|
|
378
|
+
tools=[], # no tools — just generates summaries
|
|
379
|
+
can_write=False,
|
|
380
|
+
can_delegate=False,
|
|
381
|
+
max_steps=3,
|
|
382
|
+
hidden=True,
|
|
383
|
+
),
|
|
384
|
+
"title": AgentDef(
|
|
385
|
+
name="title",
|
|
386
|
+
description="Internal agent for generating session titles from first user message.",
|
|
387
|
+
when_to_use="INTERNAL ONLY. Invoked automatically after first user message.",
|
|
388
|
+
tools=[],
|
|
389
|
+
can_write=False,
|
|
390
|
+
can_delegate=False,
|
|
391
|
+
max_steps=2,
|
|
392
|
+
hidden=True,
|
|
393
|
+
),
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
COMPACTION_PROMPT = """You are voidx compaction agent. Your job is to generate a
|
|
397
|
+
structured summary of the conversation history to free context space.
|
|
398
|
+
|
|
399
|
+
You have NO tools. Just read the conversation history below and output the
|
|
400
|
+
summary in the exact format specified.
|
|
401
|
+
|
|
402
|
+
""" + "Use template defined in CompactionService."
|
|
403
|
+
|
|
404
|
+
TITLE_PROMPT = """You are voidx title agent. Generate a short, descriptive
|
|
405
|
+
title (max 80 chars) for this conversation based on the first user message.
|
|
406
|
+
Output ONLY the title text, nothing else. No quotes, no formatting."""
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def get_agent(name: str) -> AgentDef | None:
|
|
410
|
+
return BUILTIN_AGENTS.get(name)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def get_visible_agents() -> list[AgentDef]:
|
|
414
|
+
return [a for a in BUILTIN_AGENTS.values() if not a.hidden]
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def get_subagents() -> list[AgentDef]:
|
|
418
|
+
"""Worker roles the orchestrator can run (all non-primary, non-hidden)."""
|
|
419
|
+
return [
|
|
420
|
+
a for a in BUILTIN_AGENTS.values()
|
|
421
|
+
if a.name != "orchestrator" and not a.hidden
|
|
422
|
+
]
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def child_agent_descriptions_for_llm() -> str:
|
|
426
|
+
"""Generate child-agent descriptions for the agent tool."""
|
|
427
|
+
lines = ["Available child agents and the tools they have access to:"]
|
|
428
|
+
for agent in get_subagents():
|
|
429
|
+
tools_str = ", ".join(agent.tools)
|
|
430
|
+
lines.append(
|
|
431
|
+
f"- {agent.name}: {agent.description}\n"
|
|
432
|
+
f" Tools: {tools_str}\n"
|
|
433
|
+
f" Write access: {agent.can_write}"
|
|
434
|
+
)
|
|
435
|
+
return "\n".join(lines)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def subagent_descriptions_for_llm() -> str:
|
|
439
|
+
return child_agent_descriptions_for_llm()
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Parse and materialize user file attachments."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import json
|
|
7
|
+
import mimetypes
|
|
8
|
+
import re
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".webp"}
|
|
14
|
+
MAX_TEXT_ATTACHMENT_BYTES = 200_000
|
|
15
|
+
MAX_IMAGE_ATTACHMENT_BYTES = 5_000_000
|
|
16
|
+
_ATTACHMENT_RE = re.compile(r'(?<!\S)@(?:"([^"]+)"|(\S+))')
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class Attachment:
|
|
21
|
+
path: Path
|
|
22
|
+
rel_path: str
|
|
23
|
+
kind: str
|
|
24
|
+
mime_type: str
|
|
25
|
+
size: int
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class UserMessagePayload:
|
|
30
|
+
raw_text: str
|
|
31
|
+
clean_text: str
|
|
32
|
+
display_text: str
|
|
33
|
+
title_text: str
|
|
34
|
+
content: str | list[dict[str, Any]]
|
|
35
|
+
content_format: str
|
|
36
|
+
attachments: list[Attachment] = field(default_factory=list)
|
|
37
|
+
warnings: list[str] = field(default_factory=list)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def parse_structured_content(content: str, content_format: str) -> str | list[dict[str, Any]]:
|
|
41
|
+
if content_format != "structured":
|
|
42
|
+
return content
|
|
43
|
+
try:
|
|
44
|
+
parsed = json.loads(content)
|
|
45
|
+
except Exception:
|
|
46
|
+
return content
|
|
47
|
+
return parsed if isinstance(parsed, list) else content
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def serialize_message_content(content: str | list[dict[str, Any]]) -> tuple[str, str]:
|
|
51
|
+
if isinstance(content, list):
|
|
52
|
+
return json.dumps(content, ensure_ascii=False), "structured"
|
|
53
|
+
return content, "text"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def build_user_message_payload(user_text: str, workspace: str) -> UserMessagePayload:
|
|
57
|
+
workspace_path = Path(workspace).resolve()
|
|
58
|
+
tokens = _attachment_tokens(user_text)
|
|
59
|
+
removed_spans: list[tuple[int, int]] = []
|
|
60
|
+
attachments: list[Attachment] = []
|
|
61
|
+
warnings: list[str] = []
|
|
62
|
+
text_sections: list[str] = []
|
|
63
|
+
image_parts: list[dict[str, Any]] = []
|
|
64
|
+
|
|
65
|
+
for start, end, raw_path in tokens:
|
|
66
|
+
resolved = _resolve_workspace_path(workspace_path, raw_path)
|
|
67
|
+
if resolved is None:
|
|
68
|
+
warnings.append(f"Attachment skipped outside workspace: {raw_path}")
|
|
69
|
+
continue
|
|
70
|
+
if not resolved.exists() or not resolved.is_file():
|
|
71
|
+
warnings.append(f"Attachment not found: {raw_path}")
|
|
72
|
+
continue
|
|
73
|
+
removed_spans.append((start, end))
|
|
74
|
+
rel_path = _relative_path(workspace_path, resolved)
|
|
75
|
+
mime_type = mimetypes.guess_type(str(resolved))[0] or "application/octet-stream"
|
|
76
|
+
size = resolved.stat().st_size
|
|
77
|
+
kind = "image" if is_image_path(resolved) else "file"
|
|
78
|
+
attachment = Attachment(resolved, rel_path, kind, mime_type, size)
|
|
79
|
+
attachments.append(attachment)
|
|
80
|
+
|
|
81
|
+
if kind == "image":
|
|
82
|
+
if size > MAX_IMAGE_ATTACHMENT_BYTES:
|
|
83
|
+
warnings.append(f"Image skipped because it is too large: {rel_path}")
|
|
84
|
+
continue
|
|
85
|
+
image_parts.append(_image_part(resolved, mime_type))
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
section, warning = _text_file_section(attachment)
|
|
89
|
+
if warning:
|
|
90
|
+
warnings.append(warning)
|
|
91
|
+
if section:
|
|
92
|
+
text_sections.append(section)
|
|
93
|
+
|
|
94
|
+
clean_text = _normalize_text(_remove_spans(user_text, removed_spans))
|
|
95
|
+
text_content = _build_text_content(clean_text, attachments, text_sections)
|
|
96
|
+
content: str | list[dict[str, Any]]
|
|
97
|
+
content_format = "text"
|
|
98
|
+
if image_parts:
|
|
99
|
+
content = [{"type": "text", "text": text_content}, *image_parts]
|
|
100
|
+
content_format = "structured"
|
|
101
|
+
else:
|
|
102
|
+
content = text_content
|
|
103
|
+
|
|
104
|
+
display_text = _display_text(clean_text, attachments)
|
|
105
|
+
title_text = clean_text or (f"Attached {attachments[0].rel_path}" if attachments else user_text)
|
|
106
|
+
return UserMessagePayload(
|
|
107
|
+
raw_text=user_text,
|
|
108
|
+
clean_text=clean_text,
|
|
109
|
+
display_text=display_text,
|
|
110
|
+
title_text=title_text,
|
|
111
|
+
content=content,
|
|
112
|
+
content_format=content_format,
|
|
113
|
+
attachments=attachments,
|
|
114
|
+
warnings=warnings,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def is_image_path(path: Path | str) -> bool:
|
|
119
|
+
return Path(path).suffix.lower() in IMAGE_EXTENSIONS
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _attachment_tokens(text: str) -> list[tuple[int, int, str]]:
|
|
123
|
+
return [
|
|
124
|
+
(match.start(), match.end(), match.group(1) or match.group(2) or "")
|
|
125
|
+
for match in _ATTACHMENT_RE.finditer(text)
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _remove_spans(text: str, spans: list[tuple[int, int]]) -> str:
|
|
130
|
+
if not spans:
|
|
131
|
+
return text
|
|
132
|
+
result: list[str] = []
|
|
133
|
+
last = 0
|
|
134
|
+
for start, end in spans:
|
|
135
|
+
result.append(text[last:start])
|
|
136
|
+
result.append(" ")
|
|
137
|
+
last = end
|
|
138
|
+
result.append(text[last:])
|
|
139
|
+
return "".join(result)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _resolve_workspace_path(workspace: Path, raw_path: str) -> Path | None:
|
|
143
|
+
candidate = Path(raw_path).expanduser()
|
|
144
|
+
resolved = candidate.resolve() if candidate.is_absolute() else (workspace / candidate).resolve()
|
|
145
|
+
try:
|
|
146
|
+
resolved.relative_to(workspace)
|
|
147
|
+
except ValueError:
|
|
148
|
+
return None
|
|
149
|
+
return resolved
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _relative_path(workspace: Path, path: Path) -> str:
|
|
153
|
+
try:
|
|
154
|
+
return path.relative_to(workspace).as_posix()
|
|
155
|
+
except ValueError:
|
|
156
|
+
return path.as_posix()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _image_part(path: Path, mime_type: str) -> dict[str, Any]:
|
|
160
|
+
encoded = base64.b64encode(path.read_bytes()).decode("ascii")
|
|
161
|
+
return {
|
|
162
|
+
"type": "image_url",
|
|
163
|
+
"image_url": {"url": f"data:{mime_type};base64,{encoded}"},
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _text_file_section(attachment: Attachment) -> tuple[str, str]:
|
|
168
|
+
raw = attachment.path.read_bytes()
|
|
169
|
+
truncated = len(raw) > MAX_TEXT_ATTACHMENT_BYTES
|
|
170
|
+
if truncated:
|
|
171
|
+
raw = raw[:MAX_TEXT_ATTACHMENT_BYTES]
|
|
172
|
+
if b"\x00" in raw:
|
|
173
|
+
return (
|
|
174
|
+
f"Attached binary file: {attachment.rel_path} ({attachment.mime_type}, {attachment.size} bytes).",
|
|
175
|
+
"",
|
|
176
|
+
)
|
|
177
|
+
try:
|
|
178
|
+
text = raw.decode("utf-8")
|
|
179
|
+
except UnicodeDecodeError:
|
|
180
|
+
text = raw.decode("utf-8", errors="replace")
|
|
181
|
+
suffix = ""
|
|
182
|
+
warning = ""
|
|
183
|
+
if truncated:
|
|
184
|
+
omitted = attachment.size - MAX_TEXT_ATTACHMENT_BYTES
|
|
185
|
+
suffix = f"\n\n[Attachment truncated: omitted {omitted} bytes]"
|
|
186
|
+
warning = f"Attachment truncated: {attachment.rel_path}"
|
|
187
|
+
lang = _language_from_path(attachment.rel_path)
|
|
188
|
+
return f"Attached file: {attachment.rel_path}\n```{lang}\n{text}{suffix}\n```", warning
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _language_from_path(path: str) -> str:
|
|
192
|
+
suffix = Path(path).suffix.lower().lstrip(".")
|
|
193
|
+
mapping = {
|
|
194
|
+
"py": "python",
|
|
195
|
+
"js": "javascript",
|
|
196
|
+
"ts": "typescript",
|
|
197
|
+
"tsx": "tsx",
|
|
198
|
+
"jsx": "jsx",
|
|
199
|
+
"json": "json",
|
|
200
|
+
"md": "markdown",
|
|
201
|
+
"sh": "bash",
|
|
202
|
+
"yml": "yaml",
|
|
203
|
+
"yaml": "yaml",
|
|
204
|
+
"html": "html",
|
|
205
|
+
"css": "css",
|
|
206
|
+
}
|
|
207
|
+
return mapping.get(suffix, suffix)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _build_text_content(clean_text: str, attachments: list[Attachment], text_sections: list[str]) -> str:
|
|
211
|
+
parts: list[str] = []
|
|
212
|
+
if clean_text:
|
|
213
|
+
parts.append(clean_text)
|
|
214
|
+
if attachments:
|
|
215
|
+
image_lines = [f"- {item.rel_path} ({item.mime_type}, {item.size} bytes)" for item in attachments if item.kind == "image"]
|
|
216
|
+
if image_lines:
|
|
217
|
+
parts.append("Attached images:\n" + "\n".join(image_lines))
|
|
218
|
+
parts.extend(text_sections)
|
|
219
|
+
if parts:
|
|
220
|
+
return "\n\n".join(parts)
|
|
221
|
+
return "Please review the attached file."
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _display_text(clean_text: str, attachments: list[Attachment]) -> str:
|
|
225
|
+
if not attachments:
|
|
226
|
+
return clean_text
|
|
227
|
+
names = ", ".join(item.rel_path for item in attachments[:3])
|
|
228
|
+
if len(attachments) > 3:
|
|
229
|
+
names += f", +{len(attachments) - 3} more"
|
|
230
|
+
base = clean_text or "Attached files"
|
|
231
|
+
return f"{base}\n[attachments: {names}]"
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _normalize_text(text: str) -> str:
|
|
235
|
+
return re.sub(r"[ \t]+", " ", text).strip()
|