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/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."""
|