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.
- {nanocode_cli-0.3.27/nanocode_cli.egg-info → nanocode_cli-0.3.28}/PKG-INFO +1 -1
- {nanocode_cli-0.3.27 → nanocode_cli-0.3.28}/nanocode.py +82 -53
- {nanocode_cli-0.3.27 → nanocode_cli-0.3.28/nanocode_cli.egg-info}/PKG-INFO +1 -1
- {nanocode_cli-0.3.27 → nanocode_cli-0.3.28}/pyproject.toml +1 -1
- {nanocode_cli-0.3.27 → nanocode_cli-0.3.28}/LICENSE +0 -0
- {nanocode_cli-0.3.27 → nanocode_cli-0.3.28}/MANIFEST.in +0 -0
- {nanocode_cli-0.3.27 → nanocode_cli-0.3.28}/README.md +0 -0
- {nanocode_cli-0.3.27 → nanocode_cli-0.3.28}/nanocode_cli.egg-info/SOURCES.txt +0 -0
- {nanocode_cli-0.3.27 → nanocode_cli-0.3.28}/nanocode_cli.egg-info/dependency_links.txt +0 -0
- {nanocode_cli-0.3.27 → nanocode_cli-0.3.28}/nanocode_cli.egg-info/entry_points.txt +0 -0
- {nanocode_cli-0.3.27 → nanocode_cli-0.3.28}/nanocode_cli.egg-info/requires.txt +0 -0
- {nanocode_cli-0.3.27 → nanocode_cli-0.3.28}/nanocode_cli.egg-info/top_level.txt +0 -0
- {nanocode_cli-0.3.27 → nanocode_cli-0.3.28}/setup.cfg +0 -0
|
@@ -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.
|
|
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
|
-
|
|
3464
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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 [("", "")]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|