imperal-sdk 4.2.10__tar.gz → 4.2.12__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.
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/CHANGELOG.md +71 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/PKG-INFO +1 -1
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/__init__.py +1 -1
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/context.py +102 -0
- imperal_sdk-4.2.12/src/imperal_sdk/http/client.py +86 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/ui/interactive.py +17 -3
- imperal_sdk-4.2.12/tests/test_context_background_task.py +76 -0
- imperal_sdk-4.2.12/tests/test_context_deliver_chat_message.py +96 -0
- imperal_sdk-4.2.12/tests/test_http_timeout_override.py +59 -0
- imperal_sdk-4.2.10/src/imperal_sdk/http/client.py +0 -51
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/.github/workflows/identity-contract.yml +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/.github/workflows/publish.yml +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/.github/workflows/test.yml +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/.gitignore +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/LICENSE +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/README.md +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/api_surface.json +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/pyproject.toml +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/.codebase-index-cache.pkl +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/ai/__init__.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/ai/client.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/auth/__init__.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/auth/client.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/auth/middleware.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/billing/__init__.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/billing/client.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/cache/__init__.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/cache/client.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/cache/protocol.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/chat/__init__.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/chat/action_result.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/chat/error_codes.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/chat/extension.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/chat/filters.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/chat/guards.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/chat/handler.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/chat/kernel_primitives.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/chat/narration.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/chat/narration_guard.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/chat/prompt.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/chat/refusal.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/chat/retry.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/cli/__init__.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/cli/main.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/config/__init__.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/config/client.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/db/__init__.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/db/client.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/errors.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/extension.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/extensions/__init__.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/extensions/client.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/http/__init__.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/manifest.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/manifest_schema.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/notify/__init__.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/notify/client.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/prompts/__init__.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/prompts/icnli_integrity_rules.txt +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/prompts/kernel_formatting_rule.txt +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/prompts/kernel_proactivity_rule.txt +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/protocols.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/rpc/__init__.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/rpc/codec.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/rpc/contract.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/runtime/__init__.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/runtime/executor.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/runtime/llm_provider.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/runtime/message_adapter.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/schemas/action_result.schema.json +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/schemas/balance_info.schema.json +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/schemas/chat_result.schema.json +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/schemas/completion_result.schema.json +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/schemas/document.schema.json +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/schemas/event.schema.json +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/schemas/file_info.schema.json +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/schemas/function_call.schema.json +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/schemas/http_response.schema.json +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/schemas/imperal.schema.json +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/schemas/limits_result.schema.json +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/schemas/subscription_info.schema.json +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/secrets/__init__.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/secrets/client.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/secrets/exceptions.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/secrets/panel_handler.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/secrets/spec.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/security/__init__.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/security/call_token.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/skeleton/__init__.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/skeleton/client.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/storage/__init__.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/storage/client.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/store/__init__.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/store/client.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/store/exceptions.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/testing/__init__.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/testing/mock_context.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/testing/mock_secrets.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/tools/__init__.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/tools/client.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/tools/generate_api_surface.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/tools/validate_identity_contract.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/types/__init__.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/types/action_result.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/types/chat_result.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/types/client_contracts.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/types/contracts.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/types/contributions.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/types/events.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/types/health.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/types/identity.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/types/models.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/types/pagination.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/types/store_contracts.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/ui/__init__.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/ui/actions.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/ui/base.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/ui/data.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/ui/display.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/ui/feedback.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/ui/graph.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/ui/input_components.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/ui/layout.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/ui/theme.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/validator.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/src/imperal_sdk/validator_v1_6_0.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/conftest.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/contracts/__init__.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/contracts/test_store_contracts.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/fixtures/openapi/auth-gateway.json +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/fixtures/openapi/registry.json +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/fixtures/openapi/sharelock-cases.json +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/rpc/__init__.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/rpc/test_codec.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/rpc/test_contract.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/runtime/__init__.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/runtime/test_llm_provider_config_store.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/runtime/test_llm_provider_ctx_injection.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/store/__init__.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/store/test_list_users_client.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/store/test_query_all_client.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_action_result_typed.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_as_user.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_auth.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_billing.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_cache_client.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_cache_model.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_call_token.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_chat_extension_deprecation.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_chat_filters.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_chat_guards.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_chat_guards_bleed.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_chat_prompt.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_chat_pydantic_retry.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_chat_result.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_cli.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_client_contracts.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_config_client.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_context.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_context_guards.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_contracts.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_contracts_live.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_contributions.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_document_contract.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_emits_decorator.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_error_codes.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_errors.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_event_schema_v2.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_events_health.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_extension.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_extension_v2.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_extensions_emit.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_handler_p2.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_id_shape_guard.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_identity_contract.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_imperal_schema_v2.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_kernel_primitives.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_manifest.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_manifest_roundtrip_gate.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_manifest_schema.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_manifest_v2_events.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_manifest_v2_other_sections.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_manifest_v2_webhooks.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_manifest_validator_v2.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_mock_context.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_models.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_narration_emission.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_narration_guard.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_openai_max_completion_tokens.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_pagination.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_panel_rendering_contract.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_panels.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_skeleton_decorator.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_spec_validation.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_tools_client.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_ui.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_ui_fileupload_enhanced.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_ui_html.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_ui_image_enhanced.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_ui_open.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_ui_theme.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_user.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_v7_emit_refusal.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_validator.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_validator_drift.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_validator_pep563.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_validator_v1_6_0_rules.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/test_write_arg_bleed.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/tools/__init__.py +0 -0
- {imperal_sdk-4.2.10 → imperal_sdk-4.2.12}/tests/tools/test_generate_api_surface.py +0 -0
|
@@ -2,6 +2,77 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `imperal-sdk` are documented here.
|
|
4
4
|
|
|
5
|
+
## 4.2.12 — 2026-05-14
|
|
6
|
+
|
|
7
|
+
**Feat: LONGRUN-V1 Session 1 — long-running operations primitives**
|
|
8
|
+
|
|
9
|
+
Three new SDK surfaces for ops that exceed the 30s `ctx.http` ceiling:
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`ctx.http.{get,post,put,patch,delete}(..., timeout=N)` per-call kwarg.**
|
|
14
|
+
Federal cap 180s (`I-LONGRUN-HTTP-CAP-180S`). Anything larger raises
|
|
15
|
+
`ValueError("ctx.http timeout {N}s exceeds federal cap (180s)...")` —
|
|
16
|
+
use `ctx.background_task()` for longer ops.
|
|
17
|
+
|
|
18
|
+
- **`ctx.background_task(coro, *, long_running=False, name="") -> str`**
|
|
19
|
+
— explicit opt-in for the kernel's auto-promote path. Coro runs detached
|
|
20
|
+
via `asyncio.create_task`; kernel auto-delivers its returned
|
|
21
|
+
`ActionResult` as a fresh bot message to the user's chat when done.
|
|
22
|
+
`long_running=True` raises the 180s cap to 1800s
|
|
23
|
+
(`LONG_RUNNING_TASK_S`). Returns task_id immediately.
|
|
24
|
+
|
|
25
|
+
- **`ctx.deliver_chat_message(text, *, msg_type="response", refresh_panels=None)`**
|
|
26
|
+
— public API for extension-initiated bot turn injection. Posts to
|
|
27
|
+
auth-gw `/v1/internal/chat/inject`. Text truncated to 64KB with marker.
|
|
28
|
+
`msg_type` ∈ `{response, system, tool_result}`.
|
|
29
|
+
|
|
30
|
+
### Federal invariants (new)
|
|
31
|
+
|
|
32
|
+
- `I-LONGRUN-HTTP-CAP-180S` — per-call timeout federal cap
|
|
33
|
+
- `I-LONGRUN-BG-CORO-RETURNS-ACTIONRESULT` — coro contract enforced
|
|
34
|
+
kernel-side via `_explicit_background_task_completion` isinstance
|
|
35
|
+
check; violation writes critical audit row + delivers fallback error.
|
|
36
|
+
- `I-LONGRUN-BG-USER-SCOPED` — task bound to `(ext_id, user_id)` at
|
|
37
|
+
creation (existing `util/task_manager.py` invariant).
|
|
38
|
+
- `I-LONGRUN-CHAT-INJECT-USER-SCOPED` — `X-Acting-User` header must
|
|
39
|
+
match `body.user_id` (auth-gw 403 on mismatch).
|
|
40
|
+
- `I-LONGRUN-CHAT-INJECT-AUDIT-EVERY` — every inject writes an
|
|
41
|
+
`action_ledger` row through the `write_audit` chokepoint.
|
|
42
|
+
|
|
43
|
+
### Spec
|
|
44
|
+
|
|
45
|
+
`superpowers/specs/2026-05-13-longrun-v1-design.md`. Sessions 2 (Sharelock
|
|
46
|
+
canary migration) and 3 (docs + observability) ship after 24h soak.
|
|
47
|
+
|
|
48
|
+
### Migration
|
|
49
|
+
|
|
50
|
+
None required — all additions are additive opt-in. Existing extensions
|
|
51
|
+
work unchanged.
|
|
52
|
+
|
|
53
|
+
## 4.2.11 — 2026-05-13
|
|
54
|
+
|
|
55
|
+
**Fix: `ui.Link(text=...)` no longer breaks panel render**
|
|
56
|
+
|
|
57
|
+
`ui.Link` now accepts the visible text via either `label=` (canonical) or
|
|
58
|
+
`text=` (alias matching the HTML/JSX `<a>text</a>` mental model). Before
|
|
59
|
+
v4.2.11, passing `text=` raised `TypeError: Link() got an unexpected
|
|
60
|
+
keyword argument 'text'` at panel-render time, which killed the right
|
|
61
|
+
panel for any extension that used the natural-looking kwarg.
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
# All three are now equivalent
|
|
65
|
+
ui.Link("Read docs", href="https://docs.imperal.io") # positional
|
|
66
|
+
ui.Link(label="Read docs", href="https://docs.imperal.io") # canonical kwarg
|
|
67
|
+
ui.Link(text="Read docs", href="https://docs.imperal.io") # alias kwarg
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Calling `ui.Link()` with neither `label` nor `text` raises a clear
|
|
71
|
+
`TypeError("ui.Link requires a 'label' (or 'text' alias)")` instead of
|
|
72
|
+
silently rendering an empty anchor. `label` wins if both are passed.
|
|
73
|
+
|
|
74
|
+
No migration required — existing `label=` callers are unchanged.
|
|
75
|
+
|
|
5
76
|
## 4.2.10 — 2026-05-13
|
|
6
77
|
|
|
7
78
|
**Feat: `@chat.function` default `chain_callable=True` for ALL action_types (was write/destructive only)**
|
|
@@ -4,6 +4,8 @@ from __future__ import annotations
|
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
5
|
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
|
6
6
|
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
7
9
|
from imperal_sdk.types.identity import UserContext, TenantContext
|
|
8
10
|
|
|
9
11
|
if TYPE_CHECKING:
|
|
@@ -290,6 +292,106 @@ class Context:
|
|
|
290
292
|
if cb:
|
|
291
293
|
await cb(percent, message)
|
|
292
294
|
|
|
295
|
+
async def background_task(
|
|
296
|
+
self,
|
|
297
|
+
coro,
|
|
298
|
+
*,
|
|
299
|
+
long_running: bool = False,
|
|
300
|
+
name: str = "",
|
|
301
|
+
) -> str:
|
|
302
|
+
"""Spawn a coroutine in the background; return task_id immediately.
|
|
303
|
+
|
|
304
|
+
The kernel detaches ``coro`` via ``asyncio.create_task`` and tracks
|
|
305
|
+
it as an explicit-opt-in background task (federal task lifecycle in
|
|
306
|
+
``util/task_manager.py``). When ``coro`` completes, the kernel
|
|
307
|
+
auto-delivers its returned ``ActionResult`` as a fresh bot message
|
|
308
|
+
in the user's chat via the existing ``_deliver_to_chat`` pipeline.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
coro: A coroutine returning ``ActionResult``. Required.
|
|
312
|
+
long_running: ``False`` (default) → 180s federal cap.
|
|
313
|
+
``True`` → 1800s cap (``LONG_RUNNING_TASK_S``).
|
|
314
|
+
name: Human-readable name for UI/audit (default ``"background"``).
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
task_id (str) — federal Redis key ``imperal:task:{task_id}``.
|
|
318
|
+
|
|
319
|
+
Raises:
|
|
320
|
+
RuntimeError: if ``ctx`` lacks kernel-injected spawn hook
|
|
321
|
+
(e.g. dev mode or non-extension dispatch context).
|
|
322
|
+
|
|
323
|
+
Federal: ``I-LONGRUN-BG-CORO-RETURNS-ACTIONRESULT`` — ``coro`` MUST
|
|
324
|
+
return ``ActionResult``; non-ActionResult return triggers a critical
|
|
325
|
+
audit row and delivers a fallback error to chat.
|
|
326
|
+
"""
|
|
327
|
+
spawn = getattr(self, "_background_task_spawn", None)
|
|
328
|
+
if spawn is None:
|
|
329
|
+
raise RuntimeError(
|
|
330
|
+
"ctx.background_task not available in this context — the "
|
|
331
|
+
"Context was constructed without a kernel-injected spawn "
|
|
332
|
+
"hook. This is typically a test harness or dev-mode issue."
|
|
333
|
+
)
|
|
334
|
+
return await spawn(coro, long_running=long_running, name=name or "background")
|
|
335
|
+
|
|
336
|
+
async def deliver_chat_message(
|
|
337
|
+
self,
|
|
338
|
+
text: str,
|
|
339
|
+
*,
|
|
340
|
+
msg_type: str = "response",
|
|
341
|
+
refresh_panels: list[str] | None = None,
|
|
342
|
+
) -> None:
|
|
343
|
+
"""Inject a bot message into user's chat at any time.
|
|
344
|
+
|
|
345
|
+
Mirrors the existing kernel auto-promote chat-injection path
|
|
346
|
+
(``pipeline/task_delivery.py:_deliver_to_chat``) but is callable
|
|
347
|
+
explicitly from extension code. Use cases:
|
|
348
|
+
- Background task multi-stage announcements before final result
|
|
349
|
+
- Webhook acknowledgements ("Spotify connected!")
|
|
350
|
+
- Any one-off bot message that skeleton-alert polling can't model
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
text: Bot message body (Markdown). Truncated to 64KB if larger.
|
|
354
|
+
msg_type: ``"response"`` (default — bot turn),
|
|
355
|
+
``"system"`` (italicized notice),
|
|
356
|
+
``"tool_result"`` (formatted as tool execution).
|
|
357
|
+
refresh_panels: optional list of panel_ids to re-render after.
|
|
358
|
+
|
|
359
|
+
Federal: ``I-LONGRUN-CHAT-INJECT-USER-SCOPED`` — inject scoped to
|
|
360
|
+
``(ext_id, user_id)``; cross-user inject returns 403.
|
|
361
|
+
``I-LONGRUN-CHAT-INJECT-AUDIT-EVERY`` — every inject writes an
|
|
362
|
+
audit row in ``action_ledger``.
|
|
363
|
+
"""
|
|
364
|
+
MAX_BYTES = 64 * 1024
|
|
365
|
+
if len(text.encode("utf-8")) > MAX_BYTES:
|
|
366
|
+
text = text.encode("utf-8")[:MAX_BYTES].decode("utf-8", errors="ignore") + "...(truncated)"
|
|
367
|
+
|
|
368
|
+
ext_id = getattr(self, "_extension_id", None)
|
|
369
|
+
user_id = getattr(self, "_user_id", None) or getattr(self.user, "imperal_id", None)
|
|
370
|
+
gateway_url = getattr(self, "_gateway_url", None)
|
|
371
|
+
service_token = getattr(self, "_service_token", None)
|
|
372
|
+
if not all([ext_id, user_id, gateway_url, service_token]):
|
|
373
|
+
raise RuntimeError(
|
|
374
|
+
"ctx.deliver_chat_message requires kernel-injected (ext_id, "
|
|
375
|
+
"user_id, gateway_url, service_token). Not available in this "
|
|
376
|
+
"context (likely a test harness or dev-mode dispatch)."
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
url = f"{gateway_url.rstrip('/')}/v1/internal/chat/inject"
|
|
380
|
+
body = {
|
|
381
|
+
"ext_id": ext_id,
|
|
382
|
+
"user_id": user_id,
|
|
383
|
+
"text": text,
|
|
384
|
+
"msg_type": msg_type,
|
|
385
|
+
}
|
|
386
|
+
if refresh_panels:
|
|
387
|
+
body["refresh_panels"] = refresh_panels
|
|
388
|
+
headers = {
|
|
389
|
+
"X-Service-Token": service_token,
|
|
390
|
+
"X-Acting-User": user_id,
|
|
391
|
+
}
|
|
392
|
+
async with httpx.AsyncClient(timeout=10.0) as c:
|
|
393
|
+
await c.post(url, json=body, headers=headers)
|
|
394
|
+
|
|
293
395
|
async def log(self, message: str, level: str = "info") -> None:
|
|
294
396
|
"""Structured log visible in extension dashboard."""
|
|
295
397
|
import logging
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Copyright (c) 2026 Imperal, Inc., Valentin Scerbacov, and contributors
|
|
2
|
+
# Licensed under the AGPL-3.0 License. See LICENSE file for details.
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from imperal_sdk.types.models import HTTPResponse
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _wrap(resp: httpx.Response) -> HTTPResponse:
|
|
10
|
+
"""Convert httpx.Response to typed HTTPResponse."""
|
|
11
|
+
content_type = resp.headers.get("content-type", "")
|
|
12
|
+
if "json" in content_type:
|
|
13
|
+
try:
|
|
14
|
+
body = resp.json()
|
|
15
|
+
except Exception:
|
|
16
|
+
body = resp.text
|
|
17
|
+
elif resp.content:
|
|
18
|
+
try:
|
|
19
|
+
body = resp.text
|
|
20
|
+
except Exception:
|
|
21
|
+
body = resp.content
|
|
22
|
+
else:
|
|
23
|
+
body = ""
|
|
24
|
+
return HTTPResponse(status_code=resp.status_code, body=body, headers=dict(resp.headers))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class HTTPClient:
|
|
28
|
+
"""Federal HTTP client.
|
|
29
|
+
|
|
30
|
+
Per-call ``timeout=`` kwarg overrides the instance default. Federal
|
|
31
|
+
cap is 180 seconds (``I-LONGRUN-HTTP-CAP-180S``) — anything longer
|
|
32
|
+
must use :func:`ctx.background_task` instead.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
timeout: float = 30,
|
|
38
|
+
max_redirects: int = 5,
|
|
39
|
+
max_timeout: float = 180,
|
|
40
|
+
):
|
|
41
|
+
self._timeout = timeout
|
|
42
|
+
self._max_timeout = max_timeout
|
|
43
|
+
self._max_redirects = max_redirects
|
|
44
|
+
|
|
45
|
+
def _resolve_timeout(self, override: float | None) -> float:
|
|
46
|
+
if override is None:
|
|
47
|
+
return self._timeout
|
|
48
|
+
if override <= 0:
|
|
49
|
+
raise ValueError(f"ctx.http timeout must be > 0; got {override}")
|
|
50
|
+
if override > self._max_timeout:
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f"ctx.http timeout {override}s exceeds federal cap "
|
|
53
|
+
f"({self._max_timeout}s). For >180s use ctx.background_task() "
|
|
54
|
+
f"— see concepts/long-running-operations."
|
|
55
|
+
)
|
|
56
|
+
return override
|
|
57
|
+
|
|
58
|
+
async def get(self, url: str, *, timeout: float | None = None, **kwargs) -> HTTPResponse:
|
|
59
|
+
t = self._resolve_timeout(timeout)
|
|
60
|
+
async with httpx.AsyncClient(timeout=t, follow_redirects=True,
|
|
61
|
+
max_redirects=self._max_redirects) as c:
|
|
62
|
+
return _wrap(await c.get(url, **kwargs))
|
|
63
|
+
|
|
64
|
+
async def post(self, url: str, *, timeout: float | None = None, **kwargs) -> HTTPResponse:
|
|
65
|
+
t = self._resolve_timeout(timeout)
|
|
66
|
+
async with httpx.AsyncClient(timeout=t, follow_redirects=True,
|
|
67
|
+
max_redirects=self._max_redirects) as c:
|
|
68
|
+
return _wrap(await c.post(url, **kwargs))
|
|
69
|
+
|
|
70
|
+
async def put(self, url: str, *, timeout: float | None = None, **kwargs) -> HTTPResponse:
|
|
71
|
+
t = self._resolve_timeout(timeout)
|
|
72
|
+
async with httpx.AsyncClient(timeout=t, follow_redirects=True,
|
|
73
|
+
max_redirects=self._max_redirects) as c:
|
|
74
|
+
return _wrap(await c.put(url, **kwargs))
|
|
75
|
+
|
|
76
|
+
async def patch(self, url: str, *, timeout: float | None = None, **kwargs) -> HTTPResponse:
|
|
77
|
+
t = self._resolve_timeout(timeout)
|
|
78
|
+
async with httpx.AsyncClient(timeout=t, follow_redirects=True,
|
|
79
|
+
max_redirects=self._max_redirects) as c:
|
|
80
|
+
return _wrap(await c.patch(url, **kwargs))
|
|
81
|
+
|
|
82
|
+
async def delete(self, url: str, *, timeout: float | None = None, **kwargs) -> HTTPResponse:
|
|
83
|
+
t = self._resolve_timeout(timeout)
|
|
84
|
+
async with httpx.AsyncClient(timeout=t, follow_redirects=True,
|
|
85
|
+
max_redirects=self._max_redirects) as c:
|
|
86
|
+
return _wrap(await c.delete(url, **kwargs))
|
|
@@ -73,9 +73,23 @@ def Tooltip(content: str, children: UINode | None = None) -> UINode:
|
|
|
73
73
|
return UINode(type="Tooltip", props=props)
|
|
74
74
|
|
|
75
75
|
|
|
76
|
-
def Link(
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
def Link(
|
|
77
|
+
label: str = "",
|
|
78
|
+
href: str = "",
|
|
79
|
+
on_click: UIAction | None = None,
|
|
80
|
+
*,
|
|
81
|
+
text: str = "",
|
|
82
|
+
) -> UINode:
|
|
83
|
+
"""Hyperlink — navigates via href or fires on_click action.
|
|
84
|
+
|
|
85
|
+
The visible text can be passed as either ``label`` (canonical) or
|
|
86
|
+
``text`` (alias matching the HTML/JSX `<a>text</a>` mental model).
|
|
87
|
+
Exactly one must be provided; ``label`` wins if both are set.
|
|
88
|
+
"""
|
|
89
|
+
resolved = label or text
|
|
90
|
+
if not resolved:
|
|
91
|
+
raise TypeError("ui.Link requires a 'label' (or 'text' alias)")
|
|
92
|
+
props: dict[str, Any] = {"label": resolved}
|
|
79
93
|
if href: props["href"] = href
|
|
80
94
|
if on_click: props["on_click"] = on_click
|
|
81
95
|
return UINode(type="Link", props=props)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# tests/test_context_background_task.py
|
|
2
|
+
"""Component B — ctx.background_task() public API surface tests.
|
|
3
|
+
|
|
4
|
+
Verifies the SDK contract:
|
|
5
|
+
- Returns a task_id (str) immediately.
|
|
6
|
+
- Spawns coroutine via injected kernel hook.
|
|
7
|
+
- Raises RuntimeError if no kernel hook present (e.g. dev mode).
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from imperal_sdk import ActionResult
|
|
13
|
+
from imperal_sdk.context import Context
|
|
14
|
+
from imperal_sdk.types.identity import UserContext
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _make_ctx() -> Context:
|
|
18
|
+
return Context(user=UserContext(
|
|
19
|
+
imperal_id="imp_u_test", email="test@test.com", tenant_id="t", role="user",
|
|
20
|
+
))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.mark.asyncio
|
|
24
|
+
async def test_background_task_returns_task_id_str():
|
|
25
|
+
captured: dict = {}
|
|
26
|
+
|
|
27
|
+
async def _spawn_hook(coro, *, long_running, name):
|
|
28
|
+
captured["coro_present"] = coro is not None
|
|
29
|
+
captured["long_running"] = long_running
|
|
30
|
+
captured["name"] = name
|
|
31
|
+
coro.close()
|
|
32
|
+
return "task_xyz123"
|
|
33
|
+
|
|
34
|
+
ctx = _make_ctx()
|
|
35
|
+
ctx._background_task_spawn = _spawn_hook # type: ignore[attr-defined]
|
|
36
|
+
|
|
37
|
+
async def _work():
|
|
38
|
+
return ActionResult.success(data={"ok": True})
|
|
39
|
+
|
|
40
|
+
tid = await ctx.background_task(_work(), long_running=True, name="my-task")
|
|
41
|
+
assert tid == "task_xyz123"
|
|
42
|
+
assert captured["long_running"] is True
|
|
43
|
+
assert captured["name"] == "my-task"
|
|
44
|
+
assert captured["coro_present"] is True
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@pytest.mark.asyncio
|
|
48
|
+
async def test_background_task_raises_without_kernel_hook():
|
|
49
|
+
ctx = _make_ctx()
|
|
50
|
+
|
|
51
|
+
async def _work():
|
|
52
|
+
return ActionResult.success(data={"ok": True})
|
|
53
|
+
|
|
54
|
+
coro = _work()
|
|
55
|
+
with pytest.raises(RuntimeError, match="ctx.background_task not available"):
|
|
56
|
+
await ctx.background_task(coro)
|
|
57
|
+
coro.close()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.mark.asyncio
|
|
61
|
+
async def test_background_task_default_long_running_false():
|
|
62
|
+
captured: dict = {}
|
|
63
|
+
|
|
64
|
+
async def _spawn_hook(coro, *, long_running, name):
|
|
65
|
+
captured["long_running"] = long_running
|
|
66
|
+
coro.close()
|
|
67
|
+
return "task_y"
|
|
68
|
+
|
|
69
|
+
ctx = _make_ctx()
|
|
70
|
+
ctx._background_task_spawn = _spawn_hook # type: ignore[attr-defined]
|
|
71
|
+
|
|
72
|
+
async def _work():
|
|
73
|
+
return ActionResult.success(data={"ok": True})
|
|
74
|
+
|
|
75
|
+
await ctx.background_task(_work())
|
|
76
|
+
assert captured["long_running"] is False
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# tests/test_context_deliver_chat_message.py
|
|
2
|
+
"""Component C — ctx.deliver_chat_message() public API surface tests.
|
|
3
|
+
|
|
4
|
+
Verifies the SDK contract:
|
|
5
|
+
- POSTs to auth-gw /v1/internal/chat/inject with correct payload + headers.
|
|
6
|
+
- Truncates text > 64KB with appended marker.
|
|
7
|
+
- Respects msg_type literal values.
|
|
8
|
+
- Forwards optional refresh_panels list.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
import pytest
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
from imperal_sdk.context import Context
|
|
15
|
+
from imperal_sdk.types.identity import UserContext
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _make_ctx(*, ext_id="my-ext", user_id="imp_u_abc",
|
|
19
|
+
gw="http://auth-gw:8085", svc="tok_xxx") -> Context:
|
|
20
|
+
ctx = Context(user=UserContext(
|
|
21
|
+
imperal_id=user_id, email="u@t.com", tenant_id="t", role="user",
|
|
22
|
+
))
|
|
23
|
+
ctx._extension_id = ext_id
|
|
24
|
+
ctx._user_id = user_id # type: ignore[attr-defined]
|
|
25
|
+
ctx._gateway_url = gw
|
|
26
|
+
ctx._service_token = svc
|
|
27
|
+
return ctx
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.mark.asyncio
|
|
31
|
+
async def test_deliver_chat_message_posts_to_auth_gw(monkeypatch):
|
|
32
|
+
captured: dict = {}
|
|
33
|
+
|
|
34
|
+
class _MockAsyncClient:
|
|
35
|
+
def __init__(self, *, timeout, **kw):
|
|
36
|
+
captured["timeout"] = timeout
|
|
37
|
+
async def __aenter__(self): return self
|
|
38
|
+
async def __aexit__(self, *a): pass
|
|
39
|
+
async def post(self, url, json=None, headers=None):
|
|
40
|
+
captured["url"] = url
|
|
41
|
+
captured["json"] = json
|
|
42
|
+
captured["headers"] = headers
|
|
43
|
+
return httpx.Response(200, json={"ok": True})
|
|
44
|
+
|
|
45
|
+
monkeypatch.setattr("imperal_sdk.context.httpx.AsyncClient", _MockAsyncClient)
|
|
46
|
+
ctx = _make_ctx()
|
|
47
|
+
|
|
48
|
+
await ctx.deliver_chat_message("Hello!")
|
|
49
|
+
assert captured["url"].endswith("/v1/internal/chat/inject")
|
|
50
|
+
assert captured["json"]["text"] == "Hello!"
|
|
51
|
+
assert captured["json"]["msg_type"] == "response"
|
|
52
|
+
assert captured["json"]["ext_id"] == "my-ext"
|
|
53
|
+
assert captured["json"]["user_id"] == "imp_u_abc"
|
|
54
|
+
assert captured["headers"]["X-Service-Token"] == "tok_xxx"
|
|
55
|
+
assert captured["headers"]["X-Acting-User"] == "imp_u_abc"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@pytest.mark.asyncio
|
|
59
|
+
async def test_deliver_chat_message_truncates_oversized_text(monkeypatch):
|
|
60
|
+
captured: dict = {}
|
|
61
|
+
|
|
62
|
+
class _MockAsyncClient:
|
|
63
|
+
def __init__(self, *, timeout, **kw): pass
|
|
64
|
+
async def __aenter__(self): return self
|
|
65
|
+
async def __aexit__(self, *a): pass
|
|
66
|
+
async def post(self, url, json=None, headers=None):
|
|
67
|
+
captured["text"] = json["text"]
|
|
68
|
+
return httpx.Response(200, json={"ok": True})
|
|
69
|
+
|
|
70
|
+
monkeypatch.setattr("imperal_sdk.context.httpx.AsyncClient", _MockAsyncClient)
|
|
71
|
+
ctx = _make_ctx()
|
|
72
|
+
|
|
73
|
+
big = "x" * 70000
|
|
74
|
+
await ctx.deliver_chat_message(big)
|
|
75
|
+
assert len(captured["text"]) <= 64 * 1024 + len("...(truncated)")
|
|
76
|
+
assert captured["text"].endswith("...(truncated)")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@pytest.mark.asyncio
|
|
80
|
+
async def test_deliver_chat_message_with_refresh_panels(monkeypatch):
|
|
81
|
+
captured: dict = {}
|
|
82
|
+
|
|
83
|
+
class _MockAsyncClient:
|
|
84
|
+
def __init__(self, *, timeout, **kw): pass
|
|
85
|
+
async def __aenter__(self): return self
|
|
86
|
+
async def __aexit__(self, *a): pass
|
|
87
|
+
async def post(self, url, json=None, headers=None):
|
|
88
|
+
captured["json"] = json
|
|
89
|
+
return httpx.Response(200, json={"ok": True})
|
|
90
|
+
|
|
91
|
+
monkeypatch.setattr("imperal_sdk.context.httpx.AsyncClient", _MockAsyncClient)
|
|
92
|
+
ctx = _make_ctx()
|
|
93
|
+
|
|
94
|
+
await ctx.deliver_chat_message("Done!", msg_type="tool_result", refresh_panels=["editor"])
|
|
95
|
+
assert captured["json"]["msg_type"] == "tool_result"
|
|
96
|
+
assert captured["json"]["refresh_panels"] == ["editor"]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# tests/test_http_timeout_override.py
|
|
2
|
+
import pytest
|
|
3
|
+
import httpx
|
|
4
|
+
from imperal_sdk.http.client import HTTPClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.mark.asyncio
|
|
8
|
+
async def test_post_accepts_per_call_timeout_override(monkeypatch):
|
|
9
|
+
"""Component A: timeout= kwarg overrides instance default."""
|
|
10
|
+
captured: dict = {}
|
|
11
|
+
|
|
12
|
+
class _MockAsyncClient:
|
|
13
|
+
def __init__(self, *, timeout, follow_redirects, max_redirects):
|
|
14
|
+
captured["timeout"] = timeout
|
|
15
|
+
async def __aenter__(self): return self
|
|
16
|
+
async def __aexit__(self, *a): pass
|
|
17
|
+
async def post(self, url, **kw):
|
|
18
|
+
return httpx.Response(200, json={"ok": True})
|
|
19
|
+
|
|
20
|
+
monkeypatch.setattr("imperal_sdk.http.client.httpx.AsyncClient", _MockAsyncClient)
|
|
21
|
+
c = HTTPClient(timeout=30)
|
|
22
|
+
await c.post("https://example.com", timeout=120)
|
|
23
|
+
assert captured["timeout"] == 120
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.mark.asyncio
|
|
27
|
+
async def test_post_default_when_no_override(monkeypatch):
|
|
28
|
+
"""No timeout= kwarg → uses instance default."""
|
|
29
|
+
captured: dict = {}
|
|
30
|
+
|
|
31
|
+
class _MockAsyncClient:
|
|
32
|
+
def __init__(self, *, timeout, follow_redirects, max_redirects):
|
|
33
|
+
captured["timeout"] = timeout
|
|
34
|
+
async def __aenter__(self): return self
|
|
35
|
+
async def __aexit__(self, *a): pass
|
|
36
|
+
async def post(self, url, **kw):
|
|
37
|
+
return httpx.Response(200, json={"ok": True})
|
|
38
|
+
|
|
39
|
+
monkeypatch.setattr("imperal_sdk.http.client.httpx.AsyncClient", _MockAsyncClient)
|
|
40
|
+
c = HTTPClient(timeout=30)
|
|
41
|
+
await c.post("https://example.com")
|
|
42
|
+
assert captured["timeout"] == 30
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.mark.asyncio
|
|
46
|
+
async def test_post_rejects_over_180s_cap():
|
|
47
|
+
"""Component A: federal cap 180s — anything more raises ValueError."""
|
|
48
|
+
c = HTTPClient(timeout=30)
|
|
49
|
+
with pytest.raises(ValueError, match="exceeds federal cap"):
|
|
50
|
+
await c.post("https://example.com", timeout=300)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@pytest.mark.asyncio
|
|
54
|
+
async def test_post_rejects_zero_or_negative_timeout():
|
|
55
|
+
c = HTTPClient(timeout=30)
|
|
56
|
+
with pytest.raises(ValueError, match="must be > 0"):
|
|
57
|
+
await c.post("https://example.com", timeout=0)
|
|
58
|
+
with pytest.raises(ValueError, match="must be > 0"):
|
|
59
|
+
await c.post("https://example.com", timeout=-5)
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2026 Imperal, Inc., Valentin Scerbacov, and contributors
|
|
2
|
-
# Licensed under the AGPL-3.0 License. See LICENSE file for details.
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
import httpx
|
|
5
|
-
|
|
6
|
-
from imperal_sdk.types.models import HTTPResponse
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def _wrap(resp: httpx.Response) -> HTTPResponse:
|
|
10
|
-
"""Convert httpx.Response to typed HTTPResponse."""
|
|
11
|
-
content_type = resp.headers.get("content-type", "")
|
|
12
|
-
if "json" in content_type:
|
|
13
|
-
try:
|
|
14
|
-
body = resp.json()
|
|
15
|
-
except Exception:
|
|
16
|
-
body = resp.text
|
|
17
|
-
elif resp.content:
|
|
18
|
-
# Return text for text/* or small responses; bytes otherwise
|
|
19
|
-
try:
|
|
20
|
-
body = resp.text
|
|
21
|
-
except Exception:
|
|
22
|
-
body = resp.content
|
|
23
|
-
else:
|
|
24
|
-
body = ""
|
|
25
|
-
return HTTPResponse(status_code=resp.status_code, body=body, headers=dict(resp.headers))
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class HTTPClient:
|
|
29
|
-
def __init__(self, timeout: int = 30, max_redirects: int = 5):
|
|
30
|
-
self._timeout = timeout
|
|
31
|
-
self._max_redirects = max_redirects
|
|
32
|
-
|
|
33
|
-
async def get(self, url: str, **kwargs) -> HTTPResponse:
|
|
34
|
-
async with httpx.AsyncClient(timeout=self._timeout, follow_redirects=True, max_redirects=self._max_redirects) as client:
|
|
35
|
-
return _wrap(await client.get(url, **kwargs))
|
|
36
|
-
|
|
37
|
-
async def post(self, url: str, **kwargs) -> HTTPResponse:
|
|
38
|
-
async with httpx.AsyncClient(timeout=self._timeout, follow_redirects=True, max_redirects=self._max_redirects) as client:
|
|
39
|
-
return _wrap(await client.post(url, **kwargs))
|
|
40
|
-
|
|
41
|
-
async def put(self, url: str, **kwargs) -> HTTPResponse:
|
|
42
|
-
async with httpx.AsyncClient(timeout=self._timeout, follow_redirects=True, max_redirects=self._max_redirects) as client:
|
|
43
|
-
return _wrap(await client.put(url, **kwargs))
|
|
44
|
-
|
|
45
|
-
async def patch(self, url: str, **kwargs) -> HTTPResponse:
|
|
46
|
-
async with httpx.AsyncClient(timeout=self._timeout, follow_redirects=True, max_redirects=self._max_redirects) as client:
|
|
47
|
-
return _wrap(await client.patch(url, **kwargs))
|
|
48
|
-
|
|
49
|
-
async def delete(self, url: str, **kwargs) -> HTTPResponse:
|
|
50
|
-
async with httpx.AsyncClient(timeout=self._timeout, follow_redirects=True, max_redirects=self._max_redirects) as client:
|
|
51
|
-
return _wrap(await client.delete(url, **kwargs))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|