AI record(): capture full per-turn interaction (messages + latency + usage + tool calls) - #199
Merged
Merged
Conversation
Fold in the pending ai-package-fix working-tree state so the example/agents suite runs green before extending the cassette format: fix the ToolCallView import, carry usage in the flat cassette, skip the not-yet-implemented log() tests, and regenerate the unit cassettes.
Agent.record() cassettes previously stored a flat, lossy shape keyed by the
hash of the user input ({content, tool_calls, usage}); a tool-calling turn even
dropped its tool_calls entirely, because the runner returned only the final
tool-result message.
Each recorded turn is now an ordered transcript keyed by the hash of the user
input, capturing the whole exchange:
[ human,
ai(tool_calls, uses, response_time),
tool_response(content_type, content, response_time),
ai(content, uses, response_time) ]
- AIMessage records token uses (input/output/cache/total), response_time, and
content; tool calls record the tool response and their own response_time.
- The plain Runner and the GraphRunner both capture the interaction while it
happens (preserving the model's requested tool_calls and per-call latency).
- Multi-turn is supported: each turn is a separate hash-keyed entry.
- Replay reconstructs the AgentResponse from the transcript; the reader still
accepts the legacy flat shape for backward compatibility.
Existing example/agents unit cassettes are migrated to the new format.
Contributor
Author
Code Reviewer verdict — REQUEST CHANGES (1 blocker: CI red)Logic is correct and well-tested; only a CI gate is failing. Blocker
Verified good
Approve on my end once the two files are formatted and CI is green. DO NOT MERGE. |
Drops example/agents/tests/features/test_chat_controller.py. Its stream/no-stream assertions duplicated the unit coverage, and its record-and-replay cases depended on a global-model-swap decorator that does not exist yet, so they were skipped.
The class is a predicate helper for assert_tool_called(); ToolCallAssert names its assertion role more clearly than the generic View.
Replace the HumanMessage/AIMessage/ToolCallMessage dataclasses with plain recording.human()/ai()/tool_response() builder functions. The persisted cassette JSON is unchanged (identical keys/values, and _save sorts keys).
tmgbedu
added a commit
that referenced
this pull request
Jul 25, 2026
CI runs 'ruff format --check .' repo-wide; these two files (carried in from #199) were unformatted and failed the check.
tmgbedu
added a commit
that referenced
this pull request
Jul 25, 2026
* feat(ai): add agent.assert_tokens() for accumulated token limits
agent.assert_tokens(lambda x: x.where('input', '<=', 5000).where('output', '<=', 5000))
Tokens accumulate across every prompt()/stream() in a session (summing each
ai message's token uses from the transcript, with a usage fallback), and the
fluent TokenQuery evaluates where-clauses over input/output/cache/total. Works
on both live-record and replay; to_response() now carries the transcript so
replayed turns expose their per-message uses.
* style(ai): ruff-format assert_tokens docstring
* style(ai): ruff-format runner.py and test_record_cassette_format.py
CI runs 'ruff format --check .' repo-wide; these two files (carried in from
#199) were unformatted and failed the check.
* test(orm): fix polymorphic relation tests to use the record relationship
The Like model's MorphTo relationship is named 'record'; the tests referenced
a nonexistent 'like.log' attribute, which returned None and failed to await /
assert. Aligns with the eager-load case that already uses with_('record').
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
Agent.record()cassettes previously persisted a flat, lossy shape keyed by the hash of the user input:{"<hash>": {"content": "...", "tool_calls": [], "usage": {"input": N, "output": N}}}A tool-calling turn even dropped its
tool_callsentirely — the plainRunnerreturned only the final tool-result message, whose.tool_callsis empty. This is why theexample/agentsrouter tests were red on the base.Each recorded turn is now an ordered transcript keyed by the hash of the user input, capturing the whole exchange:
{"<hash-of-user-input>": [ {"type": "human", "content": "suggest me jobs"}, {"type": "ai", "tool_calls": [...], "uses": {"input_token": N, "output_token": N, "cache_token": N, "total_token": N}, "response_time": 12.0}, {"type": "tool_response", "content_type": "json", "content": "[...]", "response_time": 5.0}, {"type": "ai", "content": "Here is a job...", "uses": {...}, "response_time": 8.0} ]}uses(input/output/cache/total),response_time(ms), and content.content_type(json/text), and its ownresponse_time.What changed
ai/recording.py(new): ordered message types (HumanMessage/AIMessage/ToolCallMessage),usage_metadata→usesmapping, andto_response()to reconstruct anAgentResponsefrom a transcript.ai/runner.py(Runner+ sharedBaseRunner): capture the interaction as it happens — preserve the model's requestedtool_calls, record each tool's response and per-call latency, and expose it viaAgentResponse.transcript/.tool_events. Fixes the dropped-tool_callsbug.ai/graph.py(GraphRunner): capture the full tool loop (ai → tool_response → ai) across the graph nodes.ai/response.py: addtool_eventsandtranscriptfields toAgentResponse.ai/testing.py(AgentRecordFake): write the new ordered-list cassette; reconstruct on replay. The hash-key inputs are unchanged, so existing keys stay valid. Backward compatible: the reader still accepts the legacy flat shape.example/agentsunit cassettes migrated to the new format (regenerated through the real record path with a faked model, so keys are identical).Testing
test_recording.py,test_runner_recording.py,test_graph_recording.py,test_record_cassette_format.py(incl. a backward-compat legacy-cassette test).example/agentssuite: 9 passed, 3 skipped — including the two previously-red router tests (test_the_router_agent,test_the_router_with_initial_messages).test_sqlite_polymorphicORM failures are pre-existing on the base — alike.log→like.recordtest bug — and are out of scope here.)Notes
ai-package-fixand targets it (notmain): therecord()/GraphAgentrefactor, the flat cassette format, and the example cassettes exist only on that branch. The first commit folds in the pendingai-package-fixworking-tree baseline so the suite runs green before the format change.