fred-runtime 3.4.0__tar.gz → 3.4.1__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 (167) hide show
  1. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/PKG-INFO +1 -1
  2. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/app/agent_app.py +203 -16
  3. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/app/context.py +54 -16
  4. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/app/openai_compat_router.py +5 -2
  5. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/capabilities/assembly.py +3 -1
  6. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/capabilities/document_access/capability.py +22 -158
  7. fred_runtime-3.4.1/fred_runtime/capabilities/document_summarize/__init__.py +38 -0
  8. fred_runtime-3.4.1/fred_runtime/capabilities/document_summarize/capability.py +325 -0
  9. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/deep/deep_runtime.py +1 -2
  10. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/model_routing/contracts.py +67 -0
  11. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/model_routing/provider.py +35 -4
  12. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/react/middleware/checkpoint_hygiene.py +15 -11
  13. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/react/middleware/tool_observability.py +5 -4
  14. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/react/react_runtime.py +1 -8
  15. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/react/react_tool_binding.py +30 -0
  16. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/support/thinking.py +72 -0
  17. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime.egg-info/PKG-INFO +1 -1
  18. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime.egg-info/SOURCES.txt +5 -0
  19. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime.egg-info/entry_points.txt +1 -0
  20. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/pyproject.toml +4 -1
  21. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_agent_app.py +267 -13
  22. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_capability_document_access_1906.py +9 -175
  23. fred_runtime-3.4.1/tests/test_capability_document_summarize.py +324 -0
  24. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_capability_endpoints_1974.py +14 -4
  25. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_context.py +162 -0
  26. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_deep_agent_middleware.py +0 -8
  27. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_history.py +219 -0
  28. fred_runtime-3.4.1/tests/test_model_reasoning_enablement.py +328 -0
  29. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_models_catalog_projection.py +60 -0
  30. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_react_middleware_frame.py +42 -0
  31. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_react_prompting.py +45 -0
  32. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_react_thinking.py +124 -0
  33. fred_runtime-3.4.1/tests/test_tool_carrier_middleware_naming.py +139 -0
  34. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/README.md +0 -0
  35. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/__init__.py +0 -0
  36. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/__main__.py +0 -0
  37. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/app/__init__.py +0 -0
  38. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/app/_catalogs.py +0 -0
  39. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/app/config.py +0 -0
  40. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/app/config_loader.py +0 -0
  41. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/app/container.py +0 -0
  42. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/app/dependencies.py +0 -0
  43. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/app/mcp_config.py +0 -0
  44. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/app/observability_factory.py +0 -0
  45. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/capabilities/__init__.py +0 -0
  46. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/capabilities/assets.py +0 -0
  47. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/capabilities/demo.py +0 -0
  48. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/capabilities/demo_migrations/env.py +0 -0
  49. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/capabilities/demo_migrations/versions/d1e2m0a0b0c0_create_demo_echo_notes.py +0 -0
  50. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/capabilities/document_access/__init__.py +0 -0
  51. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/capabilities/errors.py +0 -0
  52. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/capabilities/mcp.py +0 -0
  53. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/capabilities/openapi_dump.py +0 -0
  54. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/capabilities/registry.py +0 -0
  55. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/cli/__init__.py +0 -0
  56. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/cli/completion.py +0 -0
  57. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/cli/entrypoint.py +0 -0
  58. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/cli/history_display.py +0 -0
  59. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/cli/kpi_display.py +0 -0
  60. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/cli/pod_client.py +0 -0
  61. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/cli/repl.py +0 -0
  62. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/cli/repl_helpers.py +0 -0
  63. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/cli/url_helpers.py +0 -0
  64. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/client.py +0 -0
  65. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/common/__init__.py +0 -0
  66. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/common/context_aware_tool.py +0 -0
  67. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/common/kf_base_client.py +0 -0
  68. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/common/kf_document_client.py +0 -0
  69. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/common/kf_fast_text_client.py +0 -0
  70. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/common/kf_http_client.py +0 -0
  71. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/common/kf_markdown_media_client.py +0 -0
  72. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/common/kf_tag_client.py +0 -0
  73. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/common/kf_vectorsearch_client.py +0 -0
  74. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/common/kf_workspace_client.py +0 -0
  75. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/common/mcp_interceptors.py +0 -0
  76. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/common/mcp_runtime.py +0 -0
  77. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/common/mcp_toolkit.py +0 -0
  78. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/common/mcp_utils.py +0 -0
  79. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/common/structures.py +0 -0
  80. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/common/token_expiry.py +0 -0
  81. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/common/tool_node_utils.py +0 -0
  82. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/deep/__init__.py +0 -0
  83. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/eval/__init__.py +0 -0
  84. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/eval/collector.py +0 -0
  85. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/graph/__init__.py +0 -0
  86. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/graph/graph_runtime.py +0 -0
  87. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/integrations/__init__.py +0 -0
  88. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/integrations/inprocess_toolkit_registry.py +0 -0
  89. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/integrations/kf_vector_search/__init__.py +0 -0
  90. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/integrations/kf_vector_search/toolkit.py +0 -0
  91. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/integrations/v2_runtime/__init__.py +0 -0
  92. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/integrations/v2_runtime/adapters.py +0 -0
  93. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/migrations.py +0 -0
  94. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/model_routing/__init__.py +0 -0
  95. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/model_routing/authz.py +0 -0
  96. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/model_routing/catalog.py +0 -0
  97. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/model_routing/resolver.py +0 -0
  98. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/react/__init__.py +0 -0
  99. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/react/middleware/__init__.py +0 -0
  100. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/react/middleware/dynamic_prompt.py +0 -0
  101. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/react/middleware/frame.py +0 -0
  102. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/react/middleware/hitl.py +0 -0
  103. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/react/middleware/model_routing.py +0 -0
  104. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/react/middleware/shared.py +0 -0
  105. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/react/middleware/tracing_kpi.py +0 -0
  106. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/react/react_langchain_adapter.py +0 -0
  107. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/react/react_message_codec.py +0 -0
  108. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/react/react_model_adapter.py +0 -0
  109. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/react/react_prompting.py +0 -0
  110. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/react/react_stream_adapter.py +0 -0
  111. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/react/react_tool_loop.py +0 -0
  112. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/react/react_tool_rendering.py +0 -0
  113. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/react/react_tool_resolution.py +0 -0
  114. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/react/react_tool_utils.py +0 -0
  115. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/react/react_tracing.py +0 -0
  116. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/runtime_context.py +0 -0
  117. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/runtime_support/__init__.py +0 -0
  118. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/runtime_support/checkpoints.py +0 -0
  119. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/runtime_support/model_metadata.py +0 -0
  120. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/runtime_support/request_context_helpers.py +0 -0
  121. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/runtime_support/sql_checkpointer.py +0 -0
  122. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/runtime_support/user_token_refresher.py +0 -0
  123. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/support/__init__.py +0 -0
  124. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/support/filesystem_context.py +0 -0
  125. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime/support/tool_loop.py +0 -0
  126. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime.egg-info/dependency_links.txt +0 -0
  127. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime.egg-info/requires.txt +0 -0
  128. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/fred_runtime.egg-info/top_level.txt +0 -0
  129. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/setup.cfg +0 -0
  130. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_capability_agent_1973.py +0 -0
  131. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_capability_chat_controls_1976.py +0 -0
  132. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_capability_chat_parts_1977.py +0 -0
  133. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_capability_registry_1973.py +0 -0
  134. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_capability_routes_tables_1979.py +0 -0
  135. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_capability_selection_1974.py +0 -0
  136. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_capability_tool_return_convention.py +0 -0
  137. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_client.py +0 -0
  138. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_config_loader.py +0 -0
  139. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_context_aware_tool.py +0 -0
  140. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_conversational_memory.py +0 -0
  141. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_eval_collector.py +0 -0
  142. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_eval_trace.py +0 -0
  143. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_fred_workspace_fs.py +0 -0
  144. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_graph_capability_bridge.py +0 -0
  145. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_graph_runtime_invoke_agent.py +0 -0
  146. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_graph_runtime_observability.py +0 -0
  147. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_kf_vector_search_toolkit.py +0 -0
  148. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_kf_workspace_client.py +0 -0
  149. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_knowledge_search_tool_invoker_sources.py +0 -0
  150. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_kpi_display.py +0 -0
  151. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_mcp_config.py +0 -0
  152. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_model_enforcement.py +0 -0
  153. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_model_routing.py +0 -0
  154. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_openai_compat_router.py +0 -0
  155. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_pod_client.py +0 -0
  156. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_react_loop_regressions_1972.py +0 -0
  157. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_repl_helpers.py +0 -0
  158. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_runtime_context_prompt_injection.py +0 -0
  159. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_smoke.py +0 -0
  160. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_spike_capability_state_1971.py +0 -0
  161. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_sql_checkpointer_owner.py +0 -0
  162. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_token_expiry.py +0 -0
  163. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_tool_loop_trim.py +0 -0
  164. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_tool_observability_middleware.py +0 -0
  165. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_tracing_kpi_content_confinement.py +0 -0
  166. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_url_helpers.py +0 -0
  167. {fred_runtime-3.4.0 → fred_runtime-3.4.1}/tests/test_user_token_refresher.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fred-runtime
3
- Version: 3.4.0
3
+ Version: 3.4.1
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
@@ -56,6 +56,7 @@ from fastapi import APIRouter, Depends, FastAPI, HTTPException, Request, status
56
56
  from fastapi.middleware.cors import CORSMiddleware
57
57
  from fastapi.responses import StreamingResponse
58
58
  from fred_core.common.config_loader import get_config
59
+ from fred_core.diagnostics import install_gc_diagnostics
59
60
  from fred_core.history.history_schema import ChatMessage
60
61
  from fred_core.kpi import KPIMiddleware
61
62
  from fred_core.kpi.kpi_runtime_stage_metric import runtime_stage_timer
@@ -793,6 +794,7 @@ def _build_runtime_services(
793
794
  return RuntimeServices(
794
795
  tracer=build_default_tracer(),
795
796
  metrics=KPIWriterMetricsAdapter(runtime_config.kpi_writer),
797
+ kpi_writer=runtime_config.kpi_writer,
796
798
  chat_model_factory=runtime_config.chat_model_factory,
797
799
  tool_invoker=tool_invoker,
798
800
  tool_provider=tool_provider,
@@ -983,6 +985,19 @@ class _ModelCatalogEntry(BaseModel):
983
985
  team-settings picker needs the reverse (which profile_ids this enabled
984
986
  capability actually offers). Declaration order in the source YAML,
985
987
  first-seen first — same order `default_profile_by_capability` favors."""
988
+ thinking_profile_ids: list[str] = Field(default_factory=list)
989
+ """The subset of `profile_ids` above declaring `supports_thinking`
990
+ (`MODEL-REASONING-ENABLEMENT-RFC.md` §5.3, REASON-01). DERIVED here,
991
+ never authored twice: aptitude is declared per profile (§4, where the
992
+ behaviour actually differs), while the admin toggle is keyed per model
993
+ because that is what the capability id space and the admin UI both are
994
+ (§5.2 — the id space is not negotiable). This field is the projection
995
+ between the two.
996
+
997
+ Empty means this model has NO reasoning-capable profile, and the admin row
998
+ shows no reasoning control at all — aptitude is not a choice, an
999
+ administrator cannot make a model reason. Non-empty means the row carries
1000
+ the toggle. Same established join as `profile_ids` above."""
986
1001
 
987
1002
 
988
1003
  class _ModelCatalogResponse(BaseModel):
@@ -1017,15 +1032,20 @@ def _project_model_catalog_entries(catalog: Any) -> list[_ModelCatalogEntry]:
1017
1032
  key = (provider, name)
1018
1033
  existing = seen.get(key)
1019
1034
  if existing is None:
1020
- seen[key] = _ModelCatalogEntry(
1035
+ existing = _ModelCatalogEntry(
1021
1036
  id=model_capability_id(provider, name),
1022
1037
  provider=provider,
1023
1038
  name=name,
1024
1039
  description=profile.description,
1025
1040
  profile_ids=[profile.profile_id],
1026
1041
  )
1042
+ seen[key] = existing
1027
1043
  else:
1028
1044
  existing.profile_ids.append(profile.profile_id)
1045
+ # REASON-01 §5.3: aptitude is per profile, the toggle is per model —
1046
+ # this one condition is the whole projection between them.
1047
+ if profile.supports_thinking:
1048
+ existing.thinking_profile_ids.append(profile.profile_id)
1029
1049
  return list(seen.values())
1030
1050
 
1031
1051
 
@@ -1358,12 +1378,21 @@ async def _authorize_execution_or_raise(
1358
1378
  the owner's `team_editor` tuple self-heals on first touch inside
1359
1379
  `RebacEngine.check_user_team_permission_or_raise`, and `add_relation`'s
1360
1380
  write-guard refuses any tuple naming a personal team except that one, so
1361
- the plain `CAN_READ` check below already authorizes the owner and denies
1362
- everyone else (another user's space, or the bare `"personal"` alias, for
1363
- which no tuple is ever provisioned) no special-casing needed here.
1364
- - otherwise require the caller to hold `CAN_READ` on the requested team — the
1365
- same relation the control-plane required before it would mint a grant. The
1366
- team is caller-supplied but safe: OpenFGA only authorizes teams the user
1381
+ the `CAN_USE_TEAM_AGENTS` check below already authorizes the owner
1382
+ (`team_editor` implies `team_member`) and denies everyone else (another
1383
+ user's space, or the bare `"personal"` alias, for which no tuple is
1384
+ ever provisioned) no special-casing needed here.
1385
+ - otherwise require the caller to hold `CAN_USE_TEAM_AGENTS` on the
1386
+ requested team `team_member`-only, unlike `CAN_READ` (which also
1387
+ admits the `public` marketplace-discovery relation, TEAM-09/TEAM-10).
1388
+ Execution is real content access, not discovery, so a public-team
1389
+ visitor must not pass here even though they can see the team profile.
1390
+ This used to check `CAN_READ` (the same relation the control-plane
1391
+ required before it would mint a grant) — that predates TEAM-09's
1392
+ `public` relation and was never revisited; `product/api.py`'s
1393
+ `post_prepare_execution` made the identical correction for the same
1394
+ reason (returns prompt content, not just config). The team is
1395
+ caller-supplied but safe: OpenFGA only authorizes teams the user
1367
1396
  actually holds a relation to. Authorization and denial are both audited;
1368
1397
  any OpenFGA denial fails closed (403).
1369
1398
 
@@ -1446,7 +1475,7 @@ async def _authorize_execution_or_raise(
1446
1475
  # since no tuple is ever provisioned for that literal string).
1447
1476
  try:
1448
1477
  await rebac.check_user_team_permission_or_raise(
1449
- authenticated_user, TeamPermission.CAN_READ, team_id
1478
+ authenticated_user, TeamPermission.CAN_USE_TEAM_AGENTS, team_id
1450
1479
  )
1451
1480
  except AuthorizationError as exc:
1452
1481
  _emit_audit_event(
@@ -1730,6 +1759,27 @@ async def _validate_session_checkpoint_access(
1730
1759
  )
1731
1760
 
1732
1761
 
1762
+ @dataclass
1763
+ class _OpenThought:
1764
+ """
1765
+ One reasoning block being accumulated while mapping a turn to history rows.
1766
+
1767
+ Why the rank is reserved at THOUGHT_START rather than at THOUGHT_END:
1768
+ - a model-native block opens on the first reasoning token and closes only at
1769
+ the first answer delta (RFC AGENT-THINKING-API §7.3), so it brackets every
1770
+ tool call of the turn
1771
+ - the live UI assigns the row's rank at `thought_start` (`useChatSse.ts`);
1772
+ ranking it at close instead would file the reasoning *after* the tools it
1773
+ actually preceded, so a reloaded trace would not match what the user saw
1774
+ """
1775
+
1776
+ rank: int
1777
+ phase: str
1778
+ title: str | None
1779
+ source: str | None
1780
+ text: list[str] = field(default_factory=list)
1781
+
1782
+
1733
1783
  async def _write_turn_history(
1734
1784
  *,
1735
1785
  session_id: str,
@@ -1765,6 +1815,7 @@ async def _write_turn_history(
1765
1815
  Event-to-message mapping:
1766
1816
  - ``tool_call`` → ``Role.assistant / Channel.tool_call``
1767
1817
  - ``tool_result`` → ``Role.tool / Channel.tool_result``
1818
+ - ``thought_*`` → ``Role.assistant / Channel.thought`` (one row per block)
1768
1819
  - ``awaiting_human`` → ``Role.system / Channel.hitl_request`` (full choices)
1769
1820
  - ``node_error`` → ``Role.system / Channel.error``
1770
1821
  - ``final`` → ``Role.assistant / Channel.final`` (answer + sources)
@@ -1774,6 +1825,7 @@ async def _write_turn_history(
1774
1825
  from fred_core.history.history_schema import (
1775
1826
  Channel,
1776
1827
  ChatMessage,
1828
+ ChatMetadata,
1777
1829
  ChatTokenUsage,
1778
1830
  Role,
1779
1831
  TextPart,
@@ -1817,6 +1869,55 @@ async def _write_turn_history(
1817
1869
  messages.append(make_user_text(session_id, exchange_id, rank, request_message))
1818
1870
  rank += 1
1819
1871
 
1872
+ # Reasoning blocks still accumulating, keyed by thought_id. A block reserves
1873
+ # its rank when it opens but only becomes a row once its text is complete.
1874
+ open_thoughts: dict[str, _OpenThought] = {}
1875
+
1876
+ def _thought_row(
1877
+ block: _OpenThought,
1878
+ *,
1879
+ thought_id: str,
1880
+ conclusion: str | None = None,
1881
+ duration_ms: int | None = None,
1882
+ ) -> ChatMessage:
1883
+ """
1884
+ Build one ``Channel.thought`` row from an accumulated reasoning block.
1885
+
1886
+ The envelope must match what the live stream emits client-side
1887
+ (`useChatSse.ts`) — same role, same channel, same ``metadata.extras``
1888
+ keys — because the chat UI feeds streamed and reloaded messages through
1889
+ a single rendering path (`traceUtils.thoughtExtras`).
1890
+
1891
+ ``streaming_delta`` is deliberately absent: a persisted block is
1892
+ complete, and carrying that flag would render it as still running.
1893
+ """
1894
+ return ChatMessage(
1895
+ session_id=session_id,
1896
+ exchange_id=exchange_id,
1897
+ rank=block.rank,
1898
+ timestamp=datetime.now(timezone.utc),
1899
+ role=Role.assistant,
1900
+ channel=Channel.thought,
1901
+ parts=[TextPart(text="".join(block.text))],
1902
+ # `extras` is not a declared field: `ChatMetadata` sets
1903
+ # `extra="allow"` precisely so callers can attach structured payloads
1904
+ # without every consumer of the model growing a dependency on them.
1905
+ # `model_validate` is how that is reached type-safely — a keyword
1906
+ # argument would not type-check against the declared fields.
1907
+ metadata=ChatMetadata.model_validate(
1908
+ {
1909
+ "extras": {
1910
+ "thought_id": thought_id,
1911
+ "phase": block.phase,
1912
+ "title": block.title,
1913
+ "source": block.source,
1914
+ "conclusion": conclusion,
1915
+ "duration_ms": duration_ms,
1916
+ }
1917
+ }
1918
+ ),
1919
+ )
1920
+
1820
1921
  # 2. Map runtime events to messages
1821
1922
  final_content = ""
1822
1923
  final_sources: list[VectorSearchHit] = []
@@ -1854,6 +1955,48 @@ async def _write_turn_history(
1854
1955
  )
1855
1956
  rank += 1
1856
1957
 
1958
+ elif kind == "thought_start":
1959
+ # `tool_use` blocks are synthetic bookkeeping the runtime opens around
1960
+ # every tool call (RFC Amendment A, Layer 1). The chat UI has hidden
1961
+ # them since Amendment B — the tool call/result combo row already
1962
+ # carries what ran, how it went and how long it took. Persisting them
1963
+ # would file up to `max_tool_calls_per_turn` content-free rows a turn.
1964
+ if payload.get("phase") == "tool_use":
1965
+ continue
1966
+ thought_id = str(payload.get("thought_id") or "")
1967
+ if not thought_id:
1968
+ continue
1969
+ open_thoughts[thought_id] = _OpenThought(
1970
+ rank=rank,
1971
+ phase=str(payload.get("phase") or "planning"),
1972
+ title=payload.get("title"),
1973
+ source=payload.get("source"),
1974
+ )
1975
+ rank += 1
1976
+
1977
+ elif kind == "thought_delta":
1978
+ block = open_thoughts.get(str(payload.get("thought_id") or ""))
1979
+ if block is not None:
1980
+ block.text.append(str(payload.get("delta") or ""))
1981
+
1982
+ elif kind == "thought_end":
1983
+ thought_id = str(payload.get("thought_id") or "")
1984
+ block = open_thoughts.pop(thought_id, None)
1985
+ # A block with neither text nor conclusion carries nothing to show;
1986
+ # persisting it would render an empty reasoning card on reload. Its
1987
+ # reserved rank is simply left unused — ranks must increase, not be
1988
+ # contiguous.
1989
+ conclusion = payload.get("conclusion")
1990
+ if block is not None and ("".join(block.text) or conclusion):
1991
+ messages.append(
1992
+ _thought_row(
1993
+ block,
1994
+ thought_id=thought_id,
1995
+ conclusion=conclusion,
1996
+ duration_ms=payload.get("duration_ms"),
1997
+ )
1998
+ )
1999
+
1857
2000
  elif kind == "awaiting_human":
1858
2001
  # Store the full HITL gate definition — question and all choices —
1859
2002
  # so audit logs and UI replay have the complete structured record.
@@ -1912,6 +2055,14 @@ async def _write_turn_history(
1912
2055
  final_model = payload.get("model_name")
1913
2056
  final_finish_reason = payload.get("finish_reason")
1914
2057
 
2058
+ # 2b. Blocks the stream never closed (truncated turn, crashed node). The live
2059
+ # UI closes them itself on `final` so the accordion does not pulse forever
2060
+ # (`useChatSse.ts`); history keeps the same parity — reasoning the user was
2061
+ # shown must not disappear on reload just because the block was left open.
2062
+ for thought_id, block in open_thoughts.items():
2063
+ if "".join(block.text):
2064
+ messages.append(_thought_row(block, thought_id=thought_id))
2065
+
1915
2066
  # 3. Terminal assistant message (from FinalRuntimeEvent)
1916
2067
  if final_content or final_model:
1917
2068
  messages.append(
@@ -2646,6 +2797,35 @@ async def _iterate_runtime_event_payloads(
2646
2797
  for rule in (ctx.get("operation_route_rules") or [])
2647
2798
  ]
2648
2799
  or None,
2800
+ # Platform reasoning activation snapshot (REASON-01,
2801
+ # `MODEL-REASONING-ENABLEMENT-RFC.md` §5.5) — same channel and same
2802
+ # session-prep lifecycle as the two fields above, and forwarded here
2803
+ # for the same hard-won reason: this mapping is field-by-field, so a
2804
+ # field that is not named is silently dropped. Both comments above
2805
+ # record a production bug caused by exactly that. `RoutedChatModelFactory`
2806
+ # strips the reasoning settings for every model absent from this list
2807
+ # (§5.6.2), so dropping it here would pin reasoning permanently OFF and
2808
+ # make the admin toggle decorative — the failure §5.6.2 exists to
2809
+ # prevent, one layer up from where it is enforced.
2810
+ #
2811
+ # Intersected with level 3 (the agent's own switch) rather than taken
2812
+ # as-is, and that is the whole point of doing it HERE: this list rides
2813
+ # the request, `tuning` is resolved server-side from the managed
2814
+ # instance. An agent whose author did not enable reasoning must not
2815
+ # reason whatever the request claims the platform allows — all four
2816
+ # levels must hold (§3), so the ceiling is `level 2 AND level 3`.
2817
+ # Absent tuning (agent-to-agent invocation, no managed instance) means
2818
+ # no author ever enabled it: off, the default everywhere in REASON-01.
2819
+ reasoning_enabled_model_ids=(
2820
+ ctx.get("reasoning_enabled_model_ids")
2821
+ if tuning is not None and tuning.reasoning_enabled
2822
+ else []
2823
+ ),
2824
+ # The user's per-question reasoning choice (REASON-01 level 4). Same
2825
+ # trap as every field above: unnamed here means silently dropped. Kept
2826
+ # tri-state on purpose — `ctx.get` yielding None means "the agent never
2827
+ # offered the choice", which is NOT the same as the user answering no.
2828
+ reasoning=ctx.get("reasoning"),
2649
2829
  )
2650
2830
 
2651
2831
  binding = BoundRuntimeContext(
@@ -4027,14 +4207,19 @@ def create_agent_app(
4027
4207
  async def lifespan(app: FastAPI) -> AsyncIterator[None]:
4028
4208
  # Boot order (must be preserved — each step depends on the previous):
4029
4209
  # 1. log_setup — formatter/handlers ready for all subsequent logs
4030
- # 2. initialize_kpi_writerneeded by bootstrap_observability
4031
- # 3. initialize_control_plane_client sync, no network until first call
4032
- # 4. bootstrap_observability global tracer + metrics provider
4033
- # 5. attach_pod_container container in app.state before any request
4034
- # 6. initialize_sql async, may take time
4035
- # 7. start_metrics_exporterprometheus thread, after KPI writer exists
4036
- # 8. start_kpi_tasks asyncio tasks, after SQL engine is known
4037
- # 9. set_runtime_contextwires all built parts into the global config
4210
+ # 2. install_gc_diagnosticsindependent of everything below; installed
4211
+ # early so SIGUSR1/SIGUSR2 (fred_core.diagnostics) are live for the
4212
+ # pod's whole lifetime, protecting every agent pod (ours and third
4213
+ # parties' alike) against the reference-cycle class of leak found in
4214
+ # ISSUE-010 without any per-pod code
4215
+ # 3. initialize_kpi_writerneeded by bootstrap_observability
4216
+ # 4. initialize_control_plane_client sync, no network until first call
4217
+ # 5. bootstrap_observabilityglobal tracer + metrics provider
4218
+ # 6. attach_pod_container — container in app.state before any request
4219
+ # 7. initialize_sql — async, may take time
4220
+ # 8. start_metrics_exporter — prometheus thread, after KPI writer exists
4221
+ # 9. start_kpi_tasks — asyncio tasks, after SQL engine is known
4222
+ # 10. set_runtime_context — wires all built parts into the global config
4038
4223
  log_setup(
4039
4224
  service_name=config.app.name,
4040
4225
  log_level=config.app.log_level,
@@ -4043,6 +4228,7 @@ def create_agent_app(
4043
4228
  opensearch_config=config.storage.opensearch,
4044
4229
  ),
4045
4230
  )
4231
+ gc_diagnostics = install_gc_diagnostics()
4046
4232
  container = build_pod_container(config)
4047
4233
  container.initialize_kpi_writer()
4048
4234
  container.initialize_control_plane_client()
@@ -4111,6 +4297,7 @@ def create_agent_app(
4111
4297
  list(registry.keys()),
4112
4298
  )
4113
4299
  yield
4300
+ await gc_diagnostics.stop()
4114
4301
  await container.shutdown()
4115
4302
 
4116
4303
  app = FastAPI(
@@ -47,6 +47,11 @@ _CONTROL_PLANE_CLIENT_LIMITS = httpx.Limits(
47
47
  max_connections=50, max_keepalive_connections=20, keepalive_expiry=10.0
48
48
  )
49
49
 
50
+ # Bounds the one-time connectivity check in initialize_sql() — asyncpg's own
51
+ # connect timeout defaults to 60s, which would otherwise let a single boot
52
+ # attempt hang well past what a readiness-driven operator expects.
53
+ _SQL_BOOT_PING_TIMEOUT_S = 5.0
54
+
50
55
 
51
56
  # ---------------------------------------------------------------------------
52
57
  # TypedDicts for ring buffer entries
@@ -141,31 +146,64 @@ class PodApplicationContext:
141
146
  )
142
147
 
143
148
  async def initialize_sql(self) -> None:
144
- """Build SQL engine, checkpointer, and history store (SQL-backed only)."""
149
+ """Build SQL engine, checkpointer, and history store.
150
+
151
+ Durable storage is mandatory for every fred-runtime pod — dev uses a
152
+ local SQLite file (`sqlite_path`), production uses Postgres, but both
153
+ 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.
158
+ """
145
159
  from fred_core.history.postgres_history_store import PostgresHistoryStore
146
160
  from fred_core.sql.base_sql import create_async_engine_from_config
147
161
  from fred_core.users.store.postgres_user_store import init_user_store
162
+ from sqlalchemy import text
148
163
 
149
164
  from fred_runtime.runtime_support.sql_checkpointer import FredSqlCheckpointer
150
165
 
166
+ postgres_config = self.configuration.storage.postgres
167
+ engine = create_async_engine_from_config(postgres_config)
168
+
169
+ async def _ping() -> None:
170
+ # SQLAlchemy async engines are lazy — nothing above opened a real
171
+ # connection. Without this, an unreachable/misconfigured Postgres
172
+ # would pass initialize_sql() silently and only fail mid-turn.
173
+ async with engine.connect() as conn:
174
+ await conn.execute(text("SELECT 1"))
175
+
151
176
  try:
152
- engine = create_async_engine_from_config(
153
- self.configuration.storage.postgres
154
- )
155
- init_user_store(engine)
156
- checkpointer = FredSqlCheckpointer(engine, kpi=self.get_kpi_writer())
157
- history_store = PostgresHistoryStore(engine, kpi=self.get_kpi_writer())
158
- self._sql_engine = engine
159
- self._checkpointer = checkpointer
160
- self._history_store = history_store
161
- logger.info(
162
- "[fred-runtime] SQL checkpointer and history store ready (dialect=%s)",
177
+ await asyncio.wait_for(_ping(), timeout=_SQL_BOOT_PING_TIMEOUT_S)
178
+ except Exception as exc:
179
+ logger.error(
180
+ "[fred-runtime] SQL storage unreachable at startup — "
181
+ "dialect=%s host=%s port=%s database=%s error_type=%s",
163
182
  engine.dialect.name,
183
+ postgres_config.host,
184
+ postgres_config.port,
185
+ postgres_config.database,
186
+ type(exc).__name__,
164
187
  )
165
- except Exception:
166
- logger.exception(
167
- "[fred-runtime] Failed to initialize SQL storage running stateless"
168
- )
188
+ # This engine never reaches self._sql_engine on this path, so
189
+ # shutdown()'s own dispose() (below) can never reach it either —
190
+ # dispose it here or it leaks. Harmless today (the sole caller,
191
+ # agent_app.py's lifespan, has no retry and the process exits on
192
+ # this raise) but becomes a real leak the moment any retry/backoff
193
+ # wrapper is added around initialize_sql() or the lifespan.
194
+ await engine.dispose()
195
+ raise
196
+
197
+ init_user_store(engine)
198
+ checkpointer = FredSqlCheckpointer(engine, kpi=self.get_kpi_writer())
199
+ history_store = PostgresHistoryStore(engine, kpi=self.get_kpi_writer())
200
+ self._sql_engine = engine
201
+ self._checkpointer = checkpointer
202
+ self._history_store = history_store
203
+ logger.info(
204
+ "[fred-runtime] SQL checkpointer and history store ready (dialect=%s)",
205
+ engine.dialect.name,
206
+ )
169
207
 
170
208
  def start_metrics_exporter(self) -> None:
171
209
  """Start the Prometheus scrape endpoint when configured."""
@@ -188,7 +188,10 @@ def create_openai_compat_router(
188
188
 
189
189
  # F-A: gate this surface with the same pod-side OpenFGA check as
190
190
  # /agents/execute. When ReBAC is active, the caller must present a team
191
- # (X-Fred-Team-Id) and hold CAN_READ on it; identity is the validated JWT.
191
+ # (X-Fred-Team-Id) and hold CAN_USE_TEAM_AGENTS on it (team_member-only,
192
+ # unlike CAN_READ which also admits the public marketplace-discovery
193
+ # relation — this is real execution, not discovery); identity is the
194
+ # validated JWT.
192
195
  rebac = get_runtime_context().config.rebac_engine
193
196
  if authenticated_user is not None and rebac is not None and rebac.enabled:
194
197
  if not team_id:
@@ -198,7 +201,7 @@ def create_openai_compat_router(
198
201
  )
199
202
  try:
200
203
  await rebac.check_user_team_permission_or_raise(
201
- authenticated_user, TeamPermission.CAN_READ, team_id
204
+ authenticated_user, TeamPermission.CAN_USE_TEAM_AGENTS, team_id
202
205
  )
203
206
  except AuthorizationError as exc:
204
207
  raise HTTPException(
@@ -373,7 +373,9 @@ def build_capability_agent_block(
373
373
  # combining capability's tools() output from the ReAct binding
374
374
  # (block.tools/HITL still saw it, but create_agent() never did).
375
375
  if capability_tools:
376
- middleware.append(ToolCarrierMiddleware(capability_tools))
376
+ middleware.append(
377
+ ToolCarrierMiddleware(capability_tools, capability_id=cap_id)
378
+ )
377
379
  if type(capability).middleware is not AgentCapability.middleware:
378
380
  # Overridden: call it too, for the hook tools() cannot express.
379
381
  # No double-computation risk — the default middleware() (which