flowent 0.3.0 → 0.3.2
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.
- package/backend/pyproject.toml +1 -1
- package/backend/src/flowent/agent.py +22 -15
- package/backend/src/flowent/api_models.py +13 -8
- package/backend/src/flowent/llm.py +50 -6
- package/backend/src/flowent/mcp.py +4 -3
- package/backend/src/flowent/permissions.py +51 -38
- package/backend/src/flowent/routes/providers.py +33 -10
- package/backend/src/flowent/routes/system.py +5 -6
- package/backend/src/flowent/routes/workspace.py +33 -23
- package/backend/src/flowent/state/models.py +4 -4
- package/backend/src/flowent/state/schema.py +121 -0
- package/backend/src/flowent/state/store.py +9 -3
- package/backend/src/flowent/static/assets/index-BX18a4Jz.js +100 -0
- package/backend/src/flowent/static/assets/index-EC37agAH.css +2 -0
- package/backend/src/flowent/static/index.html +2 -2
- package/backend/src/flowent/tools.py +84 -33
- package/backend/src/flowent/usage.py +66 -0
- package/backend/src/flowent/workspace/context.py +140 -47
- package/backend/src/flowent/workspace/events.py +5 -7
- package/backend/src/flowent/workspace/output.py +129 -4
- package/backend/src/flowent/workspace/runtime.py +393 -185
- package/backend/uv.lock +1 -1
- package/dist/frontend/assets/index-BX18a4Jz.js +100 -0
- package/dist/frontend/assets/index-EC37agAH.css +2 -0
- package/dist/frontend/index.html +2 -2
- package/package.json +8 -10
- package/backend/src/flowent/static/assets/index-CvWZZMtK.css +0 -2
- package/backend/src/flowent/static/assets/index-ma2v8oW7.js +0 -90
- package/dist/frontend/assets/index-CvWZZMtK.css +0 -2
- package/dist/frontend/assets/index-ma2v8oW7.js +0 -90
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import sqlite3
|
|
2
3
|
|
|
3
4
|
|
|
@@ -83,6 +84,7 @@ def migrate(connection: sqlite3.Connection) -> None:
|
|
|
83
84
|
id TEXT PRIMARY KEY,
|
|
84
85
|
author TEXT NOT NULL,
|
|
85
86
|
content TEXT NOT NULL,
|
|
87
|
+
summary TEXT NOT NULL DEFAULT '',
|
|
86
88
|
status TEXT NOT NULL DEFAULT 'completed',
|
|
87
89
|
usage_info TEXT,
|
|
88
90
|
position INTEGER NOT NULL
|
|
@@ -154,6 +156,10 @@ def migrate(connection: sqlite3.Connection) -> None:
|
|
|
154
156
|
)
|
|
155
157
|
if "usage_info" not in message_columns:
|
|
156
158
|
connection.execute("ALTER TABLE messages ADD COLUMN usage_info TEXT")
|
|
159
|
+
if "summary" not in message_columns:
|
|
160
|
+
connection.execute(
|
|
161
|
+
"ALTER TABLE messages ADD COLUMN summary TEXT NOT NULL DEFAULT ''"
|
|
162
|
+
)
|
|
157
163
|
settings_columns = table_columns(connection, "settings")
|
|
158
164
|
if "reasoning_effort" not in settings_columns:
|
|
159
165
|
connection.execute(
|
|
@@ -180,7 +186,122 @@ def migrate(connection: sqlite3.Connection) -> None:
|
|
|
180
186
|
"ALTER TABLE workspace_context "
|
|
181
187
|
"ADD COLUMN is_compacting INTEGER NOT NULL DEFAULT 0"
|
|
182
188
|
)
|
|
189
|
+
if not migration_version_exists(connection, 2):
|
|
190
|
+
migrate_tool_result_items(connection)
|
|
191
|
+
connection.execute("INSERT INTO schema_migrations (version) VALUES (2)")
|
|
183
192
|
|
|
184
193
|
|
|
185
194
|
def table_columns(connection: sqlite3.Connection, table: str) -> set[str]:
|
|
186
195
|
return {row["name"] for row in connection.execute(f"PRAGMA table_info({table})")}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def migration_version_exists(connection: sqlite3.Connection, version: int) -> bool:
|
|
199
|
+
return (
|
|
200
|
+
connection.execute(
|
|
201
|
+
"SELECT 1 FROM schema_migrations WHERE version = ?",
|
|
202
|
+
(version,),
|
|
203
|
+
).fetchone()
|
|
204
|
+
is not None
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def migrate_tool_result_items(connection: sqlite3.Connection) -> None:
|
|
209
|
+
for row in connection.execute("SELECT id, tools, groups FROM messages"):
|
|
210
|
+
tools, tools_changed = migrate_tool_list(json.loads(row["tools"] or "[]"))
|
|
211
|
+
groups, groups_changed = migrate_tool_groups(json.loads(row["groups"] or "[]"))
|
|
212
|
+
if not tools_changed and not groups_changed:
|
|
213
|
+
continue
|
|
214
|
+
connection.execute(
|
|
215
|
+
"UPDATE messages SET tools = ?, groups = ? WHERE id = ?",
|
|
216
|
+
(
|
|
217
|
+
json.dumps(tools, ensure_ascii=False),
|
|
218
|
+
json.dumps(groups, ensure_ascii=False),
|
|
219
|
+
row["id"],
|
|
220
|
+
),
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def migrate_tool_groups(groups: object) -> tuple[object, bool]:
|
|
225
|
+
if not isinstance(groups, list):
|
|
226
|
+
return groups, False
|
|
227
|
+
changed = False
|
|
228
|
+
next_groups: list[object] = []
|
|
229
|
+
for group in groups:
|
|
230
|
+
if not isinstance(group, dict):
|
|
231
|
+
next_groups.append(group)
|
|
232
|
+
continue
|
|
233
|
+
items = group.get("items")
|
|
234
|
+
if not isinstance(items, list):
|
|
235
|
+
next_groups.append(group)
|
|
236
|
+
continue
|
|
237
|
+
next_items: list[object] = []
|
|
238
|
+
for item in items:
|
|
239
|
+
if not isinstance(item, dict) or item.get("type") != "tool":
|
|
240
|
+
next_items.append(item)
|
|
241
|
+
continue
|
|
242
|
+
tool, tool_changed = migrate_tool_item(item.get("tool"))
|
|
243
|
+
changed = changed or tool_changed
|
|
244
|
+
next_items.append({**item, "tool": tool})
|
|
245
|
+
next_groups.append({**group, "items": next_items})
|
|
246
|
+
return next_groups, changed
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def migrate_tool_list(tools: object) -> tuple[object, bool]:
|
|
250
|
+
if not isinstance(tools, list):
|
|
251
|
+
return tools, False
|
|
252
|
+
changed = False
|
|
253
|
+
next_tools: list[object] = []
|
|
254
|
+
for tool in tools:
|
|
255
|
+
next_tool, tool_changed = migrate_tool_item(tool)
|
|
256
|
+
changed = changed or tool_changed
|
|
257
|
+
next_tools.append(next_tool)
|
|
258
|
+
return next_tools, changed
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def migrate_tool_item(tool: object) -> tuple[object, bool]:
|
|
262
|
+
if not isinstance(tool, dict):
|
|
263
|
+
return tool, False
|
|
264
|
+
if "content" not in tool and "data" not in tool:
|
|
265
|
+
return tool, False
|
|
266
|
+
legacy_content = tool.get("content")
|
|
267
|
+
legacy_data = tool.get("data")
|
|
268
|
+
result = tool.get("result")
|
|
269
|
+
if not isinstance(result, dict):
|
|
270
|
+
result = legacy_tool_result(legacy_content, legacy_data)
|
|
271
|
+
return (
|
|
272
|
+
{
|
|
273
|
+
key: value
|
|
274
|
+
for key, value in {**tool, "result": result}.items()
|
|
275
|
+
if key not in {"content", "data"}
|
|
276
|
+
},
|
|
277
|
+
True,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def legacy_tool_result(content: object, data: object) -> dict[str, object]:
|
|
282
|
+
text = content if isinstance(content, str) else ""
|
|
283
|
+
payload = data if isinstance(data, dict) else {}
|
|
284
|
+
if {"command", "exit_code", "stderr", "stdout"}.issubset(payload):
|
|
285
|
+
return {
|
|
286
|
+
"type": "command",
|
|
287
|
+
"command": str(payload.get("command") or ""),
|
|
288
|
+
"exit_code": payload.get("exit_code"),
|
|
289
|
+
"stderr": str(payload.get("stderr") or ""),
|
|
290
|
+
"stdout": str(payload.get("stdout") or ""),
|
|
291
|
+
"output": text or str(payload.get("stdout") or payload.get("stderr") or ""),
|
|
292
|
+
}
|
|
293
|
+
if "server" in payload and "tool" in payload and "result" in payload:
|
|
294
|
+
return {
|
|
295
|
+
"type": "mcp",
|
|
296
|
+
"output": text,
|
|
297
|
+
"server": payload.get("server"),
|
|
298
|
+
"tool": payload.get("tool"),
|
|
299
|
+
"raw_result": payload.get("result"),
|
|
300
|
+
}
|
|
301
|
+
if "items" in payload:
|
|
302
|
+
return {"type": "plan", "output": text, **payload}
|
|
303
|
+
if "results" in payload and "query" in payload:
|
|
304
|
+
return {"type": "web_search", "output": text, **payload}
|
|
305
|
+
if "files" in payload:
|
|
306
|
+
return {"type": "patch", "output": text, **payload}
|
|
307
|
+
return {"type": "text", "text": text, **payload}
|
|
@@ -79,6 +79,7 @@ class StateStore:
|
|
|
79
79
|
],
|
|
80
80
|
id=row["id"],
|
|
81
81
|
status=row["status"],
|
|
82
|
+
summary=row["summary"],
|
|
82
83
|
thinking=row["thinking"],
|
|
83
84
|
tools=[
|
|
84
85
|
StoredToolItem.model_validate(tool)
|
|
@@ -90,7 +91,7 @@ class StateStore:
|
|
|
90
91
|
)
|
|
91
92
|
for row in connection.execute(
|
|
92
93
|
"""
|
|
93
|
-
SELECT id, author, content, tools, thinking, groups, status, usage_info
|
|
94
|
+
SELECT id, author, content, summary, tools, thinking, groups, status, usage_info
|
|
94
95
|
FROM messages
|
|
95
96
|
ORDER BY position, id
|
|
96
97
|
"""
|
|
@@ -576,6 +577,7 @@ class StateStore:
|
|
|
576
577
|
id,
|
|
577
578
|
author,
|
|
578
579
|
content,
|
|
580
|
+
summary,
|
|
579
581
|
tools,
|
|
580
582
|
thinking,
|
|
581
583
|
groups,
|
|
@@ -583,13 +585,14 @@ class StateStore:
|
|
|
583
585
|
usage_info,
|
|
584
586
|
position
|
|
585
587
|
)
|
|
586
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
588
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
587
589
|
""",
|
|
588
590
|
[
|
|
589
591
|
(
|
|
590
592
|
message.id,
|
|
591
593
|
message.author,
|
|
592
594
|
message.content,
|
|
595
|
+
message.summary,
|
|
593
596
|
json.dumps(
|
|
594
597
|
[
|
|
595
598
|
tool.model_dump(exclude_none=True)
|
|
@@ -635,6 +638,7 @@ class StateStore:
|
|
|
635
638
|
id,
|
|
636
639
|
author,
|
|
637
640
|
content,
|
|
641
|
+
summary,
|
|
638
642
|
tools,
|
|
639
643
|
thinking,
|
|
640
644
|
groups,
|
|
@@ -642,10 +646,11 @@ class StateStore:
|
|
|
642
646
|
usage_info,
|
|
643
647
|
position
|
|
644
648
|
)
|
|
645
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
649
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
646
650
|
ON CONFLICT(id) DO UPDATE SET
|
|
647
651
|
author = excluded.author,
|
|
648
652
|
content = excluded.content,
|
|
653
|
+
summary = excluded.summary,
|
|
649
654
|
tools = excluded.tools,
|
|
650
655
|
thinking = excluded.thinking,
|
|
651
656
|
groups = excluded.groups,
|
|
@@ -657,6 +662,7 @@ class StateStore:
|
|
|
657
662
|
message.id,
|
|
658
663
|
message.author,
|
|
659
664
|
message.content,
|
|
665
|
+
message.summary,
|
|
660
666
|
json.dumps(
|
|
661
667
|
[tool.model_dump(exclude_none=True) for tool in message.tools]
|
|
662
668
|
),
|