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
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Tool execution node for the agent graph."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
from langchain_core.messages import AIMessage, ToolMessage
|
|
9
|
+
|
|
10
|
+
from voidx.agent.graph_components.runtime import current_parent_tool_call_id, ui
|
|
11
|
+
from voidx.tools.base import ToolContext
|
|
12
|
+
from voidx.ui.console import _fmt_args, _title
|
|
13
|
+
from voidx.ui.dock import dock
|
|
14
|
+
from voidx.ui.events import (
|
|
15
|
+
FileChangeAppended,
|
|
16
|
+
ToolFinished,
|
|
17
|
+
ToolResultAppended,
|
|
18
|
+
ToolStarted,
|
|
19
|
+
ui_events,
|
|
20
|
+
)
|
|
21
|
+
from voidx.ui.session_changes import session_tracker
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class GraphToolExecutionMixin:
|
|
25
|
+
async def _execute_tools(self, state) -> dict:
|
|
26
|
+
last = state["messages"][-1]
|
|
27
|
+
if not isinstance(last, AIMessage) or not last.tool_calls:
|
|
28
|
+
return {}
|
|
29
|
+
|
|
30
|
+
if dock.active and dock.current_agent is not None:
|
|
31
|
+
self._turn_node = dock.current_agent
|
|
32
|
+
|
|
33
|
+
self._current_messages = state["messages"]
|
|
34
|
+
ctx = ToolContext(
|
|
35
|
+
workspace=state.get("workspace", self._workspace),
|
|
36
|
+
file_mtimes=self._file_mtimes,
|
|
37
|
+
mcp_manager=getattr(self, "_mcp_manager", None),
|
|
38
|
+
lsp_manager=getattr(self, "_lsp_manager", None),
|
|
39
|
+
sandbox_extra_paths=self._permission.sandbox_workspace_write,
|
|
40
|
+
)
|
|
41
|
+
agent_name = state.get("agent", "orchestrator")
|
|
42
|
+
session_id = self._session.id if self._session else "default"
|
|
43
|
+
plan_mode = state.get("plan_mode", False)
|
|
44
|
+
interaction_mode = state.get("interaction_mode")
|
|
45
|
+
|
|
46
|
+
tool_calls = last.tool_calls
|
|
47
|
+
self._sub_buffers = {}
|
|
48
|
+
|
|
49
|
+
approved, denied = await self._authorize_tool_calls(
|
|
50
|
+
tool_calls,
|
|
51
|
+
agent_name=agent_name,
|
|
52
|
+
plan_mode=plan_mode,
|
|
53
|
+
session_id=session_id,
|
|
54
|
+
interaction_mode=interaction_mode,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# ── Phase 2: parallel execution of all approved tools ────────
|
|
58
|
+
|
|
59
|
+
async def execute_one(tc):
|
|
60
|
+
tid = tc["name"]
|
|
61
|
+
targs = tc.get("args", {})
|
|
62
|
+
cid = tc.get("id", "")
|
|
63
|
+
|
|
64
|
+
tool_event_id = cid or f"{tid}:{id(tc)}"
|
|
65
|
+
tool_node = None
|
|
66
|
+
if dock.active and ui_events.is_running:
|
|
67
|
+
gerund = _title(ui._TOOL_GERUND.get(tid, tid + "ing"))
|
|
68
|
+
tool_node = await ui_events.request(ToolStarted(
|
|
69
|
+
tool_call_id=tool_event_id,
|
|
70
|
+
tool_name=tid,
|
|
71
|
+
label=gerund,
|
|
72
|
+
args=_fmt_args(targs),
|
|
73
|
+
raw_args=targs,
|
|
74
|
+
))
|
|
75
|
+
if dock.active and dock.current_agent is not None:
|
|
76
|
+
self._turn_node = dock.current_agent
|
|
77
|
+
elif dock.active:
|
|
78
|
+
gerund = _title(ui._TOOL_GERUND.get(tid, tid + "ing"))
|
|
79
|
+
tool_node = dock.start_tool(
|
|
80
|
+
gerund,
|
|
81
|
+
_fmt_args(targs),
|
|
82
|
+
tool_call_id=tool_event_id,
|
|
83
|
+
tool_name=tid,
|
|
84
|
+
raw_args=targs,
|
|
85
|
+
)
|
|
86
|
+
if dock.current_agent is not None:
|
|
87
|
+
self._turn_node = dock.current_agent
|
|
88
|
+
else:
|
|
89
|
+
ui.tool_call(tid, targs)
|
|
90
|
+
|
|
91
|
+
t0 = time.monotonic()
|
|
92
|
+
ok = True
|
|
93
|
+
try:
|
|
94
|
+
session_tracker.capture_tool_call(tid, targs, ctx.workspace, ctx.sandbox_extra_paths)
|
|
95
|
+
parent_tool_token = current_parent_tool_call_id.set(tool_event_id)
|
|
96
|
+
try:
|
|
97
|
+
result = await self.tools.execute_tool(tid, targs, ctx)
|
|
98
|
+
finally:
|
|
99
|
+
current_parent_tool_call_id.reset(parent_tool_token)
|
|
100
|
+
ok = self._tool_result_ok(result)
|
|
101
|
+
except Exception as e:
|
|
102
|
+
from voidx.tools.base import ToolResult
|
|
103
|
+
result = ToolResult(
|
|
104
|
+
output=f"Tool execution error: {e}",
|
|
105
|
+
metadata={"error": str(e)},
|
|
106
|
+
)
|
|
107
|
+
ok = False
|
|
108
|
+
elapsed = time.monotonic() - t0
|
|
109
|
+
|
|
110
|
+
# on-failure: notify user when auto-approved tool fails
|
|
111
|
+
if not ok and hasattr(self, "_needs_failure_check"):
|
|
112
|
+
failure_tc = self._needs_failure_check.get(cid, None)
|
|
113
|
+
if failure_tc and self._permission.approval_policy == "on-failure":
|
|
114
|
+
self._notify_tool_failure(failure_tc, result)
|
|
115
|
+
elif ok and hasattr(self, "_needs_failure_check"):
|
|
116
|
+
self._clear_failure_check(cid)
|
|
117
|
+
|
|
118
|
+
if dock.active and ui_events.is_running:
|
|
119
|
+
await ui_events.emit(ToolFinished(
|
|
120
|
+
tool_call_id=tool_event_id,
|
|
121
|
+
label=_title(tid),
|
|
122
|
+
elapsed=elapsed,
|
|
123
|
+
ok=ok,
|
|
124
|
+
))
|
|
125
|
+
elif tool_node:
|
|
126
|
+
dock.finish_tool_node(tool_node, _title(tid), elapsed, ok)
|
|
127
|
+
else:
|
|
128
|
+
ui.tool_done(tid, elapsed, ok)
|
|
129
|
+
|
|
130
|
+
# Render diff to terminal (if any)
|
|
131
|
+
if getattr(result, "diff", None) and ok:
|
|
132
|
+
session_tracker.record_diff(result.diff)
|
|
133
|
+
if dock.active and ui_events.is_running:
|
|
134
|
+
await ui_events.emit(FileChangeAppended(
|
|
135
|
+
tool_call_id=tool_event_id,
|
|
136
|
+
diff_text=result.diff,
|
|
137
|
+
))
|
|
138
|
+
elif tool_node:
|
|
139
|
+
dock.append_file_change(
|
|
140
|
+
result.diff,
|
|
141
|
+
parent=tool_node,
|
|
142
|
+
tool_call_id=tool_event_id,
|
|
143
|
+
)
|
|
144
|
+
else:
|
|
145
|
+
from voidx.ui.diff import diff_stat
|
|
146
|
+
added, removed = diff_stat(result.diff)
|
|
147
|
+
ui.print(f" [green]+{added}[/green] [red]−{removed}[/red]")
|
|
148
|
+
if self._debug and not tool_node:
|
|
149
|
+
ui.diff(result.diff)
|
|
150
|
+
elif self._debug:
|
|
151
|
+
if dock.active and ui_events.is_running:
|
|
152
|
+
await ui_events.emit(ToolResultAppended(
|
|
153
|
+
tool_call_id=tool_event_id,
|
|
154
|
+
text=result.output,
|
|
155
|
+
))
|
|
156
|
+
elif tool_node:
|
|
157
|
+
dock.append_tool_result(
|
|
158
|
+
result.output,
|
|
159
|
+
parent=tool_node,
|
|
160
|
+
tool_call_id=tool_event_id,
|
|
161
|
+
)
|
|
162
|
+
else:
|
|
163
|
+
ui.tool_result(result.output)
|
|
164
|
+
|
|
165
|
+
return ToolMessage(content=result.output, tool_call_id=cid)
|
|
166
|
+
|
|
167
|
+
# Run all approved tools in parallel
|
|
168
|
+
executed = await asyncio.gather(*[execute_one(tc) for tc in approved])
|
|
169
|
+
|
|
170
|
+
# Clear on-failure tracking for this batch (full logic in Phase 2)
|
|
171
|
+
if hasattr(self, "_needs_failure_check"):
|
|
172
|
+
self._needs_failure_check.clear()
|
|
173
|
+
|
|
174
|
+
if dock.active and ui_events.is_running:
|
|
175
|
+
await ui_events.drain()
|
|
176
|
+
|
|
177
|
+
# Child-agent messages are buffered by parent tool_call_id. Append them
|
|
178
|
+
# only after parent ToolMessages so parent tool_use→tool_result
|
|
179
|
+
# adjacency is preserved for ALL agent calls.
|
|
180
|
+
sub_buffers: dict[str, list] = getattr(self, "_sub_buffers", {})
|
|
181
|
+
approved_ids = [tc.get("id", "") for tc in approved]
|
|
182
|
+
extra: list = []
|
|
183
|
+
for call_id in approved_ids:
|
|
184
|
+
extra.extend(sub_buffers.get(call_id, []))
|
|
185
|
+
for call_id, messages in sub_buffers.items():
|
|
186
|
+
if call_id not in approved_ids:
|
|
187
|
+
extra.extend(messages)
|
|
188
|
+
self._sub_buffers = {}
|
|
189
|
+
|
|
190
|
+
# Denied tools get error messages
|
|
191
|
+
denied_msgs = [
|
|
192
|
+
ToolMessage(content=reason, tool_call_id=tc.get("id", ""))
|
|
193
|
+
for tc, reason in denied
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
return {"messages": list(executed) + extra + denied_msgs}
|
|
197
|
+
|
|
198
|
+
@staticmethod
|
|
199
|
+
def _tool_result_ok(result) -> bool:
|
|
200
|
+
metadata = getattr(result, "metadata", {}) or {}
|
|
201
|
+
if metadata.get("error") or metadata.get("blocked") or metadata.get("timeout"):
|
|
202
|
+
return False
|
|
203
|
+
if "exit_code" in metadata:
|
|
204
|
+
try:
|
|
205
|
+
return int(metadata.get("exit_code") or 0) == 0
|
|
206
|
+
except (TypeError, ValueError):
|
|
207
|
+
return False
|
|
208
|
+
return True
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"""Structured runtime context assembly for LLM calls."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Iterable
|
|
8
|
+
|
|
9
|
+
from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
from voidx.config import ApprovalReviewer, Config
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class InteractionMode(str, Enum):
|
|
16
|
+
AUTO = "auto"
|
|
17
|
+
PLAN = "plan"
|
|
18
|
+
GOAL = "goal"
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def parse(cls, value: str | "InteractionMode" | None) -> "InteractionMode":
|
|
22
|
+
if isinstance(value, cls):
|
|
23
|
+
return value
|
|
24
|
+
if not value:
|
|
25
|
+
return cls.AUTO
|
|
26
|
+
normalized = str(value).strip().lower()
|
|
27
|
+
for mode in cls:
|
|
28
|
+
if mode.value == normalized:
|
|
29
|
+
return mode
|
|
30
|
+
raise ValueError(f"Invalid interaction mode: {value}")
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def denies_writes(self) -> bool:
|
|
34
|
+
return self == InteractionMode.PLAN
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TaskIntent(str, Enum):
|
|
38
|
+
CHAT = "chat"
|
|
39
|
+
INSPECT = "inspect"
|
|
40
|
+
DESIGN = "design"
|
|
41
|
+
REVIEW = "review"
|
|
42
|
+
IMPLEMENT = "implement"
|
|
43
|
+
DEBUG = "debug"
|
|
44
|
+
AMBIGUOUS = "ambiguous"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
_IMPLEMENT_HINTS = (
|
|
48
|
+
"fix", "implement", "change", "edit", "write", "refactor", "patch",
|
|
49
|
+
"apply", "do it", "go ahead", "start coding",
|
|
50
|
+
"\u4fee\u590d", "\u5b9e\u73b0", "\u4fee\u6539", "\u6539\u4e00\u4e0b",
|
|
51
|
+
"\u76f4\u63a5\u6539", "\u5f00\u59cb\u5e72", "\u5f00\u59cb\u505a",
|
|
52
|
+
"\u52a8\u624b", "\u843d\u5730", "\u7ee7\u7eed\u6539",
|
|
53
|
+
"\u7ee7\u7eed\u505a", "\u7ee7\u7eed\u5b9e\u73b0",
|
|
54
|
+
"\u7ee7\u7eed\u4fee\u590d", "\u53ef\u4ee5\u6539",
|
|
55
|
+
"\u53ef\u4ee5\u5f00\u59cb",
|
|
56
|
+
)
|
|
57
|
+
_DESIGN_HINTS = (
|
|
58
|
+
"design", "plan", "proposal", "approach", "architecture", "suggest",
|
|
59
|
+
"\u8bbe\u8ba1", "\u65b9\u6848", "\u5efa\u8bae", "\u600e\u4e48\u6539",
|
|
60
|
+
"\u5982\u4f55\u6539", "\u8ba8\u8bba", "\u89c4\u5212",
|
|
61
|
+
)
|
|
62
|
+
_INSPECT_HINTS = (
|
|
63
|
+
"look at", "inspect", "analyze", "explain", "understand", "check",
|
|
64
|
+
"what is", "why", "how does",
|
|
65
|
+
"\u770b\u770b", "\u770b\u4e00\u4e0b", "\u5206\u6790", "\u68b3\u7406",
|
|
66
|
+
"\u4e86\u89e3", "\u68c0\u67e5", "\u73b0\u72b6", "\u662f\u4ec0\u4e48",
|
|
67
|
+
"\u4e3a\u4ec0\u4e48",
|
|
68
|
+
)
|
|
69
|
+
_REVIEW_HINTS = ("review", "\u5ba1\u67e5", "\u590d\u6838", "\u8bc4\u5ba1")
|
|
70
|
+
_DEBUG_HINTS = ("debug", "bug", "error", "traceback", "\u62a5\u9519", "\u6392\u67e5", "\u95ee\u9898")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def infer_task_intent(text: str, interaction_mode: str | InteractionMode | None = None) -> TaskIntent:
|
|
74
|
+
mode = InteractionMode.parse(interaction_mode)
|
|
75
|
+
if mode == InteractionMode.PLAN:
|
|
76
|
+
return TaskIntent.DESIGN
|
|
77
|
+
|
|
78
|
+
normalized = text.lower()
|
|
79
|
+
if _contains_any(normalized, _IMPLEMENT_HINTS):
|
|
80
|
+
return TaskIntent.IMPLEMENT
|
|
81
|
+
if _contains_any(normalized, _REVIEW_HINTS):
|
|
82
|
+
return TaskIntent.REVIEW
|
|
83
|
+
if _contains_any(normalized, _DEBUG_HINTS):
|
|
84
|
+
return TaskIntent.DEBUG
|
|
85
|
+
if _contains_any(normalized, _DESIGN_HINTS):
|
|
86
|
+
return TaskIntent.DESIGN
|
|
87
|
+
if _contains_any(normalized, _INSPECT_HINTS):
|
|
88
|
+
return TaskIntent.INSPECT
|
|
89
|
+
return TaskIntent.CHAT
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def implementation_allowed_for_intent(intent: str | TaskIntent) -> bool:
|
|
93
|
+
return TaskIntent(intent) == TaskIntent.IMPLEMENT
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ExecutionPolicy(BaseModel):
|
|
97
|
+
sandbox_mode: str
|
|
98
|
+
approval_policy: str
|
|
99
|
+
approval_reviewer: str = ApprovalReviewer.USER.value
|
|
100
|
+
extra_write_paths: list[str] = Field(default_factory=list)
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def from_config(cls, config: Config) -> "ExecutionPolicy":
|
|
104
|
+
return cls(
|
|
105
|
+
sandbox_mode=config.sandbox_mode.value,
|
|
106
|
+
approval_policy=config.approval_policy.value,
|
|
107
|
+
approval_reviewer=config.approval_reviewer.value,
|
|
108
|
+
extra_write_paths=list(config.sandbox_workspace_write),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class RuntimeEnvelope(BaseModel):
|
|
113
|
+
date: str
|
|
114
|
+
workspace: str
|
|
115
|
+
provider: str
|
|
116
|
+
model: str
|
|
117
|
+
interaction_mode: InteractionMode
|
|
118
|
+
permission_profile: str
|
|
119
|
+
execution_policy: ExecutionPolicy
|
|
120
|
+
agent: str
|
|
121
|
+
agent_id: int = -1
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ContextSection(BaseModel):
|
|
125
|
+
name: str
|
|
126
|
+
content: str
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class RuntimeContext(BaseModel):
|
|
130
|
+
sections: list[ContextSection]
|
|
131
|
+
task_sections: list[ContextSection] = Field(default_factory=list)
|
|
132
|
+
|
|
133
|
+
def section_names(self) -> list[str]:
|
|
134
|
+
names = [section.name for section in self.sections]
|
|
135
|
+
names.extend(section.name for section in self.task_sections)
|
|
136
|
+
return names
|
|
137
|
+
|
|
138
|
+
def render_system(self) -> str:
|
|
139
|
+
return _render_sections(self.sections)
|
|
140
|
+
|
|
141
|
+
def render_task_context(self) -> str:
|
|
142
|
+
return _render_sections(self.task_sections)
|
|
143
|
+
|
|
144
|
+
def apply_to_messages(self, messages: list[BaseMessage]) -> None:
|
|
145
|
+
ContextCompiler(self).apply_to_messages(messages)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class ContextCompiler:
|
|
149
|
+
"""Compile structured context sections into one clean LLM message frame."""
|
|
150
|
+
|
|
151
|
+
def __init__(self, context: RuntimeContext) -> None:
|
|
152
|
+
self.context = context
|
|
153
|
+
|
|
154
|
+
def compile_messages(self, messages: list[BaseMessage]) -> list[BaseMessage]:
|
|
155
|
+
semantic_messages = [message for message in messages if not isinstance(message, SystemMessage)]
|
|
156
|
+
current_user_index = _last_user_index(semantic_messages)
|
|
157
|
+
|
|
158
|
+
prefix = SystemMessage(content=self.context.render_system())
|
|
159
|
+
task_context = self.context.render_task_context()
|
|
160
|
+
if task_context:
|
|
161
|
+
if current_user_index is None:
|
|
162
|
+
semantic_messages.append(HumanMessage(content=task_context))
|
|
163
|
+
else:
|
|
164
|
+
current = semantic_messages[current_user_index]
|
|
165
|
+
semantic_messages[current_user_index] = _prepend_task_context(current, task_context)
|
|
166
|
+
|
|
167
|
+
return [prefix, *semantic_messages]
|
|
168
|
+
|
|
169
|
+
def apply_to_messages(self, messages: list[BaseMessage]) -> None:
|
|
170
|
+
messages[:] = self.compile_messages(messages)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class RuntimeContextBuilder:
|
|
174
|
+
def __init__(
|
|
175
|
+
self,
|
|
176
|
+
*,
|
|
177
|
+
config: Config,
|
|
178
|
+
workspace: str,
|
|
179
|
+
agent_prompt: str | None = None,
|
|
180
|
+
base_system_prompt: str | None = None,
|
|
181
|
+
role_prompt: str = "",
|
|
182
|
+
mode_prompt: str = "",
|
|
183
|
+
tool_contract: str = "",
|
|
184
|
+
agent: str,
|
|
185
|
+
interaction_mode: str | InteractionMode,
|
|
186
|
+
instructions: Iterable[str] = (),
|
|
187
|
+
skill_instructions: Iterable[str] = (),
|
|
188
|
+
active_skill_summaries: Iterable[str] = (),
|
|
189
|
+
summary: str | None = None,
|
|
190
|
+
current_user_text: str = "",
|
|
191
|
+
task_intent: str | TaskIntent | None = None,
|
|
192
|
+
implementation_allowed: bool | None = None,
|
|
193
|
+
intent_resolution_reason: str = "",
|
|
194
|
+
awaiting_implementation_approval: bool = False,
|
|
195
|
+
approved_scope: str = "",
|
|
196
|
+
goal: str = "",
|
|
197
|
+
goal_phase: str = "",
|
|
198
|
+
goal_status: str = "",
|
|
199
|
+
goal_turn_count: int = 0,
|
|
200
|
+
agent_id: int = -1,
|
|
201
|
+
) -> None:
|
|
202
|
+
self.config = config
|
|
203
|
+
self.workspace = workspace
|
|
204
|
+
self.base_system_prompt = (base_system_prompt or agent_prompt or "").strip()
|
|
205
|
+
self.role_prompt = role_prompt.strip()
|
|
206
|
+
self.mode_prompt = mode_prompt.strip()
|
|
207
|
+
self.tool_contract = tool_contract.strip()
|
|
208
|
+
self.agent = agent
|
|
209
|
+
self.interaction_mode = InteractionMode.parse(interaction_mode)
|
|
210
|
+
self.instructions = [item for item in instructions if item.strip()]
|
|
211
|
+
self.skill_instructions = [item for item in skill_instructions if item.strip()]
|
|
212
|
+
self.active_skill_summaries = [item for item in active_skill_summaries if item.strip()]
|
|
213
|
+
self.summary = summary.strip() if summary else ""
|
|
214
|
+
self.current_user_text = current_user_text.strip()
|
|
215
|
+
self.task_intent = (
|
|
216
|
+
TaskIntent(task_intent)
|
|
217
|
+
if task_intent is not None
|
|
218
|
+
else infer_task_intent(self.current_user_text, self.interaction_mode)
|
|
219
|
+
)
|
|
220
|
+
self.implementation_allowed = (
|
|
221
|
+
implementation_allowed
|
|
222
|
+
if implementation_allowed is not None
|
|
223
|
+
else implementation_allowed_for_intent(self.task_intent)
|
|
224
|
+
)
|
|
225
|
+
self.intent_resolution_reason = intent_resolution_reason.strip()
|
|
226
|
+
self.awaiting_implementation_approval = awaiting_implementation_approval
|
|
227
|
+
self.approved_scope = approved_scope.strip()
|
|
228
|
+
self.goal = goal.strip()
|
|
229
|
+
self.goal_phase = goal_phase.strip()
|
|
230
|
+
self.goal_status = goal_status.strip()
|
|
231
|
+
self.goal_turn_count = goal_turn_count
|
|
232
|
+
self.agent_id = agent_id
|
|
233
|
+
|
|
234
|
+
def build(self) -> RuntimeContext:
|
|
235
|
+
date_str = datetime.now().astimezone().strftime("%Y-%m-%d %H:%M %Z")
|
|
236
|
+
envelope = RuntimeEnvelope(
|
|
237
|
+
date=date_str,
|
|
238
|
+
workspace=self.workspace,
|
|
239
|
+
provider=self.config.model.provider,
|
|
240
|
+
model=self.config.model.model,
|
|
241
|
+
interaction_mode=self.interaction_mode,
|
|
242
|
+
permission_profile=self.config.permission_mode.value,
|
|
243
|
+
execution_policy=ExecutionPolicy.from_config(self.config),
|
|
244
|
+
agent=self.agent,
|
|
245
|
+
agent_id=self.agent_id,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
sections = [
|
|
249
|
+
ContextSection(name="Base System", content=self.base_system_prompt),
|
|
250
|
+
]
|
|
251
|
+
if self.role_prompt:
|
|
252
|
+
sections.append(ContextSection(name="Role Prompt", content=self.role_prompt))
|
|
253
|
+
if self.mode_prompt:
|
|
254
|
+
sections.append(ContextSection(name="Mode Prompt", content=self.mode_prompt))
|
|
255
|
+
if self.tool_contract:
|
|
256
|
+
sections.append(ContextSection(name="Tool Contract", content=self.tool_contract))
|
|
257
|
+
sections.extend([
|
|
258
|
+
ContextSection(name="Workspace Facts", content=f"- Current workspace: {self.workspace}"),
|
|
259
|
+
])
|
|
260
|
+
|
|
261
|
+
if self.instructions:
|
|
262
|
+
sections.append(ContextSection(
|
|
263
|
+
name="Project Facts",
|
|
264
|
+
content="\n\n".join(self.instructions),
|
|
265
|
+
))
|
|
266
|
+
if self.summary:
|
|
267
|
+
sections.append(ContextSection(
|
|
268
|
+
name="Long Summary",
|
|
269
|
+
content=self.summary,
|
|
270
|
+
))
|
|
271
|
+
|
|
272
|
+
sections.append(ContextSection(name="Current Date", content=date_str))
|
|
273
|
+
sections.append(ContextSection(name="Runtime State", content=_render_envelope(envelope)))
|
|
274
|
+
|
|
275
|
+
task_sections = [
|
|
276
|
+
ContextSection(name="Recent Messages", content="- Preserved as native conversation messages above."),
|
|
277
|
+
]
|
|
278
|
+
if self.skill_instructions:
|
|
279
|
+
task_sections.append(ContextSection(
|
|
280
|
+
name="Active Skills",
|
|
281
|
+
content="\n\n".join(self.skill_instructions),
|
|
282
|
+
))
|
|
283
|
+
task_sections.append(ContextSection(
|
|
284
|
+
name="Current Task State",
|
|
285
|
+
content=self._current_task_state(),
|
|
286
|
+
))
|
|
287
|
+
|
|
288
|
+
return RuntimeContext(sections=sections, task_sections=task_sections)
|
|
289
|
+
|
|
290
|
+
def _current_task_state(self) -> str:
|
|
291
|
+
lines = [
|
|
292
|
+
f"- Mode: {self.interaction_mode.value}",
|
|
293
|
+
f"- Intent: {self.task_intent.value}",
|
|
294
|
+
f"- Awaiting implementation approval: {str(self.awaiting_implementation_approval).lower()}",
|
|
295
|
+
f"- Agent: {self.agent}",
|
|
296
|
+
f"- Agent ID: {self.agent_id}",
|
|
297
|
+
]
|
|
298
|
+
if self.active_skill_summaries:
|
|
299
|
+
lines.append(f"- Active workflow skills: {'; '.join(self.active_skill_summaries)}")
|
|
300
|
+
if self.intent_resolution_reason:
|
|
301
|
+
lines.append(f"- Intent resolution: {self.intent_resolution_reason}")
|
|
302
|
+
if self.approved_scope:
|
|
303
|
+
lines.append(f"- Approved scope: {self.approved_scope}")
|
|
304
|
+
if self.interaction_mode == InteractionMode.GOAL:
|
|
305
|
+
lines.append("- Goal mode: true")
|
|
306
|
+
lines.append(f"- Goal: {self.goal or 'not set'}")
|
|
307
|
+
lines.append(f"- Goal phase: {self.goal_phase or 'clarify'}")
|
|
308
|
+
lines.append(f"- Goal status: {self.goal_status or 'idle'}")
|
|
309
|
+
lines.append(f"- Goal turn count: {self.goal_turn_count}")
|
|
310
|
+
if self.current_user_text:
|
|
311
|
+
first_line = self.current_user_text.splitlines()[0][:160]
|
|
312
|
+
lines.append(f"- Latest user request: {first_line}")
|
|
313
|
+
if self.interaction_mode == InteractionMode.PLAN:
|
|
314
|
+
lines.append("- Constraint: plan mode blocks write/edit/lsp_format, write-capable bash, and implement delegation.")
|
|
315
|
+
elif self.interaction_mode == InteractionMode.GOAL:
|
|
316
|
+
lines.append("- Constraint: goal mode should keep work scoped to the current user goal and task state.")
|
|
317
|
+
lines.append("- Permission gate: tool calls are governed by the current permission mode, sandbox, and interaction mode.")
|
|
318
|
+
return "\n".join(lines)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _render_sections(sections: list[ContextSection]) -> str:
|
|
322
|
+
parts = ["VOIDX_RUNTIME_CONTEXT"]
|
|
323
|
+
for section in sections:
|
|
324
|
+
if not section.content.strip():
|
|
325
|
+
continue
|
|
326
|
+
parts.append(f"## {section.name}\n{section.content.strip()}")
|
|
327
|
+
return "\n\n".join(parts)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _render_envelope(envelope: RuntimeEnvelope) -> str:
|
|
331
|
+
policy = envelope.execution_policy
|
|
332
|
+
lines = [
|
|
333
|
+
f"- Workspace: {envelope.workspace}",
|
|
334
|
+
f"- Model: {envelope.provider}/{envelope.model}",
|
|
335
|
+
f"- Interaction mode: {envelope.interaction_mode.value}",
|
|
336
|
+
f"- Permission profile: {envelope.permission_profile}",
|
|
337
|
+
f"- Sandbox: {policy.sandbox_mode}",
|
|
338
|
+
f"- Approval policy: {policy.approval_policy}",
|
|
339
|
+
f"- Approval reviewer: {policy.approval_reviewer}",
|
|
340
|
+
f"- Agent: {envelope.agent}",
|
|
341
|
+
f"- Agent ID: {envelope.agent_id}",
|
|
342
|
+
]
|
|
343
|
+
if policy.extra_write_paths:
|
|
344
|
+
lines.append(f"- Extra write paths: {', '.join(policy.extra_write_paths)}")
|
|
345
|
+
return "\n".join(lines)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _last_user_index(messages: list[BaseMessage]) -> int | None:
|
|
349
|
+
for index in range(len(messages) - 1, -1, -1):
|
|
350
|
+
if isinstance(messages[index], HumanMessage):
|
|
351
|
+
return index
|
|
352
|
+
return None
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _prepend_task_context(message: BaseMessage, task_context: str) -> BaseMessage:
|
|
356
|
+
content = message.content
|
|
357
|
+
header = f"{task_context}\n\n## User Message"
|
|
358
|
+
if isinstance(content, str):
|
|
359
|
+
new_content = f"{header}\n{content}"
|
|
360
|
+
elif isinstance(content, list):
|
|
361
|
+
new_content = [{"type": "text", "text": header}, *content]
|
|
362
|
+
else:
|
|
363
|
+
new_content = f"{header}\n{content}"
|
|
364
|
+
return message.model_copy(update={"content": new_content})
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _contains_any(text: str, hints: tuple[str, ...]) -> bool:
|
|
368
|
+
return any(hint in text for hint in hints)
|