imperal-sdk 4.2.14__tar.gz → 4.2.16__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.14 → imperal_sdk-4.2.16}/CHANGELOG.md +62 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/PKG-INFO +1 -1
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/__init__.py +1 -1
- imperal_sdk-4.2.16/src/imperal_sdk/chat/exceptions.py +14 -0
- imperal_sdk-4.2.16/src/imperal_sdk/chat/execution.py +330 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/chat/guards.py +102 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/chat/handler.py +11 -294
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_handler_p2.py +51 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/.github/workflows/identity-contract.yml +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/.github/workflows/publish.yml +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/.github/workflows/test.yml +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/.gitignore +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/LICENSE +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/README.md +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/api_surface.json +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/pyproject.toml +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/.codebase-index-cache.pkl +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/ai/__init__.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/ai/client.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/auth/__init__.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/auth/client.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/auth/middleware.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/billing/__init__.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/billing/client.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/cache/__init__.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/cache/client.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/cache/protocol.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/chat/__init__.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/chat/action_result.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/chat/error_codes.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/chat/extension.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/chat/filters.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/chat/kernel_primitives.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/chat/narration.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/chat/narration_guard.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/chat/prompt.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/chat/refusal.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/chat/retry.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/cli/__init__.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/cli/main.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/config/__init__.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/config/client.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/context.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/db/__init__.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/db/client.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/errors.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/extension.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/extensions/__init__.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/extensions/client.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/http/__init__.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/http/client.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/manifest.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/manifest_schema.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/notify/__init__.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/notify/client.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/prompts/__init__.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/prompts/icnli_integrity_rules.txt +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/prompts/kernel_formatting_rule.txt +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/prompts/kernel_proactivity_rule.txt +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/protocols.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/rpc/__init__.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/rpc/codec.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/rpc/contract.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/runtime/__init__.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/runtime/executor.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/runtime/llm_provider.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/runtime/message_adapter.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/schemas/action_result.schema.json +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/schemas/balance_info.schema.json +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/schemas/chat_result.schema.json +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/schemas/completion_result.schema.json +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/schemas/document.schema.json +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/schemas/event.schema.json +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/schemas/file_info.schema.json +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/schemas/function_call.schema.json +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/schemas/http_response.schema.json +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/schemas/imperal.schema.json +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/schemas/limits_result.schema.json +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/schemas/subscription_info.schema.json +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/secrets/__init__.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/secrets/client.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/secrets/exceptions.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/secrets/panel_handler.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/secrets/spec.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/security/__init__.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/security/call_token.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/skeleton/__init__.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/skeleton/client.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/storage/__init__.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/storage/client.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/store/__init__.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/store/client.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/store/exceptions.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/testing/__init__.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/testing/mock_context.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/testing/mock_secrets.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/tools/__init__.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/tools/client.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/tools/generate_api_surface.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/tools/validate_identity_contract.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/types/__init__.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/types/action_result.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/types/chat_result.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/types/client_contracts.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/types/contracts.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/types/contributions.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/types/events.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/types/health.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/types/identity.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/types/models.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/types/pagination.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/types/store_contracts.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/ui/__init__.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/ui/actions.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/ui/base.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/ui/data.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/ui/display.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/ui/feedback.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/ui/graph.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/ui/input_components.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/ui/interactive.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/ui/layout.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/ui/theme.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/validator.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/validator_v1_6_0.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/conftest.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/contracts/__init__.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/contracts/test_store_contracts.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/fixtures/openapi/auth-gateway.json +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/fixtures/openapi/registry.json +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/fixtures/openapi/sharelock-cases.json +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/rpc/__init__.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/rpc/test_codec.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/rpc/test_contract.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/runtime/__init__.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/runtime/test_llm_provider_config_store.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/runtime/test_llm_provider_ctx_injection.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/store/__init__.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/store/test_list_users_client.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/store/test_query_all_client.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_action_result_typed.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_as_user.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_auth.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_billing.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_cache_client.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_cache_model.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_call_token.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_chat_extension_deprecation.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_chat_filters.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_chat_function_background_flag.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_chat_guards.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_chat_guards_bleed.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_chat_prompt.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_chat_pydantic_retry.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_chat_result.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_cli.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_client_contracts.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_config_client.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_context.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_context_background_task.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_context_deliver_chat_message.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_context_guards.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_contracts.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_contracts_live.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_contributions.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_document_contract.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_emits_decorator.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_error_codes.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_errors.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_event_schema_v2.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_events_health.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_extension.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_extension_v2.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_extensions_emit.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_http_timeout_override.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_id_shape_guard.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_identity_contract.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_imperal_schema_v2.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_kernel_primitives.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_manifest.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_manifest_roundtrip_gate.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_manifest_schema.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_manifest_v2_events.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_manifest_v2_other_sections.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_manifest_v2_webhooks.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_manifest_validator_v2.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_mock_context.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_models.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_narration_emission.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_narration_guard.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_openai_max_completion_tokens.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_pagination.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_panel_rendering_contract.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_panels.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_skeleton_decorator.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_spec_validation.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_tools_client.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_ui.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_ui_fileupload_enhanced.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_ui_html.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_ui_image_enhanced.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_ui_open.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_ui_theme.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_user.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_v7_emit_refusal.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_validator.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_validator_drift.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_validator_pep563.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_validator_v1_6_0_rules.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_write_arg_bleed.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/tools/__init__.py +0 -0
- {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/tools/test_generate_api_surface.py +0 -0
|
@@ -2,6 +2,68 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `imperal-sdk` are documented here.
|
|
4
4
|
|
|
5
|
+
## 4.2.16 — 2026-05-15
|
|
6
|
+
|
|
7
|
+
Enriched tool_use log with `UNKNOWN_FUNCTION(will-reject)` marker when the
|
|
8
|
+
LLM hallucinates a tool name not in the extension's `_functions` schema.
|
|
9
|
+
Caught at `handler.py:185` guard with `UNKNOWN_SUB_FUNCTION` error_code;
|
|
10
|
+
this change makes the rejection visible operator-side. Soak monitoring
|
|
11
|
+
can grep `UNKNOWN_FUNCTION(will-reject)` to track LLM hallucination rate.
|
|
12
|
+
|
|
13
|
+
Behavior change: log line format only. No federal contract impact. No
|
|
14
|
+
SDK API surface change.
|
|
15
|
+
|
|
16
|
+
Closes: sql-db isolation investigation false-positive (operator reading
|
|
17
|
+
journals saw `tool_sql_db_chat (round 2): send(...)` and assumed an
|
|
18
|
+
isolation breach; actual cause was LLM hallucination caught by existing
|
|
19
|
+
UNKNOWN_SUB_FUNCTION guard).
|
|
20
|
+
|
|
21
|
+
## 4.2.15 — 2026-05-14
|
|
22
|
+
|
|
23
|
+
**Feat: federal placeholder-args guard (I-PARAMS-NO-PLACEHOLDER-VALUES)**
|
|
24
|
+
|
|
25
|
+
New ChatExtension guard that rejects any tool call whose arg values look
|
|
26
|
+
like LLM-emitted placeholder sentinels — e.g. `<UNKNOWN>`, `<TODO>`,
|
|
27
|
+
`<MISSING>`, `<EMAIL>`, `<PASSWORD>`, `<USER_ID>`. Runs **before**
|
|
28
|
+
write-arg-bleed, target-scope, and 2-step confirmation guards so the
|
|
29
|
+
dispatch is short-circuited before any billing-charged work or audit-ledger
|
|
30
|
+
pollution. Friendly instruction-to-LLM rejection text feeds back through
|
|
31
|
+
the chat loop as a synthetic tool_result so the LLM can self-correct
|
|
32
|
+
and ask the user a clarifying question.
|
|
33
|
+
|
|
34
|
+
Motivating incident (2026-05-14): admin extension's `tool_admin_chat`
|
|
35
|
+
ChatExtension wrapper LLM produced `create_user({'email': '<UNKNOWN>',
|
|
36
|
+
'password': '<UNKNOWN>'})` when the user had not yet provided concrete
|
|
37
|
+
values. The anti-fab response-side layer correctly caught the drift
|
|
38
|
+
(`server did not reflect 'email': requested '<UNKNOWN>', got None`) but by
|
|
39
|
+
then the dispatch had already wasted billing, polluted `action_ledger`
|
|
40
|
+
with `target=<UNKNOWN>` rows, and produced an opaque user-visible failure.
|
|
41
|
+
This guard fails fast on the request side.
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
|
|
45
|
+
- **`check_placeholder_args(tu, action_type) -> str | None`** in
|
|
46
|
+
`imperal_sdk.chat.guards` — recursive scan of `tu.input` (dict/list/str)
|
|
47
|
+
for values matching `^<[A-Z][A-Z0-9_]*>$`. Tight regex — narrow
|
|
48
|
+
false-positive surface: matches only uppercase-ASCII sentinel tokens,
|
|
49
|
+
ignores prose containing `<UNKNOWN>` as a substring (e.g. error message
|
|
50
|
+
bodies). Whitespace-tolerant via `.strip()`.
|
|
51
|
+
|
|
52
|
+
- **`_PLACEHOLDER_RE`** and **`_scan_for_placeholders(value)`** helpers
|
|
53
|
+
(module-private; recursive over dict values and list/tuple items).
|
|
54
|
+
|
|
55
|
+
- Integration into `check_guards()` orchestrator before
|
|
56
|
+
`check_write_arg_bleed`. Federal invariant
|
|
57
|
+
**I-PARAMS-NO-PLACEHOLDER-VALUES** registered in kernel
|
|
58
|
+
`tests/federal/_invariant_assertions.py`.
|
|
59
|
+
|
|
60
|
+
### Why a PATCH bump (not MINOR)
|
|
61
|
+
|
|
62
|
+
Strictly additive — new guard with default-allow surface. Existing
|
|
63
|
+
extensions emit no placeholder sentinels in legitimate flows, so the
|
|
64
|
+
guard is a no-op for every real call. No manifest schema change, no API
|
|
65
|
+
break, no rebuild required for downstream extensions.
|
|
66
|
+
|
|
5
67
|
## 4.2.14 — 2026-05-14
|
|
6
68
|
|
|
7
69
|
**Fix: regenerate static `imperal.schema.json` to match runtime `Manifest` model**
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Copyright (c) 2026 Imperal, Inc., Valentin Scerbacov, and contributors
|
|
2
|
+
# Licensed under the AGPL-3.0 License. See LICENSE file for details.
|
|
3
|
+
"""Shared chat-loop exceptions.
|
|
4
|
+
|
|
5
|
+
Lives in a leaf module so both `imperal_sdk.chat.handler` and
|
|
6
|
+
`imperal_sdk.chat.execution` can import without creating an import
|
|
7
|
+
cycle. handler.py re-exports `TaskCancelled` for back-compat with any
|
|
8
|
+
caller doing `from imperal_sdk.chat.handler import TaskCancelled`.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TaskCancelled(Exception):
|
|
14
|
+
"""Raised by ctx.progress() when the user cancels a task."""
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# Copyright (c) 2026 Imperal, Inc., Valentin Scerbacov, and contributors
|
|
2
|
+
# Licensed under the AGPL-3.0 License. See LICENSE file for details.
|
|
3
|
+
"""Single-function-call executor for ChatExtension tool-use loop.
|
|
4
|
+
|
|
5
|
+
Extracted from imperal_sdk.chat.handler in v5-27 god-file split (handler.py
|
|
6
|
+
717 LOC → ~425 LOC). `_execute_function` runs ONE tool_use block end-to-end:
|
|
7
|
+
Pydantic validation (with bounded retry feedback loop per
|
|
8
|
+
I-PYDANTIC-RETRY-BUDGET), guard pipeline, the actual handler call, and
|
|
9
|
+
return-shape normalisation. Module-private — no external callers.
|
|
10
|
+
|
|
11
|
+
Federal hooks preserved verbatim:
|
|
12
|
+
* P2 Task 20 — structured error_code on PydanticValidationError + unknown
|
|
13
|
+
sub-function early exit (no raw str(e) into tool_result).
|
|
14
|
+
* I-PYDANTIC-RETRY-BUDGET / I-PYDANTIC-FEEDBACK-STRUCTURED — bounded retry
|
|
15
|
+
with structured prose feedback piped through chat/retry helpers.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
from typing import TYPE_CHECKING
|
|
21
|
+
|
|
22
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
23
|
+
|
|
24
|
+
from imperal_sdk.chat.filters import trim_tool_result
|
|
25
|
+
from imperal_sdk.chat.action_result import ActionResult
|
|
26
|
+
from imperal_sdk.chat.exceptions import TaskCancelled
|
|
27
|
+
from imperal_sdk.chat.retry import (
|
|
28
|
+
format_pydantic_for_llm,
|
|
29
|
+
_emit_retry_outcome,
|
|
30
|
+
_RETRY_BUDGET,
|
|
31
|
+
_validation_missing_field_response,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from imperal_sdk.chat.extension import ChatExtension
|
|
36
|
+
|
|
37
|
+
log = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def _execute_function(
|
|
41
|
+
chat_ext: ChatExtension, ctx, tu, action_type: str, cfg: dict,
|
|
42
|
+
*,
|
|
43
|
+
retry_ctx: dict | None = None,
|
|
44
|
+
) -> str:
|
|
45
|
+
"""Execute a single function call and return the tool result content string.
|
|
46
|
+
|
|
47
|
+
When ``retry_ctx`` is provided AND the function uses a Pydantic params
|
|
48
|
+
model, ``PydanticValidationError`` triggers up to ``_RETRY_BUDGET=2``
|
|
49
|
+
retries with structured prose feedback to the LLM. Without ``retry_ctx``
|
|
50
|
+
(or for legacy ``**kwargs`` extensions), behavior is exactly the
|
|
51
|
+
pre-feature implementation.
|
|
52
|
+
|
|
53
|
+
``retry_ctx`` shape (passed by handle_message tool-use loop):
|
|
54
|
+
client, messages, _system, _exec_cfg, _tools_for_llm,
|
|
55
|
+
_tool_use_mt, _api_kwargs
|
|
56
|
+
|
|
57
|
+
Pre-guards (UNKNOWN_SUB_FUNCTION, I-AH-1 fabricated_id) run BEFORE the
|
|
58
|
+
retry loop and short-circuit with their own fc-append.
|
|
59
|
+
|
|
60
|
+
I-AH-1 federal: fabricated-id re-check fires on every retry attempt
|
|
61
|
+
(security guard remains effective across retries).
|
|
62
|
+
|
|
63
|
+
Error contract (I-ERR-CODE-1): every failure surfaces in BOTH the
|
|
64
|
+
JSON-encoded content AND ``_functions_called[-1]["result"]`` as a dict
|
|
65
|
+
carrying an ``error_code`` drawn from
|
|
66
|
+
:mod:`imperal_sdk.chat.error_codes`. No raw ``str(exception)`` output.
|
|
67
|
+
"""
|
|
68
|
+
# ── Unknown sub-function early exit ──────────────────────────────────
|
|
69
|
+
if tu.name not in chat_ext._functions:
|
|
70
|
+
available = list(chat_ext._functions.keys())
|
|
71
|
+
content = json.dumps({
|
|
72
|
+
"RESULT": "ERROR",
|
|
73
|
+
"error_code": "UNKNOWN_SUB_FUNCTION",
|
|
74
|
+
"detail": f"'{tu.name}' not in this extension. Available: {available}",
|
|
75
|
+
})
|
|
76
|
+
chat_ext._functions_called.append({
|
|
77
|
+
"name": tu.name, "params": tu.input, "action_type": action_type,
|
|
78
|
+
"success": False, "intercepted": False, "event": "",
|
|
79
|
+
"result": {"error_code": "UNKNOWN_SUB_FUNCTION"},
|
|
80
|
+
})
|
|
81
|
+
return trim_tool_result(content, cfg["max_result_tokens"], cfg["list_truncate_items"], cfg["string_truncate_chars"])
|
|
82
|
+
|
|
83
|
+
_func_def = chat_ext._functions[tu.name]
|
|
84
|
+
|
|
85
|
+
# I-AH-1 L3: pre-validation shape guard — reject empirically observed
|
|
86
|
+
# fabricated message_id slug shapes BEFORE Pydantic coercion so error
|
|
87
|
+
# feedback to the LLM is specific ("FABRICATED_ID_SHAPE") rather than
|
|
88
|
+
# generic ("VALIDATION_MISSING_FIELD"). Closes Bug-1 from prod chat
|
|
89
|
+
# 2026-05-01.
|
|
90
|
+
from imperal_sdk.chat.guards import check_id_shape_fabrication
|
|
91
|
+
_id_rejection = check_id_shape_fabrication(tu.input or {})
|
|
92
|
+
if _id_rejection is not None:
|
|
93
|
+
log.warning(
|
|
94
|
+
"ChatExtension I-AH-1 reject %s field=%s value=%r",
|
|
95
|
+
tu.name, _id_rejection["field"], _id_rejection["value"],
|
|
96
|
+
)
|
|
97
|
+
content = json.dumps({"RESULT": "ERROR", **_id_rejection})
|
|
98
|
+
chat_ext._functions_called.append({
|
|
99
|
+
"name": tu.name, "params": tu.input, "action_type": action_type,
|
|
100
|
+
"success": False, "intercepted": True, "event": "",
|
|
101
|
+
"result": _id_rejection,
|
|
102
|
+
})
|
|
103
|
+
return trim_tool_result(
|
|
104
|
+
content, cfg["max_result_tokens"],
|
|
105
|
+
cfg["list_truncate_items"], cfg["string_truncate_chars"],
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# === Pydantic-aware retry loop (SPEC2-LLM-ARGS-QUALITY, v4.1.0) ===
|
|
109
|
+
current_tu = tu
|
|
110
|
+
retry_count = 0
|
|
111
|
+
_ext_name = chat_ext.tool_name
|
|
112
|
+
|
|
113
|
+
# Eligibility for retry: retry_ctx provided AND function uses a Pydantic
|
|
114
|
+
# params model (legacy **kwargs paths cannot raise PydanticValidationError).
|
|
115
|
+
_retry_eligible = (
|
|
116
|
+
retry_ctx is not None
|
|
117
|
+
and bool(_func_def._pydantic_model)
|
|
118
|
+
and bool(_func_def._pydantic_param)
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
while True:
|
|
122
|
+
try:
|
|
123
|
+
# LONGRUN-V1 Component D (v4.2.13+) — declarative background-task sugar.
|
|
124
|
+
# When the handler is decorated with @chat.function(background=True),
|
|
125
|
+
# the SDK auto-wraps the call in ctx.background_task() and returns an
|
|
126
|
+
# immediate ack envelope to the LLM. The actual handler runs detached;
|
|
127
|
+
# the platform delivers its returned ActionResult as a fresh bot turn.
|
|
128
|
+
if getattr(_func_def, "background", False):
|
|
129
|
+
_bg_pydantic_model = _func_def._pydantic_model
|
|
130
|
+
_bg_pydantic_param = _func_def._pydantic_param
|
|
131
|
+
_bg_input = current_tu.input or {}
|
|
132
|
+
_bg_fn = _func_def.func
|
|
133
|
+
|
|
134
|
+
async def _bg_coro():
|
|
135
|
+
if _bg_pydantic_model and _bg_pydantic_param:
|
|
136
|
+
_mi = _bg_pydantic_model(**_bg_input)
|
|
137
|
+
return await _bg_fn(ctx, **{_bg_pydantic_param: _mi})
|
|
138
|
+
return await _bg_fn(ctx, **_bg_input)
|
|
139
|
+
|
|
140
|
+
_bg_task_id = await ctx.background_task(
|
|
141
|
+
_bg_coro(),
|
|
142
|
+
long_running=bool(getattr(_func_def, "long_running", False)),
|
|
143
|
+
name=current_tu.name,
|
|
144
|
+
)
|
|
145
|
+
result = ActionResult.success(
|
|
146
|
+
summary=(
|
|
147
|
+
f"Started '{current_tu.name}' in background — "
|
|
148
|
+
"the result will be sent to chat when it finishes."
|
|
149
|
+
),
|
|
150
|
+
data={"task_id": _bg_task_id, "background": True},
|
|
151
|
+
)
|
|
152
|
+
elif _func_def._pydantic_model and _func_def._pydantic_param:
|
|
153
|
+
_model_instance = _func_def._pydantic_model(**(current_tu.input or {}))
|
|
154
|
+
result = await _func_def.func(ctx, **{_func_def._pydantic_param: _model_instance})
|
|
155
|
+
else:
|
|
156
|
+
result = await _func_def.func(ctx, **current_tu.input)
|
|
157
|
+
|
|
158
|
+
# === SUCCESS path ===
|
|
159
|
+
_is_action_result = isinstance(result, ActionResult)
|
|
160
|
+
if _is_action_result:
|
|
161
|
+
content = json.dumps(result.to_dict(), default=str, ensure_ascii=False)
|
|
162
|
+
else:
|
|
163
|
+
content = json.dumps(result, default=str, ensure_ascii=False)
|
|
164
|
+
if _func_def.event:
|
|
165
|
+
log.warning(
|
|
166
|
+
f"ChatExtension {chat_ext.tool_name}: function '{current_tu.name}' "
|
|
167
|
+
f"has event='{_func_def.event}' but returned dict, not ActionResult"
|
|
168
|
+
)
|
|
169
|
+
content = trim_tool_result(
|
|
170
|
+
content, cfg["max_result_tokens"],
|
|
171
|
+
cfg["list_truncate_items"], cfg["string_truncate_chars"],
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
if _is_action_result:
|
|
175
|
+
success = result.status == "success"
|
|
176
|
+
else:
|
|
177
|
+
success = True
|
|
178
|
+
if isinstance(result, dict):
|
|
179
|
+
if result.get("RESULT") == "ERROR" or result.get("error"):
|
|
180
|
+
success = False
|
|
181
|
+
elif "success" in result:
|
|
182
|
+
success = bool(result["success"])
|
|
183
|
+
|
|
184
|
+
chat_ext._functions_called.append({
|
|
185
|
+
"name": current_tu.name, "params": current_tu.input,
|
|
186
|
+
"action_type": action_type, "success": success,
|
|
187
|
+
"intercepted": False,
|
|
188
|
+
"event": _func_def.event if _is_action_result else "",
|
|
189
|
+
"result": result if _is_action_result else None,
|
|
190
|
+
})
|
|
191
|
+
_emit_retry_outcome(
|
|
192
|
+
tool=current_tu.name, ext=_ext_name,
|
|
193
|
+
outcome=("no_retry" if retry_count == 0 else "success"),
|
|
194
|
+
retry_count=retry_count,
|
|
195
|
+
)
|
|
196
|
+
return content
|
|
197
|
+
|
|
198
|
+
except TaskCancelled:
|
|
199
|
+
raise
|
|
200
|
+
|
|
201
|
+
except PydanticValidationError as e:
|
|
202
|
+
if not _retry_eligible or retry_count >= _RETRY_BUDGET:
|
|
203
|
+
# Exhausted OR not eligible for retry — existing failure handling.
|
|
204
|
+
content = _validation_missing_field_response(
|
|
205
|
+
e=e, chat_ext=chat_ext, tu=current_tu,
|
|
206
|
+
action_type=action_type, cfg=cfg,
|
|
207
|
+
)
|
|
208
|
+
if _retry_eligible:
|
|
209
|
+
_emit_retry_outcome(
|
|
210
|
+
tool=current_tu.name, ext=_ext_name,
|
|
211
|
+
outcome="exhausted", retry_count=retry_count,
|
|
212
|
+
)
|
|
213
|
+
return content
|
|
214
|
+
|
|
215
|
+
# Retry path: re-prompt LLM with structured prose feedback.
|
|
216
|
+
prose = format_pydantic_for_llm(e)
|
|
217
|
+
log.info(
|
|
218
|
+
f"chat_handler validation_retry tool={current_tu.name} "
|
|
219
|
+
f"retry_count={retry_count + 1}/{_RETRY_BUDGET}"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
tmp_messages = list(retry_ctx["messages"]) + [
|
|
223
|
+
{"role": "assistant", "content": [current_tu]},
|
|
224
|
+
{"role": "user", "content": [
|
|
225
|
+
{"type": "tool_result", "tool_use_id": current_tu.id, "content": prose}
|
|
226
|
+
]},
|
|
227
|
+
]
|
|
228
|
+
retry_resp = await retry_ctx["client"].create_message(
|
|
229
|
+
max_tokens=retry_ctx["_tool_use_mt"],
|
|
230
|
+
system=retry_ctx["_system"],
|
|
231
|
+
messages=tmp_messages,
|
|
232
|
+
tools=retry_ctx["_tools_for_llm"],
|
|
233
|
+
cfg=retry_ctx["_exec_cfg"],
|
|
234
|
+
**retry_ctx["_api_kwargs"],
|
|
235
|
+
)
|
|
236
|
+
# Mirror the main loop's usage callback for retry LLM calls
|
|
237
|
+
# (handler.py:411-426). Without this, retry token cost is silently
|
|
238
|
+
# dropped from billing/observability.
|
|
239
|
+
_usage_cb = getattr(ctx, "_llm_usage_callback", None)
|
|
240
|
+
if _usage_cb and hasattr(retry_resp, "usage") and retry_resp.usage is not None:
|
|
241
|
+
try:
|
|
242
|
+
from imperal_sdk.runtime.llm_provider import LLMUsage
|
|
243
|
+
_exec_cfg = retry_ctx["_exec_cfg"]
|
|
244
|
+
_uid = str(getattr(ctx.user, "id", "")) if hasattr(ctx, "user") and ctx.user else ""
|
|
245
|
+
_usage = LLMUsage(
|
|
246
|
+
provider=_exec_cfg.provider,
|
|
247
|
+
model=_exec_cfg.model,
|
|
248
|
+
input_tokens=getattr(retry_resp.usage, "input_tokens", 0) or 0,
|
|
249
|
+
output_tokens=getattr(retry_resp.usage, "output_tokens", 0) or 0,
|
|
250
|
+
is_byollm=_exec_cfg.is_byollm,
|
|
251
|
+
purpose="execution",
|
|
252
|
+
user_id=_uid,
|
|
253
|
+
)
|
|
254
|
+
await _usage_cb(_usage)
|
|
255
|
+
except Exception as _e:
|
|
256
|
+
log.debug(f"retry usage callback failed: {_e}") # NEVER raise
|
|
257
|
+
new_tools = [b for b in retry_resp.content if getattr(b, "type", None) == "tool_use"]
|
|
258
|
+
new_tu = next((b for b in new_tools if b.name == current_tu.name), None)
|
|
259
|
+
if new_tu is None:
|
|
260
|
+
# LLM gave up (final text or different tool). Existing failure shape.
|
|
261
|
+
content = _validation_missing_field_response(
|
|
262
|
+
e=e, chat_ext=chat_ext, tu=current_tu,
|
|
263
|
+
action_type=action_type, cfg=cfg,
|
|
264
|
+
)
|
|
265
|
+
_emit_retry_outcome(
|
|
266
|
+
tool=current_tu.name, ext=_ext_name,
|
|
267
|
+
outcome="llm_gave_up", retry_count=retry_count,
|
|
268
|
+
)
|
|
269
|
+
return content
|
|
270
|
+
|
|
271
|
+
# I-AH-1 federal re-check on retry (spec section 8 E15).
|
|
272
|
+
_ret_id_rejection = check_id_shape_fabrication(new_tu.input or {})
|
|
273
|
+
if _ret_id_rejection is not None:
|
|
274
|
+
log.warning(
|
|
275
|
+
"ChatExtension I-AH-1 reject-on-retry %s field=%s value=%r",
|
|
276
|
+
new_tu.name, _ret_id_rejection["field"], _ret_id_rejection["value"],
|
|
277
|
+
)
|
|
278
|
+
content = json.dumps({"RESULT": "ERROR", **_ret_id_rejection})
|
|
279
|
+
chat_ext._functions_called.append({
|
|
280
|
+
"name": new_tu.name, "params": new_tu.input,
|
|
281
|
+
"action_type": action_type, "success": False,
|
|
282
|
+
"intercepted": True, "event": "",
|
|
283
|
+
"result": _ret_id_rejection,
|
|
284
|
+
})
|
|
285
|
+
_emit_retry_outcome(
|
|
286
|
+
tool=current_tu.name, ext=_ext_name,
|
|
287
|
+
outcome="fabricated_id_on_retry", retry_count=retry_count + 1,
|
|
288
|
+
)
|
|
289
|
+
return trim_tool_result(
|
|
290
|
+
content, cfg["max_result_tokens"],
|
|
291
|
+
cfg["list_truncate_items"], cfg["string_truncate_chars"],
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# Redundant retry detection (byte-identical args).
|
|
295
|
+
if json.dumps(new_tu.input or {}, sort_keys=True) == json.dumps(current_tu.input or {}, sort_keys=True):
|
|
296
|
+
_emit_retry_outcome(
|
|
297
|
+
tool=current_tu.name, ext=_ext_name,
|
|
298
|
+
outcome="redundant", retry_count=retry_count + 1,
|
|
299
|
+
)
|
|
300
|
+
log.warning(
|
|
301
|
+
"chat_handler validation_retry_redundant tool=%s args_unchanged=true retry_count=%d",
|
|
302
|
+
current_tu.name, retry_count + 1,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
current_tu = new_tu
|
|
306
|
+
retry_count += 1
|
|
307
|
+
continue # while True
|
|
308
|
+
|
|
309
|
+
except Exception as e:
|
|
310
|
+
log.error(f"ChatExtension internal error {current_tu.name}: {e}", exc_info=True)
|
|
311
|
+
content = json.dumps({
|
|
312
|
+
"RESULT": "ERROR",
|
|
313
|
+
"error_code": "INTERNAL",
|
|
314
|
+
"error_class": type(e).__name__,
|
|
315
|
+
})
|
|
316
|
+
chat_ext._functions_called.append({
|
|
317
|
+
"name": current_tu.name, "params": current_tu.input,
|
|
318
|
+
"action_type": action_type, "success": False,
|
|
319
|
+
"intercepted": False, "event": "",
|
|
320
|
+
"result": {"error_code": "INTERNAL", "error_class": type(e).__name__},
|
|
321
|
+
})
|
|
322
|
+
return trim_tool_result(
|
|
323
|
+
content, cfg["max_result_tokens"],
|
|
324
|
+
cfg["list_truncate_items"], cfg["string_truncate_chars"],
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# ---------------------------------------------------------------------------
|
|
329
|
+
# Main entry point
|
|
330
|
+
# ---------------------------------------------------------------------------
|
|
@@ -118,6 +118,22 @@ def check_guards(
|
|
|
118
118
|
# Do not append to _functions_called; do not return a verdict — fall
|
|
119
119
|
# through to target_scope + confirmation guards below.
|
|
120
120
|
|
|
121
|
+
# ── Placeholder-args guard (I-PARAMS-NO-PLACEHOLDER-VALUES) ───
|
|
122
|
+
# Defence-in-depth: reject ANY tool call whose arg values are LLM-emitted
|
|
123
|
+
# placeholder sentinels (`<UNKNOWN>`, `<TODO>`, `<MISSING>`, etc).
|
|
124
|
+
# Runs BEFORE write-arg-bleed + target-scope + confirmation guards so
|
|
125
|
+
# we fail fast on poisoned inputs and never waste a billing-charged
|
|
126
|
+
# dispatch or pollute the audit ledger with `target=<UNKNOWN>` rows.
|
|
127
|
+
# See federal invariant **I-PARAMS-NO-PLACEHOLDER-VALUES**.
|
|
128
|
+
placeholder_reason = check_placeholder_args(tu, action_type)
|
|
129
|
+
if placeholder_reason is not None:
|
|
130
|
+
chat_ext._functions_called.append({
|
|
131
|
+
"name": tu.name, "params": tu.input,
|
|
132
|
+
"action_type": action_type, "success": False, "intercepted": False,
|
|
133
|
+
"event": "", "result": None,
|
|
134
|
+
})
|
|
135
|
+
return json.dumps({"RESULT": "BLOCKED", "error": placeholder_reason})
|
|
136
|
+
|
|
121
137
|
# ── Write-arg-bleed guard (I-WRITE-ARG-NO-BLEED) ──────────────
|
|
122
138
|
# Defence-in-depth: reject any write/destructive call whose args contain
|
|
123
139
|
# substrings of prior ERROR_TAXONOMY codes, even if the LLM paraphrased
|
|
@@ -145,6 +161,92 @@ def check_guards(
|
|
|
145
161
|
return None
|
|
146
162
|
|
|
147
163
|
|
|
164
|
+
# Federal invariant **I-PARAMS-NO-PLACEHOLDER-VALUES** sentinel pattern.
|
|
165
|
+
# Matches LLM-emitted placeholder values of the form `<NAME>` where NAME is
|
|
166
|
+
# uppercase ASCII + digits + underscores (e.g. `<UNKNOWN>`, `<TODO>`,
|
|
167
|
+
# `<MISSING>`, `<EMAIL>`, `<PASSWORD>`, `<USER_ID>`). The pattern is
|
|
168
|
+
# deliberately tight — narrow false-positive surface — and only flags values
|
|
169
|
+
# that are EXACTLY one sentinel token after `.strip()`. Real prose containing
|
|
170
|
+
# `<UNKNOWN>` as a substring (e.g. error messages, comments) does NOT match.
|
|
171
|
+
_PLACEHOLDER_RE = re.compile(r"^<[A-Z][A-Z0-9_]*>$")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _scan_for_placeholders(value: Any, _path: str = "") -> list[tuple[str, str]]:
|
|
175
|
+
"""Recursive scan for placeholder values.
|
|
176
|
+
|
|
177
|
+
Returns a list of ``(json-path, matched-value)`` tuples; empty list when
|
|
178
|
+
nothing matched. Recurses into dict values and list/tuple items. Caps
|
|
179
|
+
recursion depth implicitly via JSON shape (no cycles in tool_use input).
|
|
180
|
+
"""
|
|
181
|
+
hits: list[tuple[str, str]] = []
|
|
182
|
+
if isinstance(value, str):
|
|
183
|
+
s = value.strip()
|
|
184
|
+
if _PLACEHOLDER_RE.match(s):
|
|
185
|
+
hits.append((_path or "$", s))
|
|
186
|
+
elif isinstance(value, dict):
|
|
187
|
+
for k, v in value.items():
|
|
188
|
+
hits.extend(_scan_for_placeholders(v, f"{_path}.{k}" if _path else str(k)))
|
|
189
|
+
elif isinstance(value, (list, tuple)):
|
|
190
|
+
for i, v in enumerate(value):
|
|
191
|
+
hits.extend(_scan_for_placeholders(v, f"{_path}[{i}]"))
|
|
192
|
+
return hits
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def check_placeholder_args(tu, action_type: str) -> str | None:
|
|
196
|
+
"""Reject any tool call whose arg values look like LLM-emitted placeholder
|
|
197
|
+
sentinels (e.g. ``<UNKNOWN>``, ``<TODO>``, ``<MISSING>``).
|
|
198
|
+
|
|
199
|
+
Federal invariant **I-PARAMS-NO-PLACEHOLDER-VALUES**: when the LLM does
|
|
200
|
+
not have a real value for a required field, it sometimes substitutes a
|
|
201
|
+
placeholder token instead of asking the user. The downstream anti-fab
|
|
202
|
+
layer catches the drift on the response side (``server did not reflect
|
|
203
|
+
'email': requested '<UNKNOWN>', got None``), but by then the dispatch has
|
|
204
|
+
already wasted billing tokens, polluted the audit ledger with
|
|
205
|
+
``target=<UNKNOWN>`` rows, and produced an opaque user-visible failure.
|
|
206
|
+
This guard fails fast on the request side and surfaces a friendly ask
|
|
207
|
+
back to the LLM so it can clarify with the user.
|
|
208
|
+
|
|
209
|
+
Applies to **all** ``action_type`` values (read/write/destructive) —
|
|
210
|
+
placeholder values are never legitimate.
|
|
211
|
+
|
|
212
|
+
Returns
|
|
213
|
+
-------
|
|
214
|
+
str | None
|
|
215
|
+
``None`` to allow the dispatch through, or a human-readable rejection
|
|
216
|
+
reason that the caller wraps into the standard
|
|
217
|
+
``{"RESULT": "BLOCKED", "error": ...}`` envelope. The reason is
|
|
218
|
+
deliberately framed as an instruction to the LLM (not the end user)
|
|
219
|
+
because the SDK chat loop feeds it back as a synthetic tool_result so
|
|
220
|
+
the LLM can self-correct and emit a clarifying question.
|
|
221
|
+
"""
|
|
222
|
+
payload = getattr(tu, "input", None)
|
|
223
|
+
if not payload:
|
|
224
|
+
return None
|
|
225
|
+
try:
|
|
226
|
+
hits = _scan_for_placeholders(payload)
|
|
227
|
+
except Exception:
|
|
228
|
+
# Defensive — never block on the scanner itself raising on exotic
|
|
229
|
+
# payloads. Real placeholder values are str-scalar and trivially
|
|
230
|
+
# serialisable; if scan blows up it is on something that cannot
|
|
231
|
+
# contain a placeholder anyway.
|
|
232
|
+
return None
|
|
233
|
+
if not hits:
|
|
234
|
+
return None
|
|
235
|
+
field_list = ", ".join(f"`{f}`" for f, _ in hits[:5])
|
|
236
|
+
log.warning(
|
|
237
|
+
f"ChatExtension guard: PLACEHOLDER_ARGS blocked {getattr(tu, 'name', '?')} "
|
|
238
|
+
f"action={action_type} fields={[f for f, _ in hits]}"
|
|
239
|
+
)
|
|
240
|
+
return (
|
|
241
|
+
f"PLACEHOLDER_ARGS rejected: tool call '{getattr(tu, 'name', '?')}' "
|
|
242
|
+
f"contains placeholder value(s) in: {field_list}. The user did not "
|
|
243
|
+
f"provide concrete values for these fields and you emitted sentinel "
|
|
244
|
+
f"tokens (e.g. <UNKNOWN>) instead. Do not dispatch with placeholders. "
|
|
245
|
+
f"Ask the user a clarifying question to obtain the missing values, "
|
|
246
|
+
f"then retry with the real data."
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
148
250
|
def check_write_arg_bleed(
|
|
149
251
|
tu,
|
|
150
252
|
functions_called: Iterable[dict[str, Any]],
|