flowent 0.1.4 → 0.1.5

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 (67) hide show
  1. package/backend/pyproject.toml +1 -1
  2. package/backend/src/flowent/__pycache__/__init__.cpython-313.pyc +0 -0
  3. package/backend/src/flowent/__pycache__/_version.cpython-313.pyc +0 -0
  4. package/backend/src/flowent/__pycache__/agent.cpython-313.pyc +0 -0
  5. package/backend/src/flowent/__pycache__/approval.cpython-313.pyc +0 -0
  6. package/backend/src/flowent/__pycache__/channels.cpython-313.pyc +0 -0
  7. package/backend/src/flowent/__pycache__/cli.cpython-313.pyc +0 -0
  8. package/backend/src/flowent/__pycache__/compact.cpython-313.pyc +0 -0
  9. package/backend/src/flowent/__pycache__/context.cpython-313.pyc +0 -0
  10. package/backend/src/flowent/__pycache__/llm.cpython-313.pyc +0 -0
  11. package/backend/src/flowent/__pycache__/logging.cpython-313.pyc +0 -0
  12. package/backend/src/flowent/__pycache__/main.cpython-313.pyc +0 -0
  13. package/backend/src/flowent/__pycache__/mcp.cpython-313.pyc +0 -0
  14. package/backend/src/flowent/__pycache__/mcp_import.cpython-313.pyc +0 -0
  15. package/backend/src/flowent/__pycache__/patch.cpython-313.pyc +0 -0
  16. package/backend/src/flowent/__pycache__/paths.cpython-313.pyc +0 -0
  17. package/backend/src/flowent/__pycache__/permissions.cpython-313.pyc +0 -0
  18. package/backend/src/flowent/__pycache__/sandbox.cpython-313.pyc +0 -0
  19. package/backend/src/flowent/__pycache__/skills.cpython-313.pyc +0 -0
  20. package/backend/src/flowent/__pycache__/storage.cpython-313.pyc +0 -0
  21. package/backend/src/flowent/__pycache__/tools.cpython-313.pyc +0 -0
  22. package/backend/src/flowent/agent.py +23 -1
  23. package/backend/src/flowent/approval.py +148 -0
  24. package/backend/src/flowent/cli.py +4 -2
  25. package/backend/src/flowent/context.py +19 -1
  26. package/backend/src/flowent/llm.py +51 -11
  27. package/backend/src/flowent/logging.py +60 -0
  28. package/backend/src/flowent/main.py +639 -210
  29. package/backend/src/flowent/patch.py +55 -31
  30. package/backend/src/flowent/permissions.py +185 -42
  31. package/backend/src/flowent/sandbox.py +55 -1
  32. package/backend/src/flowent/static/assets/index-Cl20cARb.css +2 -0
  33. package/backend/src/flowent/static/assets/index-dsDDsEym.js +81 -0
  34. package/backend/src/flowent/static/index.html +2 -2
  35. package/backend/src/flowent/storage.py +113 -18
  36. package/backend/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  37. package/backend/tests/__pycache__/test_agent_tools.cpython-313-pytest-9.0.3.pyc +0 -0
  38. package/backend/tests/__pycache__/test_approval.cpython-313-pytest-9.0.3.pyc +0 -0
  39. package/backend/tests/__pycache__/test_channels.cpython-313-pytest-9.0.3.pyc +0 -0
  40. package/backend/tests/__pycache__/test_health.cpython-313-pytest-9.0.3.pyc +0 -0
  41. package/backend/tests/__pycache__/test_llm_providers.cpython-313-pytest-9.0.3.pyc +0 -0
  42. package/backend/tests/__pycache__/test_logging.cpython-313-pytest-9.0.3.pyc +0 -0
  43. package/backend/tests/__pycache__/test_mcp.cpython-313-pytest-9.0.3.pyc +0 -0
  44. package/backend/tests/__pycache__/test_patch.cpython-313-pytest-9.0.3.pyc +0 -0
  45. package/backend/tests/__pycache__/test_permissions.cpython-313-pytest-9.0.3.pyc +0 -0
  46. package/backend/tests/__pycache__/test_persistence.cpython-313-pytest-9.0.3.pyc +0 -0
  47. package/backend/tests/__pycache__/test_skills.cpython-313-pytest-9.0.3.pyc +0 -0
  48. package/backend/tests/__pycache__/test_startup_requirements.cpython-313-pytest-9.0.3.pyc +0 -0
  49. package/backend/tests/__pycache__/test_workspace_chat.cpython-313-pytest-9.0.3.pyc +0 -0
  50. package/backend/tests/test_agent_tools.py +77 -1
  51. package/backend/tests/test_approval.py +283 -0
  52. package/backend/tests/test_llm_providers.py +216 -0
  53. package/backend/tests/test_logging.py +30 -0
  54. package/backend/tests/test_patch.py +112 -0
  55. package/backend/tests/test_permissions.py +198 -53
  56. package/backend/tests/test_persistence.py +78 -0
  57. package/backend/tests/test_startup_requirements.py +54 -0
  58. package/backend/tests/test_workspace_chat.py +855 -41
  59. package/backend/uv.lock +1 -1
  60. package/dist/frontend/assets/index-Cl20cARb.css +2 -0
  61. package/dist/frontend/assets/index-dsDDsEym.js +81 -0
  62. package/dist/frontend/index.html +2 -2
  63. package/package.json +1 -1
  64. package/backend/src/flowent/static/assets/index-BREidonU.css +0 -2
  65. package/backend/src/flowent/static/assets/index-DSniOrhL.js +0 -81
  66. package/dist/frontend/assets/index-BREidonU.css +0 -2
  67. package/dist/frontend/assets/index-DSniOrhL.js +0 -81
@@ -6,8 +6,8 @@
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>Flowent</title>
8
8
  <meta name="description" content="Flowent application" />
9
- <script type="module" crossorigin src="/assets/index-DSniOrhL.js"></script>
10
- <link rel="stylesheet" crossorigin href="/assets/index-BREidonU.css">
9
+ <script type="module" crossorigin src="/assets/index-dsDDsEym.js"></script>
10
+ <link rel="stylesheet" crossorigin href="/assets/index-Cl20cARb.css">
11
11
  </head>
12
12
  <body>
13
13
  <div id="root"></div>
@@ -1,6 +1,7 @@
1
1
  import json
2
2
  import sqlite3
3
3
  from pathlib import Path
4
+ from typing import Annotated, Literal
4
5
 
5
6
  from pydantic import BaseModel, ConfigDict, Field
6
7
 
@@ -75,15 +76,6 @@ class StoredWritablePath(BaseModel):
75
76
  path: str
76
77
 
77
78
 
78
- class StoredPermissionRequest(BaseModel):
79
- model_config = ConfigDict(extra="forbid")
80
-
81
- id: str
82
- path: str
83
- reason: str
84
- tool_call_id: str | None = None
85
-
86
-
87
79
  class StoredProvider(BaseModel):
88
80
  model_config = ConfigDict(extra="forbid")
89
81
 
@@ -98,6 +90,7 @@ class StoredProvider(BaseModel):
98
90
  class StoredSettings(BaseModel):
99
91
  model_config = ConfigDict(extra="forbid")
100
92
 
93
+ agent_prompt: str = Field(default="", exclude_if=lambda value: value == "")
101
94
  reasoning_effort: ReasoningEffort = ReasoningEffort.DEFAULT
102
95
  selected_model: str
103
96
  selected_provider_id: str
@@ -115,11 +108,64 @@ class StoredToolItem(BaseModel):
115
108
  data: dict[str, object] | None = None
116
109
 
117
110
 
111
+ class StoredThinkingOutputItem(BaseModel):
112
+ model_config = ConfigDict(extra="forbid")
113
+
114
+ content: str
115
+ id: str
116
+ type: Literal["thinking"]
117
+
118
+
119
+ class StoredTextOutputItem(BaseModel):
120
+ model_config = ConfigDict(extra="forbid")
121
+
122
+ content: str
123
+ id: str
124
+ type: Literal["text"]
125
+
126
+
127
+ class StoredErrorOutputItem(BaseModel):
128
+ model_config = ConfigDict(extra="forbid")
129
+
130
+ detail: str = Field(default="", exclude_if=lambda value: value == "")
131
+ id: str
132
+ message: str
133
+ title: str
134
+ type: Literal["error"]
135
+
136
+
137
+ class StoredToolOutputItem(BaseModel):
138
+ model_config = ConfigDict(extra="forbid")
139
+
140
+ id: str
141
+ tool: StoredToolItem
142
+ type: Literal["tool"]
143
+
144
+
145
+ StoredOutputItem = Annotated[
146
+ StoredThinkingOutputItem
147
+ | StoredTextOutputItem
148
+ | StoredErrorOutputItem
149
+ | StoredToolOutputItem,
150
+ Field(discriminator="type"),
151
+ ]
152
+
153
+
154
+ class StoredAssistantOutputGroup(BaseModel):
155
+ model_config = ConfigDict(extra="forbid")
156
+
157
+ id: str
158
+ items: list[StoredOutputItem]
159
+
160
+
118
161
  class StoredMessage(BaseModel):
119
162
  model_config = ConfigDict(extra="forbid")
120
163
 
121
164
  author: str
122
165
  content: str
166
+ groups: list[StoredAssistantOutputGroup] = Field(
167
+ default_factory=list, exclude_if=lambda value: value == []
168
+ )
123
169
  id: str
124
170
  status: str = Field(
125
171
  default="completed", exclude_if=lambda value: value == "completed"
@@ -150,7 +196,6 @@ class StoredState(BaseModel):
150
196
  mcp_servers: list[StoredMcpServer]
151
197
  messages: list[StoredMessage]
152
198
  providers: list[StoredProvider]
153
- permission_requests: list[StoredPermissionRequest] = Field(default_factory=list)
154
199
  settings: StoredSettings
155
200
  skills: list[StoredSkill]
156
201
  telegram_bot: StoredTelegramBot
@@ -196,7 +241,7 @@ class StateStore:
196
241
  ]
