You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: toolResultContains match gate on the last tool-result payload (#299)
## Why
The showcase mastra interrupt demos use a native suspend tool where pick
and cancel resume with the SAME tool_call_id; the requests differ only
inside the tool-result message content (`{"chosen_time": ...}` vs
`{"cancelled": true}`). Every existing JSON-expressible gate is
identical between the two requests, so first-match-wins replays the pick
confirmation after the user cancelled. `predicate` cannot help: the JSON
fixture loader has no way to express it.
## What
New `match.toolResultContains: string` gate: passes when the request's
LAST message is a `role: "tool"` result whose text content contains the
substring. Mirrors the existing `toolCallId` last-message rule so the
two stay consistent.
- `src/router.ts`: gate placed with the other shape predicates, right
after `toolCallId`
- `src/types.ts`: `FixtureMatch` + `FixtureFileEntry.match`
- `src/fixture-loader.ts`: `entryToFixture` carry-through; validation
(string, non-empty — empty substring would silently act as a catch-all);
userMessage dedup key; catch-all discriminator set
- `packages/aimock-pytest`: `toolResultContains` added to
`_MATCH_LEVEL_OPT_KEYS` (0.5.0)
- docs: fixtures + multi-turn pages, README, write-fixtures skill
- tests: approve/cancel discrimination sharing one toolCallId; substring
present/absent; no-tool-message request never matches; stale tool result
followed by a new user turn never matches; array-of-parts content; null
content; loader validation + dedup + carry-through
## Version
1.37.0 (changelog entry added with this PR's number in a follow-up
commit on this branch).
---
### Maintainer note — additional fixes folded in during review
Beyond the `toolResultContains` gate, this branch now also carries (all
under the single 1.37.0 release):
- **fix:** completed the duplicate-userMessage dedup key so
legitimately-distinct fixtures no longer emit spurious shadow warnings
(kind-aware matcher serialization; `predicate` keyed by identity).
- **fix:** guarded `recordedTimings.interChunkDelaysMs` against
non-array values — a malformed entry no longer throws and silently drops
every fixture in its file.
- **fix:** bumped the `aimock-pytest` default server pin to 1.37.0 so
the auto-download path actually supports the new kwarg the client
forwards.
- **ci:** the `release` script now runs `format:check` + `test:exports`
before publish.
- **docs:** corrected the match-field / dedup / `toolCallId` semantics
docs, and fixed the write-fixtures E2E lifecycle to use
`resetMatchCounts()` instead of a full `reset()` between tests.
(These consolidate PR #300, now closed, into this one release. CHANGELOG
updated accordingly.)
Copy file name to clipboardExpand all lines: CHANGELOG.md
+12Lines changed: 12 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,5 +1,17 @@
1
1
# @copilotkit/aimock
2
2
3
+
## [1.37.0] - 2026-07-15
4
+
5
+
### Added
6
+
7
+
- New `match.toolResultContains` string gate: passes when the request's LAST message is a `role: "tool"` result whose text content contains the substring (same last-message rule as `toolCallId`, and composable with it). Discriminates fixtures whose requests differ only inside the tool-result payload — e.g. a human-in-the-loop suspend tool where approve and cancel resume with the same `tool_call_id` but different result JSON. JSON-expressible, so fixture files can finally split those legs without a programmatic `predicate`. Validated at load time (non-empty string — an empty substring would silently act as a catch-all), included in the duplicate-userMessage dedup key and the catch-all discriminator set, and routed through the `aimock-pytest` match-level kwarg keys (aimock-pytest 0.5.0) (#299)
8
+
9
+
### Fixed
10
+
11
+
- The duplicate-userMessage shadow check no longer warns for fixtures that share a `userMessage` but are legitimately disambiguated by a matcher the dedup key previously omitted (`systemMessage`, `model`, `toolName`, `toolCallId`, `inputText`, `responseFormat`, `endpoint`). The dedup key now covers every discriminator the router actually matches on and serialises RegExp / array matchers kind-aware so they don't collide; a `predicate`-bearing fixture is keyed by identity so it is never a false duplicate. This only suppresses spurious warnings — the check never dropped or deduped fixtures (#299)
12
+
- Bumped the `aimock-pytest` default server pin (`_version.py`) to 1.37.0 so the auto-download path (used when `AIMOCK_CLI_PATH` is unset) runs a server that supports the new `toolResultContains` match kwarg the client forwards (#299)
13
+
- Guarded `recordedTimings.interChunkDelaysMs` against non-array values: a fixture whose `interChunkDelaysMs` is malformed (null, missing, or not an array) no longer throws during load and silently drops every fixture in that file — the value is coerced to an empty array (#299)
Copy file name to clipboardExpand all lines: README.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -50,7 +50,7 @@ Run them all on one port with `npx @copilotkit/aimock --config aimock.json`, or
50
50
51
51
-**[Record & Replay](https://aimock.copilotkit.dev/record-replay)** — Proxy real APIs, save as fixtures, replay deterministically forever
52
52
-**Timing-aware recording and replay** — Recorded fixtures capture per-frame arrival timestamps; replay uses recorded timings for approximate timing reproduction based on recorded TTFT and inter-frame cadence (replay chunk count may differ from recording — TTFT and average pace are preserved, not per-token fidelity) with configurable `--replay-speed` multiplier
53
-
-**[Multi-turn Conversations](https://aimock.copilotkit.dev/multi-turn)** — Record and replay multi-turn traces with tool rounds; match distinct turns via `turnIndex`, `hasToolResult`, `toolCallId`, `sequenceIndex`, `systemMessage` (gate on host-supplied agent context), or custom predicates
53
+
-**[Multi-turn Conversations](https://aimock.copilotkit.dev/multi-turn)** — Record and replay multi-turn traces with tool rounds; match distinct turns via `turnIndex`, `hasToolResult`, `toolCallId`, `toolResultContains` (gate on the tool-result payload), `sequenceIndex`, `systemMessage` (gate on host-supplied agent context), or custom predicates
-**Multimedia APIs** — [image generation](https://aimock.copilotkit.dev/images) (DALL-E, Imagen), [image editing](https://aimock.copilotkit.dev/images) (/v1/images/edits), [text-to-speech](https://aimock.copilotkit.dev/speech) (OpenAI + ElevenLabs), [audio transcription](https://aimock.copilotkit.dev/transcription), [audio translation](https://aimock.copilotkit.dev/transcription) (/v1/audio/translations), [video generation](https://aimock.copilotkit.dev/video), [OpenRouter video generation](https://aimock.copilotkit.dev/openrouter-video) (/api/v1/videos with async job lifecycle), [Google Veo video generation](https://aimock.copilotkit.dev/veo-video) (:predictLongRunning + /v1beta/operations async lifecycle), [Grok Imagine video generation](https://aimock.copilotkit.dev/grok-video) (/v1/videos/generations with async job lifecycle), [fal.ai](https://aimock.copilotkit.dev/fal-ai) (image / video / audio with queue lifecycle)
56
56
-**[MCP](https://aimock.copilotkit.dev/mcp-mock) / [A2A](https://aimock.copilotkit.dev/a2a-mock) / [AG-UI](https://aimock.copilotkit.dev/agui-mock) / [Vector](https://aimock.copilotkit.dev/vector-mock)** — Mock every protocol your AI agents use
0 commit comments