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.
Files changed (30) hide show
  1. package/backend/pyproject.toml +1 -1
  2. package/backend/src/flowent/agent.py +22 -15
  3. package/backend/src/flowent/api_models.py +13 -8
  4. package/backend/src/flowent/llm.py +50 -6
  5. package/backend/src/flowent/mcp.py +4 -3
  6. package/backend/src/flowent/permissions.py +51 -38
  7. package/backend/src/flowent/routes/providers.py +33 -10
  8. package/backend/src/flowent/routes/system.py +5 -6
  9. package/backend/src/flowent/routes/workspace.py +33 -23
  10. package/backend/src/flowent/state/models.py +4 -4
  11. package/backend/src/flowent/state/schema.py +121 -0
  12. package/backend/src/flowent/state/store.py +9 -3
  13. package/backend/src/flowent/static/assets/index-BX18a4Jz.js +100 -0
  14. package/backend/src/flowent/static/assets/index-EC37agAH.css +2 -0
  15. package/backend/src/flowent/static/index.html +2 -2
  16. package/backend/src/flowent/tools.py +84 -33
  17. package/backend/src/flowent/usage.py +66 -0
  18. package/backend/src/flowent/workspace/context.py +140 -47
  19. package/backend/src/flowent/workspace/events.py +5 -7
  20. package/backend/src/flowent/workspace/output.py +129 -4
  21. package/backend/src/flowent/workspace/runtime.py +393 -185
  22. package/backend/uv.lock +1 -1
  23. package/dist/frontend/assets/index-BX18a4Jz.js +100 -0
  24. package/dist/frontend/assets/index-EC37agAH.css +2 -0
  25. package/dist/frontend/index.html +2 -2
  26. package/package.json +8 -10
  27. package/backend/src/flowent/static/assets/index-CvWZZMtK.css +0 -2
  28. package/backend/src/flowent/static/assets/index-ma2v8oW7.js +0 -90
  29. package/dist/frontend/assets/index-CvWZZMtK.css +0 -2
  30. 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
  ),