nanocode-cli 0.3.26__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.26/nanocode_cli.egg-info → nanocode_cli-0.3.28}/PKG-INFO +1 -1
- {nanocode_cli-0.3.26 → nanocode_cli-0.3.28}/nanocode.py +85 -54
- {nanocode_cli-0.3.26 → nanocode_cli-0.3.28/nanocode_cli.egg-info}/PKG-INFO +1 -1
- {nanocode_cli-0.3.26 → nanocode_cli-0.3.28}/pyproject.toml +1 -1
- {nanocode_cli-0.3.26 → nanocode_cli-0.3.28}/LICENSE +0 -0
- {nanocode_cli-0.3.26 → nanocode_cli-0.3.28}/MANIFEST.in +0 -0
- {nanocode_cli-0.3.26 → nanocode_cli-0.3.28}/README.md +0 -0
- {nanocode_cli-0.3.26 → nanocode_cli-0.3.28}/nanocode_cli.egg-info/SOURCES.txt +0 -0
- {nanocode_cli-0.3.26 → nanocode_cli-0.3.28}/nanocode_cli.egg-info/dependency_links.txt +0 -0
- {nanocode_cli-0.3.26 → nanocode_cli-0.3.28}/nanocode_cli.egg-info/entry_points.txt +0 -0
- {nanocode_cli-0.3.26 → nanocode_cli-0.3.28}/nanocode_cli.egg-info/requires.txt +0 -0
- {nanocode_cli-0.3.26 → nanocode_cli-0.3.28}/nanocode_cli.egg-info/top_level.txt +0 -0
- {nanocode_cli-0.3.26 → nanocode_cli-0.3.28}/setup.cfg +0 -0
|
@@ -51,7 +51,8 @@ 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
|
+
HTTP_USER_AGENT = "nanocode/" + __version__
|
|
55
56
|
|
|
56
57
|
|
|
57
58
|
JsonValue: TypeAlias = Any
|
|
@@ -820,6 +821,10 @@ class RuntimeState:
|
|
|
820
821
|
current_model_call_started_at: float = 0.0
|
|
821
822
|
current_model_call_label: str = ""
|
|
822
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
|
|
823
828
|
conversation: list[ConversationItem] = field(default_factory=list)
|
|
824
829
|
user_rules: UserRules = field(default_factory=UserRules)
|
|
825
830
|
range_fingerprints: RangeFingerprintStore = field(default_factory=RangeFingerprintStore)
|
|
@@ -3382,9 +3387,10 @@ class ModelClient:
|
|
|
3382
3387
|
|
|
3383
3388
|
def __init__(self, session: Session):
|
|
3384
3389
|
self.session = session
|
|
3390
|
+
self._timeout_reason = "request model timeout"
|
|
3385
3391
|
|
|
3386
3392
|
def _timeout_handler(self, signum: int, frame: Any) -> None:
|
|
3387
|
-
raise ModelRequestTimeout()
|
|
3393
|
+
raise ModelRequestTimeout(self._timeout_reason)
|
|
3388
3394
|
|
|
3389
3395
|
def request(
|
|
3390
3396
|
self,
|
|
@@ -3431,15 +3437,19 @@ class ModelClient:
|
|
|
3431
3437
|
headers={
|
|
3432
3438
|
"Authorization": "Bearer " + config.key,
|
|
3433
3439
|
"Content-Type": "application/json",
|
|
3440
|
+
"User-Agent": HTTP_USER_AGENT,
|
|
3434
3441
|
},
|
|
3435
3442
|
)
|
|
3436
3443
|
try:
|
|
3437
3444
|
self.session.state.current_model_call_started_at = time.monotonic()
|
|
3438
3445
|
self.session.state.current_model_call_label = model
|
|
3439
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
|
|
3440
3449
|
request_deadline = self.session.state.current_model_call_started_at + max(0, timeout)
|
|
3441
3450
|
previous_handler = signal.getsignal(signal.SIGALRM)
|
|
3442
3451
|
signal.signal(signal.SIGALRM, self._timeout_handler)
|
|
3452
|
+
self._timeout_reason = "request model timeout"
|
|
3443
3453
|
signal.setitimer(signal.ITIMER_REAL, max(0, timeout))
|
|
3444
3454
|
try:
|
|
3445
3455
|
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
@@ -3458,8 +3468,10 @@ class ModelClient:
|
|
|
3458
3468
|
self.session.state.current_model_call_started_at = 0.0
|
|
3459
3469
|
self.session.state.current_model_call_label = ""
|
|
3460
3470
|
self.session.state.current_model_call_reasoning_label = ""
|
|
3461
|
-
|
|
3462
|
-
|
|
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")
|
|
3463
3475
|
except (socket.timeout, TimeoutError):
|
|
3464
3476
|
raise LLMError("request model timeout")
|
|
3465
3477
|
except urllib.error.HTTPError as error:
|
|
@@ -3523,6 +3535,7 @@ class ModelClient:
|
|
|
3523
3535
|
continue
|
|
3524
3536
|
if not first_content_seen:
|
|
3525
3537
|
first_content_seen = True
|
|
3538
|
+
self.session.state.current_model_call_has_content = True
|
|
3526
3539
|
self._arm_stream_timeout(request_deadline=request_deadline, first_content_seen=True, first_token_timeout=first_token_timeout)
|
|
3527
3540
|
parts.append(content)
|
|
3528
3541
|
return "".join(parts), usage
|
|
@@ -3530,9 +3543,12 @@ class ModelClient:
|
|
|
3530
3543
|
def _arm_stream_timeout(self, *, request_deadline: float, first_content_seen: bool, first_token_timeout: int | None) -> None:
|
|
3531
3544
|
remaining = request_deadline - time.monotonic()
|
|
3532
3545
|
if remaining <= 0:
|
|
3533
|
-
raise ModelRequestTimeout()
|
|
3546
|
+
raise ModelRequestTimeout("request model timeout")
|
|
3547
|
+
self._timeout_reason = "request model timeout"
|
|
3534
3548
|
if not first_content_seen and first_token_timeout is not None and first_token_timeout > 0:
|
|
3535
|
-
|
|
3549
|
+
if first_token_timeout < remaining:
|
|
3550
|
+
remaining = first_token_timeout
|
|
3551
|
+
self._timeout_reason = "request first token timeout"
|
|
3536
3552
|
signal.setitimer(signal.ITIMER_REAL, remaining)
|
|
3537
3553
|
|
|
3538
3554
|
def _write_debug_prompt(self, *, activity: str, messages: list[Json]) -> str:
|
|
@@ -3642,6 +3658,7 @@ class ModelClient:
|
|
|
3642
3658
|
if isinstance(value, dict):
|
|
3643
3659
|
if "actions" in value:
|
|
3644
3660
|
return self._actions_from_json_value(value.get("actions"))
|
|
3661
|
+
self._normalize_tool_type(value)
|
|
3645
3662
|
if not _json_str(value.get("type")):
|
|
3646
3663
|
return [], "action missing type"
|
|
3647
3664
|
return [value], ""
|
|
@@ -3651,12 +3668,19 @@ class ModelClient:
|
|
|
3651
3668
|
action = _json_dict(raw)
|
|
3652
3669
|
if not action:
|
|
3653
3670
|
return [], "array item " + str(index) + ": expected JSON object action"
|
|
3671
|
+
self._normalize_tool_type(action)
|
|
3654
3672
|
if not _json_str(action.get("type")):
|
|
3655
3673
|
return [], "array item " + str(index) + ": action missing type"
|
|
3656
3674
|
actions.append(action)
|
|
3657
3675
|
return actions, ""
|
|
3658
3676
|
return [], "expected JSON object action"
|
|
3659
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
|
+
|
|
3660
3684
|
def _parse_unmarked_actions(self, text: str) -> tuple[list[Json], str]:
|
|
3661
3685
|
actions: list[Json] = []
|
|
3662
3686
|
decoder = json.JSONDecoder()
|
|
@@ -3667,8 +3691,8 @@ class ModelClient:
|
|
|
3667
3691
|
if index < len(text) and text[index] != "{":
|
|
3668
3692
|
if text[index] == "[":
|
|
3669
3693
|
try:
|
|
3670
|
-
value, index =
|
|
3671
|
-
except json.JSONDecodeError as error:
|
|
3694
|
+
value, index = self._decode_json_array_text(text, index)
|
|
3695
|
+
except (json.JSONDecodeError, ValueError) as error:
|
|
3672
3696
|
return [], str(error)
|
|
3673
3697
|
parsed, error = self._actions_from_json_value(value)
|
|
3674
3698
|
if error:
|
|
@@ -3706,6 +3730,28 @@ class ModelClient:
|
|
|
3706
3730
|
index += 1
|
|
3707
3731
|
if index < len(text) and text[index] == ",":
|
|
3708
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)
|
|
3709
3755
|
|
|
3710
3756
|
def _has_action_frame_end(self, line: str) -> bool:
|
|
3711
3757
|
return self.ACTION_FRAME_END_SPLIT_PATTERN.search(line) is not None
|
|
@@ -4791,12 +4837,14 @@ class Agent:
|
|
|
4791
4837
|
self.session.state.turn_model_calls += 1
|
|
4792
4838
|
return self.model_client.request(system_prompt, user_prompt, activity=activity)
|
|
4793
4839
|
except LLMError as error:
|
|
4794
|
-
|
|
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):
|
|
4795
4842
|
raise
|
|
4796
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")
|
|
4797
4845
|
if on_message is not None and self.session.settings.debug:
|
|
4798
4846
|
on_message(
|
|
4799
|
-
"Retrying:
|
|
4847
|
+
"Retrying: " + timeout_reason + "; retry "
|
|
4800
4848
|
+ str(attempt + 1)
|
|
4801
4849
|
+ "/"
|
|
4802
4850
|
+ str(len(self.MODEL_TIMEOUT_RETRY_DELAYS))
|
|
@@ -4807,6 +4855,10 @@ class Agent:
|
|
|
4807
4855
|
time.sleep(delay)
|
|
4808
4856
|
raise LLMError("request model timeout")
|
|
4809
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
|
+
|
|
4810
4862
|
def compact_history(self) -> int:
|
|
4811
4863
|
return self.compactor.compact()
|
|
4812
4864
|
|
|
@@ -4832,6 +4884,7 @@ class Agent:
|
|
|
4832
4884
|
format_error = _json_str(response.get("_format_error"))
|
|
4833
4885
|
if format_error:
|
|
4834
4886
|
consecutive_format_errors += 1
|
|
4887
|
+
self._set_status_notice("err:format")
|
|
4835
4888
|
remember_error = self._remember_observe_error if self.mode == AgentMode.OBSERVE else self._remember_agent_error
|
|
4836
4889
|
remember_error(
|
|
4837
4890
|
self._format_gate_user_message("Error: model returned invalid output", format_error) + " Rule: return valid JSON action frames only."
|
|
@@ -4917,6 +4970,8 @@ class Agent:
|
|
|
4917
4970
|
def _report_gate(self, on_message: MessageCallback | None, message: str, debug_message: str) -> None:
|
|
4918
4971
|
if on_message is None:
|
|
4919
4972
|
return
|
|
4973
|
+
if message.startswith(("Retrying:", "Continuing:")) and self.session.state.status_notice_until <= time.monotonic():
|
|
4974
|
+
self._set_status_notice("err:gate")
|
|
4920
4975
|
if self.session.settings.debug:
|
|
4921
4976
|
on_message(debug_message)
|
|
4922
4977
|
return
|
|
@@ -4943,13 +4998,15 @@ class Agent:
|
|
|
4943
4998
|
if self.mode == AgentMode.OBSERVE:
|
|
4944
4999
|
system_prompt = self.prompt_builder.system_prompt(AGENT_OBSERVE_SYSTEM_PROMPT, tools=())
|
|
4945
5000
|
user_prompt = self.build_observe_prompt()
|
|
5001
|
+
activity = "observe"
|
|
4946
5002
|
else:
|
|
4947
5003
|
system_prompt = self.prompt_builder.system_prompt(
|
|
4948
5004
|
AGENT_PLAN_SYSTEM_PROMPT if self.session.settings.plan_mode else None,
|
|
4949
5005
|
tools=PLAN_MODE_TOOLS if self.session.settings.plan_mode else None,
|
|
4950
5006
|
)
|
|
4951
5007
|
user_prompt = self.build_user_prompt()
|
|
4952
|
-
|
|
5008
|
+
activity = "agent"
|
|
5009
|
+
response = self.request(system_prompt, user_prompt, activity=activity, on_message=on_message)
|
|
4953
5010
|
if _json_str(response.get("_format_error")):
|
|
4954
5011
|
return response
|
|
4955
5012
|
invalid_response = self._validate_action_response(response)
|
|
@@ -5295,29 +5352,6 @@ class Agent:
|
|
|
5295
5352
|
conflict = sorted((forgotten & protected) - released)
|
|
5296
5353
|
return "active hypothesis source: " + ", ".join(conflict) if conflict else ""
|
|
5297
5354
|
|
|
5298
|
-
def _plan_shape_error(self, actions: list[Json]) -> str:
|
|
5299
|
-
plan = [PlanItem(text=item.text, status=item.status, id=item.id, context=item.context) for item in self.blackboard.plan]
|
|
5300
|
-
changed = False
|
|
5301
|
-
for action in actions:
|
|
5302
|
-
action_type = _json_str(action.get("type"))
|
|
5303
|
-
if action_type == "start":
|
|
5304
|
-
items = self._plan_items_from_json(action.get("plan"))
|
|
5305
|
-
if items:
|
|
5306
|
-
plan = items
|
|
5307
|
-
changed = True
|
|
5308
|
-
elif action_type == "plan":
|
|
5309
|
-
items = self._plan_items_from_json(action.get("items"))
|
|
5310
|
-
if action.get("mode") != "patch":
|
|
5311
|
-
if items:
|
|
5312
|
-
plan = items
|
|
5313
|
-
changed = True
|
|
5314
|
-
continue
|
|
5315
|
-
changed = self.state_updater._apply_plan_patches(plan, action.get("items")) or changed
|
|
5316
|
-
doing = [item for item in plan if item.status == PlanStatus.DOING]
|
|
5317
|
-
if changed and len(doing) > 1:
|
|
5318
|
-
return "multiple doing plan items: " + self._format_plan_gate_items(doing)
|
|
5319
|
-
return ""
|
|
5320
|
-
|
|
5321
5355
|
def _plan_items_from_json(self, value: JsonValue) -> list[PlanItem]:
|
|
5322
5356
|
return [item for item in (self.state_updater._plan_item_from_json(raw) for raw in _json_list(value)) if item]
|
|
5323
5357
|
|
|
@@ -5425,15 +5459,6 @@ class Agent:
|
|
|
5425
5459
|
"ToolResult_Gate: " + forget_hypothesis_error + ".",
|
|
5426
5460
|
)
|
|
5427
5461
|
return True
|
|
5428
|
-
plan_shape_error = self._plan_shape_error(ctx.actions)
|
|
5429
|
-
if plan_shape_error:
|
|
5430
|
-
self._remember_agent_error("Error: Plan is invalid: " + plan_shape_error + ". Rule: at most one Plan item may be doing.")
|
|
5431
|
-
self._report_gate(
|
|
5432
|
-
on_message,
|
|
5433
|
-
"Retrying: keep only one plan item doing.",
|
|
5434
|
-
"Plan_Gate: " + plan_shape_error + ".",
|
|
5435
|
-
)
|
|
5436
|
-
return True
|
|
5437
5462
|
repeated_tool_retry_error = self._repeated_tool_retry_error(ctx.tool_calls)
|
|
5438
5463
|
if repeated_tool_retry_error:
|
|
5439
5464
|
self._remember_agent_error(
|
|
@@ -5730,15 +5755,6 @@ class Agent:
|
|
|
5730
5755
|
"ToolResult_Gate: " + forget_hypothesis_error + ".",
|
|
5731
5756
|
)
|
|
5732
5757
|
return AgentRunResult()
|
|
5733
|
-
plan_shape_error = self._plan_shape_error(ctx.actions)
|
|
5734
|
-
if plan_shape_error:
|
|
5735
|
-
self._remember_observe_error("Error: Plan is invalid: " + plan_shape_error + ". Rule: at most one Plan item may be doing.")
|
|
5736
|
-
self._report_gate(
|
|
5737
|
-
on_message,
|
|
5738
|
-
"Retrying: keep only one plan item doing.",
|
|
5739
|
-
"Plan_Gate: " + plan_shape_error + ".",
|
|
5740
|
-
)
|
|
5741
|
-
return AgentRunResult()
|
|
5742
5758
|
if any(_json_str(action.get("type")) == "verify" and _json_str(action.get("status")) == "pending" for action in ctx.actions):
|
|
5743
5759
|
self._remember_observe_error("Error: cannot request new verification before observing latest results. Rule: keep or forget latest results first.")
|
|
5744
5760
|
self._report_gate(
|
|
@@ -6239,7 +6255,7 @@ class CommandDispatcher:
|
|
|
6239
6255
|
base_url = base_url[: -len("/chat/completions")]
|
|
6240
6256
|
request = urllib.request.Request(
|
|
6241
6257
|
base_url + "/models",
|
|
6242
|
-
headers={"Authorization": "Bearer " + provider.key},
|
|
6258
|
+
headers={"Authorization": "Bearer " + provider.key, "User-Agent": HTTP_USER_AGENT},
|
|
6243
6259
|
)
|
|
6244
6260
|
try:
|
|
6245
6261
|
with urllib.request.urlopen(request, timeout=3) as response:
|
|
@@ -6645,9 +6661,24 @@ class StatusBar:
|
|
|
6645
6661
|
if show_elapsed:
|
|
6646
6662
|
parts.append(f"{turn_elapsed:.1f}s")
|
|
6647
6663
|
if session.state.current_model_call_started_at > 0:
|
|
6648
|
-
|
|
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)
|
|
6649
6676
|
return " | ".join(parts)
|
|
6650
6677
|
|
|
6678
|
+
@staticmethod
|
|
6679
|
+
def _activity_label(activity: str) -> str:
|
|
6680
|
+
return {"compact": "compacting", "observe": "observing"}.get(activity, "working")
|
|
6681
|
+
|
|
6651
6682
|
def _sweep_fragments(self, text: str, now: float) -> list[tuple[str, str]]:
|
|
6652
6683
|
if not text:
|
|
6653
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
|