flowent 0.2.3 → 0.3.0

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 (49) hide show
  1. package/README.md +3 -3
  2. package/backend/README.md +3 -3
  3. package/backend/pyproject.toml +1 -1
  4. package/backend/src/flowent/agent.py +1 -1
  5. package/backend/src/flowent/api_models.py +103 -0
  6. package/backend/src/flowent/app.py +151 -0
  7. package/backend/src/flowent/cli.py +13 -4
  8. package/backend/src/flowent/compact.py +34 -13
  9. package/backend/src/flowent/llm.py +6 -8
  10. package/backend/src/flowent/logging.py +7 -1
  11. package/backend/src/flowent/main.py +18 -1989
  12. package/backend/src/flowent/mcp.py +231 -44
  13. package/backend/src/flowent/network.py +5 -0
  14. package/backend/src/flowent/permissions.py +5 -1
  15. package/backend/src/flowent/provider_connections.py +42 -0
  16. package/backend/src/flowent/routes/__init__.py +0 -0
  17. package/backend/src/flowent/routes/integrations.py +105 -0
  18. package/backend/src/flowent/routes/permissions.py +36 -0
  19. package/backend/src/flowent/routes/providers.py +30 -0
  20. package/backend/src/flowent/routes/system.py +49 -0
  21. package/backend/src/flowent/routes/workflow_routes.py +63 -0
  22. package/backend/src/flowent/routes/workspace.py +105 -0
  23. package/backend/src/flowent/sandbox.py +1 -1
  24. package/backend/src/flowent/state/__init__.py +53 -0
  25. package/backend/src/flowent/state/models.py +257 -0
  26. package/backend/src/flowent/state/schema.py +186 -0
  27. package/backend/src/flowent/state/store.py +1013 -0
  28. package/backend/src/flowent/static/assets/index-CvWZZMtK.css +2 -0
  29. package/backend/src/flowent/static/assets/index-ma2v8oW7.js +90 -0
  30. package/backend/src/flowent/static/index.html +2 -2
  31. package/backend/src/flowent/storage.py +52 -1254
  32. package/backend/src/flowent/system_tools.py +25 -0
  33. package/backend/src/flowent/tools.py +4 -2
  34. package/backend/src/flowent/usage.py +9 -4
  35. package/backend/src/flowent/workflows.py +282 -0
  36. package/backend/src/flowent/workspace/__init__.py +0 -0
  37. package/backend/src/flowent/workspace/context.py +249 -0
  38. package/backend/src/flowent/workspace/events.py +180 -0
  39. package/backend/src/flowent/workspace/output.py +274 -0
  40. package/backend/src/flowent/workspace/runtime.py +1041 -0
  41. package/backend/uv.lock +1 -1
  42. package/dist/frontend/assets/index-CvWZZMtK.css +2 -0
  43. package/dist/frontend/assets/index-ma2v8oW7.js +90 -0
  44. package/dist/frontend/index.html +2 -2
  45. package/package.json +1 -1
  46. package/backend/src/flowent/static/assets/index-D7t9qNrC.js +0 -82
  47. package/backend/src/flowent/static/assets/index-DufpDl8x.css +0 -2
  48. package/dist/frontend/assets/index-D7t9qNrC.js +0 -82
  49. package/dist/frontend/assets/index-DufpDl8x.css +0 -2
