agentscope-ai / agentscope-ai/agentscope-java

[Bug]: ToolCallsAccumulator silently swallows JSON parse failures for streamed tool call arguments and the raw string is lost

Abierto
#2,841 0 comentarios 0 reacciones 0 asignados Ver en GitHub
bug
Lenguaje dominante
Java
Estrellas
5.6k
Forks
1.3k
Merge medio
4 d 12 h
PR fusionados (30 d)
77

Descripción

## Describe the bug

`ToolCallsAccumulator$ToolCallBuilder.build()` parses the fully accumulated tool-call
arguments with `JsonUtils.getJsonCodec().fromJson(rawContentStr, Map.class)` inside a
`try/catch (Exception)` that does nothing on failure — no log, no event, no counter.
When the accumulated arguments are not valid JSON, the parsed input map stays empty and
`content` falls back to `"{}"`, so the string the model actually emitted is gone from the
entire system.

We hit this with Chinese LLMs (Qwen via OpenAI-compatible streaming), which fairly often
emit unescaped ASCII double quotes inside string values — a *complete* argument JSON that
is broken. App-side symptom: the tool is invoked with an empty parameter map and fails with
"missing required parameter". That looks like a model/tool-schema mismatch, and nothing in
the framework logs, events or traces hints at the real cause. We could only find the root
cause by decompiling the jar — the framework gives zero signal.

Verified at three points in time:
- agentscope-core 2.0.1: the bytecode catch handler is empty (it only stores the exception)
- agentscope-core 2.0.2 (latest release): bytecode identical to 2.0.1
- main branch (checked 2026-08-25): still `catch (Exception ignored) { // Parsing failed, keep previously merged args }` (ToolCallsAccumulator.java:116-118)

## To Reproduce

A streamed tool call whose accumulated arguments string is:

```json
{"projectName": "改造"文化礼堂"及配套设施"}
```

Minimal reproduction via the public API (same shape as `ToolCallsAccumulatorTest`):

```java
ToolCallsAccumulator accumulator = new ToolCallsAccumulator();
accumulator.add(ToolUseBlock.builder()
.id("call_1")
.name("write_text_file")
.content("{\"projectName\": \"改造\"文化礼堂\"及配套设施\"}")
.build());
ToolUseBlock toolCall = accumulator.buildAllToolCalls().get(0);
// toolCall.getInput() -> {} (empty)
// toolCall.getContent() -> "{}"
```

What `build()` does (ToolCallsAccumulator.java:100-131 on main):

1. `fromJson(rawContent, Map.class)` throws (unescaped quotes) — caught and completely ignored.
2. `rawContent` is non-empty, but `JsonUtils.isValidJsonObject(rawContent)` is false, so `content` becomes `"{}"`.
3. Result: `ToolUseBlock` with `input = {}` and `content = "{}"`. The raw string is gone and this code path produces no log line at all.

The tool then fails with its own missing-parameter error, which is a misleading dead end.

## Expected behavior

The `{}` fallback itself is reasonable — the comment at lines 121-123 explains it exists
to prevent persisting malformed JSON when streaming is interrupted. The problem is that
the failure is completely unobservable:

1. Minimum: log a `warn` in the catch handler, including the raw string and the underlying
exception. A `catch (Exception ignored)` that silently drops a parse error makes this
whole class of failures effectively undiagnosable.
2. Discussion: expose the raw arguments string somewhere reachable for diagnostics without
polluting the persisted `content` — e.g. a dedicated field on `ToolUseBlock`, or via its
existing metadata, so applications can retry, repair, or at least show the user what the
model actually emitted.

## Environment

- agentscope-core 2.0.1 (also verified in 2.0.2 and on main as of 2026-08-25)
- JDK 21, Qwen via OpenAI-compatible endpoint, streaming tool calls

## Additional context

Malformed tool-call arguments are a recurring pain point here, but nobody has raised the
diagnosability angle:

- #847 — feature request for built-in multi-stage repair of malformed arguments (its
examples even include curly quotes). We deliberately do *not* ask for that here: we
repair app-side via the existing `JsonUtils.setJsonCodec()` extension point. The
blocker is that the default path gives no observable signal at all.
- #2648 — describes the same `{}` fallback for arguments truncated by `finish_reason=length`;
it likewise treats the fallback as existing behavior and asks for truncation detection.
- #1309 — stream=true malformed arguments (`unexpected end-of-input in property name`);
different root cause (incomplete chunks), same silent-failure symptom.
- #2451 (merged) — made `build()` always parse the accumulated raw JSON and repair null
input values, but only when the raw string is valid JSON; the invalid-JSON path is
still silent.

Happy to submit a PR if you agree on the direction (warn log in the catch handler plus an
observable channel for the raw string).

Guía de contribución

Abrir la guía de contribución

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.