dscli-agent 0.4.0__tar.gz → 0.5.0__tar.gz
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.
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/PKG-INFO +15 -12
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/README.md +14 -11
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/__init__.py +1 -1
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/agent.py +71 -13
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/browser.py +1780 -1697
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/cli.py +2 -0
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/compaction.py +5 -5
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/config.py +4 -0
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/display.py +5 -5
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/prompts.py +53 -20
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/protocol.py +30 -2
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/scraper.py +39 -2
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/tokens.py +2 -2
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/tui.py +1060 -1057
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli_agent.egg-info/PKG-INFO +15 -12
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/tests/test_agent.py +1268 -1125
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/tests/test_browser.py +1062 -833
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/tests/test_compaction.py +304 -302
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/tests/test_display.py +438 -398
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/tests/test_protocol.py +47 -0
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/tests/test_tokens.py +15 -8
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/tests/test_tui.py +670 -670
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/LICENSE +0 -0
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/__main__.py +0 -0
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/attachments.py +0 -0
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/branding.py +0 -0
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/providers.py +0 -0
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/tools.py +0 -0
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/ui.py +0 -0
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli_agent.egg-info/SOURCES.txt +0 -0
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli_agent.egg-info/dependency_links.txt +0 -0
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli_agent.egg-info/entry_points.txt +0 -0
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli_agent.egg-info/requires.txt +0 -0
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli_agent.egg-info/top_level.txt +0 -0
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/pyproject.toml +0 -0
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/setup.cfg +0 -0
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/tests/test_branding.py +0 -0
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/tests/test_cli.py +0 -0
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/tests/test_config.py +0 -0
- {dscli_agent-0.4.0 → dscli_agent-0.5.0}/tests/test_tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dscli-agent
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: A local coding agent that uses DeepSeek web chat as its model
|
|
5
5
|
Author: Anish
|
|
6
6
|
License-Expression: MIT
|
|
@@ -147,11 +147,11 @@ it collapses to the same limited view with a `Ctrl+O` hint; press `Ctrl+O` at th
|
|
|
147
147
|
prompt to print the full reasoning block (up to 500 rows) into the transcript.
|
|
148
148
|
The streamed block is TUI-only; `--plain` and `run` do not show it.
|
|
149
149
|
|
|
150
|
-
The prompt footer tracks session
|
|
151
|
-
same progress bar renders below the live Thinking and Responding output:
|
|
150
|
+
The prompt footer tracks the session's context volume against a 1,000,000-token budget,
|
|
151
|
+
and the same progress bar renders below the live Thinking and Responding output:
|
|
152
152
|
|
|
153
153
|
```text
|
|
154
|
-
|
|
154
|
+
context ███████░░░░░ 612.4k / 1.0M · 61%
|
|
155
155
|
```
|
|
156
156
|
|
|
157
157
|
Counts are estimated locally from every prompt sent and reply received (one token per
|
|
@@ -161,7 +161,7 @@ a new chat is opened.
|
|
|
161
161
|
|
|
162
162
|
### Conversation compaction
|
|
163
163
|
|
|
164
|
-
When the
|
|
164
|
+
When the count reaches 1,000,000 tokens, dscli compacts the session instead of
|
|
165
165
|
stopping. Before the next prompt is sent it asks the current chat to write a structured
|
|
166
166
|
compaction summary — an `<analysis>` pass followed by a `<summary>` covering the request,
|
|
167
167
|
technical decisions, files and code, errors and fixes, user messages, pending tasks, and
|
|
@@ -179,7 +179,7 @@ Files are saved under `%APPDATA%\dscli\compactions` on Windows
|
|
|
179
179
|
`~/.config/dscli/compactions` elsewhere) as `compaction-YYYYmmdd-HHMMSS.txt`. The check
|
|
180
180
|
runs before every model turn, so it also works mid-task; the TUI shows a
|
|
181
181
|
`Compacting conversation` spinner while the summary is written, then a
|
|
182
|
-
`⟳ Compacted 1.0M tokens → … · new chat` notice. `/new` clears the recorded
|
|
182
|
+
`⟳ Compacted context at 1.0M tokens → … · new chat` notice. `/new` clears the recorded
|
|
183
183
|
conversation as well.
|
|
184
184
|
|
|
185
185
|
### Deeplanning mode
|
|
@@ -439,7 +439,9 @@ Useful keys: `url` (the DeepSeek chat URL; set it only for a custom deployment),
|
|
|
439
439
|
import), `chrome_profile` (empty = Chrome's last-used profile, or a name such as
|
|
440
440
|
`Default` / `Profile 1`), `user_agent` (empty = derive a normal Chrome UA from the
|
|
441
441
|
installed browser), `response_timeout_sec`, `first_response_timeout_sec`,
|
|
442
|
-
`stable_seconds`, `fallback_stable_seconds`, `
|
|
442
|
+
`stable_seconds`, `fallback_stable_seconds`, `activity_settle_seconds` (how long the
|
|
443
|
+
page must stay free of DOM mutations before a reply counts as finished; default 1.5),
|
|
444
|
+
`auto_human_check` (true = open a visible
|
|
443
445
|
window to solve a verification check; false = fail fast), `max_steps`, `max_tool_calls`,
|
|
444
446
|
`max_read_chars`, `max_attachment_mb`, `search`, `deeplanning`, `shell_enabled`
|
|
445
447
|
(default true; every command still requires approval), `allow_outside`,
|
|
@@ -502,10 +504,11 @@ profile.
|
|
|
502
504
|
may exceed the chat's limit, or the selector may have changed. Run `doctor` and check
|
|
503
505
|
the reported counts.
|
|
504
506
|
|
|
505
|
-
**A response is cut off around 12 s after the last text change.**
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
507
|
+
**A response is cut off around 12 s after the last text change.** dscli normally waits
|
|
508
|
+
for the reply text to stabilize while the page itself stops changing DOM nodes and
|
|
509
|
+
text (`activity_settle_seconds`, default 1.5 s). If the page keeps mutating, the
|
|
510
|
+
fallback detector returns the reply after `fallback_stable_seconds` of text stability.
|
|
511
|
+
Increase `fallback_stable_seconds` if replies are still streaming at that point.
|
|
509
512
|
|
|
510
513
|
**Chrome will not launch.** Set `"channel": ""` in the config to use Playwright's
|
|
511
514
|
Chromium, after running `python -m playwright install chromium`.
|
|
@@ -522,7 +525,7 @@ Debug screenshots and unparsed replies are written to `%APPDATA%\dscli\debug`.
|
|
|
522
525
|
- Streaming is approximated by polling the DOM, not by tokens from the model. The
|
|
523
526
|
interactive chat streams a final answer as the page updates only after it declares
|
|
524
527
|
`complete: true`; tool calls still wait for the complete, validated JSON envelope.
|
|
525
|
-
The
|
|
528
|
+
The context bar is likewise a local estimate of sent and received text, not usage
|
|
526
529
|
reported by DeepSeek.
|
|
527
530
|
- Interrupting with Ctrl+C clicks DeepSeek's labelled stop button when dscli can find
|
|
528
531
|
it. If the page does not expose one, generation may continue in the web chat; the
|
|
@@ -117,11 +117,11 @@ it collapses to the same limited view with a `Ctrl+O` hint; press `Ctrl+O` at th
|
|
|
117
117
|
prompt to print the full reasoning block (up to 500 rows) into the transcript.
|
|
118
118
|
The streamed block is TUI-only; `--plain` and `run` do not show it.
|
|
119
119
|
|
|
120
|
-
The prompt footer tracks session
|
|
121
|
-
same progress bar renders below the live Thinking and Responding output:
|
|
120
|
+
The prompt footer tracks the session's context volume against a 1,000,000-token budget,
|
|
121
|
+
and the same progress bar renders below the live Thinking and Responding output:
|
|
122
122
|
|
|
123
123
|
```text
|
|
124
|
-
|
|
124
|
+
context ███████░░░░░ 612.4k / 1.0M · 61%
|
|
125
125
|
```
|
|
126
126
|
|
|
127
127
|
Counts are estimated locally from every prompt sent and reply received (one token per
|
|
@@ -131,7 +131,7 @@ a new chat is opened.
|
|
|
131
131
|
|
|
132
132
|
### Conversation compaction
|
|
133
133
|
|
|
134
|
-
When the
|
|
134
|
+
When the count reaches 1,000,000 tokens, dscli compacts the session instead of
|
|
135
135
|
stopping. Before the next prompt is sent it asks the current chat to write a structured
|
|
136
136
|
compaction summary — an `<analysis>` pass followed by a `<summary>` covering the request,
|
|
137
137
|
technical decisions, files and code, errors and fixes, user messages, pending tasks, and
|
|
@@ -149,7 +149,7 @@ Files are saved under `%APPDATA%\dscli\compactions` on Windows
|
|
|
149
149
|
`~/.config/dscli/compactions` elsewhere) as `compaction-YYYYmmdd-HHMMSS.txt`. The check
|
|
150
150
|
runs before every model turn, so it also works mid-task; the TUI shows a
|
|
151
151
|
`Compacting conversation` spinner while the summary is written, then a
|
|
152
|
-
`⟳ Compacted 1.0M tokens → … · new chat` notice. `/new` clears the recorded
|
|
152
|
+
`⟳ Compacted context at 1.0M tokens → … · new chat` notice. `/new` clears the recorded
|
|
153
153
|
conversation as well.
|
|
154
154
|
|
|
155
155
|
### Deeplanning mode
|
|
@@ -409,7 +409,9 @@ Useful keys: `url` (the DeepSeek chat URL; set it only for a custom deployment),
|
|
|
409
409
|
import), `chrome_profile` (empty = Chrome's last-used profile, or a name such as
|
|
410
410
|
`Default` / `Profile 1`), `user_agent` (empty = derive a normal Chrome UA from the
|
|
411
411
|
installed browser), `response_timeout_sec`, `first_response_timeout_sec`,
|
|
412
|
-
`stable_seconds`, `fallback_stable_seconds`, `
|
|
412
|
+
`stable_seconds`, `fallback_stable_seconds`, `activity_settle_seconds` (how long the
|
|
413
|
+
page must stay free of DOM mutations before a reply counts as finished; default 1.5),
|
|
414
|
+
`auto_human_check` (true = open a visible
|
|
413
415
|
window to solve a verification check; false = fail fast), `max_steps`, `max_tool_calls`,
|
|
414
416
|
`max_read_chars`, `max_attachment_mb`, `search`, `deeplanning`, `shell_enabled`
|
|
415
417
|
(default true; every command still requires approval), `allow_outside`,
|
|
@@ -472,10 +474,11 @@ profile.
|
|
|
472
474
|
may exceed the chat's limit, or the selector may have changed. Run `doctor` and check
|
|
473
475
|
the reported counts.
|
|
474
476
|
|
|
475
|
-
**A response is cut off around 12 s after the last text change.**
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
477
|
+
**A response is cut off around 12 s after the last text change.** dscli normally waits
|
|
478
|
+
for the reply text to stabilize while the page itself stops changing DOM nodes and
|
|
479
|
+
text (`activity_settle_seconds`, default 1.5 s). If the page keeps mutating, the
|
|
480
|
+
fallback detector returns the reply after `fallback_stable_seconds` of text stability.
|
|
481
|
+
Increase `fallback_stable_seconds` if replies are still streaming at that point.
|
|
479
482
|
|
|
480
483
|
**Chrome will not launch.** Set `"channel": ""` in the config to use Playwright's
|
|
481
484
|
Chromium, after running `python -m playwright install chromium`.
|
|
@@ -492,7 +495,7 @@ Debug screenshots and unparsed replies are written to `%APPDATA%\dscli\debug`.
|
|
|
492
495
|
- Streaming is approximated by polling the DOM, not by tokens from the model. The
|
|
493
496
|
interactive chat streams a final answer as the page updates only after it declares
|
|
494
497
|
`complete: true`; tool calls still wait for the complete, validated JSON envelope.
|
|
495
|
-
The
|
|
498
|
+
The context bar is likewise a local estimate of sent and received text, not usage
|
|
496
499
|
reported by DeepSeek.
|
|
497
500
|
- Interrupting with Ctrl+C clicks DeepSeek's labelled stop button when dscli can find
|
|
498
501
|
it. If the page does not expose one, generation may continue in the web chat; the
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import logging
|
|
5
|
+
import secrets
|
|
5
6
|
import time
|
|
6
7
|
from dataclasses import dataclass, field
|
|
7
8
|
from pathlib import Path
|
|
@@ -23,7 +24,7 @@ from .prompts import (
|
|
|
23
24
|
)
|
|
24
25
|
from .protocol import ProtocolError, Step, ToolCall, extract_final_prefix, parse_step
|
|
25
26
|
from .providers import MODEL_NAME
|
|
26
|
-
from .tokens import
|
|
27
|
+
from .tokens import MAX_CONTEXT_TOKENS, format_tokens
|
|
27
28
|
from .tools import (
|
|
28
29
|
ApprovalRequired,
|
|
29
30
|
PlanStep,
|
|
@@ -102,11 +103,12 @@ class Agent:
|
|
|
102
103
|
self._verify_timeout = config.verification_timeout()
|
|
103
104
|
self._sent_any = False
|
|
104
105
|
self._previous_plan: list[PlanStep] = []
|
|
106
|
+
self.turn_id = ""
|
|
105
107
|
|
|
106
|
-
def
|
|
107
|
-
used = getattr(self.session, "
|
|
108
|
+
def _context_budget_reached(self) -> bool:
|
|
109
|
+
used = getattr(self.session, "context_tokens", 0)
|
|
108
110
|
try:
|
|
109
|
-
return int(used or 0) >=
|
|
111
|
+
return int(used or 0) >= MAX_CONTEXT_TOKENS
|
|
110
112
|
except (TypeError, ValueError):
|
|
111
113
|
return False
|
|
112
114
|
|
|
@@ -144,9 +146,9 @@ class Agent:
|
|
|
144
146
|
*,
|
|
145
147
|
wrapup: bool,
|
|
146
148
|
) -> tuple[str, list[Path] | None]:
|
|
147
|
-
if not self.
|
|
149
|
+
if not self._context_budget_reached():
|
|
148
150
|
return message, files
|
|
149
|
-
used = int(getattr(self.session, "
|
|
151
|
+
used = int(getattr(self.session, "context_tokens", 0) or 0)
|
|
150
152
|
plan = self.toolbox.plan if self._sent_any else self._previous_plan
|
|
151
153
|
tree = project_structure(self.workspace)
|
|
152
154
|
summary = self._request_compaction_summary(plan)
|
|
@@ -157,20 +159,20 @@ class Agent:
|
|
|
157
159
|
plan=plan,
|
|
158
160
|
changed=self.toolbox.changed_files,
|
|
159
161
|
verification=self._verification_summary(),
|
|
160
|
-
|
|
162
|
+
context_tokens=used,
|
|
161
163
|
project_tree=tree,
|
|
162
164
|
summary=summary,
|
|
163
165
|
)
|
|
164
166
|
self.session.new_chat()
|
|
165
167
|
self.display.compaction_started(str(compaction.path), used)
|
|
166
168
|
log.warning(
|
|
167
|
-
"
|
|
169
|
+
"Context estimate reached %s. Compacted the conversation to %s and started "
|
|
168
170
|
"a new chat.",
|
|
169
171
|
format_tokens(used),
|
|
170
172
|
compaction.path,
|
|
171
173
|
)
|
|
172
174
|
self.history.add_note(
|
|
173
|
-
f"Compacted at {used:,} estimated tokens into {compaction.path.name}; "
|
|
175
|
+
f"Compacted at {used:,} estimated context tokens into {compaction.path.name}; "
|
|
174
176
|
"a new chat was started."
|
|
175
177
|
)
|
|
176
178
|
resume = build_resume_prompt(
|
|
@@ -190,6 +192,7 @@ class Agent:
|
|
|
190
192
|
wrapup=wrapup,
|
|
191
193
|
deeplanning=self.cfg.deeplanning,
|
|
192
194
|
max_tool_calls=self.cfg.max_tool_calls,
|
|
195
|
+
turn_id=self.turn_id,
|
|
193
196
|
)
|
|
194
197
|
attachments = [compaction.path]
|
|
195
198
|
for attachment in self.attachments:
|
|
@@ -221,7 +224,7 @@ class Agent:
|
|
|
221
224
|
|
|
222
225
|
def on_reply_snapshot(raw: str) -> None:
|
|
223
226
|
nonlocal last_final
|
|
224
|
-
final = extract_final_prefix(raw)
|
|
227
|
+
final = extract_final_prefix(raw, turn_id=self.turn_id or None)
|
|
225
228
|
if final is not None and final != last_final:
|
|
226
229
|
last_final = final
|
|
227
230
|
if not self._streamed_final:
|
|
@@ -251,7 +254,25 @@ class Agent:
|
|
|
251
254
|
),
|
|
252
255
|
)
|
|
253
256
|
|
|
257
|
+
def _turn_id_error(self, step: Step) -> str | None:
|
|
258
|
+
if not self.turn_id:
|
|
259
|
+
return None
|
|
260
|
+
if step.turn_id is None:
|
|
261
|
+
return (
|
|
262
|
+
"The envelope is missing the required turn_id field. Every reply to this "
|
|
263
|
+
f'message must include "turn_id":"{self.turn_id}".'
|
|
264
|
+
)
|
|
265
|
+
if step.turn_id != self.turn_id:
|
|
266
|
+
return (
|
|
267
|
+
f"The envelope's turn_id {step.turn_id!r} does not match the current "
|
|
268
|
+
f"message's turn id {self.turn_id!r}. It looks like text from an earlier "
|
|
269
|
+
"message, so it was not executed. Answer the current message and include "
|
|
270
|
+
f'"turn_id":"{self.turn_id}".'
|
|
271
|
+
)
|
|
272
|
+
return None
|
|
273
|
+
|
|
254
274
|
def run(self) -> AgentOutcome:
|
|
275
|
+
self.turn_id = secrets.token_hex(4)
|
|
255
276
|
self._previous_plan = [
|
|
256
277
|
step for step in self.toolbox.plan if step.status != "completed"
|
|
257
278
|
]
|
|
@@ -267,6 +288,7 @@ class Agent:
|
|
|
267
288
|
if self.followup:
|
|
268
289
|
initial = build_followup_prompt(
|
|
269
290
|
self.task,
|
|
291
|
+
turn_id=self.turn_id,
|
|
270
292
|
verify_command=self._verify_command,
|
|
271
293
|
deeplanning=self.cfg.deeplanning,
|
|
272
294
|
max_tool_calls=self.cfg.max_tool_calls,
|
|
@@ -283,6 +305,7 @@ class Agent:
|
|
|
283
305
|
verify_command=self._verify_command,
|
|
284
306
|
deeplanning=self.cfg.deeplanning,
|
|
285
307
|
max_tool_calls=self.cfg.max_tool_calls,
|
|
308
|
+
turn_id=self.turn_id,
|
|
286
309
|
)
|
|
287
310
|
if len(initial) > 80000:
|
|
288
311
|
raise ValueError(
|
|
@@ -330,7 +353,32 @@ class Agent:
|
|
|
330
353
|
if turn >= self.cfg.max_steps:
|
|
331
354
|
return self._wrap_up(turn, last_reply or str(exc))
|
|
332
355
|
message = build_protocol_repair(
|
|
333
|
-
str(exc),
|
|
356
|
+
str(exc),
|
|
357
|
+
task=self.task,
|
|
358
|
+
step=turn,
|
|
359
|
+
max_steps=self.cfg.max_steps,
|
|
360
|
+
turn_id=self.turn_id,
|
|
361
|
+
)
|
|
362
|
+
continue
|
|
363
|
+
|
|
364
|
+
turn_error = self._turn_id_error(step)
|
|
365
|
+
if turn_error is not None:
|
|
366
|
+
protocol_failures += 1
|
|
367
|
+
self.session.save_last_response(last_reply, "turn-id-error")
|
|
368
|
+
if protocol_failures > self.cfg.max_protocol_retries:
|
|
369
|
+
raise ProtocolError(
|
|
370
|
+
f"{turn_error} The raw reply was saved to the dscli debug directory.",
|
|
371
|
+
last_reply,
|
|
372
|
+
)
|
|
373
|
+
self.display.protocol_error(turn_error)
|
|
374
|
+
if turn >= self.cfg.max_steps:
|
|
375
|
+
return self._wrap_up(turn, last_reply)
|
|
376
|
+
message = build_protocol_repair(
|
|
377
|
+
turn_error,
|
|
378
|
+
task=self.task,
|
|
379
|
+
step=turn,
|
|
380
|
+
max_steps=self.cfg.max_steps,
|
|
381
|
+
turn_id=self.turn_id,
|
|
334
382
|
)
|
|
335
383
|
continue
|
|
336
384
|
|
|
@@ -378,6 +426,7 @@ class Agent:
|
|
|
378
426
|
task=self.task,
|
|
379
427
|
step=turn,
|
|
380
428
|
max_steps=self.cfg.max_steps,
|
|
429
|
+
turn_id=self.turn_id,
|
|
381
430
|
)
|
|
382
431
|
continue
|
|
383
432
|
|
|
@@ -476,6 +525,7 @@ class Agent:
|
|
|
476
525
|
verify_command=self._verify_command,
|
|
477
526
|
deeplanning=self.cfg.deeplanning,
|
|
478
527
|
max_tool_calls=self.cfg.max_tool_calls,
|
|
528
|
+
turn_id=self.turn_id,
|
|
479
529
|
)
|
|
480
530
|
|
|
481
531
|
def _completion_reminder_message(
|
|
@@ -594,6 +644,7 @@ class Agent:
|
|
|
594
644
|
step=turn,
|
|
595
645
|
plan=self.toolbox.plan,
|
|
596
646
|
verify_command=self._verify_command,
|
|
647
|
+
turn_id=self.turn_id,
|
|
597
648
|
)
|
|
598
649
|
self.display.wrapup_started()
|
|
599
650
|
reply = self._send_and_wait(prompt, stream_final=False, wrapup=True)
|
|
@@ -611,13 +662,20 @@ class Agent:
|
|
|
611
662
|
limit_reached=True,
|
|
612
663
|
plan_incomplete=plan_incomplete,
|
|
613
664
|
)
|
|
614
|
-
|
|
665
|
+
turn_error = self._turn_id_error(step)
|
|
666
|
+
if turn_error is not None:
|
|
667
|
+
self.session.save_last_response(reply, "wrapup-turn-id-error")
|
|
668
|
+
log.warning("The wrap-up reply failed the turn-id check; showing the raw text.")
|
|
669
|
+
final = reply.strip() or "Stopped: the model-turn budget was exhausted."
|
|
670
|
+
elif step.final is not None:
|
|
615
671
|
final = (step.final or "").strip() or "Stopped: the model-turn budget was exhausted."
|
|
616
672
|
else:
|
|
617
673
|
self.session.save_last_response(reply, "wrapup-with-tools")
|
|
618
674
|
final = reply.strip() or "Stopped: the model-turn budget was exhausted."
|
|
619
675
|
plan_incomplete = self.toolbox.plan_has_pending()
|
|
620
|
-
completed =
|
|
676
|
+
completed = (
|
|
677
|
+
turn_error is None and step.complete is True and not plan_incomplete
|
|
678
|
+
)
|
|
621
679
|
self._verify_if_needed()
|
|
622
680
|
return self._outcome(
|
|
623
681
|
final,
|