agentscope-ai / agentscope-ai/QwenPaw

[Bug]: Model requests whose `input` ends with an assistant text turn are rejected by Volcengine Ark Responses API — 400 "MissingParameter: partial"

Open
#7,549 2 comments 0 reactions 1 assignee Claimed by @XiuShenAl View on GitHub
bug
Dominant language
Python
Stars
34.9k
Forks
3.1k
Avg merge
1d 15h
Merged PRs (30d)
225

Description

## QwenPaw Version

2.2.0

## Description

When QwenPaw builds a model request whose conversation `input` **ends with an
assistant text turn** (a `message` item with `role: "assistant"` containing
`output_text`) instead of a user message or a tool result, Volcengine Ark's
Responses API rejects the whole request with:

```
400 BadRequest
{"error":{"code":"MissingParameter",
"message":"The request failed because it is missing `partial` parameter. ...",
"param":"partial","type":"BadRequest"}}
```

The error name is misleading: `partial` is **not** a real request parameter
(passing `"partial": true` does not help — verified). The actual trigger is
Ark's prefill validation: an assistant text turn is not a valid **last** item
of the `input` list.

Normal interactive chat never trips this, because every model call is preceded
by a fresh user message, so `input` always ends with `user`. We hit it through
the **background-tool-completion path**: when an offloaded tool call finishes,
QwenPaw injects a completion hint into the conversation **as an assistant
message** and then automatically re-invokes the model to process it. The new
request's `input` therefore ends on that assistant turn, and Ark 400s — the
whole agent turn is lost.

**Root cause (short version):** the background-tool-completion notification is
written as a plain message flattened into the context instead of being
represented as a proper tool call/result (`function_call` +
`function_call_output` items in the Responses API). Because that flat message
uses `role="assistant"`, the next model request ends on an assistant turn, which
Ark's prefill validation rejects.

The injected hint is built in `qwenpaw/tool_calls/_hint.py`
(`make_offload_hint_msg`), which constructs the hint with `role="assistant"`.
The docstring shows this was a deliberate trade-off to avoid orphan `role=tool`
wire messages in the Chat Completions format (a `tool` message must be paired
with a preceding assistant `tool_calls`; the pairing can be lost to
compaction). The shortcut fixes one provider format's hazard but breaks Ark's
Responses prefill validation:

```python
return Msg(
name="system",
role="assistant", # ← makes the request end on an assistant turn
content=[notification] + result_blocks,
)
```

where `notification` is the literal ` Background tool
call ... completed with state=... ` text and
`result_blocks` is the tool's actual (untrusted) output.

**Related PR(s):** #7409 (related — fixes the sibling empty-block trigger only, explicitly does **not** cover this case), #7402 (same family, different trigger)

**Security considerations:** The fix must not route the tool's **untrusted
output** (`result_blocks`) into an instruction channel with elevated authority.
OpenAI's instruction hierarchy is System > Developer > User > Tool: tool output
is *supposed* to live at the lowest trust level. Simply changing the hint's
role to `system` or `user` would both fix the 400 **and** raise the prompt-
injection authority of whatever the tool returned (relevant for tools that
fetch untrusted content — web search, MCP servers, scrapers). Notably, the
current `role="assistant"` placement is comparatively safe (model-own output is
not an instruction source), so the fix should preserve a low-authority channel
for the tool result (e.g. a proper `function_call` / `function_call_output`
pair) rather than promote it.

## Component(s) Affected

- [x] Core / Backend (app, agents, config, providers, utils, local_models)
- [ ] Console (frontend web UI)
- [ ] Channels (DingTalk, Feishu, QQ, Discord, iMessage, etc.)
- [ ] Skills
- [ ] CLI
- [ ] Documentation (website)
- [ ] Tests
- [ ] CI/CD
- [ ] Scripts / Deploy

## Environment

- **QwenPaw version:** 2.2.0
- **OS:** Debian 12 (bookworm), Linux x86_64 (containerized)
- **Install method:** Docker (custom image)
- **Python version (if applicable):** 3.11.2

## Steps to Reproduce

Minimal API-level repro (verified against Ark Responses API via a LiteLLM
gateway proxying to `https://ark.cn-beijing.volces.com/api/coding/v3/responses`;
the input-shape check is provider-side, not model-specific):

1. Build a Responses-API request whose `input` ends with an assistant text turn:

```json
{
"model": "deepseek-v4-flash",
"input": [
{"type": "message", "role": "user",
"content": [{"type": "input_text", "text": "hi"}]},
{"type": "message", "role": "assistant",
"content": [{"type": "output_text", "text": "hello"}]}
],
"stream": true
}
```

2. `POST .../v3/responses` with the above body.
3. Observe the `400 MissingParameter: partial` response (reproducible every time).

In-product trigger (how we first hit it):

1. Start an agent turn in the inter-agent "console" flow that runs a long tool
call which gets **offloaded** (background execution).
2. When the background tool completes, `make_offload_hint_msg`
(`tool_calls/_hint.py`) injects a `role="assistant"` hint message
(` ... completed with state=... `
+ the tool result) into the conversation history.
3. QwenPaw automatically re-invokes the model to process the hint. The request
`input` is the whole history, which now ends on that assistant turn (no
trailing user message / tool result).
4. Ark returns `400 MissingParameter: partial`; QwenPaw logs
`cancel-save: persisted interrupted turn` and the reply is lost.

## Actual vs Expected

- **Actual:** `400 MissingParameter: partial`; the entire agent turn fails and
the reply is never produced.
- **Expected:** The model accepts the background-tool-completion hint as part of
the conversation history and continues normally.

Verified boundary matrix (last item of `input`):

| Last item of `input` | stream=true | stream=false |
| ------------------------------------------ | ----------- | ------------ |
| `message` role=`user` | 200 | 200 |
| `message` role=`assistant` (`output_text`) | **400** | **400** |
| `message` role=`system` | 200 | — |
| `message` role=`developer` | 200 | — |
| `function_call` (no trailing output) | 200 | — |
| `reasoning` | 200 | — |
| `function_call_output` | 200 | — |

## Logs / Screenshots

Production payload tail (2026-09-04) — the request `input` ended with:

```json
{"role": "assistant", "content": [
{"type": "output_text", "text": "\nBackground tool call `execute_shell_command` (id=call_0cyc7uqqtz5ie97qksp9ce4f) completed with state=success. Result below.\n"},
{"type": "output_text", "text": "Command failed with exit code 18."}
]}
```

(The notification text matches `make_offload_hint_msg` byte-for-byte.)

QwenPaw runtime log:

```
runtime: unhandled error session=: Error code: 400
- {'error': {'code': 'MissingParameter', 'message': 'The request failed because
it is missing `partial` parameter. ...', 'param': 'partial', 'type': 'BadRequest'}}
cancel-save: persisted interrupted turn
```

## Additional Notes

- `partial` is a red herring: it is not a documented request parameter and
adding `"partial": true` does not change the outcome.
- The failure is independent of `stream` (non-streaming requests 400 as well).
- **Distinct from #7402 / #7409.** #7402 is an empty `output_text` block
**anywhere** in `input` (`MissingParameter: input.content.text`), fixed at the
persistence layer by #7409 (drop empty text blocks). This issue is a
**trailing assistant text turn** (`MissingParameter: partial`) and requires a
fix at the point the offload hint is injected / the model is re-invoked;
#7409 leaves non-empty trailing assistant turns untouched, so it does not
resolve this.
- **Suggested fix direction (verified):** represent the background-tool
completion as a proper, **self-contained** tool call/result pair instead of a
flattened `role="assistant"` message. Concretely: emit a `function_call`
item and its matching `function_call_output` item together (using a fresh
synthetic `call_id` so it never collides with the original background call),
with the notification + tool output as the `function_call_output` content.

Why a **pair** is mandatory (cross-provider): we verified that Ark *accepts*
an **orphan** `function_call_output` (no matching `function_call` anywhere —
tests T14/T16 returned 200), and that an `input` ending on a
`function_call_output` is accepted (200) — the **only** trailing item that
400s is an assistant text turn. But the orphan tolerance is **Ark-specific**:
OpenAI's Responses API rejects an orphan with `400 No tool call found for
function call output with call_id`, and OpenAI Chat Completions rejects
orphan `role=tool` ("must be a response to a preceding message with
'tool_calls'"). A self-contained pair is therefore never orphan and works on
every provider: it ends on `function_call_output` (Ark OK), the
`function_call`/`function_call_output` pairing is complete (OpenAI OK), the
Chat formatter emits a paired `role=tool` (OK), and the tool result stays in
the lowest-authority tool channel (Security considerations). No presence
check or history-state dependency is needed.

If the hint must remain a plain message instead, keep it at
`role="assistant"` and fix the call shape so the input never ends on an
assistant turn. Avoid `role="system"`/`role="user"` for the tool result (see
Security considerations).
- Workaround while unfixed: append a trailing user message (or a fresh user
message) so the `input` ends on a position Ark accepts.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.