flowent 0.0.10 → 0.0.12

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 (50) hide show
  1. package/README.md +14 -0
  2. package/backend/README.md +14 -0
  3. package/backend/pyproject.toml +1 -1
  4. package/backend/src/flowent/__init__.py +6 -2
  5. package/backend/src/flowent/__pycache__/__init__.cpython-313.pyc +0 -0
  6. package/backend/src/flowent/__pycache__/_version.cpython-313.pyc +0 -0
  7. package/backend/src/flowent/__pycache__/agent.cpython-313.pyc +0 -0
  8. package/backend/src/flowent/__pycache__/cli.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__/patch.cpython-313.pyc +0 -0
  14. package/backend/src/flowent/__pycache__/paths.cpython-313.pyc +0 -0
  15. package/backend/src/flowent/__pycache__/sandbox.cpython-313.pyc +0 -0
  16. package/backend/src/flowent/__pycache__/storage.cpython-313.pyc +0 -0
  17. package/backend/src/flowent/__pycache__/tools.cpython-313.pyc +0 -0
  18. package/backend/src/flowent/agent.py +15 -0
  19. package/backend/src/flowent/cli.py +11 -0
  20. package/backend/src/flowent/llm.py +49 -1
  21. package/backend/src/flowent/main.py +15 -0
  22. package/backend/src/flowent/sandbox.py +18 -3
  23. package/backend/src/flowent/static/assets/index-BwQOML_0.css +2 -0
  24. package/backend/src/flowent/static/assets/index-DXQ_smj0.js +81 -0
  25. package/backend/src/flowent/static/index.html +2 -2
  26. package/backend/src/flowent/storage.py +38 -8
  27. package/backend/src/flowent/tools.py +15 -3
  28. package/backend/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  29. package/backend/tests/__pycache__/test_agent_tools.cpython-313-pytest-9.0.3.pyc +0 -0
  30. package/backend/tests/__pycache__/test_health.cpython-313-pytest-9.0.3.pyc +0 -0
  31. package/backend/tests/__pycache__/test_llm_providers.cpython-313-pytest-9.0.3.pyc +0 -0
  32. package/backend/tests/__pycache__/test_logging.cpython-313-pytest-9.0.3.pyc +0 -0
  33. package/backend/tests/__pycache__/test_persistence.cpython-313-pytest-9.0.3.pyc +0 -0
  34. package/backend/tests/__pycache__/test_startup_requirements.cpython-313-pytest-9.0.3.pyc +0 -0
  35. package/backend/tests/__pycache__/test_workspace_chat.cpython-313-pytest-9.0.3.pyc +0 -0
  36. package/backend/tests/conftest.py +21 -0
  37. package/backend/tests/test_agent_tools.py +73 -0
  38. package/backend/tests/test_llm_providers.py +58 -0
  39. package/backend/tests/test_persistence.py +16 -0
  40. package/backend/tests/test_startup_requirements.py +48 -0
  41. package/backend/tests/test_workspace_chat.py +28 -0
  42. package/backend/uv.lock +1 -1
  43. package/dist/frontend/assets/index-BwQOML_0.css +2 -0
  44. package/dist/frontend/assets/index-DXQ_smj0.js +81 -0
  45. package/dist/frontend/index.html +2 -2
  46. package/package.json +1 -1
  47. package/backend/src/flowent/static/assets/index-C76K95ty.js +0 -81
  48. package/backend/src/flowent/static/assets/index-iUMNKvlU.css +0 -2
  49. package/dist/frontend/assets/index-C76K95ty.js +0 -81
  50. package/dist/frontend/assets/index-iUMNKvlU.css +0 -2
@@ -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-C76K95ty.js"></script>
10
- <link rel="stylesheet" crossorigin href="/assets/index-iUMNKvlU.css">
9
+ <script type="module" crossorigin src="/assets/index-DXQ_smj0.js"></script>
10
+ <link rel="stylesheet" crossorigin href="/assets/index-BwQOML_0.css">
11
11
  </head>
12
12
  <body>
13
13
  <div id="root"></div>
@@ -4,7 +4,7 @@ from pathlib import Path
4
4
 
5
5
  from pydantic import BaseModel, ConfigDict, Field
6
6
 
7
- from flowent.llm import ProviderFormat
7
+ from flowent.llm import ProviderFormat, ReasoningEffort
8
8
  from flowent.paths import data_directory
