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.
- package/backend/pyproject.toml +1 -1
- package/backend/src/flowent/agent.py +22 -15
- package/backend/src/flowent/api_models.py +13 -8
- package/backend/src/flowent/llm.py +50 -6
- package/backend/src/flowent/mcp.py +4 -3
- package/backend/src/flowent/permissions.py +51 -38
- package/backend/src/flowent/routes/providers.py +33 -10
- package/backend/src/flowent/routes/system.py +5 -6
- package/backend/src/flowent/routes/workspace.py +33 -23
- package/backend/src/flowent/state/models.py +4 -4
- package/backend/src/flowent/state/schema.py +121 -0
- package/backend/src/flowent/state/store.py +9 -3
- package/backend/src/flowent/static/assets/index-BX18a4Jz.js +100 -0
- package/backend/src/flowent/static/assets/index-EC37agAH.css +2 -0
- package/backend/src/flowent/static/index.html +2 -2
- package/backend/src/flowent/tools.py +84 -33
- package/backend/src/flowent/usage.py +66 -0
- package/backend/src/flowent/workspace/context.py +140 -47
- package/backend/src/flowent/workspace/events.py +5 -7
- package/backend/src/flowent/workspace/output.py +129 -4
- package/backend/src/flowent/workspace/runtime.py +393 -185
- package/backend/uv.lock +1 -1
- package/dist/frontend/assets/index-BX18a4Jz.js +100 -0
- package/dist/frontend/assets/index-EC37agAH.css +2 -0
- package/dist/frontend/index.html +2 -2
- package/package.json +8 -10
- package/backend/src/flowent/static/assets/index-CvWZZMtK.css +0 -2
- package/backend/src/flowent/static/assets/index-ma2v8oW7.js +0 -90
- package/dist/frontend/assets/index-CvWZZMtK.css +0 -2
- package/dist/frontend/assets/index-ma2v8oW7.js +0 -90
package/backend/pyproject.toml
CHANGED
|
@@ -25,6 +25,8 @@ from flowent.tools import (
|
|
|
25
25
|
new_tool_item,
|
|
26
26
|
parse_tool_arguments,
|
|
27
27
|
run_tool_async,
|
|
28
|
+
text_tool_result,
|
|
29
|
+
tool_result_model_content,
|
|
28
30
|
tool_specs,
|
|
29
31
|
)
|
|
30
32
|
|
|
@@ -277,7 +279,12 @@ async def run_agent_stream(
|
|
|
277
279
|
arguments = parse_tool_arguments(tool_call.arguments)
|
|
278
280
|
except Exception as error:
|
|
279
281
|
arguments = {}
|
|
280
|
-
|
|
282
|
+
result = ToolResult(
|
|
283
|
+
result=text_tool_result(str(error)),
|
|
284
|
+
ok=False,
|
|
285
|
+
title=tool_call.name or "Tool failed",
|
|
286
|
+
)
|
|
287
|
+
result_content = tool_result_model_content(result)
|
|
281
288
|
tool_item = new_tool_item(tool_call.name, arguments)
|
|
282
289
|
logger.debug("Tool call argument parse failed name=%s", tool_call.name)
|
|
283
290
|
logger.log(TRACE_LEVEL, "Tool start item=%r", tool_item)
|
|
@@ -292,10 +299,9 @@ async def run_agent_stream(
|
|
|
292
299
|
event="tool_error",
|
|
293
300
|
data={
|
|
294
301
|
"id": tool_item["id"],
|
|
295
|
-
"
|
|
296
|
-
"data": {},
|
|
302
|
+
"result": result.result,
|
|
297
303
|
"status": "failed",
|
|
298
|
-
"title":
|
|
304
|
+
"title": result.title,
|
|
299
305
|
},
|
|
300
306
|
)
|
|
301
307
|
else:
|
|
@@ -314,10 +320,12 @@ async def run_agent_stream(
|
|
|
314
320
|
if extra_tool_runner is not None
|
|
315
321
|
else None
|
|
316
322
|
)
|
|
317
|
-
|
|
318
|
-
|
|
323
|
+
tool_result: ToolResult | None = (
|
|
324
|
+
extra_result if isinstance(extra_result, ToolResult) else None
|
|
325
|
+
)
|
|
326
|
+
if tool_result is None:
|
|
319
327
|
context = ToolContext(cwd=cwd, web_searcher=web_searcher)
|
|
320
|
-
|
|
328
|
+
tool_result = await (
|
|
321
329
|
tool_runner(
|
|
322
330
|
tool_call.name,
|
|
323
331
|
arguments,
|
|
@@ -330,27 +338,26 @@ async def run_agent_stream(
|
|
|
330
338
|
context,
|
|
331
339
|
)
|
|
332
340
|
)
|
|
333
|
-
result_content =
|
|
341
|
+
result_content = tool_result_model_content(tool_result)
|
|
334
342
|
logger.debug(
|
|
335
343
|
"Tool call finished name=%s id=%s ok=%s",
|
|
336
344
|
tool_call.name,
|
|
337
345
|
tool_item["id"],
|
|
338
|
-
|
|
346
|
+
tool_result.ok,
|
|
339
347
|
)
|
|
340
348
|
logger.log(
|
|
341
349
|
TRACE_LEVEL,
|
|
342
350
|
"Tool result id=%s result=%r",
|
|
343
351
|
tool_item["id"],
|
|
344
|
-
|
|
352
|
+
tool_result.model_dump(),
|
|
345
353
|
)
|
|
346
354
|
yield AgentStreamEvent(
|
|
347
|
-
event="tool_done" if
|
|
355
|
+
event="tool_done" if tool_result.ok else "tool_error",
|
|
348
356
|
data={
|
|
349
357
|
"id": tool_item["id"],
|
|
350
|
-
"
|
|
351
|
-
"
|
|
352
|
-
"
|
|
353
|
-
"title": result.title,
|
|
358
|
+
"result": tool_result.result,
|
|
359
|
+
"status": "success" if tool_result.ok else "failed",
|
|
360
|
+
"title": tool_result.title,
|
|
354
361
|
},
|
|
355
362
|
)
|
|
356
363
|
conversation.append(tool_result_message(tool_call_id, result_content))
|
|
@@ -19,6 +19,18 @@ class ProviderModelsResponse(BaseModel):
|
|
|
19
19
|
models: list[str]
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
class ProviderModelsFailureResponse(BaseModel):
|
|
23
|
+
model_config = ConfigDict(extra="forbid")
|
|
24
|
+
|
|
25
|
+
code: Literal[
|
|
26
|
+
"connection_failed",
|
|
27
|
+
"access_denied",
|
|
28
|
+
"rate_limited",
|
|
29
|
+
"provider_unavailable",
|
|
30
|
+
"request_failed",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
22
34
|
class WorkspaceMessagesRequest(BaseModel):
|
|
23
35
|
model_config = ConfigDict(extra="forbid")
|
|
24
36
|
|
|
@@ -35,8 +47,8 @@ class WorkspaceMessageEditRequest(BaseModel):
|
|
|
35
47
|
class WorkspaceMessageEditResponse(BaseModel):
|
|
36
48
|
model_config = ConfigDict(extra="forbid")
|
|
37
49
|
|
|
50
|
+
is_responding: bool = False
|
|
38
51
|
messages: list[StoredMessage]
|
|
39
|
-
run_id: str | None = None
|
|
40
52
|
|
|
41
53
|
|
|
42
54
|
class WorkspaceRespondRequest(BaseModel):
|
|
@@ -46,16 +58,9 @@ class WorkspaceRespondRequest(BaseModel):
|
|
|
46
58
|
message_id: str | None = None
|
|
47
59
|
|
|
48
60
|
|
|
49
|
-
class WorkspaceRunResponse(BaseModel):
|
|
50
|
-
model_config = ConfigDict(extra="forbid")
|
|
51
|
-
|
|
52
|
-
run_id: str
|
|
53
|
-
|
|
54
|
-
|
|
55
61
|
class WorkspaceClearResponse(BaseModel):
|
|
56
62
|
model_config = ConfigDict(extra="forbid")
|
|
57
63
|
|
|
58
|
-
active_run_id: str | None = None
|
|
59
64
|
messages: list[StoredMessage]
|
|
60
65
|
usage_info: TokenUsageInfo | None = None
|
|
61
66
|
|
|
@@ -85,6 +85,20 @@ class LLMStreamError(RuntimeError):
|
|
|
85
85
|
pass
|
|
86
86
|
|
|
87
87
|
|
|
88
|
+
class ProviderModelFetchFailure(StrEnum):
|
|
89
|
+
CONNECTION_FAILED = "connection_failed"
|
|
90
|
+
ACCESS_DENIED = "access_denied"
|
|
91
|
+
RATE_LIMITED = "rate_limited"
|
|
92
|
+
PROVIDER_UNAVAILABLE = "provider_unavailable"
|
|
93
|
+
REQUEST_FAILED = "request_failed"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ProviderModelFetchError(RuntimeError):
|
|
97
|
+
def __init__(self, failure: ProviderModelFetchFailure) -> None:
|
|
98
|
+
super().__init__(failure.value)
|
|
99
|
+
self.failure = failure
|
|
100
|
+
|
|
101
|
+
|
|
88
102
|
async def wait_before_llm_retry(attempt_number: int) -> None:
|
|
89
103
|
await asyncio.sleep(LLM_RETRY_BASE_DELAY_SECONDS * attempt_number)
|
|
90
104
|
|
|
@@ -248,6 +262,31 @@ def unique_model_names(provider: ProviderFormat, models: Sequence[str]) -> list[
|
|
|
248
262
|
return normalized_models
|
|
249
263
|
|
|
250
264
|
|
|
265
|
+
def classify_provider_model_fetch_failure(
|
|
266
|
+
exception: Exception,
|
|
267
|
+
) -> ProviderModelFetchFailure:
|
|
268
|
+
try:
|
|
269
|
+
import litellm
|
|
270
|
+
except Exception:
|
|
271
|
+
return ProviderModelFetchFailure.REQUEST_FAILED
|
|
272
|
+
|
|
273
|
+
if isinstance(exception, (litellm.APIConnectionError, litellm.Timeout)):
|
|
274
|
+
return ProviderModelFetchFailure.CONNECTION_FAILED
|
|
275
|
+
if isinstance(
|
|
276
|
+
exception, (litellm.AuthenticationError, litellm.PermissionDeniedError)
|
|
277
|
+
):
|
|
278
|
+
return ProviderModelFetchFailure.ACCESS_DENIED
|
|
279
|
+
if isinstance(exception, litellm.RateLimitError):
|
|
280
|
+
return ProviderModelFetchFailure.RATE_LIMITED
|
|
281
|
+
if isinstance(
|
|
282
|
+
exception, (litellm.ServiceUnavailableError, litellm.InternalServerError)
|
|
283
|
+
):
|
|
284
|
+
return ProviderModelFetchFailure.PROVIDER_UNAVAILABLE
|
|
285
|
+
if isinstance(exception, (ConnectionError, TimeoutError)):
|
|
286
|
+
return ProviderModelFetchFailure.CONNECTION_FAILED
|
|
287
|
+
return ProviderModelFetchFailure.REQUEST_FAILED
|
|
288
|
+
|
|
289
|
+
|
|
251
290
|
def list_provider_models(
|
|
252
291
|
*,
|
|
253
292
|
provider: ProviderFormat,
|
|
@@ -261,12 +300,17 @@ def list_provider_models(
|
|
|
261
300
|
configure_litellm_logging()
|
|
262
301
|
model_lister = get_valid_models
|
|
263
302
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
303
|
+
try:
|
|
304
|
+
models = model_lister(
|
|
305
|
+
api_base=normalize_provider_base_url(provider, base_url),
|
|
306
|
+
api_key=secret_reference,
|
|
307
|
+
check_provider_endpoint=True,
|
|
308
|
+
custom_llm_provider=provider_litellm_name(provider),
|
|
309
|
+
)
|
|
310
|
+
except Exception as exc:
|
|
311
|
+
raise ProviderModelFetchError(
|
|
312
|
+
classify_provider_model_fetch_failure(exc)
|
|
313
|
+
) from exc
|
|
270
314
|
return unique_model_names(provider, models)
|
|
271
315
|
|
|
272
316
|
|
|
@@ -662,11 +662,12 @@ class McpManager:
|
|
|
662
662
|
content = mcp_result_content(result)
|
|
663
663
|
server_name = self._server_names.get(server_id, server_id)
|
|
664
664
|
return ToolResult(
|
|
665
|
-
|
|
666
|
-
|
|
665
|
+
result={
|
|
666
|
+
"type": "mcp",
|
|
667
|
+
"output": content,
|
|
667
668
|
"server": server_name,
|
|
668
669
|
"tool": tool_name,
|
|
669
|
-
"
|
|
670
|
+
"raw_result": result,
|
|
670
671
|
},
|
|
671
672
|
ok=not mcp_result_is_error(result),
|
|
672
673
|
title=f"Calling {server_name}.{tool_name}",
|
|
@@ -16,10 +16,13 @@ from flowent.shell import shell_invocation
|
|
|
16
16
|
from flowent.tools import (
|
|
17
17
|
ToolContext,
|
|
18
18
|
ToolResult,
|
|
19
|
+
command_tool_result,
|
|
19
20
|
number_argument,
|
|
20
21
|
patch_title_from_result,
|
|
21
22
|
run_tool_async,
|
|
23
|
+
text_tool_result,
|
|
22
24
|
tool_failure_content,
|
|
25
|
+
tool_result_model_content,
|
|
23
26
|
)
|
|
24
27
|
|
|
25
28
|
SANDBOX_WITH_ADDITIONAL_PERMISSIONS = "with_additional_permissions"
|
|
@@ -66,7 +69,7 @@ def validate_additional_permissions(arguments: dict[str, object]) -> ToolResult
|
|
|
66
69
|
return None
|
|
67
70
|
if sandbox_permissions != SANDBOX_WITH_ADDITIONAL_PERMISSIONS:
|
|
68
71
|
return ToolResult(
|
|
69
|
-
|
|
72
|
+
result=text_tool_result(
|
|
70
73
|
"additional_permissions requires sandbox_permissions to be "
|
|
71
74
|
"with_additional_permissions."
|
|
72
75
|
),
|
|
@@ -130,8 +133,10 @@ async def review_missing_write_paths(
|
|
|
130
133
|
return (
|
|
131
134
|
effective_paths,
|
|
132
135
|
ToolResult(
|
|
133
|
-
|
|
134
|
-
|
|
136
|
+
result=text_tool_result(
|
|
137
|
+
approval_denial_content(decision),
|
|
138
|
+
**review_data,
|
|
139
|
+
),
|
|
135
140
|
ok=False,
|
|
136
141
|
title="Denied by reviewer",
|
|
137
142
|
),
|
|
@@ -196,7 +201,7 @@ async def run_shell_command_with_permissions(
|
|
|
196
201
|
arguments, context, effective_paths
|
|
197
202
|
)
|
|
198
203
|
if approval_data is not None:
|
|
199
|
-
result =
|
|
204
|
+
result = tool_result_with_fields(result, approval_data)
|
|
200
205
|
if result.ok or not is_likely_sandbox_denied_result(result):
|
|
201
206
|
return result
|
|
202
207
|
review_request = ApprovalReviewRequest(
|
|
@@ -210,13 +215,16 @@ async def run_shell_command_with_permissions(
|
|
|
210
215
|
review_data = approval_result_data(review_request, decision)
|
|
211
216
|
if decision.decision == "denied":
|
|
212
217
|
return ToolResult(
|
|
213
|
-
|
|
214
|
-
|
|
218
|
+
result=text_tool_result(
|
|
219
|
+
approval_denial_content(decision),
|
|
220
|
+
previous_result=result.result,
|
|
221
|
+
**review_data,
|
|
222
|
+
),
|
|
215
223
|
ok=False,
|
|
216
224
|
title="Denied by reviewer",
|
|
217
225
|
)
|
|
218
226
|
retry_result = await shell_command_without_sandbox(arguments, context)
|
|
219
|
-
return
|
|
227
|
+
return tool_result_with_fields(retry_result, review_data)
|
|
220
228
|
|
|
221
229
|
|
|
222
230
|
async def run_apply_patch_with_permissions(
|
|
@@ -244,7 +252,7 @@ async def run_apply_patch_with_permissions(
|
|
|
244
252
|
|
|
245
253
|
result = await apply_patch_with_writable_paths(arguments, context, effective_paths)
|
|
246
254
|
if approval_data is not None:
|
|
247
|
-
result =
|
|
255
|
+
result = tool_result_with_fields(result, approval_data)
|
|
248
256
|
return result
|
|
249
257
|
|
|
250
258
|
|
|
@@ -268,18 +276,23 @@ async def apply_patch_with_writable_paths(
|
|
|
268
276
|
input_text=patch,
|
|
269
277
|
)
|
|
270
278
|
except SandboxError as error:
|
|
271
|
-
return ToolResult(
|
|
279
|
+
return ToolResult(
|
|
280
|
+
result=text_tool_result(str(error)), ok=False, title="Edit failed"
|
|
281
|
+
)
|
|
272
282
|
|
|
273
283
|
if result.exit_code != 0:
|
|
274
284
|
return ToolResult(
|
|
275
|
-
|
|
285
|
+
result=text_tool_result(tool_failure_content(result)),
|
|
276
286
|
ok=False,
|
|
277
287
|
title="Edit failed",
|
|
278
288
|
)
|
|
279
289
|
data = json.loads(result.stdout or "{}")
|
|
280
290
|
return ToolResult(
|
|
281
|
-
|
|
282
|
-
|
|
291
|
+
result={
|
|
292
|
+
"type": "patch",
|
|
293
|
+
"output": result.stdout,
|
|
294
|
+
**(data if isinstance(data, dict) else {}),
|
|
295
|
+
},
|
|
283
296
|
title=patch_title_from_result(data),
|
|
284
297
|
)
|
|
285
298
|
|
|
@@ -297,27 +310,25 @@ async def shell_command_with_writable_paths(
|
|
|
297
310
|
writable_roots=writable_paths,
|
|
298
311
|
).run_async(invocation.args, env=invocation.env, timeout_seconds=timeout_seconds)
|
|
299
312
|
ok = result.exit_code == 0
|
|
300
|
-
content = result.stdout or result.stderr
|
|
301
313
|
return ToolResult(
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
},
|
|
314
|
+
result=command_tool_result(
|
|
315
|
+
command=command,
|
|
316
|
+
exit_code=result.exit_code,
|
|
317
|
+
stderr=result.stderr,
|
|
318
|
+
stdout=result.stdout,
|
|
319
|
+
),
|
|
309
320
|
ok=ok,
|
|
310
321
|
title=f"Ran {command}",
|
|
311
322
|
)
|
|
312
323
|
|
|
313
324
|
|
|
314
325
|
def is_likely_sandbox_denied_result(result: ToolResult) -> bool:
|
|
315
|
-
|
|
316
|
-
exit_code = int_result_field(
|
|
326
|
+
payload = result.result
|
|
327
|
+
exit_code = int_result_field(payload.get("exit_code"))
|
|
317
328
|
if exit_code == 0:
|
|
318
329
|
return False
|
|
319
330
|
output = "\n".join(
|
|
320
|
-
str(
|
|
331
|
+
str(payload.get(name, "") or "") for name in ["stderr", "stdout", "output"]
|
|
321
332
|
).lower()
|
|
322
333
|
return any(
|
|
323
334
|
keyword in output
|
|
@@ -345,13 +356,17 @@ def int_result_field(value: object) -> int:
|
|
|
345
356
|
|
|
346
357
|
|
|
347
358
|
def tool_failure_text(result: ToolResult) -> str:
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
359
|
+
payload = result.result
|
|
360
|
+
stderr = str(payload.get("stderr", "") or "").strip()
|
|
361
|
+
stdout = str(payload.get("stdout", "") or "").strip()
|
|
351
362
|
parts: list[str] = []
|
|
352
|
-
for part in [stderr, stdout
|
|
363
|
+
for part in [stderr, stdout]:
|
|
353
364
|
if part and part not in parts:
|
|
354
365
|
parts.append(part)
|
|
366
|
+
if not parts:
|
|
367
|
+
content = tool_result_model_content(result).strip()
|
|
368
|
+
if content:
|
|
369
|
+
parts.append(content)
|
|
355
370
|
return "\n".join(parts)
|
|
356
371
|
|
|
357
372
|
|
|
@@ -366,15 +381,13 @@ async def shell_command_without_sandbox(
|
|
|
366
381
|
invocation.args, env=invocation.env, timeout_seconds=timeout_seconds
|
|
367
382
|
)
|
|
368
383
|
ok = result.exit_code == 0
|
|
369
|
-
content = result.stdout or result.stderr
|
|
370
384
|
return ToolResult(
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
},
|
|
385
|
+
result=command_tool_result(
|
|
386
|
+
command=command,
|
|
387
|
+
exit_code=result.exit_code,
|
|
388
|
+
stderr=result.stderr,
|
|
389
|
+
stdout=result.stdout,
|
|
390
|
+
),
|
|
378
391
|
ok=ok,
|
|
379
392
|
title=f"Ran {command}",
|
|
380
393
|
)
|
|
@@ -403,7 +416,7 @@ def approval_result_data(
|
|
|
403
416
|
}
|
|
404
417
|
|
|
405
418
|
|
|
406
|
-
def
|
|
407
|
-
result: ToolResult,
|
|
419
|
+
def tool_result_with_fields(
|
|
420
|
+
result: ToolResult, extra_fields: dict[str, object]
|
|
408
421
|
) -> ToolResult:
|
|
409
|
-
return result.model_copy(update={"
|
|
422
|
+
return result.model_copy(update={"result": {**result.result, **extra_fields}})
|
|
@@ -1,9 +1,25 @@
|
|
|
1
|
-
from fastapi import FastAPI
|
|
1
|
+
from fastapi import FastAPI, HTTPException, status
|
|
2
2
|
|
|
3
|
-
from flowent.api_models import
|
|
4
|
-
|
|
3
|
+
from flowent.api_models import (
|
|
4
|
+
ProviderModelsFailureResponse,
|
|
5
|
+
ProviderModelsRequest,
|
|
6
|
+
ProviderModelsResponse,
|
|
7
|
+
)
|
|
8
|
+
from flowent.llm import (
|
|
9
|
+
ProviderModelFetchError,
|
|
10
|
+
ProviderModelFetchFailure,
|
|
11
|
+
list_provider_models,
|
|
12
|
+
)
|
|
5
13
|
from flowent.storage import StateStore, StoredProvider, StoredSettings
|
|
6
14
|
|
|
15
|
+
PROVIDER_MODEL_FAILURE_STATUS: dict[ProviderModelFetchFailure, int] = {
|
|
16
|
+
ProviderModelFetchFailure.CONNECTION_FAILED: status.HTTP_502_BAD_GATEWAY,
|
|
17
|
+
ProviderModelFetchFailure.ACCESS_DENIED: status.HTTP_403_FORBIDDEN,
|
|
18
|
+
ProviderModelFetchFailure.RATE_LIMITED: status.HTTP_429_TOO_MANY_REQUESTS,
|
|
19
|
+
ProviderModelFetchFailure.PROVIDER_UNAVAILABLE: status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
20
|
+
ProviderModelFetchFailure.REQUEST_FAILED: status.HTTP_400_BAD_REQUEST,
|
|
21
|
+
}
|
|
22
|
+
|
|
7
23
|
|
|
8
24
|
def register_provider_routes(app: FastAPI, *, store: StateStore) -> None:
|
|
9
25
|
@app.post("/api/providers")
|
|
@@ -17,13 +33,20 @@ def register_provider_routes(app: FastAPI, *, store: StateStore) -> None:
|
|
|
17
33
|
|
|
18
34
|
@app.post("/api/providers/models")
|
|
19
35
|
async def provider_models(request: ProviderModelsRequest) -> ProviderModelsResponse:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
36
|
+
try:
|
|
37
|
+
return ProviderModelsResponse(
|
|
38
|
+
models=list_provider_models(
|
|
39
|
+
base_url=request.base_url,
|
|
40
|
+
provider=request.provider,
|
|
41
|
+
secret_reference=request.secret_reference,
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
except ProviderModelFetchError as exc:
|
|
45
|
+
detail = ProviderModelsFailureResponse(code=exc.failure.value)
|
|
46
|
+
raise HTTPException(
|
|
47
|
+
detail=detail.model_dump(),
|
|
48
|
+
status_code=PROVIDER_MODEL_FAILURE_STATUS[exc.failure],
|
|
49
|
+
) from exc
|
|
27
50
|
|
|
28
51
|
@app.put("/api/settings")
|
|
29
52
|
async def save_settings(settings: StoredSettings) -> StoredSettings:
|
|
@@ -28,14 +28,13 @@ def register_system_routes(
|
|
|
28
28
|
@app.get("/api/state")
|
|
29
29
|
async def app_state() -> StoredState:
|
|
30
30
|
state = state_with_current_model_context_window(store.read_state())
|
|
31
|
-
|
|
31
|
+
active_response = runtime.current_response()
|
|
32
32
|
update: dict[str, object] = {
|
|
33
|
-
"
|
|
34
|
-
|
|
33
|
+
"is_responding": active_response is not None
|
|
34
|
+
and not active_response.is_done,
|
|
35
|
+
"response_event_index": active_response.latest_event_index
|
|
36
|
+
if active_response
|
|
35
37
|
else 0,
|
|
36
|
-
"active_run_id": active_run.id
|
|
37
|
-
if active_run and not active_run.is_done
|
|
38
|
-
else None,
|
|
39
38
|
"mcp_servers": mcp_manager.servers_with_status(state.mcp_servers),
|
|
40
39
|
"skills": discover_skills(cwd, store),
|
|
41
40
|
}
|
|
@@ -9,7 +9,6 @@ from flowent.api_models import (
|
|
|
9
9
|
WorkspaceMessageEditResponse,
|
|
10
10
|
WorkspaceMessagesRequest,
|
|
11
11
|
WorkspaceRespondRequest,
|
|
12
|
-
WorkspaceRunResponse,
|
|
13
12
|
)
|
|
14
13
|
from flowent.logging import TRACE_LEVEL
|
|
15
14
|
from flowent.storage import StateStore
|
|
@@ -42,45 +41,54 @@ def register_workspace_routes(
|
|
|
42
41
|
len(request.content),
|
|
43
42
|
)
|
|
44
43
|
logger.log(TRACE_LEVEL, "Workspace edited user content=%r", request.content)
|
|
45
|
-
messages,
|
|
44
|
+
messages, response = runtime.edit_message(
|
|
46
45
|
message_id,
|
|
47
46
|
action=request.action,
|
|
48
47
|
content=request.content,
|
|
49
48
|
)
|
|
50
49
|
return WorkspaceMessageEditResponse(
|
|
50
|
+
is_responding=response is not None,
|
|
51
|
+
messages=messages,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
@app.post("/api/workspace/messages/{message_id}/errors/{error_id}/retry")
|
|
55
|
+
async def retry_workspace_error(
|
|
56
|
+
message_id: str,
|
|
57
|
+
error_id: str,
|
|
58
|
+
) -> WorkspaceMessageEditResponse:
|
|
59
|
+
logger.info(
|
|
60
|
+
"Workspace error retry requested message_id=%s error_id=%s",
|
|
61
|
+
message_id,
|
|
62
|
+
error_id,
|
|
63
|
+
)
|
|
64
|
+
messages, response = runtime.retry_error(
|
|
65
|
+
message_id,
|
|
66
|
+
error_id=error_id,
|
|
67
|
+
)
|
|
68
|
+
return WorkspaceMessageEditResponse(
|
|
69
|
+
is_responding=response is not None,
|
|
51
70
|
messages=messages,
|
|
52
|
-
run_id=run.id if run else None,
|
|
53
71
|
)
|
|
54
72
|
|
|
55
73
|
@app.post("/api/workspace/clear")
|
|
56
74
|
async def clear_workspace() -> WorkspaceClearResponse:
|
|
57
75
|
messages = runtime.clear()
|
|
58
|
-
await runtime.
|
|
76
|
+
await runtime.notify_cleared_response()
|
|
59
77
|
return WorkspaceClearResponse(messages=messages)
|
|
60
78
|
|
|
61
|
-
@app.
|
|
62
|
-
async def
|
|
63
|
-
request: WorkspaceRespondRequest,
|
|
64
|
-
) -> WorkspaceRunResponse:
|
|
65
|
-
logger.info("Workspace run requested content_length=%s", len(request.content))
|
|
66
|
-
logger.log(TRACE_LEVEL, "Workspace user content=%r", request.content)
|
|
67
|
-
run = runtime.create_run(request.content, message_id=request.message_id)
|
|
68
|
-
return WorkspaceRunResponse(run_id=run.id)
|
|
69
|
-
|
|
70
|
-
@app.get("/api/workspace/runs/{run_id}/stream")
|
|
71
|
-
async def stream_workspace_run(
|
|
72
|
-
run_id: str,
|
|
79
|
+
@app.get("/api/workspace/stream")
|
|
80
|
+
async def stream_workspace_response(
|
|
73
81
|
after: int = Query(default=0, ge=0),
|
|
74
82
|
) -> StreamingResponse:
|
|
75
|
-
|
|
83
|
+
response = runtime.stream_current_response()
|
|
76
84
|
return StreamingResponse(
|
|
77
|
-
runtime.
|
|
85
|
+
runtime.response_stream(response, after),
|
|
78
86
|
media_type="text/event-stream",
|
|
79
87
|
)
|
|
80
88
|
|
|
81
|
-
@app.post("/api/workspace/
|
|
82
|
-
async def
|
|
83
|
-
runtime.
|
|
89
|
+
@app.post("/api/workspace/stop")
|
|
90
|
+
async def stop_workspace_response() -> dict[str, bool]:
|
|
91
|
+
runtime.stop_response()
|
|
84
92
|
return {"ok": True}
|
|
85
93
|
|
|
86
94
|
@app.post("/api/workspace/compact", response_class=StreamingResponse)
|
|
@@ -98,8 +106,10 @@ def register_workspace_routes(
|
|
|
98
106
|
"Workspace response requested content_length=%s", len(request.content)
|
|
99
107
|
)
|
|
100
108
|
logger.log(TRACE_LEVEL, "Workspace user content=%r", request.content)
|
|
101
|
-
|
|
109
|
+
response = runtime.start_response(
|
|
110
|
+
request.content, message_id=request.message_id
|
|
111
|
+
)
|
|
102
112
|
return StreamingResponse(
|
|
103
|
-
runtime.
|
|
113
|
+
runtime.response_stream(response, include_snapshots=False),
|
|
104
114
|
media_type="text/event-stream",
|
|
105
115
|
)
|
|
@@ -151,8 +151,7 @@ class StoredToolItem(BaseModel):
|
|
|
151
151
|
status: str
|
|
152
152
|
title: str
|
|
153
153
|
arguments: dict[str, object] | None = None
|
|
154
|
-
|
|
155
|
-
data: dict[str, object] | None = None
|
|
154
|
+
result: dict[str, object] | None = None
|
|
156
155
|
|
|
157
156
|
|
|
158
157
|
class StoredThinkingOutputItem(BaseModel):
|
|
@@ -217,6 +216,7 @@ class StoredMessage(BaseModel):
|
|
|
217
216
|
status: str = Field(
|
|
218
217
|
default="completed", exclude_if=lambda value: value == "completed"
|
|
219
218
|
)
|
|
219
|
+
summary: str = Field(default="", exclude_if=lambda value: value == "")
|
|
220
220
|
thinking: str = Field(default="", exclude_if=lambda value: value == "")
|
|
221
221
|
tools: list[StoredToolItem] = Field(default_factory=list)
|
|
222
222
|
usage_info: TokenUsageInfo | None = Field(
|
|
@@ -241,8 +241,8 @@ class StoredCompactionCheckpoint(BaseModel):
|
|
|
241
241
|
class StoredState(BaseModel):
|
|
242
242
|
model_config = ConfigDict(extra="forbid")
|
|
243
243
|
|
|
244
|
-
|
|
245
|
-
|
|
244
|
+
is_responding: bool = False
|
|
245
|
+
response_event_index: int = 0
|
|
246
246
|
is_compacting: bool = False
|
|
247
247
|
mcp_servers: list[StoredMcpServer]
|
|
248
248
|
messages: list[StoredMessage]
|