Skip to content

fix(trace): key the tool-part decoders by the harness names that arrive - #955

Merged
drewstone merged 1 commit into
mainfrom
fix/kimi-tool-part-decoder
Aug 21, 2026
Merged

fix(trace): key the tool-part decoders by the harness names that arrive#955
drewstone merged 1 commit into
mainfrom
fix/kimi-tool-part-decoder

Conversation

@drewstone

Copy link
Copy Markdown
Contributor

Problem

toolPartDecoders (src/runtime/supervise/trace-source.ts:151) is the harness → tool-call-decoder registry. It held an entry under kimi, and no caller can produce that name.

Every in-repo path to decodeToolPart runs through one field:

hop file:line type
the decode site supervise/sandbox-session.ts:154 decodeToolPart(part, args.harness)
the field supervise/sandbox-session.ts:100 readonly harness: BackendType
bound at supervise/runtime.ts:1277, runtime.ts:4815 spec.harness as BackendType
BackendType @tangle-network/sandbox Exclude<HarnessType, 'gemini'>
the harness kimi is served under @tangle-network/agent-interface harness.d.ts:23, and src/sandbox-backend.ts:31 kimi-code

The literal 'kimi' appears nowhere in src/, tests/, bench/ or docs/. In the real producer it is a config toggle name and a binary name, never a wire value: cli-bridge/src/server.ts:169 registers the backend with harness: 'kimi-code'.

The entry was also wrong about the wire. kimi-code streams both shapes on one session — an Anthropic tool_use content block (cli-bridge/src/backends/kimi.ts:281) and a top-level OpenAI tool_calls entry (:307). The registry named decodeOpenAiPart, which reads only the second. The module's own docstrings already knew: :112 says decodeAnthropicPart covers "kimi's tool_use variant" and :128 says decodeOpenAiPart covers "kimi's top-level form".

Three more keys were never harness names either: anthropic, openai, router.

Why nothing broke, and why that is the dangerous part

An unregistered harness falls through to the try-all loop (:168), which is a Set of the three distinct decoder functions — including the Anthropic one. So no kimi tool call is dropped today; the measured trace is correct. Verified before any edit:

--- kimi tool_use block ---
  harness="kimi"      -> undefined   <<< the dead key, if anything could reach it
  harness="kimi-code" -> {"toolName":"read","args":{"file":"a.ts"},"callId":"k-1"}   (try-all)
--- kimi top-level tool_call ---
  harness="kimi-code" -> {"toolName":"bash","args":{"cmd":"ls"},"callId":"call_1"}   (try-all)

The defect is the trap. The obvious repair — rename the key to kimi-code, keep the decoder the entry names — makes the specific adapter win and silences the tool_use half. Measured on that repair:

  tool_use, harness="kimi-code" -> undefined   <<< now DROPPED

Half of a kimi worker's tool calls would disappear with no error: the trace simply reports fewer calls, and every rate computed from it — repeated-action detection, tool-waste, error streaks — is wrong by an unknown amount. No test would have caught it: every kimi case in tests/kernel/trace-source.test.ts calls decodeToolPart(part) with no harness, so the kimi key was never exercised.

Change

  • decodeKimiPart reads both shapes, and kimi-code maps to it.
  • The registry is typed Partial<Record<HarnessType, ToolPartDecoder>>, so a key no caller can produce does not compile — the same guard src/runtime/sandbox-backend.ts:40 already uses for the sibling list. This is what makes the defect unrepeatable; a lint rule or a drift test would only report it.
  • anthropic, openai and router are removed. A part carrying any of those wire shapes decodes identically through the try-all path, so no behavior moves.
  • decodeToolPart's harness and sandboxSessionTraceSource's harness option narrow from string to HarnessType. A caller with a harness this package does not register omits the argument, which is what try-all is for.

Proof

pnpm run lint                 615 files, no fixes
pnpm run typecheck            clean
pnpm run build                clean
pnpm run check:api-surface    2120 exports / 17 entry points, record current
                              bench: 223 exports / 41 entry points, record current
pnpm run check:testing-fixture  fixtures are current
pnpm run check:version-bump   package.json: 0 manifest and 2 export change(s) needing a minor
                              bump, paid for by 0.154.0 -> 0.155.0 (minor)
pnpm run docs:check           exit 0
pnpm test                     2943 passed / 32 failed across 10 files
  Clean origin/main on this machine: 2797 passed / 171 failed across 21 files.
  Nine of the ten failing files are in that baseline set; the tenth,
  tests/kernel/workspace.test.ts, is a 20s git-worktree timeout in the same
  macOS class and touches nothing this change goes near. CI on Linux is the
  authority.

The version gate named exactly the two symbols this change moves and nothing else:

shape changed: ./kernel decodeToolPart: shape 3122064402c0 -> c252d715cf84
shape changed: ./kernel sandboxSessionTraceSource: shape 62f065aca3b3 -> 063155b911a2

That is #953 working on its first real change: before it, this pull request would have reported "consumer surface unchanged at 0.154.0" and shipped a narrowed public signature under a version the registry already holds.

Simplification

Simplification: four unreachable keys removed from a seven-key registry; the registry's key vocabulary now has one owner (HarnessType) instead of being a free Record<string, …> that no compiler checked.
Net: +14 / -8 lines in one source file, +18 in one test; 4 registry entries removed, 1 added.
Not done here: the other unregistered HarnessType members (nanoclaw, pi, prime, hermes, openclaw, amp, factory-droids, forge, cursor, acp, cli-base) keep using the try-all path. Registering one needs its real wire shape confirmed against the bridge backend that serves it, which is a per-harness measurement, not a list edit.

Tests: +1 (both kimi-code wire shapes decode when the harness is named — the case that fails the moment anyone binds kimi-code to a single decoder, which is exactly the repair the old entry invited). It is green before this change too, because kimi-code was not a key at all; what fails before this change is the compiler, on the kimi key itself. -0 deleted.

Refs #954

toolPartDecoders held an entry under `kimi`. Every in-repo caller reaches
decodeToolPart through SteerableSandboxSession.harness, which is BackendType,
and the harness kimi is served under is `kimi-code`, so no caller could select
that entry. It was also wrong about the wire: kimi-code streams an Anthropic
tool_use content block and a top-level OpenAI tool_calls entry on one session,
and the entry named the OpenAI decoder alone.

An unknown harness falls through to the try-all path, so no tool call was lost.
The defect was a trap: renaming the key to kimi-code while keeping the decoder
it named would have made the specific adapter win and dropped the tool_use half
in silence.

The registry is now typed against HarnessType, so a key no caller can produce
does not compile, and kimi-code maps to a decoder that reads both shapes. The
three keys that were never harness names are gone; those wire shapes decode
identically through the try-all path.

@tangletools tangletools left a comment

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.

✅ Auto-approved drewstone PR — 5d81251b

This PR was opened by the trusted drewstone account.

This approval is provisional and was applied by the local stand-in because the pr-reviewer webhook host is unreachable (2026-08-21). CI on this head is fully green. The full PR reviewer audit re-runs via the resweep when the service returns and will publish findings if it detects issues.

@drewstone
drewstone merged commit c8038d2 into main Aug 21, 2026
4 checks 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.

2 participants