Skip to content

fix(claudecode): a message's content is a string or a list, not only a list - #7

Merged
pedromvgomes merged 1 commit into
mainfrom
fix/decode-string-content
Sep 7, 2026
Merged

fix(claudecode): a message's content is a string or a list, not only a list#7
pedromvgomes merged 1 commit into
mainfrom
fix/decode-string-content

Conversation

@pedromvgomes

Copy link
Copy Markdown
Owner

claudecode's decoder models message.content as a list of content blocks. Claude Code also emits it as a bare string, and json.Unmarshal returns an error for the shape it does not model. Decode returns that error, so one such line fails the entire run:

provider unavailable: claude-code emitted an unreadable event:
json: cannot unmarshal string into Go struct field .message.content of type []struct{...}

The failure surfaces as ErrProviderUnavailable — a stream that was working, reported as the CLI being unreachable, which invites a retry that fails identically.

Why the existing tolerance didn't cover it

Decode's doc says a line whose type this package does not model "is not a reason to fail a run that is working". That tolerance is keyed on the event type. A known type carrying a polymorphic field falls straight through it and reaches the json.Unmarshal error return at the top of the function.

The package already knows this exact polymorphism: toolResultText accepts string-or-blocks for a tool result's own content, and streamEvent's doc comment explains why. The same shape occurs one level up, on the message itself.

The change

message.content is decoded through a type accepting either shape, and neither — a null content leaves both fields zero rather than failing a line carrying no message body.

  • assistant, string form — the answer itself, so it surfaces as an EventKindText event rather than being dropped.
  • user, string form — the prompt echoed rather than a tool answering, so it yields the zero event, as an unmodelled line does.
  • block form — unchanged, with a test pinning it, since a decoder that accepted the string by giving up on blocks would lose every tool result.

How it was found

agtk memory curate died mid-run against v0.3.0. The store was left untouched, but the run was lost. Reproduced hermetically through agentictest.Fake, with no model call:

`{"type":"user","message":{"role":"user","content":"resuming"}}` + "\n" + resultEnvelope

Each new test was confirmed to fail before the change. Verified downstream by pointing agentic-toolkit at this branch with a replace directive: the failing case passes, and the directive was dropped again rather than committed.

Impact

claudecode is the only provider that can perform a real curation run, so on v0.3.0 this breaks agtk memory curate on the default provider whenever such a line appears.

…a list

The decoder modelled message.content as a list of content blocks. Claude Code
emits it as a bare string too — the same string-or-blocks polymorphism a tool
result's own content already has, one level up — and json.Unmarshal returned
an error for the shape it did not model.

Decode returns that error, so one such line failed the whole run. It reached
the caller as ErrProviderUnavailable: a stream that was working, reported as
the CLI being unreachable, which invites a retry that fails the same way.

Both shapes are accepted now, and neither is required: a null content leaves
the fields zero rather than failing a line carrying no message body. The
string form of an assistant message is the answer itself, so it surfaces as
text; the string form of a user message is the prompt echoed rather than a
tool answering, so it yields nothing, as an unmodelled line does.
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown

bulwark

  • scan — no findings

  • coverage — go: 82.1% (baseline 82.0%); go patch: 85.7% (12/14 new lines; baseline 82.0%)

📦 Full bulwark output — complete scan and coverage logs.

@pedromvgomes
pedromvgomes merged commit 7574adf into main Sep 7, 2026
9 checks passed
@pedromvgomes
pedromvgomes deleted the fix/decode-string-content branch September 7, 2026 07:16
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