Skip to content

fix: close remaining unified message protocol gaps#105

Merged
teng-lin merged 7 commits intomainfrom
fix/close-protocol-gaps-v2
Feb 21, 2026
Merged

fix: close remaining unified message protocol gaps#105
teng-lin merged 7 commits intomainfrom
fix/close-protocol-gaps-v2

Conversation

@teng-lin
Copy link
Copy Markdown
Owner

Summary

Closes 4 remaining open issues from the unified message protocol audit (Section 8):

  • ISSUE 1 — Claude adapter dropped image/code/refusal content blocks to empty text. Extended ContentBlock union with 3 new variants and added switch cases in translateAssistant() to pass them through instead of falling to the default drop path.

  • ISSUE 2 — Metadata key divergence across adapters. Added canonical model key to OpenCode adapter (alongside existing model_id) so the consumer mapper resolves it. Added canonical tool_name key to Codex adapter's function_call branches (both translateItemAdded and translateItemDone).

  • ISSUE 3 — Status "running" inference is Claude-specific. Expanded the comment in handleStreamEvent() to document why generalizing it was rejected (false positives from sub-agent streams). No code change.

  • ISSUE 4 — Consumer mapper tests were missing for 4 of 7 content types. Added tests for thinking, code, image (with flattened source verification), and refusal blocks. All pass immediately — the mapper already implemented these.

Additionally cleaned up an unnecessary type cast in the thinking block handler now that TypeScript narrows correctly with the extended ContentBlock union.

Files changed (9 files, +223/-4)

File Change
src/types/cli-messages.ts Add image/code/refusal to ContentBlock union
src/adapters/claude/message-translator.ts Add 3 switch cases + remove stale cast
src/adapters/claude/message-translator.test.ts Add 6 tests (image/code/refusal/unknown + thinking split)
src/adapters/opencode/opencode-message-translator.ts Add canonical model key
src/adapters/opencode/opencode-message-translator.test.ts Assert model key
src/adapters/codex/codex-message-translator.ts Add tool_name to 2 function_call metadata objects
src/adapters/codex/codex-message-translator.test.ts Assert tool_name in both paths
src/core/consumer-message-mapper.test.ts Add 4 content type tests
src/core/unified-message-router.ts Expand status inference comment

Test plan

  • npx tsc --noEmit — clean
  • npx vitest run — all 2441 tests pass (158 files)
  • Pre-commit hooks (biome) pass on all commits
  • CI passes

🤖 Generated with Claude Code

Add tests for thinking, code, image, and refusal content blocks
in mapAssistantMessage, verifying the mapper already handles all
7 content types correctly (ISSUE 4).
… adapter

Add image, code, and refusal variants to the ContentBlock union type.
Handle these content types in the Claude adapter's translateAssistant()
switch instead of dropping them to empty text (ISSUE 1).

Add tests for passthrough of all three new types plus verification
that truly unknown block types are still dropped with metadata tracking.
Add canonical 'model' key to OpenCode adapter's assistant metadata
so the consumer mapper can resolve it (ISSUE 2).

Add canonical 'tool_name' key to Codex adapter's function_call
metadata in both translateItemAdded and translateItemDone (ISSUE 2).
Expand the comment in handleStreamEvent() to explain why "running"
status inference is Claude-specific and why generalizing it was
rejected (ISSUE 3).
…erage

Remove redundant `(block as { budget_tokens?: number })` cast in the
thinking case — TypeScript now narrows correctly after the ContentBlock
union was extended.

Split the thinking test into two: one verifying budget_tokens is
forwarded when present, and one asserting undefined when absent.
Rename test description to match the naming convention of the other
content type tests.
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @teng-lin, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request refines the unified message protocol by addressing several identified gaps. It ensures proper handling and consistent metadata for various content types across different adapters, improving the robustness and interoperability of message translation. The changes primarily focus on enhancing content block processing, standardizing metadata keys, and clarifying status inference logic, all while maintaining existing functionality and adding comprehensive test coverage.

