已合并
feat(core): concurrent tool batch, verified finish, plan on control path #193
RomanAndr创建于 6月14日
feat(core): concurrent tool batch, verified finish, plan on control path #193
已合并
RomanAndr创建于 6月14日
RomanAndr
RomanAndr
6月14日

Was This PR authored or co-authored using generative AI tooling?

[ ] - No

[x] - Yes
__1. AI Agent : Claude Code
__2. AI Model : Claude Opus 4.8 xhigh

What this PR does / why we need it

The agent loop has four structural weaknesses that no prompt change can fix. tool_exec runs every tool call in a turn sequentially, so a turn that emits several independent reads/greps awaits them back-to-back and wastes turn budget on calls with no ordering dependency. decide accepts the model's first stop as terminal, so a run ends the instant the model claims it is done — a confidently-wrong, unverified answer finishes the run. todo_write plan state is write-only: the loop never reads back open items, so the model can stop with its own plan half-done. And stale tool-result bodies accumulate in the message history forever, paying input tokens for output already consumed. These are properties of the control path, not the prompt.

This PR reworks the loop core. tool_exec now builds all calls, then executes the valid ones through futures_util::future::join_all and stitches the outcomes back into call order — only the awaits overlap; history, suspend, and stop-after-result semantics are byte-for-byte the prior order. A suspending join_subagent is sorted last in the batch, and a batch that writes and reads the same file_path falls back to sequential execution, so concurrency never strands a committed mutation across a suspend or races two writers on one file. decide no longer completes on the first stop: it first consults open plan items (tool::open_todo_lines, re-injected each turn) and then runs a one-shot completion checklist that re-anchors on the original task — only a second stop completes, and the checklist is gated to runs that expose tools so a conversational turn is never nudged. LoopRunResult::Suspended now carries a Vec<SuspendedToolCall> so a turn with several join_subagent calls suspends and resolves all of them; session_supervisor.rs resolves each in turn and drop_unanswered_tool_uses defensively clears any tool_use stranded by an earlier stop short-circuit. is_transient now classifies StreamError/IoError as recoverable; repair_tool_names normalizes malformed tool names before rejection; the final turn withholds tools so the model commits an answer rather than a cut-off tool call; prune_stale_tool_output collapses tool-result bodies beyond a recent window to a marker; and a repeated identical failing or successful tool call injects a one-shot strategy-change nudge. The stop verdict, the resume contract, and per-call tool execution semantics are otherwise unchanged.

Defensive properties:

  • Concurrency preserves call order — Pass 1 builds, Pass 2 executes (concurrent or, for same-path writes, sequential), Pass 3 stitches outcomes back by input order; history and stop/suspend handling are identical to the sequential loop, only the awaits overlap.
  • Suspend never strands a siblingjoin_subagent is ordered last so every side-effecting sibling has its tool_result recorded before the first suspend; all suspends are collected and each is resolved on resume, and drop_unanswered_tool_uses guarantees the resumed history carries no dangling tool_use.
  • Nudges are boundedplan_nudged/completion_nudged/streak counters fire each at most once per condition and force a single extra turn, so verification and plan reminders cannot loop; both gate on turn_number < max_turns and the completion checklist gates on tools being visible.
  • Transient/permanent split stays conservative — only stream/io/timeout/throttle retry with backoff; auth, config, model-not-found, context-length, and bad-request faults still abort terminally without burning the retry budget.
  • Pruning is keyed to a recent-bytes window — only tool-result bodies beyond KEEP_RECENT_TOOL_BYTES and above MIN_PRUNABLE_BYTES collapse to a marker; recent output and small results are untouched, and the marker itself is skipped on re-entry.

No new unit tests are added; one existing loop test updates its expected turn_count (2 → 3) to account for the one verification turn the completion nudge now inserts when tools are visible.

This PR adds futures-util as a direct dependency of crates/core (futures-util.workspace = true) to drive the concurrent tool batch; Cargo.lock is committed alongside.