@@ -0,0 +1,105 @@
1
+ import logging
2
+
3
+ from fastapi import FastAPI, Query
4
+ from fastapi.responses import StreamingResponse
5
+
6
+ from flowent.api_models import (
7
+ WorkspaceClearResponse,
8
+ WorkspaceMessageEditRequest,
9
+ WorkspaceMessageEditResponse,
10
+ WorkspaceMessagesRequest,
11
+ WorkspaceRespondRequest,
12
+ WorkspaceRunResponse,
13
+ )
14
+ from flowent.logging import TRACE_LEVEL
15
+ from flowent.storage import StateStore
16
+ from flowent.workspace.runtime import WorkspaceRuntime
17
+
18
+ logger = logging.getLogger("flowent.routes.workspace")
19
+
20
+
21
+ def register_workspace_routes(
22
+ app: FastAPI,
23
+ *,
24
+ runtime: WorkspaceRuntime,
25
+ store: StateStore,
26
+ ) -> None:
27
+ @app.put("/api/workspace/messages")
28
+ async def save_workspace_messages(
29
+ request: WorkspaceMessagesRequest,
30
+ ) -> WorkspaceMessagesRequest:
31
+ return WorkspaceMessagesRequest(messages=store.save_messages(request.messages))
32
+
33
+ @app.post("/api/workspace/messages/{message_id}/edit")
34
+ async def edit_workspace_message(
35
+ message_id: str,
36
+ request: WorkspaceMessageEditRequest,
37
+ ) -> WorkspaceMessageEditResponse:
38
+ logger.info(
39
+ "Workspace message edit requested action=%s message_id=%s content_length=%s",
40
+ request.action,
41
+ message_id,
42
+ len(request.content),
43
+ )
44
+ logger.log(TRACE_LEVEL, "Workspace edited user content=%r", request.content)
45
+ messages, run = runtime.edit_message(
46
+ message_id,
47
+ action=request.action,
48
+ content=request.content,
49
+ )
50
+ return WorkspaceMessageEditResponse(
51
+ messages=messages,
52
+ run_id=run.id if run else None,
53
+ )
54
+
55
+ @app.post("/api/workspace/clear")
56
+ async def clear_workspace() -> WorkspaceClearResponse:
57
+ messages = runtime.clear()
58
+ await runtime.notify_cleared_runs()
59
+ return WorkspaceClearResponse(messages=messages)
60
+
61
+ @app.post("/api/workspace/runs")
62
+ async def start_workspace_run(
63
+ request: WorkspaceRespondRequest,
64
+ ) -> WorkspaceRunResponse:
65
+ logger.info("Workspace run requested content_length=%s", len(request.content))
66
+ logger.log(TRACE_LEVEL, "Workspace user content=%r", request.content)
67
+ run = runtime.create_run(request.content, message_id=request.message_id)
68
+ return WorkspaceRunResponse(run_id=run.id)
69
+
70
+ @app.get("/api/workspace/runs/{run_id}/stream")
71
+ async def stream_workspace_run(
72
+ run_id: str,
73
+ after: int = Query(default=0, ge=0),
74
+ ) -> StreamingResponse:
75
+ run = runtime.run_by_id(run_id)
76
+ return StreamingResponse(
77
+ runtime.run_stream(run, after),
78
+ media_type="text/event-stream",
79
+ )
80
+
81
+ @app.post("/api/workspace/runs/{run_id}/stop")
82
+ async def stop_workspace_run(run_id: str) -> dict[str, bool]:
83
+ runtime.stop_run(run_id)
84
+ return {"ok": True}
85
+
86
+ @app.post("/api/workspace/compact", response_class=StreamingResponse)
87
+ async def compact_workspace() -> StreamingResponse:
88
+ return StreamingResponse(
89
+ runtime.compact_stream(),
90
+ media_type="text/event-stream",
91
+ )
92
+
93
+ @app.post("/api/workspace/respond")
94
+ async def respond_to_workspace(
95
+ request: WorkspaceRespondRequest,
96
+ ) -> StreamingResponse:
97
+ logger.info(
98
+ "Workspace response requested content_length=%s", len(request.content)
99
+ )
100
+ logger.log(TRACE_LEVEL, "Workspace user content=%r", request.content)
101
+ run = runtime.create_run(request.content, message_id=request.message_id)
102
+ return StreamingResponse(
103
+ runtime.run_stream(run, include_snapshots=False),
104
+ media_type="text/event-stream",
105
+ )
@@ -262,7 +262,7 @@ class SandboxRunner:
262
262
  if root == self.cwd:
263
263
  continue
264
264
  if not root.exists():
265
- root.mkdir(mode=0o700, parents=True, exist_ok=True)
265
+ continue
266
266
  args.extend(["--bind", str(root), str(root)])
267
267
  args.extend(
268
268
  [
@@ -0,0 +1,53 @@
1
+ from flowent.state.models import (
2
+ StoredAssistantOutputGroup,
3
+ StoredCompactionCheckpoint,
4
+ StoredErrorOutputItem,
5
+ StoredMcpServer,
6
+ StoredMcpTool,
7
+ StoredMessage,
8
+ StoredOutputItem,
9
+ StoredProvider,
10
+ StoredSettings,
11
+ StoredSkill,
12
+ StoredState,
13
+ StoredTelegramBot,
14
+ StoredTelegramSession,
15
+ StoredTextOutputItem,
16
+ StoredThinkingOutputItem,
17
+ StoredToolItem,
18
+ StoredToolOutputItem,
19
+ StoredWorkflow,
20
+ StoredWorkflowDefinition,
21
+ StoredWorkflowEdge,
22
+ StoredWorkflowNode,
23
+ StoredWorkflowNodePosition,
24
+ StoredWritablePath,
25
+ )
26
+ from flowent.state.store import StateStore
27
+
28
+ __all__ = [
29
+ "StateStore",
30
+ "StoredAssistantOutputGroup",
31
+ "StoredCompactionCheckpoint",
32
+ "StoredErrorOutputItem",
33
+ "StoredMcpServer",
34
+ "StoredMcpTool",
35
+ "StoredMessage",
36
+ "StoredOutputItem",
37
+ "StoredProvider",
38
+ "StoredSettings",
39
+ "StoredSkill",
40
+ "StoredState",
41
+ "StoredTelegramBot",
42
+ "StoredTelegramSession",
43
+ "StoredTextOutputItem",
44
+ "StoredThinkingOutputItem",
45
+ "StoredToolItem",
46
+ "StoredToolOutputItem",
47
+ "StoredWorkflow",
48
+ "StoredWorkflowDefinition",
49
+ "StoredWorkflowEdge",
50
+ "StoredWorkflowNode",
51
+ "StoredWorkflowNodePosition",
52
+ "StoredWritablePath",
53
+ ]
@@ -0,0 +1,257 @@
1
+ from typing import Annotated, Literal
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field, PositiveInt
4
+
5
+ from flowent.llm import ChatMessage, ProviderFormat, ReasoningEffort
6
+ from flowent.usage import TokenUsageInfo
7
+
8
+
9
+ class StoredTelegramSession(BaseModel):
10
+ model_config = ConfigDict(extra="forbid")
11
+
12
+ chat_id: str
13
+ display_name: str = ""
14
+ recent_message: str = ""
15
+ status: str
16
+ updated_at: int = 0
17
+ user_id: str = ""
18
+ username: str = ""
19
+
20
+
21
+ class StoredTelegramBot(BaseModel):
22
+ model_config = ConfigDict(extra="forbid")
23
+
24
+ bot_token: str
25
+ enabled: bool
26
+ error: str = ""
27
+ sessions: list[StoredTelegramSession] = Field(default_factory=list)
28
+ status: str = "disabled"
29
+
30
+
31
+ class StoredMcpTool(BaseModel):
32
+ model_config = ConfigDict(extra="forbid")
33
+
34
+ description: str = ""
35
+ input_schema: dict[str, object] = Field(default_factory=dict)
36
+ name: str
37
+ output_schema: dict[str, object] | None = None
38
+
39
+
40
+ class StoredMcpServer(BaseModel):
41
+ model_config = ConfigDict(extra="forbid")
42
+
43
+ args: list[str] = Field(default_factory=list)
44
+ command: str = ""
45
+ config: dict[str, object] = Field(default_factory=dict)
46
+ enabled: bool = True
47
+ error: str = ""
48
+ id: str
49
+ name: str
50
+ status: str = "disabled"
51
+ tools: list[StoredMcpTool] = Field(default_factory=list)
52
+ type: str
53
+ url: str = ""
54
+
55
+
56
+ class StoredSkill(BaseModel):
57
+ model_config = ConfigDict(extra="forbid")
58
+
59
+ description: str
60
+ enabled: bool = True
61
+ error: str = ""
62
+ id: str
63
+ name: str
64
+ path: str
65
+ scope: str
66
+ slug: str
67
+
68
+
69
+ class StoredWritablePath(BaseModel):
70
+ model_config = ConfigDict(extra="forbid")
71
+
72
+ created_at: int = 0
73
+ path: str
74
+
75
+
76
+ class StoredWorkflowNodePosition(BaseModel):
77
+ model_config = ConfigDict(extra="forbid")
78
+
79
+ x: float = 0
80
+ y: float = 0
81
+
82
+
83
+ class StoredWorkflowNode(BaseModel):
84
+ model_config = ConfigDict(extra="forbid")
85
+
86
+ data: dict[str, object] = Field(default_factory=dict)
87
+ description: str = ""
88
+ id: str
89
+ name: str
90
+ position: StoredWorkflowNodePosition = Field(
91
+ default_factory=StoredWorkflowNodePosition
92
+ )
93
+ type: Literal["input", "agent", "merge", "output"]
94
+
95
+
96
+ class StoredWorkflowEdge(BaseModel):
97
+ model_config = ConfigDict(extra="forbid")
98
+
99
+ id: str
100
+ label: str = ""
101
+ source: str
102
+ source_handle: str = ""
103
+ target: str
104
+ target_handle: str = ""
105
+
106
+
107
+ class StoredWorkflowDefinition(BaseModel):
108
+ model_config = ConfigDict(extra="forbid")
109
+
110
+ edges: list[StoredWorkflowEdge] = Field(default_factory=list)
111
+ nodes: list[StoredWorkflowNode] = Field(default_factory=list)
112
+ version: int = 1
113
+
114
+
115
+ class StoredWorkflow(BaseModel):
116
+ model_config = ConfigDict(extra="forbid")
117
+
118
+ created_at: int = 0
119
+ definition: StoredWorkflowDefinition
120
+ id: str
121
+ name: str
122
+ updated_at: int = 0
123
+
124
+
125
+ class StoredProvider(BaseModel):
126
+ model_config = ConfigDict(extra="forbid")
127
+
128
+ api_key: str
129
+ base_url: str
130
+ id: str
131
+ models: list[str]
132
+ name: str
133
+ type: ProviderFormat
134
+
135
+
136
+ class StoredSettings(BaseModel):
137
+ model_config = ConfigDict(extra="forbid")
138
+
139
+ agent_prompt: str = Field(default="", exclude_if=lambda value: value == "")
140
+ context_window_limit: PositiveInt | None = None
141
+ reasoning_effort: ReasoningEffort = ReasoningEffort.DEFAULT
142
+ selected_model: str
143
+ selected_provider_id: str
144
+
145
+
146
+ class StoredToolItem(BaseModel):
147
+ model_config = ConfigDict(extra="forbid")
148
+
149
+ id: str
150
+ name: str
151
+ status: str
152
+ title: str
153
+ arguments: dict[str, object] | None = None
154
+ content: str | None = None
155
+ data: dict[str, object] | None = None
156
+
157
+
158
+ class StoredThinkingOutputItem(BaseModel):
159
+ model_config = ConfigDict(extra="forbid")
160
+
161
+ content: str
162
+ id: str
163
+ type: Literal["thinking"]
164
+
165
+
166
+ class StoredTextOutputItem(BaseModel):
167
+ model_config = ConfigDict(extra="forbid")
168
+
169
+ content: str
170
+ id: str
171
+ type: Literal["text"]
172
+
173
+
174
+ class StoredErrorOutputItem(BaseModel):
175
+ model_config = ConfigDict(extra="forbid")
176
+
177
+ detail: str = Field(default="", exclude_if=lambda value: value == "")
178
+ id: str
179
+ message: str
180
+ title: str
181
+ type: Literal["error"]
182
+
183
+
184
+ class StoredToolOutputItem(BaseModel):
185
+ model_config = ConfigDict(extra="forbid")
186
+
187
+ id: str
188
+ tool: StoredToolItem
189
+ type: Literal["tool"]
190
+
191
+
192
+ StoredOutputItem = Annotated[
193
+ StoredThinkingOutputItem
194
+ | StoredTextOutputItem
195
+ | StoredErrorOutputItem
196
+ | StoredToolOutputItem,
197
+ Field(discriminator="type"),
198
+ ]
199
+
200
+
201
+ class StoredAssistantOutputGroup(BaseModel):
202
+ model_config = ConfigDict(extra="forbid")
203
+
204
+ id: str
205
+ items: list[StoredOutputItem]
206
+
207
+
208
+ class StoredMessage(BaseModel):
209
+ model_config = ConfigDict(extra="forbid")
210
+
211
+ author: str
212
+ content: str
213
+ groups: list[StoredAssistantOutputGroup] = Field(
214
+ default_factory=list, exclude_if=lambda value: value == []
215
+ )
216
+ id: str
217
+ status: str = Field(
218
+ default="completed", exclude_if=lambda value: value == "completed"
219
+ )
220
+ thinking: str = Field(default="", exclude_if=lambda value: value == "")
221
+ tools: list[StoredToolItem] = Field(default_factory=list)
222
+ usage_info: TokenUsageInfo | None = Field(
223
+ default=None, exclude_if=lambda value: value is None
224
+ )
225
+
226
+
227
+ class StoredCompactionCheckpoint(BaseModel):
228
+ model_config = ConfigDict(extra="forbid")
229
+
230
+ created_at: int = 0
231
+ id: str
232
+ method: str
233
+ replacement_history: list[ChatMessage]
234
+ source_message_id: str | None = None
235
+ summary: str
236
+ token_after: int = 0
237
+ token_before: int = 0
238
+ trigger: str
239
+
240
+
241
+ class StoredState(BaseModel):
242
+ model_config = ConfigDict(extra="forbid")
243
+
244
+ active_run_event_index: int = 0
245
+ active_run_id: str | None = None
246
+ is_compacting: bool = False
247
+ mcp_servers: list[StoredMcpServer]
248
+ messages: list[StoredMessage]
249
+ providers: list[StoredProvider]
250
+ settings: StoredSettings
251
+ skills: list[StoredSkill]
252
+ telegram_bot: StoredTelegramBot
253
+ usage_info: TokenUsageInfo | None = Field(
254
+ default=None, exclude_if=lambda value: value is None
255
+ )
256
+ writable_paths: list[StoredWritablePath] = Field(default_factory=list)
257
+ workflows: list[StoredWorkflow] = Field(default_factory=list)
@@ -0,0 +1,186 @@
1
+ import sqlite3
2
+
3
+
4
+ def migrate(connection: sqlite3.Connection) -> None:
5
+ connection.executescript(
6
+ """
7
+ CREATE TABLE IF NOT EXISTS workflows (
8
+ id TEXT PRIMARY KEY,
9
+ name TEXT NOT NULL,
10
+ definition TEXT NOT NULL,
11
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
12
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
13
+ );
14
+
15
+ CREATE TABLE IF NOT EXISTS mcp_servers (
16
+ id TEXT PRIMARY KEY,
17
+ name TEXT NOT NULL,
18
+ type TEXT NOT NULL,
19
+ command TEXT NOT NULL DEFAULT '',
20
+ args TEXT NOT NULL DEFAULT '[]',
21
+ config TEXT NOT NULL DEFAULT '{}',
22
+ url TEXT NOT NULL DEFAULT '',
23
+ enabled INTEGER NOT NULL DEFAULT 1,
24
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
25
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
26
+ );
27
+
28
+ CREATE TABLE IF NOT EXISTS mcp_tools (
29
+ server_id TEXT NOT NULL REFERENCES mcp_servers(id) ON DELETE CASCADE,
30
+ name TEXT NOT NULL,
31
+ description TEXT NOT NULL DEFAULT '',
32
+ input_schema TEXT NOT NULL DEFAULT '{}',
33
+ output_schema TEXT,
34
+ position INTEGER NOT NULL,
35
+ PRIMARY KEY (server_id, name)
36
+ );
37
+
38
+ CREATE TABLE IF NOT EXISTS telegram_bot (
39
+ id INTEGER PRIMARY KEY CHECK (id = 1),
40
+ enabled INTEGER NOT NULL DEFAULT 0,
41
+ bot_token TEXT NOT NULL,
42
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
43
+ );
44
+
45
+ CREATE TABLE IF NOT EXISTS telegram_sessions (
46
+ chat_id TEXT PRIMARY KEY,
47
+ user_id TEXT NOT NULL DEFAULT '',
48
+ username TEXT NOT NULL DEFAULT '',
49
+ display_name TEXT NOT NULL DEFAULT '',
50
+ recent_message TEXT NOT NULL DEFAULT '',
51
+ status TEXT NOT NULL,
52
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
53
+ );
54
+
55
+ CREATE TABLE IF NOT EXISTS providers (
56
+ id TEXT PRIMARY KEY,
57
+ name TEXT NOT NULL,
58
+ type TEXT NOT NULL,
59
+ base_url TEXT NOT NULL,
60
+ api_key TEXT NOT NULL,
61
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
62
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
63
+ );
64
+
65
+ CREATE TABLE IF NOT EXISTS provider_models (
66
+ provider_id TEXT NOT NULL REFERENCES providers(id) ON DELETE CASCADE,
67
+ model TEXT NOT NULL,
68
+ position INTEGER NOT NULL,
69
+ PRIMARY KEY (provider_id, model)
70
+ );
71
+
72
+ CREATE TABLE IF NOT EXISTS settings (
73
+ id INTEGER PRIMARY KEY CHECK (id = 1),
74
+ selected_provider_id TEXT NOT NULL DEFAULT '',
75
+ selected_model TEXT NOT NULL DEFAULT '',
76
+ reasoning_effort TEXT NOT NULL DEFAULT 'default',
77
+ agent_prompt TEXT NOT NULL DEFAULT '',
78
+ context_window_limit INTEGER,
79
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
80
+ );
81
+
82
+ CREATE TABLE IF NOT EXISTS messages (
83
+ id TEXT PRIMARY KEY,
84
+ author TEXT NOT NULL,
85
+ content TEXT NOT NULL,
86
+ status TEXT NOT NULL DEFAULT 'completed',
87
+ usage_info TEXT,
88
+ position INTEGER NOT NULL
89
+ );
90
+
91
+ CREATE TABLE IF NOT EXISTS workspace_context (
92
+ id INTEGER PRIMARY KEY CHECK (id = 1),
93
+ compacted_summary TEXT NOT NULL DEFAULT '',
94
+ active_compaction_id TEXT,
95
+ is_compacting INTEGER NOT NULL DEFAULT 0,
96
+ usage_info TEXT,
97
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
98
+ );
99
+
100
+ CREATE TABLE IF NOT EXISTS compaction_checkpoints (
101
+ id TEXT PRIMARY KEY,
102
+ trigger TEXT NOT NULL,
103
+ method TEXT NOT NULL,
104
+ summary TEXT NOT NULL,
105
+ replacement_history TEXT NOT NULL DEFAULT '[]',
106
+ source_message_id TEXT,
107
+ token_before INTEGER NOT NULL DEFAULT 0,
108
+ token_after INTEGER NOT NULL DEFAULT 0,
109
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
110
+ );
111
+
112
+ CREATE TABLE IF NOT EXISTS skill_settings (
113
+ id TEXT PRIMARY KEY,
114
+ enabled INTEGER NOT NULL DEFAULT 1,
115
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
116
+ );
117
+
118
+ CREATE TABLE IF NOT EXISTS writable_paths (
119
+ path TEXT PRIMARY KEY,
120
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
121
+ );
122
+
123
+ CREATE TABLE IF NOT EXISTS schema_migrations (
124
+ version INTEGER PRIMARY KEY
125
+ );
126
+
127
+ INSERT OR IGNORE INTO schema_migrations (version) VALUES (1);
128
+ """
129
+ )
130
+ mcp_server_columns = table_columns(connection, "mcp_servers")
131
+ if "config" not in mcp_server_columns:
132
+ connection.execute(
133
+ """
134
+ ALTER TABLE mcp_servers
135
+ ADD COLUMN config TEXT NOT NULL DEFAULT '{}'
136
+ """
137
+ )
138
+ message_columns = table_columns(connection, "messages")
139
+ if "tools" not in message_columns:
140
+ connection.execute(
141
+ "ALTER TABLE messages ADD COLUMN tools TEXT NOT NULL DEFAULT '[]'"
142
+ )
143
+ if "thinking" not in message_columns:
144
+ connection.execute(
145
+ "ALTER TABLE messages ADD COLUMN thinking TEXT NOT NULL DEFAULT ''"
146
+ )
147
+ if "status" not in message_columns:
148
+ connection.execute(
149
+ "ALTER TABLE messages ADD COLUMN status TEXT NOT NULL DEFAULT 'completed'"
150
+ )
151
+ if "groups" not in message_columns:
152
+ connection.execute(
153
+ "ALTER TABLE messages ADD COLUMN groups TEXT NOT NULL DEFAULT '[]'"
154
+ )
155
+ if "usage_info" not in message_columns:
156
+ connection.execute("ALTER TABLE messages ADD COLUMN usage_info TEXT")
157
+ settings_columns = table_columns(connection, "settings")
158
+ if "reasoning_effort" not in settings_columns:
159
+ connection.execute(
160
+ "ALTER TABLE settings "
161
+ "ADD COLUMN reasoning_effort TEXT NOT NULL DEFAULT 'default'"
162
+ )
163
+ if "agent_prompt" not in settings_columns:
164
+ connection.execute(
165
+ "ALTER TABLE settings ADD COLUMN agent_prompt TEXT NOT NULL DEFAULT ''"
166
+ )
167
+ if "context_window_limit" not in settings_columns:
168
+ connection.execute(
169
+ "ALTER TABLE settings ADD COLUMN context_window_limit INTEGER"
170
+ )
171
+ workspace_context_columns = table_columns(connection, "workspace_context")
172
+ if "active_compaction_id" not in workspace_context_columns:
173
+ connection.execute(
174
+ "ALTER TABLE workspace_context ADD COLUMN active_compaction_id TEXT"
175
+ )
176
+ if "usage_info" not in workspace_context_columns:
177
+ connection.execute("ALTER TABLE workspace_context ADD COLUMN usage_info TEXT")
178
+ if "is_compacting" not in workspace_context_columns:
179
+ connection.execute(
180
+ "ALTER TABLE workspace_context "
181
+ "ADD COLUMN is_compacting INTEGER NOT NULL DEFAULT 0"
182
+ )
183
+
184
+
185
+ def table_columns(connection: sqlite3.Connection, table: str) -> set[str]:
186
+ return {row["name"] for row in connection.execute(f"PRAGMA table_info({table})")}