flowent 0.3.3 → 0.3.4

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.
@@ -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-CROofCFl.js"></script>
10
- <link rel="stylesheet" crossorigin href="/assets/index-CCf0mo80.css">
9
+ <script type="module" crossorigin src="/assets/index-D3WSbctU.js"></script>
10
+ <link rel="stylesheet" crossorigin href="/assets/index-ByGH1ZWH.css">
11
11
  </head>
12
12
  <body>
13
13
  <div id="root"></div>
@@ -1,8 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from collections.abc import Mapping
4
+ from pathlib import Path
4
5
 
6
+ from flowent.agent_runtime import FlowentAgentRuntime
5
7
  from flowent.llm import CompletionCallable
8
+ from flowent.mcp import McpManager
6
9
  from flowent.provider_connections import selected_connection
7
10
  from flowent.storage import StateStore, StoredWorkflow, StoredWorkflowDefinition
8
11
  from flowent.workflows import (
@@ -18,10 +21,21 @@ class WorkflowService:
18
21
  self,
19
22
  *,
20
23
  chat_completion: CompletionCallable | None,
24
+ cwd: Path,
25
+ mcp_manager: McpManager,
21
26
  store: StateStore,
22
27
  ) -> None:
23
28
  self.chat_completion = chat_completion
29
+ self.cwd = cwd
30
+ self.mcp_manager = mcp_manager
24
31
  self.store = store
32
+ self.agent_runtime = FlowentAgentRuntime(
33
+ chat_completion=chat_completion,
34
+ cwd=cwd,
35
+ mcp_manager=mcp_manager,
36
+ store=store,
37
+ workflow_service=self,
38
+ )
25
39
 
26
40
  def list_workflows(self) -> list[StoredWorkflow]:
27
41
  return self.store.read_workflows()
@@ -54,6 +68,8 @@ class WorkflowService:
54
68
  *,
55
69
  default_input: str = "",
56
70
  input_values: Mapping[str, str] | None = None,
71
+ timer_node_id: str = "",
72
+ workflow_depth: int = 0,
57
73
  ) -> WorkflowRunResponse:
58
74
  workflow = self.get_workflow(workflow_id)
59
75
  connection = (
@@ -62,11 +78,13 @@ class WorkflowService:
62
78
  else None
63
79
  )
64
80
  return await run_workflow_definition(
65
- completion=self.chat_completion,
66
81
  connection=connection,
67
82
  default_input=default_input,
68
83
  definition=workflow.definition,
69
84
  input_values=input_values,
85
+ runtime=self.agent_runtime,
86
+ timer_node_id=timer_node_id,
87
+ workflow_depth=workflow_depth,
70
88
  workflow_id=workflow.id,
71
89
  )
72
90
 
@@ -76,6 +94,8 @@ class WorkflowService:
76
94
  default_input: str = "",
77
95
  definition: StoredWorkflowDefinition,
78
96
  input_values: Mapping[str, str] | None = None,
97
+ timer_node_id: str = "",
98
+ workflow_depth: int = 0,
79
99
  workflow_id: str,
80
100
  ) -> WorkflowRunResponse:
81
101
  connection = (
@@ -84,10 +104,12 @@ class WorkflowService:
84
104
  else None
85
105
  )
86
106
  return await run_workflow_definition(
87
- completion=self.chat_completion,
88
107
  connection=connection,
89
108
  default_input=default_input,
90
109
  definition=definition,
91
110
  input_values=input_values,
111
+ runtime=self.agent_runtime,
112
+ timer_node_id=timer_node_id,
113
+ workflow_depth=workflow_depth,
92
114
  workflow_id=workflow_id,
93
115
  )
@@ -1,12 +1,19 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing import TYPE_CHECKING
4
+ from uuid import uuid4
5
+
3
6
  from pydantic import ValidationError
4
7
 
5
8
  from flowent.storage import StoredWorkflow
6
9
  from flowent.tools import ToolResult, text_tool_result
7
- from flowent.workflow_service import WorkflowService
8
10
  from flowent.workflows import WorkflowRunResponse
