forked from farhoodlabs/paperclip
Handle Gemini assistant message events in JSONL parser (#5143)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies, including
agents
> running the Gemini CLI (`gemini-local` adapter)
> - The Gemini CLI emits a JSONL event stream during a run that the
adapter
> parses to extract the assistant's response text, tool results, and
usage
> - Recent versions of the Gemini CLI emit assistant responses as
> `{ "type": "message", "role": "assistant", "content": ... }` events in
> addition to the previously-handled event shapes
> - The parser was not handling the new event type, so the assistant's
actual
> response text was being silently dropped from parsed output. Callers
ended
> up with empty assistant messages even when Gemini had successfully
> responded
> - This PR teaches the parser to recognize `{type: "message", role:
> "assistant"}` events and extract their content text via the same
> `collectMessageText` helper used for other message-shaped events
> - The benefit is that Gemini runs surface the assistant's real
response in
> downstream consumers (issue comments, run logs, downstream agent
context)
> instead of vanishing
## What Changed
- `packages/adapters/gemini-local/src/server/parse.ts`: in
`parseGeminiJsonl(...)`, add a branch for `event.type === "message"`
with
`role === "assistant"` that calls
`messages.push(...collectMessageText(event.content))`.
- `packages/adapters/gemini-local/src/server/parse.test.ts`: ~19 lines
of
coverage for the new branch.
## Verification
- `pnpm --filter @paperclipai/adapter-gemini-local test -- parse`
- Manual QA: run a Gemini agent on an issue, confirm the assistant's
response
appears as the issue comment / run output. Before this fix the comment
was
empty even when the run completed successfully.
## Risks
- Tightly scoped: 8 lines of production code in one parser branch. No
effect
on existing event shapes or other adapters.
- If the Gemini CLI changes its event schema again, this branch may need
to be
revisited — but adding it is strictly additive over current behaviour.
## Model Used
- OpenAI GPT-5.4 (reasoning effort: high) via Codex CLI
- Provider: OpenAI
- Used to author the code changes in this PR
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots — N/A
- [ ] I have updated relevant documentation to reflect my changes — N/A
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parseGeminiJsonl } from "./parse.js";
|
||||
|
||||
describe("parseGeminiJsonl", () => {
|
||||
it("collects assistant text from message events with string content", () => {
|
||||
const stdout = [
|
||||
'{"type":"init","session_id":"session-1"}',
|
||||
'{"type":"message","role":"user","content":"Respond with hello."}',
|
||||
'{"type":"message","role":"assistant","content":"hello","delta":true}',
|
||||
'{"type":"result","status":"success"}',
|
||||
].join("\n");
|
||||
|
||||
const parsed = parseGeminiJsonl(stdout);
|
||||
|
||||
expect(parsed.sessionId).toBe("session-1");
|
||||
expect(parsed.summary).toBe("hello");
|
||||
expect(parsed.errorMessage).toBeNull();
|
||||
});
|
||||
|
||||
it("collects assistant text from message events with structured object content", () => {
|
||||
const stdout = [
|
||||
'{"type":"init","session_id":"session-2"}',
|
||||
'{"type":"message","role":"assistant","content":{"content":[{"type":"text","text":"first part"},{"type":"text","text":"second part"}]}}',
|
||||
'{"type":"result","status":"success"}',
|
||||
].join("\n");
|
||||
|
||||
const parsed = parseGeminiJsonl(stdout);
|
||||
|
||||
expect(parsed.sessionId).toBe("session-2");
|
||||
expect(parsed.summary).toBe("first part\n\nsecond part");
|
||||
expect(parsed.errorMessage).toBeNull();
|
||||
});
|
||||
|
||||
it("ignores non-assistant message events", () => {
|
||||
const stdout = [
|
||||
'{"type":"message","role":"user","content":"hidden user input"}',
|
||||
'{"type":"message","role":"system","content":"hidden system note"}',
|
||||
'{"type":"message","role":"assistant","content":"visible response"}',
|
||||
'{"type":"result","status":"success"}',
|
||||
].join("\n");
|
||||
|
||||
const parsed = parseGeminiJsonl(stdout);
|
||||
|
||||
expect(parsed.summary).toBe("visible response");
|
||||
});
|
||||
});
|
||||
@@ -121,6 +121,19 @@ export function parseGeminiJsonl(stdout: string) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type === "message") {
|
||||
const role = asString(event.role, "").trim().toLowerCase();
|
||||
if (role === "assistant") {
|
||||
// Mirror the assistant-event handling above: collect every assistant
|
||||
// message including deltas. Gemini CLI emits these as discrete final
|
||||
// messages (one per assistant turn), not as cumulative streaming
|
||||
// tokens, so collecting all of them produces the expected concatenated
|
||||
// turn-by-turn summary rather than duplicated text.
|
||||
messages.push(...collectMessageText(event.content));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type === "result") {
|
||||
resultEvent = event;
|
||||
accumulateUsage(usage, event.usage ?? event.usageMetadata);
|
||||
|
||||
Reference in New Issue
Block a user