Runtime: add WP_Agent_Tool_Pair_Validator#176
Merged
Conversation
Detects and prunes orphan tool_call / tool_result envelopes in a transcript.
Provider request shapes (Anthropic-style tool_use/tool_result, OpenAI-style
tool_calls + tool messages) require every tool call to be paired with a
result; an unpaired transcript is a 400 waiting to happen. This validator
gives consumers a substrate-level helper they can run before dispatch or
after compaction / archive / session-import operations that can leave
mid-cycle gaps.
Pairing is FIFO by payload.tool_name, mirroring how providers resolve
tool-use IDs positionally when multiple calls share a name. Pair-aware
boundary picking already lives in WP_Agent_Conversation_Compaction; this
class complements it by validating arbitrary message lists (the case that
boundary-picking can't cover, e.g. overflow archive of mid-cycle messages,
manual deletes, partial transcript restores).
API:
- validate( array $messages ): array -- orphan reports sorted by index
- is_paired( array $messages ): bool
- prune( array $messages ): array -- { messages, removed, events }
emits tool_pair_validated (clean) or tool_pair_pruned lifecycle events
in the same shape used by compaction.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
phpstan: $messages is typed array<int, array<string, mixed>>, so the per-element is_array() check always evaluates true. Remove it. phpcs: align double arrows on the tool_pair_pruned event metadata array. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
WP_Agent_Tool_Pair_Validator— a small runtime utility that detects and prunes orphantool_call/tool_resultenvelopes in a message list.Why
Every tool-aware provider request shape (Anthropic-style
tool_use/tool_resultblocks, OpenAI-styletool_calls+toolmessages) requires each call to be paired with its result. An unpaired transcript is a provider 400 waiting to happen, and there are several ways one can show up even on a substrate that "does the right thing" by default:WP_Agent_Conversation_Compactionkeeps the boundary safe, but a re-archive of older content can split pairs the first pass didn't).Pair-aware boundary picking already lives in
WP_Agent_Conversation_Compaction::move_boundary_to_safe_index(), but it only protects the cutoff. This class complements it by validating arbitrary message lists, which is the case boundary-picking can't cover.API
payload.tool_name, mirroring how providers resolve tool-use IDs positionally when several pending calls share a name.prune()emits atool_pair_validated(clean) ortool_pair_prunedlifecycle event in the same{ type, metadata }shape used by the compaction lifecycle, so consumers can forward both through a single event sink.Composition
preserve_tool_boundaries=truekeeps the cutoff between pairsprune()recovers a sendable transcriptis_paired()gates dispatch;prune()repairsprune()removes the dangling callTest plan
php tests/tool-pair-validator-smoke.php— new smoke covering empty, plain, paired, orphan-call, orphan-result, FIFO matching by name, crossed names, prune output + lifecycle events, clean-prune no-op. (33 assertions)composer test— full suite green; no regressions in existing compaction / loop / channels smokes.🤖 Generated with Claude Code