9
9
 
10
10
 
@@ -22,6 +22,7 @@ class StoredProvider(BaseModel):
22
22
  class StoredSettings(BaseModel):
23
23
  model_config = ConfigDict(extra="forbid")
24
24
 
25
+ reasoning_effort: ReasoningEffort = ReasoningEffort.DEFAULT
25
26
  selected_model: str
26
27
  selected_provider_id: str
27
28
 
@@ -44,6 +45,7 @@ class StoredMessage(BaseModel):
44
45
  author: str
45
46
  content: str
46
47
  id: str
48
+ thinking: str = Field(default="", exclude_if=lambda value: value == "")
47
49
  tools: list[StoredToolItem] = Field(default_factory=list)
48
50
 
49
51
 
@@ -91,7 +93,7 @@ class StateStore:
91
93
  ]
92
94
  settings_row = connection.execute(
93
95
  """
94
- SELECT selected_provider_id, selected_model
96
+ SELECT selected_provider_id, selected_model, reasoning_effort
95
97
  FROM settings
96
98
  WHERE id = 1
97
99
  """
@@ -101,6 +103,7 @@ class StateStore:
101
103
  author=row["author"],
102
104
  content=row["content"],
103
105
  id=row["id"],
106
+ thinking=row["thinking"],
104
107
  tools=[
105
108
  StoredToolItem.model_validate(tool)
106
109
  for tool in json.loads(row["tools"] or "[]")
@@ -108,7 +111,7 @@ class StateStore:
108
111
  )
109
112
  for row in connection.execute(
110
113
  """
111
- SELECT id, author, content, tools
114
+ SELECT id, author, content, tools, thinking
112
115
  FROM messages
113
116
  ORDER BY position, id
114
117
  """
@@ -119,6 +122,9 @@ class StateStore:
119
122
  messages=messages,
120
123
  providers=providers,
121
124
  settings=StoredSettings(
125
+ reasoning_effort=settings_row["reasoning_effort"]
126
+ if settings_row
127
+ else ReasoningEffort.DEFAULT,
122
128
  selected_model=settings_row["selected_model"] if settings_row else "",
123
129
  selected_provider_id=settings_row["selected_provider_id"]
124
130
  if settings_row
@@ -166,14 +172,24 @@ class StateStore:
166
172
  with self.connect() as connection:
167
173
  connection.execute(
168
174
  """
169
- INSERT INTO settings (id, selected_provider_id, selected_model)
170
- VALUES (1, ?, ?)
175
+ INSERT INTO settings (
176
+ id,
177
+ selected_provider_id,
178
+ selected_model,
179
+ reasoning_effort
180
+ )
181
+ VALUES (1, ?, ?, ?)
171
182
  ON CONFLICT(id) DO UPDATE SET
172
183
  selected_provider_id = excluded.selected_provider_id,
173
184
  selected_model = excluded.selected_model,
185
+ reasoning_effort = excluded.reasoning_effort,
174
186
  updated_at = unixepoch()
175
187
  """,
176
- (settings.selected_provider_id, settings.selected_model),
188
+ (
189
+ settings.selected_provider_id,
190
+ settings.selected_model,
191
+ settings.reasoning_effort.value,
192
+ ),
177
193
  )
178
194
  return settings
179
195
 
@@ -182,8 +198,8 @@ class StateStore:
182
198
  connection.execute("DELETE FROM messages")
183
199
  connection.executemany(
184
200
  """
185
- INSERT INTO messages (id, author, content, tools, position)
186
- VALUES (?, ?, ?, ?, ?)
201
+ INSERT INTO messages (id, author, content, tools, thinking, position)
202
+ VALUES (?, ?, ?, ?, ?, ?)
187
203
  """,
188
204
  [
189
205
  (
@@ -196,6 +212,7 @@ class StateStore:
196
212
  for tool in message.tools
197
213
  ]
198
214
  ),
215
+ message.thinking,
199
216
  position,
200
217
  )
201
218
  for position, message in enumerate(messages)
@@ -270,6 +287,7 @@ class StateStore:
270
287
  id INTEGER PRIMARY KEY CHECK (id = 1),
271
288
  selected_provider_id TEXT NOT NULL DEFAULT '',
272
289
  selected_model TEXT NOT NULL DEFAULT '',
290
+ reasoning_effort TEXT NOT NULL DEFAULT 'default',
273
291
  updated_at INTEGER NOT NULL DEFAULT (unixepoch())
274
292
  );
275
293
 
@@ -300,3 +318,15 @@ class StateStore:
300
318
  connection.execute(
301
319
  "ALTER TABLE messages ADD COLUMN tools TEXT NOT NULL DEFAULT '[]'"
302
320
  )
321
+ if "thinking" not in columns:
322
+ connection.execute(
323
+ "ALTER TABLE messages ADD COLUMN thinking TEXT NOT NULL DEFAULT ''"
324
+ )
325
+ settings_columns = {
326
+ row["name"] for row in connection.execute("PRAGMA table_info(settings)")
327
+ }
328
+ if "reasoning_effort" not in settings_columns:
329
+ connection.execute(
330
+ "ALTER TABLE settings "
331
+ "ADD COLUMN reasoning_effort TEXT NOT NULL DEFAULT 'default'"
332
+ )
@@ -261,9 +261,7 @@ def apply_patch_tool(arguments: dict[str, object], context: ToolContext) -> Tool
261
261
  input_text=patch,
262
262
  )
263
263
  if result.exit_code != 0:
264
- raise SandboxError(
265
- result.stderr or result.stdout or "Patch could not be applied."
266
- )
264
+ raise SandboxError(tool_failure_content(result))
267
265
  return ToolResult(
268
266
  content=result.stdout,
269
267
  data={"files": [str(path) for path in paths]},
@@ -271,6 +269,20 @@ def apply_patch_tool(arguments: dict[str, object], context: ToolContext) -> Tool
271
269
  )
272
270
 
273
271
 
272
+ def tool_failure_content(result: object) -> str:
273
+ stdout = str(getattr(result, "stdout", "") or "").strip()
274
+ stderr = str(getattr(result, "stderr", "") or "").strip()
275
+ if stdout:
276
+ try:
277
+ payload = json.loads(stdout)
278
+ except json.JSONDecodeError:
279
+ payload = None
280
+ if isinstance(payload, dict) and isinstance(payload.get("error"), str):
281
+ return payload["error"]
282
+ parts = [part for part in [stderr, stdout] if part]
283
+ return "\n".join(parts) or "Tool failed."
284
+
285
+
274
286
  def shell_command(arguments: dict[str, object], context: ToolContext) -> ToolResult:
275
287
  command = str(arguments["command"])
276
288
  timeout_seconds = integer_argument(arguments, "timeout_seconds", 30)
@@ -0,0 +1,21 @@
1
+ import os
2
+ import stat
3
+ import tempfile
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+
8
+ _test_environment = Path(tempfile.mkdtemp(prefix="flowent-tests-"))
9
+ _test_bin = _test_environment / "bin"
10
+ _test_bin.mkdir(parents=True, exist_ok=True)
11
+ _test_bwrap = _test_bin / "bwrap"
12
+ _test_bwrap.write_text("#!/bin/sh\nexit 0\n")
13
+ _test_bwrap.chmod(_test_bwrap.stat().st_mode | stat.S_IXUSR)
14
+
15
+ os.environ.setdefault("FLOWENT_DATA_DIR", str(_test_environment / "data"))
16
+ os.environ["PATH"] = f"{_test_bin}{os.pathsep}{os.environ.get('PATH', '')}"
17
+
18
+
19
+ @pytest.fixture(autouse=True)
20
+ def sandbox_available(monkeypatch):
21
+ monkeypatch.setattr("flowent.sandbox.sandbox_binary", lambda: "/usr/bin/bwrap")
@@ -38,6 +38,7 @@ def configure_provider(client: TestClient) -> None:
38
38
  client.put(
39
39
  "/api/settings",
40
40
  json={
41
+ "reasoning_effort": "default",
41
42
  "selected_model": "gpt-5.1",
42
43
  "selected_provider_id": "provider-openai",
43
44
  },
@@ -72,6 +73,10 @@ def text_chunk(content: str) -> dict[str, object]:
72
73
  return {"choices": [{"delta": {"content": content}}]}
73
74
 
74
75
 
76
+ def thinking_chunk(content: str) -> dict[str, object]:
77
+ return {"choices": [{"delta": {"reasoning_content": content}}]}
78
+
79
+
75
80
  def test_workspace_response_streams_tool_process_and_final_text(
76
81
  tmp_path, monkeypatch
77
82
  ) -> None:
@@ -296,6 +301,34 @@ def test_apply_patch_uses_internal_subcommand(tmp_path, monkeypatch) -> None:
296
301
  assert calls[0][1:4] == ["-m", "flowent.cli", "apply-patch"]
297
302
 
298
303
 
304
+ def test_apply_patch_reports_patch_error_when_stderr_has_warning(
305
+ tmp_path, monkeypatch
306
+ ) -> None:
307
+ def fake_run(self, command, **kwargs):
308
+ from flowent.sandbox import CommandResult
309
+
310
+ return CommandResult(
311
+ command=" ".join(command),
312
+ exit_code=1,
313
+ stderr="RuntimeWarning: flowent.cli was already imported\n",
314
+ stdout='{"error": "Patch context was not found."}\n',
315
+ )
316
+
317
+ monkeypatch.setattr(SandboxRunner, "run", fake_run)
318
+ patch = """*** Begin Patch
319
+ *** Update File: notes.txt
320
+ @@
321
+ -missing
322
+ +ready
323
+ *** End Patch
324
+ """
325
+
326
+ result = run_tool("apply_patch", {"patch": patch}, ToolContext(cwd=tmp_path))
327
+
328
+ assert not result.ok
329
+ assert result.content == "Patch context was not found."
330
+
331
+
299
332
  def test_web_search_result_enters_tool_output(tmp_path) -> None:
300
333
  def fake_search(query: str):
301
334
  return [{"title": "Result", "url": "https://example.test", "snippet": query}]
@@ -403,6 +436,46 @@ def test_agent_finishes_without_tools(tmp_path, monkeypatch) -> None:
403
436
  assert events[-1]["data"]["message"]["content"] == "Direct answer."
404
437
 
405
438
 
439
+ def test_agent_streams_and_persists_thinking(tmp_path, monkeypatch) -> None:
440
+ monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path / "data"))
441
+ monkeypatch.chdir(tmp_path)
442
+
443
+ async def fake_completion(**request: object) -> object:
444
+ async def chunks() -> object:
445
+ yield thinking_chunk("Checking context.")
446
+ yield thinking_chunk(" Preparing answer.")
447
+ yield text_chunk("Direct answer.")
448
+
449
+ return chunks()
450
+
451
+ client = TestClient(
452
+ create_app(serve_frontend=False, chat_completion=fake_completion)
453
+ )
454
+ configure_provider(client)
455
+
456
+ response = client.post(
457
+ "/api/workspace/respond",
458
+ json={"content": "Answer directly."},
459
+ )
460
+
461
+ assert response.status_code == 200
462
+ events = stream_events(response.text)
463
+ assert [event["event"] for event in events] == [
464
+ "start",
465
+ "output_start",
466
+ "thinking_delta",
467
+ "thinking_delta",
468
+ "delta",
469
+ "done",
470
+ ]
471
+ assert events[2]["data"] == {"content": "Checking context."}
472
+ assert events[-1]["data"]["message"]["thinking"] == (
473
+ "Checking context. Preparing answer."
474
+ )
475
+ state = client.get("/api/state").json()
476
+ assert state["messages"][-1]["thinking"] == ("Checking context. Preparing answer.")
477
+
478
+
406
479
  def test_tool_failure_is_reported_and_agent_continues(tmp_path, monkeypatch) -> None:
407
480
  monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path / "data"))
408
481
  monkeypatch.chdir(tmp_path)
@@ -4,7 +4,9 @@ from flowent.llm import (
4
4
  ChatMessage,
5
5
  ProviderConnection,
6
6
  ProviderFormat,
7
+ ReasoningEffort,
7
8
  build_litellm_request,
9
+ chunk_delta_reasoning,
8
10
  complete_chat,
9
11
  stream_chat,
10
12
  )
@@ -45,6 +47,62 @@ def test_build_litellm_request_maps_provider_connection_to_completion_args() ->
45
47
  }
46
48
 
47
49
 
50
+ def test_build_litellm_request_omits_default_reasoning_effort() -> None:
51
+ connection = ProviderConnection(
52
+ name="Primary",
53
+ provider=ProviderFormat.OPENAI,
54
+ model="gpt-5.1",
55
+ secret_reference="connection-primary",
56
+ reasoning_effort=ReasoningEffort.DEFAULT,
57
+ )
58
+
59
+ request = build_litellm_request(
60
+ connection, [ChatMessage(role="user", content="Draft a checklist.")]
61
+ )
62
+
63
+ assert "reasoning_effort" not in request
64
+
65
+
66
+ def test_build_litellm_request_includes_selected_reasoning_effort() -> None:
67
+ connection = ProviderConnection(
68
+ name="Primary",
69
+ provider=ProviderFormat.OPENAI,
70
+ model="gpt-5.1",
71
+ secret_reference="connection-primary",
72
+ reasoning_effort=ReasoningEffort.XHIGH,
73
+ )
74
+
75
+ request = build_litellm_request(
76
+ connection, [ChatMessage(role="user", content="Draft a checklist.")]
77
+ )
78
+
79
+ assert request["reasoning_effort"] == "xhigh"
80
+
81
+
82
+ def test_chunk_delta_reasoning_reads_litellm_reasoning_fields() -> None:
83
+ assert (
84
+ chunk_delta_reasoning(
85
+ {"choices": [{"delta": {"reasoning_content": "Checking files."}}]}
86
+ )
87
+ == "Checking files."
88
+ )
89
+ assert (
90
+ chunk_delta_reasoning(
91
+ {
92
+ "choices": [
93
+ {
94
+ "delta": {
95
+ "thinking_blocks": [{"thinking": "Read files."}],
96
+ "reasoning_items": [{"summary": "Summarize."}],
97
+ }
98
+ }
99
+ ]
100
+ }
101
+ )
102
+ == "Read files.Summarize."
103
+ )
104
+
105
+
48
106
  @pytest.mark.anyio
49
107
  async def test_complete_chat_uses_injected_litellm_completion() -> None:
50
108
  captured_request: dict[str, object] = {}
@@ -59,6 +59,7 @@ def test_app_state_persists_settings_and_workspace_messages(
59
59
  settings_response = client.put(
60
60
  "/api/settings",
61
61
  json={
62
+ "reasoning_effort": "xhigh",
62
63
  "selected_model": "claude-sonnet-4-5",
63
64
  "selected_provider_id": "provider-anthropic",
64
65
  },
@@ -71,6 +72,7 @@ def test_app_state_persists_settings_and_workspace_messages(
71
72
  "author": "assistant",
72
73
  "content": "Draft a launch checklist",
73
74
  "id": "message-1",
75
+ "thinking": "Read the request.",
74
76
  "tools": [
75
77
  {
76
78
  "id": "tool-1",
@@ -91,6 +93,7 @@ def test_app_state_persists_settings_and_workspace_messages(
91
93
  state = restarted_client.get("/api/state").json()
92
94
 
93
95
  assert state["settings"] == {
96
+ "reasoning_effort": "xhigh",
94
97
  "selected_model": "claude-sonnet-4-5",
95
98
  "selected_provider_id": "provider-anthropic",
96
99
  }
@@ -99,6 +102,7 @@ def test_app_state_persists_settings_and_workspace_messages(
99
102
  "author": "assistant",
100
103
  "content": "Draft a launch checklist",
101
104
  "id": "message-1",
105
+ "thinking": "Read the request.",
102
106
  "tools": [
103
107
  {
104
108
  "arguments": None,
@@ -123,3 +127,15 @@ def test_data_directory_uses_flowent_data_dir(tmp_path, monkeypatch) -> None:
123
127
 
124
128
  assert response.status_code == 200
125
129
  assert (data_dir / "flowent.db").is_file()
130
+
131
+
132
+ def test_app_state_defaults_reasoning_effort_for_existing_settings(
133
+ tmp_path, monkeypatch
134
+ ) -> None:
135
+ monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
136
+ client = TestClient(create_app(serve_frontend=False))
137
+
138
+ response = client.get("/api/state")
139
+
140
+ assert response.status_code == 200
141
+ assert response.json()["settings"]["reasoning_effort"] == "default"
@@ -0,0 +1,48 @@
1
+ import pytest
2
+
3
+ from flowent.cli import main
4
+ from flowent.main import create_app
5
+ from flowent.sandbox import SandboxError
6
+
7
+
8
+ def test_create_app_fails_when_sandbox_is_missing(monkeypatch) -> None:
9
+ monkeypatch.setattr("flowent.sandbox.sandbox_binary", lambda: None)
10
+
11
+ with pytest.raises(SandboxError, match="Install bubblewrap"):
12
+ create_app(serve_frontend=False)
13
+
14
+
15
+ def test_create_app_starts_when_bwrap_is_available(monkeypatch) -> None:
16
+ monkeypatch.setattr("flowent.sandbox.sandbox_binary", lambda: "/usr/bin/bwrap")
17
+
18
+ app = create_app(serve_frontend=False)
19
+
20
+ assert app.title == "Flowent"
21
+
22
+
23
+ def test_create_app_starts_when_bubblewrap_fallback_is_available(monkeypatch) -> None:
24
+ monkeypatch.setattr("flowent.sandbox.sandbox_binary", lambda: "/usr/bin/bubblewrap")
25
+
26
+ app = create_app(serve_frontend=False)
27
+
28
+ assert app.title == "Flowent"
29
+
30
+
31
+ def test_doctor_reports_missing_sandbox(monkeypatch, capsys) -> None:
32
+ monkeypatch.setattr("flowent.sandbox.sandbox_binary", lambda: None)
33
+
34
+ with pytest.raises(SystemExit) as error:
35
+ main(["doctor"])
36
+
37
+ assert error.value.code == 1
38
+ assert "Sandbox: missing." in capsys.readouterr().err
39
+
40
+
41
+ def test_doctor_reports_available_sandbox(monkeypatch, capsys) -> None:
42
+ monkeypatch.setattr("flowent.sandbox.sandbox_binary", lambda: "/usr/bin/bwrap")
43
+
44
+ with pytest.raises(SystemExit) as error:
45
+ main(["doctor"])
46
+
47
+ assert error.value.code == 0
48
+ assert "Sandbox: /usr/bin/bwrap" in capsys.readouterr().out
@@ -12,6 +12,7 @@ def configure_provider(
12
12
  name: str = "OpenAI",
13
13
  provider_id: str = "provider-openai",
14
14
  provider_type: str = "openai",
15
+ reasoning_effort: str = "default",
15
16
  ) -> None:
16
17
  client.post(
17
18
  "/api/providers",
@@ -27,6 +28,7 @@ def configure_provider(
27
28
  client.put(
28
29
  "/api/settings",
29
30
  json={
31
+ "reasoning_effort": reasoning_effort,
30
32
  "selected_model": model,
31
33
  "selected_provider_id": provider_id,
32
34
  },
@@ -324,6 +326,32 @@ def test_workspace_response_includes_project_and_environment_context(
324
326
  }
325
327
 
326
328
 
329
+ def test_workspace_response_uses_selected_reasoning_effort(
330
+ tmp_path, monkeypatch
331
+ ) -> None:
332
+ monkeypatch.chdir(tmp_path)
333
+ monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path / "data"))
334
+ captured_request: dict[str, object] = {}
335
+
336
+ async def fake_completion(**request: object) -> object:
337
+ captured_request.update(request)
338
+
339
+ async def chunks() -> object:
340
+ yield {"choices": [{"delta": {"content": "Done."}}]}
341
+
342
+ return chunks()
343
+
344
+ client = TestClient(
345
+ create_app(serve_frontend=False, chat_completion=fake_completion)
346
+ )
347
+ configure_provider(client, reasoning_effort="xhigh")
348
+
349
+ response = client.post("/api/workspace/respond", json={"content": "Hello."})
350
+
351
+ assert response.status_code == 200
352
+ assert captured_request["reasoning_effort"] == "xhigh"
353
+
354
+
327
355
  def test_workspace_response_prefers_agents_override(tmp_path, monkeypatch) -> None:
328
356
  monkeypatch.chdir(tmp_path)
329
357
  monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path / "data"))
package/backend/uv.lock CHANGED
@@ -388,7 +388,7 @@ wheels = [
388
388
 
389
389
  [[package]]
390
390
  name = "flowent"
391
- version = "0.0.10"
391
+ version = "0.0.12"
392
392
  source = { editable = "." }
393
393
  dependencies = [
394
394
  { name = "fastapi", extra = ["standard"] },