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.
Files changed (223) hide show
  1. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/PKG-INFO +8 -3
  2. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/README.md +5 -0
  3. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/deep/deep_runtime.py +58 -19
  4. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/integrations/v2_runtime/adapters.py +2 -1
  5. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/middleware/__init__.py +13 -4
  6. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/middleware/checkpoint_hygiene.py +5 -2
  7. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/middleware/frame.py +6 -1
  8. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/middleware/hitl.py +61 -2
  9. fred_runtime-4.1.0/fred_runtime/react/middleware/rate_limit_retry.py +159 -0
  10. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/middleware/tool_observability.py +50 -0
  11. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/react_tool_binding.py +15 -29
  12. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/react_tool_resolution.py +2 -1
  13. fred_runtime-4.1.0/fred_runtime/react/react_tracing.py +86 -0
  14. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/support/tool_loop.py +10 -0
  15. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime.egg-info/PKG-INFO +8 -3
  16. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime.egg-info/SOURCES.txt +2 -0
  17. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime.egg-info/requires.txt +2 -2
  18. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/pyproject.toml +4 -3
  19. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_deep_agent_hitl_integration.py +431 -3
  20. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_deep_agent_middleware.py +240 -13
  21. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_hitl_resume_langgraph_integration.py +8 -2
  22. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_langfuse_tracing.py +8 -0
  23. fred_runtime-4.1.0/tests/test_rate_limit_retry_middleware.py +554 -0
  24. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_react_middleware_frame.py +5 -2
  25. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_tool_loop_trim.py +29 -0
  26. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_tool_observability_middleware.py +328 -2
  27. fred_runtime-4.0.0/fred_runtime/react/react_tracing.py +0 -31
  28. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/__init__.py +0 -0
  29. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/__main__.py +0 -0
  30. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/app/__init__.py +0 -0
  31. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/app/_catalogs.py +0 -0
  32. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/app/agent_app.py +0 -0
  33. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/app/chat_input_limit.py +0 -0
  34. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/app/config.py +0 -0
  35. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/app/config_loader.py +0 -0
  36. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/app/container.py +0 -0
  37. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/app/context.py +0 -0
  38. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/app/dependencies.py +0 -0
  39. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/app/mcp_config.py +0 -0
  40. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/app/observability_factory.py +0 -0
  41. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/app/openai_compat_router.py +0 -0
  42. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/app/platform_sql.py +0 -0
  43. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/__init__.py +0 -0
  44. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/assembly.py +0 -0
  45. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/assets.py +0 -0
  46. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/demo.py +0 -0
  47. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/demo_migrations/env.py +0 -0
  48. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/demo_migrations/versions/d1e2m0a0b0c0_create_demo_echo_notes.py +0 -0
  49. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/document_access/__init__.py +0 -0
  50. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/document_access/capability.py +0 -0
  51. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/document_extract/__init__.py +0 -0
  52. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/document_extract/capability.py +0 -0
  53. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/document_label_search/__init__.py +0 -0
  54. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/document_label_search/capability.py +0 -0
  55. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/document_read_common.py +0 -0
  56. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/document_similarity/__init__.py +0 -0
  57. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/document_similarity/capability.py +0 -0
  58. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/document_summarize/__init__.py +0 -0
  59. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/document_summarize/capability.py +0 -0
  60. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/document_verbatim/__init__.py +0 -0
  61. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/document_verbatim/capability.py +0 -0
  62. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/errors.py +0 -0
  63. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/mcp.py +0 -0
  64. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/openapi_dump.py +0 -0
  65. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/capabilities/registry.py +0 -0
  66. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/cli/__init__.py +0 -0
  67. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/cli/completion.py +0 -0
  68. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/cli/entrypoint.py +0 -0
  69. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/cli/history_display.py +0 -0
  70. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/cli/kpi_display.py +0 -0
  71. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/cli/pod_client.py +0 -0
  72. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/cli/repl.py +0 -0
  73. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/cli/repl_helpers.py +0 -0
  74. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/cli/url_helpers.py +0 -0
  75. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/client.py +0 -0
  76. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/__init__.py +0 -0
  77. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/context_aware_tool.py +0 -0
  78. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/kf_base_client.py +0 -0
  79. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/kf_document_client.py +0 -0
  80. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/kf_fast_text_client.py +0 -0
  81. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/kf_http_client.py +0 -0
  82. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/kf_markdown_media_client.py +0 -0
  83. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/kf_tag_client.py +0 -0
  84. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/kf_vectorsearch_client.py +0 -0
  85. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/kf_workspace_client.py +0 -0
  86. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/mcp_interceptors.py +0 -0
  87. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/mcp_runtime.py +0 -0
  88. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/mcp_toolkit.py +0 -0
  89. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/mcp_utils.py +0 -0
  90. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/structures.py +0 -0
  91. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/table_hits.py +0 -0
  92. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/token_expiry.py +0 -0
  93. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/common/tool_node_utils.py +0 -0
  94. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/deep/__init__.py +0 -0
  95. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/eval/__init__.py +0 -0
  96. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/eval/collector.py +0 -0
  97. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/graph/__init__.py +0 -0
  98. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/graph/graph_runtime.py +0 -0
  99. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/integrations/__init__.py +0 -0
  100. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/integrations/inprocess_toolkit_registry.py +0 -0
  101. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/integrations/kf_vector_search/__init__.py +0 -0
  102. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/integrations/kf_vector_search/toolkit.py +0 -0
  103. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/integrations/v2_runtime/__init__.py +0 -0
  104. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/migrations/__init__.py +0 -0
  105. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/migrations/env.py +0 -0
  106. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/migrations/script.py.mako +0 -0
  107. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/migrations/versions/__init__.py +0 -0
  108. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/migrations/versions/a1e2f3c4d5b6_create_session_history.py +0 -0
  109. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/migrations/versions/b2f3a4e5c6d7_add_exchange_id.py +0 -0
  110. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/migrations/versions/c3d4b5a6f7e8_add_team_and_instance.py +0 -0
  111. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/model_routing/__init__.py +0 -0
  112. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/model_routing/authz.py +0 -0
  113. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/model_routing/catalog.py +0 -0
  114. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/model_routing/contracts.py +0 -0
  115. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/model_routing/provider.py +0 -0
  116. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/model_routing/resolver.py +0 -0
  117. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/__init__.py +0 -0
  118. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/middleware/dynamic_prompt.py +0 -0
  119. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/middleware/shared.py +0 -0
  120. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/middleware/tracing_kpi.py +0 -0
  121. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/react_langchain_adapter.py +0 -0
  122. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/react_message_codec.py +0 -0
  123. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/react_model_adapter.py +0 -0
  124. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/react_prompting.py +0 -0
  125. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/react_runtime.py +0 -0
  126. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/react_stream_adapter.py +0 -0
  127. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/react_tool_loop.py +0 -0
  128. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/react_tool_rendering.py +0 -0
  129. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/react/react_tool_utils.py +0 -0
  130. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/runtime_context.py +0 -0
  131. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/runtime_support/__init__.py +0 -0
  132. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/runtime_support/checkpoints.py +0 -0
  133. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/runtime_support/model_metadata.py +0 -0
  134. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/runtime_support/request_context_helpers.py +0 -0
  135. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/runtime_support/sql_checkpointer.py +0 -0
  136. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/runtime_support/trace_payloads.py +0 -0
  137. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/runtime_support/user_token_refresher.py +0 -0
  138. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/support/__init__.py +0 -0
  139. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/support/filesystem_context.py +0 -0
  140. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime/support/thinking.py +0 -0
  141. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime.egg-info/dependency_links.txt +0 -0
  142. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime.egg-info/entry_points.txt +0 -0
  143. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/fred_runtime.egg-info/top_level.txt +0 -0
  144. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/setup.cfg +0 -0
  145. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_agent_app.py +0 -0
  146. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_capability_agent_1973.py +0 -0
  147. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_capability_chat_controls_1976.py +0 -0
  148. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_capability_chat_parts_1977.py +0 -0
  149. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_capability_document_access_1906.py +0 -0
  150. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_capability_document_label_search.py +0 -0
  151. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_capability_document_reading.py +0 -0
  152. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_capability_document_similarity.py +0 -0
  153. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_capability_document_summarize.py +0 -0
  154. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_capability_endpoints_1974.py +0 -0
  155. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_capability_registry_1973.py +0 -0
  156. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_capability_routes_tables_1979.py +0 -0
  157. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_capability_selection_1974.py +0 -0
  158. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_capability_tool_return_convention.py +0 -0
  159. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_chat_input_limit.py +0 -0
  160. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_checkpoint_namespace.py +0 -0
  161. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_client.py +0 -0
  162. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_config_loader.py +0 -0
  163. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_context.py +0 -0
  164. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_context_aware_tool.py +0 -0
  165. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_conversational_memory.py +0 -0
  166. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_deep_agent_dispatch.py +0 -0
  167. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_document_port_error_redaction.py +0 -0
  168. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_document_read_scope.py +0 -0
  169. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_document_similarity_adapter.py +0 -0
  170. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_eval_collector.py +0 -0
  171. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_eval_trace.py +0 -0
  172. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_fred_workspace_fs.py +0 -0
  173. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_graph_capability_bridge.py +0 -0
  174. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_graph_checkpoint_namespace.py +0 -0
  175. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_graph_runtime_invoke_agent.py +0 -0
  176. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_graph_runtime_observability.py +0 -0
  177. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_graph_runtime_token_usage_totals.py +0 -0
  178. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_history.py +0 -0
  179. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_history_display.py +0 -0
  180. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_kf_base_client_refresh_shapes.py +0 -0
  181. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_kf_vector_search_toolkit.py +0 -0
  182. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_kf_workspace_client.py +0 -0
  183. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_knowledge_search_table_ordering.py +0 -0
  184. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_knowledge_search_tool_invoker_sources.py +0 -0
  185. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_kpi_display.py +0 -0
  186. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_langgraph_interrupt_id_semantics.py +0 -0
  187. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_mcp_config.py +0 -0
  188. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_migrations.py +0 -0
  189. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_model_enforcement.py +0 -0
  190. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_model_metadata_cache_tokens.py +0 -0
  191. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_model_reasoning_enablement.py +0 -0
  192. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_model_routing.py +0 -0
  193. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_model_routing_provider.py +0 -0
  194. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_models_catalog_projection.py +0 -0
  195. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_openai_compat_router.py +0 -0
  196. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_platform_sql.py +0 -0
  197. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_platform_sql_postgres_integration.py +0 -0
  198. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_pod_client.py +0 -0
  199. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_react_checkpoint_namespace.py +0 -0
  200. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_react_loop_regressions_1972.py +0 -0
  201. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_react_message_codec_resume.py +0 -0
  202. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_react_prompting.py +0 -0
  203. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_react_thinking.py +0 -0
  204. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_react_token_usage_totals.py +0 -0
  205. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_react_tool_binding_span_status.py +0 -0
  206. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_react_tool_error_final_2244.py +0 -0
  207. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_react_tool_resolution.py +0 -0
  208. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_repl_helpers.py +0 -0
  209. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_runtime_context_prompt_injection.py +0 -0
  210. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_similarity_search_tool_invoker.py +0 -0
  211. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_smoke.py +0 -0
  212. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_spike_capability_state_1971.py +0 -0
  213. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_sql_checkpointer_hitl_claim.py +0 -0
  214. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_sql_checkpointer_owner.py +0 -0
  215. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_table_repair_wiring.py +0 -0
  216. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_team_wiki_adapter.py +0 -0
  217. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_token_expiry.py +0 -0
  218. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_tool_carrier_middleware_naming.py +0 -0
  219. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_trace_payloads.py +0 -0
  220. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_tracing_kpi_content_confinement.py +0 -0
  221. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_url_helpers.py +0 -0
  222. {fred_runtime-4.0.0 → fred_runtime-4.1.0}/tests/test_user_token_refresher.py +0 -0
  223. {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.0.0
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.0.0
16
- Requires-Dist: fred-sdk>=4.0.0
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
- if checkpointer is None:
218
- return cast(
219
- _CompiledReActAgent,
220
- create_deep_agent(
221
- model=model,
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
- Assemble Deep's middleware list: capability stack, platform observability,
271
- the HITL gate (capability-declared and operator-configured approval
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. TracingKpiMiddleware — innermost `wrap_model_call`: the
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
- 5. ToolObservabilityMiddleware — `wrap_tool_call`: `agent.tool_latency_ms`
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
- 6. FredHitlMiddleware — `after_model`: filesystem tool-argument
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
- 7. ToolCallLimitMiddleware — LangChain prebuilt, appended only when
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",
@@ -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 (#1972)."""
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 = sanitize_dangling_tool_calls(list(request.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
+ )
@@ -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"