From 455723c1daa126c4b90cd7051c668161b1593adb Mon Sep 17 00:00:00 2001 From: ye shengjie <184427520+ye1457@users.noreply.github.com> Date: Fri, 25 Sep 2026 09:51:40 +0000 Subject: [PATCH 1/2] fix(codex): enforce turn budgets for reasoning and tool-only responses --- .../rst_source/usage/configure_planner.rst | 76 +++++--- .../rst_source/usage/configure_planner.rst | 64 +++++-- rpent/planner/codex.py | 39 +++- .../rpent/planner/test_codex_contracts.py | 180 ++++++++++++++++++ 4 files changed, 309 insertions(+), 50 deletions(-) diff --git a/docs/source-en/rst_source/usage/configure_planner.rst b/docs/source-en/rst_source/usage/configure_planner.rst index 5a495a41..c762658d 100644 --- a/docs/source-en/rst_source/usage/configure_planner.rst +++ b/docs/source-en/rst_source/usage/configure_planner.rst @@ -47,6 +47,59 @@ loop is orchestrated, and which model SDK is used. - You want to re-run a known-good plan on new layouts, without online LLM planning. Perception and VLA services are still required. +Configure planner limits +------------------------ + +``--max-turns`` is a shared CLI option for ``api``, ``claude_code`` and +``codex``, with a default of ``100``. The unit of a turn and the enforcement +of that limit depend on the backend. The reported ``turns_used`` and the +budget are not always the same counter: + +.. list-table:: Turn accounting by backend + :header-rows: 1 + :widths: 15 40 45 + + * - Planner + - Reported ``turns_used`` + - Budget enforcement + * - ``api`` + - Each model response consumed by RPent's loop. + - RPent checks this counter after handling the response's tool calls. + * - ``claude_code`` + - Top-level assistant responses, deduplicated by message ID, including + the final text-only response. This counter is for reporting; it does + not trigger an interrupt. + - RPent sets ``ClaudeAgentOptions.max_turns``; the SDK passes it to the + Claude Code CLI as ``--max-turns``. Claude limits tool-use round trips + per query and returns ``error_max_turns`` when the limit is reached. + * - ``codex`` + - Completed model responses from cumulative SDK usage updates. + Reasoning/tool-only responses count; multiple items in one response + count once, and duplicate usage updates are ignored. + - CLI and Dashboard use this counter to request one interrupt at the + limit and preserve an already recorded ``finish``. + +In an interactive Claude session, each query gets a new SDK turn budget, +while RPent's ``turns_used`` accumulates across queries. A final text-only +response contributes to RPent's report but not to Claude's tool-use budget. +See `Claude's turn-limit documentation +`_. + +``flash`` replays a plan without an LLM loop, so this budget does not apply; +it reports ``turns_used=0``. + +Other limits have different scopes: + +- ``--max-tokens`` caps each reply's tokens for ``api`` only (default ``8192``). +- ``--planner-timeout-s`` limits elapsed planner time, with backend-specific + defaults and interactive-mode behavior described below. + +When the model calls ``finish``, the planner records the finish state. +Reaching a turn limit stops the current loop; an interactive Claude session +can still accept another query. The main program saves the transcript when +the run ends. Timeouts or SDK exceptions are stored in the planner result +and written to the log. + The ``api`` planner (direct model API) --------------------------------------- @@ -79,8 +132,6 @@ needed): Relevant ``api`` planner knobs: - ``--max-tokens`` — cap each LLM reply (default ``8192``). -- ``--max-turns`` — cap the number of tool-calling turns (default - ``100``). - ``--no-images`` — never send image bytes; this is required for text-only models. The agent then reasons from textual state alone, so task performance may not be satisfactory. @@ -108,8 +159,6 @@ Notes: - Do **not** add a provider prefix to ``--model``. If it is omitted, RPent uses ``sonnet``. -- ``--max-turns`` is passed to the Claude Agent SDK and defaults to - ``100``. - ``--planner-timeout-s`` limits non-interactive runs. It defaults to ``CELL_TIMEOUT_S``, or ``1200`` seconds when that variable is unset. The limit is not applied in ``--interactive`` mode. @@ -311,22 +360,3 @@ adding a planner does not require changes to tools or environment servers. See :doc:`../development/architecture` for the interface, and :doc:`../development/add_primitive` if you want to expose new tools to your custom planner. - -Configure planner limits ------------------------- - -The limiting options apply to different planners: - -- ``--max-tokens`` caps *per-reply* tokens only for the ``api`` - planner. LIBERO-style tasks usually - finish comfortably under ``8192``; longer-horizon RoboCasa episodes - benefit from raising it if your model supports it. -- ``--max-turns`` caps the *total number of tool-calling turns*. A - single LIBERO task rarely needs more than ~30 turns; RoboCasa - long-horizon tasks can approach the default ``100``. -- ``--planner-timeout-s`` limits the planner's running time. - -When the model calls the ``finish`` tool, the planner records the -corresponding finish state. Reaching a turn limit or timeout ends the -run, and the main program still saves the transcript. Timeouts or SDK -exceptions are stored in the planner result and written to the log. diff --git a/docs/source-zh/rst_source/usage/configure_planner.rst b/docs/source-zh/rst_source/usage/configure_planner.rst index 3ca3196f..a1135473 100644 --- a/docs/source-zh/rst_source/usage/configure_planner.rst +++ b/docs/source-zh/rst_source/usage/configure_planner.rst @@ -41,6 +41,52 @@ SDK。 :doc:`flash`。 - 想在新布局上低成本地重跑一个已知可行的方案,无需 LLM 在线规划;仍需要感知和 VLA 服务。 +设置 planner 的运行限制 +----------------------- + +``--max-turns`` 是 ``api``、``claude_code`` 和 ``codex`` 共用的 CLI 参数,默认 +``100``。各后端对一轮的定义和上限的执行方式不同,报告的 ``turns_used`` +与执行预算时使用的计数不一定相同: + +.. list-table:: 各后端的轮数统计与限制 + :header-rows: 1 + :widths: 15 40 45 + + * - Planner + - 报告的 ``turns_used`` + - 预算限制方式 + * - ``api`` + - RPent 循环中收到的每次模型响应。 + - RPent 处理完该响应的工具调用后,用这一计数检查预算。 + * - ``claude_code`` + - 按消息 ID 去重的顶层 assistant 响应,包括最后一次纯文本响应。 + 这一计数只用于报告,不触发中断。 + - RPent 设置 ``ClaudeAgentOptions.max_turns``,SDK 将它作为 + ``--max-turns`` 传给 Claude Code CLI。Claude 限制每次 query 的 + 工具调用往返次数,达到上限时返回 ``error_max_turns``。 + * - ``codex`` + - 根据 SDK 累计 usage 更新统计已完成的模型响应。只有推理或工具调用 + 的响应也计数;同一次响应的多个条目只计一次,重复 usage 通知不重复计数。 + - CLI 和 Dashboard 使用这一计数,在达到上限时只请求一次中断, + 并保留已记录的 ``finish``。 + +在 Claude 交互会话中,每次 query 都会获得新的 SDK 轮数预算,而 RPent 的 +``turns_used`` 在多次 query 之间累计。最后一次纯文本响应会计入 RPent 的报告, +但不计入 Claude 的工具调用预算。详见 `Claude 的轮数限制说明 +`_。 + +``flash`` 直接重放计划,不运行 LLM 循环,因此不使用这一预算,报告的 +``turns_used`` 为 ``0``。 + +其他限制的作用范围不同: + +- ``--max-tokens`` 仅限制 ``api`` 每次回复的 token 数,默认 ``8192``。 +- ``--planner-timeout-s`` 限制 planner 的运行时间;各后端的默认值及交互模式行为见下文。 + +模型调用 ``finish`` 后,planner 会记录结束状态。达到轮数上限时,当前循环停止; +Claude 交互会话仍可接收下一次 query。运行结束时,主程序会保存 transcript。 +超时或 SDK 异常会写入 planner 结果,并输出到日志。 + ``api`` planner(直接调用模型 API) ------------------------------------- @@ -71,7 +117,6 @@ SDK。 ``api`` planner 的相关调节参数: - ``--max-tokens`` —— 单次 LLM 回复的 token 上限(默认 ``8192``)。 -- ``--max-turns`` —— 工具调用轮数上限(默认 ``100``)。 - ``--no-images`` —— 不向模型发送图片字节;纯文本模型必须加此参数。此时 智能体只依赖文本状态推理,任务表现可能不够理想。 @@ -96,7 +141,6 @@ RPent 为 Claude 规划会话关闭文件系统配置来源,因此不会自动 注意事项: - ``--model`` **不要** 加模型提供商前缀;省略时默认使用 ``sonnet``。 -- ``--max-turns`` 会传给 Claude Agent SDK,默认 ``100``。 - 非交互运行受 ``--planner-timeout-s`` 限制;默认读取 ``CELL_TIMEOUT_S``,未设置时为 ``1200`` 秒。``--interactive`` 模式 不应用这一时限。 @@ -282,19 +326,3 @@ agent SDK,可以实现 ``rpent.planner.base.Planner`` 协议,并在 工具或环境服务。接口参见 :doc:`../development/architecture`;想给 自定义 planner 暴露新工具,见 :doc:`../development/add_primitive`。 - -设置 planner 的运行限制 ------------------------ - -以下参数的作用范围并不相同: - -- ``--max-tokens`` 只限制 ``api`` planner *每次回复* 的 token 数。 - LIBERO 类任务通常 ``8192`` 就够;更长时序的 RoboCasa episode - 如果模型支持可以调大。 -- ``--max-turns`` 限制工具调用的总轮数。单个 LIBERO 任务通常 - 不会超过 30 轮;RoboCasa 的长时序任务可能接近默认的 ``100``。 -- ``--planner-timeout-s`` 限制 planner 的运行时间。 - -模型调用 ``finish`` 工具后,planner 会记录相应的结束状态。达到轮数上限或 -超时时,运行结束,主程序仍会保存 transcript。超时或 SDK 异常会写入 -planner 结果,并输出到日志。 diff --git a/rpent/planner/codex.py b/rpent/planner/codex.py index 14eb87ac..d28be491 100644 --- a/rpent/planner/codex.py +++ b/rpent/planner/codex.py @@ -326,6 +326,7 @@ def _steer() -> None: daemon=True, ).start() + limit_reached = False try: for event in turn.stream(): _write_jsonl(raw_f, _message_to_json(event)) @@ -335,6 +336,14 @@ def _steer() -> None: out_f.write(rendered) out_f.flush() logger.info(rendered.strip()) + if ( + str(_get(event, "method", "")) != "turn/completed" + and not limit_reached + and recorder.finish_result is None + and recorder.turns >= recorder.max_turns + ): + limit_reached = True + turn.interrupt() finally: if stop_steer is not None: stop_steer.set() @@ -618,6 +627,7 @@ class _Recorder: max_turns: int dashboard_events: DashboardEventSink turns: int = 0 + _seen_usage: set[tuple[int, ...]] = field(default_factory=set) tool_calls: int = 0 usage: dict[str, int] = field( default_factory=lambda: { @@ -643,7 +653,8 @@ def observe(self, event: Any) -> str: if method == "item/completed": return self._render_item(_get(payload, "item")) if method == "thread/tokenUsage/updated": - self._set_usage(_get(payload, "token_usage")) + if self._set_usage(_get(payload, "token_usage")): + return f"\n[agent] === turn {self.turns}/{self.max_turns} ===\n" return "" if method == "turn/completed": return self._render_turn_completed(_get(payload, "turn")) @@ -672,12 +683,8 @@ def _render_item(self, item: Any) -> str: if not text: return "" self.final_response = text - self.turns += 1 self.dashboard_events.emit(TranscriptEvent({"type": "text", "text": text})) - return ( - f"\n[agent] === turn {self.turns}/{self.max_turns} ===\n" - f"[codex] {text}\n" - ) + return f"\n[codex] {text}\n" if item_type == "reasoning": text = _extract_text(_get(item, "summary") or _get(item, "content")) @@ -731,11 +738,18 @@ def _render_turn_completed(self, turn: Any) -> str: # -- helpers ----------------------------------------------------------- - def _set_usage(self, usage: Any) -> None: + def _set_usage(self, usage: Any) -> bool: + """Count completed model responses, including reasoning/tool-only ones. + + The SDK updates cumulative token usage after each model response. Text + and tool items within that response do not consume additional turns. + Repeated notifications (including context-window-only updates) do not + count again or overwrite newer usage totals. + """ if usage is None: - return + return False total = _get(usage, "total", usage) - self.usage = { + updated = { "total_input_tokens": _int_attr(total, "input_tokens"), "total_cached_input_tokens": _int_attr(total, "cached_input_tokens"), "total_output_tokens": _int_attr(total, "output_tokens"), @@ -743,6 +757,12 @@ def _set_usage(self, usage: Any) -> None: total, "reasoning_output_tokens" ), } + key = tuple(updated.values()) + if not any(key) or key in self._seen_usage: + return False + self._seen_usage.add(key) + self.usage = updated + self.turns += 1 self.dashboard_events.emit( UsageEvent( inp=self.usage["total_input_tokens"], @@ -750,6 +770,7 @@ def _set_usage(self, usage: Any) -> None: tool_calls=self.tool_calls, ) ) + return True def _maybe_capture_finish(self, name: str, item: Any) -> None: if self.finish_result is not None: diff --git a/tests/unit_tests/rpent/planner/test_codex_contracts.py b/tests/unit_tests/rpent/planner/test_codex_contracts.py index 44221ae7..d4367e66 100644 --- a/tests/unit_tests/rpent/planner/test_codex_contracts.py +++ b/tests/unit_tests/rpent/planner/test_codex_contracts.py @@ -14,6 +14,7 @@ from __future__ import annotations +import asyncio import json import os import queue @@ -454,6 +455,185 @@ def test_successful_fake_codex_lifecycle_uses_fake_mcp_and_accounts_events( assert any(isinstance(event, UsageEvent) for event in sink.events) +def _model_response_events(index: int, *, text: bool = False, finish: bool = False): + yield { + "method": "item/completed", + "payload": {"item": {"type": "reasoning", "summary": []}}, + } + for message in ("working", "still working") if text else ("",): + yield { + "method": "item/completed", + "payload": {"item": {"type": "agentMessage", "text": message}}, + } + for tool in ("read_text_file", "finish" if finish else "list_dir"): + yield { + "method": "item/completed", + "payload": { + "item": { + "type": "mcpToolCall", + "tool": tool, + "status": "completed", + "arguments": {"status": "stuck", "summary": "done"} + if tool == "finish" + else {}, + "result": "accepted", + } + }, + } + usage = { + "method": "thread/tokenUsage/updated", + "payload": { + "token_usage": { + "total": {"input_tokens": 10 * index, "output_tokens": 2 * index} + } + }, + } + yield usage + yield usage # A repeated SDK notification is not another model response. + + +def test_cli_interrupts_once_at_tool_only_response_budget(tmp_path, monkeypatch): + install_fake_backend(monkeypatch) + + def stream(self): + for index in range(1, 6): + if self.interrupt_calls: + break + yield from _model_response_events(index) + yield { + "method": "turn/completed", + "payload": {"turn": {"status": "interrupted"}}, + } + + monkeypatch.setattr(FakeTurn, "stream", stream) + result = make_planner(tmp_path, RecordingSink()).solve( + system_prompt="system", + user_message="task", + toolkit=FakeToolkit(), + max_turns=2, + ) + assert result.stats["turns_used"] == 2 + assert result.stats["tool_calls"] == 4 + assert result.stats["total_input_tokens"] == 20 + assert FakeCodex.instances[0].thread.fake_turn.interrupt_calls == 1 + assert FakeCodex.instances[0].closed + assert FakeMcpServer.instances[0].stopped + assert result.error is None + + +def test_cli_keeps_finish_at_the_response_budget(tmp_path, monkeypatch): + install_fake_backend(monkeypatch) + FakeCodex.events = [ + *_model_response_events(1, finish=True), + {"method": "turn/completed", "payload": {"turn": {"status": "completed"}}}, + ] + result = make_planner(tmp_path, RecordingSink()).solve( + system_prompt="system", + user_message="task", + toolkit=FakeToolkit(), + max_turns=1, + ) + assert result.stats["turns_used"] == 1 + assert result.finish_result["status"] == "stuck" + assert FakeCodex.instances[0].thread.fake_turn.interrupt_calls == 0 + assert FakeCodex.instances[0].closed + assert FakeMcpServer.instances[0].stopped + assert result.error is None + + +def test_dashboard_interrupts_at_response_budget_and_closes(): + from rpent.planner.codex import _CodexDashboardSession, _Recorder + + async def run(): + class Turn: + interrupt_calls = 0 + + async def stream(self): + for index in range(1, 6): + if self.interrupt_calls: + break + for event in _model_response_events(index): + yield event + yield { + "method": "turn/completed", + "payload": {"turn": {"status": "interrupted"}}, + } + + async def interrupt(self): + self.interrupt_calls += 1 + + class Control: + ended = False + closed = False + + async def tool_completed(self, session): + pass + + async def complete(self, session): + raise AssertionError("a budget-limited session must end") + + def end(self): + self.ended = True + + async def close(self): + self.closed = True + + recorder = _Recorder(max_turns=1, dashboard_events=RecordingSink()) + control = Control() + session = _CodexDashboardSession( + config=None, + thread_options={}, + turn_options={}, + recorder=recorder, + emit_event=recorder.observe, + control=control, + ) + turn = Turn() + done = asyncio.Event() + session._codex = control + session._turn = turn + session._turn_done = done + await session._consume_turn(turn, done) + await session.close() + assert done.is_set() and control.ended and control.closed + assert session.error is None + assert turn.interrupt_calls == 1 + assert recorder.turns == 1 + assert recorder.finish_result is None + + asyncio.run(run()) + + +def test_response_count_ignores_items_and_duplicate_usage(): + from rpent.planner.codex import _Recorder + + recorder = _Recorder(max_turns=10, dashboard_events=RecordingSink()) + usage_events = [] + for index, text in [(1, False), (2, True)]: + events = list(_model_response_events(index, text=text)) + # Reasoning, empty/text messages and multiple tools belong to one response. + for event in events[:-2]: + recorder.observe(event) + assert recorder.turns == index - 1 + for event in events[-2:]: + recorder.observe(event) + assert recorder.turns == index + usage_events.append(events[-1]) + assert recorder.tool_calls == 4 + assert recorder.final_response == "still working" + + for event in reversed(usage_events): + recorder.observe(event) + recorder.observe( + { + "method": "thread/tokenUsage/updated", + "payload": {"token_usage": {"total": {}}}, + } + ) + assert recorder.turns == 2 + assert recorder.stats()["total_input_tokens"] == 20 + + def test_rejected_finish_item_is_not_promoted() -> None: from rpent.planner.codex import _Recorder From 0abaca27c6f3f85149b0fedc93cdb2bfc7f663f2 Mon Sep 17 00:00:00 2001 From: ye shengjie <184427520+ye1457@users.noreply.github.com> Date: Fri, 25 Sep 2026 09:55:33 +0000 Subject: [PATCH 2/2] docs(planner): explain turn limits with a concrete example --- .../rst_source/usage/configure_planner.rst | 52 +++++++------------ .../rst_source/usage/configure_planner.rst | 40 +++++--------- 2 files changed, 32 insertions(+), 60 deletions(-) diff --git a/docs/source-en/rst_source/usage/configure_planner.rst b/docs/source-en/rst_source/usage/configure_planner.rst index c762658d..fb52707e 100644 --- a/docs/source-en/rst_source/usage/configure_planner.rst +++ b/docs/source-en/rst_source/usage/configure_planner.rst @@ -50,39 +50,25 @@ loop is orchestrated, and which model SDK is used. Configure planner limits ------------------------ -``--max-turns`` is a shared CLI option for ``api``, ``claude_code`` and -``codex``, with a default of ``100``. The unit of a turn and the enforcement -of that limit depend on the backend. The reported ``turns_used`` and the -budget are not always the same counter: - -.. list-table:: Turn accounting by backend - :header-rows: 1 - :widths: 15 40 45 - - * - Planner - - Reported ``turns_used`` - - Budget enforcement - * - ``api`` - - Each model response consumed by RPent's loop. - - RPent checks this counter after handling the response's tool calls. - * - ``claude_code`` - - Top-level assistant responses, deduplicated by message ID, including - the final text-only response. This counter is for reporting; it does - not trigger an interrupt. - - RPent sets ``ClaudeAgentOptions.max_turns``; the SDK passes it to the - Claude Code CLI as ``--max-turns``. Claude limits tool-use round trips - per query and returns ``error_max_turns`` when the limit is reached. - * - ``codex`` - - Completed model responses from cumulative SDK usage updates. - Reasoning/tool-only responses count; multiple items in one response - count once, and duplicate usage updates are ignored. - - CLI and Dashboard use this counter to request one interrupt at the - limit and preserve an already recorded ``finish``. - -In an interactive Claude session, each query gets a new SDK turn budget, -while RPent's ``turns_used`` accumulates across queries. A final text-only -response contributes to RPent's report but not to Claude's tool-use budget. -See `Claude's turn-limit documentation +``--max-turns N`` sets the planner's turn limit; the default is ``100``. +A turn is not a robot action: one model response can request several tools. + +- **API / Codex:** Each model response counts once, including responses with + only reasoning or tool calls. Several tools requested in the same response + still count as one turn. RPent enforces this limit. +- **Claude Code:** A turn means the model requests tools, those tools run, + and their results return to the model. A final answer without tool calls + does not use this budget. RPent passes the limit to Claude Code, which + returns ``error_max_turns`` if the limit is reached. + +For example, the model requests two file reads in one response, then +summarizes their results in another. API/Codex count two turns; Claude uses +one tool-use turn. RPent records model responses in ``turns_used``, so it +reports two in this example even when using Claude. + +In interactive Claude sessions, each new user input gets a fresh turn +budget, while ``turns_used`` keeps accumulating. See +`Claude's turn-limit documentation `_. ``flash`` replays a plan without an LLM loop, so this budget does not apply; diff --git a/docs/source-zh/rst_source/usage/configure_planner.rst b/docs/source-zh/rst_source/usage/configure_planner.rst index a1135473..3d8bd602 100644 --- a/docs/source-zh/rst_source/usage/configure_planner.rst +++ b/docs/source-zh/rst_source/usage/configure_planner.rst @@ -44,35 +44,21 @@ SDK。 设置 planner 的运行限制 ----------------------- -``--max-turns`` 是 ``api``、``claude_code`` 和 ``codex`` 共用的 CLI 参数,默认 -``100``。各后端对一轮的定义和上限的执行方式不同,报告的 ``turns_used`` -与执行预算时使用的计数不一定相同: +``--max-turns N`` 设置规划轮数上限,默认 ``100``。 +一轮不是一次机器人动作:模型的一次回复可以要求调用多个工具。 -.. list-table:: 各后端的轮数统计与限制 - :header-rows: 1 - :widths: 15 40 45 +- **API / Codex:** 模型每回复一次算一轮。只有推理或工具调用、没有文字的 + 回复也计数;同一次回复中的多个工具调用不会分别计数。RPent 负责执行这个上限。 +- **Claude Code:** 一轮是“模型请求工具 → 工具执行 → 结果返回模型”。 + 最后不调用工具的文字答复不占用这一预算。RPent 把上限交给 Claude Code + 执行,达到上限时返回 ``error_max_turns``。 - * - Planner - - 报告的 ``turns_used`` - - 预算限制方式 - * - ``api`` - - RPent 循环中收到的每次模型响应。 - - RPent 处理完该响应的工具调用后,用这一计数检查预算。 - * - ``claude_code`` - - 按消息 ID 去重的顶层 assistant 响应,包括最后一次纯文本响应。 - 这一计数只用于报告,不触发中断。 - - RPent 设置 ``ClaudeAgentOptions.max_turns``,SDK 将它作为 - ``--max-turns`` 传给 Claude Code CLI。Claude 限制每次 query 的 - 工具调用往返次数,达到上限时返回 ``error_max_turns``。 - * - ``codex`` - - 根据 SDK 累计 usage 更新统计已完成的模型响应。只有推理或工具调用 - 的响应也计数;同一次响应的多个条目只计一次,重复 usage 通知不重复计数。 - - CLI 和 Dashboard 使用这一计数,在达到上限时只请求一次中断, - 并保留已记录的 ``finish``。 - -在 Claude 交互会话中,每次 query 都会获得新的 SDK 轮数预算,而 RPent 的 -``turns_used`` 在多次 query 之间累计。最后一次纯文本响应会计入 RPent 的报告, -但不计入 Claude 的工具调用预算。详见 `Claude 的轮数限制说明 +例如,模型先在一次回复中要求读取两个文件,拿到结果后再给出文字总结: +API/Codex 计两轮,Claude 的工具轮数为一轮。RPent 的 ``turns_used`` +记录模型回复数,因此这个例子即使用 Claude,也会报告两次回复。 + +Claude 交互模式下,每次新增用户输入都会获得新的轮数预算,``turns_used`` +则继续累计。详见 `Claude 的轮数限制说明 `_。 ``flash`` 直接重放计划,不运行 LLM 循环,因此不使用这一预算,报告的