reflex-agentkit 0.1.0rc1__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.
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/CHANGELOG.md +33 -4
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/PKG-INFO +14 -8
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/README.md +12 -7
- reflex_agentkit-0.2.0/changes/README.md +11 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/pyproject.toml +1 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/__init__.py +1 -1
- reflex_agentkit-0.2.0/src/reflex_agentkit/adapters/_edits.py +35 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/adapters/langgraph.py +19 -3
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/adapters/pydantic_ai.py +62 -3
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/core/commands.py +4 -3
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/core/reducer.py +14 -1
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/contract/test_pydantic_ai_facts.py +14 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/integration/test_langgraph_hitl.py +2 -2
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/integration/test_pydantic_ai_hitl.py +46 -3
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/test_smoke.py +1 -1
- reflex_agentkit-0.2.0/tests/unit/adapters/test_edits.py +44 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/adapters/test_langgraph_resume.py +33 -1
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/core/test_commands.py +9 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/core/test_reducer_state.py +38 -2
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/.gitignore +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/LICENSE +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/examples/_shared/README.md +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/examples/langgraph_dojo/README.md +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/examples/pydantic_ai_dojo/README.md +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/adapters/__init__.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/adapters/_require.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/components/__init__.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/components/approval.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/components/chat.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/components/composer.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/components/messages.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/components/shared_state.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/components/status.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/components/tools.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/core/__init__.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/core/_log.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/core/_patch.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/core/clients/__init__.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/core/clients/base.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/core/clients/http.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/core/clients/in_process.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/core/clients/sse.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/core/conversation.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/core/errors.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/core/models.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/core/run_input.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/core/tools.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/py.typed +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/state/__init__.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/state/_driver.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/state/_projection.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/state/_registry.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/state/agent_state.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/bench/conftest.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/bench/test_bench_flush.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/bench/test_bench_reducer.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/conftest.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/contract/test_agui_protocol.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/contract/test_langgraph_facts.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/contract/test_reflex_facts.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/client_tool_marked.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/custom_ring.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/finish_cancelled_outcome.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/finish_interrupt_generic_multi.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/finish_interrupt_tool_call.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/finish_mixed_interrupts.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/finish_success.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/finish_with_pending_client_tool.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/hitl_approve.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/hitl_approve_edited.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/hitl_cancel_while_paused.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/hitl_generic_queue.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/hitl_reject.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/ignored_families.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/invalid_event.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/lifecycle_happy.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/messages_snapshot.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/pydantic_ai_approval_recorded.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/reasoning_encrypted.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/reasoning_stream.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/run_error.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/stale_run_event.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/state_delta_fails.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/state_snapshot_delta.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/steps.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/text_chunks.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/text_stream.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/text_unclosed_at_finish.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/text_unknown_id.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/tool_call_args_too_large.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/tool_call_chunks.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/tool_call_happy_path.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/tool_call_invalid_args.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/tool_call_unknown_id.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/fixtures/events/unknown_type.jsonl +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/helpers/__init__.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/helpers/dojo.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/helpers/fake_chat_model.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/helpers/fake_clients.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/helpers/fake_server.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/helpers/fake_state_access.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/helpers/fixtures.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/helpers/invariants.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/helpers/reflex_harness.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/helpers/strategies.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/helpers/ui_state.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/integration/conftest.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/integration/test_concurrent_clients.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/integration/test_fake_server.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/integration/test_http_client.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/integration/test_multiworker_cancel.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/integration/test_responsiveness.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/integration/test_secret_hygiene.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/integration/test_security_vectors.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/smoke/conftest.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/smoke/test_dojo_langgraph.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/smoke/test_dojo_pydantic_ai.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/smoke/test_quickstart.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/adapters/test_langgraph_legacy.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/adapters/test_langgraph_unit.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/adapters/test_pydantic_ai_unit.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/adapters/test_require.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/components/test_approval.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/components/test_chat.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/components/test_composer.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/components/test_messages.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/components/test_shared_state.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/components/test_status.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/components/test_tool_card.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/core/clients/test_base.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/core/clients/test_in_process.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/core/clients/test_sse.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/core/test_conversation.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/core/test_errors.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/core/test_fixtures.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/core/test_isolation.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/core/test_models.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/core/test_patch.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/core/test_reducer_finish.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/core/test_reducer_lifecycle.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/core/test_reducer_properties.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/core/test_reducer_text.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/core/test_reducer_tools.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/core/test_run_input.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/core/test_tools.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/examples/test_example_models.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/state/conftest.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/state/test_agent_state_config.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/state/test_agent_state_handlers.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/state/test_driver_cancel.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/state/test_driver_client_tools.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/state/test_driver_streaming.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/state/test_projection.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/state/test_reflex_harness.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/state/test_registry.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/test_deps_latest_workflow.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/test_public_api.py +0 -0
- {reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/unit/test_release.py +0 -0
|
@@ -7,6 +7,32 @@ 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
|
+
|
|
32
|
+
## [0.1.0] - 2026-10-31
|
|
33
|
+
|
|
34
|
+
The first release. `0.1.0rc1` was published on 2026-10-31 as its release candidate.
|
|
35
|
+
|
|
10
36
|
### Added
|
|
11
37
|
|
|
12
38
|
- Project skeleton: uv/hatchling packaging, lint, type and test configuration, and CI.
|
|
@@ -92,12 +118,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
92
118
|
- A weekly workflow that tests against the newest allowed dependency versions and opens
|
|
93
119
|
an issue when they fail.
|
|
94
120
|
|
|
95
|
-
### Changed
|
|
96
|
-
|
|
97
|
-
- The package version is `0.1.0rc1`, the release candidate for `0.1.0`.
|
|
98
|
-
|
|
99
121
|
### Fixed
|
|
100
122
|
|
|
123
|
+
- A tool call that a model sends in one chunk (Ollama through LangGraph) now shows its
|
|
124
|
+
arguments in the approval dialog: a messages snapshot fills in the arguments of a call
|
|
125
|
+
that streamed none. Found in the `0.1.0rc1` walkthrough.
|
|
101
126
|
- A `TOOL_CALL_RESULT` no longer moves a rejected or failed tool call back to `done`.
|
|
102
127
|
- A `MESSAGES_SNAPSHOT` now fills in the result of a tool call it keeps, so LangGraph tool
|
|
103
128
|
calls end as `done` after their tool node runs.
|
|
@@ -113,3 +138,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
113
138
|
registered as Reflex event handlers, so the browser cannot call them.
|
|
114
139
|
- The latest-dependencies workflow runs the newest, unvetted versions without a token or
|
|
115
140
|
stored git credentials; a separate job reports failures.
|
|
141
|
+
|
|
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
|
|
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.
|
|
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:
|
|
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
|
-
>
|
|
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.
|
|
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
|
|
@@ -176,10 +179,11 @@ Work follows the approved spec chain in [`specs/`](specs/) and the task list in
|
|
|
176
179
|
| WP2 — Clients | `AgentClient`/`AGUIAgent` protocols, SSE parser, `HttpAGUIClient`, `InProcessAGUIClient` | ✅ done |
|
|
177
180
|
| WP3 — Reflex state engine | `AgentState` mixin, streaming driver, cancellation, client-tool rounds | ✅ done |
|
|
178
181
|
| WP4 — PydanticAI adapter | `PydanticAIAgent`: a PydanticAI agent as an `AGUIAgent`, tool approvals, client tools | ✅ done |
|
|
179
|
-
| WP5 — Components and quickstart | `agent_chat` and its parts, quickstart example, release workflow | ✅ done
|
|
182
|
+
| WP5 — Components and quickstart | `agent_chat` and its parts, quickstart example, release workflow | ✅ done |
|
|
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
|
-
| WP8 — Hardening, docs, release | Security review, benchmarks, documentation site, `0.1.0` on PyPI |
|
|
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:
|
|
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
|
-
>
|
|
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.
|
|
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
|
|
@@ -137,10 +139,11 @@ Work follows the approved spec chain in [`specs/`](specs/) and the task list in
|
|
|
137
139
|
| WP2 — Clients | `AgentClient`/`AGUIAgent` protocols, SSE parser, `HttpAGUIClient`, `InProcessAGUIClient` | ✅ done |
|
|
138
140
|
| WP3 — Reflex state engine | `AgentState` mixin, streaming driver, cancellation, client-tool rounds | ✅ done |
|
|
139
141
|
| WP4 — PydanticAI adapter | `PydanticAIAgent`: a PydanticAI agent as an `AGUIAgent`, tool approvals, client tools | ✅ done |
|
|
140
|
-
| WP5 — Components and quickstart | `agent_chat` and its parts, quickstart example, release workflow | ✅ done
|
|
142
|
+
| WP5 — Components and quickstart | `agent_chat` and its parts, quickstart example, release workflow | ✅ done |
|
|
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
|
-
| WP8 — Hardening, docs, release | Security review, benchmarks, documentation site, `0.1.0` on PyPI |
|
|
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.
|
|
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)}"
|
{reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/adapters/langgraph.py
RENAMED
|
@@ -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
|
-
|
|
124
|
-
approved=True, 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
|
{reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/src/reflex_agentkit/adapters/pydantic_ai.py
RENAMED
|
@@ -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
|
|
10
|
-
|
|
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
|
|
150
|
-
|
|
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))
|
|
@@ -816,6 +816,16 @@ def _reasoning_encrypted(
|
|
|
816
816
|
# --- MESSAGES_SNAPSHOT ---------------------------------------------------------------------
|
|
817
817
|
|
|
818
818
|
|
|
819
|
+
def _with_snapshot_args(call: ToolCall, arguments: str) -> ToolCall:
|
|
820
|
+
"""``call`` with the snapshot's arguments if it streamed none and they are an object."""
|
|
821
|
+
if call.args_text.strip() or call.args or not arguments.strip():
|
|
822
|
+
return call # streamed or edited arguments win
|
|
823
|
+
args = _parse_args(arguments)
|
|
824
|
+
if args is None:
|
|
825
|
+
return call
|
|
826
|
+
return dataclasses.replace(call, args_text=arguments, args=args)
|
|
827
|
+
|
|
828
|
+
|
|
819
829
|
def _snapshot_call(
|
|
820
830
|
conv: ConversationState, tool_call: Any, parent_id: str, results: dict[str, Any]
|
|
821
831
|
) -> ToolCall:
|
|
@@ -823,11 +833,14 @@ def _snapshot_call(
|
|
|
823
833
|
|
|
824
834
|
A kept record with no result yet takes the one from the snapshot's ``tool`` message,
|
|
825
835
|
as ``TOOL_CALL_RESULT`` would: ag-ui-langgraph sends a tool node's result only here
|
|
826
|
-
(DM v1.10). ``rejected`` and ``error`` stay terminal.
|
|
836
|
+
(DM v1.10). ``rejected`` and ``error`` stay terminal. A kept record that streamed no
|
|
837
|
+
arguments takes the snapshot's, when they parse to an object (DM v1.11): a tool call
|
|
838
|
+
sent in one chunk reaches ag-ui-langgraph's stream with no ``TOOL_CALL_ARGS``.
|
|
827
839
|
"""
|
|
828
840
|
existing = _get_call(conv, tool_call.id)
|
|
829
841
|
result = results.get(tool_call.id)
|
|
830
842
|
if existing is not None:
|
|
843
|
+
existing = _with_snapshot_args(existing, tool_call.function.arguments)
|
|
831
844
|
if result is None or existing.result is not None:
|
|
832
845
|
return existing
|
|
833
846
|
status: ToolCallStatus
|
|
@@ -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,
|
|
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"
|
{reflex_agentkit-0.1.0rc1 → reflex_agentkit-0.2.0}/tests/integration/test_pydantic_ai_hitl.py
RENAMED
|
@@ -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
|
|
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 ==
|
|
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.
|
|
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"})
|