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.
- package/backend/pyproject.toml +1 -1
- package/backend/src/flowent/__pycache__/__init__.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/_version.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/agent.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/approval.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/channels.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/cli.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/compact.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/context.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/llm.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/logging.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/main.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/mcp.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/mcp_import.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/patch.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/paths.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/permissions.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/sandbox.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/skills.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/storage.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/tools.cpython-313.pyc +0 -0
- package/backend/src/flowent/agent.py +23 -1
- package/backend/src/flowent/approval.py +148 -0
- package/backend/src/flowent/cli.py +4 -2
- package/backend/src/flowent/context.py +19 -1
- package/backend/src/flowent/llm.py +51 -11
- package/backend/src/flowent/logging.py +60 -0
- package/backend/src/flowent/main.py +639 -210
- package/backend/src/flowent/patch.py +55 -31
- package/backend/src/flowent/permissions.py +185 -42
- package/backend/src/flowent/sandbox.py +55 -1
- package/backend/src/flowent/static/assets/index-Cl20cARb.css +2 -0
- package/backend/src/flowent/static/assets/index-dsDDsEym.js +81 -0
- package/backend/src/flowent/static/index.html +2 -2
- package/backend/src/flowent/storage.py +113 -18
- package/backend/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_agent_tools.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_approval.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_channels.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_health.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_llm_providers.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_logging.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_mcp.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_patch.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_permissions.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_persistence.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_skills.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_startup_requirements.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_workspace_chat.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/test_agent_tools.py +77 -1
- package/backend/tests/test_approval.py +283 -0
- package/backend/tests/test_llm_providers.py +216 -0
- package/backend/tests/test_logging.py +30 -0
- package/backend/tests/test_patch.py +112 -0
- package/backend/tests/test_permissions.py +198 -53
- package/backend/tests/test_persistence.py +78 -0
- package/backend/tests/test_startup_requirements.py +54 -0
- package/backend/tests/test_workspace_chat.py +855 -41
- package/backend/uv.lock +1 -1
- package/dist/frontend/assets/index-Cl20cARb.css +2 -0
- package/dist/frontend/assets/index-dsDDsEym.js +81 -0
- package/dist/frontend/index.html +2 -2
- package/package.json +1 -1
- package/backend/src/flowent/static/assets/index-BREidonU.css +0 -2
- package/backend/src/flowent/static/assets/index-DSniOrhL.js +0 -81
- package/dist/frontend/assets/index-BREidonU.css +0 -2
- 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-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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 (
|
|
553
|
-
|
|
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 (
|
|
592
|
-
|
|
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)")
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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",
|