Which issue this PR fixes

Closes #31

Special notes for your reviewer:

  • Scope: 11 files across xiaoo-core, tool, and xiaoo-app. Core: agent_loop.rs (loop-core slice — concurrency, verified finish, plan-on-control-path, error classification, tool withholding, pruning), loop_state.rs (live-only nudge/streak fields), suspend.rs (Suspended now holds a Vec), Cargo.toml (futures-util). App: session_supervisor.rs (resolves several suspends per turn + drop_unanswered_tool_uses). Tool: todo_write/{executor.rs,mod.rs}, impl/{mod.rs,builtin/mod.rs}, lib.rs (the open_todo_lines reader plumbed out for the loop).
  • Caller contract preserved: per-call tool execution is unchanged; the only LoopRunResult shape change is Suspended(SuspendedToolCall)Suspended(Vec<SuspendedToolCall>), and its sole consumer (session_supervisor.rs) is updated in this PR. New LoopState fields are live-only and reset on resume, so the snapshot schema is untouched. The cached-tokens accounting from the original bundle lives in a sibling PR and is not part of this slice.
  • Commits are split for review:
    • feat(core): concurrent tool batch, verified finish, plan on control path — the loop-core slice in full (single commit on this branch).
  • Based on chore/test-fixture-drift: this branch is stacked on the test-repair PR that makes dev's suite compile and pass; merge that PR first. Until it lands, this PR's diff also lists those 4 test files.
  • Verified locally: cargo test --workspace on this branch (chore base + this feature) — green (560 passed, 0 failed).
likedislike
Pull Request已成功合入, 合并人@openeuler-ci-bot
(感谢 RomanAndr 的贡献)
RomanAndrRomanAndr
6月14日 创建了 pull request,commit 72b67f1e
openeuler-ci-botopeneuler-ci-bot成员
6月14日 将lyhu,huzhengce,liumiao27,fromhsc,gogoxiaoxiao,wqianli,leon-wang2021,kaitiandu设为审查人
openeuler-ci-botopeneuler-ci-bot成员
6月14日 添加了label:sig/sig-intelligence
openeuler-ci-bot
openeuler-ci-bot成员
6月14日 评论:

Welcome To openEuler Community

Hey @Roman-Andr , thanks for your contribution to the community.

Bot Usage Manual

I'm the Bot here serving you. You can find the instructions on how to interact with me at Here . That means you can comment below every pull request or issue to trigger Bot Commands.

Contact Guide

If you have any questions, please contact the SIG: sig-intelligence ,
and any of the maintainers: @fromhsc, @gogoxiaoxiao, @huzhengce, @kaitiandu, @leon-wang2021, @liumiao27, @lyhu, @wqianli ,
and any of the committers: @extinctfire, @hypothesiser, @xiangyou_xie .

likedislike
openeuler-ci-botopeneuler-ci-bot成员
6月14日 添加了label:openeuler-cla/yes
此处折叠了42条消息 查看更多
openeuler-ci-bot
openeuler-ci-bot成员
6月15日 评论:

Review Code Feedback

  • The label lgtm was added to this pull request. It means that hypothesiser reviewed the code changes. 👋
Tips
  • If this pull request is not merged while all conditions are met, comment /check-pr to try again. 😄
likedislike
openeuler-ci-botopeneuler-ci-bot成员
6月15日 添加了label:approved
openeuler-ci-bot
openeuler-ci-bot成员
6月15日 评论:

Review Code Feedback

  • The label approved was added to this pull request. It means that hypothesiser reviewed the code changes. 👋
Tips
  • If this pull request is not merged while all conditions are met, comment /check-pr to try again. 😄
likedislike
openeuler-ci-botopeneuler-ci-bot成员
6月15日 合入了pull request,合并节点 SHA:39a6a24791d6a57f919b0b72900ad6ffe6cf494f
RomanAndrRomanAndr
6月15日 修改了pull request 的描述