Skip to content

Remove the main agent's ReAct turn cap; fix dark-mode legibility in the tool-call panel - #106

Merged
Luhaozhu merged 2 commits into
mainfrom
fix/main-agent-turn-cap-and-dark-mode-tool-panel
Aug 16, 2026
Merged

Remove the main agent's ReAct turn cap; fix dark-mode legibility in the tool-call panel#106
Luhaozhu merged 2 commits into
mainfrom
fix/main-agent-turn-cap-and-dark-mode-tool-panel

Conversation

@Luhaozhu

Copy link
Copy Markdown
Contributor

Two unrelated fixes, one backend and one frontend.

Remove the main agent's ReAct turn cap

A fixed round ceiling bounds the wrong axis. What a long task exhausts is context, not rounds, and any number picked is simultaneously too low for report-scale work and too high to catch a genuine runaway. Both guarantees the cap was supposed to provide already live elsewhere:

  • runaway protection is the chat-run watchdog's job, which sees wall-clock and output (CHAT_RUN_INACTIVITY_TIMEOUT_SEC / CHAT_RUN_MAX_AGE_SEC / CHAT_RUN_HARD_MAX_AGE_SEC reap silent and immortal runs regardless of round count);
  • context exhaustion is handled by compaction.

So the main agent is now unbounded. _UNBOUNDED_ITERS exists only because AgentScope's ReActConfig needs an int; it is a loop backstop, not a budget.

Bounded budgets survive wherever the bound is a deliberate contract: sub-agents, turbo's quick-lookup cap, a custom agent's own max_iters, a published profile's turn budget, and the CHAT_MAIN_MAX_ITERS opt-in for operators who do want the main loop fenced.

Where a budget exists, it is now stated up front. The existing wrap-up reminder can only ask a loop that already burned its rounds to salvage something one tool call at a time; a static hint instead changes how the rounds get spent. It names parallel fan-out as the lever, because a round is one reasoning step rather than one tool call — the budget binds serial round-trips, never total tool calls. The hint is byte-stable for a given budget so it stays prefix-cache friendly, and it is deliberately never rendered for the unbounded main agent, where a false scarcity claim would make the model cut work short.

Profile defaults follow: max_react_turns defaults to the sentinel 0 (UNBOUNDED_REACT_TURNS), a missing key resolves to that sentinel rather than an invented ceiling, and validation accepts the sentinel while still rejecting out-of-band values.

Fix dark-mode legibility in the tool-call panel and status strip

Three independent root causes, none of them visible in light mode.

  1. Tool-call area rendered black-on-white. highlight.js ships a fixed light theme and tool.css carried zero dark overrides, so the code block and its surrounding chrome both stayed on light surfaces. The mechanism was hard-coded literals and var(--token, #fff) fallbacks — the fallback silently wins wherever the token is undefined, keeping a light value alive across the theme switch. Those call sites now resolve through theme tokens, and a new code-highlight.css layer re-tints the highlight.js palette for dark.

  2. Status strip unreadable and shimmering backwards. The "completed" grey sat too close to the background, and the running-state gradient was built as "slate-400 base + slate-600 highlight" — it creates its highlight by darkening the text, which works over a light surface and inverts over a dark one, leaving the highlight below 2:1 contrast. Dark mode now runs the gradient dark-to-light so the brightest point really is the highlight.

  3. Shimmering title collapsed into a solid grey block. The dark override used the background shorthand, which resets background-clip back to border-box — precisely the property clipping the gradient into the glyphs. It now sets background-image and restates the clip.

Testing

  • test_iter_budget_middleware.py: the hint states the actual number, leads with the parallel-fan-out strategy, and is byte-stable for a given budget; an unbounded profile validates while an out-of-band one does not.
  • test_policies_and_memory_ops.py: the built-in profile stays unbounded, and a profile published without an explicit turn budget does too.
  • Dark-mode changes verified in the browser across the tool-call panel, status strip, and shimmering title.

Luhaozhu added 2 commits August 16, 2026 02:58
…ts up front

A fixed round ceiling bounds the wrong axis. What a long task exhausts is
context, not rounds, and any number picked here is at once too low for
report-scale work and too high to catch a genuine runaway. Both things the cap
was supposed to buy are already covered elsewhere: the chat-run watchdog reaps
silent and immortal runs on wall-clock and output, and compaction handles
context exhaustion.

The main agent is therefore unbounded; _UNBOUNDED_ITERS is a loop backstop for
AgentScope's int-typed ReActConfig, not a budget. Bounded budgets survive only
where the bound is a deliberate contract: sub-agents, turbo's quick-lookup cap,
a custom agent's own max_iters, a published profile's turn budget, and the
CHAT_MAIN_MAX_ITERS opt-in for operators who do want the main loop fenced.

Where a budget does exist, state it up front instead of only shouting at the
end. The wrap-up reminder can merely ask a loop that already burned its rounds
to salvage something; a static hint changes how the rounds get spent, and names
parallel fan-out as the lever, since a round is one reasoning step rather than
one tool call. The hint is byte-stable for a given budget so it stays
prefix-cache friendly, and it is never rendered for the unbounded main agent,
where a false scarcity claim would make the model cut work short.

Profile defaults follow: max_react_turns now defaults to the sentinel 0
("unbounded"), a missing key resolves to it rather than an invented ceiling,
and validation accepts the sentinel while still rejecting out-of-band values.
…s strip

Three independent root causes, all invisible in light mode.

The tool-call area rendered as black text on white. highlight.js ships a fixed
light theme, and tool.css had zero dark overrides, so both the code block and
its surrounding chrome stayed pinned to light surfaces. Hard-coded literals and
`var(--token, #fff)` fallbacks were the mechanism: the fallback silently wins
wherever the token is not defined, which makes a light value survive the theme
switch. Those call sites now resolve through theme tokens, and a
code-highlight.css layer re-tints the highlight.js palette for the dark theme.

The status strip's "completed" grey sat too close to the background to read,
and the running-state shimmer pulsed the wrong way: the gradient was built as
"slate-400 base plus slate-600 highlight", creating its highlight by darkening
text. That works over a light surface and inverts over a dark one, leaving the
highlight at under 2:1 contrast. Dark mode now runs the gradient dark-to-light
so the brightest point is the highlight.

The shimmering title also collapsed into a solid grey block, because the dark
override set `background`, and the shorthand resets `background-clip` back to
`border-box` — the very property clipping the gradient into the glyphs. It now
sets `background-image` and restates the clip.
@Luhaozhu
Luhaozhu merged commit 9f0c1d2 into main Aug 16, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant