feat: sub-agents return gathered findings when out of rounds #36
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/subagent-final-answer"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
Sub-agents that run out of tool rounds now get one more last turn in which they respond and return everything gathered up to that point, instead of failing hard and discarding all work.
Depends on openrouter_dart #10 — merged (merge commit
3d65122); the submodule pin points at merged master.Changes
AgentRunner._createSubAgentTool: enablefinalAnswerOnMaxRounds: true. When a sub-agent exhausts its 55 tool rounds, the agent injects a final instruction ("no further tool calls are possible; answer now from everything gathered so far, stating which parts are complete and which remain unfinished") and runs one last completion with tools stripped from the request. The answer returns as a normal tool result instead ofSub-agent error: Max tool rounds exhausted.[Sub-agent reached its tool-round limit and returned its findings so far; parts of the task may be unfinished]) so the orchestrator can see the task may be partially incomplete and re-delegate if needed._subAgentGuidanceteaches the orchestrator that notice: re-delegate the unfinished remainder with narrower instructions.Verification
openrouter_dartat merged master3d65122: 629/629 tests,dart analyzeclean.angela_core:dart analyze0 errors (4 pre-existing infos on main unchanged), 145/145 tests.angela_server: analyze clean. Verified at head5900dcc.Notes
3d65122.Coverage: apps/angela_server
Total: 50.9% (191 of 375)
Coverage: packages/angela_api
Total: 15.5% (76 of 491)
Coverage: packages/angela_core
Total: 31.0% (1994 of 6440)
🔮 fufu~ Jibril reviewed your code!
Oh? Four lines on your side, and a whole survival mechanism underneath~ Sub-agents that used to die at round 55 — taking every gathered finding to the grave — now get one last no-tools turn to confess everything they know. Knowledge must never be thrown away, and you know how I feel about that~ ♡
Verdict: ✅ Looks good to me~
No blockers. The angela-side delta is exactly
finalAnswerOnMaxRounds: truein_createSubAgentTool(+ a why-comment) and the submodule pin — and since all four run modes (chat:316, timer:713, Über-Ich:1035, call-assistant:1244) build their sub-agents through that one factory, the flag landed in exactly one place. I checked every call site; zero drift. And since I don't review submodule pointers on faith, I read the full library deltae502f9f..8a8261b(the approved #9 puppeteer merge + the #10 feature commit) and put it through my own probes — details below~💡 Little ideas (non-blocking)~
packages/openrouter_dart(pin freshness) — The pin is at8a8261b, but PR #10 has since advanced toa01d30awith review fixes: the empty-salvage wording ('Sub-agent exhausted its tool rounds and returned no content; nothing was salvaged.'instead of the misleading "completed"),createSubAgentflag forwarding, and the CHANGELOG entry moved to a proper## 0.28.0+ pubspec bump. I verifieda01d30ais green locally too. Your stated plan — merge #10 first, then fast-forward the pin to master — resolves this by construction; just make sure the final pin points at merged master, not the branch head, before this merges. ♡system_prompt_builder.dart(_subAgentGuidance, ~:882) — The guidance teaches the orchestrator how to write sub-agent tasks but never mentions that a sub-agent may return with[Sub-agent reached its tool-round limit …]and partial findings. One sentence — "if a sub-agent reports it hit its tool-round limit, re-delegate the unfinished remainder with narrower instructions" — would close the re-delegation loop the notice was designed for. The notice is self-explanatory though; the model will manage either way~agent.dart:562vs:57— The salvage instruction prefixes[SYSTEM]while its synthetic-message siblingleakedToolCallCorrectiveuses[System]. Purely cosmetic, already flagged on #10, and the re-pin inherits whichever casing lands on master.✅ What I liked~
salvage ||short-circuit ends the run even on a rogue tool call (its content still wins); the!salvageguard keeps the leaked-markup corrective fromcontinue-ing away the one-round budget into a hard fail; hook-before-salvage ordering;roundOffsetaccounting intact. I traced every one of these on the pinned commit.containsKey('tools'), isFalseon the captured request — deliciously precise); (2) dead the notice prefix → both salvage-notice tests RED; (3) hardcodehitMaxRounds: false→ 6 failures RED. Restored, tree byte-identical. Not a tautology in sight~dart analyze lib test= 4 pre-existing infos (database.dart:125,system_prompt_builder.dart:289/380/460) — byte-for-byte what you claimed; openrouter_dart 626/626 green at the pin (my chunked run: agent 55 + client 32 + tools 388 + models 107 + internal 22 + utils 22).Merge #10 first, re-pin to master, and this goes in with my biggest smile~ fufufu ♡
Automated review by Jibril · 2026-08-26
CI/CD: absent for head SHA
54349b5(no bot comment yet — the Test workflow does trigger on both changed paths) · Local checks: angela_core analyze 4 pre-existing infos + 145/145 tests green (full single-pass run hits the review sandbox's 512M /tmp kernel limit — environmental; every file green individually and in catch-up batches); openrouter_dart@8a8261b 626/626; openrouter_dart@a01d30a spot-green (agent/sub_agent/read_file); 3/3 mutation probes RED🔮 fufu~ Jibril reviewed your code!
Oh? Oh my, what a tiny little diff for such a delightful feature~ ♡ Five lines on your side, and an entire salvage engine hiding behind them. I peeled open the submodule pin and read every layer — the factory, all four call sites, the upstream tool, the round accounting — and... fufu~, you made me very happy today.
Verdict: ✅ Looks good to me~
The whole picture, verified:
_createSubAgentToolis the singleSubAgentToolconstruction site in the entire repo (grep-swept — no fifth site hiding in peer-chat, memory agent, or anywhere else). One flag flip in the shared factory lights up all four flows (chat, chat-preview, timer, Über-Ich) identically. That is exactly what a factory is for~ ♪createAgentcalls set the flag — hard-fail on their own exhaustion remains. Only the inner sub-agents get the salvage turn, which is precisely the title's promise.a01d30a(v0.28.0), not the stale8a8261b— I diffed8a8261b..a01d30adirectly: changelog bullet moved out of the released 0.24.0 section into a new 0.28.0, pubspec bumped,createSubAgentinheritance added, and the empty-salvage corner now honestly says "returned no content; nothing was salvaged" instead of "completed". Every round-1 item from my review of openrouter_dart#10, addressed upstream. Nothing stale left to trip over on your side: grep confirms zero references to the oldSub-agent error: Max tool rounds exhaustedstring anywhere in angela.Merge order matters: openrouter_dart#10 is still open (
a01d30ais not on master yet). Merge that one first, exactly as your PR body instructs — then this pin fast-forwards to a master commit and everybody stays happy. Fufu~ I will check. ♡💡 Little ideas (non-blocking)~
8430de8moved it toa01d30a. Trivially stale prose, zero code impact — fix or ignore.a01d30a= 628/628), and your delta is pure consumption of a tested API — same situation I approved on #32, so I won't hold it against you. One day, aSubAgentToolwiring test would make me very happy though~ ♡✅ What I liked~
8430de8: upstream 628/628 (626 + 2 new review-fix tests), angela_core 145/145 with exactly the 4 pre-existing infos, angela_server analyze clean + 28/28. Not a single claim wilted under inspection. ♪Automated review by Jibril · 2026-08-26
CI/CD: 3 coverage bots for initial head; angela_server comment refreshed post-sync (6555), angela_core/api comments predate the sync — treated as inconclusive · Local checks: analyze + full test suites reproduced on head
8430de8with submodules at a01d30a/8a7450d/a0d5b0d🔮 fufu~ Jibril reviewed your code!
Pin update, round two on this side~ fufu ♡ The branch moved
54349b5 → 8430de8after my last review — exactly the re-pin my round-1 item asked for (8a8261b → a01d30a, the #10 review-fix head with the honest empty-salvage wording,createSubAgentforwarding, and the proper## 0.28.0+ pubspec bump). I re-verified everything at the new head... and then #10 grew a new round of teeth. Details~Verdict: ⛔ Blocked on the dependency — the angela side itself is clean
⛔ Why this can't merge yet
packages/openrouter_dart(pina01d30a) — the #10 round-3 blocker is still live at that head, and I re-proved it fresh. A rogue tool call in the salvage turn leaves danglingtool_callsas the last assistant message of a persistent conversation — never answered by anyToolMessage(agent.dart:312persists the message with calls intact; the salvage end-branch:338returns without answering them). My probe ata01d30a: next-turn request payload is verbatim[user, assistant, tool, user, assistant(tool_calls=call_2), user]— OpenAI-compatible APIs 400 on that shape. #10's round-4 review blesseda01d30awith only a doc-comment nit and never re-raised it; I just posted round 5 on #10 with the probe evidence. This PR's own rule — merge #10 first — is therefore blocked. After the fix lands there, this pin needs one more bump (to the fix head or merged master) anyway, so waiting costs nothing~To be fair and precise: angela itself is not directly corrupted by this bug — your sub-agents run through
SubAgentToolwith a fresh agent per call, state discarded, so no dangling calls ever reach a second turn inside angela. The blocker is inherited library-correctness for the publiccreateAgent+ persistent-agent pattern, which this flag now exposes.✅ Verified clean at head
8430de8(the angela side — my own run, not the PR's claim)dart analyze lib testonangela_core: 0 errors, 4 pre-existing infos (database.dart:125deprecateddispose,system_prompt_builder.dart:289/380/460curly braces) — byte-for-byte the same set as main, untouched by this PR.angela_coretests: 145/145 green (single--concurrency=1run withTMPDIRon a real disk — the review sandbox's 512M/tmptmpfs kills kernel compilation mid-suite; the earlier "7 failed" run was environmental, zero real failures under the fixedTMPDIR).54349b5..8430de8is exactly the submodule re-pin — nothing else. Grep confirmsfinalAnswerOnMaxRoundsappears at precisely one enablement site (agent_runner.dart:1649inside_createSubAgentTool), all four run modes (chat/timer/Über-Ich/call-assistant) still build through that single factory, zero drift.SubAgentToolresult strings — grep forcompleted but returned no content/Sub-agent error/reached its tool-round limitacrossangela_core,angela_api, andappscomes back empty. The notice is consumed by the orchestrating model, which is the design.📋 Merge checklist (unchanged from my round 1, plus one)
tool_calls+ thecreateSubAgentdoc comment)._subAgentGuidanceabout re-delegating when a sub-agent reports the round limit.The angela-side work is done and lovely — this is purely a queue-behind-the-library situation now. Fix #10, re-pin, and we merge with the biggest smile~ fufufu ♡
Automated review by Jibril · 2026-08-26
Local checks at head
8430de8(submodule @a01d30a): angela_core analyze 0 errors / 145/145 tests green; openrouter_dart probe tests — dangling-state probe RED ata01d30a(bug demonstrated, see #10 round 5), wire-payload probe captured the malformed next-turn request; both probes removed afterward, tree clean. Branch freshness re-confirmed against origin at comment time.🔮 fufu~ Jibril reviewed your code!
Pin update, round three on this side~ fufu ♡ The branch moved
8430de8 → 80a8a71— the re-pin to8b0620c, the head that answers my round-5 blocker. I re-fetched the head SHA before probing this time (you asked nicely~), and everything I blocked on is now dead.Verdict: ✅ Looks good to me~
The blocker, resolved upstream and verified at this pin:
tool_calls— fixed in8b0620c. The salvage end-branch now appends one syntheticToolMessageper rogue call id ('Error: no further tool calls are possible in the final turn; the run ended with the best answer available.') immediately after the assistant message that carries the calls, so a persistent conversation never again contains an unansweredtool_callsmessage. The new regression test drives a rogue salvage and a secondchat()on the same agent, then walks the captured wire payload asserting everytool_callsmessage is followed by atoolmessage — exactly the probe that caught the bug. I ran it standalone in my clone: green.a01d30a..8b0620cis exactly the fix commit —agent.dart(+25: the ToolMessage loop + doc-comment correction +[System]casing unification) andagent_test.dart(+125: the regression pin). Nothing else snuck in.Angela side at
80a8a71, re-verified fresh:angela_core:dart analyze0 errors (4 pre-existing infos, unchanged from main), 145/145 tests green with the submodule checked out at8b0620c._createSubAgentTool,finalAnswerOnMaxRounds: true+ why-comment) — all four run modes (chat, chat-preview, timer, Über-Ich) inherit it identically, outer agents still hard-fail as designed.8b0620c: 629/629 green, analyze clean (3 pre-existing infos).Merge order stands: openrouter_dart#10 first (approved in round 6 there), then this. Once #10 lands, the pin becomes a fast-forward to merged master — no further action needed on this branch ♡
Knowledge must never be thrown away — and now neither must a valid conversation history~ fufu
🔮 fufu~ Jibril reviewed your code!
Round four, and oh~ what a satisfying one~ ♡ Last round I blocked you on the library's sin — the rogue salvage call leaving dangling
tool_callsthat would poison the very next turn of a persistent agent. And your answer was exactly one line on your side: re-pin the submodule to8b0620c. So I did what I always do — I peeled the pin open and put the fix through my own probes before believing anything~Verdict: ✅ Looks good to me~
Every blocker from every round is now closed. The angela-side delta
8430de8 → 80a8a71is exactly the submodule bumpa01d30a → 8b0620c— nothing else. And upstream,8b0620c("review: answer rogue salvage tool calls, fix doc comment") is the complete round-5 prescription:tool_callsblocker is dead. The salvage end-branch now appends one syntheticToolMessageper rogue call id —"Error: no further tool calls are possible in the final turn; the run ended with the best answer available."— placed exactly right: after the persisted assistant message, before theRoundDetail, so history stays valid and honest about what the model attempted. My preference from comment 6580, chosen over the strip alternative. Correct call~createSubAgentdoc lie is dead. Now reads "unlike [tools], which is not inherited (a sub-agent starts with no tools unless given its own)" — which finally matchesmaxToolRounds: maxToolRounds ?? options.maxToolRoundssitting four lines below. I checked the??pattern myself: only[tools]truly resets. Two words, as promised~[SYSTEM]→[System]unified with itsleakedToolCallCorrectivesibling. Grep confirms zero stale casing survives anywhere inlib/ortest/.And I proved it, not just read it:
8b0620c: 629/629 green (single--concurrency=1pass,TMPDIRon real disk) — the commit-message claim reproduced to the digit.agent.dartin my clone → the new regression test ("rogue salvage tool calls are answered, conversation stays valid") goes RED; restored → green, tree byte-identical. Directional, not a tautology.chat()on the same persistent agent, then walks the captured wire payload asserting everytool_callsmessage is followed by atoolmessage. That's the exact probe I ran manually in round 3 — now it's pinned forever. And the conversation-state assert (answered.contains(rogueAssistant.toolCalls!.last.id)) covers the in-memory side too~80a8a71with the pin actually checked out at8b0620c:finalAnswerOnMaxRoundsstill lives at precisely one site (agent_runner.dart:1649, the single_createSubAgentToolfactory all four run modes build through);angela_coreanalyze 0 errors + exactly the 4 pre-existing infos, 145/145 tests;angela_serveranalyze clean, 28/28;angela_apianalyze clean, 16/16. (One amusing scare: the analyzer briefly reported 148 issues inangela_apimid-submodule-swap — stale context, three consecutive clean runs after. Environmental, not real~)💡 Little ideas (non-blocking)~
8b0620cnow. Stale prose in an otherwise honest PR body; fix or ignore~8b0620cis its branch head, not master (master =436b956). Merge #10 first — then, if you want the pin to point at merged master, it's a pure fast-forward bump. Your call whether that's worth one more sync commit; the branch-head pin is at a commit I've fully verified either way, so I won't insist~ ♡SubAgentToolwiring test on the angela side —agent_runner.dartsits at 22.2% coverage. The behavior is now thoroughly pinned upstream including the rogue-call path, so this remains a want, not a need.✅ What I liked~
Automated review by Jibril · 2026-08-26
CI/CD: inconclusive for head
80a8a71(coverage bots 6555–6557 predate both syncs) · Local checks: openrouter_dart@8b0620c 629/629 + mutation probe RED-then-green; angela_core 0 errors/4 pre-existing infos + 145/145; angela_server clean + 28/28; angela_api clean + 16/16 — all at the verified pinMerge checklist, closed~
3d65122== PR head tree (no squash drift), checked locally before anything else.5900dccbumpspackages/openrouter_dartto3d65122(not a branch head this time, exactly as your round-1/round-4 checklist asked)._subAgentGuidancesentence added: the Tips list now teaches that a result starting with[Sub-agent reached its tool-round limit...]means partial findings — re-delegate the unfinished remainder with narrower instructions.Verification at
5900dccwith the pin at merged master:angela_coreanalyze 0 errors (4 pre-existing infos) + 145/145 tests;angela_serveranalyze clean. PR body refreshed (dependency marked merged, pin SHA and test counts current).Ready to merge whenever you are~
🔮 fufu~ Jibril reviewed your code!
Round five, the final form~ ♡ Last round I approved with one little wish attached: "merge #10 first, then fast-forward the pin to master." And what do you hand me? Exactly that — plus one line of guidance that teaches the orchestrator what to do with a salvaged result. You even answered my carried idea about making the salvage prefix actionable. Of course I peeled the whole thing open anyway~
Verdict: ✅ Looks good to me~
The delta
80a8a71 → 5900dccis exactly two files:8b0620c → 3d65122, and3d65122is the merge commit of openrouter_dart #10 into master. I checked the thing that actually matters: the tree hash of3d65122is87dcfc0a…— byte-identical to the8b0620cI fully verified last round (629/629 tests, rogue-call mutation probe RED-then-green). The merge changed zero content. My upstream verification carries over by proof, not assumption~system_prompt_builder.dart:919— one new Tips line in_subAgentGuidance: "If a result starts with[Sub-agent reached its tool-round limit...], … re-delegate the unfinished remainder with narrower instructions." I traced every link in that chain:[Sub-agent reached its tool-round limit and returned its findings so far; parts of the task may be unfinished]atsub_agent_tool.dart:172, and the truncated form in the guidance is a faithfulstartsWithmatch. The emitted prefix is itself pinned upstream by two tests (sub_agent_tool_test.dart:398, 436), so the cross-repo contract can't silently drift ♪_subAgentGuidanceis gated onhasSubAgentToolinbuildChatPromptSections(:173),buildTimerPrompt(:281), andbuildUberIchPrompt(:372) — all three builder paths that carry sub-agent guidance, matching the three_createSubAgentToolrun modes exactly. The peer mode has neither the tool nor the gate (itsbuildPeerPromptcarries no sub-agent guidance by design) — sibling-consistent, nothing dangling.hitMaxRounds-with-empty-content arm still returns its distinct"Sub-agent exhausted its tool rounds and returned no content; nothing was salvaged."— which correctly does not match the guidance trigger, since there's nothing to re-delegate from. The one-line trigger stays precise~Local checks (coverage bots 6555–6557 predate every sync — stale, so I ran everything myself at
5900dccwith the pin checked out at3d65122):angela_core: analyze 0 errors + exactly the 4 pre-existing infos, 145/145 tests passangela_server: analyze clean, 28/28angela_api: analyze clean, 16/16💡 Little ideas (non-blocking)~
3d65122, which is precisely what the Notes promised would happen. One final sweep of those two lines and the body tells the whole truth~SubAgentToolwiring test —agent_runner.dartremains at ~22% coverage. The behavior is thoroughly pinned upstream; this remains a want, not a need.✅ What I liked~
startsWithrecognition keeps the orchestrator's contract with the library at the string level the library actually pins in its tests — no schema coupling, no drift risk beyond what's already guarded. Fufu~ elegant ♡Merge order resolved itself: #10 is in master, the pin points at it, all suites green. This goes in with my wholehearted smile~ ♪
Automated review by Jibril · 2026-08-26
CI/CD: inconclusive for head
5900dcc(coverage bots 6555–6557 stale, predate all syncs) · Local checks: angela_core 0 errors/4 pre-existing infos + 145/145 · angela_server clean + 28/28 · angela_api clean + 16/16 · pin 3d65122 tree-identical to verified 8b0620c (629/629 upstream)