fred-runtime 3.4.4__tar.gz → 3.4.5__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 (195) hide show
  1. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/PKG-INFO +3 -3
  2. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/app/agent_app.py +120 -49
  3. fred_runtime-3.4.5/fred_runtime/app/chat_input_limit.py +109 -0
  4. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/app/config.py +8 -0
  5. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/app/context.py +29 -4
  6. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/app/openai_compat_router.py +19 -11
  7. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/capabilities/document_access/capability.py +65 -13
  8. fred_runtime-3.4.5/fred_runtime/capabilities/document_extract/__init__.py +28 -0
  9. fred_runtime-3.4.5/fred_runtime/capabilities/document_extract/capability.py +197 -0
  10. fred_runtime-3.4.5/fred_runtime/capabilities/document_label_search/__init__.py +30 -0
  11. fred_runtime-3.4.5/fred_runtime/capabilities/document_label_search/capability.py +246 -0
  12. fred_runtime-3.4.5/fred_runtime/capabilities/document_read_common.py +216 -0
  13. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/capabilities/document_summarize/capability.py +19 -11
  14. fred_runtime-3.4.5/fred_runtime/capabilities/document_verbatim/__init__.py +27 -0
  15. fred_runtime-3.4.5/fred_runtime/capabilities/document_verbatim/capability.py +128 -0
  16. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/common/kf_base_client.py +35 -18
  17. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/common/kf_document_client.py +105 -2
  18. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/common/kf_fast_text_client.py +5 -5
  19. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/common/kf_tag_client.py +3 -2
  20. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/common/kf_workspace_client.py +7 -3
  21. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/common/mcp_interceptors.py +7 -3
  22. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/common/mcp_runtime.py +4 -3
  23. fred_runtime-3.4.5/fred_runtime/common/structures.py +109 -0
  24. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/graph/graph_runtime.py +4 -2
  25. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/integrations/v2_runtime/adapters.py +229 -29
  26. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/migrations.py +53 -0
  27. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/model_routing/contracts.py +12 -0
  28. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/model_routing/provider.py +1 -0
  29. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/model_routing/resolver.py +39 -19
  30. fred_runtime-3.4.5/fred_runtime/react/middleware/checkpoint_hygiene.py +216 -0
  31. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/middleware/frame.py +7 -1
  32. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/middleware/hitl.py +12 -8
  33. fred_runtime-3.4.5/fred_runtime/react/middleware/shared.py +81 -0
  34. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/middleware/tool_observability.py +69 -4
  35. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/react_prompting.py +5 -4
  36. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/react_runtime.py +48 -13
  37. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/react_stream_adapter.py +10 -1
  38. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/react_tool_loop.py +33 -0
  39. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/runtime_support/__init__.py +5 -1
  40. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/runtime_support/model_metadata.py +25 -5
  41. fred_runtime-3.4.5/fred_runtime/runtime_support/user_token_refresher.py +650 -0
  42. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/support/thinking.py +1 -1
  43. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/support/tool_loop.py +150 -25
  44. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime.egg-info/PKG-INFO +3 -3
  45. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime.egg-info/SOURCES.txt +15 -0
  46. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime.egg-info/entry_points.txt +3 -0
  47. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime.egg-info/requires.txt +2 -2
  48. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/pyproject.toml +13 -3
  49. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_agent_app.py +179 -4
  50. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_capability_document_access_1906.py +91 -3
  51. fred_runtime-3.4.5/tests/test_capability_document_label_search.py +377 -0
  52. fred_runtime-3.4.5/tests/test_capability_document_reading.py +483 -0
  53. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_capability_document_summarize.py +5 -0
  54. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_capability_endpoints_1974.py +13 -9
  55. fred_runtime-3.4.5/tests/test_chat_input_limit.py +84 -0
  56. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_config_loader.py +52 -2
  57. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_context.py +64 -1
  58. fred_runtime-3.4.5/tests/test_document_port_error_redaction.py +231 -0
  59. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_graph_runtime_token_usage_totals.py +2 -0
  60. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_history.py +43 -9
  61. fred_runtime-3.4.5/tests/test_kf_base_client_refresh_shapes.py +129 -0
  62. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_kf_vector_search_toolkit.py +1 -1
  63. fred_runtime-3.4.5/tests/test_model_metadata_cache_tokens.py +119 -0
  64. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_model_reasoning_enablement.py +5 -2
  65. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_model_routing.py +49 -0
  66. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_models_catalog_projection.py +135 -1
  67. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_openai_compat_router.py +82 -4
  68. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_react_loop_regressions_1972.py +259 -7
  69. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_react_thinking.py +1 -1
  70. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_react_token_usage_totals.py +55 -0
  71. fred_runtime-3.4.5/tests/test_react_tool_error_final_2244.py +261 -0
  72. fred_runtime-3.4.5/tests/test_tool_loop_trim.py +216 -0
  73. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_tool_observability_middleware.py +109 -0
  74. fred_runtime-3.4.5/tests/test_user_token_refresher.py +1090 -0
  75. fred_runtime-3.4.4/fred_runtime/common/structures.py +0 -53
  76. fred_runtime-3.4.4/fred_runtime/react/middleware/checkpoint_hygiene.py +0 -119
  77. fred_runtime-3.4.4/fred_runtime/react/middleware/shared.py +0 -43
  78. fred_runtime-3.4.4/fred_runtime/runtime_support/user_token_refresher.py +0 -60
  79. fred_runtime-3.4.4/tests/test_tool_loop_trim.py +0 -105
  80. fred_runtime-3.4.4/tests/test_user_token_refresher.py +0 -134
  81. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/README.md +0 -0
  82. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/__init__.py +0 -0
  83. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/__main__.py +0 -0
  84. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/app/__init__.py +0 -0
  85. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/app/_catalogs.py +0 -0
  86. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/app/config_loader.py +0 -0
  87. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/app/container.py +0 -0
  88. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/app/dependencies.py +0 -0
  89. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/app/mcp_config.py +0 -0
  90. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/app/observability_factory.py +0 -0
  91. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/capabilities/__init__.py +0 -0
  92. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/capabilities/assembly.py +0 -0
  93. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/capabilities/assets.py +0 -0
  94. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/capabilities/demo.py +0 -0
  95. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/capabilities/demo_migrations/env.py +0 -0
  96. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/capabilities/demo_migrations/versions/d1e2m0a0b0c0_create_demo_echo_notes.py +0 -0
  97. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/capabilities/document_access/__init__.py +0 -0
  98. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/capabilities/document_summarize/__init__.py +0 -0
  99. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/capabilities/errors.py +0 -0
  100. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/capabilities/mcp.py +0 -0
  101. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/capabilities/openapi_dump.py +0 -0
  102. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/capabilities/registry.py +0 -0
  103. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/cli/__init__.py +0 -0
  104. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/cli/completion.py +0 -0
  105. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/cli/entrypoint.py +0 -0
  106. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/cli/history_display.py +0 -0
  107. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/cli/kpi_display.py +0 -0
  108. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/cli/pod_client.py +0 -0
  109. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/cli/repl.py +0 -0
  110. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/cli/repl_helpers.py +0 -0
  111. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/cli/url_helpers.py +0 -0
  112. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/client.py +0 -0
  113. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/common/__init__.py +0 -0
  114. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/common/context_aware_tool.py +0 -0
  115. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/common/kf_http_client.py +0 -0
  116. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/common/kf_markdown_media_client.py +0 -0
  117. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/common/kf_vectorsearch_client.py +0 -0
  118. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/common/mcp_toolkit.py +0 -0
  119. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/common/mcp_utils.py +0 -0
  120. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/common/token_expiry.py +0 -0
  121. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/common/tool_node_utils.py +0 -0
  122. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/deep/__init__.py +0 -0
  123. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/deep/deep_runtime.py +0 -0
  124. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/eval/__init__.py +0 -0
  125. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/eval/collector.py +0 -0
  126. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/graph/__init__.py +0 -0
  127. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/integrations/__init__.py +0 -0
  128. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/integrations/inprocess_toolkit_registry.py +0 -0
  129. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/integrations/kf_vector_search/__init__.py +0 -0
  130. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/integrations/kf_vector_search/toolkit.py +0 -0
  131. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/integrations/v2_runtime/__init__.py +0 -0
  132. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/model_routing/__init__.py +0 -0
  133. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/model_routing/authz.py +0 -0
  134. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/model_routing/catalog.py +0 -0
  135. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/__init__.py +0 -0
  136. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/middleware/__init__.py +0 -0
  137. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/middleware/dynamic_prompt.py +0 -0
  138. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/middleware/model_routing.py +0 -0
  139. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/middleware/tracing_kpi.py +0 -0
  140. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/react_langchain_adapter.py +0 -0
  141. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/react_message_codec.py +0 -0
  142. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/react_model_adapter.py +0 -0
  143. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/react_tool_binding.py +0 -0
  144. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/react_tool_rendering.py +0 -0
  145. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/react_tool_resolution.py +0 -0
  146. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/react_tool_utils.py +0 -0
  147. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/react_tracing.py +0 -0
  148. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/runtime_context.py +0 -0
  149. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/runtime_support/checkpoints.py +0 -0
  150. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/runtime_support/request_context_helpers.py +0 -0
  151. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/runtime_support/sql_checkpointer.py +0 -0
  152. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/support/__init__.py +0 -0
  153. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/support/filesystem_context.py +0 -0
  154. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime.egg-info/dependency_links.txt +0 -0
  155. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime.egg-info/top_level.txt +0 -0
  156. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/setup.cfg +0 -0
  157. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_capability_agent_1973.py +0 -0
  158. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_capability_chat_controls_1976.py +0 -0
  159. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_capability_chat_parts_1977.py +0 -0
  160. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_capability_registry_1973.py +0 -0
  161. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_capability_routes_tables_1979.py +0 -0
  162. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_capability_selection_1974.py +0 -0
  163. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_capability_tool_return_convention.py +0 -0
  164. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_client.py +0 -0
  165. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_context_aware_tool.py +0 -0
  166. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_conversational_memory.py +0 -0
  167. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_deep_agent_middleware.py +0 -0
  168. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_eval_collector.py +0 -0
  169. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_eval_trace.py +0 -0
  170. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_fred_workspace_fs.py +0 -0
  171. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_graph_capability_bridge.py +0 -0
  172. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_graph_runtime_invoke_agent.py +0 -0
  173. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_graph_runtime_observability.py +0 -0
  174. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_history_display.py +0 -0
  175. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_hitl_resume_langgraph_integration.py +0 -0
  176. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_kf_workspace_client.py +0 -0
  177. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_knowledge_search_tool_invoker_sources.py +0 -0
  178. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_kpi_display.py +0 -0
  179. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_langgraph_interrupt_id_semantics.py +0 -0
  180. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_mcp_config.py +0 -0
  181. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_model_enforcement.py +0 -0
  182. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_pod_client.py +0 -0
  183. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_react_message_codec_resume.py +0 -0
  184. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_react_middleware_frame.py +0 -0
  185. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_react_prompting.py +0 -0
  186. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_repl_helpers.py +0 -0
  187. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_runtime_context_prompt_injection.py +0 -0
  188. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_smoke.py +0 -0
  189. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_spike_capability_state_1971.py +0 -0
  190. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_sql_checkpointer_hitl_claim.py +0 -0
  191. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_sql_checkpointer_owner.py +0 -0
  192. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_token_expiry.py +0 -0
  193. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_tool_carrier_middleware_naming.py +0 -0
  194. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_tracing_kpi_content_confinement.py +0 -0
  195. {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_url_helpers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fred-runtime
3
- Version: 3.4.4
3
+ Version: 3.4.5
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>=3.8.0
16
- Requires-Dist: fred-sdk>=3.3.3
15
+ Requires-Dist: fred-core>=3.8.2
16
+ Requires-Dist: fred-sdk>=3.4.3
17
17
  Requires-Dist: alembic>=1.18.4
18
18
  Requires-Dist: deepagents>=0.6.12
19
19
  Requires-Dist: httpx>=0.28.1
@@ -65,7 +65,6 @@ from fred_core.logs.audit_log import emit_audit_log
65
65
  from fred_core.logs.log_setup import log_setup
66
66
  from fred_core.logs.log_store_factory import build_log_store
67
67
  from fred_core.security.models import AuthorizationError
68
- from fred_core.security.oidc import get_keycloak_client_id, get_keycloak_url
69
68
  from fred_core.security.rebac.rebac_engine import (
70
69
  ORGANIZATION_ID,
71
70
  OrganizationPermission,
@@ -152,7 +151,9 @@ from ..integrations.v2_runtime.adapters import (
152
151
  AgentConfigAssetsAdapter,
153
152
  CompositeToolInvoker,
154
153
  DocumentContentAdapter,
154
+ DocumentExtractionAdapter,
155
155
  DocumentFolderAdapter,
156
+ DocumentMarkdownAdapter,
156
157
  DocumentSearchAdapter,
157
158
  DocumentSummarizeAdapter,
158
159
  DocumentTreeAdapter,
@@ -160,6 +161,7 @@ from ..integrations.v2_runtime.adapters import (
160
161
  FredMcpToolProvider,
161
162
  FredWorkspaceFs,
162
163
  KPIWriterMetricsAdapter,
164
+ _refresh_runtime_context_access_token,
163
165
  build_default_tracer,
164
166
  )
165
167
  from ..runtime_context import (
@@ -168,7 +170,8 @@ from ..runtime_context import (
168
170
  set_runtime_context,
169
171
  )
170
172
  from ..runtime_context import RuntimeContext as FredRuntimeContext
171
- from ..runtime_support import refresh_user_access_token_from_keycloak
173
+ from ..runtime_support import aclose_token_refresh_client
174
+ from .chat_input_limit import validate_runtime_request
172
175
  from .config import AgentPodConfig
173
176
  from .container import build_pod_container
174
177
  from .context import AuditEventRecord, KpiTurnRecord, PodApplicationContext
@@ -349,6 +352,7 @@ def _execute_response_adapter() -> TypeAdapter[Any]:
349
352
  cache = _execute_response_adapter_cache
350
353
  if cache is None or cache[0] is not union_token:
351
354
  cache = (union_token, TypeAdapter(RuntimeEvent | _RuntimeErrorPayload))
355
+ # codeql[py/unused-global-variable]
352
356
  _execute_response_adapter_cache = cache
353
357
  return cache[1]
354
358
 
@@ -455,58 +459,26 @@ class _MediaClientAgentAdapter:
455
459
  self.runtime_context = binding.runtime_context
456
460
  self.agent_settings: AgentSettingsLike = settings
457
461
 
458
- def refresh_user_access_token(self) -> str:
462
+ async def refresh_user_access_token(self) -> str:
459
463
  """
460
464
  Refresh the user access token for media downloads.
461
465
 
462
466
  Why this exists:
463
467
  - media fetch retries need the same Keycloak refresh path as other
464
- runtime adapters
468
+ runtime adapters. It delegates rather than reimplementing: this used
469
+ to be a ~40-line copy of `_refresh_runtime_context_access_token`
470
+ which had already drifted — the copy never wrote
471
+ `access_token_expires_at`, so any consumer gating on expiry saw stale
472
+ data after a media-path refresh.
465
473
 
466
474
  How to use it:
467
- - called by `KfMarkdownMediaClient` when the current token is expired
475
+ - awaited by `KfMarkdownMediaClient` when the current token is expired
468
476
 
469
477
  Example:
470
- - `token = adapter.refresh_user_access_token()`
478
+ - `token = await adapter.refresh_user_access_token()`
471
479
  """
472
480
 
473
- refresh_token = self.runtime_context.refresh_token
474
- if not refresh_token:
475
- raise RuntimeError(
476
- "Cannot refresh user access token: refresh_token missing from runtime context."
477
- )
478
-
479
- keycloak_url = get_keycloak_url()
480
- client_id = get_keycloak_client_id()
481
- if not keycloak_url:
482
- raise RuntimeError(
483
- "User security realm_url is not configured for Keycloak."
484
- )
485
- if not client_id:
486
- raise RuntimeError(
487
- "User security client_id is not configured for Keycloak."
488
- )
489
-
490
- payload = refresh_user_access_token_from_keycloak(
491
- keycloak_url=keycloak_url,
492
- client_id=client_id,
493
- refresh_token=refresh_token,
494
- )
495
- new_access_token = payload.get("access_token")
496
- raw_refresh = payload.get("refresh_token")
497
- new_refresh_token: str = (
498
- raw_refresh
499
- if isinstance(raw_refresh, str) and raw_refresh
500
- else refresh_token
501
- )
502
- if not isinstance(new_access_token, str) or not new_access_token:
503
- raise RuntimeError(
504
- "Keycloak refresh response did not include a valid access_token."
505
- )
506
-
507
- self.runtime_context.access_token = new_access_token
508
- self.runtime_context.refresh_token = new_refresh_token
509
- return new_access_token
481
+ return await _refresh_runtime_context_access_token(self.runtime_context)
510
482
 
511
483
 
512
484
  def _definition_to_agent_tuning(
@@ -752,6 +724,18 @@ def _build_runtime_services(
752
724
  binding=binding,
753
725
  settings=settings,
754
726
  )
727
+ # Paginated full-markdown read (DOCREAD-01): powers document_verbatim,
728
+ # same private-binding doctrine.
729
+ document_markdown = DocumentMarkdownAdapter(
730
+ binding=binding,
731
+ settings=settings,
732
+ )
733
+ # Server-side exhaustive extraction (DOCREAD-01 Phase 2): powers
734
+ # document_extract's single-call path.
735
+ document_extraction = DocumentExtractionAdapter(
736
+ binding=binding,
737
+ settings=settings,
738
+ )
755
739
  tool_provider = FredMcpToolProvider(
756
740
  binding=binding,
757
741
  settings=settings,
@@ -811,6 +795,8 @@ def _build_runtime_services(
811
795
  document_folders=DocumentFolderAdapter(binding=binding, settings=settings),
812
796
  document_tree=document_tree,
813
797
  document_summarize=document_summarize,
798
+ document_markdown=document_markdown,
799
+ document_extraction=document_extraction,
814
800
  )
815
801
 
816
802
 
@@ -960,6 +946,9 @@ class _AgentTemplateSummary(BaseModel):
960
946
  # for); this field is the one control-plane reads to resolve what a
961
947
  # `selected_capability_ids = None` instance activates (#1980).
962
948
  default_capability_ids: list[str] = Field(default_factory=list)
949
+ # Pod-scoped deployment policy mirrored per template so control-plane can
950
+ # publish it to the managed-chat composer without another runtime endpoint.
951
+ max_chat_input_chars: int
963
952
 
964
953
 
965
954
  class _McpCatalogEntry(BaseModel):
@@ -1001,6 +990,19 @@ class _ModelCatalogEntry(BaseModel):
1001
990
  shows no reasoning control at all — aptitude is not a choice, an
1002
991
  administrator cannot make a model reason. Non-empty means the row carries
1003
992
  the toggle. Same established join as `profile_ids` above."""
993
+ display_name: str | None = None
994
+ """The ops-authored `model_display_name` of this model's first profile that
995
+ declares one — the label the composer shows instead of splitting the
996
+ capability id apart. None means no profile named this model and the
997
+ frontend falls back to that heuristic. First-declared wins, same
998
+ first-seen rule as `description` above."""
999
+ reasoning_effort: str | None = None
1000
+ """The ops-authored `settings.reasoning_effort` of this model's first
1001
+ thinking profile — DERIVED from the settings (never declared twice; the
1002
+ settings key is the single source of truth per review 2026-08-12). The
1003
+ composer's reasoning menu displays it as the level a reasoning turn will
1004
+ actually run with ("Élevé" for Mistral small); None when no thinking
1005
+ profile ships the key (the menu then falls back to a generic On label)."""
1004
1006
 
1005
1007
 
1006
1008
  class _ModelCatalogResponse(BaseModel):
@@ -1040,15 +1042,23 @@ def _project_model_catalog_entries(catalog: Any) -> list[_ModelCatalogEntry]:
1040
1042
  provider=provider,
1041
1043
  name=name,
1042
1044
  description=profile.description,
1045
+ display_name=profile.model_display_name,
1043
1046
  profile_ids=[profile.profile_id],
1044
1047
  )
1045
1048
  seen[key] = existing
1046
1049
  else:
1047
1050
  existing.profile_ids.append(profile.profile_id)
1051
+ # First declaring profile wins; a later one only fills a gap.
1052
+ if existing.display_name is None:
1053
+ existing.display_name = profile.model_display_name
1048
1054
  # REASON-01 §5.3: aptitude is per profile, the toggle is per model —
1049
1055
  # this one condition is the whole projection between them.
1050
1056
  if profile.supports_thinking:
1051
1057
  existing.thinking_profile_ids.append(profile.profile_id)
1058
+ if existing.reasoning_effort is None:
1059
+ existing.reasoning_effort = (profile.model.settings or {}).get(
1060
+ "reasoning_effort"
1061
+ )
1052
1062
  return list(seen.values())
1053
1063
 
1054
1064
 
@@ -2167,11 +2177,15 @@ async def _write_turn_history(
2167
2177
  )
2168
2178
 
2169
2179
  elif kind == "awaiting_human":
2170
- # Store the full HITL gate definition — question and all choices —
2171
- # so audit logs and UI replay have the complete structured record.
2180
+ # Store the full HITL gate definition — question, all choices, and
2181
+ # the resume identity (interrupt_id/checkpoint_id/pending_calls) —
2182
+ # so audit logs and UI replay have the complete structured record
2183
+ # AND a reload while the gate is still open can reconstruct a
2184
+ # working (not just readable) prompt.
2172
2185
  req = payload.get("request", {})
2173
2186
  question = req.get("question") or req.get("title") or "HITL pause"
2174
2187
  raw_choices = req.get("choices") or []
2188
+ raw_pending_calls = req.get("pending_calls") or []
2175
2189
  messages.append(
2176
2190
  make_hitl_request(
2177
2191
  session_id,
@@ -2188,6 +2202,18 @@ async def _write_turn_history(
2188
2202
  ],
2189
2203
  stage=req.get("stage"),
2190
2204
  title=req.get("title"),
2205
+ free_text=bool(req.get("free_text")),
2206
+ interrupt_id=req.get("interrupt_id"),
2207
+ checkpoint_id=req.get("checkpoint_id"),
2208
+ pending_calls=[
2209
+ {
2210
+ "tool_call_id": c.get("tool_call_id", ""),
2211
+ "tool_name": c.get("tool_name", ""),
2212
+ "args_preview": c.get("args_preview", ""),
2213
+ }
2214
+ for c in raw_pending_calls
2215
+ if isinstance(c, dict)
2216
+ ],
2191
2217
  )
2192
2218
  )
2193
2219
  rank += 1
@@ -2220,6 +2246,8 @@ async def _write_turn_history(
2220
2246
  input_tokens=tu.get("input_tokens", 0),
2221
2247
  output_tokens=tu.get("output_tokens", 0),
2222
2248
  total_tokens=tu.get("total_tokens", 0),
2249
+ cache_read_tokens=tu.get("cache_read_tokens", 0),
2250
+ cache_creation_tokens=tu.get("cache_creation_tokens", 0),
2223
2251
  )
2224
2252
  final_model = payload.get("model_name")
2225
2253
  final_finish_reason = payload.get("finish_reason")
@@ -2276,6 +2304,7 @@ class _TurnOutcome:
2276
2304
  token_usage: dict[str, Any] | None
2277
2305
  input_tokens: int | None
2278
2306
  output_tokens: int | None
2307
+ cache_read_tokens: int | None
2279
2308
  tool_count: int
2280
2309
  is_error: bool
2281
2310
  total_ms: int
@@ -2299,6 +2328,7 @@ def _parse_turn_outcome(
2299
2328
  token_usage=token_usage,
2300
2329
  input_tokens=token_usage.get("input_tokens") if token_usage else None,
2301
2330
  output_tokens=token_usage.get("output_tokens") if token_usage else None,
2331
+ cache_read_tokens=token_usage.get("cache_read_tokens") if token_usage else None,
2302
2332
  tool_count=tool_count,
2303
2333
  is_error=is_error,
2304
2334
  total_ms=total_ms,
@@ -2444,6 +2474,7 @@ def _emit_turn_completed(
2444
2474
  "tool_count": outcome.tool_count,
2445
2475
  "input_tokens": outcome.input_tokens,
2446
2476
  "output_tokens": outcome.output_tokens,
2477
+ "cache_read_tokens": outcome.cache_read_tokens,
2447
2478
  },
2448
2479
  actor=KPIActor(type="human", user_id=user_id),
2449
2480
  )
@@ -3409,6 +3440,7 @@ def _capability_route_base_url(base_url: str, capability_id: str) -> str:
3409
3440
  def _build_agent_router(
3410
3441
  registry: Mapping[str, ReActAgentDefinition | GraphAgentDefinition],
3411
3442
  security_enabled: bool,
3443
+ max_chat_input_chars: int,
3412
3444
  base_url: str = "",
3413
3445
  ) -> APIRouter:
3414
3446
  """
@@ -3433,6 +3465,14 @@ def _build_agent_router(
3433
3465
  # Returns None in local-dev mode so dev pods start without Keycloak.
3434
3466
  _authenticated_user = _make_user_dependency(get_current_user, security_enabled)
3435
3467
 
3468
+ def _enforce_chat_input_limit(request: RuntimeExecuteRequest) -> None:
3469
+ violation = validate_runtime_request(request, max_chat_input_chars)
3470
+ if violation is not None:
3471
+ raise HTTPException(
3472
+ status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
3473
+ detail=violation.native_detail(),
3474
+ )
3475
+
3436
3476
  @router.get("")
3437
3477
  async def list_agents() -> list[str]:
3438
3478
  """Return the agent IDs registered in this pod."""
@@ -3567,6 +3607,7 @@ def _build_agent_router(
3567
3607
  default_capability_ids=[
3568
3608
  ref.id for ref in definition.default_mcp_servers
3569
3609
  ],
3610
+ max_chat_input_chars=max_chat_input_chars,
3570
3611
  )
3571
3612
  for definition in registry.values()
3572
3613
  if include_non_public or getattr(definition, "public", True)
@@ -4262,6 +4303,7 @@ def _build_agent_router(
4262
4303
  - This endpoint does not implement pod discovery or routing.
4263
4304
  Those concerns belong to Kubernetes Service, Ingress, and Argo CD.
4264
4305
  """
4306
+ _enforce_chat_input_limit(request)
4265
4307
  auth = http_request.headers.get("Authorization", "")
4266
4308
  access_token = auth.removeprefix("Bearer ").strip() or None
4267
4309
 
@@ -4347,6 +4389,7 @@ def _build_agent_router(
4347
4389
  Intended for evaluation harnesses (DeepEval, Promptfoo) that need
4348
4390
  input, output, retrieval_context, tools_called, and steps in one response.
4349
4391
  """
4392
+ _enforce_chat_input_limit(request)
4350
4393
  auth = http_request.headers.get("Authorization", "")
4351
4394
  access_token = auth.removeprefix("Bearer ").strip() or None
4352
4395
 
@@ -4458,6 +4501,7 @@ def _build_agent_router(
4458
4501
  - This endpoint does not implement pod discovery or routing.
4459
4502
  Those concerns belong to Kubernetes Service, Ingress, and Argo CD.
4460
4503
  """
4504
+ _enforce_chat_input_limit(request)
4461
4505
  auth = http_request.headers.get("Authorization", "")
4462
4506
  access_token = auth.removeprefix("Bearer ").strip() or None
4463
4507
 
@@ -4682,9 +4726,32 @@ def create_agent_app(
4682
4726
  "prometheus" if config.observability.kpi.prometheus.enabled else "logging",
4683
4727
  list(registry.keys()),
4684
4728
  )
4685
- yield
4686
- await gc_diagnostics.stop()
4687
- await container.shutdown()
4729
+ try:
4730
+ yield
4731
+ finally:
4732
+ # Shutdown must RUN and must COMPLETE.
4733
+ # - `finally`: a plain sequence after `yield` is skipped entirely
4734
+ # when an exception propagates out of the app through the
4735
+ # generator, which is exactly the case where releasing sockets
4736
+ # and stopping tasks matters most.
4737
+ # - per-step guard: these three release INDEPENDENT resources, so a
4738
+ # raising `container.shutdown()` must not strand the Keycloak
4739
+ # refresh pool behind it. The pod is going away either way;
4740
+ # losing a connection pool because an unrelated subsystem failed
4741
+ # is strictly worse than logging both failures.
4742
+ # `except Exception` deliberately lets `CancelledError` through —
4743
+ # a shutdown being cancelled is not a step failing.
4744
+ for label, close in (
4745
+ ("gc diagnostics", gc_diagnostics.stop),
4746
+ ("pod container", container.shutdown),
4747
+ # Release the Keycloak refresh pool deterministically rather
4748
+ # than leaving it to GC, as the control-plane client above is.
4749
+ ("keycloak refresh pool", aclose_token_refresh_client),
4750
+ ):
4751
+ try:
4752
+ await close()
4753
+ except Exception:
4754
+ logger.exception("[fred-runtime] shutdown step failed: %s", label)
4688
4755
 
4689
4756
  app = FastAPI(
4690
4757
  title=config.app.name,
@@ -4717,7 +4784,10 @@ def create_agent_app(
4717
4784
  api_router = APIRouter(prefix=base_url)
4718
4785
  api_router.include_router(
4719
4786
  _build_agent_router(
4720
- registry, security_enabled=security_enabled, base_url=base_url
4787
+ registry,
4788
+ security_enabled=security_enabled,
4789
+ max_chat_input_chars=config.app.max_chat_input_chars,
4790
+ base_url=base_url,
4721
4791
  )
4722
4792
  )
4723
4793
 
@@ -4747,6 +4817,7 @@ def create_agent_app(
4747
4817
  openai_router = create_openai_compat_router(
4748
4818
  registry,
4749
4819
  security_enabled=security_enabled,
4820
+ max_chat_input_chars=config.app.max_chat_input_chars,
4750
4821
  )
4751
4822
  app.include_router(openai_router, prefix="/v1")
4752
4823
  logger.info("[fred-runtime] OpenAI-compat endpoints enabled at /v1")
@@ -0,0 +1,109 @@
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
+ """Field-aware chat-input length validation shared by runtime HTTP surfaces."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from collections.abc import Mapping
19
+ from dataclasses import dataclass
20
+ from typing import Any
21
+
22
+ from fred_sdk.contracts.execution import RuntimeExecuteRequest
23
+
24
+ CHAT_INPUT_TOO_LONG_CODE = "chat_input_too_long"
25
+ _HITL_TEXT_FIELDS = ("choice_id", "answer", "text")
26
+
27
+
28
+ @dataclass(frozen=True, slots=True)
29
+ class ChatInputTooLong:
30
+ """Safe error data for an oversized submitted message.
31
+
32
+ The rejected text is deliberately not retained on this object, so logging
33
+ or serializing the validation result cannot accidentally echo user content.
34
+ """
35
+
36
+ limit_chars: int
37
+ actual_chars: int
38
+
39
+ @property
40
+ def message(self) -> str:
41
+ return f"Your message exceeds the {self.limit_chars:,}-character limit."
42
+
43
+ def native_detail(self) -> dict[str, str | int]:
44
+ """Return the Fred-native ``HTTPException.detail`` payload."""
45
+
46
+ return {
47
+ "code": CHAT_INPUT_TOO_LONG_CODE,
48
+ "message": self.message,
49
+ "limit_chars": self.limit_chars,
50
+ "actual_chars": self.actual_chars,
51
+ }
52
+
53
+ def openai_error(self) -> dict[str, str | int]:
54
+ """Return the OpenAI-compatible ``error`` object."""
55
+
56
+ return {
57
+ "message": self.message,
58
+ "type": "invalid_request_error",
59
+ "param": "messages",
60
+ "code": CHAT_INPUT_TOO_LONG_CODE,
61
+ "limit_chars": self.limit_chars,
62
+ "actual_chars": self.actual_chars,
63
+ }
64
+
65
+
66
+ def validate_chat_text(text: str, max_chars: int) -> ChatInputTooLong | None:
67
+ """Validate one string using Python's Unicode-code-point semantics."""
68
+
69
+ actual_chars = len(text)
70
+ if actual_chars <= max_chars:
71
+ return None
72
+ return ChatInputTooLong(limit_chars=max_chars, actual_chars=actual_chars)
73
+
74
+
75
+ def runtime_request_chat_char_count(request: RuntimeExecuteRequest) -> int:
76
+ """Count the user-authored chat text that this runtime request submits.
77
+
78
+ Ordinary turns count ``input``. A HITL resume counts a bare string or the
79
+ combined string values of the canonical ``choice_id``, ``answer``, and
80
+ ``text`` fields. Values are summed per field without deduplication: callers
81
+ may use either ``choice_id`` or ``answer`` for a selection, and managed chat
82
+ currently mirrors the identifier in both. Arbitrary JSON keys are
83
+ intentionally not traversed.
84
+ """
85
+
86
+ resume_payload: Any = request.resume_payload
87
+ if resume_payload is None:
88
+ return len(request.input)
89
+ if isinstance(resume_payload, str):
90
+ return len(resume_payload)
91
+ if isinstance(resume_payload, Mapping):
92
+ return sum(
93
+ len(value)
94
+ for key in _HITL_TEXT_FIELDS
95
+ if isinstance((value := resume_payload.get(key)), str)
96
+ )
97
+ return 0
98
+
99
+
100
+ def validate_runtime_request(
101
+ request: RuntimeExecuteRequest,
102
+ max_chars: int,
103
+ ) -> ChatInputTooLong | None:
104
+ """Validate the effective ordinary-turn or HITL text in one request."""
105
+
106
+ actual_chars = runtime_request_chat_char_count(request)
107
+ if actual_chars <= max_chars:
108
+ return None
109
+ return ChatInputTooLong(limit_chars=max_chars, actual_chars=actual_chars)
@@ -120,6 +120,14 @@ class PodAppConfig(BaseModel):
120
120
  "limit."
121
121
  ),
122
122
  )
123
+ max_chat_input_chars: int = Field(
124
+ default=5_000,
125
+ ge=1,
126
+ description=(
127
+ "Maximum number of Unicode code points accepted in one submitted "
128
+ "chat message, including supported HITL resume text fields."
129
+ ),
130
+ )
123
131
  gcu_version: str | None = None
124
132
  openai_compat: bool = False
125
133
  """
@@ -52,6 +52,12 @@ _CONTROL_PLANE_CLIENT_LIMITS = httpx.Limits(
52
52
  # attempt hang well past what a readiness-driven operator expects.
53
53
  _SQL_BOOT_PING_TIMEOUT_S = 5.0
54
54
 
55
+ # Schema ownership: fred-runtime's Alembic tree creates `session_history` and
56
+ # stamps `alembic_version_runtime`; no store creates tables at runtime (#2290).
57
+ # This is the exact command the Helm migration hook and `make db-upgrade` run.
58
+ _MIGRATE_COMMAND = "python -m fred_runtime migrate"
59
+ _RUNTIME_VERSION_TABLE = "alembic_version_runtime"
60
+
55
61
 
56
62
  # ---------------------------------------------------------------------------
57
63
  # TypedDicts for ring buffer entries
@@ -151,13 +157,15 @@ class PodApplicationContext:
151
157
  Durable storage is mandatory for every fred-runtime pod — dev uses a
152
158
  local SQLite file (`sqlite_path`), production uses Postgres, but both
153
159
  are "durable", never "no storage". Any failure here (bad config,
154
- unreachable/misconfigured Postgres) must abort pod startup so the
155
- FastAPI lifespan never completes and Kubernetes never routes traffic
156
- to this replica — see RUNTIME-EXECUTION-CONTRACT.md §8 (dated entry)
157
- for the incident this closes.
160
+ unreachable/misconfigured Postgres, unmigrated schema) must abort pod
161
+ startup so the FastAPI lifespan never completes and Kubernetes never
162
+ routes traffic to this replica — see RUNTIME-EXECUTION-CONTRACT.md §8
163
+ (dated entries) for the incidents this closes.
158
164
  """
165
+ from fred_core.history.history_models import SessionHistoryRow
159
166
  from fred_core.history.postgres_history_store import PostgresHistoryStore
160
167
  from fred_core.sql.base_sql import create_async_engine_from_config
168
+ from fred_core.sql.schema_guard import require_tables
161
169
  from fred_core.users.store.postgres_user_store import init_user_store
162
170
  from sqlalchemy import text
163
171
 
@@ -194,6 +202,23 @@ class PodApplicationContext:
194
202
  await engine.dispose()
195
203
  raise
196
204
 
205
+ try:
206
+ # Startup-only (never on a read/write path): the history store no
207
+ # longer creates its own tables — Alembic owns that DDL (#2290) —
208
+ # so an install that skipped its migration job must be caught here,
209
+ # not hours later as an UndefinedTableError mid-turn (#2137).
210
+ await require_tables(
211
+ engine,
212
+ [SessionHistoryRow.__tablename__],
213
+ component="fred-runtime",
214
+ migrate_command=_MIGRATE_COMMAND,
215
+ version_table=_RUNTIME_VERSION_TABLE,
216
+ )
217
+ except Exception:
218
+ # Same leak-on-failure reasoning as the ping path above.
219
+ await engine.dispose()
220
+ raise
221
+
197
222
  init_user_store(engine)
198
223
  checkpointer = FredSqlCheckpointer(engine, kpi=self.get_kpi_writer())
199
224
  history_store = PostgresHistoryStore(engine, kpi=self.get_kpi_writer())
@@ -49,8 +49,8 @@ from collections.abc import AsyncIterator, Mapping
49
49
  from typing import Any, Callable
50
50
  from uuid import uuid4
51
51
 
52
- from fastapi import APIRouter, Depends, HTTPException, Request, status
53
- from fastapi.responses import StreamingResponse
52
+ from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
53
+ from fastapi.responses import JSONResponse, StreamingResponse
54
54
  from fred_core import AuthorizationError, KeycloakUser, TeamPermission
55
55
  from fred_sdk.contracts.models import GraphAgentDefinition, ReActAgentDefinition
56
56
  from fred_sdk.contracts.openai_compat import (
@@ -67,6 +67,7 @@ from .agent_app import (
67
67
  _iterate_runtime_event_payloads,
68
68
  _resolve_agent_instance,
69
69
  )
70
+ from .chat_input_limit import validate_chat_text
70
71
  from .dependencies import get_pod_container_from_app
71
72
 
72
73
  logger = logging.getLogger(__name__)
@@ -75,6 +76,7 @@ logger = logging.getLogger(__name__)
75
76
  def create_openai_compat_router(
76
77
  registry: Mapping[str, ReActAgentDefinition | GraphAgentDefinition],
77
78
  security_enabled: bool,
79
+ max_chat_input_chars: int,
78
80
  authenticated_user_dep: Callable[..., Any] | None = None,
79
81
  ) -> APIRouter:
80
82
  """
@@ -141,7 +143,7 @@ def create_openai_compat_router(
141
143
  request: OpenAIChatRequest,
142
144
  http_request: Request,
143
145
  authenticated_user: KeycloakUser | None = Depends(user_dep),
144
- ) -> StreamingResponse:
146
+ ) -> Response:
145
147
  """
146
148
  Execute one agent turn over the OpenAI chat completions SSE protocol.
147
149
 
@@ -164,6 +166,20 @@ def create_openai_compat_router(
164
166
  - `fred.node_error`: graph node error message (finish_reason="stop")
165
167
  - `fred.token_usage`: input/output token counts (on the final chunk)
166
168
  """
169
+ user_messages = [m for m in request.messages if m.role == "user"]
170
+ if not user_messages:
171
+ raise HTTPException(
172
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
173
+ detail="Request must contain at least one user message.",
174
+ )
175
+ message = user_messages[-1].content
176
+ violation = validate_chat_text(message, max_chat_input_chars)
177
+ if violation is not None:
178
+ return JSONResponse(
179
+ status_code=status.HTTP_400_BAD_REQUEST,
180
+ content={"error": violation.openai_error()},
181
+ )
182
+
167
183
  if request.model not in registry:
168
184
  raise HTTPException(
169
185
  status_code=status.HTTP_404_NOT_FOUND,
@@ -173,14 +189,6 @@ def create_openai_compat_router(
173
189
  ),
174
190
  )
175
191
 
176
- user_messages = [m for m in request.messages if m.role == "user"]
177
- if not user_messages:
178
- raise HTTPException(
179
- status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
180
- detail="Request must contain at least one user message.",
181
- )
182
- message = user_messages[-1].content
183
-
184
192
  session_id = http_request.headers.get("X-Fred-Session-Id") or uuid4().hex
185
193
  team_id = http_request.headers.get("X-Fred-Team-Id")
186
194
  auth = http_request.headers.get("Authorization", "")