lcode-cli 0.2.2__py3-none-any.whl → 0.3.1__py3-none-any.whl

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.
lcode/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """lcode — a local-first terminal coding agent powered by open-weight models via Ollama."""
2
2
 
3
- __version__ = "0.2.2"
3
+ __version__ = "0.3.1"
lcode/agent.py CHANGED
@@ -19,6 +19,7 @@ from rich.panel import Panel
19
19
  from rich.text import Text
20
20
 
21
21
  from lcode import catalog, limits, sessions, web
22
+ from lcode.checkpoints import Checkpoints
22
23
  from lcode.config import format_tokens
23
24
  from lcode.ollama import Ollama, OllamaError
24
25
  from lcode.permissions import Permissions
@@ -136,6 +137,7 @@ class Settings:
136
137
  web: str = "on" # on | ask | off
137
138
  search_backend: str = "auto"
138
139
  searxng_url: str | None = None
140
+ checkpoints: bool = True # snapshot files before the model changes them, for /undo
139
141
 
140
142
 
141
143
  class Agent:
@@ -147,10 +149,12 @@ class Agent:
147
149
  self.perms = Permissions(self.console, settings.permission_mode)
148
150
  self.tools = Toolbox(self)
149
151
  self.session_id = self.new_session_id()
152
+ self.checkpoints = Checkpoints(self.console, settings.checkpoints)
150
153
  self.session_name = ""
151
154
  self.session_title = ""
152
155
  self.ctx_used = 0
153
156
  self.last_speed = 0.0
157
+ self.usage = {"requests": 0, "prompt_tokens": 0, "prompt_ns": 0, "output_tokens": 0, "output_ns": 0}
154
158
  self.messages: list[dict] = []
155
159
  self.reset()
156
160
 
@@ -220,6 +224,7 @@ class Agent:
220
224
  "name": self.session_name,
221
225
  "title": self.session_title,
222
226
  "messages": self.messages,
227
+ "checkpoints": self.checkpoints.to_json(),
223
228
  }
224
229
  f.write_text(json.dumps(data))
225
230
 
@@ -228,6 +233,7 @@ class Agent:
228
233
  self.session_id = self.new_session_id()
229
234
  self.session_name = ""
230
235
  self.session_title = ""
236
+ self.checkpoints.reset(self.session_id)
231
237
 
232
238
  def rename(self, name: str) -> None:
233
239
  self.session_name = " ".join(name.split())
@@ -249,6 +255,7 @@ class Agent:
249
255
  note = f"The session's directory {saved_cwd} no longer exists; staying in {self.cwd}"
250
256
  self.messages = [{"role": "system", "content": self.system_prompt()}, *data.get("messages", [])[1:]]
251
257
  self.session_id = info.id
258
+ self.checkpoints.load(info.id, data.get("checkpoints") or [])
252
259
  self.session_name = data.get("name", "")
253
260
  self.session_title = data.get("title") or sessions.title_from(self.messages)
254
261
  self.tools.read_mtimes.clear() # files may have changed since; the model must read them again
@@ -384,6 +391,11 @@ class Agent:
384
391
  message["tool_calls"] = tool_calls
385
392
  self.messages.append(message)
386
393
  if final:
394
+ self.usage["requests"] += 1
395
+ self.usage["prompt_tokens"] += final.get("prompt_eval_count", 0)
396
+ self.usage["prompt_ns"] += final.get("prompt_eval_duration", 0)
397
+ self.usage["output_tokens"] += final.get("eval_count", 0)
398
+ self.usage["output_ns"] += final.get("eval_duration", 0)
387
399
  self.ctx_used = final.get("prompt_eval_count", 0) + final.get("eval_count", 0)
388
400
  seconds = final.get("eval_duration", 0) / 1e9
389
401
  self.last_speed = final.get("eval_count", 0) / seconds if seconds else 0.0
@@ -406,6 +418,17 @@ class Agent:
406
418
  return name
407
419
 
408
420
  def run_turn(self, user_text: str) -> None:
421
+ self.checkpoints.begin_turn(self.session_id, user_text, len(self.messages))
422
+ try:
423
+ self._run_turn(user_text)
424
+ finally:
425
+ self.checkpoints.end_turn()
426
+
427
+ def checkpoint(self) -> None:
428
+ """Called before the model changes files: snapshot them once per request, for /undo."""
429
+ self.checkpoints.before_change(self.cwd)
430
+
431
+ def _run_turn(self, user_text: str) -> None:
409
432
  self.messages.append({"role": "user", "content": self.expand_mentions(user_text)})
410
433
  malformed = 0
411
434
  for _ in range(MAX_STEPS_PER_TURN):