fix: Gemini system messages, tool result formatting, and argument key symbolization#6
Merged
Merged
Conversation
…olize tool arguments Three fixes: 1. Gemini build_request_body: Extract role:system messages from the messages array and send them via the `systemInstruction` field instead of as `contents` entries. Gemini API rejects system messages in the contents array with HTTP 400. 2. Gemini format_message: Convert role:tool messages to Gemini's `functionResponse` format instead of passing them as plain text with an invalid 'tool' role. When the agent loop adds tool results to state and calls the LLM again, the messages must use Gemini's native function response format. 3. Executor deep_symbolize_keys: LLM providers return tool arguments as JSON with string keys, but Ruby tool implementations typically access args with symbol keys (args[:field]). Recursively symbolize argument keys before passing to tool blocks. All three issues cause the agent loop to fail with HTTP 400 when tools are involved, triggering the Fallback provider to retry with Anthropic (which may also fail due to mismatched message formats). Includes specs for all three fixes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Three issues in ruby-pi cause the agent loop to fail with HTTP 400 errors when tools are involved, which is the root cause of the ongoing olli-social avatar chat failures (see ejwhite7/olli-social-app PR #78 discussion).
Issue 1: Gemini rejects system messages in
contentsThe
Loop#build_llm_messagesprepends a{ role: :system }message. Gemini's API does not acceptsystemas a role in thecontentsarray — it requires system instructions to be sent via the dedicatedsystemInstructionfield. This causes an immediate HTTP 400 on every Gemini call when a system prompt is set.Issue 2: Gemini rejects
role: "tool"incontentsWhen the agent loop executes tool calls, it adds results as
role: :toolmessages to state. On the nextthink()call,format_messagemaps these to{ role: "tool", parts: [{ text: ... }] }— but Gemini only acceptsuserandmodelroles. Tool results must be sent asfunctionResponseparts. This causes HTTP 400 on the second LLM call in any tool-using conversation.Issue 3: String-keyed arguments break tool implementations
LLM providers return tool arguments as JSON with string keys (
{"query" => "..."}) but Ruby tool implementations expect symbol keys (args[:query]). This causes tools to silently receivenilfor all parameters, leading to incorrect results or errors.Fix
lib/ruby_pi/llm/gemini.rbbuild_request_body: Extractsrole: :systemmessages from the messages array and setsbody[:systemInstruction]with their content. Only non-system messages are included incontents.format_message: Detectsrole: "tool"messages and converts them to Gemini'sfunctionResponseformat ({ role: "user", parts: [{ functionResponse: { name: ..., response: { result: ... } } }] }). Falls back to plain text withrole: "user"when tool name is missing.lib/ruby_pi/tools/executor.rbexecute_single: Callsdeep_symbolize_keyson arguments before passing them to the tool block.deep_symbolize_keys(new private method): Recursively converts all string keys to symbols in hashes, including nested hashes and arrays.Tests
Added specs for all three fixes:
systemInstructionis set and system messages excluded fromcontentsfunctionResponseAll 334 specs pass.