agent-code 2.0.0__py3-none-any.whl → 2.0.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.
agent_code/__init__.py CHANGED
@@ -1,2 +1,2 @@
1
1
  """Agent + Skill Store — local AI agent with headless browser and skills."""
2
- __version__ = "2.0.0"
2
+ __version__ = "2.0.1"
agent_code/agent.py CHANGED
@@ -655,7 +655,7 @@ MAX_LOG_LINES = 900
655
655
  MAX_LOG_BATCH = 40
656
656
  MAX_RENDERED_RESPONSE_CHARS = 24000
657
657
  MAX_USER_MESSAGE_CHARS = 100000
658
- LLM_TIMEOUT = 120
658
+ LLM_TIMEOUT = 600
659
659
 
660
660
  PERMISSION_READONLY = "readonly"
661
661
  PERMISSION_ASK = "ask"
@@ -6364,6 +6364,12 @@ class App:
6364
6364
  self.activity.see("end")
6365
6365
  self.activity.configure(state="disabled")
6366
6366
  continue
6367
+ if tag in ("stream_chunk", "stream_end"):
6368
+ self.log.configure(state="normal")
6369
+ self.log.insert("end", text if tag == "stream_chunk" else "\n")
6370
+ self.log.see("end")
6371
+ self.log.configure(state="disabled")
6372
+ continue
6367
6373
  self.log.configure(state="normal")
6368
6374
  if tag == "agent_md":
6369
6375
  # Bound markdown parsing/rendering cost for very long answers.
@@ -6827,12 +6833,70 @@ class App:
6827
6833
  if self.execution_mode == "build" else
6828
6834
  [schema for schema in self.all_schemas
6829
6835
  if schema.get("function", {}).get("name") in plan_read_only])
6830
- resp = self._run_async(self._async_create_completion(
6836
+ # Stream model output into the desktop transcript as it arrives.
6837
+ # Tool calls are accumulated by index and are not dispatched until
6838
+ # the provider has finished sending the complete arguments.
6839
+ stream = self._create_completion_with_retry(
6831
6840
  purpose="the model response",
6832
6841
  model=self.model, messages=self.messages,
6833
6842
  tools=compact_tool_schemas(active_schemas) or None,
6834
6843
  tool_choice=("auto" if active_schemas else "none"),
6835
- temperature=0.2, timeout=LLM_TIMEOUT))
6844
+ temperature=0.2, timeout=LLM_TIMEOUT, stream=True)
6845
+ content_parts = []
6846
+ tool_parts = {}
6847
+ try:
6848
+ for chunk in stream:
6849
+ if self._stop_event.is_set():
6850
+ close = getattr(stream, "close", None)
6851
+ if callable(close):
6852
+ close()
6853
+ break
6854
+ choices = getattr(chunk, "choices", None) or []
6855
+ if not choices:
6856
+ continue
6857
+ delta = getattr(choices[0], "delta", None)
6858
+ if delta is None:
6859
+ continue
6860
+ piece = getattr(delta, "content", None)
6861
+ if isinstance(piece, str) and piece:
6862
+ content_parts.append(piece)
6863
+ self.log_line(piece, "stream_chunk")
6864
+ for call in (getattr(delta, "tool_calls", None) or []):
6865
+ index = getattr(call, "index", 0)
6866
+ slot = tool_parts.setdefault(index, {"id": "", "name": "", "arguments": ""})
6867
+ if getattr(call, "id", None):
6868
+ slot["id"] = call.id
6869
+ function = getattr(call, "function", None)
6870
+ if function is not None:
6871
+ if getattr(function, "name", None):
6872
+ slot["name"] += function.name
6873
+ if getattr(function, "arguments", None):
6874
+ slot["arguments"] += function.arguments
6875
+ finally:
6876
+ close = getattr(stream, "close", None)
6877
+ if callable(close):
6878
+ try:
6879
+ close()
6880
+ except Exception:
6881
+ pass
6882
+ if self._stop_event.is_set():
6883
+ self.log_line("[stopped during response streaming]", "error")
6884
+ self._stopped_last = True
6885
+ self._can_continue = True
6886
+ self._last_run_status = "stopped"
6887
+ return
6888
+ from types import SimpleNamespace
6889
+ assembled_calls = []
6890
+ for index, slot in sorted(tool_parts.items()):
6891
+ if not slot["id"]:
6892
+ slot["id"] = "call_" + uuid.uuid4().hex
6893
+ assembled_calls.append(SimpleNamespace(
6894
+ id=slot["id"],
6895
+ function=SimpleNamespace(name=slot["name"], arguments=slot["arguments"])))
6896
+ if content_parts:
6897
+ self.log_line("", "stream_end")
6898
+ msg = SimpleNamespace(content="".join(content_parts), tool_calls=assembled_calls)
6899
+ resp = SimpleNamespace(choices=[SimpleNamespace(message=msg)])
6836
6900
  except Exception as e:
6837
6901
  raw_error = f"{type(e).__name__}: {e}"
6838
6902
  import re
@@ -6869,7 +6933,9 @@ class App:
6869
6933
  if self._last_agent_answer.lower() in {"(done)", "done", "(completed)", "completed"}:
6870
6934
  self.log_line("Done.", "dim")
6871
6935
  elif self._last_agent_answer:
6872
- self.log_line(self._last_agent_answer, "agent_md")
6936
+ # The response has already been appended incrementally through
6937
+ # stream_chunk events; do not print it a second time.
6938
+ pass
6873
6939
  else:
6874
6940
  self.log_line("Done.", "dim")
6875
6941
  self._can_continue = False
agent_code/tui.py CHANGED
@@ -39,7 +39,7 @@ def api_keys(cfg):
39
39
  class TUI:
40
40
  def __init__(self, screen):
41
41
  self.s=screen; self.cfg=read_config(); self.messages=[]; self.notice="Welcome. Press ? for help."; self.mode="Build"
42
- self.input=""; self.scroll=0; self.cwd="."; self.page="Chat"; self.running=True; self.busy=False; self.cancel_event=threading.Event(); self.events=queue.Queue(); self.activity=["Ready — send a task to watch agent progress."]; self.tool_details=[]
42
+ self.input=""; self.scroll=0; self.cwd="."; self.page="Chat"; self.running=True; self.busy=False; self.cancel_event=threading.Event(); self.events=queue.Queue(); self.activity=["Ready — send a task to watch agent progress."]; self.tool_details=[]; self.activity_scroll=0; self.stream_text=""
43
43
  curses.curs_set(1); curses.start_color(); curses.use_default_colors()
44
44
  curses.init_pair(1, curses.COLOR_CYAN, -1); curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_CYAN)
45
45
  curses.init_pair(3, curses.COLOR_GREEN, -1); curses.init_pair(4, curses.COLOR_YELLOW, -1)
@@ -137,14 +137,24 @@ class TUI:
137
137
  elif self.page=="Activity":
138
138
  self.safe_add(4,content,"LIVE ACTIVITY — tool calls and results, not private model reasoning",curses.color_pair(1))
139
139
  self.safe_add(6,content,"Workspace: "+str(WORKSPACE))
140
- for i,line in enumerate(self.activity[-max(1,h-12):]):
140
+ # Activity is a complete chronological event log. Tool arguments and
141
+ # results are included here (never private model reasoning).
142
+ lines = []
143
+ for item in self.activity:
144
+ lines.extend(textwrap.wrap(str(item), max(10,w-content-3), replace_whitespace=False) or [""])
145
+ for item in self.tool_details:
146
+ lines.append("─ TOOL DETAIL ─")
147
+ lines.extend(textwrap.wrap(str(item), max(10,w-content-3), replace_whitespace=False) or [""])
148
+ viewport=max(1,h-12)
149
+ max_back=max(0,len(lines)-viewport)
150
+ self.activity_scroll=max(0,min(self.activity_scroll,max_back))
151
+ end=len(lines)-self.activity_scroll
152
+ start=max(0,end-viewport)
153
+ for i,line in enumerate(lines[start:end]):
141
154
  self.safe_add(8+i,content,line)
