Skip to content

Compaction produced no summary when the conversation ends on a tool result #53242

Open

Description

@LegendaryFire

Description

Auto-compaction often fails with "Compaction produced no summary" when using a models with tooling. I can only reproduce when the conversation ends on a tool result.

The summary request still includes the session's tools. The model then writes its summary in reasoning_content, makes a tool call, and returns empty content. OpenCode only reads content, so the summary is lost.

Plugins

None

OpenCode version

v2.0.22

Steps to reproduce

  1. Configure a provider pointing at llama.cpp using a Qwen3-based model. Use the reasoning_content config.
  2. Start a session that runs many tool calls in a row (e.g. Playwright MCP) until it reaches the compaction threshold.
  3. Auto-compaction fires while the last message is a tool result.
  4. You get Compaction produced no summary.

Screenshot and/or share link

No response

Operating System

Arch Linux 7.2.2

Terminal

Gnome Terminal

Activity

  1. Shalin-Shah-2002 commented on Oct 5, 2026

    @Shalin-Shah-2002

    I'd like to take this. The summary request still sends the session's tools, so the model can answer with a tool call and empty text. I'll set toolChoice: "none" on the summary request in session/compaction.ts and add a test.

  2. self-assigned this
    on Oct 22, 2026
  3. opencodejr commented on Oct 27, 2026

    @opencodejr

    Thanks for the clear report. I reproduced this on v2.0.22, and it still happens on the latest dev (fd8268e).

    There were no real model credentials here, so I used a mock llama.cpp-style OpenAI-compatible server for the model's side of what you describe. The provider is set up like yours: @ai-sdk/openai-compatible with interleaved: { field: "reasoning_content" } and a small context limit.

    • On normal turns the mock makes a shell tool call and reports a prompt near the context limit.
    • When it gets the summary request, it writes the summary in reasoning_content, makes a tool call, and returns empty content.

    Auto-compaction fires right after a tool result. The summary request and the follow-up reminder both still carry all of the session's tools, and only text content is read back. The run ends with Error: Compaction produced no summary. The script below reproduces it against either the installed release or a source checkout.

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Reproduces #53242: auto-compaction fails with "Compaction produced no summary" when the
    # model (llama.cpp/Qwen3 style) answers the summary request with reasoning_content + a tool call.
    # Usage: ./repro.sh            (uses `opencode` on PATH)
    #        OC_DEV=/path/to/opencode-checkout ./repro.sh   (runs from source)
    set -euo pipefail
    DIR=$(mktemp -d); cd "$DIR"
    cat > mock.ts <<'TS'
    Bun.serve({
      port: 8089,
      async fetch(req) {
        const body: any = await req.json()
        const msgs = body.messages
        const last = msgs.at(-1)
        const lastText = typeof last.content === "string" ? last.content : JSON.stringify(last.content)
        const isSummary = last.role === "user" && /## Objective|summary template/.test(lastText)
        const names = (body.tools ?? []).map((t: any) => t.function.name)
        const tool = names.find((n: string) => /bash|shell/.test(n)) ?? "bash"
        console.error(`[mock] ${msgs.length} msgs, roles=${msgs.map((m: any) => m.role).join(">")}, tools=${names.length}, summaryRequest=${isSummary}`)
        const id = "c" + Math.random()
        const chunk = (delta: any, finish: string | null = null, usage?: any) =>
          `data: ${JSON.stringify({ id, object: "chat.completion.chunk", created: 0, model: body.model, choices: [{ index: 0, delta, finish_reason: finish }], ...(usage ? { usage } : {}) })}\n\n`
        const call = () => ({ tool_calls: [{ index: 0, id: "call_" + Math.floor(Math.random() * 1e9), type: "function", function: { name: tool, arguments: JSON.stringify({ command: "echo step", description: "step" }) } }] })
        let out = ""
        if (isSummary) {
          // What the report describes: summary in reasoning_content, then a tool call, empty content.
          out += chunk({ role: "assistant", reasoning_content: "## Objective\nKeep browsing.\n\n## Next Move\nContinue.\n" })
          out += chunk(call())
          out += chunk({}, "tool_calls", { prompt_tokens: 7000, completion_tokens: 50, total_tokens: 7050 })
        } else {
          // Normal turn: a tool call, with a prompt size close to the 8192 context limit.
          out += chunk({ role: "assistant", reasoning_content: "Need another tool call." })
          out += chunk(call())
          out += chunk({}, "tool_calls", { prompt_tokens: 7900, completion_tokens: 20, total_tokens: 7920 })
        }
        return new Response(out + "data: [DONE]\n\n", { headers: { "content-type": "text/event-stream" } })
      },
    })
    TS
    bun mock.ts & MOCK=$!; trap 'kill $MOCK' EXIT; sleep 1
    mkdir proj && cd proj && git init -q
    cat > opencode.json <<'JSON'
    {
      "$schema": "https://opencode.ai/config.json",
      "model": "llama.cpp/qwen3",
      "permission": { "bash": "allow", "shell": "allow" },
      "provider": {
        "llama.cpp": {
          "npm": "@ai-sdk/openai-compatible",
          "options": { "baseURL": "http://127.0.0.1:8089/v1", "apiKey": "x" },
          "models": { "qwen3": { "name": "Qwen3", "reasoning": true, "interleaved": { "field": "reasoning_content" },
            "limit": { "context": 8192, "output": 1024 } } }
        }
      }
    }
    JSON
    if [ -n "${OC_DEV:-}" ]; then
      export BUN_OPTIONS="--conditions=browser --preload=$OC_DEV/packages/tui/node_modules/@opentui/solid/scripts/preload.js"; timeout 120 bun --conditions=browser --preload="$OC_DEV/packages/tui/node_modules/@opentui/solid/scripts/preload.js" "$OC_DEV/packages/cli/src/index.ts" run "browse around" 2>&1 || true
    else
      opencode --version; timeout 120 opencode run "browse around" 2>&1 || true
    fi

    Run it: bash repro.sh

  4. infinitr0us commented on Oct 30, 2026

    @infinitr0us

    Another data point, with a variant that the proposed toolChoice: "none" fix may not cover: no tool call at all, only reasoning.

    Setup: opencode 2.2.2 on macOS, an OpenAI-compatible provider (a local gateway in front of oMLX 0.7.2, which serves Qwen3.8-Flash-Next through the Splash engine), reasoning returned as reasoning_content, effort xhigh.

    • Auto-compaction fired right after a tool result, at about 207k tokens of a 256k window.
    • The summary request ended with finish_reason: stop after 3,223 tokens, 3,222 of them reasoning, and no text at all.
    • The retry ("The previous response did not fill in the required summary template...") did the same: 1,578 tokens, 1,577 of them reasoning.
    • Result: Compaction produced no summary. No output limit was involved (about 48k of output budget was left), and the server logged no error.

    Two earlier compactions in the same session also followed tool results and produced normal summaries, so it is intermittent. The model seems to write the summary inside its thinking and then stop. Sending the session a user message ("Please continue.") made the next compaction succeed.

    Since only text is read back, it might help to also handle a reply that is all reasoning: for example, retry with thinking off (or at low effort) and without tools, or use the reasoning text when the reply has no content.

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions