fred-runtime 4.0.0__tar.gz → 4.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/PKG-INFO +8 -3
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/README.md +5 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/deep/deep_runtime.py +58 -19
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/integrations/v2_runtime/adapters.py +2 -1
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/middleware/__init__.py +13 -4
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/middleware/checkpoint_hygiene.py +5 -2
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/middleware/frame.py +6 -1
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/middleware/hitl.py +61 -2
- fred_runtime-4.1.0/fred_runtime/react/middleware/rate_limit_retry.py +159 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/middleware/tool_observability.py +50 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/react_tool_binding.py +15 -29
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/react_tool_resolution.py +2 -1
- fred_runtime-4.1.0/fred_runtime/react/react_tracing.py +86 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/support/tool_loop.py +10 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime.egg-info/PKG-INFO +8 -3
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime.egg-info/SOURCES.txt +2 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime.egg-info/requires.txt +2 -2
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/pyproject.toml +4 -3
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_deep_agent_hitl_integration.py +431 -3
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_deep_agent_middleware.py +240 -13
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_hitl_resume_langgraph_integration.py +8 -2
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_langfuse_tracing.py +8 -0
- fred_runtime-4.1.0/tests/test_rate_limit_retry_middleware.py +554 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_react_middleware_frame.py +5 -2
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_tool_loop_trim.py +29 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_tool_observability_middleware.py +328 -2
- fred_runtime-4.0.0/fred_runtime/react/react_tracing.py +0 -31
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/__init__.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/__main__.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/app/__init__.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/app/_catalogs.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/app/agent_app.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/app/chat_input_limit.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/app/config.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/app/config_loader.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/app/container.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/app/context.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/app/dependencies.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/app/mcp_config.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/app/observability_factory.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/app/openai_compat_router.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/app/platform_sql.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/__init__.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/assembly.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/assets.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/demo.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/demo_migrations/env.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/demo_migrations/versions/d1e2m0a0b0c0_create_demo_echo_notes.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/document_access/__init__.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/document_access/capability.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/document_extract/__init__.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/document_extract/capability.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/document_label_search/__init__.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/document_label_search/capability.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/document_read_common.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/document_similarity/__init__.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/document_similarity/capability.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/document_summarize/__init__.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/document_summarize/capability.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/document_verbatim/__init__.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/document_verbatim/capability.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/errors.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/mcp.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/openapi_dump.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/registry.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/cli/__init__.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/cli/completion.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/cli/entrypoint.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/cli/history_display.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/cli/kpi_display.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/cli/pod_client.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/cli/repl.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/cli/repl_helpers.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/cli/url_helpers.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/client.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/__init__.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/context_aware_tool.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/kf_base_client.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/kf_document_client.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/kf_fast_text_client.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/kf_http_client.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/kf_markdown_media_client.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/kf_tag_client.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/kf_vectorsearch_client.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/kf_workspace_client.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/mcp_interceptors.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/mcp_runtime.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/mcp_toolkit.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/mcp_utils.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/structures.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/table_hits.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/token_expiry.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/tool_node_utils.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/deep/__init__.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/eval/__init__.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/eval/collector.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/graph/__init__.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/graph/graph_runtime.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/integrations/__init__.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/integrations/inprocess_toolkit_registry.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/integrations/kf_vector_search/__init__.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/integrations/kf_vector_search/toolkit.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/integrations/v2_runtime/__init__.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/migrations/__init__.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/migrations/env.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/migrations/script.py.mako +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/migrations/versions/__init__.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/migrations/versions/a1e2f3c4d5b6_create_session_history.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/migrations/versions/b2f3a4e5c6d7_add_exchange_id.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/migrations/versions/c3d4b5a6f7e8_add_team_and_instance.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/model_routing/__init__.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/model_routing/authz.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/model_routing/catalog.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/model_routing/contracts.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/model_routing/provider.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/model_routing/resolver.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/__init__.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/middleware/dynamic_prompt.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/middleware/shared.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/middleware/tracing_kpi.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/react_langchain_adapter.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/react_message_codec.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/react_model_adapter.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/react_prompting.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/react_runtime.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/react_stream_adapter.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/react_tool_loop.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/react_tool_rendering.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/react_tool_utils.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/runtime_context.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/runtime_support/__init__.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/runtime_support/checkpoints.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/runtime_support/model_metadata.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/runtime_support/request_context_helpers.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/runtime_support/sql_checkpointer.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/runtime_support/trace_payloads.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/runtime_support/user_token_refresher.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/support/__init__.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/support/filesystem_context.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/support/thinking.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime.egg-info/dependency_links.txt +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime.egg-info/entry_points.txt +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime.egg-info/top_level.txt +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/setup.cfg +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_agent_app.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_capability_agent_1973.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_capability_chat_controls_1976.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_capability_chat_parts_1977.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_capability_document_access_1906.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_capability_document_label_search.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_capability_document_reading.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_capability_document_similarity.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_capability_document_summarize.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_capability_endpoints_1974.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_capability_registry_1973.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_capability_routes_tables_1979.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_capability_selection_1974.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_capability_tool_return_convention.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_chat_input_limit.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_checkpoint_namespace.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_client.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_config_loader.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_context.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_context_aware_tool.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_conversational_memory.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_deep_agent_dispatch.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_document_port_error_redaction.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_document_read_scope.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_document_similarity_adapter.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_eval_collector.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_eval_trace.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_fred_workspace_fs.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_graph_capability_bridge.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_graph_checkpoint_namespace.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_graph_runtime_invoke_agent.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_graph_runtime_observability.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_graph_runtime_token_usage_totals.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_history.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_history_display.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_kf_base_client_refresh_shapes.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_kf_vector_search_toolkit.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_kf_workspace_client.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_knowledge_search_table_ordering.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_knowledge_search_tool_invoker_sources.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_kpi_display.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_langgraph_interrupt_id_semantics.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_mcp_config.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_migrations.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_model_enforcement.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_model_metadata_cache_tokens.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_model_reasoning_enablement.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_model_routing.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_model_routing_provider.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_models_catalog_projection.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_openai_compat_router.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_platform_sql.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_platform_sql_postgres_integration.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_pod_client.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_react_checkpoint_namespace.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_react_loop_regressions_1972.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_react_message_codec_resume.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_react_prompting.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_react_thinking.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_react_token_usage_totals.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_react_tool_binding_span_status.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_react_tool_error_final_2244.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_react_tool_resolution.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_repl_helpers.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_runtime_context_prompt_injection.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_similarity_search_tool_invoker.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_smoke.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_spike_capability_state_1971.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_sql_checkpointer_hitl_claim.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_sql_checkpointer_owner.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_table_repair_wiring.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_team_wiki_adapter.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_token_expiry.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_tool_carrier_middleware_naming.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_trace_payloads.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_tracing_kpi_content_confinement.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_url_helpers.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_user_token_refresher.py +0 -0
- {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_vector_similarity_search_retry.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fred-runtime
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.1.0
|
|
4
4
|
Summary: Runtime adapters and infrastructure wiring for Fred v2 agents.
|
|
5
5
|
Author-email: Thales <noreply@thalesgroup.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -12,8 +12,8 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
12
12
|
Classifier: Operating System :: OS Independent
|
|
13
13
|
Requires-Python: <3.13,>=3.12
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
|
-
Requires-Dist: fred-core>=4.
|
|
16
|
-
Requires-Dist: fred-sdk>=4.
|
|
15
|
+
Requires-Dist: fred-core>=4.1.0
|
|
16
|
+
Requires-Dist: fred-sdk[agents]>=4.1.0
|
|
17
17
|
Requires-Dist: alembic>=1.18.4
|
|
18
18
|
Requires-Dist: deepagents>=0.6.12
|
|
19
19
|
Requires-Dist: httpx>=0.28.1
|
|
@@ -230,6 +230,10 @@ pip install fred-runtime
|
|
|
230
230
|
pip install fred-runtime[app]
|
|
231
231
|
```
|
|
232
232
|
|
|
233
|
+
`fred-runtime` depends on `fred-sdk[agents]`, so an agent pod installs nothing
|
|
234
|
+
else to get the authoring surface. A Knowledge Base pod installs
|
|
235
|
+
`fred-sdk[knowledge-base]` instead and never needs `fred-runtime`.
|
|
236
|
+
|
|
233
237
|
Requires Python 3.12.
|
|
234
238
|
|
|
235
239
|
---
|
|
@@ -288,6 +292,7 @@ See [fred-samples](https://github.com/ThalesGroup/fred-samples) for a working re
|
|
|
288
292
|
|
|
289
293
|
| Package | PyPI | Role |
|
|
290
294
|
| -------------- | ---------------------------------------------- | ----------------------------------------------------------------------------- |
|
|
295
|
+
| `fred-pod` | [pypi](https://pypi.org/project/fred-pod/) | Pod floor — configuration, identity, naming |
|
|
291
296
|
| `fred-core` | [pypi](https://pypi.org/project/fred-core/) | Pure utilities — logging, model factories, embeddings, portable observability |
|
|
292
297
|
| `fred-sdk` | [pypi](https://pypi.org/project/fred-sdk/) | Agent authoring — ReAct, Graph, tool contracts |
|
|
293
298
|
| `fred-runtime` | [pypi](https://pypi.org/project/fred-runtime/) | This package |
|
|
@@ -190,6 +190,10 @@ pip install fred-runtime
|
|
|
190
190
|
pip install fred-runtime[app]
|
|
191
191
|
```
|
|
192
192
|
|
|
193
|
+
`fred-runtime` depends on `fred-sdk[agents]`, so an agent pod installs nothing
|
|
194
|
+
else to get the authoring surface. A Knowledge Base pod installs
|
|
195
|
+
`fred-sdk[knowledge-base]` instead and never needs `fred-runtime`.
|
|
196
|
+
|
|
193
197
|
Requires Python 3.12.
|
|
194
198
|
|
|
195
199
|
---
|
|
@@ -248,6 +252,7 @@ See [fred-samples](https://github.com/ThalesGroup/fred-samples) for a working re
|
|
|
248
252
|
|
|
249
253
|
| Package | PyPI | Role |
|
|
250
254
|
| -------------- | ---------------------------------------------- | ----------------------------------------------------------------------------- |
|
|
255
|
+
| `fred-pod` | [pypi](https://pypi.org/project/fred-pod/) | Pod floor — configuration, identity, naming |
|
|
251
256
|
| `fred-core` | [pypi](https://pypi.org/project/fred-core/) | Pure utilities — logging, model factories, embeddings, portable observability |
|
|
252
257
|
| `fred-sdk` | [pypi](https://pypi.org/project/fred-sdk/) | Agent authoring — ReAct, Graph, tool contracts |
|
|
253
258
|
| `fred-runtime` | [pypi](https://pypi.org/project/fred-runtime/) | This package |
|
|
@@ -41,10 +41,13 @@ from langchain_core.tools import BaseTool
|
|
|
41
41
|
from langgraph.types import Checkpointer
|
|
42
42
|
|
|
43
43
|
from fred_runtime.capabilities.assembly import CapabilityAgentBlock
|
|
44
|
+
from fred_runtime.react.middleware.checkpoint_hygiene import CheckpointHygieneMiddleware
|
|
44
45
|
from fred_runtime.react.middleware.hitl import (
|
|
45
46
|
CapabilityHitlBinding,
|
|
47
|
+
DeepChildHitlMiddleware,
|
|
46
48
|
FredHitlMiddleware,
|
|
47
49
|
)
|
|
50
|
+
from fred_runtime.react.middleware.rate_limit_retry import RateLimitRetryMiddleware
|
|
48
51
|
from fred_runtime.react.middleware.tool_observability import (
|
|
49
52
|
ToolObservabilityMiddleware,
|
|
50
53
|
)
|
|
@@ -75,6 +78,16 @@ from fred_runtime.react.react_tool_resolution import ReActRuntimeToolResolver
|
|
|
75
78
|
|
|
76
79
|
logger = logging.getLogger(__name__)
|
|
77
80
|
|
|
81
|
+
_SUBAGENT_FRAMING = (
|
|
82
|
+
"You are running as a sub-agent: another instance of yourself delegated one "
|
|
83
|
+
"task to you. There is no user in this conversation and no one to ask. "
|
|
84
|
+
"Instructions about greeting a user, asking what to do next or offering "
|
|
85
|
+
"options do not apply here. Do not ask clarifying questions or delegate "
|
|
86
|
+
"further. Carry out the task as described, preserve its arguments, and make "
|
|
87
|
+
"reasonable assumptions where it is silent. Report any action requiring "
|
|
88
|
+
"human approval to the parent agent instead of attempting to bypass it."
|
|
89
|
+
)
|
|
90
|
+
|
|
78
91
|
_FILESYSTEM_TOOL_NAMES: tuple[str, ...] = (
|
|
79
92
|
"ls",
|
|
80
93
|
"read_file",
|
|
@@ -144,6 +157,12 @@ class DeepAgentRuntime(ReActRuntime):
|
|
|
144
157
|
available_tool_names.update(
|
|
145
158
|
tool.name for tool in capability_block.tools if tool.name
|
|
146
159
|
)
|
|
160
|
+
available_tool_names.update(
|
|
161
|
+
tool.name
|
|
162
|
+
for entry in capability_block.middleware
|
|
163
|
+
for tool in getattr(entry, "tools", ())
|
|
164
|
+
if tool.name
|
|
165
|
+
)
|
|
147
166
|
system_prompt = _render_prompt_template(
|
|
148
167
|
policy.system_prompt_template,
|
|
149
168
|
binding=binding,
|
|
@@ -190,6 +209,15 @@ class DeepAgentRuntime(ReActRuntime):
|
|
|
190
209
|
available_tool_names=available_tool_names,
|
|
191
210
|
capability_block=capability_block,
|
|
192
211
|
),
|
|
212
|
+
subagent_middleware=_build_deepagent_runtime_middleware(
|
|
213
|
+
tracer=self.services.tracer,
|
|
214
|
+
kpi=self.services.kpi_writer,
|
|
215
|
+
binding=binding,
|
|
216
|
+
approval_policy=policy.tool_approval,
|
|
217
|
+
available_tool_names=available_tool_names,
|
|
218
|
+
capability_block=capability_block,
|
|
219
|
+
child=True,
|
|
220
|
+
),
|
|
193
221
|
)
|
|
194
222
|
return _TransportBackedReActExecutor(
|
|
195
223
|
compiled_agent=compiled_agent,
|
|
@@ -206,6 +234,7 @@ def _create_compiled_deep_agent(
|
|
|
206
234
|
system_prompt: str,
|
|
207
235
|
checkpointer: Checkpointer,
|
|
208
236
|
middleware: Sequence[AgentMiddleware],
|
|
237
|
+
subagent_middleware: Sequence[AgentMiddleware],
|
|
209
238
|
) -> _CompiledReActAgent:
|
|
210
239
|
try:
|
|
211
240
|
from deepagents import create_deep_agent
|
|
@@ -214,17 +243,23 @@ def _create_compiled_deep_agent(
|
|
|
214
243
|
"DeepAgentRuntime requires the optional `deepagents` package."
|
|
215
244
|
) from exc
|
|
216
245
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
tools=list(tools),
|
|
223
|
-
system_prompt=system_prompt,
|
|
224
|
-
middleware=list(middleware),
|
|
225
|
-
),
|
|
226
|
-
)
|
|
246
|
+
from deepagents.middleware.subagents import (
|
|
247
|
+
DEFAULT_SUBAGENT_PROMPT,
|
|
248
|
+
GENERAL_PURPOSE_SUBAGENT,
|
|
249
|
+
SubAgent,
|
|
250
|
+
)
|
|
227
251
|
|
|
252
|
+
# Explicitly replace the library's general-purpose child, which otherwise
|
|
253
|
+
# has neither Fred's composed prompt nor its middleware frame.
|
|
254
|
+
subagent: SubAgent = {
|
|
255
|
+
"name": GENERAL_PURPOSE_SUBAGENT["name"],
|
|
256
|
+
"description": GENERAL_PURPOSE_SUBAGENT["description"],
|
|
257
|
+
"system_prompt": "\n\n".join(
|
|
258
|
+
(system_prompt, _SUBAGENT_FRAMING, DEFAULT_SUBAGENT_PROMPT)
|
|
259
|
+
),
|
|
260
|
+
"tools": list(tools),
|
|
261
|
+
"middleware": list(subagent_middleware),
|
|
262
|
+
}
|
|
228
263
|
return cast(
|
|
229
264
|
_CompiledReActAgent,
|
|
230
265
|
create_deep_agent(
|
|
@@ -232,6 +267,7 @@ def _create_compiled_deep_agent(
|
|
|
232
267
|
tools=list(tools),
|
|
233
268
|
system_prompt=system_prompt,
|
|
234
269
|
middleware=list(middleware),
|
|
270
|
+
subagents=[subagent],
|
|
235
271
|
checkpointer=checkpointer,
|
|
236
272
|
),
|
|
237
273
|
)
|
|
@@ -265,27 +301,30 @@ def _build_deepagent_runtime_middleware(
|
|
|
265
301
|
approval_policy: ToolApprovalPolicy,
|
|
266
302
|
available_tool_names: set[str] | frozenset[str],
|
|
267
303
|
capability_block: CapabilityAgentBlock | None = None,
|
|
304
|
+
child: bool = False,
|
|
268
305
|
) -> list[AgentMiddleware]:
|
|
269
|
-
"""
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
alike), then the filesystem-tool guard — same relative order as
|
|
273
|
-
`build_react_platform_middleware_frame` (`after_model` hooks run in
|
|
274
|
-
REVERSE list order, so the filesystem guard still blocks a disabled call
|
|
275
|
-
before the human gate ever sees it). RUNTIME-EXECUTION-CONTRACT.md §8.77.
|
|
306
|
+
"""Keep hygiene outermost and guard disabled tools before HITL runs.
|
|
307
|
+
|
|
308
|
+
After-model hooks run in reverse order; see the execution contract §8.77.
|
|
276
309
|
"""
|
|
277
310
|
capability_hitl: Mapping[str, CapabilityHitlBinding] | None = (
|
|
278
311
|
capability_block.hitl if capability_block is not None else None
|
|
279
312
|
)
|
|
280
313
|
middleware: list[AgentMiddleware] = [
|
|
314
|
+
CheckpointHygieneMiddleware(
|
|
315
|
+
max_history_messages=None,
|
|
316
|
+
binding=binding,
|
|
317
|
+
kpi=kpi,
|
|
318
|
+
),
|
|
281
319
|
*(capability_block.middleware if capability_block is not None else ()),
|
|
320
|
+
RateLimitRetryMiddleware(kpi=kpi, binding=binding),
|
|
282
321
|
TracingKpiMiddleware(
|
|
283
322
|
tracer=tracer,
|
|
284
323
|
kpi=kpi,
|
|
285
324
|
binding=binding,
|
|
286
325
|
),
|
|
287
|
-
ToolObservabilityMiddleware(kpi=kpi, binding=binding),
|
|
288
|
-
FredHitlMiddleware(
|
|
326
|
+
ToolObservabilityMiddleware(kpi=kpi, binding=binding, tracer=tracer),
|
|
327
|
+
(DeepChildHitlMiddleware if child else FredHitlMiddleware)(
|
|
289
328
|
binding=binding,
|
|
290
329
|
approval_policy=approval_policy,
|
|
291
330
|
available_tool_names=available_tool_names,
|
|
@@ -119,6 +119,7 @@ from fred_runtime.common.kf_workspace_client import (
|
|
|
119
119
|
from fred_runtime.common.mcp_runtime import MCPRuntime
|
|
120
120
|
from fred_runtime.common.structures import AgentSettingsLike
|
|
121
121
|
from fred_runtime.common.table_hits import repair_table_hits
|
|
122
|
+
from fred_runtime.react.react_tracing import RUNTIME_TOOL_SPAN_NAME
|
|
122
123
|
from fred_runtime.runtime_context import get_runtime_context
|
|
123
124
|
from fred_runtime.runtime_support import (
|
|
124
125
|
get_attachment_uids,
|
|
@@ -135,7 +136,7 @@ logger = logging.getLogger(__name__)
|
|
|
135
136
|
_TRACE_MODEL_SPAN_NAMES = frozenset({"v2.graph.model", "v2.react.model"})
|
|
136
137
|
_TRACE_AWAIT_HUMAN_SPAN_NAMES = frozenset({"v2.graph.await_human"})
|
|
137
138
|
_TRACE_TOOL_SPAN_NAMES = frozenset(
|
|
138
|
-
{"v2.graph.tool", "v2.graph.runtime_tool", "tool.invoke"}
|
|
139
|
+
{"v2.graph.tool", "v2.graph.runtime_tool", RUNTIME_TOOL_SPAN_NAME, "tool.invoke"}
|
|
139
140
|
)
|
|
140
141
|
_TRACE_AGENT_SPAN_NAMES = frozenset({"agent.invoke", "agent.stream"})
|
|
141
142
|
# Tool-shaped spans that are not the generic tool invoker.
|
|
@@ -36,24 +36,30 @@ The frame, in `create_agent` middleware list order:
|
|
|
36
36
|
Capability middleware stacks are inserted here, sorted by capability id
|
|
37
37
|
(RFC §5.3). Their `wrap_model_call` nests inside the platform prompt and
|
|
38
38
|
outside tracing, so observability always records the final request.
|
|
39
|
-
4.
|
|
39
|
+
4. RateLimitRetryMiddleware — retries a model call the provider
|
|
40
|
+
rate-limited (HTTP 429), with backoff + jitter, bounded by both an
|
|
41
|
+
attempt count and a wall-clock budget. Sits just OUTSIDE tracing so
|
|
42
|
+
each attempt is its own span / `llm.call_latency_ms` sample, and
|
|
43
|
+
INSIDE the capability block so hygiene, prompt and capability
|
|
44
|
+
middleware are computed once for the whole retried call.
|
|
45
|
+
5. TracingKpiMiddleware — innermost `wrap_model_call`: the
|
|
40
46
|
`v2.react.model` span, `llm.call_latency_ms` KPI timer, and the
|
|
41
47
|
`[LLM][CALL]`/`[LLM][RESPONSE]` logs measure/describe the bare model
|
|
42
48
|
call, exactly as the legacy `reasoner` node did. The model itself is
|
|
43
49
|
resolved once per turn at runtime activation (`ChatModelFactoryPort.build`)
|
|
44
50
|
— no per-call routing middleware sits in this frame.
|
|
45
|
-
|
|
51
|
+
6. ToolObservabilityMiddleware — `wrap_tool_call`: `agent.tool_latency_ms`
|
|
46
52
|
/ `agent.tool_failed_total` KPI and `agent.tool.invocation.
|
|
47
53
|
{started,completed}` audit events for EVERY tool call the tool node
|
|
48
54
|
executes — MCP-catalog tools and capability-native tools alike (#2011).
|
|
49
55
|
Placed next to TracingKpiMiddleware (same job, the other axis: model
|
|
50
56
|
calls vs tool calls).
|
|
51
|
-
|
|
57
|
+
7. FredHitlMiddleware — `after_model`: filesystem tool-argument
|
|
52
58
|
rewrite + the human tool-approval gate (RFC §5.4). ONE combined
|
|
53
59
|
`interrupt()` per turn covering every gated call at once (#2177
|
|
54
60
|
batching) with the `HumanInputRequest` payload; cancel jumps back to
|
|
55
61
|
the model without executing any tool of the batch.
|
|
56
|
-
|
|
62
|
+
8. ToolCallLimitMiddleware — LangChain prebuilt, appended only when
|
|
57
63
|
`max_tool_calls_per_turn` is set. Listed AFTER FredHitl on purpose:
|
|
58
64
|
`after_model` hooks run in REVERSE list order, so the limit blocks
|
|
59
65
|
over-limit calls BEFORE a human is asked to approve them.
|
|
@@ -83,6 +89,7 @@ from .hitl import (
|
|
|
83
89
|
FredHitlMiddleware,
|
|
84
90
|
build_tool_approval_request,
|
|
85
91
|
)
|
|
92
|
+
from .rate_limit_retry import ProviderRateLimitError, RateLimitRetryMiddleware
|
|
86
93
|
from .tool_observability import ToolObservabilityMiddleware
|
|
87
94
|
from .tracing_kpi import TracingKpiMiddleware
|
|
88
95
|
|
|
@@ -91,6 +98,8 @@ __all__ = [
|
|
|
91
98
|
"CheckpointHygieneMiddleware",
|
|
92
99
|
"DynamicPromptMiddleware",
|
|
93
100
|
"FredHitlMiddleware",
|
|
101
|
+
"ProviderRateLimitError",
|
|
102
|
+
"RateLimitRetryMiddleware",
|
|
94
103
|
"ToolObservabilityMiddleware",
|
|
95
104
|
"TracingKpiMiddleware",
|
|
96
105
|
"build_react_platform_middleware_frame",
|
{fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/middleware/checkpoint_hygiene.py
RENAMED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
-
"""CheckpointHygieneMiddleware — request-scoped model-input hygiene
|
|
15
|
+
"""CheckpointHygieneMiddleware — request-scoped model-input hygiene."""
|
|
16
16
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
@@ -31,6 +31,7 @@ from fred_runtime.support.tool_loop import (
|
|
|
31
31
|
ChatTurnTooLargeError,
|
|
32
32
|
collect_tool_outputs,
|
|
33
33
|
sanitize_dangling_tool_calls,
|
|
34
|
+
strip_message_names,
|
|
34
35
|
total_char_len,
|
|
35
36
|
trim_to_char_budget,
|
|
36
37
|
trim_to_human_boundary,
|
|
@@ -80,7 +81,9 @@ class CheckpointHygieneMiddleware(AgentMiddleware):
|
|
|
80
81
|
request: ModelRequest,
|
|
81
82
|
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
|
|
82
83
|
) -> ModelResponse:
|
|
83
|
-
messages =
|
|
84
|
+
messages = strip_message_names(
|
|
85
|
+
sanitize_dangling_tool_calls(list(request.messages))
|
|
86
|
+
)
|
|
84
87
|
if self._max_history_messages is not None:
|
|
85
88
|
trimmed = trim_to_human_boundary(messages, self._max_history_messages)
|
|
86
89
|
logger.debug(
|
|
@@ -34,6 +34,7 @@ from langchain.agents.middleware import AgentMiddleware, ToolCallLimitMiddleware
|
|
|
34
34
|
from .checkpoint_hygiene import CheckpointHygieneMiddleware
|
|
35
35
|
from .dynamic_prompt import DynamicPromptMiddleware
|
|
36
36
|
from .hitl import CapabilityHitlBinding, FredHitlMiddleware
|
|
37
|
+
from .rate_limit_retry import RateLimitRetryMiddleware
|
|
37
38
|
from .tool_observability import ToolObservabilityMiddleware
|
|
38
39
|
from .tracing_kpi import TracingKpiMiddleware
|
|
39
40
|
|
|
@@ -81,6 +82,10 @@ def build_react_platform_middleware_frame(
|
|
|
81
82
|
DynamicPromptMiddleware(available_tool_names=available_tool_names),
|
|
82
83
|
# --- CAPABILITY BLOCK INSERTION SLOT (#1973, RFC §5.3) ---
|
|
83
84
|
*capability_middleware,
|
|
85
|
+
# Just outside tracing on purpose: every retried attempt is then a real
|
|
86
|
+
# span and a real latency sample, while hygiene, prompt and capability
|
|
87
|
+
# middleware are computed once for the whole retried call.
|
|
88
|
+
RateLimitRetryMiddleware(kpi=kpi, binding=binding),
|
|
84
89
|
TracingKpiMiddleware(
|
|
85
90
|
tracer=tracer,
|
|
86
91
|
kpi=kpi,
|
|
@@ -95,7 +100,7 @@ def build_react_platform_middleware_frame(
|
|
|
95
100
|
# actually executes, i.e. strictly after FredHitl's `after_model`
|
|
96
101
|
# gate has already let the call through (a HITL-refused proposal
|
|
97
102
|
# never reaches here, so it never produces a "started" event).
|
|
98
|
-
ToolObservabilityMiddleware(kpi=kpi, binding=binding),
|
|
103
|
+
ToolObservabilityMiddleware(kpi=kpi, binding=binding, tracer=tracer),
|
|
99
104
|
FredHitlMiddleware(
|
|
100
105
|
binding=binding,
|
|
101
106
|
approval_policy=approval_policy,
|
|
@@ -18,7 +18,7 @@ from __future__ import annotations
|
|
|
18
18
|
|
|
19
19
|
import json
|
|
20
20
|
import logging
|
|
21
|
-
from collections.abc import Mapping, Sequence
|
|
21
|
+
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
|
22
22
|
from dataclasses import dataclass
|
|
23
23
|
from typing import Any
|
|
24
24
|
|
|
@@ -35,7 +35,9 @@ from fred_sdk.contracts.runtime import (
|
|
|
35
35
|
PendingToolCall,
|
|
36
36
|
)
|
|
37
37
|
from langchain.agents.middleware import AgentMiddleware, AgentState
|
|
38
|
-
from langchain.agents.middleware.types import hook_config
|
|
38
|
+
from langchain.agents.middleware.types import ModelRequest, ModelResponse, hook_config
|
|
39
|
+
from langchain_core.messages import ToolMessage
|
|
40
|
+
from langchain_core.tools import BaseTool
|
|
39
41
|
from langgraph.runtime import Runtime
|
|
40
42
|
from langgraph.types import interrupt
|
|
41
43
|
|
|
@@ -478,6 +480,11 @@ class FredHitlMiddleware(AgentMiddleware):
|
|
|
478
480
|
# so a partial per-call answer was never meaningful even before this.
|
|
479
481
|
if not gated:
|
|
480
482
|
return None
|
|
483
|
+
return self._resolve_approval(gated)
|
|
484
|
+
|
|
485
|
+
def _resolve_approval(
|
|
486
|
+
self, gated: Sequence[GatedToolCall]
|
|
487
|
+
) -> dict[str, Any] | None:
|
|
481
488
|
request = build_tool_approval_request(binding=self._binding, calls=gated)
|
|
482
489
|
decision = interrupt(request.model_dump(mode="json"))
|
|
483
490
|
if _is_cancelled_human_decision(decision):
|
|
@@ -485,3 +492,55 @@ class FredHitlMiddleware(AgentMiddleware):
|
|
|
485
492
|
# let the model replan.
|
|
486
493
|
return {"jump_to": "model"}
|
|
487
494
|
return None
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
class DeepChildHitlMiddleware(FredHitlMiddleware):
|
|
498
|
+
"""Native children reuse the gate but cannot open a human approval wait."""
|
|
499
|
+
|
|
500
|
+
async def awrap_model_call(
|
|
501
|
+
self,
|
|
502
|
+
request: ModelRequest,
|
|
503
|
+
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
|
|
504
|
+
) -> ModelResponse:
|
|
505
|
+
def visible(tool: BaseTool | dict[str, Any]) -> bool:
|
|
506
|
+
if isinstance(tool, BaseTool):
|
|
507
|
+
name = tool.name
|
|
508
|
+
else:
|
|
509
|
+
function = tool.get("function")
|
|
510
|
+
name = (
|
|
511
|
+
function.get("name")
|
|
512
|
+
if isinstance(function, dict)
|
|
513
|
+
else tool.get("name")
|
|
514
|
+
)
|
|
515
|
+
if not name:
|
|
516
|
+
return True
|
|
517
|
+
bound = self._capability_hitl.get(name)
|
|
518
|
+
return not (
|
|
519
|
+
self._requires_human_approval(name)
|
|
520
|
+
or (bound is not None and bound.spec.require)
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
kept = [tool for tool in request.tools if visible(tool)]
|
|
524
|
+
if len(kept) != len(request.tools):
|
|
525
|
+
request = request.override(tools=kept)
|
|
526
|
+
return await handler(request)
|
|
527
|
+
|
|
528
|
+
def _resolve_approval(self, gated: Sequence[GatedToolCall]) -> dict[str, Any]:
|
|
529
|
+
# A result prevents execution of that call while allowing ungated calls
|
|
530
|
+
# in the same batch. Without a call ID, fail closed on the whole batch.
|
|
531
|
+
if not all(call.tool_call_id for call in gated):
|
|
532
|
+
return {"jump_to": "model"}
|
|
533
|
+
return {
|
|
534
|
+
"messages": [
|
|
535
|
+
ToolMessage(
|
|
536
|
+
content=(
|
|
537
|
+
"This tool requires human approval, which is unavailable in a "
|
|
538
|
+
"delegated task. Ask the parent agent to run it directly."
|
|
539
|
+
),
|
|
540
|
+
tool_call_id=call.tool_call_id or "",
|
|
541
|
+
name=call.tool_name,
|
|
542
|
+
status="error",
|
|
543
|
+
)
|
|
544
|
+
for call in gated
|
|
545
|
+
]
|
|
546
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Copyright Thales 2026
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""RateLimitRetryMiddleware — absorb provider 429s instead of failing the turn."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import asyncio
|
|
20
|
+
import logging
|
|
21
|
+
import random
|
|
22
|
+
import time
|
|
23
|
+
from collections.abc import Awaitable, Callable
|
|
24
|
+
|
|
25
|
+
from fred_core import is_rate_limit
|
|
26
|
+
from fred_core.kpi import BaseKPIWriter, KPIActor
|
|
27
|
+
from fred_core.kpi.kpi_writer_structures import MetricNames
|
|
28
|
+
from fred_sdk.contracts.context import BoundRuntimeContext
|
|
29
|
+
from langchain.agents.middleware import AgentMiddleware
|
|
30
|
+
from langchain.agents.middleware.types import ModelRequest, ModelResponse
|
|
31
|
+
|
|
32
|
+
from ..react_model_adapter import extract_model_name_from_object
|
|
33
|
+
from .shared import identity_kpi_dims
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
# The scheduling budget complements the model factory transport timeouts.
|
|
38
|
+
_MAX_ATTEMPTS = 4
|
|
39
|
+
_RETRY_BUDGET_S = 60.0
|
|
40
|
+
_BACKOFF_BASE_S = 2.0
|
|
41
|
+
_BACKOFF_CAP_S = 30.0
|
|
42
|
+
# Below this much budget left, waiting is not worth the turn's remaining time.
|
|
43
|
+
_MIN_USEFUL_WAIT_S = 1.0
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ProviderRateLimitError(RuntimeError):
|
|
47
|
+
"""Raised when a model call stays rate-limited after every retry.
|
|
48
|
+
|
|
49
|
+
Its message reaches the user as the turn's `execution_error`, so it names
|
|
50
|
+
the condition in plain language instead of leaking provider JSON.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(self, *, attempts: int, elapsed_s: float) -> None:
|
|
54
|
+
plural = "" if attempts == 1 else "s"
|
|
55
|
+
super().__init__(
|
|
56
|
+
"The model provider is rate-limiting this deployment. Fred gave up "
|
|
57
|
+
f"after {attempts} attempt{plural} over {elapsed_s:.0f}s. "
|
|
58
|
+
"Please send your message again in a moment."
|
|
59
|
+
)
|
|
60
|
+
self.attempts = attempts
|
|
61
|
+
self.elapsed_s = elapsed_s
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class RateLimitRetryMiddleware(AgentMiddleware):
|
|
65
|
+
"""Retry rejected model calls without replaying the agent graph or tools."""
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
*,
|
|
70
|
+
kpi: BaseKPIWriter | None,
|
|
71
|
+
binding: BoundRuntimeContext,
|
|
72
|
+
) -> None:
|
|
73
|
+
super().__init__()
|
|
74
|
+
self._kpi = kpi
|
|
75
|
+
self._binding = binding
|
|
76
|
+
|
|
77
|
+
async def awrap_model_call(
|
|
78
|
+
self,
|
|
79
|
+
request: ModelRequest,
|
|
80
|
+
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
|
|
81
|
+
) -> ModelResponse:
|
|
82
|
+
started = time.monotonic()
|
|
83
|
+
|
|
84
|
+
for attempt in range(_MAX_ATTEMPTS):
|
|
85
|
+
try:
|
|
86
|
+
return await handler(request)
|
|
87
|
+
except Exception as exc:
|
|
88
|
+
throttled, retry_after = is_rate_limit(exc)
|
|
89
|
+
if not throttled:
|
|
90
|
+
raise
|
|
91
|
+
|
|
92
|
+
# Resolved here, not before the call: the happy path runs on
|
|
93
|
+
# every model call of every turn and must stay free of work
|
|
94
|
+
# only the throttled path needs.
|
|
95
|
+
model_name = extract_model_name_from_object(request.model)
|
|
96
|
+
elapsed = time.monotonic() - started
|
|
97
|
+
remaining = _RETRY_BUDGET_S - elapsed
|
|
98
|
+
delay = self._delay_for(attempt, retry_after)
|
|
99
|
+
exhausted = (
|
|
100
|
+
attempt == _MAX_ATTEMPTS - 1
|
|
101
|
+
or remaining < _MIN_USEFUL_WAIT_S
|
|
102
|
+
or delay >= remaining
|
|
103
|
+
)
|
|
104
|
+
self._record(model_name, exhausted=exhausted)
|
|
105
|
+
|
|
106
|
+
if exhausted:
|
|
107
|
+
logger.error(
|
|
108
|
+
"model=%s rate limit persisted after %d attempts in %.1fs; "
|
|
109
|
+
"failing the turn",
|
|
110
|
+
model_name,
|
|
111
|
+
attempt + 1,
|
|
112
|
+
elapsed,
|
|
113
|
+
)
|
|
114
|
+
raise ProviderRateLimitError(
|
|
115
|
+
attempts=attempt + 1, elapsed_s=elapsed
|
|
116
|
+
) from exc
|
|
117
|
+
|
|
118
|
+
logger.warning(
|
|
119
|
+
"model=%s rate-limited (attempt %d/%d); backing off %.1fs",
|
|
120
|
+
model_name,
|
|
121
|
+
attempt + 1,
|
|
122
|
+
_MAX_ATTEMPTS,
|
|
123
|
+
delay,
|
|
124
|
+
)
|
|
125
|
+
await asyncio.sleep(delay)
|
|
126
|
+
# Scheduling delays can overshoot: do not start a late attempt.
|
|
127
|
+
elapsed = time.monotonic() - started
|
|
128
|
+
if elapsed >= _RETRY_BUDGET_S:
|
|
129
|
+
raise ProviderRateLimitError(
|
|
130
|
+
attempts=attempt + 1, elapsed_s=elapsed
|
|
131
|
+
) from exc
|
|
132
|
+
|
|
133
|
+
raise AssertionError("unreachable: the loop returns or raises")
|
|
134
|
+
|
|
135
|
+
def _delay_for(self, attempt: int, retry_after: float | None) -> float:
|
|
136
|
+
"""Honor provider minimum delays and spread fallback retries."""
|
|
137
|
+
|
|
138
|
+
if retry_after is not None and retry_after >= 0:
|
|
139
|
+
return retry_after + random.uniform(0, 1.0) # nosec B311 — retry jitter, not security-sensitive
|
|
140
|
+
ceiling = min(_BACKOFF_CAP_S, _BACKOFF_BASE_S * (2**attempt))
|
|
141
|
+
return random.uniform(ceiling / 2, ceiling) # nosec B311 — retry jitter, not security-sensitive
|
|
142
|
+
|
|
143
|
+
def _record(self, model_name: str | None, *, exhausted: bool) -> None:
|
|
144
|
+
if self._kpi is None:
|
|
145
|
+
return
|
|
146
|
+
# `model_name` is set unconditionally: PrometheusKPIStore freezes a
|
|
147
|
+
# metric's label set from its FIRST event, so a dim omitted on the first
|
|
148
|
+
# 429 of a pod's life is dropped from the counter for good.
|
|
149
|
+
dims: dict[str, str | None] = {
|
|
150
|
+
**identity_kpi_dims(self._binding),
|
|
151
|
+
"status": "error" if exhausted else "ok",
|
|
152
|
+
"model_name": model_name or "unknown",
|
|
153
|
+
}
|
|
154
|
+
self._kpi.count(
|
|
155
|
+
MetricNames.LLM_RATE_LIMITS,
|
|
156
|
+
1,
|
|
157
|
+
dims=dims,
|
|
158
|
+
actor=KPIActor(type="system"),
|
|
159
|
+
)
|
{fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/middleware/tool_observability.py
RENAMED
|
@@ -83,6 +83,7 @@ from fred_core.logs.audit_log import emit_audit_log
|
|
|
83
83
|
from fred_core.security.models import Resource
|
|
84
84
|
from fred_core.security.rebac.rebac_engine import RebacReference, TeamPermission
|
|
85
85
|
from fred_sdk.contracts.context import BoundRuntimeContext
|
|
86
|
+
from fred_sdk.contracts.runtime import SpanPort, TracerPort
|
|
86
87
|
from langchain.agents.middleware import AgentMiddleware
|
|
87
88
|
from langchain_core.messages.tool import ToolMessage
|
|
88
89
|
from langgraph.prebuilt.tool_node import ToolCallRequest
|
|
@@ -91,6 +92,9 @@ from langgraph.types import Command
|
|
|
91
92
|
from fred_runtime.common.context_aware_tool import ContextAwareTool
|
|
92
93
|
from fred_runtime.runtime_context import get_runtime_context
|
|
93
94
|
|
|
95
|
+
from ..react_tool_binding import SELF_TRACED_TOOL_METADATA_KEY
|
|
96
|
+
from ..react_tracing import RUNTIME_TOOL_SPAN_NAME, tool_span
|
|
97
|
+
|
|
94
98
|
logger = logging.getLogger(__name__)
|
|
95
99
|
|
|
96
100
|
|
|
@@ -115,10 +119,12 @@ class ToolObservabilityMiddleware(AgentMiddleware):
|
|
|
115
119
|
*,
|
|
116
120
|
kpi: BaseKPIWriter | None,
|
|
117
121
|
binding: BoundRuntimeContext,
|
|
122
|
+
tracer: TracerPort | None = None,
|
|
118
123
|
) -> None:
|
|
119
124
|
super().__init__()
|
|
120
125
|
self._kpi = kpi
|
|
121
126
|
self._binding = binding
|
|
127
|
+
self._tracer = tracer
|
|
122
128
|
|
|
123
129
|
def _base_dims(self, *, tool_name: str, source: str) -> dict[str, Optional[str]]:
|
|
124
130
|
"""
|
|
@@ -211,10 +217,51 @@ class ToolObservabilityMiddleware(AgentMiddleware):
|
|
|
211
217
|
RebacReference(Resource.TEAM, team_id),
|
|
212
218
|
)
|
|
213
219
|
|
|
220
|
+
def _span_tracer(self, request: ToolCallRequest) -> TracerPort | None:
|
|
221
|
+
"""Skip binder-owned spans; middleware tools need their own span."""
|
|
222
|
+
|
|
223
|
+
metadata = getattr(request.tool, "metadata", None) or {}
|
|
224
|
+
if metadata.get(SELF_TRACED_TOOL_METADATA_KEY):
|
|
225
|
+
return None
|
|
226
|
+
return self._tracer
|
|
227
|
+
|
|
214
228
|
async def awrap_tool_call(
|
|
215
229
|
self,
|
|
216
230
|
request: ToolCallRequest,
|
|
217
231
|
handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]],
|
|
232
|
+
) -> ToolMessage | Command[Any]:
|
|
233
|
+
tracer = self._span_tracer(request)
|
|
234
|
+
tool_call = request.tool_call
|
|
235
|
+
async with tool_span(
|
|
236
|
+
tracer,
|
|
237
|
+
name=RUNTIME_TOOL_SPAN_NAME,
|
|
238
|
+
context=self._binding.portable_context,
|
|
239
|
+
attributes={"tool_name": self._tool_name(request)},
|
|
240
|
+
input_payload=tool_call.get("args")
|
|
241
|
+
if isinstance(tool_call, dict)
|
|
242
|
+
else None,
|
|
243
|
+
) as span:
|
|
244
|
+
result = await self._observe_tool_call(request, handler, span=span)
|
|
245
|
+
if span is not None and tracer is not None and tracer.captures_content:
|
|
246
|
+
output = getattr(result, "content", None)
|
|
247
|
+
if isinstance(result, Command) and isinstance(result.update, dict):
|
|
248
|
+
messages = result.update.get("messages", [])
|
|
249
|
+
if isinstance(messages, (list, tuple)):
|
|
250
|
+
output = [
|
|
251
|
+
message.content
|
|
252
|
+
for message in messages
|
|
253
|
+
if isinstance(message, ToolMessage)
|
|
254
|
+
and message.tool_call_id == tool_call.get("id")
|
|
255
|
+
]
|
|
256
|
+
span.set_io(output=output)
|
|
257
|
+
return result
|
|
258
|
+
|
|
259
|
+
async def _observe_tool_call(
|
|
260
|
+
self,
|
|
261
|
+
request: ToolCallRequest,
|
|
262
|
+
handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]],
|
|
263
|
+
*,
|
|
264
|
+
span: SpanPort | None,
|
|
218
265
|
) -> ToolMessage | Command[Any]:
|
|
219
266
|
tool_name = self._tool_name(request)
|
|
220
267
|
# `ContextAwareTool` wraps every MCP-catalog tool (`mcp_toolkit.py`);
|
|
@@ -341,6 +388,9 @@ class ToolObservabilityMiddleware(AgentMiddleware):
|
|
|
341
388
|
if status_is_error
|
|
342
389
|
else "tool_error_artifact"
|
|
343
390
|
)
|
|
391
|
+
if span is not None:
|
|
392
|
+
span.set_attribute("status", "error")
|
|
393
|
+
span.set_attribute("error_type", error_code)
|
|
344
394
|
if kpi_dims is not None:
|
|
345
395
|
# `status` only — see the timer comment above.
|
|
346
396
|
kpi_dims["status"] = "error"
|