197
242
  settings_row = connection.execute(
198
243
  """
199
- SELECT selected_provider_id, selected_model, reasoning_effort
244
+ SELECT selected_provider_id, selected_model, reasoning_effort, agent_prompt
200
245
  FROM settings
201
246
  WHERE id = 1
202
247
  """
@@ -205,6 +250,10 @@ class StateStore:
205
250
  StoredMessage(
206
251
  author=row["author"],
207
252
  content=row["content"],
253
+ groups=[
254
+ StoredAssistantOutputGroup.model_validate(group)
255
+ for group in json.loads(row["groups"] or "[]")
256
+ ],
208
257
  id=row["id"],
209
258
  status=row["status"],
210
259
  thinking=row["thinking"],
@@ -215,7 +264,7 @@ class StateStore:
215
264
  )
216
265
  for row in connection.execute(
217
266
  """
218
- SELECT id, author, content, tools, thinking, status
267
+ SELECT id, author, content, tools, thinking, groups, status
219
268
  FROM messages
220
269
  ORDER BY position, id
221
270
  """
@@ -227,6 +276,7 @@ class StateStore:
227
276
  messages=messages,
228
277
  providers=providers,
229
278
  settings=StoredSettings(
279
+ agent_prompt=settings_row["agent_prompt"] if settings_row else "",
230
280
  reasoning_effort=settings_row["reasoning_effort"]
231
281
  if settings_row
232
282
  else ReasoningEffort.DEFAULT,
@@ -527,19 +577,22 @@ class StateStore:
527
577
  id,
528
578
  selected_provider_id,
529
579
  selected_model,
530
- reasoning_effort
580
+ reasoning_effort,
581
+ agent_prompt
531
582
  )
532
- VALUES (1, ?, ?, ?)
583
+ VALUES (1, ?, ?, ?, ?)
533
584
  ON CONFLICT(id) DO UPDATE SET
534
585
  selected_provider_id = excluded.selected_provider_id,
535
586
  selected_model = excluded.selected_model,
536
587
  reasoning_effort = excluded.reasoning_effort,
588
+ agent_prompt = excluded.agent_prompt,
537
589
  updated_at = unixepoch()
538
590
  """,
539
591
  (
540
592
  settings.selected_provider_id,
541
593
  settings.selected_model,
542
594
  settings.reasoning_effort.value,
595
+ settings.agent_prompt,
543
596
  ),
544
597
  )
545
598
  return settings
@@ -549,8 +602,17 @@ class StateStore:
549
602
  connection.execute("DELETE FROM messages")
550
603
  connection.executemany(
551
604
  """
552
- INSERT INTO messages (id, author, content, tools, thinking, status, position)
553
- VALUES (?, ?, ?, ?, ?, ?, ?)
605
+ INSERT INTO messages (
606
+ id,
607
+ author,
608
+ content,
609
+ tools,
610
+ thinking,
611
+ groups,
612
+ status,
613
+ position
614
+ )
615
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
554
616
  """,
555
617
  [
556
618
  (
@@ -564,6 +626,13 @@ class StateStore:
564
626
  ]
565
627
  ),
566
628
  message.thinking,
629
+ json.dumps(
630
+ [
631
+ group.model_dump(exclude_none=True)
632
+ for group in message.groups
633
+ ],
634
+ ensure_ascii=False,
635
+ ),
567
636
  message.status,
568
637
  position,
569
638
  )
@@ -588,13 +657,23 @@ class StateStore:
588
657
  position = position_row["position"]
589
658
  connection.execute(
590
659
  """
591
- INSERT INTO messages (id, author, content, tools, thinking, status, position)
592
- VALUES (?, ?, ?, ?, ?, ?, ?)
660
+ INSERT INTO messages (
661
+ id,
662
+ author,
663
+ content,
664
+ tools,
665
+ thinking,
666
+ groups,
667
+ status,
668
+ position
669
+ )
670
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
593
671
  ON CONFLICT(id) DO UPDATE SET
594
672
  author = excluded.author,
595
673
  content = excluded.content,
596
674
  tools = excluded.tools,
597
675
  thinking = excluded.thinking,
676
+ groups = excluded.groups,
598
677
  status = excluded.status,
599
678
  position = excluded.position
600
679
  """,
@@ -606,6 +685,13 @@ class StateStore:
606
685
  [tool.model_dump(exclude_none=True) for tool in message.tools]
607
686
  ),
608
687
  message.thinking,
688
+ json.dumps(
689
+ [
690
+ group.model_dump(exclude_none=True)
691
+ for group in message.groups
692
+ ],
693
+ ensure_ascii=False,
694
+ ),
609
695
  message.status,
610
696
  position,
611
697
  ),
@@ -925,6 +1011,7 @@ class StateStore:
925
1011
  selected_provider_id TEXT NOT NULL DEFAULT '',
926
1012
  selected_model TEXT NOT NULL DEFAULT '',
927
1013
  reasoning_effort TEXT NOT NULL DEFAULT 'default',
1014
+ agent_prompt TEXT NOT NULL DEFAULT '',
928
1015
  updated_at INTEGER NOT NULL DEFAULT (unixepoch())
929
1016
  );
930
1017
 
@@ -998,6 +1085,10 @@ class StateStore:
998
1085
  connection.execute(
999
1086
  "ALTER TABLE messages ADD COLUMN status TEXT NOT NULL DEFAULT 'completed'"
1000
1087
  )
1088
+ if "groups" not in columns:
1089
+ connection.execute(
1090
+ "ALTER TABLE messages ADD COLUMN groups TEXT NOT NULL DEFAULT '[]'"
1091
+ )
1001
1092
  settings_columns = {
1002
1093
  row["name"] for row in connection.execute("PRAGMA table_info(settings)")
1003
1094
  }
@@ -1006,6 +1097,10 @@ class StateStore:
1006
1097
  "ALTER TABLE settings "
1007
1098
  "ADD COLUMN reasoning_effort TEXT NOT NULL DEFAULT 'default'"
1008
1099
  )
1100
+ if "agent_prompt" not in settings_columns:
1101
+ connection.execute(
1102
+ "ALTER TABLE settings ADD COLUMN agent_prompt TEXT NOT NULL DEFAULT ''"
1103
+ )
1009
1104
  workspace_context_columns = {
1010
1105
  row["name"]
1011
1106
  for row in connection.execute("PRAGMA table_info(workspace_context)")
@@ -11,7 +11,7 @@ from flowent.agent import FLOWENT_AGENT_SYSTEM_PROMPT, run_agent_stream
11
11
  from flowent.llm import ProviderConnection, ProviderFormat
12
12
  from flowent.main import create_app
13
13
  from flowent.sandbox import SandboxCommand, SandboxRunner
14
- from flowent.tools import ToolContext, run_tool
14
+ from flowent.tools import ToolContext, ToolResult, run_tool
15
15
 
16
16
 
17
17
  def stream_events(content: str) -> list[dict[str, object]]:
@@ -249,6 +249,19 @@ def test_sandbox_command_omits_proc_mount_when_preflight_reports_permission_erro
249
249
  assert "--proc" not in command.args
250
250
 
251
251
 
252
+ def test_sandbox_command_binds_writable_socket_path(tmp_path, monkeypatch) -> None:
253
+ socket_path = tmp_path / "docker.sock"
254
+ socket_path.touch()
255
+ runner = SandboxRunner(cwd=tmp_path, writable_roots=[socket_path])
256
+ monkeypatch.setattr("flowent.sandbox.sandbox_supports_proc_mount", lambda: False)
257
+
258
+ command = runner.build_command(["/bin/true"])
259
+
260
+ bind_index = command.args.index(str(socket_path))
261
+ assert command.args[bind_index - 1] == "--bind"
262
+ assert command.args[bind_index + 1] == str(socket_path)
263
+
264
+
252
265
  def test_sandbox_proc_preflight_does_not_hide_non_proc_errors(
253
266
  tmp_path, monkeypatch
254
267
  ) -> None:
@@ -901,6 +914,69 @@ def test_tool_failure_is_reported_and_agent_continues(tmp_path, monkeypatch) ->
901
914
  assert events[-1]["data"]["message"]["content"] == "I could not read it."
902
915
 
903
916
 
917
+ @pytest.mark.anyio
918
+ async def test_approval_denial_result_is_sent_to_agent(tmp_path) -> None:
919
+ captured_requests: list[dict[str, object]] = []
920
+
921
+ async def fake_completion(**request: object) -> object:
922
+ captured_requests.append(request)
923
+
924
+ async def chunks() -> object:
925
+ if len(captured_requests) == 1:
926
+ yield tool_call_chunk(
927
+ "shell_command",
928
+ {"command": "rm -rf /important"},
929
+ )
930
+ else:
931
+ yield text_chunk("I need explicit approval for that risk.")
932
+
933
+ return chunks()
934
+
935
+ async def denying_tool_runner(
936
+ name: str,
937
+ arguments: dict[str, object],
938
+ context: ToolContext,
939
+ ) -> ToolResult:
940
+ return ToolResult(
941
+ content=(
942
+ "Automatic approval review denied this action as high risk: "
943
+ "The command can delete broad data. The agent must not work around "
944
+ "this denial."
945
+ ),
946
+ ok=False,
947
+ title="Denied by reviewer",
948
+ )
949
+
950
+ events = [
951
+ event
952
+ async for event in run_agent_stream(
953
+ completion=fake_completion,
954
+ connection=ProviderConnection(
955
+ model="gpt-5.1",
956
+ name="Provider",
957
+ provider=ProviderFormat.OPENAI,
958
+ secret_reference="secret",
959
+ ),
960
+ cwd=tmp_path,
961
+ messages=[{"role": "user", "content": "Delete the important directory."}],
962
+ tool_runner=denying_tool_runner,
963
+ )
964
+ ]
965
+
966
+ assert len(captured_requests) == 2
967
+ assert captured_requests[1]["messages"][-1]["role"] == "tool"
968
+ assert "Automatic approval review denied this action" in str(
969
+ captured_requests[1]["messages"][-1]["content"]
970
+ )
971
+ assert "must not work around" in str(
972
+ captured_requests[1]["messages"][-1]["content"]
973
+ )
974
+ assert events[-2].data["content"] == "I need explicit approval for that risk."
975
+ assert events[-1].data["message"]["content"] == (
976
+ "I need explicit approval for that risk."
977
+ )
978
+
979
+
904
980
  def test_update_plan_outputs_plan_state(tmp_path) -> None:
905
981
  result = run_tool(
906
982
  "update_plan",