maestro/docs/issues/2026-06-30-agent-loop-followups.md
oss-sync b1292e34b2
Some checks failed
CI / build-and-test (push) Has been cancelled
sync: update from private repo (edc775f2)
2026-07-06 01:04:12 +00:00

6.0 KiB

Agent loop follow-up issues

Issue 1: Fix dangling control tool calls across movement transitions

Problem

transition and complete tool calls can be appended to the live conversation without a matching tool result message. When the same Conversation is reused by the next movement, strict OpenAI-compatible providers may reject the next request because every assistant tool_call must have a corresponding tool message.

Evidence

  • src/engine/agent-loop.ts: assistant messages are recorded with all pending tool calls, including control calls.
  • src/engine/agent-loop/terminal-control.ts: valid transition returns a MovementResult without a tool result message.
  • src/engine/agent-loop.ts: valid complete returns immediately without a tool result message.
  • src/engine/context/conversation.ts: replayableTurns() sanitizes persisted transcript replay, but live conversation.messages shared across movements is not sanitized.

Expected behavior

After a movement exits via transition or complete, the next model request must not contain unresolved control tool_calls.

Suggested fix

Normalize live conversation state before entering the next movement, or avoid recording terminal/control tool calls without corresponding tool messages.

Acceptance criteria

  • Add a regression test that uses a shared Conversation across a real transition.
  • The second movement's model input contains no dangling transition or complete tool calls.
  • Existing agent-loop tests continue to pass.

Issue 2: Make context overflow terminal defaults consistently abort

Problem

Context overflow handling has two different policies:

  • buildContextOverflowResult() converts terminal defaults (COMPLETE, ASK) to ABORT.
  • applyContextManagerUpdate() with force_transition uses movement.defaultNext ?? 'ABORT' directly.

This means context pressure can falsely complete or ask from a movement whose default next step is terminal, even though the safer policy is to abort on context loss.

Evidence

  • src/engine/agent-loop/context-control.ts: buildContextOverflowResult() normalizes COMPLETE and ASK to ABORT.
  • src/engine/agent-loop/context-control.ts: applyContextManagerUpdate() force transition uses movement.defaultNext directly.

Expected behavior

All context-overflow forced exits should use the same terminal-default policy. When context is compromised, terminal defaults should not be treated as successful completion.

Suggested fix

Share one helper for context-overflow movement results, or apply the same terminal-default normalization in applyContextManagerUpdate().

Acceptance criteria

  • Add a regression test for force_transition with defaultNext: "COMPLETE".
  • The result is an abort-style movement result, not a successful completion.
  • Existing context overflow tests continue to pass.

Issue 3: Enforce why_no_default for needs_user_input

Problem

The prompt and tool description require why_no_default when calling complete({ status: "needs_user_input" }), but runtime validation only requires missing_info.

This lets the agent ask the user without documenting why it could not choose a reasonable default, which weakens the intended "avoid unnecessary user questions" behavior.

Evidence

  • src/engine/agent-loop/prompt.ts: instructs the model to provide why_no_default.
  • src/engine/agent-loop/terminal-control.ts: tool description says why_no_default is required.
  • src/engine/agent-loop/terminal-control.ts: validateCompleteArgs() only checks missing_info.
  • src/engine/agent-loop/terminal-control.ts: movement output ignores why_no_default.

Expected behavior

needs_user_input should be rejected unless both missing_info and why_no_default are non-empty strings.

Suggested fix

Validate why_no_default in validateCompleteArgs() and include it in the result/debug output if useful.

Acceptance criteria

  • Add a negative test for missing why_no_default.
  • Add a positive test for needs_user_input with both fields.
  • Existing complete behavior remains unchanged for other statuses.

Issue 4: Validate interactive browse waiting-human session id

Problem

parseInteractiveBrowseWaitingHuman() casts sessionId to string without checking that it is present and actually a string.

If the tool returns malformed waiting_human output, the movement result can carry browserSessionId: undefined despite the type expecting a string.

Evidence

  • src/engine/agent-loop/tool-dispatcher.ts: sessionId is read via parsed["sessionId"] as string.
  • The function validates action and waitReason, but not sessionId.

Expected behavior

Malformed waiting-human output should not produce a typed waiting-human movement result with an undefined session id.

Suggested fix

Require typeof parsed["sessionId"] === "string" and a non-empty value before returning a waiting-human result.

Acceptance criteria

  • Add a test for malformed waiting_human output without sessionId.
  • Add a test for valid waiting_human output.
  • Existing interactive browse behavior remains unchanged for valid tool output.

Issue 5: Split tool-dispatcher.ts before it becomes the next agent-loop

Problem

tool-dispatcher.ts now owns several distinct responsibilities:

  • cache routing
  • cache hit/miss event logging
  • tool execution
  • memory checkpoint behavior
  • waiting-human parsing
  • batched result recording

This file is becoming the next high-coupling module after the agent-loop split.

Expected behavior

Tool dispatch should be decomposed into focused modules so bugs in cache routing, execution, and special tool outputs are easier to test independently.

Suggested fix

Split along behavioral boundaries, for example:

  • tool-cache-routing.ts
  • tool-execution.ts
  • tool-result-recorder.ts
  • interactive-browse-result.ts

Acceptance criteria

  • No behavior change.
  • Existing agent-loop and tool-loop tests pass.
  • New modules expose narrow, testable functions.