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.
Files changed (40) hide show
  1. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/PKG-INFO +15 -12
  2. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/README.md +14 -11
  3. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/__init__.py +1 -1
  4. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/agent.py +71 -13
  5. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/browser.py +1780 -1697
  6. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/cli.py +2 -0
  7. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/compaction.py +5 -5
  8. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/config.py +4 -0
  9. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/display.py +5 -5
  10. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/prompts.py +53 -20
  11. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/protocol.py +30 -2
  12. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/scraper.py +39 -2
  13. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/tokens.py +2 -2
  14. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/tui.py +1060 -1057
  15. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli_agent.egg-info/PKG-INFO +15 -12
  16. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/tests/test_agent.py +1268 -1125
  17. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/tests/test_browser.py +1062 -833
  18. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/tests/test_compaction.py +304 -302
  19. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/tests/test_display.py +438 -398
  20. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/tests/test_protocol.py +47 -0
  21. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/tests/test_tokens.py +15 -8
  22. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/tests/test_tui.py +670 -670
  23. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/LICENSE +0 -0
  24. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/__main__.py +0 -0
  25. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/attachments.py +0 -0
  26. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/branding.py +0 -0
  27. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/providers.py +0 -0
  28. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/tools.py +0 -0
  29. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli/ui.py +0 -0
  30. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli_agent.egg-info/SOURCES.txt +0 -0
  31. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli_agent.egg-info/dependency_links.txt +0 -0
  32. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli_agent.egg-info/entry_points.txt +0 -0
  33. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli_agent.egg-info/requires.txt +0 -0
  34. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/dscli_agent.egg-info/top_level.txt +0 -0
  35. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/pyproject.toml +0 -0
  36. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/setup.cfg +0 -0
  37. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/tests/test_branding.py +0 -0
  38. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/tests/test_cli.py +0 -0
  39. {dscli_agent-0.4.0 → dscli_agent-0.5.0}/tests/test_config.py +0 -0
  40. {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.4.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 token usage against a 1,000,000-token budget, and the
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
- tokens ███████░░░░░ 612.4k / 1.0M · 61%
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 estimate reaches 1,000,000 tokens, dscli compacts the session instead of
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`, `auto_human_check` (true = open a visible
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.** DeepSeek did not
506
- show its completion toolbar (`Read aloud`), so dscli used the fallback completion
507
- detector. Increase `fallback_stable_seconds` if replies are still streaming at that
508
- point.
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 token bar is likewise a local estimate of sent and received text, not usage
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 token usage against a 1,000,000-token budget, and the
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
- tokens ███████░░░░░ 612.4k / 1.0M · 61%
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 estimate reaches 1,000,000 tokens, dscli compacts the session instead of
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`, `auto_human_check` (true = open a visible
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.** DeepSeek did not
476
- show its completion toolbar (`Read aloud`), so dscli used the fallback completion
477
- detector. Increase `fallback_stable_seconds` if replies are still streaming at that
478
- point.
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 token bar is likewise a local estimate of sent and received text, not usage
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
@@ -1,3 +1,3 @@
1
1
  """dscli - a local coding agent for your terminal."""
2
2
 
3
- __version__ = "0.4.0"
3
+ __version__ = "0.5.0"
@@ -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 MAX_TOKENS, format_tokens
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 _token_budget_reached(self) -> bool:
107
- used = getattr(self.session, "tokens_used", 0)
108
+ def _context_budget_reached(self) -> bool:
109
+ used = getattr(self.session, "context_tokens", 0)
108
110
  try:
109
- return int(used or 0) >= MAX_TOKENS
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._token_budget_reached():
149
+ if not self._context_budget_reached():
148
150
  return message, files
149
- used = int(getattr(self.session, "tokens_used", 0) or 0)
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
- tokens_used=used,
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
- "Token estimate reached %s. Compacted the conversation to %s and started "
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), task=self.task, step=turn, max_steps=self.cfg.max_steps
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
- if step.final is not None:
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 = step.complete is True and not plan_incomplete
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,