fix(grpc): send raw goon bytes in formatted_text
Summary
GoonFormatter::format returns Value::String(raw_goon_bytes). The gRPC dispatch in service.rs#L312-L322 called formatted.to_string() for the LLM branch, which JSON-encodes the inner string a first time. Workhorse then JSON-encodes again when wrapping the response body, so the UI receives literal \n instead of newlines and the goon body becomes unreadable to the LLM.
Extract the inner string for the LLM path so the gRPC formatted_text field carries raw goon bytes. The RAW path is unchanged.
This change pairs with the matching Workhorse fix in gitlab-org/gitlab!235318 (merged) that drops the {result, query_type, raw_query_strings, row_count} envelope for format=llm. KG-only is not sufficient. Once KG sends raw goon bytes, json.RawMessage(...) in Workhorse emits invalid JSON (a body starting with @), and the actual production failure is a silent empty body, captured here:
orbit.SendQuery: write response: json: error calling MarshalJSON for type json.RawMessage: invalid character '@' looking for beginning of valueThe two MRs must merge together.
Relates to !1289 (merged), !1291 (merged), !904 (merged). Surfaced from the screenshot review described on !1289 (merged)#note_3331102607.
Test plan
-
cargo build --bin gkg-serverand end-to-end test through the GDK Duo Workflow chat path. With both fixes, the LLM tool result fororbit_query_graphis the raw goon body. Without the Workhorse fix, the body is empty and the LLM confabulates. - Existing
goon_formatter_e2eintegration tests pass; they exercise the formatter atValue::Stringlevel so they were not affected by the change. - Pipeline.
End-to-end verification (with paired Workhorse change)
Two Duo Chat sessions were run against the same query. The user message in both:
Call orbit_query_graph with traversal for User id=1.
Before (KG-only, Workhorse unchanged): the orbit_query_graph tool result content was the empty string. The LLM responded:
The query ran successfully but returned no output. This means the traversal for User id=1 produced an empty result set, which can happen for a few reasons: ...
That answer is fabricated. Workhorse silently dropped the response body because the goon bytes are not valid JSON.
After (KG + Workhorse): the tool result content was the raw goon body, and the LLM grounded its reply in the header:
The traversal query for User id=1 returned no results, 0 nodes and 0 edges were found.