Skip to content

AI_APICallError: tool_use blocks found without corresponding tool_result blocks #2720

Closed

Description

@sahilchouksey

Description

Getting an AI_APICallError when tool_use blocks are not immediately followed by tool_result blocks.

Error Message

AI_APICallError: messages.69: `tool_use` ids were found without `tool_result` blocks immediately after: toolu_01X6jLG6TqrVeBTSEqq4ozEN. Each `tool_use` block must have a corresponding `tool_result` block in the next message.

Expected Behavior

The system should handle tool_use blocks properly and ensure each tool_use has a corresponding tool_result.

Additional Context

  • This appears to be a message formatting/validation issue
  • The error references a specific tool_use ID: toolu_01X6jLG6TqrVeBTSEqq4ozEN
  • Message index 69 suggests this occurs in longer conversations

Steps to Reproduce

This error occurred during normal usage when the AI made tool calls but the tool results weren't properly matched.

Activity

  1. arsham commented on Sep 21, 2025

    @arsham

    I have experienced this too. For now you could do this: type /undo, then enter. It will resend your last prompt and it usually works. Sometimes I had to restart opencode and load the session, then undo the last prompt and continue. It worked for me, hopefully it works for you tool

  2. rekram1-node commented on Sep 21, 2025

    @rekram1-node
    Contributor

    can anyone share a session for this by chance?

  3. self-assigned this
    on Sep 21, 2025
  4. sahilchouksey commented on Sep 22, 2025

    @sahilchouksey
    Author

    I am not able to find the session for this one. If this error appears again, I will provide the session in this thread.

  5. Davincible commented on Dec 9, 2025

    @Davincible

    @rekram1-node this just happened to me on Opus 4.5, opencode version 1.0.134

    Session log

    Optimizing YouTube transcript retrieval

    Session ID: ses_4fc89a65dffeXDBJYG337eRw9o
    Created: 12/9/2025, 3:14:06 PM
    Updated: 12/9/2025, 3:25:33 PM


    User

    At the end, I always use this library to get transcripts. Right now, we take the difficult route, being downloading the audio file, and then transcribing it. However for a lot of videos there is a transcript available directly from youtube, and then we can bypass the manual download entirely.

    I found this library online: https://github.com/jdepoix/youtube-transcript-api

    What I want the new flow to be when syncing a video:

    1. CHeck if there is a transcript available directly
    2. If not, take the original route. If there is, download transcript, and proceed

    We can do these steps in prallel as well. E.g. we synchronously go through the video list, but when we need to actually download an mp3, we can do that in a different worker, and already check the next video for a direct transcript, then queue up MP3 downloads with the worker

    This is a brief overview of what I want. Please think this through in depth, create a complete plan and spect to implement this, and write it down in a file


    Assistant

    I'll analyze this request thoroughly. Let me first understand the current implementation and then explore the youtube-transcript-api library to create a comprehensive plan.

    Tool: task
    
    Tool: webfetch
    

    Assistant

    I now have a good understanding of both the current codebase and the youtube-transcript-api library. Let me ask some clarifying questions before I create a detailed plan.

    Clarifying Questions

    Before I create the complete spec, I have a few questions to ensure the plan matches your expectations:

    1. Transcript source preference: When a YouTube transcript is available, should we:

      • Always prefer YouTube's transcript over MLX Whisper transcription?
      • Allow user configuration (e.g., --prefer-youtube-transcript flag)?
      • What if YouTube has auto-generated captions vs. manual captions - do you have a preference?
    2. Language handling: The youtube-transcript-api supports multiple languages. Should we:

      • Try to match the user's configured language preference?
      • Fall back to English if the preferred language isn't available?
      • Store language metadata in the database?
    3. Transcript format consistency: YouTube transcripts have timestamps for each snippet. Should we:

      • Keep the same output format as MLX Whisper (just text with metadata header)?
      • Preserve timestamps in a different format?
      • Store a flag indicating the transcript source (youtube vs whisper)?
    4. Parallel worker architecture: You mentioned:

      "we synchronously go through the video list, but when we need to actually download an mp3, we can do that in a different worker"

      Currently downloads are sequential (main thread) and transcriptions are concurrent (worker thread). Are you proposing:

      • Option A: Keep downloads sequential, but check YouTube transcripts first (fast), then queue MP3 downloads only when needed
      • Option B: Add a separate download worker pool for parallel MP3 downloads
      • Option C: Something else?
    5. Error handling: If YouTube transcript fetch fails (e.g., video has no transcript, IP blocked), should we:

      • Silently fall back to download+transcribe?
      • Log a warning and fall back?
      • Retry with different language codes?
    6. Database schema: Do you want to track:

      • Which videos have YouTube transcripts available?
      • Which source was used for each transcript (youtube_api vs whisper)?
      • Transcript language?

    Let me know your thoughts on these questions and I'll create a comprehensive implementation plan.


    User

    1. Yes, always prefer from the API, both auto generated or manual
    2. always english, I don't care about other languages
    3. Don't care about timestamps. Just text like we get with whisper
    4. You can architect it, but I think: 1 worker to check transcript API (fast), if not, queue and send off to 1 download worker, then we have the transcript worker after that
    5. If IP blocked, please log it out to console as a nice warning, and fallback to download. No transcript available is expected behavior and then we fall back to mp3
    6. If currently we track anything about videos in e.g. database somewhere, then sure you can add that. Otherwise don't bother

    Assistant

    Thank you for the clarifications. Now I have everything I need to create a comprehensive plan. Let me write this to a spec file.

    Tool: read
    
    Tool: read
    
    Tool: read
    

    Assistant

    Now I have a complete understanding of the codebase. Let me also check the config and transcriber for any relevant details:

    Tool: read
    
    Tool: read
    

    Assistant

    Now I have all the information I need. Let me create a comprehensive spec document:

    Tool: write
    

    Now I have all the context I need. Let me write a comprehensive specification document:

    Tool: write
    

    Assistant


    I don't want to undo the last whole message as I think that would delete the result, but having to start a whole new conversation is also annoying

    Image
  6. Mishkun commented on Dec 16, 2025

    @Mishkun
    Contributor

    @rekram1-node Hey, what's the priority on this? It is a single most annoying bug that can kill the long running session randomly without a chance to recover or summarize to handover to another session. And it happens to me regularily enough to be a pain

  7. rekram1-node commented on Dec 16, 2025

    @rekram1-node
    Contributor

    @Mishkun if you can gimme a opencode export > session.json I can fix it

    But I need the actual session data to figure out where it went wrong

  8. rrohrer commented on Dec 16, 2025

    @rrohrer

    session-ses_4d6d.md
    session.json

    @rekram1-node not OP but here is a session md and json from me hitting this in version *.164

  9. rekram1-node commented on Dec 16, 2025

    @rekram1-node
    Contributor

    okay on my list to checkout here

  10. Mishkun commented on Dec 23, 2025

    @Mishkun
    Contributor

    #1662 related

  11. rekram1-node commented on Jan 2, 2026

    @rekram1-node
    Contributor

    I think there was a fix in upstream ai-sdk package that we updated to, not sure if it resolved this or not

  12. arielpollack commented on Jan 14, 2026

    @arielpollack

    still happens, and in most of my sessions it evetualy happens

  13. zyriab commented on Jan 24, 2026

    @zyriab

    it happens all the time, it makes working with Anthropic models pretty hard :(

  14. Yukimir commented on Jan 24, 2026

    @Yukimir

    i find that if the agent called a command that print nothing (such as mkdir), the framework will not add the tool call result to the message list and cause this problem.

  15. lontaian commented on Feb 22, 2026

    @lontaian

    Additional Root Cause Analysis

    I traced this bug end-to-end through OpenCode's binary and the oh-my-opencode plugin (v3.8.3). Here's what I found:

    The Bug (confirmed, still present)

    When a tool execution is aborted (ESC, timeout, or "Tool execution aborted"), OpenCode records the assistant's tool_use block in the message history but does not insert a corresponding tool_result placeholder. The next API call sends this malformed history, and Anthropic rejects it with a 400 error.

    Where it breaks (from binary strings extraction)

    OpenCode's session.error event schema:

    BusEvent.define("session.error", zod_default.object({
      sessionID: zod_default.string().optional(),
      error: MessageV2.Assistant.shape.error
    }))

    Note: No messageID field is included. This means plugin-level recovery mechanisms cannot identify which assistant message caused the failure. oh-my-opencode has a session-recovery hook that can inject synthetic tool_result blocks, but it silently fails because messageID is missing from the event payload.

    Specific triggers observed

    1. User presses ESC during tool execution
    2. Tool execution times out (bash 120s timeout)
    3. "Tool execution aborted" from user interrupt while multiple parallel tool calls are in-flight
    4. Tool that produces empty output (as noted by @Yukimir above)

    Suggested fix

    1. Primary: When a tool call is aborted, OpenCode should inject a tool_result with is_error: true and content like "Tool execution was aborted" — similar to how Claude Code handles this
    2. Secondary: Include messageID in the session.error event so plugins can implement recovery

    Environment

    • OpenCode binary: Feb 20, 2026 build
    • Anthropic model: claude-opus-4-6
    • OS: WSL2 (Ubuntu)
    • Still reproducible as of today

    This is currently the most impactful bug for long-running sessions with Anthropic models. Once it hits, the session is permanently broken with no recovery possible.

  16. gotgenes commented on Feb 27, 2026

    @gotgenes

    New trigger scenario + database recovery method

    OpenCode version: 1.2.15
    OS: macOS
    Model: claude-opus-4-6 (Anthropic)

    Trigger: duplicate question tool calls in a single assistant turn

    I hit this bug during a /plan session. The assistant's response included two question tool calls in the same turn. The first call had empty input ({}) and was immediately aborted; the second completed normally (I answered it via the UI widget).

    The aborted part in the SQLite database looked like this:

    {
      "type": "tool",
      "callID": "toolu_012njbgNGVjv9fBgq9bikyse",
      "tool": "question",
      "state": {
        "status": "error",
        "input": {},
        "error": "Tool execution aborted",
        "time": { "start": 1772227548994, "end": 1772227548994 }
      }
    }

    This is consistent with the stream truncation scenario described in #13102 — the first tool call appears to have been emitted with incomplete/empty arguments, OpenCode aborted it, but no tool_result was generated. The session was permanently broken from that point.

    Database-level recovery procedure

    For anyone stuck with a corrupted session and no way to /undo or /share, here's a manual recovery method that worked for me. Back up the database first.

    The database is at ~/.local/share/opencode/opencode.db (SQLite).

    Step 1: Find the aborted tool part:

    SELECT p.id, json_extract(p.data, '$.callID') AS callID,
           json_extract(p.data, '$.state.status') AS status,
           json_extract(p.data, '$.state.error') AS error
    FROM part p
    WHERE p.session_id = '<YOUR_SESSION_ID>'
      AND json_extract(p.data, '$.type') = 'tool'
      AND json_extract(p.data, '$.state.status') = 'error';

    Step 2: Delete the aborted tool part (the one whose callID matches the error message):

    DELETE FROM part WHERE id = '<ABORTED_PART_ID>';

    Step 3: Delete the error retry messages (all the failed user+assistant message pairs that accumulated after the corruption):

    -- Find the timestamp of the first error message
    SELECT id, time_created FROM message
    WHERE session_id = '<YOUR_SESSION_ID>'
      AND json_extract(data, '$.error') IS NOT NULL
    ORDER BY time_created LIMIT 1;
    
    -- Delete parts belonging to error/retry messages
    DELETE FROM part WHERE message_id IN (
      SELECT id FROM message
      WHERE session_id = '<YOUR_SESSION_ID>'
        AND time_created >= <FIRST_ERROR_TIMESTAMP>
    );
    
    -- Delete the error/retry messages themselves
    DELETE FROM message
    WHERE session_id = '<YOUR_SESSION_ID>'
      AND time_created >= <FIRST_ERROR_TIMESTAMP>;

    After this, reopening the session and sending a new message worked — the conversation resumed from the last valid state.

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions