codex-a2a 0.6.0__tar.gz → 0.6.1__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.
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/.gitignore +2 -0
- {codex_a2a-0.6.0/src/codex_a2a.egg-info → codex_a2a-0.6.1}/PKG-INFO +1 -1
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/stream_interrupts.py +13 -5
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/session_control.py +16 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/session_query.py +16 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/upstream/conversation_facade.py +115 -4
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/upstream/interrupts.py +17 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/upstream/models.py +4 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1/src/codex_a2a.egg-info}/PKG-INFO +1 -1
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/execution/test_stream_interrupts.py +31 -2
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/jsonrpc/test_codex_session_extension.py +77 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/upstream/test_codex_client_params.py +105 -1
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/upstream/test_modular_upstream_components.py +3 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/.github/workflows/ci.yml +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/.github/workflows/publish.yml +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/.pre-commit-config.yaml +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/.secrets.baseline +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/AGENTS.md +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/CONTRIBUTING.md +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/LICENSE +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/README.md +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/SECURITY.md +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/docs/architecture.md +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/docs/compatibility.md +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/docs/extension-specifications.md +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/docs/guide.md +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/docs/maintainer-architecture.md +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/mypy.ini +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/pyproject.toml +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/scripts/README.md +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/scripts/publish_github_release.sh +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/scripts/smoke_test_built_cli.sh +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/scripts/sync_codex_docs.sh +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/scripts/validate_baseline.sh +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/scripts/validate_runtime_matrix.sh +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/setup.cfg +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/__init__.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/auth.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/cli.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/client/__init__.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/client/agent_card.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/client/auth.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/client/client.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/client/config.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/client/errors.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/client/manager.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/client/payload_text.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/client/request_context.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/client/types.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/config.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/contracts/__init__.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/contracts/extensions.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/contracts/runtime_output.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/__init__.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/cancellation.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/directory_policy.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/discovery_runtime.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/exec_runtime.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/executor.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/output_mapping.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/request_metadata.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/request_overrides.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/response_emitter.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/review_runtime.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/session_runtime.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/stream_chunks.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/stream_processor.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/stream_state.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/streaming.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/thread_lifecycle_runtime.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/tool_call_payloads.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/input_mapping.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/__init__.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/application.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/control_params.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/discovery_control.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/discovery_params.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/discovery_payload_mapping.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/discovery_query.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/dispatch.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/errors.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/exec_control.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/hooks.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/interrupt_lifecycle.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/interrupt_params.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/interrupt_recovery.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/interrupt_recovery_params.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/interrupts.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/params.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/params_common.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/payload_mapping.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/query_params.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/review_control.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/review_control_params.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/thread_lifecycle_control.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/thread_lifecycle_params.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/turn_control.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/turn_control_params.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/logging_context.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/media_modes.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/metrics.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/parts/text.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/profile/__init__.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/profile/runtime.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/py.typed +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/server/__init__.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/server/agent_card.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/server/application.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/server/call_context.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/server/database.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/server/http_middlewares.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/server/migrations.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/server/openapi.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/server/output_negotiation.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/server/request_handler.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/server/runtime_state.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/server/task_store.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/upstream/__init__.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/upstream/client.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/upstream/exec_facade.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/upstream/interrupt_bridge.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/upstream/notification_mapping.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/upstream/request_mapping.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/upstream/startup.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/upstream/stream_bridge.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/upstream/transport.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a.egg-info/SOURCES.txt +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a.egg-info/dependency_links.txt +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a.egg-info/entry_points.txt +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a.egg-info/requires.txt +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a.egg-info/top_level.txt +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/__init__.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/client/__init__.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/client/test_agent_card.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/client/test_cli.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/client/test_client_flow.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/client/test_client_manager.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/client/test_errors.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/client/test_lifecycle.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/client/test_payload_text.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/client/test_request_context.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/client/test_types.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/config/__init__.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/config/test_settings.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/contracts/__init__.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/contracts/test_extension_contract_consistency.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/execution/__init__.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/execution/test_agent_errors.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/execution/test_cancellation.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/execution/test_codex_agent_session_binding.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/execution/test_directory_validation.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/execution/test_discovery_exec_runtime.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/execution/test_metrics.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/execution/test_review_runtime.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/execution/test_session_ownership.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/execution/test_session_persistence.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/execution/test_session_runtime.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/execution/test_stream_chunks.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/execution/test_streaming_output_contract.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/execution/test_thread_lifecycle_runtime.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/execution/test_tool_call_payloads.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/fixtures/codex_app_server/command_execution_output_delta.json +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/fixtures/codex_app_server/file_change_output_delta.json +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/jsonrpc/__init__.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/jsonrpc/test_codex_discovery_extension.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/jsonrpc/test_codex_exec_extension.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/jsonrpc/test_codex_thread_lifecycle_extension.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/jsonrpc/test_codex_turn_review_extension.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/jsonrpc/test_dispatch.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/jsonrpc/test_guard_hooks.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/jsonrpc/test_jsonrpc_models.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/package/__init__.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/package/test_logging.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/package/test_publish_github_release_script.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/package/test_release_distribution_contract.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/package/test_typing_contract.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/package/test_version.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/parts/__init__.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/parts/test_text.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/profile/__init__.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/profile/test_profile.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/server/__init__.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/server/test_agent_card.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/server/test_auth.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/server/test_call_context_builder.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/server/test_cli.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/server/test_database.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/server/test_database_app_persistence.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/server/test_migrations.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/server/test_request_handler.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/server/test_runtime_state.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/server/test_task_store.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/server/test_transport_contract.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/support/__init__.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/support/context.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/support/dummy_clients.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/support/fixtures.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/support/settings.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/test_input_mapping.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/upstream/__init__.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/upstream/test_interrupt_persistence.py +0 -0
- {codex_a2a-0.6.0 → codex_a2a-0.6.1}/uv.lock +0 -0
|
@@ -3,6 +3,8 @@ from __future__ import annotations
|
|
|
3
3
|
from collections.abc import Mapping
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
+
from codex_a2a.upstream.interrupts import resolve_permission_interrupt_semantic
|
|
7
|
+
|
|
6
8
|
_INTERRUPT_ASKED_EVENT_TYPES = {
|
|
7
9
|
"permission.asked",
|
|
8
10
|
"question.asked",
|
|
@@ -189,12 +191,18 @@ def extract_interrupt_asked_event(event: Mapping[str, Any]) -> dict[str, Any] |
|
|
|
189
191
|
if not normalized_request_id:
|
|
190
192
|
return None
|
|
191
193
|
if event_type == "permission.asked":
|
|
192
|
-
|
|
193
|
-
|
|
194
|
+
permission = _first_nested_string(
|
|
195
|
+
props,
|
|
196
|
+
("permission",),
|
|
197
|
+
("metadata", "raw", "permission"),
|
|
198
|
+
) or resolve_permission_interrupt_semantic(
|
|
199
|
+
_first_nested_string(
|
|
194
200
|
props,
|
|
195
|
-
("
|
|
196
|
-
|
|
197
|
-
|
|
201
|
+
("metadata", "method"),
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
details: dict[str, Any] = {
|
|
205
|
+
"permission": permission,
|
|
198
206
|
"patterns": extract_interrupt_patterns(props),
|
|
199
207
|
"always": extract_string_list(_nested_value(props, "always"))
|
|
200
208
|
or extract_string_list(_nested_value(props, "metadata", "raw", "always")),
|
|
@@ -35,6 +35,7 @@ from codex_a2a.jsonrpc.payload_mapping import (
|
|
|
35
35
|
as_a2a_message,
|
|
36
36
|
message_to_item,
|
|
37
37
|
)
|
|
38
|
+
from codex_a2a.upstream.models import CodexRPCError, is_thread_not_found_error
|
|
38
39
|
|
|
39
40
|
if TYPE_CHECKING:
|
|
40
41
|
from codex_a2a.jsonrpc.application import CodexSessionQueryJSONRPCApplication
|
|
@@ -187,6 +188,21 @@ async def handle_session_control_request(
|
|
|
187
188
|
data={"type": "UPSTREAM_UNREACHABLE", "session_id": session_id},
|
|
188
189
|
),
|
|
189
190
|
)
|
|
191
|
+
except CodexRPCError as exc:
|
|
192
|
+
if is_thread_not_found_error(exc):
|
|
193
|
+
return app._generate_error_response(
|
|
194
|
+
base_request.id,
|
|
195
|
+
JSONRPCError(
|
|
196
|
+
code=ERR_SESSION_NOT_FOUND,
|
|
197
|
+
message="Session not found",
|
|
198
|
+
data={"type": "SESSION_NOT_FOUND", "session_id": session_id},
|
|
199
|
+
),
|
|
200
|
+
)
|
|
201
|
+
logger.exception("Codex session control JSON-RPC method failed")
|
|
202
|
+
return app._generate_error_response(
|
|
203
|
+
base_request.id,
|
|
204
|
+
A2AError(root=InternalError(message=str(exc))),
|
|
205
|
+
)
|
|
190
206
|
except Exception as exc:
|
|
191
207
|
logger.exception("Codex session control JSON-RPC method failed")
|
|
192
208
|
return app._generate_error_response(
|
|
@@ -24,6 +24,7 @@ from codex_a2a.jsonrpc.payload_mapping import (
|
|
|
24
24
|
as_a2a_session_task,
|
|
25
25
|
extract_raw_items,
|
|
26
26
|
)
|
|
27
|
+
from codex_a2a.upstream.models import CodexRPCError, is_thread_not_found_error
|
|
27
28
|
|
|
28
29
|
if TYPE_CHECKING:
|
|
29
30
|
from codex_a2a.jsonrpc.application import CodexSessionQueryJSONRPCApplication
|
|
@@ -81,6 +82,21 @@ async def handle_session_query_request(
|
|
|
81
82
|
data={"type": "UPSTREAM_UNREACHABLE"},
|
|
82
83
|
),
|
|
83
84
|
)
|
|
85
|
+
except CodexRPCError as exc:
|
|
86
|
+
if is_thread_not_found_error(exc) and session_id is not None:
|
|
87
|
+
return app._generate_error_response(
|
|
88
|
+
base_request.id,
|
|
89
|
+
JSONRPCError(
|
|
90
|
+
code=ERR_SESSION_NOT_FOUND,
|
|
91
|
+
message="Session not found",
|
|
92
|
+
data={"type": "SESSION_NOT_FOUND", "session_id": session_id},
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
logger.exception("Codex session query JSON-RPC method failed")
|
|
96
|
+
return app._generate_error_response(
|
|
97
|
+
base_request.id,
|
|
98
|
+
A2AError(root=InternalError(message=str(exc))),
|
|
99
|
+
)
|
|
84
100
|
except Exception as exc:
|
|
85
101
|
logger.exception("Codex session query JSON-RPC method failed")
|
|
86
102
|
return app._generate_error_response(
|
|
@@ -7,7 +7,12 @@ from typing import Any
|
|
|
7
7
|
|
|
8
8
|
from codex_a2a.execution.request_overrides import RequestExecutionOptions
|
|
9
9
|
from codex_a2a.input_mapping import build_turn_input_from_normalized_items
|
|
10
|
-
from codex_a2a.upstream.models import
|
|
10
|
+
from codex_a2a.upstream.models import (
|
|
11
|
+
CodexMessage,
|
|
12
|
+
CodexRPCError,
|
|
13
|
+
_TurnTracker,
|
|
14
|
+
is_thread_not_found_error,
|
|
15
|
+
)
|
|
11
16
|
from codex_a2a.upstream.request_mapping import (
|
|
12
17
|
apply_thread_start_execution_options,
|
|
13
18
|
apply_turn_start_execution_options,
|
|
@@ -37,6 +42,8 @@ class CodexConversationFacade:
|
|
|
37
42
|
self._rpc_request = rpc_request
|
|
38
43
|
self._get_or_create_tracker = get_or_create_tracker
|
|
39
44
|
self._turn_trackers = turn_trackers
|
|
45
|
+
self._loaded_thread_ids: set[str] = set()
|
|
46
|
+
self._thread_resume_locks: dict[str, asyncio.Lock] = {}
|
|
40
47
|
|
|
41
48
|
async def create_session(
|
|
42
49
|
self,
|
|
@@ -67,7 +74,9 @@ class CodexConversationFacade:
|
|
|
67
74
|
session_id = thread.get("id")
|
|
68
75
|
if not isinstance(session_id, str) or not session_id.strip():
|
|
69
76
|
raise RuntimeError("codex thread/start response missing thread id")
|
|
70
|
-
|
|
77
|
+
session_id = session_id.strip()
|
|
78
|
+
self._loaded_thread_ids.add(session_id)
|
|
79
|
+
return session_id
|
|
71
80
|
|
|
72
81
|
async def list_sessions(self, *, query: dict[str, Any]) -> list[dict[str, Any]]:
|
|
73
82
|
rpc_params: dict[str, Any] = {}
|
|
@@ -98,6 +107,7 @@ class CodexConversationFacade:
|
|
|
98
107
|
thread = normalize_thread_summary(result.get("thread"))
|
|
99
108
|
if thread is None:
|
|
100
109
|
raise RuntimeError("codex thread/fork response missing thread")
|
|
110
|
+
self._loaded_thread_ids.add(thread["id"])
|
|
101
111
|
return thread
|
|
102
112
|
|
|
103
113
|
async def thread_archive(self, thread_id: str) -> None:
|
|
@@ -113,6 +123,7 @@ class CodexConversationFacade:
|
|
|
113
123
|
thread = normalize_thread_summary(result.get("thread"))
|
|
114
124
|
if thread is None:
|
|
115
125
|
raise RuntimeError("codex thread/unarchive response missing thread")
|
|
126
|
+
self._loaded_thread_ids.add(thread["id"])
|
|
116
127
|
return thread
|
|
117
128
|
|
|
118
129
|
async def thread_metadata_update(
|
|
@@ -216,7 +227,12 @@ class CodexConversationFacade:
|
|
|
216
227
|
default_model_id=self._model_id,
|
|
217
228
|
)
|
|
218
229
|
|
|
219
|
-
result = await self.
|
|
230
|
+
result = await self._start_turn(
|
|
231
|
+
session_id,
|
|
232
|
+
params,
|
|
233
|
+
directory=directory,
|
|
234
|
+
execution_options=execution_options,
|
|
235
|
+
)
|
|
220
236
|
if not isinstance(result, dict):
|
|
221
237
|
raise RuntimeError("codex turn/start response missing result object")
|
|
222
238
|
turn = result.get("turn")
|
|
@@ -268,7 +284,12 @@ class CodexConversationFacade:
|
|
|
268
284
|
execution_options=coerce_request_execution_options(execution_options),
|
|
269
285
|
default_model_id=self._model_id,
|
|
270
286
|
)
|
|
271
|
-
result = await self.
|
|
287
|
+
result = await self._start_turn(
|
|
288
|
+
session_id,
|
|
289
|
+
params,
|
|
290
|
+
directory=directory,
|
|
291
|
+
execution_options=execution_options,
|
|
292
|
+
)
|
|
272
293
|
if not isinstance(result, dict):
|
|
273
294
|
raise RuntimeError("codex turn/start response missing result object")
|
|
274
295
|
turn = result.get("turn")
|
|
@@ -279,6 +300,96 @@ class CodexConversationFacade:
|
|
|
279
300
|
raise RuntimeError("codex turn/start response missing turn id")
|
|
280
301
|
return {"ok": True, "session_id": session_id, "turn_id": turn_id.strip()}
|
|
281
302
|
|
|
303
|
+
async def _start_turn(
|
|
304
|
+
self,
|
|
305
|
+
session_id: str,
|
|
306
|
+
params: dict[str, Any],
|
|
307
|
+
*,
|
|
308
|
+
directory: str | None,
|
|
309
|
+
execution_options: RequestExecutionOptions | None,
|
|
310
|
+
) -> Any:
|
|
311
|
+
await self._ensure_thread_loaded(
|
|
312
|
+
session_id,
|
|
313
|
+
directory=directory,
|
|
314
|
+
execution_options=execution_options,
|
|
315
|
+
)
|
|
316
|
+
try:
|
|
317
|
+
result = await self._rpc_request("turn/start", params)
|
|
318
|
+
except CodexRPCError as exc:
|
|
319
|
+
if not is_thread_not_found_error(exc):
|
|
320
|
+
raise
|
|
321
|
+
self._loaded_thread_ids.discard(session_id)
|
|
322
|
+
await self._ensure_thread_loaded(
|
|
323
|
+
session_id,
|
|
324
|
+
directory=directory,
|
|
325
|
+
execution_options=execution_options,
|
|
326
|
+
force_resume=True,
|
|
327
|
+
)
|
|
328
|
+
result = await self._rpc_request("turn/start", params)
|
|
329
|
+
self._loaded_thread_ids.add(session_id)
|
|
330
|
+
return result
|
|
331
|
+
|
|
332
|
+
async def _ensure_thread_loaded(
|
|
333
|
+
self,
|
|
334
|
+
session_id: str,
|
|
335
|
+
*,
|
|
336
|
+
directory: str | None,
|
|
337
|
+
execution_options: RequestExecutionOptions | None,
|
|
338
|
+
force_resume: bool = False,
|
|
339
|
+
) -> None:
|
|
340
|
+
if not force_resume and session_id in self._loaded_thread_ids:
|
|
341
|
+
return
|
|
342
|
+
|
|
343
|
+
lock = self._thread_resume_locks.setdefault(session_id, asyncio.Lock())
|
|
344
|
+
async with lock:
|
|
345
|
+
if not force_resume and session_id in self._loaded_thread_ids:
|
|
346
|
+
return
|
|
347
|
+
|
|
348
|
+
if not force_resume:
|
|
349
|
+
loaded_threads = await self._list_loaded_thread_ids()
|
|
350
|
+
self._loaded_thread_ids.update(loaded_threads)
|
|
351
|
+
if session_id in self._loaded_thread_ids:
|
|
352
|
+
return
|
|
353
|
+
|
|
354
|
+
await self._resume_thread(
|
|
355
|
+
session_id,
|
|
356
|
+
directory=directory,
|
|
357
|
+
execution_options=execution_options,
|
|
358
|
+
)
|
|
359
|
+
self._loaded_thread_ids.add(session_id)
|
|
360
|
+
|
|
361
|
+
async def _list_loaded_thread_ids(self) -> set[str]:
|
|
362
|
+
result = await self._rpc_request("thread/loaded/list", {})
|
|
363
|
+
if not isinstance(result, dict):
|
|
364
|
+
return set()
|
|
365
|
+
data = result.get("data")
|
|
366
|
+
if not isinstance(data, list):
|
|
367
|
+
return set()
|
|
368
|
+
loaded: set[str] = set()
|
|
369
|
+
for item in data:
|
|
370
|
+
if isinstance(item, str) and item.strip():
|
|
371
|
+
loaded.add(item.strip())
|
|
372
|
+
return loaded
|
|
373
|
+
|
|
374
|
+
async def _resume_thread(
|
|
375
|
+
self,
|
|
376
|
+
session_id: str,
|
|
377
|
+
*,
|
|
378
|
+
directory: str | None,
|
|
379
|
+
execution_options: RequestExecutionOptions | None,
|
|
380
|
+
) -> Any:
|
|
381
|
+
params: dict[str, Any] = {"threadId": session_id}
|
|
382
|
+
if directory:
|
|
383
|
+
params["cwd"] = directory
|
|
384
|
+
elif self._workspace_root:
|
|
385
|
+
params["cwd"] = self._workspace_root
|
|
386
|
+
apply_thread_start_execution_options(
|
|
387
|
+
params,
|
|
388
|
+
execution_options=coerce_request_execution_options(execution_options),
|
|
389
|
+
default_model_id=self._model_id,
|
|
390
|
+
)
|
|
391
|
+
return await self._rpc_request("thread/resume", params)
|
|
392
|
+
|
|
282
393
|
async def turn_steer(
|
|
283
394
|
self,
|
|
284
395
|
thread_id: str,
|
|
@@ -5,6 +5,13 @@ from collections.abc import Mapping
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
|
+
_PERMISSION_INTERRUPT_METHOD_MAP = {
|
|
9
|
+
"item/commandExecution/requestApproval": "command_execution",
|
|
10
|
+
"execCommandApproval": "command_execution",
|
|
11
|
+
"item/fileChange/requestApproval": "file_change",
|
|
12
|
+
"applyPatchApproval": "apply_patch",
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
|
|
9
16
|
def _normalized_string(value: Any) -> str | None:
|
|
10
17
|
if not isinstance(value, str):
|
|
@@ -87,6 +94,13 @@ def _extract_mapping(params: dict[str, Any], key: str) -> dict[str, Any] | None:
|
|
|
87
94
|
return dict(value)
|
|
88
95
|
|
|
89
96
|
|
|
97
|
+
def resolve_permission_interrupt_semantic(method: str | None) -> str | None:
|
|
98
|
+
normalized_method = _normalized_string(method)
|
|
99
|
+
if normalized_method is None:
|
|
100
|
+
return None
|
|
101
|
+
return _PERMISSION_INTERRUPT_METHOD_MAP.get(normalized_method)
|
|
102
|
+
|
|
103
|
+
|
|
90
104
|
class InterruptRequestError(RuntimeError):
|
|
91
105
|
def __init__(
|
|
92
106
|
self,
|
|
@@ -149,6 +163,9 @@ def build_codex_permission_interrupt_properties(
|
|
|
149
163
|
)
|
|
150
164
|
if display_message is not None:
|
|
151
165
|
properties["display_message"] = display_message
|
|
166
|
+
permission = resolve_permission_interrupt_semantic(method)
|
|
167
|
+
if permission is not None:
|
|
168
|
+
properties["permission"] = permission
|
|
152
169
|
patterns = _extract_permission_patterns(params)
|
|
153
170
|
if patterns:
|
|
154
171
|
properties["patterns"] = patterns
|
|
@@ -24,6 +24,10 @@ class CodexRPCError(RuntimeError):
|
|
|
24
24
|
self.data = data
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
def is_thread_not_found_error(exc: BaseException) -> bool:
|
|
28
|
+
return isinstance(exc, CodexRPCError) and "thread not found" in str(exc).strip().lower()
|
|
29
|
+
|
|
30
|
+
|
|
27
31
|
@dataclass
|
|
28
32
|
class _PendingRpcRequest:
|
|
29
33
|
request_id: str
|
|
@@ -48,10 +48,11 @@ def test_extract_permission_interrupt_promotes_reason_and_parsed_paths() -> None
|
|
|
48
48
|
"properties": {
|
|
49
49
|
"id": "perm-2",
|
|
50
50
|
"metadata": {
|
|
51
|
+
"method": "execCommandApproval",
|
|
51
52
|
"raw": {
|
|
52
53
|
"reason": "The command needs confirmation.",
|
|
53
54
|
"parsedCmd": [{"path": "/repo/.env"}],
|
|
54
|
-
}
|
|
55
|
+
},
|
|
55
56
|
},
|
|
56
57
|
},
|
|
57
58
|
}
|
|
@@ -60,7 +61,7 @@ def test_extract_permission_interrupt_promotes_reason_and_parsed_paths() -> None
|
|
|
60
61
|
"request_id": "perm-2",
|
|
61
62
|
"interrupt_type": "permission",
|
|
62
63
|
"details": {
|
|
63
|
-
"permission":
|
|
64
|
+
"permission": "command_execution",
|
|
64
65
|
"patterns": ["/repo/.env"],
|
|
65
66
|
"always": [],
|
|
66
67
|
"display_message": "The command needs confirmation.",
|
|
@@ -68,6 +69,34 @@ def test_extract_permission_interrupt_promotes_reason_and_parsed_paths() -> None
|
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
|
|
72
|
+
def test_extract_permission_interrupt_derives_semantic_from_method_when_needed() -> None:
|
|
73
|
+
event = {
|
|
74
|
+
"type": "permission.asked",
|
|
75
|
+
"properties": {
|
|
76
|
+
"id": "perm-3",
|
|
77
|
+
"metadata": {
|
|
78
|
+
"method": "item/fileChange/requestApproval",
|
|
79
|
+
"raw": {
|
|
80
|
+
"request": {
|
|
81
|
+
"description": "Agent wants to update the patch.",
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
assert extract_interrupt_asked_event(event) == {
|
|
89
|
+
"request_id": "perm-3",
|
|
90
|
+
"interrupt_type": "permission",
|
|
91
|
+
"details": {
|
|
92
|
+
"permission": "file_change",
|
|
93
|
+
"patterns": [],
|
|
94
|
+
"always": [],
|
|
95
|
+
"display_message": "Agent wants to update the patch.",
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
71
100
|
def test_extract_question_interrupt_promotes_nested_questions() -> None:
|
|
72
101
|
event = {
|
|
73
102
|
"type": "question.asked",
|
|
@@ -25,6 +25,7 @@ from codex_a2a.jsonrpc.application import CodexSessionQueryJSONRPCApplication
|
|
|
25
25
|
from codex_a2a.jsonrpc.hooks import SessionGuardHooks
|
|
26
26
|
from codex_a2a.profile.runtime import build_runtime_profile
|
|
27
27
|
from codex_a2a.server.agent_card import build_agent_card
|
|
28
|
+
from codex_a2a.upstream.models import CodexRPCError
|
|
28
29
|
from tests.support.dummy_clients import DummySessionQueryCodexClient as DummyCodexClient
|
|
29
30
|
from tests.support.settings import make_settings
|
|
30
31
|
|
|
@@ -565,6 +566,39 @@ async def test_session_query_extension_maps_404_to_session_not_found(monkeypatch
|
|
|
565
566
|
assert payload["error"]["data"]["type"] == "SESSION_NOT_FOUND"
|
|
566
567
|
|
|
567
568
|
|
|
569
|
+
@pytest.mark.asyncio
|
|
570
|
+
async def test_session_query_extension_maps_thread_not_found_to_session_not_found(monkeypatch):
|
|
571
|
+
import codex_a2a.server.application as app_module
|
|
572
|
+
|
|
573
|
+
class NotFoundCodexClient(DummyCodexClient):
|
|
574
|
+
async def list_messages(self, session_id: str, *, params=None):
|
|
575
|
+
raise CodexRPCError(code=-32000, message=f"thread not found: {session_id}")
|
|
576
|
+
|
|
577
|
+
monkeypatch.setattr(app_module, "CodexClient", NotFoundCodexClient)
|
|
578
|
+
app = app_module.create_app(
|
|
579
|
+
make_settings(a2a_bearer_token="t-1", a2a_log_payloads=False, **_BASE_SETTINGS)
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
transport = httpx.ASGITransport(app=app)
|
|
583
|
+
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
|
584
|
+
headers = {"Authorization": "Bearer t-1"}
|
|
585
|
+
resp = await client.post(
|
|
586
|
+
"/",
|
|
587
|
+
headers=headers,
|
|
588
|
+
json={
|
|
589
|
+
"jsonrpc": "2.0",
|
|
590
|
+
"id": 3,
|
|
591
|
+
"method": "codex.sessions.messages.list",
|
|
592
|
+
"params": {"session_id": "s-rpc-404"},
|
|
593
|
+
},
|
|
594
|
+
)
|
|
595
|
+
payload = resp.json()
|
|
596
|
+
assert payload["jsonrpc"] == "2.0"
|
|
597
|
+
assert payload["id"] == 3
|
|
598
|
+
assert payload["error"]["code"] == -32001
|
|
599
|
+
assert payload["error"]["data"]["type"] == "SESSION_NOT_FOUND"
|
|
600
|
+
|
|
601
|
+
|
|
568
602
|
@pytest.mark.asyncio
|
|
569
603
|
async def test_session_query_extension_does_not_log_response_bodies(monkeypatch, caplog):
|
|
570
604
|
import codex_a2a.server.application as app_module
|
|
@@ -641,6 +675,48 @@ async def test_session_control_prompt_async_returns_turn_handle(monkeypatch):
|
|
|
641
675
|
}
|
|
642
676
|
|
|
643
677
|
|
|
678
|
+
@pytest.mark.asyncio
|
|
679
|
+
async def test_session_control_prompt_async_maps_thread_not_found_to_session_not_found(monkeypatch):
|
|
680
|
+
import codex_a2a.server.application as app_module
|
|
681
|
+
|
|
682
|
+
class NotFoundCodexClient(DummyCodexClient):
|
|
683
|
+
async def session_prompt_async(
|
|
684
|
+
self,
|
|
685
|
+
session_id: str,
|
|
686
|
+
request: dict[str, Any],
|
|
687
|
+
*,
|
|
688
|
+
directory: str | None = None,
|
|
689
|
+
execution_options=None,
|
|
690
|
+
):
|
|
691
|
+
del request, directory, execution_options
|
|
692
|
+
raise CodexRPCError(code=-32000, message=f"thread not found: {session_id}")
|
|
693
|
+
|
|
694
|
+
monkeypatch.setattr(app_module, "CodexClient", NotFoundCodexClient)
|
|
695
|
+
app = app_module.create_app(
|
|
696
|
+
make_settings(a2a_bearer_token="t-1", a2a_log_payloads=False, **_BASE_SETTINGS)
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
transport = httpx.ASGITransport(app=app)
|
|
700
|
+
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
|
701
|
+
headers = {"Authorization": "Bearer t-1"}
|
|
702
|
+
resp = await client.post(
|
|
703
|
+
"/",
|
|
704
|
+
headers=headers,
|
|
705
|
+
json={
|
|
706
|
+
"jsonrpc": "2.0",
|
|
707
|
+
"id": 22,
|
|
708
|
+
"method": "codex.sessions.prompt_async",
|
|
709
|
+
"params": {
|
|
710
|
+
"session_id": "s-rpc-404",
|
|
711
|
+
"request": {"parts": [{"type": "text", "text": "resume"}]},
|
|
712
|
+
},
|
|
713
|
+
},
|
|
714
|
+
)
|
|
715
|
+
payload = resp.json()
|
|
716
|
+
assert payload["error"]["code"] == -32001
|
|
717
|
+
assert payload["error"]["data"]["type"] == "SESSION_NOT_FOUND"
|
|
718
|
+
|
|
719
|
+
|
|
644
720
|
@pytest.mark.asyncio
|
|
645
721
|
async def test_session_control_prompt_async_accepts_rich_input(monkeypatch):
|
|
646
722
|
import codex_a2a.server.application as app_module
|
|
@@ -1163,6 +1239,7 @@ async def test_interrupt_recovery_extension_lists_identity_scoped_pending_reques
|
|
|
1163
1239
|
"properties": {
|
|
1164
1240
|
"id": "perm-1",
|
|
1165
1241
|
"sessionID": "ses-1",
|
|
1242
|
+
"permission": "command_execution",
|
|
1166
1243
|
"metadata": {
|
|
1167
1244
|
"method": "item/commandExecution/requestApproval",
|
|
1168
1245
|
"raw": {"patterns": ["/tmp/config.yml"]},
|
|
@@ -17,6 +17,7 @@ from codex_a2a.upstream.client import (
|
|
|
17
17
|
_PendingInterruptRequest,
|
|
18
18
|
_PendingRpcRequest,
|
|
19
19
|
)
|
|
20
|
+
from codex_a2a.upstream.models import CodexRPCError
|
|
20
21
|
from tests.support.fixtures import (
|
|
21
22
|
replay_codex_jsonrpc_line_fixture,
|
|
22
23
|
replay_codex_notification_fixture,
|
|
@@ -262,6 +263,7 @@ async def test_session_prompt_async_maps_rich_input_parts_to_turn_start() -> Non
|
|
|
262
263
|
return {"turn": {"id": "turn-42"}}
|
|
263
264
|
|
|
264
265
|
client._rpc_request = fake_rpc_request
|
|
266
|
+
client._conversation_facade._loaded_thread_ids.add("thr-1")
|
|
265
267
|
|
|
266
268
|
result = await client.session_prompt_async(
|
|
267
269
|
"thr-1",
|
|
@@ -304,6 +306,105 @@ async def test_session_prompt_async_maps_rich_input_parts_to_turn_start() -> Non
|
|
|
304
306
|
]
|
|
305
307
|
|
|
306
308
|
|
|
309
|
+
@pytest.mark.asyncio
|
|
310
|
+
async def test_session_prompt_async_resumes_unloaded_thread_before_turn_start() -> None:
|
|
311
|
+
client = CodexClient(
|
|
312
|
+
make_settings(
|
|
313
|
+
a2a_bearer_token="t-1",
|
|
314
|
+
codex_workspace_root="/safe",
|
|
315
|
+
codex_timeout=1.0,
|
|
316
|
+
codex_model_id="gpt-5.2-codex",
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
seen: list[tuple[str, dict | None]] = []
|
|
321
|
+
|
|
322
|
+
async def fake_rpc_request(method: str, params: dict | None = None):
|
|
323
|
+
seen.append((method, params))
|
|
324
|
+
if method == "thread/loaded/list":
|
|
325
|
+
return {"data": [], "nextCursor": None}
|
|
326
|
+
if method == "thread/resume":
|
|
327
|
+
return {"thread": {"id": "thr-1"}}
|
|
328
|
+
return {"turn": {"id": "turn-42"}}
|
|
329
|
+
|
|
330
|
+
client._rpc_request = fake_rpc_request
|
|
331
|
+
|
|
332
|
+
result = await client.session_prompt_async(
|
|
333
|
+
"thr-1",
|
|
334
|
+
{"parts": [{"type": "text", "text": "Use the app."}]},
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
assert result == {"ok": True, "session_id": "thr-1", "turn_id": "turn-42"}
|
|
338
|
+
assert seen == [
|
|
339
|
+
("thread/loaded/list", {}),
|
|
340
|
+
("thread/resume", {"threadId": "thr-1", "cwd": "/safe", "model": "gpt-5.2-codex"}),
|
|
341
|
+
(
|
|
342
|
+
"turn/start",
|
|
343
|
+
{
|
|
344
|
+
"threadId": "thr-1",
|
|
345
|
+
"input": [{"type": "text", "text": "Use the app.", "text_elements": []}],
|
|
346
|
+
"cwd": "/safe",
|
|
347
|
+
"model": "gpt-5.2-codex",
|
|
348
|
+
},
|
|
349
|
+
),
|
|
350
|
+
]
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
@pytest.mark.asyncio
|
|
354
|
+
async def test_session_prompt_async_retries_turn_start_after_thread_not_found() -> None:
|
|
355
|
+
client = CodexClient(
|
|
356
|
+
make_settings(
|
|
357
|
+
a2a_bearer_token="t-1",
|
|
358
|
+
codex_workspace_root="/safe",
|
|
359
|
+
codex_timeout=1.0,
|
|
360
|
+
)
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
seen: list[tuple[str, dict | None]] = []
|
|
364
|
+
turn_attempts = 0
|
|
365
|
+
|
|
366
|
+
async def fake_rpc_request(method: str, params: dict | None = None):
|
|
367
|
+
nonlocal turn_attempts
|
|
368
|
+
seen.append((method, params))
|
|
369
|
+
if method == "turn/start":
|
|
370
|
+
turn_attempts += 1
|
|
371
|
+
if turn_attempts == 1:
|
|
372
|
+
raise CodexRPCError(code=-32000, message="thread not found: thr-1")
|
|
373
|
+
return {"turn": {"id": "turn-43"}}
|
|
374
|
+
if method == "thread/resume":
|
|
375
|
+
return {"thread": {"id": "thr-1"}}
|
|
376
|
+
return {}
|
|
377
|
+
|
|
378
|
+
client._rpc_request = fake_rpc_request
|
|
379
|
+
client._conversation_facade._loaded_thread_ids.add("thr-1")
|
|
380
|
+
|
|
381
|
+
result = await client.session_prompt_async(
|
|
382
|
+
"thr-1",
|
|
383
|
+
{"parts": [{"type": "text", "text": "Retry after resume."}]},
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
assert result == {"ok": True, "session_id": "thr-1", "turn_id": "turn-43"}
|
|
387
|
+
assert seen == [
|
|
388
|
+
(
|
|
389
|
+
"turn/start",
|
|
390
|
+
{
|
|
391
|
+
"threadId": "thr-1",
|
|
392
|
+
"input": [{"type": "text", "text": "Retry after resume.", "text_elements": []}],
|
|
393
|
+
"cwd": "/safe",
|
|
394
|
+
},
|
|
395
|
+
),
|
|
396
|
+
("thread/resume", {"threadId": "thr-1", "cwd": "/safe"}),
|
|
397
|
+
(
|
|
398
|
+
"turn/start",
|
|
399
|
+
{
|
|
400
|
+
"threadId": "thr-1",
|
|
401
|
+
"input": [{"type": "text", "text": "Retry after resume.", "text_elements": []}],
|
|
402
|
+
"cwd": "/safe",
|
|
403
|
+
},
|
|
404
|
+
),
|
|
405
|
+
]
|
|
406
|
+
|
|
407
|
+
|
|
307
408
|
@pytest.mark.asyncio
|
|
308
409
|
async def test_exec_start_uses_interactive_command_exec_params() -> None:
|
|
309
410
|
client = CodexClient(
|
|
@@ -1375,6 +1476,7 @@ async def test_send_message_uses_structured_input_items_when_provided() -> None:
|
|
|
1375
1476
|
return {"turn": {"id": "turn-1"}}
|
|
1376
1477
|
|
|
1377
1478
|
client._rpc_request = fake_rpc_request
|
|
1479
|
+
client._conversation_facade._loaded_thread_ids.add("thr-1")
|
|
1378
1480
|
tracker = client._get_or_create_tracker("thr-1", "turn-1")
|
|
1379
1481
|
tracker.text_chunks.append("done")
|
|
1380
1482
|
tracker.completed.set()
|
|
@@ -1426,6 +1528,7 @@ async def test_send_message_request_execution_options_are_forwarded_to_turn_star
|
|
|
1426
1528
|
return {"turn": {"id": "turn-1"}}
|
|
1427
1529
|
|
|
1428
1530
|
client._rpc_request = fake_rpc_request
|
|
1531
|
+
client._conversation_facade._loaded_thread_ids.add("thr-1")
|
|
1429
1532
|
tracker = client._get_or_create_tracker("thr-1", "turn-1")
|
|
1430
1533
|
tracker.text_chunks.append("done")
|
|
1431
1534
|
tracker.completed.set()
|
|
@@ -1513,8 +1616,8 @@ async def test_permission_request_emits_shared_message_and_patterns() -> None:
|
|
|
1513
1616
|
assert props["id"] == "301"
|
|
1514
1617
|
assert props["sessionID"] == "thr-1"
|
|
1515
1618
|
assert props["display_message"] == "The command needs confirmation before continuing."
|
|
1619
|
+
assert props["permission"] == "command_execution"
|
|
1516
1620
|
assert props["patterns"] == ["/repo/.env"]
|
|
1517
|
-
assert "permission" not in props
|
|
1518
1621
|
assert "always" not in props
|
|
1519
1622
|
assert "reason" not in props
|
|
1520
1623
|
assert props["metadata"]["raw"]["parsedCmd"] == [
|
|
@@ -1676,6 +1779,7 @@ async def test_permission_request_promotes_nested_request_message() -> None:
|
|
|
1676
1779
|
assert len(events) == 1
|
|
1677
1780
|
props = events[0]["properties"]
|
|
1678
1781
|
assert props["display_message"] == "Agent wants to read the environment file."
|
|
1782
|
+
assert props["permission"] == "command_execution"
|
|
1679
1783
|
assert props["patterns"] == ["/repo/.env"]
|
|
1680
1784
|
assert "request" not in props
|
|
1681
1785
|
assert props["metadata"]["raw"]["request"] == {
|