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.
Files changed (126) hide show
  1. voidx/__init__.py +3 -0
  2. voidx/agent/__init__.py +0 -0
  3. voidx/agent/agents.py +439 -0
  4. voidx/agent/attachments.py +235 -0
  5. voidx/agent/graph.py +463 -0
  6. voidx/agent/graph_components/__init__.py +1 -0
  7. voidx/agent/graph_components/compaction.py +268 -0
  8. voidx/agent/graph_components/permissions.py +139 -0
  9. voidx/agent/graph_components/run_loop.py +532 -0
  10. voidx/agent/graph_components/runtime.py +14 -0
  11. voidx/agent/graph_components/streaming.py +351 -0
  12. voidx/agent/graph_components/subagent.py +278 -0
  13. voidx/agent/graph_components/tool_execution.py +208 -0
  14. voidx/agent/runtime_context.py +368 -0
  15. voidx/agent/slash.py +466 -0
  16. voidx/agent/slash_components/__init__.py +1 -0
  17. voidx/agent/slash_components/code_ide.py +68 -0
  18. voidx/agent/slash_components/lsp.py +105 -0
  19. voidx/agent/slash_components/mcp.py +332 -0
  20. voidx/agent/slash_components/model.py +419 -0
  21. voidx/agent/slash_components/runtime.py +55 -0
  22. voidx/agent/slash_components/skills.py +94 -0
  23. voidx/agent/state.py +32 -0
  24. voidx/agent/task_state.py +278 -0
  25. voidx/agent/tool_filters.py +27 -0
  26. voidx/config.py +707 -0
  27. voidx/llm/__init__.py +0 -0
  28. voidx/llm/catalog.py +188 -0
  29. voidx/llm/compaction.py +267 -0
  30. voidx/llm/context.py +43 -0
  31. voidx/llm/instruction.py +220 -0
  32. voidx/llm/provider.py +312 -0
  33. voidx/llm/usage.py +341 -0
  34. voidx/lsp/__init__.py +30 -0
  35. voidx/lsp/client.py +259 -0
  36. voidx/lsp/config.py +172 -0
  37. voidx/lsp/detector.py +512 -0
  38. voidx/lsp/errors.py +19 -0
  39. voidx/lsp/manager.py +280 -0
  40. voidx/lsp/schema.py +179 -0
  41. voidx/lsp/service.py +103 -0
  42. voidx/main.py +154 -0
  43. voidx/mcp/__init__.py +33 -0
  44. voidx/mcp/client.py +458 -0
  45. voidx/mcp/manager.py +267 -0
  46. voidx/mcp/schema.py +112 -0
  47. voidx/mcp/tool.py +122 -0
  48. voidx/mcp_servers/__init__.py +1 -0
  49. voidx/mcp_servers/web.py +104 -0
  50. voidx/memory/__init__.py +0 -0
  51. voidx/memory/context_frames.py +188 -0
  52. voidx/memory/model_profiles.py +98 -0
  53. voidx/memory/runtime_state.py +240 -0
  54. voidx/memory/session.py +272 -0
  55. voidx/memory/store.py +245 -0
  56. voidx/memory/transcript.py +137 -0
  57. voidx/permission/__init__.py +28 -0
  58. voidx/permission/engine.py +430 -0
  59. voidx/permission/evaluate.py +114 -0
  60. voidx/permission/sandbox.py +280 -0
  61. voidx/permission/schema.py +24 -0
  62. voidx/permission/service.py +314 -0
  63. voidx/permission/wildcard.py +34 -0
  64. voidx/skills/__init__.py +18 -0
  65. voidx/skills/bundled/superpowers/receiving-code-review/SKILL.md +30 -0
  66. voidx/skills/bundled/superpowers/requesting-code-review/SKILL.md +27 -0
  67. voidx/skills/bundled/superpowers/systematic-debugging/SKILL.md +36 -0
  68. voidx/skills/bundled/superpowers/test-driven-development/SKILL.md +33 -0
  69. voidx/skills/bundled/superpowers/verification-before-completion/SKILL.md +31 -0
  70. voidx/skills/bundled/superpowers/writing-plans/SKILL.md +27 -0
  71. voidx/skills/policy.py +97 -0
  72. voidx/skills/registry.py +162 -0
  73. voidx/skills/schema.py +47 -0
  74. voidx/skills/service.py +199 -0
  75. voidx/tools/__init__.py +0 -0
  76. voidx/tools/agent.py +81 -0
  77. voidx/tools/base.py +86 -0
  78. voidx/tools/bash.py +105 -0
  79. voidx/tools/file_ops.py +193 -0
  80. voidx/tools/lsp.py +155 -0
  81. voidx/tools/registry.py +104 -0
  82. voidx/tools/repomap.py +238 -0
  83. voidx/tools/search.py +162 -0
  84. voidx/tools/task_status.py +57 -0
  85. voidx/tools/task_tracker.py +81 -0
  86. voidx/tools/todo.py +82 -0
  87. voidx/tools/web_content.py +357 -0
  88. voidx/tools/web_mcp.py +107 -0
  89. voidx/tools/webfetch.py +155 -0
  90. voidx/tools/websearch.py +276 -0
  91. voidx/ui/__init__.py +0 -0
  92. voidx/ui/app.py +1033 -0
  93. voidx/ui/app_components/__init__.py +1 -0
  94. voidx/ui/app_components/clipboard_image.py +245 -0
  95. voidx/ui/app_components/commands.py +18 -0
  96. voidx/ui/app_components/controls.py +29 -0
  97. voidx/ui/app_components/file_picker.py +115 -0
  98. voidx/ui/app_components/formatting.py +187 -0
  99. voidx/ui/app_components/git_changes.py +51 -0
  100. voidx/ui/app_components/rendering.py +1169 -0
  101. voidx/ui/browse.py +160 -0
  102. voidx/ui/capture.py +169 -0
  103. voidx/ui/code_ide.py +251 -0
  104. voidx/ui/commands.py +83 -0
  105. voidx/ui/console.py +381 -0
  106. voidx/ui/console_components/__init__.py +1 -0
  107. voidx/ui/console_components/formatting.py +96 -0
  108. voidx/ui/console_components/streaming.py +253 -0
  109. voidx/ui/diff.py +331 -0
  110. voidx/ui/dock.py +372 -0
  111. voidx/ui/dock_components/__init__.py +1 -0
  112. voidx/ui/dock_components/formatting.py +123 -0
  113. voidx/ui/dock_components/nodes.py +401 -0
  114. voidx/ui/dock_components/state.py +51 -0
  115. voidx/ui/event_components/__init__.py +1 -0
  116. voidx/ui/event_components/schema.py +249 -0
  117. voidx/ui/events.py +341 -0
  118. voidx/ui/session_changes.py +163 -0
  119. voidx/ui/startup.py +161 -0
  120. voidx/ui/transcript.py +148 -0
  121. voidx/ui/tree.py +316 -0
  122. voidx-1.0.0.dist-info/METADATA +59 -0
  123. voidx-1.0.0.dist-info/RECORD +126 -0
  124. voidx-1.0.0.dist-info/WHEEL +5 -0
  125. voidx-1.0.0.dist-info/entry_points.txt +2 -0
  126. voidx-1.0.0.dist-info/top_level.txt +1 -0
voidx/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """VoidX - A coding agent that quantifies everything."""
2
+
3
+ __version__ = "1.0.0"
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()