Highlights

  • Claude Adapter Content Blocks: Extended the ContentBlock union with image, code, and refusal variants, and updated the Claude adapter to correctly pass these content blocks through instead of dropping them.
  • Metadata Key Harmonization: Added a canonical model key to the OpenCode adapter and a tool_name key to the Codex adapter's function_call branches to ensure consistent metadata across adapters.
  • Status Inference Documentation: Expanded comments in the UnifiedMessageRouter to clarify why 'running' status inference remains Claude-specific, addressing potential false positives from sub-agent streams.
  • Consumer Mapper Test Coverage: Added new tests for thinking, code, image, and refusal content types in the consumer mapper, confirming existing functionality for these blocks.
  • Type Cast Cleanup: Removed an unnecessary type cast in the Claude adapter's thinking block handler, leveraging improved TypeScript narrowing.
Changelog
  • src/adapters/claude/message-translator.test.ts
    • Updated the thinking block test to include budget_tokens.
    • Added a test case for thinking blocks where budget_tokens is undefined.
    • Added tests to verify that image, code, and refusal content blocks are passed through correctly.
    • Added a test to confirm that truly unknown content blocks are dropped with metadata tracking.
  • src/adapters/claude/message-translator.ts
    • Added case statements to translateAssistant to handle image, code, and refusal content blocks.
    • Removed an unnecessary type cast for budget_tokens in the thinking block handler.
  • src/adapters/codex/codex-message-translator.test.ts
    • Added assertions for the tool_name key in tests for translateItemAdded and translateItemDone.
  • src/adapters/codex/codex-message-translator.ts
    • Added the tool_name key to the metadata objects in both translateItemAdded and translateItemDone functions.
  • src/adapters/opencode/opencode-message-translator.test.ts
    • Added an assertion to verify the presence and value of the model key in translated messages.
  • src/adapters/opencode/opencode-message-translator.ts
    • Added a canonical model key to the metadata in translateMessageUpdated.
  • src/core/consumer-message-mapper.test.ts
    • Added new tests for mapping thinking, code, image (with flattened source), and refusal content blocks.
  • src/core/unified-message-router.ts
    • Expanded comments in handleStreamEvent to document the Claude-specific nature of 'running' status inference and the rationale for not generalizing it.
  • src/types/cli-messages.ts
    • Added image, code, and refusal types to the ContentBlock union.
Activity
  • The author, teng-lin, created this pull request to address four remaining issues from a unified message protocol audit.
  • The pull request includes updates to message translators for Claude, OpenCode, and Codex adapters.
  • New test cases were added for content block handling and metadata consistency.
  • Documentation was expanded to clarify specific protocol behaviors.
  • The author has confirmed that npx tsc --noEmit runs clean, npx vitest run passes all 2441 tests, and pre-commit hooks pass.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request does an excellent job of closing gaps in the unified message protocol. The changes are well-reasoned and address several inconsistencies across different adapters, such as handling new content block types (image, code, refusal) and standardizing metadata keys like model and tool_name. The addition of comprehensive tests for the new functionality and for previously untested parts of the consumer message mapper significantly improves the robustness of the codebase. The documentation update in unified-message-router.ts is also a valuable clarification for future development. Additionally, consider extracting repeated logic blocks into a private helper method to improve maintainability and avoid code duplication, especially in tests.

Comment on lines +212 to +319
it("maps thinking content blocks", () => {
const msg = createUnifiedMessage({
type: "assistant",
role: "assistant",
content: [{ type: "thinking", thinking: "Let me analyze...", budget_tokens: 5000 }],
metadata: {
message_id: "msg-007",
model: "claude-sonnet-4-5-20250929",
stop_reason: null,
usage: {
input_tokens: 0,
output_tokens: 0,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
},
parent_tool_use_id: null,
},
});

const result = mapAssistantMessage(msg);
const assistant = result as Extract<typeof result, { type: "assistant" }>;
expect(assistant.message.content).toEqual([
{ type: "thinking", thinking: "Let me analyze...", budget_tokens: 5000 },
]);
});

it("maps code content blocks", () => {
const msg = createUnifiedMessage({
type: "assistant",
role: "assistant",
content: [{ type: "code", language: "typescript", code: "const x = 1;" }],
metadata: {
message_id: "msg-008",
model: "claude-sonnet-4-5-20250929",
stop_reason: null,
usage: {
input_tokens: 0,
output_tokens: 0,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
},
parent_tool_use_id: null,
},
});

const result = mapAssistantMessage(msg);
const assistant = result as Extract<typeof result, { type: "assistant" }>;
expect(assistant.message.content).toEqual([
{ type: "code", language: "typescript", code: "const x = 1;" },
]);
});