142
- if self.tool_details:
143
- self.safe_add(max(8,h-5),content,"LATEST TOOL OUTPUT",curses.color_pair(1)|curses.A_BOLD)
144
- detail = self.tool_details[-1]
145
- for j,line in enumerate(textwrap.wrap(detail, max(10,w-content-3))[:3]):
146
- self.safe_add(max(9,h-4)+j,content,line)
147
- self.safe_add(h-3,content,"Updates and recent tool output. Press 1 for Chat.",curses.A_DIM)
155
+ if max_back:
156
+ self.safe_add(6,content,f"Activity log · {len(lines)} lines · {self.activity_scroll} lines back (PgUp/PgDn)",curses.A_DIM)
157
+ self.safe_add(h-3,content,"Full activity and tool details. Press 1 for Chat.",curses.A_DIM)
148
158
  elif self.page=="Skills":
149
159
  d=paths.global_skills_dir(); names=sorted(p.name for p in d.iterdir() if p.is_dir()) if d.exists() else []
150
160
  self.safe_add(4,content,f"Installed skills: {len(names)}")
@@ -314,7 +324,7 @@ class TUI:
314
324
  client=make_client(keys,base,provider)
315
325
  if client is None:
316
326
  self.messages.append(("assistant","No API key configured. Add one or more keys in Settings.")); return
317
- client.timeout=30
327
+ client.timeout=600
318
328
  # Use the same core workspace tools as the desktop agent, not a chat-only stub.
319
329
  names={"read_file","write_file","append_file","edit_file","glob_files","grep_files","shell_run","background_shell","web_search","web_fetch","task","task_status"}
320
330
  schemas=[x for x in engine.CORE_SCHEMAS if x.get("function",{}).get("name") in names]
@@ -378,8 +388,58 @@ class TUI:
378
388
  self.activity.append("Task stopped by user.")
379
389
  break
380
390
  self.activity.append("Requesting model response…")
381
- response=client.chat.completions.create(model=model,messages=history,tools=active_schemas or None,tool_choice=("auto" if active_schemas else "none"))
382
- msg=response.choices[0].message
391
+ # Stream every response so slow/large generations remain visible.
392
+ # Assemble tool-call deltas before dispatch; partial JSON is never run.
393
+ self.stream_text = ""
394
+ self.messages.append(("assistant", ""))
395
+ stream = client.chat.completions.create(
396
+ model=model, messages=history, tools=active_schemas or None,
397
+ tool_choice=("auto" if active_schemas else "none"), stream=True,
398
+ timeout=600,
399
+ )
400
+ content_parts=[]
401
+ tool_parts={}
402
+ try:
403
+ for chunk in stream:
404
+ if self.cancel_event.is_set():
405
+ close=getattr(stream,"close",None)
406
+ if callable(close): close()
407
+ break
408
+ choices=getattr(chunk,"choices",[]) or []
409
+ if not choices: continue
410
+ delta=getattr(choices[0],"delta",None)
411
+ if delta is None: continue
412
+ piece=getattr(delta,"content",None)
413
+ if isinstance(piece,str) and piece:
414
+ content_parts.append(piece)
415
+ self.stream_text += piece
416
+ self.messages[-1] = ("assistant", self.stream_text)
417
+ for call in (getattr(delta,"tool_calls",None) or []):
418
+ idx=getattr(call,"index",0)
419
+ slot=tool_parts.setdefault(idx,{"id":"","name":"","arguments":""})
420
+ if getattr(call,"id",None): slot["id"]=call.id
421
+ fn=getattr(call,"function",None)
422
+ if fn:
423
+ if getattr(fn,"name",None): slot["name"] += fn.name
424
+ if getattr(fn,"arguments",None): slot["arguments"] += fn.arguments
425
+ finally:
426
+ close=getattr(stream,"close",None)
427
+ if callable(close):
428
+ try: close()
429
+ except Exception: pass
430
+ if self.cancel_event.is_set():
431
+ answer="Task stopped by user."
432
+ self.activity.append("Task stopped by user during streaming.")
433
+ break
434
+ from types import SimpleNamespace
435
+ assembled_calls=[]
436
+ for idx,slot in sorted(tool_parts.items()):
437
+ if not slot["id"]: slot["id"]="call_"+uuid.uuid4().hex
438
+ assembled_calls.append(SimpleNamespace(id=slot["id"],function=SimpleNamespace(name=slot["name"],arguments=slot["arguments"])))
439
+ msg=SimpleNamespace(content="".join(content_parts),tool_calls=assembled_calls)
440
+ # The streamed assistant placeholder becomes commentary when tools
441
+ # follow, or the final answer when no tools were requested.
442
+ self.messages.pop()
383
443
  if self.cancel_event.is_set():
