Files
tai/research/agentic-tool-loop-design/conclusions/structural-loop-control-and-repair.md
T
retoor afc88b8087 ask_user tool, prompt-JSON CLI repair loop, tool-loop research archive
- New ask_user core tool: mid-turn user question with option picking,
  refusing cleanly in autonomous mode, for background workers, and
  without a tty; system prompt points at it instead of prose questions.
- Structural loop control for prompt-JSON CLI backends (opencode,
  grok, claude, codex, gemini): shared cli_json_turn repairs a
  missing JSON envelope exactly once, then accepts prose; prompt
  contract hardened against narrate-without-calling.
- research/agentic-tool-loop-design: 74 sources plus 2 conclusions
  backing the loop-control decisions; INDEX updated to 6 subjects.
2026-10-11 02:54:17 +02:00

5.6 KiB

Conclusion: Structural Loop Control with JSON-Contract Repair

Metadata

Field Value
Institute Molodetz Institute — Research & Retrieval Division
Subject agentic-tool-loop-design
Document type Conclusion
Tier N/A
Date decided 2026-10-11
Decided by retoor, advised by tai (agent, Muse Spark)
Status Active
Confidence High

Question / problem statement

In REPL use through the opencode backend, tai kept ending its turn after narrating an intended action ("Ik kijk even mee op je scherm...") instead of calling the tool, so the user had to type "doe dan..." to prod it forward step by step. The question was how to make the agent autonomous again without resorting to regexes that guess intent from assistant prose.

Conclusion

Loop control stays purely structural: a turn continues if and only if the backend reply carries tool calls, and ends on content without them. Tai's normalized tool_calls presence/absence is its cross-backend equivalent of Anthropic's stop_reason, and assistant text must never drive loop decisions. The narration-instead-of-call failure is fixed at the layer that manufactures that signal: the prompt-JSON CLI adapters (opencode, grok, claude, codex, gemini) now treat a reply with no JSON envelope while tools are offered as a structural contract miss, retry exactly once with an explicit correction prompt, and only then accept prose as the final answer. The prompt contract is hardened alongside: both the default system message and the CLI-backend instructions now state that an empty tool_calls list ends the turn at once and that a planned action must never be narrated without its tool call.

Supporting sources

  • Anthropic: Building effective agents: the canonical agent shape (LLM plus tools in a loop on environmental feedback, bounded by stopping conditions); tai's run_turn already matches it, so the fix belongs in adapters and contracts, not loop restructuring.
  • Claude docs: Stop reasons and fallback: the deterministic loop-control contract (tool_use continues, end_turn ends); tai's tool_calls signal is the multi-backend equivalent.
  • Anthropic: Writing effective tools for agents: descriptions and truncation steer behavior measurably; backs the prompt-contract half of the fix and error-as-result conventions.
  • Claude: Building agents with the Claude Agent SDK: gather-context/take-action/verify-work reference loop; validates tai's architecture direction.
  • elsung: small-model-tool-reliability: measured 25% narration-instead-of-call on weak models, tool_choice: required not portable, grammar enforcement unavailable to CLI wrappers; forces the repair-retry plus prompt-contract route tai took.
  • Internals Decoded: The ReAct Loop: failure-as-observation and stuck-detector-as-observation patterns; the repair prompt lives in the observation channel, not in text scraping.
  • AI Engineering Playbook: Tool Calling: free-form text plus regex parsing breaks on wording drift; its failure-mode table (system-prompt nudge for not-calling, harness caps for runaway retry) is the template for this fix.

Alternatives considered and rejected

  • Regexes on assistant prose to detect "intent to act" and force continuation: rejected by retoor outright and by the research (wording drift across models and languages, false positives on genuine finals, unmaintainable pattern lists).
  • Grammar-constrained decoding (the elsung report's primary fix): impossible through tai's CLI backends, which expose no sampler access; documented as the preferred fix if a backend ever offers it.
  • Reordering backends to prefer native tool-call HTTP APIs over prompt-JSON CLI models: would trade away offline/free-first behavior and is a bigger product decision; left for later, noted as a risk below.
  • Failing the turn on a contract miss with no repair (the looprig position): rejected because one cheap retry recovers the common weak-model stumble, and the repair is bounded to a single attempt.
  • tool_choice: required on HTTP backends: not portable per the elsung measurements and risks the forced-call infinite loop the OpenAI example warns about; not adopted.

Risks / open questions

  • Valid JSON with narration content and empty tool_calls still ends the turn; no structural check can catch intent phrased as a compliant final answer. Mitigation is the prompt contract plus, eventually, stronger backends.
  • Each genuine prose final on a CLI backend costs nothing extra (repair fires only on missing JSON), but each contract miss costs one extra model call; on slow free models that is noticeable latency per stumble.
  • If free-model quality drops further, the single repair may often fail; the retry budget is deliberately one, and raising it needs fresh evidence, not hope.

Action taken

Shipped in tai.py the same session: opencode_parse split into opencode_find_answer plus a behavior-identical lenient wrapper, new cli_json_repair_prompt, new ChatClient.cli_json_turn shared by all six prompt-JSON CLI turns with exactly-once repair (failed repair keeps the first answer), prompt-contract sentences in DEFAULT_SYSTEM and opencode_prompt, and a new CliJsonRepairTests class (7 tests) in test_tai.py. Full suite green: 357 test_tai.py plus 16 test_seal.py.

Changelog

Date Change
2026-10-11 Initial conclusion recorded.