9
11
 
12
+ if TYPE_CHECKING:
13
+ from flowent.workflow_service import WorkflowService
14
+
15
+ MAX_WORKFLOW_TOOL_DEPTH = 3
16
+
10
17
 
11
18
  def workflow_tool_specs() -> list[dict[str, object]]:
12
19
  workflow_schema: dict[str, object] = {"type": "object"}
@@ -92,8 +99,9 @@ def workflow_tool_title(name: str) -> str | None:
92
99
 
93
100
 
94
101
  class WorkflowAgentTools:
95
- def __init__(self, service: WorkflowService) -> None:
102
+ def __init__(self, service: WorkflowService, *, workflow_depth: int = 0) -> None:
96
103
  self.service = service
104
+ self.workflow_depth = workflow_depth
97
105
 
98
106
  async def run_tool(
99
107
  self, name: str, arguments: dict[str, object]
@@ -157,12 +165,15 @@ class WorkflowAgentTools:
157
165
  )
158
166
 
159
167
  async def run_workflow(self, arguments: dict[str, object]) -> ToolResult:
168
+ if self.workflow_depth >= MAX_WORKFLOW_TOOL_DEPTH:
169
+ raise ValueError("Workflow nesting is too deep.")
160
170
  workflow_id = str(arguments["workflow_id"])
161
171
  workflow = self.service.get_workflow(workflow_id)
162
172
  result = await self.service.run_workflow(
163
173
  workflow_id,
164
174
  default_input=string_argument(arguments, "input"),
165
175
  input_values=string_map_argument(arguments, "inputs"),
176
+ workflow_depth=self.workflow_depth + 1,
166
177
  )
167
178
  output = workflow_run_output(workflow.name, result)
168
179
  return ToolResult(
@@ -184,12 +195,9 @@ class WorkflowAgentTools:
184
195
 
185
196
  def create_workflow(self, arguments: dict[str, object]) -> ToolResult:
186
197
  workflow = workflow_argument(arguments)
187
- try:
188
- self.service.get_workflow(workflow.id)
189
- except ValueError:
190
- saved = self.service.save_workflow(workflow)
191
- else:
192
- raise ValueError("Workflow already exists.")
198
+ saved = self.service.save_workflow(
199
+ workflow.model_copy(update={"id": str(uuid4())})
200
+ )
193
201
  return saved_workflow_result(saved, "Created")
194
202
 
195
203
  def update_workflow(self, arguments: dict[str, object]) -> ToolResult:
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import json
2
4
  import re
3
5
  import sys
@@ -5,15 +7,12 @@ import tempfile
5
7
  from collections import defaultdict, deque
6
8
  from collections.abc import Mapping
7
9
  from pathlib import Path
10
+ from typing import TYPE_CHECKING
8
11
 
9
12
  from pydantic import BaseModel, ConfigDict, Field
10
13
 
11
- from flowent.llm import (
12
- ChatMessage,
13
- CompletionCallable,
14
- ProviderConnection,
15
- complete_chat,
16
- )
14
+ from flowent.context import runtime_context_messages
15
+ from flowent.llm import ProviderConnection
17
16
  from flowent.sandbox import SandboxRunner
18
17
  from flowent.storage import (
19
18
  StoredWorkflow,
@@ -22,6 +21,9 @@ from flowent.storage import (
22
21
  StoredWorkflowNode,
23
22
  )
24
23
 
24
+ if TYPE_CHECKING:
25
+ from flowent.agent_runtime import FlowentAgentRuntime
26
+
25
27
 
26
28
  class WorkflowNodeRunResult(BaseModel):
27
29
  model_config = ConfigDict(extra="forbid")
@@ -41,6 +43,13 @@ class WorkflowRunResponse(BaseModel):
41
43
  workflow_id: str
42
44
 
43
45
 
46
+ class WorkflowRunRequestValues(BaseModel):
47
+ model_config = ConfigDict(extra="forbid")
48
+
49
+ default_input: str = ""
50
+ input_values: dict[str, str] = Field(default_factory=dict)
51
+
52
+
44
53
  PLACEHOLDER_PATTERN = re.compile(r"\{\{\s*([A-Za-z0-9_.-]+)\.output\s*\}\}")
45
54
  PYTHON_CODE_RUNNER = r"""
46
55
  import contextlib
@@ -106,8 +115,8 @@ def validate_workflow_definition(definition: StoredWorkflowDefinition) -> list[s
106
115
  raise ValueError("Workflow node ids must not be empty.")
107
116
  if len(set(node_ids)) != len(node_ids):
108
117
  raise ValueError("Workflow node ids must be unique.")
109
- if not any(node.type == "input" for node in definition.nodes):
110
- raise ValueError("Workflow needs an input node.")
118
+ if not any(node.type in {"input", "timer"} for node in definition.nodes):
119
+ raise ValueError("Workflow needs an input or timer node.")
111
120
  if not any(node.type == "output" for node in definition.nodes):
112
121
  raise ValueError("Workflow needs an output node.")
113
122
 
@@ -129,6 +138,43 @@ def workflow_requires_connection(definition: StoredWorkflowDefinition) -> bool:
129
138
  return any(node.type == "agent" for node in definition.nodes)
130
139
 
131
140
 
141
+ def timer_run_node_ids(
142
+ definition: StoredWorkflowDefinition, timer_node_id: str
143
+ ) -> set[str]:
144
+ nodes = {node.id: node for node in definition.nodes}
145
+ timer_node = nodes.get(timer_node_id)
146
+ if timer_node is None or timer_node.type != "timer":
147
+ raise ValueError("Timer node not found.")
148
+
149
+ outgoing: dict[str, list[str]] = defaultdict(list)
150
+ incoming: dict[str, list[str]] = defaultdict(list)
151
+ for edge in definition.edges:
152
+ outgoing[edge.source].append(edge.target)
153
+ incoming[edge.target].append(edge.source)
154
+
155
+ active = {timer_node_id}
156
+ queue = deque([timer_node_id])
157
+ while queue:
158
+ node_id = queue.popleft()
159
+ for target in outgoing[node_id]:
160
+ if target not in active:
161
+ active.add(target)
162
+ queue.append(target)
163
+
164
+ queue = deque(active)
165
+ while queue:
166
+ node_id = queue.popleft()
167
+ for source in incoming[node_id]:
168
+ source_node = nodes[source]
169
+ if source_node.type == "timer" and source != timer_node_id:
170
+ continue
171
+ if source not in active:
172
+ active.add(source)
173
+ queue.append(source)
174
+
175
+ return active
176
+
177
+
132
178
  def topological_node_ids(definition: StoredWorkflowDefinition) -> list[str]:
133
179
  node_ids = [node.id for node in definition.nodes]
134
180
  outgoing: dict[str, list[str]] = defaultdict(list)
@@ -162,39 +208,73 @@ def topological_node_ids(definition: StoredWorkflowDefinition) -> list[str]:
162
208
 
163
209
  async def run_workflow_definition(
164
210
  *,
165
- completion: CompletionCallable | None,
166
211
  connection: ProviderConnection | None,
167
212
  definition: StoredWorkflowDefinition,
168
213
  default_input: str = "",
169
214
  input_values: Mapping[str, str] | None = None,
215
+ runtime: FlowentAgentRuntime | None = None,
216
+ timer_node_id: str = "",
217
+ workflow_depth: int = 0,
170
218
  workflow_id: str,
171
219
  ) -> WorkflowRunResponse:
172
220
  ordered_ids = validate_workflow_definition(definition)
173
221
  if workflow_requires_connection(definition) and connection is None:
174
222
  raise ValueError("Choose a provider and model before running.")
175
223
 
224
+ return await run_workflow_once(
225
+ connection=connection,
226
+ definition=definition,
227
+ input_values=WorkflowRunRequestValues(
228
+ default_input=default_input,
229
+ input_values=dict(input_values or {}),
230
+ ),
231
+ ordered_ids=ordered_ids,
232
+ runtime=runtime,
233
+ timer_node_id=timer_node_id,
234
+ workflow_depth=workflow_depth,
235
+ workflow_id=workflow_id,
236
+ )
237
+
238
+
239
+ async def run_workflow_once(
240
+ *,
241
+ connection: ProviderConnection | None,
242
+ definition: StoredWorkflowDefinition,
243
+ input_values: WorkflowRunRequestValues,
244
+ ordered_ids: list[str],
245
+ runtime: FlowentAgentRuntime | None = None,
246
+ timer_node_id: str = "",
247
+ workflow_depth: int = 0,
248
+ workflow_id: str,
249
+ ) -> WorkflowRunResponse:
176
250
  nodes = {node.id: node for node in definition.nodes}
177
251
  incoming_edges = edges_by_target(definition.edges)
252
+ active_node_ids = (
253
+ timer_run_node_ids(definition, timer_node_id) if timer_node_id else None
254
+ )
178
255
  results: dict[str, WorkflowNodeRunResult] = {
179
256
  node.id: WorkflowNodeRunResult(id=node.id, status="pending")
180
257
  for node in definition.nodes
181
258
  }
182
259
  outputs: dict[str, str] = {}
183
260
  named_outputs: dict[str, str] = {}
184
- remaining_default_input = default_input
261
+ remaining_default_input = input_values.default_input
185
262
 
186
263
  for node_id in ordered_ids:
187
264
  node = nodes[node_id]
265
+ if active_node_ids is not None and node.id not in active_node_ids:
266
+ continue
188
267
  results[node.id] = WorkflowNodeRunResult(id=node.id, status="running")
189
268
  try:
190
269
  output = await run_node(
191
- completion=completion,
192
270
  connection=connection,
193
271
  default_input=remaining_default_input,
194
- input_values=input_values or {},
272
+ input_values=input_values.input_values,
195
273
  incoming_edges=incoming_edges[node.id],
196
274
  node=node,
197
275
  outputs=outputs,
276
+ runtime=runtime,
277
+ workflow_depth=workflow_depth,
198
278
  )
199
279
  if node.type == "input" and remaining_default_input:
200
280
  remaining_default_input = ""
@@ -229,13 +309,14 @@ async def run_workflow_definition(
229
309
 
230
310
  async def run_node(
231
311
  *,
232
- completion: CompletionCallable | None,
233
312
  connection: ProviderConnection | None,
234
313
  default_input: str,
235
314
  input_values: Mapping[str, str],
236
315
  incoming_edges: list[StoredWorkflowEdge],
237
316
  node: StoredWorkflowNode,
238
317
  outputs: Mapping[str, str],
318
+ runtime: FlowentAgentRuntime | None = None,
319
+ workflow_depth: int = 0,
239
320
  ) -> str:
240
321
  if node.type == "input":
241
322
  if node.id in input_values:
@@ -246,17 +327,25 @@ async def run_node(
246
327
  if node.type == "agent":
247
328
  if connection is None:
248
329
  raise ValueError("Choose a provider and model before running.")
330
+ if runtime is None:
331
+ raise ValueError("Agent runtime is not available.")
249
332
  prompt = render_template(
250
333
  node_data_text(node, "prompt")
251
334
  or joined_upstream_outputs(incoming_edges, outputs),
252
335
  outputs,
253
336
  )
254
- response = await complete_chat(
255
- connection,
256
- [ChatMessage(role="user", content=prompt)],
257
- completion=completion,
337
+ result = await runtime.complete(
338
+ connection=connection,
339
+ messages=[
340
+ *runtime_context_messages(
341
+ runtime.cwd, runtime.store.read_state().settings.agent_prompt
342
+ ),
343
+ {"role": "user", "content": prompt},
344
+ ],
345
+ user_request=prompt,
346
+ workflow_depth=workflow_depth,
258
347
  )
259
- return response.content
348
+ return result.content
260
349
  if node.type == "merge":
261
350
  upstream = upstream_outputs(incoming_edges, outputs)
262
351
  if node_data_text(node, "merge_strategy") == "json":
@@ -264,6 +353,8 @@ async def run_node(
264
353
  return "\n".join(output for output in upstream if output)
265
354
  if node.type == "code":
266
355
  return await run_code_node(node, upstream_outputs(incoming_edges, outputs))
356
+ if node.type == "timer":
357
+ return timer_payload(node)
267
358
  if node.type == "output":
268
359
  return joined_upstream_outputs(incoming_edges, outputs)
269
360
  raise ValueError("Node type is not supported.")
@@ -313,6 +404,10 @@ def node_output_key(node: StoredWorkflowNode) -> str:
313
404
  return node_data_text(node, "output_key") or node.id
314
405
 
315
406
 
407
+ def timer_payload(node: StoredWorkflowNode) -> str:
408
+ return node_data_text(node, "payload") or "Timer fired."
409
+
410
+
316
411
  def upstream_outputs(
317
412
  incoming_edges: list[StoredWorkflowEdge],
318
413
  outputs: Mapping[str, str],
@@ -10,14 +10,13 @@ from uuid import uuid4
10
10
 
11
11
  from fastapi import HTTPException
12
12
 
13
- from flowent.agent import AgentContextUpdate, run_agent_stream
14
- from flowent.approval import ApprovalReviewRequest, review_approval_request
13
+ from flowent.agent import AgentContextUpdate
14
+ from flowent.agent_runtime import FlowentAgentRuntime
15
15
  from flowent.compact import CompactInput, CompactProvider
16
16
  from flowent.context import runtime_context_messages
17
17
  from flowent.llm import ChatMessage, CompletionCallable, ProviderConnection
18
18
  from flowent.logging import TRACE_LEVEL
19
19
  from flowent.mcp import McpManager
20
- from flowent.permissions import run_tool_with_path_permissions
21
20
  from flowent.provider_connections import selected_connection
22
21
  from flowent.skills import explicit_skill_messages
23
22
  from flowent.storage import (
@@ -27,7 +26,7 @@ from flowent.storage import (
27
26
  StoredState,
28
27
  StoredToolItem,
29
28
  )
30
- from flowent.tools import ToolContext, ToolResult, text_tool_result, tool_specs
29
+ from flowent.tools import text_tool_result
31
30
  from flowent.usage import (
32
31
  TokenUsage,
33
32
  TokenUsageInfo,
@@ -37,11 +36,6 @@ from flowent.usage import (
37
36
  recompute_context_usage,
38
37
  )
39
38
  from flowent.workflow_service import WorkflowService
40
- from flowent.workflow_tools import (
41
- WorkflowAgentTools,
42
- workflow_tool_specs,
43
- workflow_tool_title,
44
- )
45
39
  from flowent.workspace.context import (
46
40
  COMPACTED_CONTEXT_MARKER,
47
41
  OPTIMIZED_CONTEXT_MARKER,
@@ -97,38 +91,27 @@ class WorkspaceRuntime:
97
91
  self.chat_completion = chat_completion
98
92
  self.compact_provider = compact_provider
99
93
  self.cwd = cwd
100
- self.mcp_manager = mcp_manager
101
94
  self.store = store
102
95
  self.workflow_service = workflow_service
96
+ self.agent_runtime = FlowentAgentRuntime(
97
+ chat_completion=chat_completion,
98
+ cwd=cwd,
99
+ mcp_manager=mcp_manager,
100
+ store=store,
101
+ workflow_service=workflow_service,
102
+ )
103
103
  self.active_response: WorkspaceResponse | None = None
104
104
  self.generation = 0
105
105
  self.active_compact_task: WorkspaceCompactTask | None = None
106
106
 
107
107
  def extra_tool_specs(self) -> list[Mapping[str, object]]:
108
- return [
109
- *workflow_tool_specs(),
110
- *list(self.mcp_manager.tool_specs()),
111
- ]
108
+ return self.agent_runtime.extra_tool_specs()
112
109
 
113
110
  def model_tool_specs(self) -> list[Mapping[str, object]]:
114
- return [*tool_specs(), *self.extra_tool_specs()]
115
-
116
- def workflow_tools(self) -> WorkflowAgentTools:
117
- return WorkflowAgentTools(self.workflow_service)
118
-
119
- async def run_extra_tool(
120
- self,
121
- workflow_tools: WorkflowAgentTools,
122
- name: str,
123
- arguments: dict[str, object],
124
- ) -> ToolResult | None:
125
- workflow_result = await workflow_tools.run_tool(name, arguments)
126
- if workflow_result is not None:
127
- return workflow_result
128
- return await self.mcp_manager.run_tool(name, arguments)
111
+ return self.agent_runtime.model_tool_specs()
129
112
 
130
113
  def extra_tool_title(self, name: str) -> str | None:
131
- return workflow_tool_title(name) or self.mcp_manager.tool_title(name)
114
+ return self.agent_runtime.extra_tool_title(name)
132
115
 
133
116
  def request_messages_for_content(
134
117
  self,
@@ -263,7 +246,6 @@ class WorkspaceRuntime:
263
246
  )
264
247
  next_messages = [*state.messages, user_message]
265
248
  self.store.save_messages(next_messages)
266
- extra_tool_specs = self.extra_tool_specs()
267
249
  model_tool_specs = self.model_tool_specs()
268
250
  model_history: list[ChatMessage | Mapping[str, object]] = [
269
251
  *runtime_context_messages(self.cwd, state.settings.agent_prompt),
@@ -297,49 +279,11 @@ class WorkspaceRuntime:
297
279
  current_output_index = 0
298
280
  latest_usage_output_index: int | None = None
299
281
 
300
- async def review_tool_approval(request: ApprovalReviewRequest):
301
- return await review_approval_request(
302
- connection,
303
- request.model_copy(
304
- update={
305
- "transcript": approval_transcript(next_messages),
306
- "user_request": content,
307
- }
308
- ),
309
- completion=self.chat_completion,
310
- )
311
-
312
- async def tool_runner(
313
- name: str,
314
- arguments: dict[str, object],
315
- context: ToolContext,
316
- ):
317
- return await run_tool_with_path_permissions(
318
- name,
319
- arguments,
320
- context,
321
- review_approval=review_tool_approval,
322
- writable_paths=[
323
- Path(path.path) for path in self.store.read_writable_paths()
324
- ],
325
- )
326
-
327
- workflow_tools = self.workflow_tools()
328
-
329
- async def extra_tool_runner(
330
- name: str, arguments: dict[str, object]
331
- ) -> ToolResult | None:
332
- return await self.run_extra_tool(workflow_tools, name, arguments)
333
-
334
- async for event in run_agent_stream(
335
- completion=self.chat_completion,
282
+ async for event in self.agent_runtime.stream(
283
+ approval_transcript=approval_transcript(next_messages),
336
284
  connection=connection,
337
- cwd=self.cwd,
338
- extra_tool_runner=extra_tool_runner,
339
- extra_tool_specs=extra_tool_specs,
340
- extra_tool_title=self.extra_tool_title,
341
285
  messages=request_messages,
342
- tool_runner=tool_runner,
286
+ user_request=content,
343
287
  ):
344
288
  if event.event == "start":
345
289
  event_id = event.data.get("id")
@@ -656,16 +600,37 @@ class WorkspaceRuntime:
656
600
  *base_request_messages,
657
601
  *model_visible_assistant_output_messages(trimmed_message),
658
602
  ]
659
- response = self._start_response_from_messages(
660
- content=previous_user_message.content,
661
- initial_assistant_message=trimmed_message,
662
- next_messages=next_messages,
663
- output_start_index=assistant_retry_output_start_index(trimmed_message),
664
- request_messages=request_messages,
665
- state=state_before_assistant,
666
- usage_request_messages=base_request_messages,
667
- user_message=previous_user_message,
668
- )
603
+ try:
604
+ response = self._start_response_from_messages(
605
+ content=previous_user_message.content,
606
+ initial_assistant_message=trimmed_message,
607
+ next_messages=next_messages,
608
+ output_start_index=assistant_retry_output_start_index(trimmed_message),
609
+ request_messages=request_messages,
610
+ state=state_before_assistant,
611
+ usage_request_messages=base_request_messages,
612
+ user_message=previous_user_message,
613
+ )
614
+ except HTTPException as error:
615
+ error_detail = str(error.detail or "")
616
+ assistant_output = AssistantOutputBuilder.from_message(trimmed_message)
617
+ assistant_output.append_error(
618
+ run_error_output_item(trimmed_message.id, error_detail).model_copy(
619
+ update={"id": error_id}
620
+ )
621
+ )
622
+ failed_message = StoredMessage(
623
+ author="assistant",
624
+ content=assistant_output.content,
625
+ groups=assistant_output.groups,
626
+ id=trimmed_message.id,
627
+ status="failed",
628
+ thinking=assistant_output.thinking,
629
+ tools=list(assistant_output.tools.values()),
630
+ usage_info=self.store.read_usage_info(),
631
+ )
632
+ self.store.save_messages([*previous_messages, failed_message])
633
+ raise
669
634
  return next_messages, response
670
635
 
671
636
  def _start_response_from_messages(
@@ -770,7 +735,6 @@ class WorkspaceRuntime:
770
735
  turn_usage_info: TokenUsageInfo | None = None
771
736
  current_output_index = 0
772
737
  latest_usage_output_index: int | None = None
773
- extra_tool_specs = self.extra_tool_specs()
774
738
  model_tool_specs = self.model_tool_specs()
775
739
  if request_messages is None:
776
740
  current_request_messages = self.request_messages_for_content(
@@ -853,33 +817,6 @@ class WorkspaceRuntime:
853
817
  else current_request_messages
854
818
  )
855
819
 
856
- async def review_tool_approval(request: ApprovalReviewRequest):
857
- return await review_approval_request(
858
- connection,
859
- request.model_copy(
860
- update={
861
- "transcript": approval_transcript(next_messages),
862
- "user_request": content,
863
- }
864
- ),
865
- completion=self.chat_completion,
866
- )
867
-
868
- async def tool_runner(
869
- name: str,
870
- arguments: dict[str, object],
871
- context: ToolContext,
872
- ):
873
- return await run_tool_with_path_permissions(
874
- name,
875
- arguments,
876
- context,
877
- review_approval=review_tool_approval,
878
- writable_paths=[
879
- Path(path.path) for path in self.store.read_writable_paths()
880
- ],
881
- )
882
-
883
820
  async def context_compactor(
884
821
  conversation: Sequence[Mapping[str, object]],
885
822
  ) -> AgentContextUpdate | None:
@@ -926,23 +863,12 @@ class WorkspaceRuntime:
926
863
  },
927
864
  )
928
865
 
929
- workflow_tools = self.workflow_tools()
930
-
931
- async def extra_tool_runner(
932
- name: str, arguments: dict[str, object]
933
- ) -> ToolResult | None:
934
- return await self.run_extra_tool(workflow_tools, name, arguments)
935
-
936
- async for event in run_agent_stream(
937
- completion=self.chat_completion,
866
+ async for event in self.agent_runtime.stream(
867
+ approval_transcript=approval_transcript(next_messages),
938
868
  connection=connection,
939
869
  context_compactor=context_compactor,
940
- cwd=self.cwd,
941
- extra_tool_runner=extra_tool_runner,
942
- extra_tool_specs=extra_tool_specs,
943
- extra_tool_title=self.extra_tool_title,
944
870
  messages=current_request_messages,
945
- tool_runner=tool_runner,
871
+ user_request=content,
946
872
  ):
947
873
  if not is_current_generation() or response.discard_on_cancel:
948
874
  raise asyncio.CancelledError
package/backend/uv.lock CHANGED
@@ -701,7 +701,7 @@ wheels = [
701
701
 
702
702
  [[package]]
703
703
  name = "flowent"
704
- version = "0.3.3"
704
+ version = "0.3.4"
705
705
  source = { editable = "." }
706
706
  dependencies = [
707
707
  { name = "fastapi", extra = ["standard"] },