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: validtransitionreturns aMovementResultwithout a tool result message.src/engine/agent-loop.ts: validcompletereturns immediately without a tool result message.src/engine/context/conversation.ts:replayableTurns()sanitizes persisted transcript replay, but liveconversation.messagesshared 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
Conversationacross a realtransition. - The second movement's model input contains no dangling
transitionorcompletetool 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) toABORT.applyContextManagerUpdate()withforce_transitionusesmovement.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()normalizesCOMPLETEandASKtoABORT.src/engine/agent-loop/context-control.ts:applyContextManagerUpdate()force transition usesmovement.defaultNextdirectly.
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_transitionwithdefaultNext: "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 providewhy_no_default.src/engine/agent-loop/terminal-control.ts: tool description sayswhy_no_defaultis required.src/engine/agent-loop/terminal-control.ts:validateCompleteArgs()only checksmissing_info.src/engine/agent-loop/terminal-control.ts: movement output ignoreswhy_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_inputwith both fields. - Existing
completebehavior 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:sessionIdis read viaparsed["sessionId"] as string.- The function validates
actionandwaitReason, but notsessionId.
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_humanoutput withoutsessionId. - Add a test for valid
waiting_humanoutput. - 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.tstool-execution.tstool-result-recorder.tsinteractive-browse-result.ts
Acceptance criteria
- No behavior change.
- Existing agent-loop and tool-loop tests pass.
- New modules expose narrow, testable functions.