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,188 @@
1
+ """Compiled LLM context frame snapshots."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ from typing import Any
8
+
9
+ from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage, ToolMessage
10
+ from pydantic import BaseModel, Field
11
+
12
+ from voidx.memory.session import _now
13
+ from voidx.memory.store import _execute_commit, _fetch_all
14
+
15
+
16
+ class ContextFrameRecord(BaseModel):
17
+ id: int | None = None
18
+ session_id: str
19
+ user_message_id: int | None = None
20
+ frame_kind: str = "main"
21
+ agent_role: str = "orchestrator"
22
+ provider: str
23
+ model: str
24
+ prefix_hash: str
25
+ frame_hash: str
26
+ message_count: int
27
+ token_estimate: int = 0
28
+ messages: list[dict[str, Any]]
29
+ metadata: dict[str, Any] = Field(default_factory=dict)
30
+ created_at: str = Field(default_factory=_now)
31
+
32
+
33
+ def build_context_frame(
34
+ *,
35
+ session_id: str,
36
+ messages: list[BaseMessage],
37
+ provider: str,
38
+ model: str,
39
+ frame_kind: str = "main",
40
+ agent_role: str = "orchestrator",
41
+ user_message_id: int | None = None,
42
+ token_estimate: int = 0,
43
+ metadata: dict[str, Any] | None = None,
44
+ ) -> ContextFrameRecord:
45
+ serialized = [_serialize_message(message) for message in messages]
46
+ return ContextFrameRecord(
47
+ session_id=session_id,
48
+ user_message_id=user_message_id,
49
+ frame_kind=frame_kind,
50
+ agent_role=agent_role,
51
+ provider=provider,
52
+ model=model,
53
+ prefix_hash=_hash_payload(_stable_prefix_payload(serialized)),
54
+ frame_hash=_hash_payload(serialized),
55
+ message_count=len(serialized),
56
+ token_estimate=token_estimate,
57
+ messages=serialized,
58
+ metadata=metadata or {},
59
+ )
60
+
61
+
62
+ async def save_context_frame(record: ContextFrameRecord) -> int:
63
+ cur = await _execute_commit(
64
+ """INSERT INTO context_frames (
65
+ session_id, user_message_id, frame_kind, agent_role, provider,
66
+ model, prefix_hash, frame_hash, message_count, token_estimate,
67
+ messages_json, metadata_json, created_at
68
+ )
69
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
70
+ (
71
+ record.session_id,
72
+ record.user_message_id,
73
+ record.frame_kind,
74
+ record.agent_role,
75
+ record.provider,
76
+ record.model,
77
+ record.prefix_hash,
78
+ record.frame_hash,
79
+ record.message_count,
80
+ record.token_estimate,
81
+ json.dumps(record.messages, ensure_ascii=False, sort_keys=True),
82
+ json.dumps(record.metadata, ensure_ascii=False, sort_keys=True),
83
+ record.created_at,
84
+ ),
85
+ )
86
+ return cur.lastrowid
87
+
88
+
89
+ async def save_context_frame_from_messages(
90
+ *,
91
+ session_id: str,
92
+ messages: list[BaseMessage],
93
+ provider: str,
94
+ model: str,
95
+ frame_kind: str = "main",
96
+ agent_role: str = "orchestrator",
97
+ user_message_id: int | None = None,
98
+ token_estimate: int = 0,
99
+ metadata: dict[str, Any] | None = None,
100
+ ) -> int:
101
+ return await save_context_frame(build_context_frame(
102
+ session_id=session_id,
103
+ messages=messages,
104
+ provider=provider,
105
+ model=model,
106
+ frame_kind=frame_kind,
107
+ agent_role=agent_role,
108
+ user_message_id=user_message_id,
109
+ token_estimate=token_estimate,
110
+ metadata=metadata,
111
+ ))
112
+
113
+
114
+ async def load_context_frames(session_id: str, limit: int = 50) -> list[ContextFrameRecord]:
115
+ rows = await _fetch_all(
116
+ """SELECT * FROM context_frames
117
+ WHERE session_id = ?
118
+ ORDER BY id DESC
119
+ LIMIT ?""",
120
+ (session_id, limit),
121
+ )
122
+ records = [
123
+ ContextFrameRecord(
124
+ id=row["id"],
125
+ session_id=row["session_id"],
126
+ user_message_id=row["user_message_id"],
127
+ frame_kind=row["frame_kind"],
128
+ agent_role=row["agent_role"],
129
+ provider=row["provider"],
130
+ model=row["model"],
131
+ prefix_hash=row["prefix_hash"],
132
+ frame_hash=row["frame_hash"],
133
+ message_count=row["message_count"],
134
+ token_estimate=row["token_estimate"],
135
+ messages=json.loads(row["messages_json"]),
136
+ metadata=json.loads(row["metadata_json"]),
137
+ created_at=row["created_at"],
138
+ )
139
+ for row in rows
140
+ ]
141
+ return list(reversed(records))
142
+
143
+
144
+ def _serialize_message(message: BaseMessage) -> dict[str, Any]:
145
+ payload: dict[str, Any] = {
146
+ "role": _message_role(message),
147
+ "content": _json_safe(getattr(message, "content", "")),
148
+ }
149
+ if isinstance(message, AIMessage) and getattr(message, "tool_calls", None):
150
+ payload["tool_calls"] = _json_safe(message.tool_calls)
151
+ if isinstance(message, ToolMessage):
152
+ payload["tool_call_id"] = getattr(message, "tool_call_id", "")
153
+ return payload
154
+
155
+
156
+ def _message_role(message: BaseMessage) -> str:
157
+ if isinstance(message, SystemMessage):
158
+ return "system"
159
+ if isinstance(message, HumanMessage):
160
+ return "user"
161
+ if isinstance(message, AIMessage):
162
+ return "assistant"
163
+ if isinstance(message, ToolMessage):
164
+ return "tool"
165
+ return getattr(message, "type", message.__class__.__name__)
166
+
167
+
168
+ def _stable_prefix_payload(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
169
+ if not messages or messages[0].get("role") != "system":
170
+ return []
171
+ content = str(messages[0].get("content", ""))
172
+ stable_content = content.split("\n\n## Current Date", 1)[0]
173
+ return [{"role": "system", "content": stable_content}]
174
+
175
+
176
+ def _hash_payload(payload: Any) -> str:
177
+ encoded = json.dumps(
178
+ payload,
179
+ ensure_ascii=False,
180
+ sort_keys=True,
181
+ separators=(",", ":"),
182
+ default=str,
183
+ ).encode("utf-8")
184
+ return hashlib.sha256(encoded).hexdigest()
185
+
186
+
187
+ def _json_safe(value: Any) -> Any:
188
+ return json.loads(json.dumps(value, ensure_ascii=False, sort_keys=True, default=str))
@@ -0,0 +1,98 @@
1
+ """User-level model profile persistence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from voidx.memory.session import _now
8
+ from voidx.memory.store import _get_db, _write_lock
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class ModelProfileRow:
13
+ name: str
14
+ provider: str
15
+ model: str
16
+ api_key: str = ""
17
+ base_url: str | None = None
18
+ protocol: str | None = None
19
+
20
+ def list_model_profiles() -> list[ModelProfileRow]:
21
+ conn = _get_db()
22
+ rows = conn.execute(
23
+ """SELECT name, provider, model, api_key, base_url, protocol
24
+ FROM model_profiles
25
+ ORDER BY updated_at DESC, name ASC"""
26
+ ).fetchall()
27
+ return [_row_to_profile(row) for row in rows]
28
+
29
+
30
+ def get_model_profile(name: str) -> ModelProfileRow | None:
31
+ conn = _get_db()
32
+ row = conn.execute(
33
+ """SELECT name, provider, model, api_key, base_url, protocol
34
+ FROM model_profiles
35
+ WHERE name = ?""",
36
+ (name,),
37
+ ).fetchone()
38
+ return _row_to_profile(row) if row else None
39
+
40
+
41
+ def first_model_profile_for_provider(provider: str) -> ModelProfileRow | None:
42
+ conn = _get_db()
43
+ row = conn.execute(
44
+ """SELECT name, provider, model, api_key, base_url, protocol
45
+ FROM model_profiles
46
+ WHERE provider = ?
47
+ ORDER BY updated_at DESC, name ASC
48
+ LIMIT 1""",
49
+ (provider,),
50
+ ).fetchone()
51
+ return _row_to_profile(row) if row else None
52
+
53
+
54
+ def save_model_profile(profile: ModelProfileRow) -> None:
55
+ now = _now()
56
+ conn = _get_db()
57
+ with _write_lock:
58
+ conn.execute(
59
+ """INSERT INTO model_profiles
60
+ (name, provider, model, api_key, base_url, protocol, created_at, updated_at)
61
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
62
+ ON CONFLICT(name) DO UPDATE SET
63
+ provider = excluded.provider,
64
+ model = excluded.model,
65
+ api_key = excluded.api_key,
66
+ base_url = excluded.base_url,
67
+ protocol = excluded.protocol,
68
+ updated_at = excluded.updated_at""",
69
+ (
70
+ profile.name,
71
+ profile.provider,
72
+ profile.model,
73
+ profile.api_key,
74
+ profile.base_url,
75
+ profile.protocol,
76
+ now,
77
+ now,
78
+ ),
79
+ )
80
+ conn.commit()
81
+
82
+
83
+ def delete_model_profile(name: str) -> None:
84
+ conn = _get_db()
85
+ with _write_lock:
86
+ conn.execute("DELETE FROM model_profiles WHERE name = ?", (name,))
87
+ conn.commit()
88
+
89
+
90
+ def _row_to_profile(row) -> ModelProfileRow:
91
+ return ModelProfileRow(
92
+ name=row["name"],
93
+ provider=row["provider"],
94
+ model=row["model"],
95
+ api_key=row["api_key"],
96
+ base_url=row["base_url"],
97
+ protocol=row["protocol"],
98
+ )
@@ -0,0 +1,240 @@
1
+ """Structured runtime state persistence for session resume."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+ from voidx.agent.runtime_context import InteractionMode, TaskIntent
8
+ from voidx.agent.task_state import TaskPhase, TaskRun, TaskRunStatus, TaskState
9
+ from voidx.memory.session import _now
10
+ from voidx.memory.store import _execute_commit, _fetch_one
11
+
12
+
13
+ class RuntimeStateSnapshot(BaseModel):
14
+ interaction_mode: InteractionMode = InteractionMode.AUTO
15
+ task_state: TaskState = Field(default_factory=TaskState)
16
+ task_run: TaskRun = Field(default_factory=TaskRun)
17
+ compaction_summary: str = ""
18
+
19
+
20
+ class MessageRuntimeSnapshot(BaseModel):
21
+ message_id: int
22
+ session_id: str
23
+ interaction_mode: InteractionMode = InteractionMode.AUTO
24
+ task_intent: TaskIntent = TaskIntent.CHAT
25
+ implementation_allowed: bool = False
26
+ intent_resolution_reason: str = ""
27
+ goal: str = ""
28
+ goal_phase: str = TaskPhase.CLARIFY.value
29
+ goal_status: str = TaskRunStatus.IDLE.value
30
+ goal_turn_count: int = 0
31
+ awaiting_implementation_approval: bool = False
32
+ approved_scope: str = ""
33
+ created_at: str = Field(default_factory=_now)
34
+
35
+
36
+ async def save_runtime_state(session_id: str, snapshot: RuntimeStateSnapshot) -> None:
37
+ await save_session_runtime_state(
38
+ session_id,
39
+ snapshot.interaction_mode,
40
+ snapshot.task_state,
41
+ snapshot.compaction_summary,
42
+ )
43
+ await save_session_task_run(session_id, snapshot.task_run)
44
+
45
+
46
+ async def load_runtime_state(session_id: str) -> RuntimeStateSnapshot:
47
+ return RuntimeStateSnapshot(
48
+ interaction_mode=await load_interaction_mode(session_id),
49
+ task_state=await load_task_state(session_id),
50
+ task_run=await load_task_run(session_id),
51
+ compaction_summary=await load_compaction_summary(session_id),
52
+ )
53
+
54
+
55
+ async def save_session_runtime_state(
56
+ session_id: str,
57
+ interaction_mode: InteractionMode,
58
+ task_state: TaskState,
59
+ compaction_summary: str = "",
60
+ ) -> None:
61
+ await _execute_commit(
62
+ """INSERT INTO session_runtime_state (
63
+ session_id, interaction_mode, current_intent, previous_intent,
64
+ current_goal, awaiting_implementation_approval, approved_scope,
65
+ last_plan_summary, compaction_summary, updated_at
66
+ )
67
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
68
+ ON CONFLICT(session_id) DO UPDATE SET
69
+ interaction_mode = excluded.interaction_mode,
70
+ current_intent = excluded.current_intent,
71
+ previous_intent = excluded.previous_intent,
72
+ current_goal = excluded.current_goal,
73
+ awaiting_implementation_approval = excluded.awaiting_implementation_approval,
74
+ approved_scope = excluded.approved_scope,
75
+ last_plan_summary = excluded.last_plan_summary,
76
+ compaction_summary = excluded.compaction_summary,
77
+ updated_at = excluded.updated_at""",
78
+ (
79
+ session_id,
80
+ interaction_mode.value,
81
+ task_state.current_intent.value,
82
+ task_state.previous_intent.value if task_state.previous_intent else None,
83
+ task_state.current_goal,
84
+ 1 if task_state.awaiting_implementation_approval else 0,
85
+ task_state.approved_scope,
86
+ task_state.last_plan_summary,
87
+ compaction_summary,
88
+ _now(),
89
+ ),
90
+ )
91
+
92
+
93
+ async def load_interaction_mode(session_id: str) -> InteractionMode:
94
+ row = await _fetch_one(
95
+ "SELECT interaction_mode FROM session_runtime_state WHERE session_id = ?",
96
+ (session_id,),
97
+ )
98
+ if not row:
99
+ return InteractionMode.AUTO
100
+ return InteractionMode.parse(row["interaction_mode"])
101
+
102
+
103
+ async def load_task_state(session_id: str) -> TaskState:
104
+ row = await _fetch_one(
105
+ "SELECT * FROM session_runtime_state WHERE session_id = ?",
106
+ (session_id,),
107
+ )
108
+ if not row:
109
+ return TaskState()
110
+ return TaskState(
111
+ current_intent=TaskIntent(row["current_intent"]),
112
+ previous_intent=TaskIntent(row["previous_intent"]) if row["previous_intent"] else None,
113
+ current_goal=row["current_goal"],
114
+ awaiting_implementation_approval=bool(row["awaiting_implementation_approval"]),
115
+ approved_scope=row["approved_scope"],
116
+ last_plan_summary=row["last_plan_summary"],
117
+ )
118
+
119
+
120
+ async def load_compaction_summary(session_id: str) -> str:
121
+ row = await _fetch_one(
122
+ "SELECT compaction_summary FROM session_runtime_state WHERE session_id = ?",
123
+ (session_id,),
124
+ )
125
+ if not row:
126
+ return ""
127
+ return row["compaction_summary"] if "compaction_summary" in row.keys() else ""
128
+
129
+
130
+ async def save_session_task_run(session_id: str, task_run: TaskRun) -> None:
131
+ await _execute_commit(
132
+ """INSERT INTO session_task_runs (
133
+ session_id, goal, phase, status, approved_scope,
134
+ awaiting_implementation_approval, turn_count, updated_at
135
+ )
136
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
137
+ ON CONFLICT(session_id) DO UPDATE SET
138
+ goal = excluded.goal,
139
+ phase = excluded.phase,
140
+ status = excluded.status,
141
+ approved_scope = excluded.approved_scope,
142
+ awaiting_implementation_approval = excluded.awaiting_implementation_approval,
143
+ turn_count = excluded.turn_count,
144
+ updated_at = excluded.updated_at""",
145
+ (
146
+ session_id,
147
+ task_run.goal,
148
+ task_run.phase.value,
149
+ task_run.status.value,
150
+ task_run.approved_scope,
151
+ 1 if task_run.awaiting_implementation_approval else 0,
152
+ task_run.turn_count,
153
+ _now(),
154
+ ),
155
+ )
156
+
157
+
158
+ async def load_task_run(session_id: str) -> TaskRun:
159
+ row = await _fetch_one(
160
+ "SELECT * FROM session_task_runs WHERE session_id = ?",
161
+ (session_id,),
162
+ )
163
+ if not row:
164
+ return TaskRun()
165
+ return TaskRun(
166
+ goal=row["goal"],
167
+ phase=TaskPhase(row["phase"]),
168
+ status=TaskRunStatus(row["status"]),
169
+ approved_scope=row["approved_scope"],
170
+ awaiting_implementation_approval=bool(row["awaiting_implementation_approval"]),
171
+ turn_count=row["turn_count"],
172
+ )
173
+
174
+
175
+ async def save_message_runtime_snapshot(snapshot: MessageRuntimeSnapshot) -> None:
176
+ await _execute_commit(
177
+ """INSERT INTO message_runtime_snapshots (
178
+ message_id, session_id, interaction_mode, task_intent,
179
+ implementation_allowed, intent_resolution_reason, goal, goal_phase,
180
+ goal_status, goal_turn_count, awaiting_implementation_approval,
181
+ approved_scope, created_at
182
+ )
183
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
184
+ ON CONFLICT(message_id) DO UPDATE SET
185
+ interaction_mode = excluded.interaction_mode,
186
+ task_intent = excluded.task_intent,
187
+ implementation_allowed = excluded.implementation_allowed,
188
+ intent_resolution_reason = excluded.intent_resolution_reason,
189
+ goal = excluded.goal,
190
+ goal_phase = excluded.goal_phase,
191
+ goal_status = excluded.goal_status,
192
+ goal_turn_count = excluded.goal_turn_count,
193
+ awaiting_implementation_approval = excluded.awaiting_implementation_approval,
194
+ approved_scope = excluded.approved_scope""",
195
+ (
196
+ snapshot.message_id,
197
+ snapshot.session_id,
198
+ snapshot.interaction_mode.value,
199
+ snapshot.task_intent.value,
200
+ 1 if snapshot.implementation_allowed else 0,
201
+ snapshot.intent_resolution_reason,
202
+ snapshot.goal,
203
+ snapshot.goal_phase,
204
+ snapshot.goal_status,
205
+ snapshot.goal_turn_count,
206
+ 1 if snapshot.awaiting_implementation_approval else 0,
207
+ snapshot.approved_scope,
208
+ snapshot.created_at,
209
+ ),
210
+ )
211
+
212
+
213
+ async def load_message_runtime_snapshot(message_id: int) -> MessageRuntimeSnapshot | None:
214
+ row = await _fetch_one(
215
+ "SELECT * FROM message_runtime_snapshots WHERE message_id = ?",
216
+ (message_id,),
217
+ )
218
+ if not row:
219
+ return None
220
+ return MessageRuntimeSnapshot(
221
+ message_id=row["message_id"],
222
+ session_id=row["session_id"],
223
+ interaction_mode=InteractionMode(row["interaction_mode"]),
224
+ task_intent=TaskIntent(row["task_intent"]),
225
+ implementation_allowed=bool(row["implementation_allowed"]),
226
+ intent_resolution_reason=row["intent_resolution_reason"],
227
+ goal=row["goal"],
228
+ goal_phase=row["goal_phase"],
229
+ goal_status=row["goal_status"],
230
+ goal_turn_count=row["goal_turn_count"],
231
+ awaiting_implementation_approval=bool(row["awaiting_implementation_approval"]),
232
+ approved_scope=row["approved_scope"],
233
+ created_at=row["created_at"],
234
+ )
235
+
236
+
237
+ async def clear_runtime_state(session_id: str) -> None:
238
+ await _execute_commit("DELETE FROM session_runtime_state WHERE session_id = ?", (session_id,))
239
+ await _execute_commit("DELETE FROM session_task_runs WHERE session_id = ?", (session_id,))
240
+ await _execute_commit("DELETE FROM message_runtime_snapshots WHERE session_id = ?", (session_id,))