Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 39 additions & 23 deletions docs/source-en/rst_source/usage/configure_planner.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,45 @@ 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 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
<https://code.claude.com/docs/en/agent-sdk/agent-loop#turns-and-budget>`_.

``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)
---------------------------------------

Expand Down Expand Up @@ -79,8 +118,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.
Expand Down Expand Up @@ -108,8 +145,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.
Expand Down Expand Up @@ -311,22 +346,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.
50 changes: 32 additions & 18 deletions docs/source-zh/rst_source/usage/configure_planner.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,38 @@ SDK。
:doc:`flash`。
- 想在新布局上低成本地重跑一个已知可行的方案,无需 LLM 在线规划;仍需要感知和 VLA 服务。

设置 planner 的运行限制
-----------------------

``--max-turns N`` 设置规划轮数上限,默认 ``100``。
一轮不是一次机器人动作:模型的一次回复可以要求调用多个工具。

- **API / Codex:** 模型每回复一次算一轮。只有推理或工具调用、没有文字的
回复也计数;同一次回复中的多个工具调用不会分别计数。RPent 负责执行这个上限。
- **Claude Code:** 一轮是“模型请求工具 → 工具执行 → 结果返回模型”。
最后不调用工具的文字答复不占用这一预算。RPent 把上限交给 Claude Code
执行,达到上限时返回 ``error_max_turns``。

例如,模型先在一次回复中要求读取两个文件,拿到结果后再给出文字总结:
API/Codex 计两轮,Claude 的工具轮数为一轮。RPent 的 ``turns_used``
记录模型回复数,因此这个例子即使用 Claude,也会报告两次回复。

Claude 交互模式下,每次新增用户输入都会获得新的轮数预算,``turns_used``
则继续累计。详见 `Claude 的轮数限制说明
<https://code.claude.com/docs/en/agent-sdk/agent-loop#turns-and-budget>`_。

``flash`` 直接重放计划,不运行 LLM 循环,因此不使用这一预算,报告的
``turns_used`` 为 ``0``。

其他限制的作用范围不同:

- ``--max-tokens`` 仅限制 ``api`` 每次回复的 token 数,默认 ``8192``。
- ``--planner-timeout-s`` 限制 planner 的运行时间;各后端的默认值及交互模式行为见下文。

模型调用 ``finish`` 后,planner 会记录结束状态。达到轮数上限时,当前循环停止;
Claude 交互会话仍可接收下一次 query。运行结束时,主程序会保存 transcript。
超时或 SDK 异常会写入 planner 结果,并输出到日志。

``api`` planner(直接调用模型 API)
-------------------------------------

Expand Down Expand Up @@ -71,7 +103,6 @@ SDK。
``api`` planner 的相关调节参数:

- ``--max-tokens`` —— 单次 LLM 回复的 token 上限(默认 ``8192``)。
- ``--max-turns`` —— 工具调用轮数上限(默认 ``100``)。
- ``--no-images`` —— 不向模型发送图片字节;纯文本模型必须加此参数。此时
智能体只依赖文本状态推理,任务表现可能不够理想。

Expand All @@ -96,7 +127,6 @@ RPent 为 Claude 规划会话关闭文件系统配置来源,因此不会自动
注意事项:

- ``--model`` **不要** 加模型提供商前缀;省略时默认使用 ``sonnet``。
- ``--max-turns`` 会传给 Claude Agent SDK,默认 ``100``。
- 非交互运行受 ``--planner-timeout-s`` 限制;默认读取
``CELL_TIMEOUT_S``,未设置时为 ``1200`` 秒。``--interactive`` 模式
不应用这一时限。
Expand Down Expand Up @@ -282,19 +312,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 结果,并输出到日志。
39 changes: 30 additions & 9 deletions rpent/planner/codex.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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()
Expand Down Expand Up @@ -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: {
Expand All @@ -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"))
Expand Down Expand Up @@ -672,12 +683,8 @@ def _render_item(self, item: Any) -> str:
if not text:
return ""
self.final_response = text
self.turns += 1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this line an error or an unused assignment?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The removal is intentional. agentMessage rendering now only handles text; _set_usage counts completed responses, including reasoning/tool-only responses. Restoring this increment would double-count responses that also emit text. The four existing budget regression tests pass.

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"))
Expand Down Expand Up @@ -731,25 +738,39 @@ 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"),
"total_reasoning_output_tokens": _int_attr(
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"],
out=self.usage["total_output_tokens"],
tool_calls=self.tool_calls,
)
)
return True

def _maybe_capture_finish(self, name: str, item: Any) -> None:
if self.finish_result is not None:
Expand Down
Loading
Loading