AGENTS.md@packages/ai/src/tool-call-middleware · git:20260807.133a795 · 2026-08-07 · sha256 b086e41278ba2e66

AGENTS.md@packages/ai/src/tool-call-middleware git:20260807.133a795A

Immutable. This exact content is served forever at /api/v1/blob/b086e41278ba2e66.

# packages/ai/src/tool-call-middleware

Text-format tool-call protocols for providers that don't support native function calling. Wraps `openai-completions` streams and parses `<tool_call>` / XML / delimiter formats back into pi's canonical `toolCall` events. Fork-modified — see `changes.md`.

Generated: 2026-08-07. Commit `4f26b8282`.

## FILES

```
tool-call-middleware/
├── index.ts                    # Public exports — wrap()/registry/types
├── types.ts                    # Protocol interface (parse, format, stream)
├── context-transformer.ts      # System-prompt + message rewriting per protocol
├── stream-wrapper.ts           # Wraps AssistantMessageEventStream → re-parses chunks
├── stream-wrapper-shared.ts    # Shared wrapper plumbing (stream-wrapper + recovery wrapper)
├── stream-message-metadata.ts  # Message metadata carried through wrapped streams
├── stream-thinking-projection.ts # Thinking-block projection for wrapped streams
├── recovery-*.ts               # Leaked-invoke recovery subsystem; entry: wrapStreamWithInvokeRecovery
│                               #   (recovery-stream-wrapper), plus recovery-code-mask,
│                               #   recovery-content-lifecycle, recovery-diagnostics,
│                               #   recovery-event-stream, recovery-message-snapshot,
│                               #   recovery-native-projection, recovery-stream-failure,
│                               #   recovery-stream-terminal, recovery-text-projection
├── protocols/
│   ├── hermes.ts               # Hermes <tool_call>{json}</tool_call> (Qwen, Mistral fine-tunes)
│   ├── morph-xml.ts            # <fn><arg>val</arg></fn> XML (canonical "morph-xml"; "xml" deprecated alias)
│   ├── yaml-xml.ts             # YAML body inside XML tags
│   ├── gemma4.ts               # Gemma 4 delimiter format `<|tool_call>call:name{…}<tool_call|>`
│   ├── json-mix.ts             # Shared JSON-mix helper (Hermes + delimited variants)
│   ├── xml-tool-tag-scanner.ts # Streaming XML tag boundary scanner
│   ├── kimi-xtml/              # Kimi K3 XTML channel format (format/parse/stream/markers,
│   │                           #   recovery-stream.ts, thinking-recovery*.ts)
│   ├── anthropic-xml/          # Legacy <invoke>/<parameter> parser, coercion, scanner, formatter, stream parser
│   └── antml/                  # ANTML <function_calls>/<invoke> protocol with Claude-Code-style failure tolerance
├── TESTING.md                  # Manual test commands per protocol (OpenRouter live API)
└── changes.md                  # Fork tracker: morph-xml strict mode, yaml+xml, stream error preservation
```

## WHERE TO LOOK

| Task | File |
|------|------|
| Add a new text-tool protocol | `protocols/<name>.ts` + register in `index.ts` + extend the `ToolCallFormat` union/whitelist (see ADD A PROTOCOL step 4) |
| Fix a parser bug | `protocols/<name>.ts` (parse step) |
| Fix streaming partial-arg bug | `protocols/<name>.ts` (stream step) + `stream-wrapper.ts` |
| System-prompt format for tools | `context-transformer.ts` (per-protocol prompt injector) |
| Cross-provider stream error fallback | `stream-wrapper.ts` — preserves reconstructed outer text+toolCalls on transport error |

## ADD A PROTOCOL (5 steps)

1. Implement `Protocol` interface in `protocols/<name>.ts` or `protocols/<name>/index.ts` plus focused helpers (parse, format, stream).
2. Add system-prompt rendering for tools to `context-transformer.ts`.
3. Export from `index.ts` and register in the protocol registry.
4. Add "<name>" to the ToolCallFormat union in types.ts (this dir) and to the literal whitelist in getToolCallFormat() in index.ts. models.json accepts toolCallFormat as a free string (packages/coding-agent/src/core/model-config.ts:104, Type.String() by design), so the getToolCallFormat() whitelist is the ONLY enforcement point — a format missing there silently deactivates the middleware.
5. Add manual test command to `TESTING.md` and an automated test under `packages/ai/test/tool-call-middleware/<name>*.test.ts`.

## CONVENTIONS

- **Stream-error preservation** (2026-04-11): when a provider stream errors AFTER complete tool-call blocks were reconstructed, finish the turn as `toolUse` so the agent still executes those tools. Do NOT fall back to the raw provider message.
- **Strict parsing**: reject malformed XML/JSON instead of coercing into invalid strings (precedent: `morph-xml` array<object> handling).
- **Delegate to `json-mix.ts`** for any new JSON-inside-delimiters protocol — minimizes drift across Hermes-family parsers.
- **`xml-tool-tag-scanner.ts`** is the canonical streaming boundary detector; reuse it for any XML-tag-based protocol.

## ANTI-PATTERNS

- Coercing malformed input into a "best-effort" string — produces invalid downstream `tool_call.arguments`. Reject instead.
- Duplicating Hermes-style parsing in a new file — extract a shared helper in `json-mix.ts`.
- Forgetting to inject the protocol-specific system-prompt block in `context-transformer.ts` — model never emits tool calls.
- Writing tests against live OpenRouter without `describe.skipIf(!process.env.OPENROUTER_API_KEY)` gating.

## NOTES

- `TESTING.md` documents the canonical live-API test commands per protocol (Qwen for Hermes, Gemini for MorphXML, Gemma 4 for delimiter, and Anthropic XML). Update it when adding new protocols.
- This package's middleware is fork-modified — see `changes.md` for the architectural rewrite toward `minpeter/ai-sdk-tool-call-middleware` style.
- `compat.toolCallFormat` on a custom model in `~/.senpi/agent/models.json` is what activates middleware for that model.