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/agent/graph.py ADDED
@@ -0,0 +1,463 @@
1
+ """Agent graph — LangGraph state machine with 5-agent system.
2
+
3
+ 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 (PASS/FAIL/NEEDS_CHANGE)
9
+
10
+ Depth limit = 1: child agents cannot start further child agents.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import asyncio
16
+ import time
17
+
18
+ from langchain_core.messages import (
19
+ AIMessage,
20
+ HumanMessage,
21
+ SystemMessage,
22
+ )
23
+ from langgraph.graph import END, StateGraph
24
+
25
+ from voidx.agent.agents import BASE_SYSTEM_PROMPT, PLAN_MODE_APPEND, get_agent, AgentDef
26
+ from voidx.agent.graph_components.compaction import GraphCompactionMixin
27
+ from voidx.agent.graph_components.permissions import GraphPermissionMixin
28
+ from voidx.agent.graph_components.runtime import (
29
+ console,
30
+ current_parent_tool_call_id as _current_parent_tool_call_id,
31
+ ui,
32
+ )
33
+ from voidx.agent.graph_components.run_loop import GraphRunLoopMixin
34
+ from voidx.agent.state import AgentState
35
+ from voidx.agent.graph_components.streaming import stream_llm as _stream_llm
36
+ from voidx.agent.graph_components.subagent import run_subagent as _run_subagent
37
+ from voidx.agent.graph_components.tool_execution import GraphToolExecutionMixin
38
+ from voidx.agent.runtime_context import InteractionMode, RuntimeContextBuilder
39
+ from voidx.agent.task_state import TaskRun, TaskState
40
+ from voidx.agent.tool_filters import filter_unavailable_lsp_tools
41
+ from voidx.config import Config, Settings
42
+ from voidx.llm.compaction import CompactionService
43
+ from voidx.llm.instruction import InstructionService
44
+ from voidx.llm.provider import create_chat_model, resolve_protocol
45
+ from voidx.llm.usage import (
46
+ UsageStats,
47
+ estimate_context_tokens,
48
+ estimate_message_tokens,
49
+ extract_token_usage,
50
+ )
51
+ from voidx.memory.context_frames import save_context_frame_from_messages
52
+ from voidx.memory.session import SessionInfo
53
+ from voidx.agent.slash import SlashHandler
54
+ from voidx.permission.service import PermissionService
55
+ from voidx.tools.registry import ToolRegistry
56
+ from voidx.tools.agent import AgentTool
57
+ from voidx.tools.task_status import TaskStatusTool
58
+ from voidx.tools.task_tracker import TaskTracker
59
+ from voidx.tools.todo import TodoWriteTool
60
+ from voidx.ui.console import StreamingRenderer
61
+ from voidx.ui.dock import dock
62
+ from voidx.ui.events import (
63
+ SubagentFinished,
64
+ SubagentStarted,
65
+ ui_events,
66
+ )
67
+ from voidx.ui.tree import OutputNode
68
+
69
+
70
+ # ── LangGraph nodes ────────────────────────────────────────────────────────
71
+
72
+ def _prepare(state: AgentState) -> dict:
73
+ """Advance step counters before LLM execution."""
74
+ agent_name = state.get("agent", "orchestrator")
75
+ agent_def = get_agent(agent_name)
76
+
77
+ return {
78
+ "step_count": state.get("step_count", 0) + 1,
79
+ "max_steps": state.get("max_steps", agent_def.max_steps if agent_def else 50),
80
+ }
81
+
82
+
83
+ class VoidXGraph(
84
+ GraphRunLoopMixin,
85
+ GraphCompactionMixin,
86
+ GraphToolExecutionMixin,
87
+ GraphPermissionMixin,
88
+ ):
89
+ """The voidx agent as a LangGraph state machine."""
90
+
91
+ def __init__(self, config: Config, api_key: str | None, session: SessionInfo | None = None, settings: Settings | None = None):
92
+ self.config = config
93
+ self.api_key = api_key
94
+ self.model = create_chat_model(api_key, config.model) if api_key else None
95
+ self._session = session
96
+ self._workspace = config.workspace
97
+ self._settings = settings
98
+
99
+ # Bind settings to catalog so list_models() merges custom models
100
+ if settings:
101
+ from voidx.llm.catalog import bind_settings
102
+ bind_settings(settings)
103
+
104
+ # Build tool registry, wire agent/todo/task_status to tracker
105
+ self.tools = ToolRegistry(settings=settings)
106
+ self._tracker = TaskTracker()
107
+ agent_tool = AgentTool(runner=self._subagent_runner)
108
+ self.tools.register("agent", agent_tool, agent_tool.description, agent_tool.parameters_schema())
109
+ task_status_tool = TaskStatusTool(tracker=self._tracker)
110
+ self.tools.register("task_status", task_status_tool, task_status_tool.description, task_status_tool.parameters_schema())
111
+ # Replace built-in todo with tracker-aware version
112
+ todo_tool = TodoWriteTool(tracker=self._tracker)
113
+ self.tools.register("todo", todo_tool, todo_tool.description, todo_tool.parameters_schema())
114
+
115
+ # AGENTS.md instruction service — refreshed each turn
116
+ self._instruction = InstructionService(self._workspace, settings=settings)
117
+
118
+ # Permission service — sandbox → allow/deny/ask per tool call
119
+ self._permission = PermissionService(
120
+ permission_mode=config.permission_mode.value,
121
+ sandbox_mode=config.sandbox_mode.value,
122
+ sandbox_workspace_write=config.sandbox_workspace_write,
123
+ approval_policy=config.approval_policy.value,
124
+ approval_reviewer=config.approval_reviewer.value,
125
+ )
126
+
127
+ self._interaction_mode: InteractionMode = InteractionMode.AUTO
128
+ self._debug: bool = True
129
+ ui.set_debug(self._debug)
130
+
131
+ # File mtime staleness guard — shared across tool calls
132
+ self._file_mtimes: dict[str, float] = {}
133
+ self._turn_node: OutputNode | None = None
134
+ self._current_tree: OutputTree | None = None
135
+ self._current_messages: list | None = None
136
+ self._sub_buffers: dict[str, list] = {}
137
+ self._pending_summary: str | None = None
138
+ self._compaction_summary: str = ""
139
+ self._app: PromptToolkitTui | None = None
140
+ self._next_agent_id: int = 0
141
+ self._task_state = TaskState()
142
+ self._task_run = TaskRun()
143
+
144
+ # Context compaction service — provider-aware limits
145
+ from voidx.llm.provider import get_context_limit
146
+ context_limit = get_context_limit(config.model.provider)
147
+ self._usage_stats = UsageStats(context_limit=context_limit)
148
+ self._compaction = CompactionService(
149
+ context_limit=context_limit,
150
+ output_token_max=config.model.max_tokens,
151
+ )
152
+
153
+ self._build()
154
+ self._slash = SlashHandler(self)
155
+
156
+ # MCP (Model Context Protocol) servers — start on run()
157
+ from voidx.mcp import McpManager
158
+ self._mcp_manager = McpManager(
159
+ settings=self._settings,
160
+ registry=self.tools,
161
+ permission=self._permission,
162
+ )
163
+ from voidx.lsp import LspManager
164
+ self._lsp_manager = LspManager(self._workspace)
165
+
166
+ @property
167
+ def _plan_mode(self) -> bool:
168
+ return self._interaction_mode == InteractionMode.PLAN
169
+
170
+ @_plan_mode.setter
171
+ def _plan_mode(self, value: bool) -> None:
172
+ self._interaction_mode = InteractionMode.PLAN if value else InteractionMode.AUTO
173
+
174
+ def set_interaction_mode(self, mode: str | InteractionMode) -> InteractionMode:
175
+ self._interaction_mode = InteractionMode.parse(mode)
176
+ return self._interaction_mode
177
+
178
+ def interaction_mode(self) -> InteractionMode:
179
+ return self._interaction_mode
180
+
181
+ async def _subagent_runner(self, agent_def: AgentDef, description: str, model_override: str | None) -> str:
182
+ parent_messages = getattr(self, '_current_messages', None)
183
+ sub_buffer: list = []
184
+ session_id = self._session.id if self._session else "default"
185
+ agent_id = self._next_agent_id
186
+ self._next_agent_id += 1
187
+ parent_tool_call_id = _current_parent_tool_call_id.get()
188
+ started_at = time.monotonic()
189
+
190
+ async def authorize(calls, agent_name: str):
191
+ return await self._authorize_tool_calls(
192
+ calls,
193
+ agent_name=agent_name,
194
+ plan_mode=self._plan_mode,
195
+ session_id=session_id,
196
+ interaction_mode=self._interaction_mode.value,
197
+ )
198
+
199
+ if dock.active and ui_events.is_running:
200
+ await ui_events.emit(SubagentStarted(
201
+ agent_id=agent_id,
202
+ subagent_id=f"agent_{agent_id}",
203
+ name=agent_def.name,
204
+ description=description,
205
+ parent_agent_id=-1,
206
+ parent_tool_call_id=parent_tool_call_id,
207
+ ))
208
+
209
+ ok = False
210
+ try:
211
+ if self._current_tree and self._turn_node:
212
+ parent = self._turn_node
213
+ result = await _run_subagent(
214
+ agent_def,
215
+ description,
216
+ model_override,
217
+ self.api_key,
218
+ self.config,
219
+ self._tracker,
220
+ self._current_tree,
221
+ parent,
222
+ parent_messages=parent_messages,
223
+ sub_messages=sub_buffer,
224
+ authorize_tools=authorize,
225
+ debug=self._debug,
226
+ agent_id=agent_id,
227
+ session_id=session_id if self._session else None,
228
+ usage_stats=self._usage_stats,
229
+ lsp_manager=getattr(self, "_lsp_manager", None),
230
+ skill_selection=self._settings.get_skill_selection() if self._settings else None,
231
+ )
232
+ else:
233
+ result = await _run_subagent(
234
+ agent_def,
235
+ description,
236
+ model_override,
237
+ self.api_key,
238
+ self.config,
239
+ self._tracker,
240
+ parent_messages=parent_messages,
241
+ sub_messages=sub_buffer,
242
+ authorize_tools=authorize,
243
+ debug=self._debug,
244
+ agent_id=agent_id,
245
+ session_id=session_id if self._session else None,
246
+ usage_stats=self._usage_stats,
247
+ lsp_manager=getattr(self, "_lsp_manager", None),
248
+ skill_selection=self._settings.get_skill_selection() if self._settings else None,
249
+ )
250
+ ok = True
251
+ key = parent_tool_call_id or f"agent:{agent_id}"
252
+ self._sub_buffers.setdefault(key, []).extend(sub_buffer)
253
+ return result
254
+ finally:
255
+ if dock.active and ui_events.is_running:
256
+ await ui_events.emit(SubagentFinished(
257
+ agent_id=agent_id,
258
+ subagent_id=f"agent_{agent_id}",
259
+ ok=ok,
260
+ elapsed=time.monotonic() - started_at,
261
+ ))
262
+
263
+ def set_debug(self, value: bool) -> None:
264
+ self._debug = value
265
+ ui.set_debug(value)
266
+
267
+ def _build(self) -> None:
268
+ workflow = StateGraph(AgentState)
269
+
270
+ workflow.add_node("prepare", self._prepare_with_stream)
271
+ workflow.add_node("call_llm", self._call_llm)
272
+ workflow.add_node("execute_tools", self._execute_tools)
273
+ workflow.add_node("finalize", self._finalize)
274
+
275
+ workflow.set_entry_point("prepare")
276
+ workflow.add_edge("prepare", "call_llm")
277
+ workflow.add_conditional_edges("call_llm", self._router, {
278
+ "execute": "execute_tools",
279
+ "end": "finalize",
280
+ })
281
+ workflow.add_edge("execute_tools", "call_llm")
282
+ workflow.add_edge("finalize", END)
283
+
284
+ self.graph = workflow.compile()
285
+
286
+ # ── nodes ───────────────────────────────────────────────────────────
287
+
288
+ async def _prepare_with_stream(self, state: AgentState) -> dict:
289
+ base = _prepare(state)
290
+ agent_name = state.get("agent", "orchestrator")
291
+ self._current_agent = get_agent(agent_name)
292
+ role_prompt = self._current_agent.role_prompt if self._current_agent else ""
293
+ tool_contract = self._current_agent.tool_contract if self._current_agent else ""
294
+
295
+ interaction_mode = state.get("interaction_mode") or (
296
+ InteractionMode.PLAN.value if state.get("plan_mode", False) else self._interaction_mode.value
297
+ )
298
+ latest_user_text = _latest_user_text(state.get("messages", []))
299
+ instructions = await self._instruction.system()
300
+ skill_context = await self._instruction.skill_context_for(
301
+ latest_user_text,
302
+ agent=agent_name,
303
+ task_intent=state.get("task_intent"),
304
+ interaction_mode=interaction_mode,
305
+ )
306
+ mode_prompt = PLAN_MODE_APPEND if InteractionMode.parse(interaction_mode) == InteractionMode.PLAN else ""
307
+ summary = self._pending_summary or self._compaction_summary
308
+ self._pending_summary = None
309
+
310
+ context = RuntimeContextBuilder(
311
+ config=self.config,
312
+ workspace=state.get("workspace", "."),
313
+ base_system_prompt=BASE_SYSTEM_PROMPT,
314
+ role_prompt=role_prompt,
315
+ mode_prompt=mode_prompt,
316
+ tool_contract=tool_contract,
317
+ agent=agent_name,
318
+ interaction_mode=interaction_mode,
319
+ instructions=instructions,
320
+ skill_instructions=skill_context.instructions,
321
+ active_skill_summaries=skill_context.active,
322
+ summary=summary,
323
+ current_user_text=latest_user_text,
324
+ task_intent=state.get("task_intent"),
325
+ implementation_allowed=state.get("implementation_allowed"),
326
+ intent_resolution_reason=state.get("intent_resolution_reason", ""),
327
+ awaiting_implementation_approval=state.get("awaiting_implementation_approval", False),
328
+ approved_scope=state.get("approved_scope", ""),
329
+ goal=state.get("goal", ""),
330
+ goal_phase=state.get("goal_phase", ""),
331
+ goal_status=state.get("goal_status", ""),
332
+ goal_turn_count=state.get("goal_turn_count", 0),
333
+ ).build()
334
+ context.apply_to_messages(state.get("messages", []))
335
+
336
+ return base
337
+
338
+ async def _call_llm(self, state: AgentState) -> dict:
339
+ step = state.get("step_count", 0)
340
+ max_s = state.get("max_steps", 50)
341
+ if step > max_s:
342
+ return {"should_continue": False}
343
+
344
+ if self.model is None:
345
+ return {
346
+ "messages": [AIMessage(content=(
347
+ "No model configured. Use /model new to create a profile."
348
+ ))],
349
+ "step_count": step,
350
+ "should_continue": False,
351
+ }
352
+
353
+ agent = get_agent(state.get("agent", "orchestrator"))
354
+ agent_tool_ids = agent.tools if agent else None
355
+ all_tool_defs = self.tools.tools_for_llm()
356
+
357
+ # Filter tools based on agent's allowlist
358
+ if agent_tool_ids is not None:
359
+ tool_defs = [t for t in all_tool_defs if t["function"]["name"] in agent_tool_ids]
360
+ else:
361
+ tool_defs = all_tool_defs
362
+ tool_defs = filter_unavailable_lsp_tools(tool_defs, getattr(self, "_lsp_manager", None))
363
+
364
+ has_tool_budget = step < max_s - 1
365
+ if not has_tool_budget:
366
+ tool_defs = []
367
+
368
+ agent_name = state.get("agent", "orchestrator")
369
+ if self._debug:
370
+ ui.print()
371
+ ui.step_header(step, max_s, agent_name)
372
+
373
+ # ── LLM call with retry ────────────────────────────────────────
374
+ context_tokens = estimate_context_tokens(state["messages"], self.config.model.model)
375
+ self._usage_stats.update_context(context_tokens)
376
+ if self._session is not None:
377
+ await save_context_frame_from_messages(
378
+ session_id=self._session.id,
379
+ user_message_id=state.get("user_message_id"),
380
+ frame_kind="main",
381
+ agent_role=agent_name,
382
+ provider=self.config.model.provider,
383
+ model=self.config.model.model,
384
+ messages=state["messages"],
385
+ token_estimate=context_tokens,
386
+ metadata={
387
+ "step": step,
388
+ "max_steps": max_s,
389
+ "tool_count": len(tool_defs),
390
+ },
391
+ )
392
+ max_retries = 2
393
+ last_error = None
394
+ for attempt in range(max_retries + 1):
395
+ try:
396
+ renderer = StreamingRenderer(console, debug=self._debug)
397
+ model_with_tools = self.model.bind_tools(tool_defs) if tool_defs else self.model
398
+ assistant_msg = await _stream_llm(model_with_tools, state["messages"], renderer, resolve_protocol(self.config.model))
399
+ self._usage_stats.record_call(
400
+ extract_token_usage(assistant_msg),
401
+ fallback_input_tokens=context_tokens,
402
+ fallback_output_tokens=estimate_message_tokens(assistant_msg, self.config.model.model),
403
+ messages=state["messages"],
404
+ model=self.config.model.model,
405
+ cache_key=f"{self.config.model.provider}/{self.config.model.model}",
406
+ )
407
+ if self._debug or not assistant_msg.tool_calls:
408
+ ui.print()
409
+ break
410
+ except Exception as e:
411
+ last_error = e
412
+ if attempt < max_retries:
413
+ delay = (attempt + 1) * 2
414
+ ui.print(f"[dim]LLM error, retrying in {delay}s: {e}[/dim]")
415
+ await asyncio.sleep(delay)
416
+ else:
417
+ ui.error(f"LLM call failed after {max_retries + 1} attempts: {e}")
418
+ return {
419
+ "messages": [AIMessage(content=f"LLM call failed: {e}")],
420
+ "step_count": step,
421
+ "should_continue": False,
422
+ }
423
+ else:
424
+ # All retries exhausted
425
+ return {
426
+ "messages": [AIMessage(content=f"LLM call failed after all retries: {last_error}")],
427
+ "step_count": step,
428
+ "should_continue": False,
429
+ }
430
+
431
+ return {
432
+ "messages": [assistant_msg],
433
+ "step_count": step + 1,
434
+ }
435
+
436
+ def _router(self, state: AgentState) -> str:
437
+ last = state["messages"][-1]
438
+ if isinstance(last, AIMessage) and last.tool_calls:
439
+ if state.get("step_count", 0) >= state.get("max_steps", 50):
440
+ return "end"
441
+ return "execute"
442
+ return "end"
443
+
444
+ async def _finalize(self, state: AgentState) -> dict:
445
+ return {}
446
+
447
+
448
+ def _latest_user_text(messages: list) -> str:
449
+ for msg in reversed(messages):
450
+ if isinstance(msg, HumanMessage):
451
+ content = msg.content
452
+ if isinstance(content, str):
453
+ return content
454
+ if isinstance(content, list):
455
+ parts: list[str] = []
456
+ for item in content:
457
+ if isinstance(item, dict) and item.get("type") == "text":
458
+ text = item.get("text", "")
459
+ if isinstance(text, str):
460
+ parts.append(text)
461
+ return "\n".join(parts)
462
+ return str(content)
463
+ return ""
@@ -0,0 +1 @@
1
+ """Implementation parts for VoidXGraph."""