nanocode-cli 0.3.27__tar.gz → 0.3.28__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nanocode-cli
3
- Version: 0.3.27
3
+ Version: 0.3.28
4
4
  Summary: A lightweight terminal-based AI coding assistant
5
5
  Author-email: hit9 <hit9@icloud.com>
6
6
  License-Expression: BSD-3-Clause
@@ -51,7 +51,7 @@ from prompt_toolkit.output.defaults import create_output
51
51
  from prompt_toolkit.patch_stdout import patch_stdout
52
52
  from prompt_toolkit.styles import Style
53
53
 
54
- __version__ = "0.3.27"
54
+ __version__ = "0.3.28"
55
55
  HTTP_USER_AGENT = "nanocode/" + __version__
56
56
 
57
57
 
@@ -821,6 +821,10 @@ class RuntimeState:
821
821
  current_model_call_started_at: float = 0.0
822
822
  current_model_call_label: str = ""
823
823
  current_model_call_reasoning_label: str = ""
824
+ current_model_call_activity: str = ""
825
+ current_model_call_has_content: bool = False
826
+ status_notice: str = ""
827
+ status_notice_until: float = 0.0
824
828
  conversation: list[ConversationItem] = field(default_factory=list)
825
829
  user_rules: UserRules = field(default_factory=UserRules)
826
830
  range_fingerprints: RangeFingerprintStore = field(default_factory=RangeFingerprintStore)
@@ -3383,9 +3387,10 @@ class ModelClient:
3383
3387
 
3384
3388
  def __init__(self, session: Session):
3385
3389
  self.session = session
3390
+ self._timeout_reason = "request model timeout"
3386
3391
 
3387
3392
  def _timeout_handler(self, signum: int, frame: Any) -> None:
3388
- raise ModelRequestTimeout()
3393
+ raise ModelRequestTimeout(self._timeout_reason)
3389
3394
 
3390
3395
  def request(
3391
3396
  self,
@@ -3439,9 +3444,12 @@ class ModelClient:
3439
3444
  self.session.state.current_model_call_started_at = time.monotonic()
3440
3445
  self.session.state.current_model_call_label = model
3441
3446
  self.session.state.current_model_call_reasoning_label = config.reasoning_effort if config.reasoning else "off"
3447
+ self.session.state.current_model_call_activity = activity
3448
+ self.session.state.current_model_call_has_content = False
3442
3449
  request_deadline = self.session.state.current_model_call_started_at + max(0, timeout)
3443
3450
  previous_handler = signal.getsignal(signal.SIGALRM)
3444
3451
  signal.signal(signal.SIGALRM, self._timeout_handler)
3452
+ self._timeout_reason = "request model timeout"
3445
3453
  signal.setitimer(signal.ITIMER_REAL, max(0, timeout))
3446
3454
  try:
3447
3455
  with urllib.request.urlopen(request, timeout=timeout) as response:
@@ -3460,8 +3468,10 @@ class ModelClient:
3460
3468
  self.session.state.current_model_call_started_at = 0.0
3461
3469
  self.session.state.current_model_call_label = ""
3462
3470
  self.session.state.current_model_call_reasoning_label = ""
3463
- except ModelRequestTimeout:
3464
- raise LLMError("request model timeout")
3471
+ self.session.state.current_model_call_activity = ""
3472
+ self.session.state.current_model_call_has_content = False
3473
+ except ModelRequestTimeout as error:
3474
+ raise LLMError(str(error) or "request model timeout")
3465
3475
  except (socket.timeout, TimeoutError):
3466
3476
  raise LLMError("request model timeout")
3467
3477
  except urllib.error.HTTPError as error:
@@ -3525,6 +3535,7 @@ class ModelClient:
3525
3535
  continue
3526
3536
  if not first_content_seen:
3527
3537
  first_content_seen = True
3538
+ self.session.state.current_model_call_has_content = True
3528
3539
  self._arm_stream_timeout(request_deadline=request_deadline, first_content_seen=True, first_token_timeout=first_token_timeout)
3529
3540
  parts.append(content)
3530
3541
  return "".join(parts), usage
@@ -3532,9 +3543,12 @@ class ModelClient:
3532
3543
  def _arm_stream_timeout(self, *, request_deadline: float, first_content_seen: bool, first_token_timeout: int | None) -> None:
3533
3544
  remaining = request_deadline - time.monotonic()
3534
3545
  if remaining <= 0:
3535
- raise ModelRequestTimeout()
3546
+ raise ModelRequestTimeout("request model timeout")
3547
+ self._timeout_reason = "request model timeout"
3536
3548
  if not first_content_seen and first_token_timeout is not None and first_token_timeout > 0:
3537
- remaining = min(remaining, first_token_timeout)
3549
+ if first_token_timeout < remaining:
3550
+ remaining = first_token_timeout
3551
+ self._timeout_reason = "request first token timeout"
3538
3552
  signal.setitimer(signal.ITIMER_REAL, remaining)
3539
3553
 
3540
3554
  def _write_debug_prompt(self, *, activity: str, messages: list[Json]) -> str:
@@ -3644,6 +3658,7 @@ class ModelClient:
3644
3658
  if isinstance(value, dict):
3645
3659
  if "actions" in value:
3646
3660
  return self._actions_from_json_value(value.get("actions"))
3661
+ self._normalize_tool_type(value)
3647
3662
  if not _json_str(value.get("type")):
3648
3663
  return [], "action missing type"
3649
3664
  return [value], ""
@@ -3653,12 +3668,19 @@ class ModelClient:
3653
3668
  action = _json_dict(raw)
3654
3669
  if not action:
3655
3670
  return [], "array item " + str(index) + ": expected JSON object action"
3671
+ self._normalize_tool_type(action)
3656
3672
  if not _json_str(action.get("type")):
3657
3673
  return [], "array item " + str(index) + ": action missing type"
3658
3674
  actions.append(action)
3659
3675
  return actions, ""
3660
3676
  return [], "expected JSON object action"
3661
3677
 
3678
+ def _normalize_tool_type(self, action: Json) -> None:
3679
+ action_type = _json_str(action.get("type"))
3680
+ if action_type in TOOL_REGISTRY:
3681
+ action["type"] = "tool"
3682
+ action.setdefault("name", action_type)
3683
+
3662
3684
  def _parse_unmarked_actions(self, text: str) -> tuple[list[Json], str]:
3663
3685
  actions: list[Json] = []
3664
3686
  decoder = json.JSONDecoder()
@@ -3669,8 +3691,8 @@ class ModelClient:
3669
3691
  if index < len(text) and text[index] != "{":
3670
3692
  if text[index] == "[":
3671
3693
  try:
3672
- value, index = decoder.raw_decode(text, index)
3673
- except json.JSONDecodeError as error:
3694
+ value, index = self._decode_json_array_text(text, index)
3695
+ except (json.JSONDecodeError, ValueError) as error:
3674
3696
  return [], str(error)
3675
3697
  parsed, error = self._actions_from_json_value(value)
3676
3698
  if error:
@@ -3708,6 +3730,28 @@ class ModelClient:
3708
3730
  index += 1
3709
3731
  if index < len(text) and text[index] == ",":
3710
3732
  index += 1
3733
+ continue
3734
+ if index < len(text) and text[index] != "{":
3735
+ next_action = text.find("{", index)
3736
+ if next_action < 0:
3737
+ return [], "unexpected text after JSON action"
3738
+ progress = _shorten(" ".join(text[index:next_action].split()), 500)
3739
+ if progress:
3740
+ actions.append({"type": "progress", "text": progress})
3741
+ index = next_action
3742
+
3743
+ def _decode_json_array_text(self, text: str, index: int) -> tuple[JsonValue, int]:
3744
+ decoder = json.JSONDecoder()
3745
+ value, end = decoder.raw_decode(text, index)
3746
+ cursor = end
3747
+ while cursor < len(text) and text[cursor].isspace():
3748
+ cursor += 1
3749
+ if cursor >= len(text):
3750
+ return value, cursor
3751
+ value = json_repair.loads(text[index:])
3752
+ if not isinstance(value, list):
3753
+ raise ValueError("expected JSON action array")
3754
+ return value, len(text)
3711
3755
 
3712
3756
  def _has_action_frame_end(self, line: str) -> bool:
3713
3757
  return self.ACTION_FRAME_END_SPLIT_PATTERN.search(line) is not None
@@ -4793,12 +4837,14 @@ class Agent:
4793
4837
  self.session.state.turn_model_calls += 1
4794
4838
  return self.model_client.request(system_prompt, user_prompt, activity=activity)
4795
4839
  except LLMError as error:
4796
- if str(error) != "request model timeout" or attempt >= len(self.MODEL_TIMEOUT_RETRY_DELAYS):
4840
+ timeout_reason = str(error)
4841
+ if timeout_reason not in ("request model timeout", "request first token timeout") or attempt >= len(self.MODEL_TIMEOUT_RETRY_DELAYS):
4797
4842
  raise
4798
4843
  delay = self.MODEL_TIMEOUT_RETRY_DELAYS[attempt]
4844
+ self._set_status_notice("err:first_token" if timeout_reason == "request first token timeout" else "err:timeout")
4799
4845
  if on_message is not None and self.session.settings.debug:
4800
4846
  on_message(
4801
- "Retrying: request model timeout; retry "
4847
+ "Retrying: " + timeout_reason + "; retry "
4802
4848
  + str(attempt + 1)
4803
4849
  + "/"
4804
4850
  + str(len(self.MODEL_TIMEOUT_RETRY_DELAYS))
@@ -4809,6 +4855,10 @@ class Agent:
4809
4855
  time.sleep(delay)
4810
4856
  raise LLMError("request model timeout")
4811
4857
 
4858
+ def _set_status_notice(self, text: str, ttl: float = 5.0) -> None:
4859
+ self.session.state.status_notice = text
4860
+ self.session.state.status_notice_until = time.monotonic() + ttl
4861
+
4812
4862
  def compact_history(self) -> int:
4813
4863
  return self.compactor.compact()
4814
4864
 
@@ -4834,6 +4884,7 @@ class Agent:
4834
4884
  format_error = _json_str(response.get("_format_error"))
4835
4885
  if format_error:
4836
4886
  consecutive_format_errors += 1
4887
+ self._set_status_notice("err:format")
4837
4888
  remember_error = self._remember_observe_error if self.mode == AgentMode.OBSERVE else self._remember_agent_error
4838
4889
  remember_error(
4839
4890
  self._format_gate_user_message("Error: model returned invalid output", format_error) + " Rule: return valid JSON action frames only."
@@ -4919,6 +4970,8 @@ class Agent:
4919
4970
  def _report_gate(self, on_message: MessageCallback | None, message: str, debug_message: str) -> None:
4920
4971
  if on_message is None:
4921
4972
  return
4973
+ if message.startswith(("Retrying:", "Continuing:")) and self.session.state.status_notice_until <= time.monotonic():
4974
+ self._set_status_notice("err:gate")
4922
4975
  if self.session.settings.debug:
4923
4976
  on_message(debug_message)
4924
4977
  return
@@ -4945,13 +4998,15 @@ class Agent:
4945
4998
  if self.mode == AgentMode.OBSERVE:
4946
4999
  system_prompt = self.prompt_builder.system_prompt(AGENT_OBSERVE_SYSTEM_PROMPT, tools=())
4947
5000
  user_prompt = self.build_observe_prompt()
5001
+ activity = "observe"
4948
5002
  else:
4949
5003
  system_prompt = self.prompt_builder.system_prompt(
4950
5004
  AGENT_PLAN_SYSTEM_PROMPT if self.session.settings.plan_mode else None,
4951
5005
  tools=PLAN_MODE_TOOLS if self.session.settings.plan_mode else None,
4952
5006
  )
4953
5007
  user_prompt = self.build_user_prompt()
4954
- response = self.request(system_prompt, user_prompt, activity="agent", on_message=on_message)
5008
+ activity = "agent"
5009
+ response = self.request(system_prompt, user_prompt, activity=activity, on_message=on_message)
4955
5010
  if _json_str(response.get("_format_error")):
4956
5011
  return response
4957
5012
  invalid_response = self._validate_action_response(response)
@@ -5297,29 +5352,6 @@ class Agent:
5297
5352
  conflict = sorted((forgotten & protected) - released)
5298
5353
  return "active hypothesis source: " + ", ".join(conflict) if conflict else ""
5299
5354
 
5300
- def _plan_shape_error(self, actions: list[Json]) -> str:
5301
- plan = [PlanItem(text=item.text, status=item.status, id=item.id, context=item.context) for item in self.blackboard.plan]
5302
- changed = False
5303
- for action in actions:
5304
- action_type = _json_str(action.get("type"))
5305
- if action_type == "start":
5306
- items = self._plan_items_from_json(action.get("plan"))
5307
- if items:
5308
- plan = items
5309
- changed = True
5310
- elif action_type == "plan":
5311
- items = self._plan_items_from_json(action.get("items"))
5312
- if action.get("mode") != "patch":
5313
- if items:
5314
- plan = items
5315
- changed = True
5316
- continue
5317
- changed = self.state_updater._apply_plan_patches(plan, action.get("items")) or changed
5318
- doing = [item for item in plan if item.status == PlanStatus.DOING]
5319
- if changed and len(doing) > 1:
5320
- return "multiple doing plan items: " + self._format_plan_gate_items(doing)
5321
- return ""
5322
-
5323
5355
  def _plan_items_from_json(self, value: JsonValue) -> list[PlanItem]:
5324
5356
  return [item for item in (self.state_updater._plan_item_from_json(raw) for raw in _json_list(value)) if item]
5325
5357
 
@@ -5427,15 +5459,6 @@ class Agent:
5427
5459
  "ToolResult_Gate: " + forget_hypothesis_error + ".",
5428
5460
  )
5429
5461
  return True
5430
- plan_shape_error = self._plan_shape_error(ctx.actions)
5431
- if plan_shape_error:
5432
- self._remember_agent_error("Error: Plan is invalid: " + plan_shape_error + ". Rule: at most one Plan item may be doing.")
5433
- self._report_gate(
5434
- on_message,
5435
- "Retrying: keep only one plan item doing.",
5436
- "Plan_Gate: " + plan_shape_error + ".",
5437
- )
5438
- return True
5439
5462
  repeated_tool_retry_error = self._repeated_tool_retry_error(ctx.tool_calls)
5440
5463
  if repeated_tool_retry_error:
5441
5464
  self._remember_agent_error(
@@ -5732,15 +5755,6 @@ class Agent:
5732
5755
  "ToolResult_Gate: " + forget_hypothesis_error + ".",
5733
5756
  )
5734
5757
  return AgentRunResult()
5735
- plan_shape_error = self._plan_shape_error(ctx.actions)
5736
- if plan_shape_error:
5737
- self._remember_observe_error("Error: Plan is invalid: " + plan_shape_error + ". Rule: at most one Plan item may be doing.")
5738
- self._report_gate(
5739
- on_message,
5740
- "Retrying: keep only one plan item doing.",
5741
- "Plan_Gate: " + plan_shape_error + ".",
5742
- )
5743
- return AgentRunResult()
5744
5758
  if any(_json_str(action.get("type")) == "verify" and _json_str(action.get("status")) == "pending" for action in ctx.actions):
5745
5759
  self._remember_observe_error("Error: cannot request new verification before observing latest results. Rule: keep or forget latest results first.")
5746
5760
  self._report_gate(
@@ -6647,9 +6661,24 @@ class StatusBar:
6647
6661
  if show_elapsed:
6648
6662
  parts.append(f"{turn_elapsed:.1f}s")
6649
6663
  if session.state.current_model_call_started_at > 0:
6650
- parts.append("calling(" + str(session.state.turn_model_calls) + "):" + f"{max(0.0, now - session.state.current_model_call_started_at):.1f}s")
6664
+ activity = self._activity_label(session.state.current_model_call_activity)
6665
+ if session.state.current_model_call_has_content:
6666
+ activity += "*"
6667
+ parts.append(
6668
+ activity
6669
+ + "("
6670
+ + str(session.state.turn_model_calls)
6671
+ + "):"
6672
+ + f"{max(0.0, now - session.state.current_model_call_started_at):.1f}s"
6673
+ )
6674
+ if session.state.status_notice and session.state.status_notice_until > now:
6675
+ parts.append(session.state.status_notice)
6651
6676
  return " | ".join(parts)
6652
6677
 
6678
+ @staticmethod
6679
+ def _activity_label(activity: str) -> str:
6680
+ return {"compact": "compacting", "observe": "observing"}.get(activity, "working")
6681
+
6653
6682
  def _sweep_fragments(self, text: str, now: float) -> list[tuple[str, str]]:
6654
6683
  if not text:
6655
6684
  return [("", "")]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nanocode-cli
3
- Version: 0.3.27
3
+ Version: 0.3.28
4
4
  Summary: A lightweight terminal-based AI coding assistant
5
5
  Author-email: hit9 <hit9@icloud.com>
6
6
  License-Expression: BSD-3-Clause
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "nanocode-cli"
7
- version = "0.3.27"
7
+ version = "0.3.28"
8
8
  description = "A lightweight terminal-based AI coding assistant"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
File without changes
File without changes
File without changes
File without changes