reflex-agentkit 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/CHANGELOG.md +24 -1
  2. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/PKG-INFO +12 -6
  3. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/README.md +10 -5
  4. reflex_agentkit-0.2.0/changes/README.md +11 -0
  5. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/pyproject.toml +1 -0
  6. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/__init__.py +1 -1
  7. reflex_agentkit-0.2.0/src/reflex_agentkit/adapters/_edits.py +35 -0
  8. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/adapters/langgraph.py +19 -3
  9. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/adapters/pydantic_ai.py +62 -3
  10. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/core/commands.py +4 -3
  11. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/contract/test_pydantic_ai_facts.py +14 -0
  12. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/integration/test_langgraph_hitl.py +2 -2
  13. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/integration/test_pydantic_ai_hitl.py +46 -3
  14. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/test_smoke.py +1 -1
  15. reflex_agentkit-0.2.0/tests/unit/adapters/test_edits.py +44 -0
  16. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/adapters/test_langgraph_resume.py +33 -1
  17. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/core/test_commands.py +9 -0
  18. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/.gitignore +0 -0
  19. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/LICENSE +0 -0
  20. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/examples/_shared/README.md +0 -0
  21. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/examples/langgraph_dojo/README.md +0 -0
  22. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/examples/pydantic_ai_dojo/README.md +0 -0
  23. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/adapters/__init__.py +0 -0
  24. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/adapters/_require.py +0 -0
  25. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/components/__init__.py +0 -0
  26. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/components/approval.py +0 -0
  27. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/components/chat.py +0 -0
  28. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/components/composer.py +0 -0
  29. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/components/messages.py +0 -0
  30. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/components/shared_state.py +0 -0
  31. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/components/status.py +0 -0
  32. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/components/tools.py +0 -0
  33. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/core/__init__.py +0 -0
  34. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/core/_log.py +0 -0
  35. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/core/_patch.py +0 -0
  36. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/core/clients/__init__.py +0 -0
  37. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/core/clients/base.py +0 -0
  38. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/core/clients/http.py +0 -0
  39. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/core/clients/in_process.py +0 -0
  40. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/core/clients/sse.py +0 -0
  41. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/core/conversation.py +0 -0
  42. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/core/errors.py +0 -0
  43. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/core/models.py +0 -0
  44. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/core/reducer.py +0 -0
  45. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/core/run_input.py +0 -0
  46. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/core/tools.py +0 -0
  47. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/py.typed +0 -0
  48. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/state/__init__.py +0 -0
  49. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/state/_driver.py +0 -0
  50. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/state/_projection.py +0 -0
  51. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/state/_registry.py +0 -0
  52. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/src/reflex_agentkit/state/agent_state.py +0 -0
  53. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/bench/conftest.py +0 -0
  54. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/bench/test_bench_flush.py +0 -0
  55. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/bench/test_bench_reducer.py +0 -0
  56. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/conftest.py +0 -0
  57. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/contract/test_agui_protocol.py +0 -0
  58. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/contract/test_langgraph_facts.py +0 -0
  59. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/contract/test_reflex_facts.py +0 -0
  60. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/client_tool_marked.jsonl +0 -0
  61. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/custom_ring.jsonl +0 -0
  62. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/finish_cancelled_outcome.jsonl +0 -0
  63. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/finish_interrupt_generic_multi.jsonl +0 -0
  64. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/finish_interrupt_tool_call.jsonl +0 -0
  65. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/finish_mixed_interrupts.jsonl +0 -0
  66. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/finish_success.jsonl +0 -0
  67. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/finish_with_pending_client_tool.jsonl +0 -0
  68. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/hitl_approve.jsonl +0 -0
  69. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/hitl_approve_edited.jsonl +0 -0
  70. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/hitl_cancel_while_paused.jsonl +0 -0
  71. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/hitl_generic_queue.jsonl +0 -0
  72. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/hitl_reject.jsonl +0 -0
  73. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/ignored_families.jsonl +0 -0
  74. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/invalid_event.jsonl +0 -0
  75. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/lifecycle_happy.jsonl +0 -0
  76. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/messages_snapshot.jsonl +0 -0
  77. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/pydantic_ai_approval_recorded.jsonl +0 -0
  78. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/reasoning_encrypted.jsonl +0 -0
  79. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/reasoning_stream.jsonl +0 -0
  80. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/run_error.jsonl +0 -0
  81. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/stale_run_event.jsonl +0 -0
  82. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/state_delta_fails.jsonl +0 -0
  83. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/state_snapshot_delta.jsonl +0 -0
  84. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/steps.jsonl +0 -0
  85. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/text_chunks.jsonl +0 -0
  86. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/text_stream.jsonl +0 -0
  87. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/text_unclosed_at_finish.jsonl +0 -0
  88. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/text_unknown_id.jsonl +0 -0
  89. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/tool_call_args_too_large.jsonl +0 -0
  90. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/tool_call_chunks.jsonl +0 -0
  91. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/tool_call_happy_path.jsonl +0 -0
  92. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/tool_call_invalid_args.jsonl +0 -0
  93. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/tool_call_unknown_id.jsonl +0 -0
  94. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/fixtures/events/unknown_type.jsonl +0 -0
  95. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/helpers/__init__.py +0 -0
  96. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/helpers/dojo.py +0 -0
  97. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/helpers/fake_chat_model.py +0 -0
  98. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/helpers/fake_clients.py +0 -0
  99. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/helpers/fake_server.py +0 -0
  100. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/helpers/fake_state_access.py +0 -0
  101. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/helpers/fixtures.py +0 -0
  102. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/helpers/invariants.py +0 -0
  103. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/helpers/reflex_harness.py +0 -0
  104. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/helpers/strategies.py +0 -0
  105. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/helpers/ui_state.py +0 -0
  106. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/integration/conftest.py +0 -0
  107. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/integration/test_concurrent_clients.py +0 -0
  108. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/integration/test_fake_server.py +0 -0
  109. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/integration/test_http_client.py +0 -0
  110. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/integration/test_multiworker_cancel.py +0 -0
  111. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/integration/test_responsiveness.py +0 -0
  112. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/integration/test_secret_hygiene.py +0 -0
  113. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/integration/test_security_vectors.py +0 -0
  114. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/smoke/conftest.py +0 -0
  115. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/smoke/test_dojo_langgraph.py +0 -0
  116. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/smoke/test_dojo_pydantic_ai.py +0 -0
  117. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/smoke/test_quickstart.py +0 -0
  118. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/adapters/test_langgraph_legacy.py +0 -0
  119. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/adapters/test_langgraph_unit.py +0 -0
  120. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/adapters/test_pydantic_ai_unit.py +0 -0
  121. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/adapters/test_require.py +0 -0
  122. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/components/test_approval.py +0 -0
  123. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/components/test_chat.py +0 -0
  124. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/components/test_composer.py +0 -0
  125. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/components/test_messages.py +0 -0
  126. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/components/test_shared_state.py +0 -0
  127. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/components/test_status.py +0 -0
  128. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/components/test_tool_card.py +0 -0
  129. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/core/clients/test_base.py +0 -0
  130. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/core/clients/test_in_process.py +0 -0
  131. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/core/clients/test_sse.py +0 -0
  132. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/core/test_conversation.py +0 -0
  133. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/core/test_errors.py +0 -0
  134. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/core/test_fixtures.py +0 -0
  135. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/core/test_isolation.py +0 -0
  136. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/core/test_models.py +0 -0
  137. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/core/test_patch.py +0 -0
  138. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/core/test_reducer_finish.py +0 -0
  139. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/core/test_reducer_lifecycle.py +0 -0
  140. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/core/test_reducer_properties.py +0 -0
  141. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/core/test_reducer_state.py +0 -0
  142. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/core/test_reducer_text.py +0 -0
  143. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/core/test_reducer_tools.py +0 -0
  144. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/core/test_run_input.py +0 -0
  145. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/core/test_tools.py +0 -0
  146. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/examples/test_example_models.py +0 -0
  147. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/state/conftest.py +0 -0
  148. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/state/test_agent_state_config.py +0 -0
  149. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/state/test_agent_state_handlers.py +0 -0
  150. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/state/test_driver_cancel.py +0 -0
  151. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/state/test_driver_client_tools.py +0 -0
  152. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/state/test_driver_streaming.py +0 -0
  153. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/state/test_projection.py +0 -0
  154. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/state/test_reflex_harness.py +0 -0
  155. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/state/test_registry.py +0 -0
  156. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/test_deps_latest_workflow.py +0 -0
  157. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/test_public_api.py +0 -0
  158. {reflex_agentkit-0.1.0 → reflex_agentkit-0.2.0}/tests/unit/test_release.py +0 -0
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.0] - 2026-10-31
11
+
12
+ ### Added
13
+
14
+ - The documentation site is published to GitHub Pages from `main`
15
+ (<https://blog.seraph.to/reflex-agentkit/>).
16
+ - The model is told when the user approves a tool call with edited arguments (REQ-035).
17
+ Without this, models tended to report the edited result as a mistake or call the tool
18
+ again with their own arguments.
19
+ - `PydanticAIAgent` appends a notice to the edited call's result. Turn it off with
20
+ `notify_edited_args=False`.
21
+ - For LangGraph, `ApprovalDecision` gains `edited` and `tool_result(result)`. Build the
22
+ `ToolMessage` from `decision.tool_result(...)`.
23
+
24
+ ### Changed
25
+
26
+ - Approving with "edited" arguments identical to the proposed ones now records a plain
27
+ approval, without `editedArgs` (REQ-013.3).
28
+ - `astral-sh/setup-uv` is pinned to a commit SHA in every workflow.
29
+ - Markdown-image exfiltration is an accepted residual risk; markdown images stay
30
+ supported (TD v1.15).
31
+
10
32
  ## [0.1.0] - 2026-10-31
11
33
 
12
34
  The first release. `0.1.0rc1` was published on 2026-10-31 as its release candidate.
@@ -117,5 +139,6 @@ The first release. `0.1.0rc1` was published on 2026-10-31 as its release candida
117
139
  - The latest-dependencies workflow runs the newest, unvetted versions without a token or
118
140
  stored git credentials; a separate job reports failures.
119
141
 
120
- [Unreleased]: https://github.com/ecrespo/reflex-agentkit/compare/v0.1.0...HEAD
142
+ [Unreleased]: https://github.com/ecrespo/reflex-agentkit/compare/v0.2.0...HEAD
143
+ [0.2.0]: https://github.com/ecrespo/reflex-agentkit/compare/v0.1.0...v0.2.0
121
144
  [0.1.0]: https://github.com/ecrespo/reflex-agentkit/releases/tag/v0.1.0
@@ -1,8 +1,9 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: reflex-agentkit
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: The production UI layer for Python AI agents, built on Reflex and the AG-UI protocol.
5
5
  Project-URL: Homepage, https://github.com/ecrespo/reflex-agentkit
6
+ Project-URL: Documentation, https://blog.seraph.to/reflex-agentkit/
6
7
  Project-URL: Repository, https://github.com/ecrespo/reflex-agentkit
7
8
  Project-URL: Issues, https://github.com/ecrespo/reflex-agentkit/issues
8
9
  Project-URL: Changelog, https://github.com/ecrespo/reflex-agentkit/blob/main/CHANGELOG.md
@@ -44,10 +45,11 @@ chat, tool-call cards and human-in-the-loop approval for LangGraph, PydanticAI a
44
45
  AG-UI-compatible agent. The production UI layer your agent was missing — no TypeScript,
45
46
  no Streamlit.
46
47
 
47
- > **Status: `0.1.0`, the first release.** Everything in v0.1 is implemented and tested: the pure core,
48
+ > **Status: `0.2.0`.** Everything in v0.1 is implemented and tested: the pure core,
48
49
  > the agent clients, the Reflex state engine (`AgentState`), the chat components, both
49
- > adapters (PydanticAI and LangGraph) and the AG-UI Dojo examples.
50
- > `pip install "reflex-agentkit[pydantic-ai]"` or `uv add "reflex-agentkit[pydantic-ai]"`. Documentation: [`docs/`](docs/) (`uv run mkdocs serve`).
50
+ > adapters (PydanticAI and LangGraph) and the AG-UI Dojo examples. `0.2.0` tells the model
51
+ > when the user edits a tool call's arguments before approving it.
52
+ > `pip install "reflex-agentkit[pydantic-ai]"` or `uv add "reflex-agentkit[pydantic-ai]"`. Documentation: https://blog.seraph.to/reflex-agentkit/
51
53
 
52
54
  ## Why
53
55
 
@@ -161,7 +163,8 @@ an accessible label.
161
163
 
162
164
  The MkDocs site in [`docs/`](docs/) covers getting started (uv and pip), the concepts,
163
165
  guides (your own UI, human in the loop, sidecar deployment, local models with Ollama), the
164
- API reference, the security model and troubleshooting. Preview it with
166
+ API reference, the security model and troubleshooting. It is published at
167
+ <https://blog.seraph.to/reflex-agentkit/> from `main`; preview it locally with
165
168
  `uv run mkdocs serve`.
166
169
 
167
170
  ## Progress
@@ -180,6 +183,7 @@ Work follows the approved spec chain in [`specs/`](specs/) and the task list in
180
183
  | WP6 — LangGraph adapter | `LangGraphAGUIAgent`, `request_tool_approval`, legacy interrupts | ✅ done |
181
184
  | WP7 — Dojo examples | The five AG-UI Dojo demos for PydanticAI and LangGraph | ✅ done |
182
185
  | WP8 — Hardening, docs, release | Security review, benchmarks, documentation site, `0.1.0` on PyPI | ✅ done |
186
+ | Change: edited-args notice | REQ-035: the model learns about edited tool arguments (`changes/_archive/`) | ✅ `0.2.0` |
183
187
 
184
188
  ## What works today: `reflex_agentkit.core`
185
189
 
@@ -333,7 +337,8 @@ app.add_page(index)
333
337
 
334
338
  Install the extra (`reflex-agentkit[pydantic-ai]`) and wrap the agent. Tools declared with
335
339
  `requires_approval=True` pause the run for `approve_tool` / `reject_tool` with no extra
336
- configuration; edited arguments reach the tool.
340
+ configuration; edited arguments reach the tool, and the tool's result tells the model the
341
+ user changed them (`notify_edited_args=False` turns that off).
337
342
 
338
343
  ```python
339
344
  from typing import ClassVar
@@ -383,6 +388,7 @@ def tools(state: MessagesState) -> dict[str, Any]:
383
388
  call = last.tool_calls[0]
384
389
  decision = request_tool_approval(str(call["id"]), call["name"], call["args"])
385
390
  content = f"Booked {decision.args['city']}" if decision.approved else "Not booked"
391
+ content = decision.tool_result(content) # tells the model about edited arguments
386
392
  return {"messages": [ToolMessage(content=content, tool_call_id=call["id"])]}
387
393
 
388
394
 
@@ -5,10 +5,11 @@ chat, tool-call cards and human-in-the-loop approval for LangGraph, PydanticAI a
5
5
  AG-UI-compatible agent. The production UI layer your agent was missing — no TypeScript,
6
6
  no Streamlit.
7
7
 
8
- > **Status: `0.1.0`, the first release.** Everything in v0.1 is implemented and tested: the pure core,
8
+ > **Status: `0.2.0`.** Everything in v0.1 is implemented and tested: the pure core,
9
9
  > the agent clients, the Reflex state engine (`AgentState`), the chat components, both
10
- > adapters (PydanticAI and LangGraph) and the AG-UI Dojo examples.
11
- > `pip install "reflex-agentkit[pydantic-ai]"` or `uv add "reflex-agentkit[pydantic-ai]"`. Documentation: [`docs/`](docs/) (`uv run mkdocs serve`).
10
+ > adapters (PydanticAI and LangGraph) and the AG-UI Dojo examples. `0.2.0` tells the model
11
+ > when the user edits a tool call's arguments before approving it.
12
+ > `pip install "reflex-agentkit[pydantic-ai]"` or `uv add "reflex-agentkit[pydantic-ai]"`. Documentation: https://blog.seraph.to/reflex-agentkit/
12
13
 
13
14
  ## Why
14
15
 
@@ -122,7 +123,8 @@ an accessible label.
122
123
 
123
124
  The MkDocs site in [`docs/`](docs/) covers getting started (uv and pip), the concepts,
124
125
  guides (your own UI, human in the loop, sidecar deployment, local models with Ollama), the
125
- API reference, the security model and troubleshooting. Preview it with
126
+ API reference, the security model and troubleshooting. It is published at
127
+ <https://blog.seraph.to/reflex-agentkit/> from `main`; preview it locally with
126
128
  `uv run mkdocs serve`.
127
129
 
128
130
  ## Progress
@@ -141,6 +143,7 @@ Work follows the approved spec chain in [`specs/`](specs/) and the task list in
141
143
  | WP6 — LangGraph adapter | `LangGraphAGUIAgent`, `request_tool_approval`, legacy interrupts | ✅ done |
142
144
  | WP7 — Dojo examples | The five AG-UI Dojo demos for PydanticAI and LangGraph | ✅ done |
143
145
  | WP8 — Hardening, docs, release | Security review, benchmarks, documentation site, `0.1.0` on PyPI | ✅ done |
146
+ | Change: edited-args notice | REQ-035: the model learns about edited tool arguments (`changes/_archive/`) | ✅ `0.2.0` |
144
147
 
145
148
  ## What works today: `reflex_agentkit.core`
146
149
 
@@ -294,7 +297,8 @@ app.add_page(index)
294
297
 
295
298
  Install the extra (`reflex-agentkit[pydantic-ai]`) and wrap the agent. Tools declared with
296
299
  `requires_approval=True` pause the run for `approve_tool` / `reject_tool` with no extra
297
- configuration; edited arguments reach the tool.
300
+ configuration; edited arguments reach the tool, and the tool's result tells the model the
301
+ user changed them (`notify_edited_args=False` turns that off).
298
302
 
299
303
  ```python
300
304
  from typing import ClassVar
@@ -344,6 +348,7 @@ def tools(state: MessagesState) -> dict[str, Any]:
344
348
  call = last.tool_calls[0]
345
349
  decision = request_tool_approval(str(call["id"]), call["name"], call["args"])
346
350
  content = f"Booked {decision.args['city']}" if decision.approved else "Not booked"
351
+ content = decision.tool_result(content) # tells the model about edited arguments
347
352
  return {"messages": [ToolMessage(content=content, tool_call_id=call["id"])]}
348
353
 
349
354
 
@@ -0,0 +1,11 @@
1
+ # Changes
2
+
3
+ Proposals that change the observable behavior or the public API of a released
4
+ reflex-agentkit (CLAUDE.md, spec-anchored process). Each change lives in
5
+ `changes/<yyyy-mm>-<slug>/` with `proposal.md`, `delta-spec.md` and `tasks.md`. Once the
6
+ maintainer approves it and it ships, the delta is folded into `specs/` in the same PR and
7
+ the folder moves to `changes/_archive/`.
8
+
9
+ | Change | Status |
10
+ |---|---|
11
+ | [`2026-11-edited-args-notice`](_archive/2026-11-edited-args-notice/proposal.md) | Released in `0.2.0`, folded into `specs/`, archived |
@@ -36,6 +36,7 @@ all = ["reflex-agentkit[pydantic-ai,langgraph]"]
36
36
 
37
37
  [project.urls]
38
38
  Homepage = "https://github.com/ecrespo/reflex-agentkit"
39
+ Documentation = "https://blog.seraph.to/reflex-agentkit/"
39
40
  Repository = "https://github.com/ecrespo/reflex-agentkit"
40
41
  Issues = "https://github.com/ecrespo/reflex-agentkit/issues"
41
42
  Changelog = "https://github.com/ecrespo/reflex-agentkit/blob/main/CHANGELOG.md"
@@ -29,7 +29,7 @@ if TYPE_CHECKING: # type checkers see the lazy names; runtime imports them on f
29
29
  from reflex_agentkit.components.chat import agent_chat as agent_chat
30
30
  from reflex_agentkit.state.agent_state import AgentState as AgentState
31
31
 
32
- __version__ = "0.1.0"
32
+ __version__ = "0.2.0"
33
33
 
34
34
  # Public name -> module that defines it (API §1, §3).
35
35
  _LAZY_EXPORTS: dict[str, str] = {
@@ -0,0 +1,35 @@
1
+ """The edited-arguments notice both adapters give the model (REQ-035, DD-016).
2
+
3
+ When the user approves a tool call with edited arguments, the model only sees a result
4
+ that does not match what it asked for, and may "correct" the user by calling the tool
5
+ again. The notice tells it the edit was deliberate.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from collections.abc import Mapping
12
+ from typing import Any
13
+
14
+ import pydantic_core
15
+
16
+ __all__ = ["EDITED_ARGS_NOTICE", "with_edited_args_notice"]
17
+
18
+ EDITED_ARGS_NOTICE = (
19
+ "Note: the user edited this tool call's arguments before approving it, so the tool "
20
+ "ran with: {args}. These are the arguments the user wants: do not call the tool "
21
+ "again to restore the earlier ones, and do not report the change as an error."
22
+ )
23
+ """Normative text (REQ-035); tests pin it, so a change here is a spec change."""
24
+
25
+
26
+ def with_edited_args_notice(result: Any, args: Mapping[str, Any]) -> str:
27
+ """``result`` followed by a blank line and the notice naming ``args``.
28
+
29
+ A non-string result is serialized to JSON first, as PydanticAI does for tool returns.
30
+ """
31
+ text = (
32
+ result if isinstance(result, str) else pydantic_core.to_json(result, fallback=str).decode()
33
+ )
34
+ shown = json.dumps(dict(args), ensure_ascii=False, default=str)
35
+ return f"{text}\n\n{EDITED_ARGS_NOTICE.format(args=shown)}"
@@ -37,6 +37,7 @@ from langgraph.graph.state import CompiledStateGraph # noqa: E402
37
37
  from langgraph.types import Command, interrupt # noqa: E402
38
38
  from langgraph.types import Interrupt as LangGraphInterrupt # noqa: E402
39
39
 
40
+ from reflex_agentkit.adapters._edits import with_edited_args_notice # noqa: E402
40
41
  from reflex_agentkit.core.errors import ConfigurationError # noqa: E402
41
42
 
42
43
  __all__ = ["ApprovalDecision", "LangGraphAGUIAgent", "request_tool_approval"]
@@ -76,11 +77,26 @@ class ApprovalDecision:
76
77
  otherwise the original ones.
77
78
  reason: The rejection reason, ``"cancelled"`` when the run was cancelled while
78
79
  waiting, or ``None``.
80
+ edited: Whether the user approved with arguments that differ from the proposed
81
+ ones (REQ-035.2, REQ-035.5).
79
82
  """
80
83
 
81
84
  approved: bool
82
85
  args: dict[str, Any]
83
86
  reason: str | None = None
87
+ edited: bool = False
88
+
89
+ def tool_result(self, result: Any) -> Any:
90
+ """The result to give the model: with the edited-arguments notice when ``edited``.
91
+
92
+ Use it to build the ``ToolMessage`` so the model knows the user changed the
93
+ arguments on purpose (REQ-035.2). Without edits ``result`` comes back unchanged.
94
+ """
95
+ if not self.edited:
96
+ return result
97
+ text = with_edited_args_notice(result, self.args)
98
+ _log.debug("edited-arguments notice: %s", text)
99
+ return text
84
100
 
85
101
 
86
102
  def request_tool_approval(
@@ -120,9 +136,9 @@ def _decision(answer: Any, args: dict[str, Any]) -> ApprovalDecision:
120
136
  if answer.get("approved") is not True:
121
137
  return ApprovalDecision(approved=False, args=args, reason=reason)
122
138
  edited = answer.get("editedArgs")
123
- return ApprovalDecision(
124
- approved=True, args=dict(edited) if isinstance(edited, Mapping) else args
125
- )
139
+ if not isinstance(edited, Mapping) or dict(edited) == args:
140
+ return ApprovalDecision(approved=True, args=args)
141
+ return ApprovalDecision(approved=True, args=dict(edited), edited=True)
126
142
 
127
143
 
128
144
  class LangGraphAGUIAgent(LangGraphAgent): # type: ignore[misc] # untyped upstream base
@@ -6,21 +6,31 @@ from reflex_agentkit.adapters._require import require
6
6
 
7
7
  require("pydantic-ai", "pydantic_ai", "pydantic_ai.ui.ag_ui")
8
8
 
9
- import inspect # noqa: E402 - the guard must run before the optional imports
10
- from collections.abc import AsyncIterator, Awaitable, Callable, Sequence # noqa: E402
9
+ import dataclasses # noqa: E402 - the guard must run before the optional imports
10
+ import inspect # noqa: E402
11
+ import logging # noqa: E402
12
+ from collections.abc import AsyncIterator, Awaitable, Callable, Mapping, Sequence # noqa: E402
11
13
  from typing import Any # noqa: E402
12
14
 
13
15
  from ag_ui.core import BaseEvent, RunAgentInput # noqa: E402
14
- from pydantic_ai import DeferredToolRequests # noqa: E402
16
+ from pydantic_ai import DeferredToolRequests, RunContext # noqa: E402
15
17
  from pydantic_ai.agent import AbstractAgent # noqa: E402
18
+ from pydantic_ai.capabilities import AbstractCapability # noqa: E402
19
+ from pydantic_ai.exceptions import UserError # noqa: E402
20
+ from pydantic_ai.messages import ToolCallPart # noqa: E402
21
+ from pydantic_ai.tools import ToolDefinition # noqa: E402
16
22
  from pydantic_ai.ui.ag_ui import AGUIAdapter # noqa: E402
23
+ from pydantic_ai.ui.ag_ui._interrupt import interrupt_id_to_tool_call_id # noqa: E402
17
24
 
25
+ from reflex_agentkit.adapters._edits import with_edited_args_notice # noqa: E402
18
26
  from reflex_agentkit.core.errors import ConfigurationError # noqa: E402
19
27
 
20
28
  __all__ = ["PydanticAIAgent"]
21
29
 
22
30
  DepsFactory = Callable[[RunAgentInput], Any | Awaitable[Any]]
23
31
 
32
+ _log = logging.getLogger(__name__)
33
+
24
34
 
25
35
  class PydanticAIAgent:
26
36
  """Run a PydanticAI agent as an ``AGUIAgent`` (REQ-022.1).
@@ -34,6 +44,9 @@ class PydanticAIAgent:
34
44
  deps: Dependencies passed to every run.
35
45
  deps_factory: Builds the dependencies of one run from its input (sync or async).
36
46
  Mutually exclusive with ``deps``.
47
+ notify_edited_args: Tell the model when the user approved a tool call with
48
+ edited arguments, by appending a notice to the tool's result (REQ-035.1,
49
+ REQ-035.4).
37
50
  run_kwargs: Forwarded to ``AGUIAdapter.run_stream`` (``model_settings``,
38
51
  ``usage_limits``, ``model``, ...).
39
52
  """
@@ -44,6 +57,7 @@ class PydanticAIAgent:
44
57
  *,
45
58
  deps: Any = None,
46
59
  deps_factory: DepsFactory | None = None,
60
+ notify_edited_args: bool = True,
47
61
  **run_kwargs: Any,
48
62
  ) -> None:
49
63
  if not isinstance(agent, AbstractAgent):
@@ -56,6 +70,7 @@ class PydanticAIAgent:
56
70
  self._deps = deps
57
71
  self._deps_factory = deps_factory
58
72
  self._run_kwargs = run_kwargs
73
+ self._notify_edited_args = notify_edited_args
59
74
  self._output_type = _with_deferred(agent.output_type)
60
75
 
61
76
  def __repr__(self) -> str:
@@ -71,6 +86,9 @@ class PydanticAIAgent:
71
86
  kwargs = dict(self._run_kwargs)
72
87
  if self._output_type is not None:
73
88
  kwargs.setdefault("output_type", self._output_type)
89
+ edits = _edited_calls(run_input) if self._notify_edited_args else {}
90
+ if edits:
91
+ kwargs["capabilities"] = [*(kwargs.get("capabilities") or ()), _EditNotice(edits)]
74
92
  adapter = AGUIAdapter(agent=self._agent, run_input=run_input)
75
93
  stream = adapter.run_stream(deps=deps, **kwargs)
76
94
  try:
@@ -91,3 +109,44 @@ def _with_deferred(output_type: Any) -> list[Any] | None:
91
109
  if any(t is DeferredToolRequests for t in types):
92
110
  return None
93
111
  return [*types, DeferredToolRequests]
112
+
113
+
114
+ def _edited_calls(run_input: RunAgentInput) -> dict[str, dict[str, Any]]:
115
+ """Tool call ID → edited arguments, for every approval resumed with ``editedArgs``."""
116
+ edits: dict[str, dict[str, Any]] = {}
117
+ for entry in run_input.resume or ():
118
+ payload = entry.payload
119
+ if entry.status != "resolved" or not isinstance(payload, Mapping):
120
+ continue
121
+ edited = payload.get("editedArgs")
122
+ if payload.get("approved") is not True or not isinstance(edited, Mapping):
123
+ continue
124
+ try:
125
+ call_id = interrupt_id_to_tool_call_id(entry.interrupt_id)
126
+ except UserError: # not one of PydanticAI's approval interrupts
127
+ continue
128
+ edits[call_id] = dict(edited)
129
+ return edits
130
+
131
+
132
+ @dataclasses.dataclass
133
+ class _EditNotice(AbstractCapability[Any]):
134
+ """Append the edited-arguments notice to the results of edited calls (DD-016)."""
135
+
136
+ edits: dict[str, dict[str, Any]]
137
+
138
+ async def after_tool_execute(
139
+ self,
140
+ ctx: RunContext[Any],
141
+ *,
142
+ call: ToolCallPart,
143
+ tool_def: ToolDefinition,
144
+ args: Any,
145
+ result: Any,
146
+ ) -> Any:
147
+ edited = self.edits.get(call.tool_call_id)
148
+ if edited is None:
149
+ return result
150
+ text = with_edited_args_notice(result, edited)
151
+ _log.debug("edited-arguments notice: %s", text)
152
+ return text
@@ -121,7 +121,7 @@ def _with_call(conv: ConversationState, call: ToolCall) -> tuple[ToolCall, ...]:
121
121
  def apply_approval(
122
122
  conv: ConversationState, call_id: str, edited_args: object = None
123
123
  ) -> ConversationState:
124
- """Approve a pending tool call, optionally with edited arguments (REQ-013.2-3, 013.7).
124
+ """Approve a pending tool call, optionally with edited arguments (REQ-013.2-3, 013.7, 035.5).
125
125
 
126
126
  A ``str`` ``edited_args`` is parsed as JSON. Edits that are not a JSON object or that
127
127
  exceed :data:`MAX_PAYLOAD_BYTES` serialized set ``INVALID_EDITED_ARGS`` and leave the
@@ -146,8 +146,9 @@ def apply_approval(
146
146
  return _invalid_edit(conv, "the edited arguments are not serializable as JSON")
147
147
  if too_large:
148
148
  return _invalid_edit(conv, f"the edited arguments exceed {MAX_PAYLOAD_BYTES} bytes")
149
- args = edited_args
150
- payload["editedArgs"] = edited_args
149
+ if edited_args != call.args: # an unchanged "edit" is a plain approval (REQ-035.5)
150
+ args = edited_args
151
+ payload["editedArgs"] = edited_args
151
152
  running = replace(call, status=ToolCallStatus.RUNNING, args=args)
152
153
  answer = InterruptAnswer(interrupt_id=interrupt.id, status="resolved", payload=payload)
153
154
  return _record(conv, answer, tool_calls=_with_call(conv, running))
@@ -132,3 +132,17 @@ async def test_TD_8_4_2_frontend_tool_call_ends_with_success_not_interrupt() ->
132
132
  assert last.outcome is None or last.outcome.type == "success"
133
133
  pending = getattr(last.outcome, "pending_tool_call_ids", None)
134
134
  assert pending in (None, [starts[0].tool_call_id])
135
+
136
+
137
+ def test_TD_DD_016_interrupt_ids_map_back_to_tool_call_ids() -> None:
138
+ """DD-016: the private helper the adapter uses to find the edited call still exists."""
139
+ from pydantic_ai.ui.ag_ui._interrupt import interrupt_id_to_tool_call_id
140
+
141
+ assert interrupt_id_to_tool_call_id("int-call_1") == "call_1"
142
+
143
+
144
+ def test_TD_DD_016_run_stream_accepts_run_level_capabilities() -> None:
145
+ """DD-016: the notice rides on a per-run capability, merged with the agent's own."""
146
+ import inspect
147
+
148
+ assert "capabilities" in inspect.signature(AGUIAdapter.run_stream).parameters
@@ -128,14 +128,14 @@ async def _pause_for_approval(rx_: ReflexHarness, token: str) -> HotelGraphState
128
128
  async def test_REQ_034_3_approval_with_edits_reaches_the_tool(
129
129
  reflex_harness: ReflexHarness,
130
130
  ) -> None:
131
- """REQ-034.3: the helper returns the decision, edited args included."""
131
+ """REQ-034.3, REQ-035.2: the helper returns the decision, edits flagged."""
132
132
  await _pause_for_approval(reflex_harness, "edit")
133
133
  await reflex_harness.call(
134
134
  "edit", HotelGraphState, "approve_tool", call_id="c1", edited_args={"city": "Porto"}
135
135
  )
136
136
  state = await reflex_harness.state("edit", HotelGraphState)
137
137
  assert state.run_status == RunStatus.IDLE, state.error
138
- assert decisions == [ApprovalDecision(approved=True, args={"city": "Porto"})]
138
+ assert decisions == [ApprovalDecision(approved=True, args={"city": "Porto"}, edited=True)]
139
139
  call = next(c for c in state.tool_calls if c.id == "c1")
140
140
  assert call.status == ToolCallStatus.DONE
141
141
  assert call.result == "Booked Porto"
@@ -6,6 +6,7 @@
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import logging
9
10
  from typing import ClassVar
10
11
 
11
12
  import pytest
@@ -61,12 +62,19 @@ async def test_REQ_022_2_approve_runs_the_tool(reflex_harness: ReflexHarness) ->
61
62
  assert state.run_status == RunStatus.IDLE
62
63
  assert booked == ["a"] # TestModel's generated argument
63
64
  call = state.tool_calls[0]
64
- assert (call.status, call.result) == (ToolCallStatus.DONE, "booked a")
65
+ assert (call.status, call.result) == (ToolCallStatus.DONE, "booked a") # REQ-035.3
65
66
  assert state.messages[-1].role == "assistant"
66
67
 
67
68
 
69
+ NOTICE = (
70
+ "booked Porto\n\nNote: the user edited this tool call's arguments before approving it, "
71
+ 'so the tool ran with: {"city": "Porto"}. These are the arguments the user wants: do not '
72
+ "call the tool again to restore the earlier ones, and do not report the change as an error."
73
+ )
74
+
75
+
68
76
  async def test_REQ_022_3_approve_with_edited_args(reflex_harness: ReflexHarness) -> None:
69
- """REQ-022.3: the tool receives the edited arguments."""
77
+ """REQ-022.3, REQ-035.1: the tool gets the edits and the model is told about them."""
70
78
  state = await _pause(reflex_harness, "edit")
71
79
  call_id = state.pending_approvals[0].id
72
80
  await reflex_harness.call(
@@ -74,10 +82,45 @@ async def test_REQ_022_3_approve_with_edited_args(reflex_harness: ReflexHarness)
74
82
  )
75
83
  state = await reflex_harness.state("edit", HotelState)
76
84
  assert booked == ["Porto"]
77
- assert state.tool_calls[0].result == "booked Porto"
85
+ assert state.tool_calls[0].result == NOTICE
78
86
  assert state.run_status == RunStatus.IDLE
79
87
 
80
88
 
89
+ class QuietHotelState(AgentState, rx.State):
90
+ agent: ClassVar = PydanticAIAgent(_hotel_agent(), notify_edited_args=False)
91
+
92
+
93
+ async def test_REQ_035_4_notice_can_be_turned_off(reflex_harness: ReflexHarness) -> None:
94
+ """REQ-035.4: ``notify_edited_args=False`` leaves the tool result as the tool returned it."""
95
+ await reflex_harness.call("quiet", QuietHotelState, "send_message", text="book a hotel")
96
+ state = await reflex_harness.state("quiet", QuietHotelState)
97
+ call_id = state.pending_approvals[0].id
98
+ await reflex_harness.call(
99
+ "quiet", QuietHotelState, "approve_tool", call_id=call_id, edited_args={"city": "Porto"}
100
+ )
101
+ state = await reflex_harness.state("quiet", QuietHotelState)
102
+ assert booked == ["Porto"]
103
+ assert state.tool_calls[0].result == "booked Porto"
104
+
105
+
106
+ async def test_REQ_035_6_notice_is_logged_only_at_debug(
107
+ reflex_harness: ReflexHarness, caplog: pytest.LogCaptureFixture
108
+ ) -> None:
109
+ """REQ-035.6: the notice holds user content, so nothing about it is logged above DEBUG."""
110
+ caplog.set_level(logging.DEBUG, logger="reflex_agentkit")
111
+ state = await _pause(reflex_harness, "log")
112
+ await reflex_harness.call(
113
+ "log",
114
+ HotelState,
115
+ "approve_tool",
116
+ call_id=state.pending_approvals[0].id,
117
+ edited_args={"city": "Porto"},
118
+ )
119
+ porto = [r for r in caplog.records if "Porto" in r.getMessage()]
120
+ assert porto, "the notice is logged at DEBUG"
121
+ assert all(r.levelno == logging.DEBUG for r in porto)
122
+
123
+
81
124
  async def test_REQ_022_2_reject_does_not_run_the_tool(reflex_harness: ReflexHarness) -> None:
82
125
  """REQ-022.2: rejection resumes without running the tool; the model sees the denial."""
83
126
  state = await _pause(reflex_harness, "reject")
@@ -9,7 +9,7 @@ def test_package_imports() -> None:
9
9
  """REQ-031.1: the package and its pure core import cleanly."""
10
10
  import reflex_agentkit.core
11
11
 
12
- assert reflex_agentkit.__version__ == "0.1.0"
12
+ assert reflex_agentkit.__version__ == "0.2.0"
13
13
 
14
14
 
15
15
  def test_unknown_attribute_raises_attribute_error() -> None:
@@ -0,0 +1,44 @@
1
+ """The edited-arguments notice shared by both adapters (REQ-035, DD-016)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from reflex_agentkit.adapters._edits import EDITED_ARGS_NOTICE, with_edited_args_notice
6
+
7
+
8
+ def test_REQ_035_1_notice_text_is_pinned() -> None:
9
+ """REQ-035: the normative notice; changing it changes what every model reads."""
10
+ assert EDITED_ARGS_NOTICE == (
11
+ "Note: the user edited this tool call's arguments before approving it, so the tool "
12
+ "ran with: {args}. These are the arguments the user wants: do not call the tool "
13
+ "again to restore the earlier ones, and do not report the change as an error."
14
+ )
15
+
16
+
17
+ def test_REQ_035_1_string_results_get_the_notice_after_a_blank_line() -> None:
18
+ text = with_edited_args_notice("Booked 3 nights in Porto.", {"nights": 3, "city": "Porto"})
19
+ assert text == (
20
+ "Booked 3 nights in Porto.\n\nNote: the user edited this tool call's arguments "
21
+ 'before approving it, so the tool ran with: {"nights": 3, "city": "Porto"}. '
22
+ "These are the arguments the user wants: do not call the tool again to restore the "
23
+ "earlier ones, and do not report the change as an error."
24
+ )
25
+
26
+
27
+ def test_REQ_035_1_other_results_are_serialized_as_json_first() -> None:
28
+ """Non-string results are serialized the way PydanticAI serializes tool returns."""
29
+ text = with_edited_args_notice({"booked": True, "city": "Porto"}, {"city": "Porto"})
30
+ assert text.startswith('{"booked":true,"city":"Porto"}\n\nNote: ')
31
+
32
+
33
+ def test_REQ_035_1_non_json_values_fall_back_to_str() -> None:
34
+ class Ticket:
35
+ def __str__(self) -> str:
36
+ return "ticket-7"
37
+
38
+ text = with_edited_args_notice(Ticket(), {"when": Ticket()})
39
+ assert text.startswith('"ticket-7"\n\nNote: ')
40
+ assert 'the tool ran with: {"when": "ticket-7"}. ' in text
41
+
42
+
43
+ def test_REQ_035_1_unicode_stays_readable() -> None:
44
+ assert '{"city": "Málaga"}' in with_edited_args_notice("ok", {"city": "Málaga"})
@@ -125,7 +125,39 @@ def test_REQ_034_3_approved_returns_the_original_args(fake: FakeInterrupt) -> No
125
125
 
126
126
  def test_REQ_034_3_edited_args_take_precedence(fake: FakeInterrupt) -> None:
127
127
  decision = _approve(fake, {"approved": True, "editedArgs": {"city": "Porto", "nights": 3}})
128
- assert decision == ApprovalDecision(approved=True, args={"city": "Porto", "nights": 3})
128
+ assert decision == ApprovalDecision(
129
+ approved=True, args={"city": "Porto", "nights": 3}, edited=True
130
+ )
131
+
132
+
133
+ def test_REQ_035_2_tool_result_appends_the_notice_when_edited(fake: FakeInterrupt) -> None:
134
+ """REQ-035.2: an edited approval's result carries the edited-arguments notice."""
135
+ decision = _approve(fake, {"approved": True, "editedArgs": {"city": "Porto", "nights": 3}})
136
+ assert decision.tool_result("Booked.") == (
137
+ "Booked.\n\nNote: the user edited this tool call's arguments before approving it, "
138
+ 'so the tool ran with: {"city": "Porto", "nights": 3}. These are the arguments the '
139
+ "user wants: do not call the tool again to restore the earlier ones, and do not "
140
+ "report the change as an error."
141
+ )
142
+
143
+
144
+ @pytest.mark.parametrize(
145
+ "payload",
146
+ [
147
+ {"approved": True},
148
+ {"approved": True, "editedArgs": dict(ARGS)},
149
+ {"approved": False, "reason": "no"},
150
+ ],
151
+ ids=["plain", "unchanged-edit", "rejected"],
152
+ )
153
+ def test_REQ_035_3_tool_result_is_unchanged_without_edits(
154
+ fake: FakeInterrupt, payload: Any
155
+ ) -> None:
156
+ """REQ-035.3, REQ-035.5: no notice without a real edit, nor on rejection."""
157
+ decision = _approve(fake, payload)
158
+ assert decision.edited is False
159
+ assert decision.tool_result("Booked.") == "Booked."
160
+ assert decision.tool_result({"ok": 1}) == {"ok": 1}
129
161
 
130
162
 
131
163
  def test_REQ_034_3_rejection_carries_the_reason(fake: FakeInterrupt) -> None:
@@ -205,6 +205,15 @@ def test_REQ_013_3_approve_with_edited_args_records_payload(edited: object) -> N
205
205
  assert conv.tool_calls[0].args == {"city": "Porto"}
206
206
 
207
207
 
208
+ @pytest.mark.parametrize("edited", [{"city": "Lisbon"}, '{"city": "Lisbon"}'], ids=["dict", "str"])
209
+ def test_REQ_035_5_unchanged_edits_record_a_plain_approval(edited: object) -> None:
210
+ """REQ-035.5: edits equal to the current args are a plain approval, without ``editedArgs``."""
211
+ conv = apply_approval(_paused(_approval(), calls=(_pending_call(),)), "c1", edited)
212
+ assert conv.answers["int-c1"].payload == {"approved": True}
213
+ assert conv.tool_calls[0].args == {"city": "Lisbon"}
214
+ check_invariants(conv)
215
+
216
+
208
217
  @pytest.mark.parametrize(
209
218
  "edited",
210
219
  [[1, 2], "not json", '"a string"', {"big": "x" * MAX_PAYLOAD_BYTES}],
File without changes