Skip to content

feat(agent): persist unresolved manual tool calls as awaiting_client_tools#64

Open
LukasParke wants to merge 2 commits into
mainfrom
feat/manual-tool-pending-state
Open

feat(agent): persist unresolved manual tool calls as awaiting_client_tools#64
LukasParke wants to merge 2 commits into
mainfrom
feat/manual-tool-pending-state

Conversation

@LukasParke

@LukasParke LukasParke commented Jul 14, 2026

Copy link
Copy Markdown

Problem

Manual tools (execute: false) stop the loop correctly (#59) but persist nothing: getPendingToolCalls()[], status stays in_progress, and serialized state carries no trace of the calls. Serverless/cold-start consumers must scrape getNewMessagesStream() to capture proposals (agent-monorepo wikillm FRICTION #3).

Fix

Persist unresolved manual calls to state.pendingToolCalls with new ConversationStatus 'awaiting_client_tools' at all three stop paths (first-round all-manual, mid-loop all-manual, mixed unresolved). Resume with new input clears pendings (manual calls aren't approved/rejected by id — callers execute externally and continue with function_call_output). HITL awaiting_hitl untouched.

Tests

4 new cases in manual-tool-pending-state.test.ts incl. JSON round-trip + HITL no-regression; suite 301 green, zero existing tests adjusted. Changeset: minor (additive status).


Open in Devin Review

…tools

Manual tools (execute: false) stopped the loop without persisting
anything; getPendingToolCalls() returned [] and cold-start consumers
could not see the calls in serialized state. Persist unresolved manual
calls to pendingToolCalls with new status 'awaiting_client_tools' at
all three stop paths. Resume with new input clears pendings. HITL
(awaiting_hitl) untouched.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

@LukasParke

Copy link
Copy Markdown
Author

Reviewed 503d5b0 — strictly better than the original resume semantics. Clearing pendingToolCalls at load time meant a failed resume request destroyed the only copy of the pending manual calls — precisely wrong for the serverless/cold-start flows this feature targets. Deferring the clear to saveResponseToState (atomic with the successful response) and skipping the eager in_progress flip keeps the paused state truthful while the request is in flight.

Verified: flag resets after first save (multi-turn safe); loaded state object no longer mutated (good catch — load() may return a shared reference, and there's a dedicated test for it); changeset updated to match; clear-on-success / keep-on-failure / no-mutation tests all present; HITL regression still green.

Suite verified: 27 files / 304 tests green.

@perry-the-pr-reviewer perry-the-pr-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perry's Review

Verdict: 💬 Comments / questions

(Posting as COMMENT, not APPROVE: the maintainer GitHub App lacks pull_requests:write on OpenRouterTeam, so Perry cannot record a formal approval here. Content-wise this is an LGTM.)

Details

Clean, additive feature. Unresolved manual tool calls now persist to pendingToolCalls with a new distinct awaiting_client_tools status at all three stop paths (first-round all-manual, mid-loop all-manual, mixed auto+manual), mirroring persistHitlPause exactly. The resume semantics are the careful part and they're right: resumingFromClientTools defers clearing the stale pendings until after the resumed request produces a response (cleared atomically in saveResponseToState), so a failed resume leaves the paused state intact — and there's a test for exactly that. HITL (awaiting_hitl) is untouched, requiresApproval() now also reports the new status, and the type + README doc comments are updated. Suite green locally (7/7), including a JSON round-trip and a HITL no-regression case.

Risk is Medium only because this adds a new public ConversationStatus value and changes the terminal state of a previously-"complete" (well, previously-mislabeled) run — consumers switching exhaustively on status will need the new arm. That's the intended behavior and it's a minor changeset, so it's fine; flagging for reviewer awareness. Two non-blocking notes inline.

if (
status === 'awaiting_approval' ||
status === 'awaiting_hitl' ||
status === 'awaiting_client_tools'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding awaiting_client_tools to the public ConversationStatus union is a breaking-ish surface change for any consumer doing an exhaustive switch on status (the repo's own style mandates default: value satisfies never). Please confirm no in-repo or known downstream consumer switches exhaustively on ConversationStatus without a default, and that the changeset being minor (not major) is the intended call for adding an enum member.

▶ Prompt for agents: Grep consumers for exhaustive switches over ConversationStatus and confirm they either have a default arm or are updated to handle awaiting_client_tools. Confirm the minor changeset bump is correct for adding a public status value.

): Promise<void> {
this.finalResponse = currentResponse;

if (!this.stateAccessor || unresolvedCalls.length === 0) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

persistClientToolsPause early-returns without persisting when !this.stateAccessor, so a manual-tool run with no state accessor now silently ends with status left as-is (not complete, since the old markStateComplete() path was replaced) and no pendings surfaced. That's arguably better than the old false complete, but confirm the no-accessor manual path is acceptable — the caller can still read getPendingToolCalls() off the in-memory result, but nothing is durable. Worth a one-line test or doc note on the no-accessor behavior.

▶ Prompt for agents: Add a test for the manual-tool stop path with no StateAccessor to pin the intended behavior (result exposes pending calls in-memory; nothing persisted), or document that manual tools require a StateAccessor to be recoverable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant