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,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)