it("maps image content blocks with flattened source", () => {
const msg = createUnifiedMessage({
type: "assistant",
role: "assistant",
content: [
{
type: "image",
source: { type: "base64", media_type: "image/png", data: "iVBOR..." },
},
],
metadata: {
message_id: "msg-009",
model: "claude-sonnet-4-5-20250929",
stop_reason: null,
usage: {
input_tokens: 0,
output_tokens: 0,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
},
parent_tool_use_id: null,
},
});

const result = mapAssistantMessage(msg);
const assistant = result as Extract<typeof result, { type: "assistant" }>;
expect(assistant.message.content).toEqual([
{ type: "image", media_type: "image/png", data: "iVBOR..." },
]);
});

it("maps refusal content blocks", () => {
const msg = createUnifiedMessage({
type: "assistant",
role: "assistant",
content: [{ type: "refusal", refusal: "I cannot assist with that." }],
metadata: {
message_id: "msg-010",
model: "claude-sonnet-4-5-20250929",
stop_reason: null,
usage: {
input_tokens: 0,
output_tokens: 0,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
},
parent_tool_use_id: null,
},
});

const result = mapAssistantMessage(msg);
const assistant = result as Extract<typeof result, { type: "assistant" }>;
expect(assistant.message.content).toEqual([
{ type: "refusal", refusal: "I cannot assist with that." },
]);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

These new tests for different content blocks are great additions. However, there's a significant amount of duplication in creating the test UnifiedMessage objects, specifically the metadata block which is repeated across four new tests.

To improve maintainability and reduce boilerplate, consider creating a helper function to generate these test messages. This would make the tests cleaner, easier to read, and simpler to update in the future.

For example:

function createTestAssistantMessage(content: UnifiedContent[], messageId: string) {
  return createUnifiedMessage({
    type: "assistant",
    role: "assistant",
    content,
    metadata: {
      message_id: messageId,
      model: "claude-sonnet-4-5-20250929",
      stop_reason: null,
      usage: {
        input_tokens: 0,
        output_tokens: 0,
        cache_creation_input_tokens: 0,
        cache_read_input_tokens: 0,
      },
      parent_tool_use_id: null,
    },
  });
}

// Then in your test:
it("maps thinking content blocks", () => {
  const msg = createTestAssistantMessage(
    [{ type: "thinking", thinking: "Let me analyze...", budget_tokens: 5000 }],
    "msg-007"
  );
  const result = mapAssistantMessage(msg);
  const assistant = result as Extract<typeof result, { type: "assistant" }>;
  expect(assistant.message.content).toEqual([
    { type: "thinking", thinking: "Let me analyze...", budget_tokens: 5000 },
  ]);
});
References
  1. To improve maintainability and avoid code duplication, extract repeated logic blocks into a private helper method.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit 34600bb: extracted makeAssistantMsg(content, messageId) helper to eliminate repeated metadata boilerplate across the 4 new content type tests. Net -40 lines.

teng-lin and others added 2 commits February 21, 2026 13:21
… content types

Exercises the full pipeline (CLIMessage → translate → router → consumer)
for newly supported content block types. Verifies image source flattening,
code block passthrough, refusal passthrough, and mixed content handling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address review feedback from gemini-code-assist on PR #105.
@teng-lin teng-lin merged commit 58b8d22 into main Feb 21, 2026
7 checks passed
@teng-lin teng-lin deleted the fix/close-protocol-gaps-v2 branch February 21, 2026 19:54
teng-lin added a commit that referenced this pull request Feb 21, 2026
Address the broader Gemini review feedback from PR #105 by converting
pre-existing mapAssistantMessage tests to use the makeAssistantMsg
helper, adding optional metadata overrides support. Net -49 lines.
teng-lin added a commit that referenced this pull request Feb 21, 2026
Address the broader Gemini review feedback from PR #105 by converting
pre-existing mapAssistantMessage tests to use the makeAssistantMsg
helper, adding optional metadata overrides support. Net -49 lines.
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