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
@@ -0,0 +1,268 @@
1
+ """Context compaction methods for the agent graph."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage
6
+
7
+ from voidx.agent.graph_components.runtime import console, ui
8
+ from voidx.agent.graph_components.streaming import extract_text, stream_llm
9
+ from voidx.llm.provider import resolve_protocol
10
+ from voidx.llm.usage import estimate_context_tokens, estimate_message_tokens, extract_token_usage
11
+ from voidx.memory.context_frames import save_context_frame_from_messages
12
+ from voidx.ui.console import StreamingRenderer
13
+ from voidx.ui.dock import dock
14
+ from voidx.ui.events import StatusFinished, StatusUpdated, ui_events
15
+
16
+
17
+ class GraphCompactionMixin:
18
+ async def _maybe_compact(
19
+ self,
20
+ messages: list,
21
+ session_msgs: list,
22
+ *,
23
+ force: bool = False,
24
+ ask: bool = True,
25
+ ) -> tuple[list | None, str | None]:
26
+ """Check overflow and compact if needed."""
27
+ total_tokens = estimate_context_tokens(messages, self.config.model.model)
28
+ tokens = {"total": total_tokens, "input": total_tokens, "output": 0, "reasoning": 0}
29
+
30
+ if not force and not self._compaction.is_overflow(tokens):
31
+ return None, None
32
+
33
+ if not force and ask and getattr(self.config, "ask_compact", False):
34
+ should_compact = await self._ask_compact(total_tokens)
35
+ if not should_compact:
36
+ if dock.active and ui_events.is_running:
37
+ await ui_events.emit(StatusFinished(
38
+ status_id="compaction",
39
+ label="Compaction skipped",
40
+ remove=False,
41
+ ))
42
+ else:
43
+ ui.print("[dim]Compaction skipped[/dim]")
44
+ return None, None
45
+
46
+ if dock.active and ui_events.is_running:
47
+ await ui_events.emit(StatusUpdated(
48
+ status_id="compaction",
49
+ label="Compacting context",
50
+ detail=(
51
+ f"{total_tokens} tokens exceed the active context budget"
52
+ if not force
53
+ else f"manual compaction of {len(messages)} messages"
54
+ ),
55
+ stage="compacting",
56
+ ))
57
+ else:
58
+ ui.print(
59
+ "[yellow]Context overflow — compacting...[/yellow]"
60
+ if not force
61
+ else "[yellow]Compacting context...[/yellow]"
62
+ )
63
+
64
+ head_msgs, tail_id = self._compaction.select(messages)
65
+
66
+ if not head_msgs or not tail_id:
67
+ # Hard fallback: keep only last 6 messages
68
+ keep = min(6, len(messages))
69
+ if dock.active and ui_events.is_running:
70
+ await ui_events.emit(StatusUpdated(
71
+ status_id="compaction",
72
+ label="Compacting context",
73
+ detail=f"fallback truncation, keeping last {keep} messages",
74
+ stage="compacting",
75
+ ))
76
+ await ui_events.emit(StatusFinished(
77
+ status_id="compaction",
78
+ label=f"Compaction fallback kept last {keep} messages",
79
+ remove=False,
80
+ ))
81
+ else:
82
+ ui.print(f"[dim]Aggressive truncation: keeping last {keep} messages[/dim]")
83
+ # Remove old messages, keep system + last N
84
+ system_msgs = [m for m in messages if isinstance(m, SystemMessage)]
85
+ other_msgs = [m for m in messages if not isinstance(m, SystemMessage)]
86
+ messages.clear()
87
+ messages.extend(system_msgs)
88
+ messages.extend(other_msgs[-keep:])
89
+ return messages[:max(0, len(messages) - keep)], None
90
+
91
+ # Run compaction agent
92
+ try:
93
+ if dock.active and ui_events.is_running:
94
+ await ui_events.emit(StatusUpdated(
95
+ status_id="compaction",
96
+ label="Compacting context",
97
+ detail=f"summarizing {len(head_msgs)} old messages",
98
+ stage="compacting",
99
+ ))
100
+ previous_summary = getattr(self, "_compaction_summary", "") or None
101
+ summary = await self._run_compaction_agent(head_msgs, previous_summary)
102
+ except Exception as e:
103
+ if dock.active and ui_events.is_running:
104
+ await ui_events.emit(StatusUpdated(
105
+ status_id="compaction",
106
+ label="Compaction agent failed",
107
+ detail=f"{e}; falling back to truncation",
108
+ stage="compacting",
109
+ ))
110
+ else:
111
+ ui.print(f"[dim]Compaction agent failed ({e}) — aggressive truncation[/dim]")
112
+ keep = min(6, len(messages))
113
+ system_msgs = [m for m in messages if isinstance(m, SystemMessage)]
114
+ other_msgs = [m for m in messages if not isinstance(m, SystemMessage)]
115
+ messages.clear()
116
+ messages.extend(system_msgs)
117
+ messages.extend(other_msgs[-keep:])
118
+ if dock.active and ui_events.is_running:
119
+ await ui_events.emit(StatusFinished(
120
+ status_id="compaction",
121
+ label=f"Compaction fallback kept last {keep} messages",
122
+ ok=False,
123
+ remove=False,
124
+ ))
125
+ return messages[:max(0, len(messages) - keep)], None
126
+
127
+ if summary:
128
+ keep_from = len(head_msgs)
129
+ tail_msgs = messages[keep_from:]
130
+ messages.clear()
131
+ messages.extend(tail_msgs)
132
+ self._pending_summary = summary
133
+ self._compaction_summary = summary
134
+ self._compaction.compaction_count += 1
135
+ await self._persist_compaction(head_msgs)
136
+ if dock.active and ui_events.is_running:
137
+ await ui_events.emit(StatusFinished(
138
+ status_id="compaction",
139
+ label=f"Compacted {len(head_msgs)} messages into summary",
140
+ remove=False,
141
+ ))
142
+ else:
143
+ ui.print(f"[dim]Compacted: {len(head_msgs)} messages → summary[/dim]")
144
+ elif dock.active and ui_events.is_running:
145
+ await ui_events.emit(StatusFinished(
146
+ status_id="compaction",
147
+ label="Compaction produced no summary",
148
+ ok=False,
149
+ remove=False,
150
+ ))
151
+ return None, None
152
+ else:
153
+ return None, None
154
+
155
+ return head_msgs, tail_id
156
+
157
+ async def _ask_compact(self, total_tokens: int) -> bool:
158
+ choices = [
159
+ ("Compact", "compact", "Summarize older context and continue"),
160
+ ("Skip once", "skip", "Continue without compacting this turn"),
161
+ ]
162
+ app = getattr(self, "_app", None)
163
+ if app:
164
+ choice = await app.ask_choice("Compact context?", choices)
165
+ return choice == "compact"
166
+ ui.print("")
167
+ ui.print(f" [yellow]Context is large ({total_tokens} tokens); compacting automatically.[/yellow]")
168
+ return True
169
+
170
+ async def _persist_compaction(self, head_messages: list) -> None:
171
+ if getattr(self, "_session", None) is None:
172
+ return
173
+ if hasattr(self, "_persist_runtime_state"):
174
+ await self._persist_runtime_state()
175
+ last_message_id = _max_persisted_message_id(head_messages)
176
+ if last_message_id is None:
177
+ return
178
+ from voidx.memory.session import delete_messages_through
179
+
180
+ await delete_messages_through(self._session.id, last_message_id)
181
+
182
+ async def _compact_session_history(self, *, force: bool = True) -> bool:
183
+ if getattr(self, "_session", None) is None:
184
+ ui.print("[dim]No active session to compact.[/dim]")
185
+ return False
186
+
187
+ from voidx.agent.attachments import parse_structured_content
188
+ from voidx.memory.session import load_messages
189
+
190
+ rows = await load_messages(self._session.id)
191
+ messages = []
192
+ for row in rows:
193
+ msg_id = str(row.id) if row.id is not None else None
194
+ if row.role == "system":
195
+ messages.append(SystemMessage(content=row.content, id=msg_id))
196
+ elif row.role == "user":
197
+ messages.append(HumanMessage(
198
+ content=parse_structured_content(row.content, row.content_format),
199
+ id=msg_id,
200
+ ))
201
+ elif row.role == "assistant":
202
+ messages.append(AIMessage(
203
+ content=parse_structured_content(row.content, row.content_format),
204
+ tool_calls=row.tool_calls or [],
205
+ id=msg_id,
206
+ ))
207
+ elif row.role == "tool":
208
+ messages.append(ToolMessage(
209
+ content=row.content,
210
+ tool_call_id=row.tool_call_id or "",
211
+ id=msg_id,
212
+ ))
213
+
214
+ head, _tail_id = await self._maybe_compact(messages, rows, force=force, ask=False)
215
+ return bool(head)
216
+
217
+ async def _run_compaction_agent(self, head_messages: list, previous_summary: str | None) -> str | None:
218
+ """Run the compaction agent to generate a structured summary."""
219
+ from voidx.agent.agents import COMPACTION_PROMPT
220
+
221
+ if self.model is None:
222
+ return None
223
+
224
+ prompt = self._compaction.build_prompt(head_messages, previous_summary)
225
+ renderer = StreamingRenderer(console, debug=self._debug, stream_to_dock=False)
226
+
227
+ messages = [SystemMessage(content=COMPACTION_PROMPT)]
228
+ messages.append(HumanMessage(content=prompt))
229
+
230
+ # Use a cheap/fast call for compaction — no tools
231
+ context_tokens = estimate_context_tokens(messages, self.config.model.model)
232
+ self._usage_stats.update_context(context_tokens)
233
+ if self._session is not None:
234
+ await save_context_frame_from_messages(
235
+ session_id=self._session.id,
236
+ frame_kind="compaction",
237
+ agent_role="compaction",
238
+ provider=self.config.model.provider,
239
+ model=self.config.model.model,
240
+ messages=messages,
241
+ token_estimate=context_tokens,
242
+ metadata={
243
+ "head_message_count": len(head_messages),
244
+ "has_previous_summary": previous_summary is not None,
245
+ },
246
+ )
247
+ assistant_msg = await stream_llm(self.model, messages, renderer, resolve_protocol(self.config.model))
248
+ self._usage_stats.record_call(
249
+ extract_token_usage(assistant_msg),
250
+ fallback_input_tokens=context_tokens,
251
+ fallback_output_tokens=estimate_message_tokens(assistant_msg, self.config.model.model),
252
+ messages=messages,
253
+ model=self.config.model.model,
254
+ cache_key=f"{self.config.model.provider}/{self.config.model.model}",
255
+ )
256
+ text = extract_text(assistant_msg)
257
+ return text if text else None
258
+
259
+
260
+ def _max_persisted_message_id(messages: list) -> int | None:
261
+ ids: list[int] = []
262
+ for message in messages:
263
+ raw = getattr(message, "id", None)
264
+ try:
265
+ ids.append(int(raw))
266
+ except (TypeError, ValueError):
267
+ continue
268
+ return max(ids) if ids else None
@@ -0,0 +1,139 @@
1
+ """Tool permission UI adapter for the agent graph."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from voidx.agent.graph_components.runtime import ui
6
+ from voidx.permission.engine import (
7
+ PermissionContext,
8
+ authorize_tool_call,
9
+ build_pattern,
10
+ classify_tool_call,
11
+ )
12
+ from voidx.ui.events import PermissionToolDetail
13
+
14
+
15
+ class GraphPermissionMixin:
16
+ _needs_failure_check: dict[str, dict]
17
+
18
+ async def _authorize_tool_calls(
19
+ self,
20
+ tool_calls: list[dict],
21
+ agent_name: str,
22
+ plan_mode: bool,
23
+ session_id: str,
24
+ interaction_mode: str | None = None,
25
+ ) -> tuple[list[dict], list[tuple[dict, str]]]:
26
+ approved: list[dict] = []
27
+ denied: list[tuple[dict, str]] = []
28
+ need_ask: list[dict] = []
29
+
30
+ if not hasattr(self, '_needs_failure_check'):
31
+ self._needs_failure_check = {}
32
+
33
+ context = PermissionContext.from_service(
34
+ self._permission,
35
+ workspace=self._workspace,
36
+ interaction_mode=interaction_mode,
37
+ plan_mode=plan_mode,
38
+ )
39
+
40
+ for tc in tool_calls:
41
+ decision = authorize_tool_call(tc, context)
42
+ if decision.action == "allow":
43
+ approved.append(decision.tool_call)
44
+ if decision.failure_check:
45
+ self._needs_failure_check[decision.tool_call.get("id", "")] = decision.tool_call
46
+ elif decision.action == "deny":
47
+ denied.append((decision.tool_call, decision.reason))
48
+ else:
49
+ need_ask.append(decision.tool_call)
50
+
51
+ if need_ask:
52
+ await self._ask_and_apply_permission(need_ask, approved, denied)
53
+
54
+ return approved, denied
55
+
56
+ async def _ask_and_apply_permission(
57
+ self,
58
+ need_ask: list[dict],
59
+ approved: list[dict],
60
+ denied: list[tuple[dict, str]],
61
+ ) -> None:
62
+ choice = await self._ask_tool_permission(need_ask)
63
+ if choice is None:
64
+ choice = "n"
65
+
66
+ if choice == "a":
67
+ for tc in need_ask:
68
+ self._permission.allow_silent(tc["name"])
69
+ self._notice_permission_result(f"{len(need_ask)} tools allowed for this session")
70
+ approved.extend(need_ask)
71
+ elif choice == "y":
72
+ self._notice_permission_result(f"{len(need_ask)} tools allowed once")
73
+ approved.extend(need_ask)
74
+ else:
75
+ self._notice_permission_result(f"{len(need_ask)} tools denied")
76
+ for tc in need_ask:
77
+ denied.append((tc, f"User denied: {tc['name']}"))
78
+
79
+ async def _ask_tool_permission(self, tool_calls: list[dict]) -> str | None:
80
+ tool_list = ", ".join(t["name"] for t in tool_calls)
81
+ choices = [
82
+ ("Yes, always", "a", "Allow these tools for this session"),
83
+ ("Yes", "y", "Allow this tool use once"),
84
+ ("No", "n", "Deny these tools"),
85
+ ]
86
+ details = [item.model_dump() for item in self._permission_tool_details(tool_calls)]
87
+
88
+ if not self._app:
89
+ ui.print("")
90
+ ui.print(f" [yellow]Allow tools: [bold]{tool_list}[/bold]?[/yellow]")
91
+
92
+ if self._app:
93
+ return await self._app.ask_choice("Allow tool use?", choices, details=details)
94
+ return "n"
95
+
96
+ def _notice_permission_result(self, message: str) -> None:
97
+ if self._show_permission_output(message):
98
+ return
99
+ ui.print(f"[dim]✓ {message}[/dim]")
100
+
101
+ def _notify_tool_failure(self, tc: dict, result) -> None:
102
+ """Notify user when an auto-approved tool (on-failure policy) fails.
103
+
104
+ The tool was auto-allowed by the on-failure policy and then
105
+ actually failed. Let the user know so they can decide whether
106
+ to abort or let the agent retry.
107
+ """
108
+ tool_name = tc.get("name", "unknown")
109
+ error_preview = str(result.output)[:200]
110
+ message = f"[on-failure] '{tool_name}' failed: {error_preview}"
111
+ if not self._show_permission_output(message):
112
+ ui.print(f"\n[yellow]{message}[/yellow]")
113
+
114
+ def _show_permission_output(self, message: str) -> bool:
115
+ app = self._app
116
+ if not app:
117
+ return False
118
+ show_transient = getattr(app, "show_transient_output", None)
119
+ if callable(show_transient):
120
+ show_transient(message, title="Permission")
121
+ return True
122
+ app.begin_command_output("Permission")
123
+ app.append_command_output(message)
124
+ return True
125
+
126
+ def _clear_failure_check(self, cid: str) -> None:
127
+ """Remove a tool call ID from on-failure tracking (used on success)."""
128
+ self._needs_failure_check.pop(cid, None)
129
+
130
+ def _permission_tool_details(self, tool_calls: list[dict]) -> list[PermissionToolDetail]:
131
+ details: list[PermissionToolDetail] = []
132
+ for call in tool_calls:
133
+ classified = classify_tool_call(call)
134
+ details.append(PermissionToolDetail(
135
+ name=classified.name,
136
+ pattern=build_pattern(classified.name, classified.args),
137
+ args=classified.args,
138
+ ))
139
+ return details