Skip to content

refactor: rewrite tool arg translation to handle SSE chunk boundaries#8

Merged
dotCipher merged 2 commits intodotCipher:mainfrom
BNasraoui:fix/agent-subagent-type-and-question-mapping
Apr 16, 2026
Merged

refactor: rewrite tool arg translation to handle SSE chunk boundaries#8
dotCipher merged 2 commits intodotCipher:mainfrom
BNasraoui:fix/agent-subagent-type-and-question-mapping

Conversation

@BNasraoui
Copy link
Copy Markdown
Contributor

Summary

Fixes #7 — three tool mapping bugs:

  • Agent subagent_type now required in schema. OpenCode's task tool validates it as required; omitting it caused invalid_type errors.
  • general-purposegeneral (was build). OpenCode has a dedicated general subagent per official docs. Also adds generalgeneral-purpose to outbound map.
  • AskUserQuestionquestion bidirectional name mapping added. Previously stub-only, so the model could never ask users clarifying questions.

Changes

  • src/claude-tools.ts:217: required: ["description", "prompt", "subagent_type"]
  • src/index.ts:592-597: Add general: "general-purpose" to outbound agent map
  • src/index.ts:749: "general-purpose": "general" in inbound agent map
  • src/index.ts:91-92: question/mcp_questionAskUserQuestion outbound
  • src/index.ts:106: AskUserQuestionquestion inbound
  • Updated stale comment about OpenCode agent types

Test plan

  • npm run build — clean
  • npm test — 12/12 pass
  • Verified subagent_type: "explore" resolves correctly via opencode run
  • Verified subagent_type: "plan" resolves correctly via opencode run
  • Verified subagent_type: "build" resolves correctly via opencode run
  • Verify AskUserQuestionquestion mapping works end-to-end

@BNasraoui BNasraoui marked this pull request as draft April 10, 2026 06:18
@dotCipher
Copy link
Copy Markdown
Owner

@BNasraoui I'll take a look at this when you complete the AskUserQuestion mapping, let me know

This PR was previously based on pre-v1.10.2 main. Upstream has since
landed PRs dotCipher#9 (OAuth), dotCipher#10 (Windows compat), dotCipher#11 (shared tool mappings),
and dotCipher#12 (CI). Rebased onto current main and reworked our improvements
to layer cleanly on top of upstream's tool-mapping work.

## What changed from upstream v1.10.2

Upstream dotCipher#11 uses regex-on-raw-bytes to translate tool arguments as
they stream in. That works when the whole JSON fits in one chunk, but
fails silently when Anthropic splits a tool argument across a TCP
boundary — e.g. "file_" in one chunk and "path" in the next. The regex
never matches a split key, so the consumer receives untranslated
snake_case and produces tool validation errors.

This PR replaces that approach with three structural changes:

1. **SSE event framing (src/stream.ts).** New module with a stateful
   processor that parses SSE frames on \n\n boundaries, buffers
   input_json_delta fragments per content-block index, and emits a
   single translated input_json_delta at content_block_stop.
   Chunk-boundary corruption becomes structurally impossible. Uses
   start() + async loop instead of pull() — Bun's ReadableStream
   doesn't reliably re-invoke pull() when the handler resolves
   without enqueuing (which happens while we're buffering deltas).

2. **JSON.parse for tool arg translation (src/claude-tools.ts).**
   translateToolArgsJsonString parses the assembled JSON, walks the
   object, and serializes back. Key renames on parsed objects can't
   corrupt string values that happen to contain key names (e.g. a
   TodoWrite item whose content literally says "activeForm", or a Bash
   command with "file_path=" inside a heredoc). Replaces every regex
   substitution from dotCipher#11.

3. **Consolidated outbound translation (translateArgsOpencodeToClaude).**
   The inbound path is now a single function; the outbound path in
   index.ts body transform was a wall of nested if-blocks. Extracted
   to a sibling function so inbound/outbound stay in lockstep — next
   bug fix touches one place.

## What was carried forward from dotCipher#11

All of upstream's tool-mapping fixes are preserved:

- Agent: subagent_type required; general-purpose ↔ general; strip
  model/run_in_background/isolation inbound; strip task_id/command
  outbound; default subagent_type=general inbound
- AskUserQuestion: bidirectional multiSelect ↔ multiple per question
- Skill: skill ↔ name; strip args inbound
- WebFetch: strip prompt inbound, inject default format=markdown;
  synthesize prompt from format outbound, strip timeout
- Bash: strip run_in_background/dangerouslyDisableSandbox inbound
- Read: strip pages inbound
- Grep: strip output_mode/-B/-A/-C/context/-n/-i/type/head_limit/
  offset/multiline inbound
- TodoWrite: bidirectional activeForm ↔ priority; cancelled → completed
  outbound

## Other changes

- context_management and output_config.effort only injected for
  thinking-capable models (opus, sonnet-4-6). Previously sent to
  haiku too, which 400s.
- Silent try/catch {} blocks now log via debugLog() gated by
  OPENCODE_CLAUDE_BRIDGE_DEBUG=1. Diagnosable without source changes.
- flush() logs a debug warning if a tool_use block is abandoned
  mid-stream (upstream disconnect between start and stop).

## Tests

81 unit tests (up from 11). Test file imports actual production
modules (createSseProcessor from ./stream, translateToolArgsJsonString
and translateArgsOpencodeToClaude from ./claude-tools) — not local
reimplementations. Assertions parse SSE events and use assert.deepEqual
on object shape rather than substring matches. Covers:

- Every INBOUND_TOOL_NAME_MAP entry (name mapping)
- Every tool's argument translation (including the corruption cases)
- Chunk boundary splits (fragmented args, events across chunks)
- Interleaved tool_use blocks (per-block state isolation)
- Error paths (malformed JSON, translator throws, abandoned blocks)
- Byte-exact pass-through for ping/message_start/text_delta
- Round-trip symmetry between inbound/outbound translation
- All inbound field stripping (Agent/Bash/Read/Grep/Skill)
- All outbound field handling (Agent/AskUserQuestion/WebFetch/TodoWrite)

## Verified end-to-end via opencode run

Read, Edit, Write, Grep, Glob, Bash, Agent, WebFetch, Skill — all
working. WebSearch remains a stub (not in OpenCode's AVAILABLE_TOOLS).
@BNasraoui BNasraoui force-pushed the fix/agent-subagent-type-and-question-mapping branch from f1e56e7 to dafe299 Compare April 15, 2026 04:35
@dotCipher dotCipher changed the title fix: Agent subagent_type required, correct type mapping, add AskUserQuestion refactor: rewrite tool arg translation to handle SSE chunk boundaries Apr 16, 2026
Strip the OPENCODE_CLAUDE_BRIDGE_DEBUG debug logger and revert the
catch sites it wired into back to silent catches. The SSE processor's
optional debug hook in stream.ts stays in place (used by tests) but is
no longer wired from index.ts.
@dotCipher
Copy link
Copy Markdown
Owner

Doing the manual test of the "Verify AskUserQuestion → question mapping works end-to-end" now, I'll look to resolve it once I get a test running if it's not resolved yet, then I'll merge this @BNasraoui

@dotCipher dotCipher merged commit adccd6d into dotCipher:main Apr 16, 2026
1 check passed
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.

Tool mapping bugs causing task failures and missing functionality

2 participants