384
444
  answer = "Task stopped by user."
385
445
  self.activity.append("Task stopped by user.")
@@ -439,10 +499,16 @@ class TUI:
439
499
  # Keep full tool data for the model, but show only a compact
440
500
  # outcome in Chat so the final answer remains visible.
441
501
  self.messages.append(("tool", user_result))
442
- self.tool_details.append(f"{name}: {result_text[:1800]}")
443
- self.tool_details = self.tool_details[-30:]
502
+ self.tool_details.append(
503
+ f"TOOL: {name}\nARGUMENTS: {json.dumps(args, ensure_ascii=False, default=str)}\nRESULT: {result_text[:12000]}"
504
+ )
505
+ self.tool_details = self.tool_details[-100:]
444
506
  history.append({"role":"tool","tool_call_id":tc.id,"content":result_text[:12000]})
445
- except Exception as e: answer=f"Request failed: {e}"
507
+ except Exception as e:
508
+ answer=f"Request failed: {type(e).__name__}: {e}"
509
+ if self.messages and self.messages[-1][0] == "assistant" and not self.messages[-1][1]:
510
+ self.messages.pop()
511
+ self.activity.append("Provider request failed: " + answer[:500])
446
512
  self.messages.append(("assistant",answer)); self.activity.append("Task finished."); self.activity=self.activity[-200:]; self.notice="Ready"; self.busy=False
447
513
 
448
514
  def install_podman(self):
@@ -480,6 +546,7 @@ class TUI:
480
546
  self.messages.clear()
481
547
  self.scroll = 0
482
548
  self.tool_details.clear()
549
+ self.activity_scroll = 0
483
550
  self.activity = ["Chat cleared."]
484
551
  self.notice = "Chat cleared."
485
552
 
@@ -596,10 +663,14 @@ class TUI:
596
663
  self.input += ch
597
664
  continue
598
665
  if ch == curses.KEY_PPAGE:
599
- self.scroll += max(3, self.s.getmaxyx()[0] // 2)
666
+ amount=max(3,self.s.getmaxyx()[0]//2)
667
+ if self.page=="Activity": self.activity_scroll += amount
668
+ else: self.scroll += amount
600
669
  continue
601
670
  if ch == curses.KEY_NPAGE:
602
- self.scroll = max(0, self.scroll - max(3, self.s.getmaxyx()[0] // 2))
671
+ amount=max(3,self.s.getmaxyx()[0]//2)
672
+ if self.page=="Activity": self.activity_scroll=max(0,self.activity_scroll-amount)
673
+ else: self.scroll=max(0,self.scroll-amount)
603
674
  continue
604
675
  if ch == curses.KEY_HOME and self.page == "Chat":
605
676
  self.scroll = 10**9