flowent 0.3.1 → 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/mcp.py +4 -3
- package/backend/src/flowent/permissions.py +51 -38
- package/backend/src/flowent/state/models.py +1 -2
- package/backend/src/flowent/state/schema.py +116 -0
- package/backend/src/flowent/static/assets/{index-BaZmIi2Y.js → index-BX18a4Jz.js} +9 -7
- package/backend/src/flowent/static/index.html +1 -1
- package/backend/src/flowent/tools.py +84 -33
- package/backend/src/flowent/usage.py +66 -0
- package/backend/src/flowent/workspace/context.py +14 -7
- package/backend/src/flowent/workspace/output.py +4 -1
- package/backend/src/flowent/workspace/runtime.py +94 -5
- package/backend/uv.lock +1 -1
- package/dist/frontend/assets/{index-BaZmIi2Y.js → index-BX18a4Jz.js} +9 -7
- package/dist/frontend/index.html +1 -1
- package/package.json +8 -10
|
@@ -11,7 +11,7 @@ from dataclasses import dataclass
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from uuid import uuid4
|
|
13
13
|
|
|
14
|
-
from pydantic import BaseModel, ConfigDict
|
|
14
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
15
15
|
|
|
16
16
|
from flowent.network import flowent_user_agent
|
|
17
17
|
from flowent.patch import affected_paths
|
|
@@ -23,8 +23,7 @@ from flowent.system_tools import ensure_ripgrep_available
|
|
|
23
23
|
class ToolResult(BaseModel):
|
|
24
24
|
model_config = ConfigDict(extra="forbid")
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
data: dict[str, object] = {}
|
|
26
|
+
result: dict[str, object] = Field(default_factory=dict)
|
|
28
27
|
ok: bool = True
|
|
29
28
|
title: str
|
|
30
29
|
|
|
@@ -35,6 +34,46 @@ class ToolContext:
|
|
|
35
34
|
web_searcher: Callable[[str], Sequence[dict[str, str]]] | None = None
|
|
36
35
|
|
|
37
36
|
|
|
37
|
+
def text_tool_result(text: str, **metadata: object) -> dict[str, object]:
|
|
38
|
+
return {"type": "text", "text": text, **metadata}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def command_tool_result(
|
|
42
|
+
*,
|
|
43
|
+
command: str,
|
|
44
|
+
exit_code: int,
|
|
45
|
+
stderr: str,
|
|
46
|
+
stdout: str,
|
|
47
|
+
) -> dict[str, object]:
|
|
48
|
+
return {
|
|
49
|
+
"type": "command",
|
|
50
|
+
"command": command,
|
|
51
|
+
"exit_code": exit_code,
|
|
52
|
+
"stderr": stderr,
|
|
53
|
+
"stdout": stdout,
|
|
54
|
+
"output": stdout or stderr,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def tool_result_model_content(result: ToolResult | dict[str, object]) -> str:
|
|
59
|
+
payload = result.result if isinstance(result, ToolResult) else result
|
|
60
|
+
result_type = payload.get("type")
|
|
61
|
+
if result_type == "command":
|
|
62
|
+
output = str(payload.get("output") or "")
|
|
63
|
+
metadata: dict[str, object] = {}
|
|
64
|
+
if "exit_code" in payload:
|
|
65
|
+
metadata["exit_code"] = payload["exit_code"]
|
|
66
|
+
return json.dumps(
|
|
67
|
+
{"output": output, "metadata": metadata},
|
|
68
|
+
ensure_ascii=False,
|
|
69
|
+
)
|
|
70
|
+
for key in ("text", "output"):
|
|
71
|
+
value = payload.get(key)
|
|
72
|
+
if value is not None:
|
|
73
|
+
return str(value)
|
|
74
|
+
return json.dumps(payload, ensure_ascii=False)
|
|
75
|
+
|
|
76
|
+
|
|
38
77
|
def tool_specs() -> list[dict[str, object]]:
|
|
39
78
|
return [
|
|
40
79
|
{
|
|
@@ -222,7 +261,7 @@ def run_tool(
|
|
|
222
261
|
title = (
|
|
223
262
|
"Edit failed" if name == "apply_patch" else tool_call_title(name, arguments)
|
|
224
263
|
)
|
|
225
|
-
return ToolResult(
|
|
264
|
+
return ToolResult(result=text_tool_result(str(error)), ok=False, title=title)
|
|
226
265
|
|
|
227
266
|
|
|
228
267
|
async def run_tool_async(
|
|
@@ -238,7 +277,7 @@ async def run_tool_async(
|
|
|
238
277
|
title = (
|
|
239
278
|
"Edit failed" if name == "apply_patch" else tool_call_title(name, arguments)
|
|
240
279
|
)
|
|
241
|
-
return ToolResult(
|
|
280
|
+
return ToolResult(result=text_tool_result(str(error)), ok=False, title=title)
|
|
242
281
|
|
|
243
282
|
|
|
244
283
|
def integer_argument(arguments: dict[str, object], name: str, default: int) -> int:
|
|
@@ -266,7 +305,10 @@ def read_file(arguments: dict[str, object], context: ToolContext) -> ToolResult:
|
|
|
266
305
|
lines = path.read_text(errors="replace").splitlines()
|
|
267
306
|
selected = lines[offset : offset + limit]
|
|
268
307
|
content = "\n".join(selected)
|
|
269
|
-
return ToolResult(
|
|
308
|
+
return ToolResult(
|
|
309
|
+
result=text_tool_result(content, path=str(path)),
|
|
310
|
+
title=f"Read {path}",
|
|
311
|
+
)
|
|
270
312
|
|
|
271
313
|
|
|
272
314
|
def list_dir(arguments: dict[str, object], context: ToolContext) -> ToolResult:
|
|
@@ -279,7 +321,8 @@ def list_dir(arguments: dict[str, object], context: ToolContext) -> ToolResult:
|
|
|
279
321
|
f"{entry.name}/" if entry.is_dir() else entry.name for entry in entries[:limit]
|
|
280
322
|
]
|
|
281
323
|
return ToolResult(
|
|
282
|
-
|
|
324
|
+
result=text_tool_result("\n".join(rendered), path=str(path)),
|
|
325
|
+
title=f"Listed {path}",
|
|
283
326
|
)
|
|
284
327
|
|
|
285
328
|
|
|
@@ -297,8 +340,7 @@ def grep_files(arguments: dict[str, object], context: ToolContext) -> ToolResult
|
|
|
297
340
|
)
|
|
298
341
|
output = completed.stdout or completed.stderr
|
|
299
342
|
return ToolResult(
|
|
300
|
-
|
|
301
|
-
data={"path": str(path), "pattern": pattern},
|
|
343
|
+
result=text_tool_result(output[:20000], path=str(path), pattern=pattern),
|
|
302
344
|
title=f"Searched {pattern}",
|
|
303
345
|
)
|
|
304
346
|
|
|
@@ -317,8 +359,11 @@ def apply_patch_tool(arguments: dict[str, object], context: ToolContext) -> Tool
|
|
|
317
359
|
raise SandboxError(tool_failure_content(result))
|
|
318
360
|
data = json.loads(result.stdout or "{}")
|
|
319
361
|
return ToolResult(
|
|
320
|
-
|
|
321
|
-
|
|
362
|
+
result={
|
|
363
|
+
"type": "patch",
|
|
364
|
+
"output": result.stdout,
|
|
365
|
+
**(data if isinstance(data, dict) else {}),
|
|
366
|
+
},
|
|
322
367
|
title=patch_title_from_result(data),
|
|
323
368
|
)
|
|
324
369
|
|
|
@@ -339,8 +384,11 @@ async def apply_patch_tool_async(
|
|
|
339
384
|
raise SandboxError(tool_failure_content(result))
|
|
340
385
|
data = json.loads(result.stdout or "{}")
|
|
341
386
|
return ToolResult(
|
|
342
|
-
|
|
343
|
-
|
|
387
|
+
result={
|
|
388
|
+
"type": "patch",
|
|
389
|
+
"output": result.stdout,
|
|
390
|
+
**(data if isinstance(data, dict) else {}),
|
|
391
|
+
},
|
|
344
392
|
title=patch_title_from_result(data),
|
|
345
393
|
)
|
|
346
394
|
|
|
@@ -388,15 +436,13 @@ def shell_command(arguments: dict[str, object], context: ToolContext) -> ToolRes
|
|
|
388
436
|
invocation.args, env=invocation.env, timeout_seconds=timeout_seconds
|
|
389
437
|
)
|
|
390
438
|
ok = result.exit_code == 0
|
|
391
|
-
content = result.stdout or result.stderr
|
|
392
439
|
return ToolResult(
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
},
|
|
440
|
+
result=command_tool_result(
|
|
441
|
+
command=command,
|
|
442
|
+
exit_code=result.exit_code,
|
|
443
|
+
stderr=result.stderr,
|
|
444
|
+
stdout=result.stdout,
|
|
445
|
+
),
|
|
400
446
|
ok=ok,
|
|
401
447
|
title=f"Ran {command}",
|
|
402
448
|
)
|
|
@@ -412,15 +458,13 @@ async def shell_command_async(
|
|
|
412
458
|
invocation.args, env=invocation.env, timeout_seconds=timeout_seconds
|
|
413
459
|
)
|
|
414
460
|
ok = result.exit_code == 0
|
|
415
|
-
content = result.stdout or result.stderr
|
|
416
461
|
return ToolResult(
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
},
|
|
462
|
+
result=command_tool_result(
|
|
463
|
+
command=command,
|
|
464
|
+
exit_code=result.exit_code,
|
|
465
|
+
stderr=result.stderr,
|
|
466
|
+
stdout=result.stdout,
|
|
467
|
+
),
|
|
424
468
|
ok=ok,
|
|
425
469
|
title=f"Ran {command}",
|
|
426
470
|
)
|
|
@@ -430,8 +474,11 @@ def update_plan(arguments: dict[str, object]) -> ToolResult:
|
|
|
430
474
|
items = arguments.get("items", [])
|
|
431
475
|
content = json.dumps(items, ensure_ascii=False)
|
|
432
476
|
return ToolResult(
|
|
433
|
-
|
|
434
|
-
|
|
477
|
+
result={
|
|
478
|
+
"type": "plan",
|
|
479
|
+
"items": items if isinstance(items, list) else [],
|
|
480
|
+
"output": content,
|
|
481
|
+
},
|
|
435
482
|
title="Updated plan",
|
|
436
483
|
)
|
|
437
484
|
|
|
@@ -473,8 +520,12 @@ def web_search(arguments: dict[str, object], context: ToolContext) -> ToolResult
|
|
|
473
520
|
for result in results
|
|
474
521
|
)
|
|
475
522
|
return ToolResult(
|
|
476
|
-
|
|
477
|
-
|
|
523
|
+
result={
|
|
524
|
+
"type": "web_search",
|
|
525
|
+
"output": content or "No results.",
|
|
526
|
+
"query": query,
|
|
527
|
+
"results": results,
|
|
528
|
+
},
|
|
478
529
|
title=f"Searched web for {query}",
|
|
479
530
|
)
|
|
480
531
|
|
|
@@ -148,6 +148,9 @@ def current_model_context_window(model_name: str | None = None) -> int:
|
|
|
148
148
|
|
|
149
149
|
def model_context_window_for(model_name: str | None = None) -> int:
|
|
150
150
|
candidates = normalized_model_name_candidates(model_name)
|
|
151
|
+
metadata_context_window = litellm_input_context_window_for(candidates)
|
|
152
|
+
if metadata_context_window is not None:
|
|
153
|
+
return metadata_context_window
|
|
151
154
|
for candidate in candidates:
|
|
152
155
|
context_window = MODEL_CONTEXT_WINDOWS.get(candidate)
|
|
153
156
|
if context_window is not None:
|
|
@@ -159,6 +162,22 @@ def model_context_window_for(model_name: str | None = None) -> int:
|
|
|
159
162
|
return DEFAULT_MODEL_CONTEXT_WINDOW
|
|
160
163
|
|
|
161
164
|
|
|
165
|
+
def litellm_input_context_window_for(candidates: Sequence[str]) -> int | None:
|
|
166
|
+
try:
|
|
167
|
+
from litellm import model_cost
|
|
168
|
+
except Exception:
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
for candidate in candidates:
|
|
172
|
+
metadata = model_cost.get(candidate)
|
|
173
|
+
if metadata is None:
|
|
174
|
+
continue
|
|
175
|
+
context_window = first_int_value(value_at(metadata, "max_input_tokens"))
|
|
176
|
+
if context_window is not None and context_window > 0:
|
|
177
|
+
return context_window
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
|
|
162
181
|
def normalized_model_name_candidates(model_name: str | None) -> tuple[str, ...]:
|
|
163
182
|
if model_name is None:
|
|
164
183
|
return ()
|
|
@@ -261,6 +280,53 @@ def estimated_token_usage_for_messages(
|
|
|
261
280
|
)
|
|
262
281
|
|
|
263
282
|
|
|
283
|
+
def estimated_token_usage_for_request(
|
|
284
|
+
messages: Sequence[Mapping[str, object]],
|
|
285
|
+
*,
|
|
286
|
+
output_content: str = "",
|
|
287
|
+
tools: Sequence[Mapping[str, object]] = (),
|
|
288
|
+
) -> TokenUsage:
|
|
289
|
+
message_usage = estimated_token_usage_for_messages(
|
|
290
|
+
messages,
|
|
291
|
+
output_content=output_content,
|
|
292
|
+
)
|
|
293
|
+
tool_tokens = sum(
|
|
294
|
+
approximate_token_count(json.dumps(tool, ensure_ascii=False)) for tool in tools
|
|
295
|
+
)
|
|
296
|
+
input_tokens = message_usage.input_tokens + tool_tokens
|
|
297
|
+
return TokenUsage(
|
|
298
|
+
input_tokens=input_tokens,
|
|
299
|
+
output_tokens=message_usage.output_tokens,
|
|
300
|
+
total_tokens=input_tokens + message_usage.output_tokens,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def full_context_usage(
|
|
305
|
+
usage_info: TokenUsageInfo | None,
|
|
306
|
+
*,
|
|
307
|
+
model_context_window: int,
|
|
308
|
+
) -> TokenUsageInfo:
|
|
309
|
+
info = usage_info or TokenUsageInfo(model_context_window=model_context_window)
|
|
310
|
+
return TokenUsageInfo(
|
|
311
|
+
total_token_usage=info.total_token_usage,
|
|
312
|
+
last_token_usage=TokenUsage(total_tokens=max(0, model_context_window)),
|
|
313
|
+
model_context_window=model_context_window,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def is_context_window_error(error: BaseException) -> bool:
|
|
318
|
+
message = str(error).lower()
|
|
319
|
+
return any(
|
|
320
|
+
marker in message
|
|
321
|
+
for marker in (
|
|
322
|
+
"context window",
|
|
323
|
+
"context_length_exceeded",
|
|
324
|
+
"maximum context length",
|
|
325
|
+
"too many tokens",
|
|
326
|
+
)
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
|
|
264
330
|
def estimate_mapping_message_tokens(message: Mapping[str, object]) -> int:
|
|
265
331
|
total = approximate_token_count(string_content(message.get("content")))
|
|
266
332
|
tool_calls = message.get("tool_calls")
|
|
@@ -12,10 +12,11 @@ from flowent.storage import (
|
|
|
12
12
|
StoredSettings,
|
|
13
13
|
StoredState,
|
|
14
14
|
)
|
|
15
|
+
from flowent.tools import tool_result_model_content
|
|
15
16
|
from flowent.usage import (
|
|
16
17
|
TokenUsageInfo,
|
|
17
18
|
current_model_context_window,
|
|
18
|
-
|
|
19
|
+
estimated_token_usage_for_request,
|
|
19
20
|
recompute_context_usage,
|
|
20
21
|
)
|
|
21
22
|
from flowent.workspace.output import error_context_summary, message_error_items
|
|
@@ -51,13 +52,15 @@ def should_auto_compact(
|
|
|
51
52
|
messages: Sequence[ChatMessage | Mapping[str, object]],
|
|
52
53
|
*,
|
|
53
54
|
context_window: int,
|
|
55
|
+
tools: Sequence[Mapping[str, object]] = (),
|
|
54
56
|
) -> bool:
|
|
55
57
|
token_limit = auto_compact_token_limit(context_window)
|
|
56
58
|
if token_limit <= 0:
|
|
57
59
|
return False
|
|
58
60
|
return (
|
|
59
|
-
|
|
60
|
-
model_request_messages_data(messages)
|
|
61
|
+
estimated_token_usage_for_request(
|
|
62
|
+
model_request_messages_data(messages),
|
|
63
|
+
tools=tools,
|
|
61
64
|
).total_tokens
|
|
62
65
|
>= token_limit
|
|
63
66
|
)
|
|
@@ -83,17 +86,19 @@ def update_context_usage_for_response(
|
|
|
83
86
|
messages: Sequence[Mapping[str, object]],
|
|
84
87
|
output_content: str,
|
|
85
88
|
output_tools: Sequence[Mapping[str, object]] = (),
|
|
89
|
+
request_tools: Sequence[Mapping[str, object]] = (),
|
|
86
90
|
model_context_window: int,
|
|
87
91
|
) -> TokenUsageInfo:
|
|
88
92
|
return recompute_context_usage(
|
|
89
93
|
usage_info,
|
|
90
|
-
|
|
94
|
+
estimated_token_usage_for_request(
|
|
91
95
|
[
|
|
92
96
|
*model_visible_messages_for_usage(messages),
|
|
93
97
|
*model_visible_response_messages_for_usage(
|
|
94
98
|
output_content, output_tools
|
|
95
99
|
),
|
|
96
100
|
],
|
|
101
|
+
tools=request_tools,
|
|
97
102
|
).total_tokens,
|
|
98
103
|
model_context_window=model_context_window,
|
|
99
104
|
)
|
|
@@ -107,6 +112,8 @@ def model_visible_response_messages_for_usage(
|
|
|
107
112
|
for index, tool in enumerate(output_tools):
|
|
108
113
|
tool_id = str(tool.get("id") or f"call_{index}")
|
|
109
114
|
arguments = tool.get("arguments")
|
|
115
|
+
result_payload = tool.get("result")
|
|
116
|
+
tool_result = result_payload if isinstance(result_payload, dict) else {}
|
|
110
117
|
visible_messages.append(
|
|
111
118
|
{
|
|
112
119
|
"role": "assistant",
|
|
@@ -130,7 +137,7 @@ def model_visible_response_messages_for_usage(
|
|
|
130
137
|
{
|
|
131
138
|
"role": "tool",
|
|
132
139
|
"tool_call_id": tool_id,
|
|
133
|
-
"content":
|
|
140
|
+
"content": tool_result_model_content(tool_result),
|
|
134
141
|
}
|
|
135
142
|
)
|
|
136
143
|
if output_content:
|
|
@@ -175,7 +182,7 @@ def model_visible_assistant_output_messages(
|
|
|
175
182
|
{
|
|
176
183
|
"role": "tool",
|
|
177
184
|
"tool_call_id": tool.id,
|
|
178
|
-
"content": tool.
|
|
185
|
+
"content": tool_result_model_content(tool.result or {}),
|
|
179
186
|
}
|
|
180
187
|
for tool in group_tools
|
|
181
188
|
if tool.status != "running"
|
|
@@ -260,7 +267,7 @@ def usage_info_for_model(
|
|
|
260
267
|
model_context_window: int,
|
|
261
268
|
) -> TokenUsageInfo | None:
|
|
262
269
|
if usage_info is None:
|
|
263
|
-
return
|
|
270
|
+
return TokenUsageInfo(model_context_window=model_context_window)
|
|
264
271
|
return usage_info.model_copy(update={"model_context_window": model_context_window})
|
|
265
272
|
|
|
266
273
|
|
|
@@ -13,6 +13,7 @@ from flowent.storage import (
|
|
|
13
13
|
StoredToolItem,
|
|
14
14
|
StoredToolOutputItem,
|
|
15
15
|
)
|
|
16
|
+
from flowent.tools import tool_result_model_content
|
|
16
17
|
|
|
17
18
|
APPROVAL_TRANSCRIPT_MESSAGE_LIMIT = 12
|
|
18
19
|
APPROVAL_TRANSCRIPT_TEXT_LIMIT = 2_000
|
|
@@ -84,7 +85,9 @@ def approval_transcript(
|
|
|
84
85
|
if content:
|
|
85
86
|
entries.append(ApprovalTranscriptEntry(role=role, content=content))
|
|
86
87
|
for tool in message.tools:
|
|
87
|
-
tool_content = approval_transcript_text(
|
|
88
|
+
tool_content = approval_transcript_text(
|
|
89
|
+
tool_result_model_content(tool.result or {})
|
|
90
|
+
)
|
|
88
91
|
if tool_content:
|
|
89
92
|
entries.append(
|
|
90
93
|
ApprovalTranscriptEntry(
|
|
@@ -27,11 +27,13 @@ from flowent.storage import (
|
|
|
27
27
|
StoredState,
|
|
28
28
|
StoredToolItem,
|
|
29
29
|
)
|
|
30
|
-
from flowent.tools import ToolContext
|
|
30
|
+
from flowent.tools import ToolContext, text_tool_result, tool_specs
|
|
31
31
|
from flowent.usage import (
|
|
32
32
|
TokenUsage,
|
|
33
33
|
TokenUsageInfo,
|
|
34
34
|
append_token_usage,
|
|
35
|
+
full_context_usage,
|
|
36
|
+
is_context_window_error,
|
|
35
37
|
recompute_context_usage,
|
|
36
38
|
)
|
|
37
39
|
from flowent.workspace.context import (
|
|
@@ -67,6 +69,7 @@ logger = logging.getLogger("flowent.workspace.runtime")
|
|
|
67
69
|
|
|
68
70
|
AUTO_COMPACT_RETAINED_MESSAGE_TOKEN_BUDGET = 20_000
|
|
69
71
|
WORKSPACE_PROGRESS_FLUSH_INTERVAL_SECONDS = 0.5
|
|
72
|
+
USER_VISIBLE_MANUAL_COMPACT_ERROR_MESSAGE = "Context could not be compacted."
|
|
70
73
|
|
|
71
74
|
|
|
72
75
|
@dataclass
|
|
@@ -188,13 +191,16 @@ class WorkspaceRuntime:
|
|
|
188
191
|
*,
|
|
189
192
|
connection: ProviderConnection,
|
|
190
193
|
context_window_limit: int,
|
|
194
|
+
budget_messages: Sequence[ChatMessage | Mapping[str, object]] | None = None,
|
|
191
195
|
messages: list[StoredMessage],
|
|
192
196
|
model_history: Sequence[ChatMessage | Mapping[str, object]],
|
|
193
197
|
source_message_id: str | None = None,
|
|
198
|
+
tools: Sequence[Mapping[str, object]] = (),
|
|
194
199
|
) -> tuple[StoredMessage, list[dict[str, object]], TokenUsageInfo] | None:
|
|
195
200
|
if not should_auto_compact(
|
|
196
|
-
model_history,
|
|
201
|
+
budget_messages or model_history,
|
|
197
202
|
context_window=context_window_limit,
|
|
203
|
+
tools=tools,
|
|
198
204
|
):
|
|
199
205
|
return None
|
|
200
206
|
logger.info("Workspace auto compact requested")
|
|
@@ -223,6 +229,10 @@ class WorkspaceRuntime:
|
|
|
223
229
|
)
|
|
224
230
|
next_messages = [*state.messages, user_message]
|
|
225
231
|
self.store.save_messages(next_messages)
|
|
232
|
+
model_tool_specs = [
|
|
233
|
+
*tool_specs(),
|
|
234
|
+
*list(self.mcp_manager.tool_specs()),
|
|
235
|
+
]
|
|
226
236
|
model_history: list[ChatMessage | Mapping[str, object]] = [
|
|
227
237
|
*runtime_context_messages(self.cwd, state.settings.agent_prompt),
|
|
228
238
|
*workspace_chat_messages(
|
|
@@ -234,9 +244,13 @@ class WorkspaceRuntime:
|
|
|
234
244
|
auto_compaction = await self.auto_compact_messages(
|
|
235
245
|
connection=connection,
|
|
236
246
|
context_window_limit=context_window_limit,
|
|
247
|
+
budget_messages=self.request_messages_for_content(
|
|
248
|
+
state, next_messages, content
|
|
249
|
+
),
|
|
237
250
|
messages=state.messages,
|
|
238
251
|
model_history=model_history,
|
|
239
252
|
source_message_id=None,
|
|
253
|
+
tools=model_tool_specs,
|
|
240
254
|
)
|
|
241
255
|
if auto_compaction is not None:
|
|
242
256
|
marker, _, _ = auto_compaction
|
|
@@ -341,6 +355,7 @@ class WorkspaceRuntime:
|
|
|
341
355
|
tool.model_dump(exclude_none=True)
|
|
342
356
|
for tool in assistant_output.tools.values()
|
|
343
357
|
],
|
|
358
|
+
request_tools=model_tool_specs,
|
|
344
359
|
model_context_window=context_window_limit,
|
|
345
360
|
)
|
|
346
361
|
self.store.save_usage_info(final_usage_info)
|
|
@@ -709,6 +724,10 @@ class WorkspaceRuntime:
|
|
|
709
724
|
turn_usage_info: TokenUsageInfo | None = None
|
|
710
725
|
current_output_index = 0
|
|
711
726
|
latest_usage_output_index: int | None = None
|
|
727
|
+
model_tool_specs = [
|
|
728
|
+
*tool_specs(),
|
|
729
|
+
*list(self.mcp_manager.tool_specs()),
|
|
730
|
+
]
|
|
712
731
|
if request_messages is None:
|
|
713
732
|
current_request_messages = self.request_messages_for_content(
|
|
714
733
|
state,
|
|
@@ -723,9 +742,11 @@ class WorkspaceRuntime:
|
|
|
723
742
|
auto_compaction = await self.auto_compact_messages(
|
|
724
743
|
connection=connection,
|
|
725
744
|
context_window_limit=context_window_limit,
|
|
745
|
+
budget_messages=current_request_messages,
|
|
726
746
|
messages=state.messages,
|
|
727
747
|
model_history=pre_turn_request_messages,
|
|
728
748
|
source_message_id=None,
|
|
749
|
+
tools=model_tool_specs,
|
|
729
750
|
)
|
|
730
751
|
if auto_compaction is not None:
|
|
731
752
|
marker, _, usage_info = auto_compaction
|
|
@@ -746,6 +767,42 @@ class WorkspaceRuntime:
|
|
|
746
767
|
)
|
|
747
768
|
else:
|
|
748
769
|
current_request_messages = request_messages
|
|
770
|
+
auto_compaction = await self.auto_compact_messages(
|
|
771
|
+
connection=connection,
|
|
772
|
+
context_window_limit=context_window_limit,
|
|
773
|
+
messages=next_messages,
|
|
774
|
+
model_history=compact_prompt_chat_messages(
|
|
775
|
+
current_request_messages
|
|
776
|
+
),
|
|
777
|
+
source_message_id=assistant_message.id,
|
|
778
|
+
tools=model_tool_specs,
|
|
779
|
+
)
|
|
780
|
+
if auto_compaction is not None:
|
|
781
|
+
marker, replacement_history, usage_info = auto_compaction
|
|
782
|
+
assistant_message = assistant_message.model_copy(
|
|
783
|
+
update={"usage_info": usage_info}
|
|
784
|
+
)
|
|
785
|
+
next_messages = append_or_replace_message(
|
|
786
|
+
[*next_messages, marker], assistant_message
|
|
787
|
+
)
|
|
788
|
+
self.store.save_messages(next_messages)
|
|
789
|
+
await self.append_event(
|
|
790
|
+
response,
|
|
791
|
+
"context_optimized",
|
|
792
|
+
{
|
|
793
|
+
"message": marker.model_dump(),
|
|
794
|
+
**usage_event_data(usage_info),
|
|
795
|
+
},
|
|
796
|
+
)
|
|
797
|
+
current_request_messages = model_request_messages_data(
|
|
798
|
+
[
|
|
799
|
+
*runtime_context_messages(
|
|
800
|
+
self.cwd, state.settings.agent_prompt
|
|
801
|
+
),
|
|
802
|
+
*explicit_skill_messages(self.cwd, self.store, content),
|
|
803
|
+
*replacement_history,
|
|
804
|
+
]
|
|
805
|
+
)
|
|
749
806
|
context_usage_messages = (
|
|
750
807
|
usage_request_messages
|
|
751
808
|
if usage_request_messages is not None
|
|
@@ -801,6 +858,7 @@ class WorkspaceRuntime:
|
|
|
801
858
|
messages=next_messages,
|
|
802
859
|
model_history=compact_prompt_chat_messages(conversation),
|
|
803
860
|
source_message_id=assistant_snapshot.id,
|
|
861
|
+
tools=model_tool_specs,
|
|
804
862
|
)
|
|
805
863
|
if auto_result is None:
|
|
806
864
|
return None
|
|
@@ -944,6 +1002,7 @@ class WorkspaceRuntime:
|
|
|
944
1002
|
tool.model_dump(exclude_none=True)
|
|
945
1003
|
for tool in assistant_output.tools.values()
|
|
946
1004
|
],
|
|
1005
|
+
request_tools=model_tool_specs,
|
|
947
1006
|
model_context_window=context_window_limit,
|
|
948
1007
|
)
|
|
949
1008
|
self.store.save_usage_info(final_usage_info)
|
|
@@ -976,6 +1035,12 @@ class WorkspaceRuntime:
|
|
|
976
1035
|
raise
|
|
977
1036
|
except Exception as error:
|
|
978
1037
|
logger.exception("Workspace response failed")
|
|
1038
|
+
if is_context_window_error(error):
|
|
1039
|
+
usage_info = full_context_usage(
|
|
1040
|
+
self.store.read_usage_info(),
|
|
1041
|
+
model_context_window=context_window_limit,
|
|
1042
|
+
)
|
|
1043
|
+
self.store.save_usage_info(usage_info)
|
|
979
1044
|
if (
|
|
980
1045
|
current_tool_id is not None
|
|
981
1046
|
and current_tool_id in assistant_output.tools
|
|
@@ -983,7 +1048,10 @@ class WorkspaceRuntime:
|
|
|
983
1048
|
):
|
|
984
1049
|
assistant_output.update_tool(
|
|
985
1050
|
current_tool_id,
|
|
986
|
-
{
|
|
1051
|
+
{
|
|
1052
|
+
"result": text_tool_result(str(error) or "Tool failed."),
|
|
1053
|
+
"status": "failed",
|
|
1054
|
+
},
|
|
987
1055
|
)
|
|
988
1056
|
error_item = assistant_output.append_error(
|
|
989
1057
|
run_error_output_item(
|
|
@@ -1142,10 +1210,31 @@ class WorkspaceRuntime:
|
|
|
1142
1210
|
async def compact_events() -> AsyncIterator[str]:
|
|
1143
1211
|
try:
|
|
1144
1212
|
marker, usage_info = await asyncio.shield(compact_task)
|
|
1145
|
-
except Exception:
|
|
1213
|
+
except Exception as error:
|
|
1214
|
+
assistant_id = str(uuid4())
|
|
1215
|
+
assistant_output = AssistantOutputBuilder(assistant_id)
|
|
1216
|
+
error_item = run_error_output_item(assistant_id, str(error)).model_copy(
|
|
1217
|
+
update={"message": USER_VISIBLE_MANUAL_COMPACT_ERROR_MESSAGE}
|
|
1218
|
+
)
|
|
1219
|
+
assistant_output.append_error(error_item)
|
|
1220
|
+
failed_message = StoredMessage(
|
|
1221
|
+
author="assistant",
|
|
1222
|
+
content="",
|
|
1223
|
+
groups=assistant_output.groups,
|
|
1224
|
+
id=assistant_id,
|
|
1225
|
+
status="failed",
|
|
1226
|
+
)
|
|
1227
|
+
self.store.save_messages(
|
|
1228
|
+
[*self.store.read_state().messages, failed_message]
|
|
1229
|
+
)
|
|
1230
|
+
failed_message_data = stream_message_data(failed_message)
|
|
1231
|
+
yield stream_event("snapshot", {"message": failed_message_data})
|
|
1146
1232
|
yield stream_event(
|
|
1147
1233
|
"error",
|
|
1148
|
-
{
|
|
1234
|
+
{
|
|
1235
|
+
"error": error_item.model_dump(exclude_none=True),
|
|
1236
|
+
"message": USER_VISIBLE_MANUAL_COMPACT_ERROR_MESSAGE,
|
|
1237
|
+
},
|
|
1149
1238
|
)
|
|
1150
1239
|
return
|
|
1151
1240
|
|