fred-runtime 3.5.2__tar.gz → 4.0.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 (220) hide show
  1. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/PKG-INFO +3 -3
  2. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/app/agent_app.py +119 -29
  3. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/app/config.py +23 -8
  4. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/app/context.py +1 -1
  5. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/capabilities/document_read_common.py +3 -1
  6. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/capabilities/mcp.py +5 -0
  7. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/cli/history_display.py +14 -5
  8. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/cli/pod_client.py +8 -0
  9. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/cli/repl.py +10 -5
  10. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/integrations/v2_runtime/adapters.py +5 -0
  11. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/react/react_prompting.py +11 -8
  12. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/react/react_stream_adapter.py +3 -9
  13. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/runtime_context.py +2 -0
  14. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/runtime_support/sql_checkpointer.py +35 -19
  15. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime.egg-info/PKG-INFO +3 -3
  16. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime.egg-info/requires.txt +2 -2
  17. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/pyproject.toml +3 -3
  18. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_agent_app.py +168 -1
  19. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_capability_chat_parts_1977.py +1 -0
  20. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_capability_document_reading.py +14 -0
  21. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_client.py +16 -0
  22. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_config_loader.py +80 -1
  23. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_context.py +13 -0
  24. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_history.py +121 -0
  25. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_history_display.py +8 -0
  26. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_hitl_resume_langgraph_integration.py +140 -7
  27. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_openai_compat_router.py +1 -0
  28. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_pod_client.py +9 -3
  29. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_react_prompting.py +6 -4
  30. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_sql_checkpointer_hitl_claim.py +70 -1
  31. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/README.md +0 -0
  32. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/__init__.py +0 -0
  33. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/__main__.py +0 -0
  34. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/app/__init__.py +0 -0
  35. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/app/_catalogs.py +0 -0
  36. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/app/chat_input_limit.py +0 -0
  37. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/app/config_loader.py +0 -0
  38. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/app/container.py +0 -0
  39. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/app/dependencies.py +0 -0
  40. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/app/mcp_config.py +0 -0
  41. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/app/observability_factory.py +0 -0
  42. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/app/openai_compat_router.py +0 -0
  43. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/app/platform_sql.py +0 -0
  44. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/capabilities/__init__.py +0 -0
  45. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/capabilities/assembly.py +0 -0
  46. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/capabilities/assets.py +0 -0
  47. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/capabilities/demo.py +0 -0
  48. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/capabilities/demo_migrations/env.py +0 -0
  49. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/capabilities/demo_migrations/versions/d1e2m0a0b0c0_create_demo_echo_notes.py +0 -0
  50. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/capabilities/document_access/__init__.py +0 -0
  51. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/capabilities/document_access/capability.py +0 -0
  52. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/capabilities/document_extract/__init__.py +0 -0
  53. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/capabilities/document_extract/capability.py +0 -0
  54. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/capabilities/document_label_search/__init__.py +0 -0
  55. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/capabilities/document_label_search/capability.py +0 -0
  56. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/capabilities/document_similarity/__init__.py +0 -0
  57. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/capabilities/document_similarity/capability.py +0 -0
  58. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/capabilities/document_summarize/__init__.py +0 -0
  59. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/capabilities/document_summarize/capability.py +0 -0
  60. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/capabilities/document_verbatim/__init__.py +0 -0
  61. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/capabilities/document_verbatim/capability.py +0 -0
  62. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/capabilities/errors.py +0 -0
  63. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/capabilities/openapi_dump.py +0 -0
  64. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/capabilities/registry.py +0 -0
  65. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/cli/__init__.py +0 -0
  66. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/cli/completion.py +0 -0
  67. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/cli/entrypoint.py +0 -0
  68. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/cli/kpi_display.py +0 -0
  69. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/cli/repl_helpers.py +0 -0
  70. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/cli/url_helpers.py +0 -0
  71. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/client.py +0 -0
  72. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/common/__init__.py +0 -0
  73. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/common/context_aware_tool.py +0 -0
  74. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/common/kf_base_client.py +0 -0
  75. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/common/kf_document_client.py +0 -0
  76. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/common/kf_fast_text_client.py +0 -0
  77. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/common/kf_http_client.py +0 -0
  78. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/common/kf_markdown_media_client.py +0 -0
  79. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/common/kf_tag_client.py +0 -0
  80. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/common/kf_vectorsearch_client.py +0 -0
  81. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/common/kf_workspace_client.py +0 -0
  82. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/common/mcp_interceptors.py +0 -0
  83. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/common/mcp_runtime.py +0 -0
  84. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/common/mcp_toolkit.py +0 -0
  85. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/common/mcp_utils.py +0 -0
  86. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/common/structures.py +0 -0
  87. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/common/table_hits.py +0 -0
  88. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/common/token_expiry.py +0 -0
  89. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/common/tool_node_utils.py +0 -0
  90. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/deep/__init__.py +0 -0
  91. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/deep/deep_runtime.py +0 -0
  92. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/eval/__init__.py +0 -0
  93. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/eval/collector.py +0 -0
  94. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/graph/__init__.py +0 -0
  95. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/graph/graph_runtime.py +0 -0
  96. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/integrations/__init__.py +0 -0
  97. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/integrations/inprocess_toolkit_registry.py +0 -0
  98. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/integrations/kf_vector_search/__init__.py +0 -0
  99. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/integrations/kf_vector_search/toolkit.py +0 -0
  100. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/integrations/v2_runtime/__init__.py +0 -0
  101. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/migrations/__init__.py +0 -0
  102. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/migrations/env.py +0 -0
  103. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/migrations/script.py.mako +0 -0
  104. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/migrations/versions/__init__.py +0 -0
  105. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/migrations/versions/a1e2f3c4d5b6_create_session_history.py +0 -0
  106. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/migrations/versions/b2f3a4e5c6d7_add_exchange_id.py +0 -0
  107. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/migrations/versions/c3d4b5a6f7e8_add_team_and_instance.py +0 -0
  108. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/model_routing/__init__.py +0 -0
  109. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/model_routing/authz.py +0 -0
  110. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/model_routing/catalog.py +0 -0
  111. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/model_routing/contracts.py +0 -0
  112. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/model_routing/provider.py +0 -0
  113. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/model_routing/resolver.py +0 -0
  114. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/react/__init__.py +0 -0
  115. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/react/middleware/__init__.py +0 -0
  116. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/react/middleware/checkpoint_hygiene.py +0 -0
  117. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/react/middleware/dynamic_prompt.py +0 -0
  118. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/react/middleware/frame.py +0 -0
  119. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/react/middleware/hitl.py +0 -0
  120. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/react/middleware/shared.py +0 -0
  121. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/react/middleware/tool_observability.py +0 -0
  122. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/react/middleware/tracing_kpi.py +0 -0
  123. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/react/react_langchain_adapter.py +0 -0
  124. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/react/react_message_codec.py +0 -0
  125. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/react/react_model_adapter.py +0 -0
  126. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/react/react_runtime.py +0 -0
  127. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/react/react_tool_binding.py +0 -0
  128. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/react/react_tool_loop.py +0 -0
  129. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/react/react_tool_rendering.py +0 -0
  130. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/react/react_tool_resolution.py +0 -0
  131. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/react/react_tool_utils.py +0 -0
  132. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/react/react_tracing.py +0 -0
  133. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/runtime_support/__init__.py +0 -0
  134. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/runtime_support/checkpoints.py +0 -0
  135. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/runtime_support/model_metadata.py +0 -0
  136. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/runtime_support/request_context_helpers.py +0 -0
  137. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/runtime_support/trace_payloads.py +0 -0
  138. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/runtime_support/user_token_refresher.py +0 -0
  139. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/support/__init__.py +0 -0
  140. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/support/filesystem_context.py +0 -0
  141. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/support/thinking.py +0 -0
  142. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime/support/tool_loop.py +0 -0
  143. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime.egg-info/SOURCES.txt +0 -0
  144. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime.egg-info/dependency_links.txt +0 -0
  145. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime.egg-info/entry_points.txt +0 -0
  146. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/fred_runtime.egg-info/top_level.txt +0 -0
  147. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/setup.cfg +0 -0
  148. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_capability_agent_1973.py +0 -0
  149. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_capability_chat_controls_1976.py +0 -0
  150. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_capability_document_access_1906.py +0 -0
  151. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_capability_document_label_search.py +0 -0
  152. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_capability_document_similarity.py +0 -0
  153. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_capability_document_summarize.py +0 -0
  154. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_capability_endpoints_1974.py +0 -0
  155. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_capability_registry_1973.py +0 -0
  156. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_capability_routes_tables_1979.py +0 -0
  157. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_capability_selection_1974.py +0 -0
  158. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_capability_tool_return_convention.py +0 -0
  159. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_chat_input_limit.py +0 -0
  160. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_checkpoint_namespace.py +0 -0
  161. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_context_aware_tool.py +0 -0
  162. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_conversational_memory.py +0 -0
  163. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_deep_agent_dispatch.py +0 -0
  164. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_deep_agent_hitl_integration.py +0 -0
  165. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_deep_agent_middleware.py +0 -0
  166. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_document_port_error_redaction.py +0 -0
  167. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_document_read_scope.py +0 -0
  168. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_document_similarity_adapter.py +0 -0
  169. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_eval_collector.py +0 -0
  170. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_eval_trace.py +0 -0
  171. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_fred_workspace_fs.py +0 -0
  172. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_graph_capability_bridge.py +0 -0
  173. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_graph_checkpoint_namespace.py +0 -0
  174. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_graph_runtime_invoke_agent.py +0 -0
  175. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_graph_runtime_observability.py +0 -0
  176. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_graph_runtime_token_usage_totals.py +0 -0
  177. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_kf_base_client_refresh_shapes.py +0 -0
  178. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_kf_vector_search_toolkit.py +0 -0
  179. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_kf_workspace_client.py +0 -0
  180. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_knowledge_search_table_ordering.py +0 -0
  181. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_knowledge_search_tool_invoker_sources.py +0 -0
  182. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_kpi_display.py +0 -0
  183. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_langfuse_tracing.py +0 -0
  184. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_langgraph_interrupt_id_semantics.py +0 -0
  185. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_mcp_config.py +0 -0
  186. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_migrations.py +0 -0
  187. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_model_enforcement.py +0 -0
  188. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_model_metadata_cache_tokens.py +0 -0
  189. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_model_reasoning_enablement.py +0 -0
  190. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_model_routing.py +0 -0
  191. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_model_routing_provider.py +0 -0
  192. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_models_catalog_projection.py +0 -0
  193. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_platform_sql.py +0 -0
  194. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_platform_sql_postgres_integration.py +0 -0
  195. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_react_checkpoint_namespace.py +0 -0
  196. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_react_loop_regressions_1972.py +0 -0
  197. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_react_message_codec_resume.py +0 -0
  198. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_react_middleware_frame.py +0 -0
  199. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_react_thinking.py +0 -0
  200. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_react_token_usage_totals.py +0 -0
  201. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_react_tool_binding_span_status.py +0 -0
  202. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_react_tool_error_final_2244.py +0 -0
  203. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_react_tool_resolution.py +0 -0
  204. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_repl_helpers.py +0 -0
  205. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_runtime_context_prompt_injection.py +0 -0
  206. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_similarity_search_tool_invoker.py +0 -0
  207. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_smoke.py +0 -0
  208. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_spike_capability_state_1971.py +0 -0
  209. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_sql_checkpointer_owner.py +0 -0
  210. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_table_repair_wiring.py +0 -0
  211. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_team_wiki_adapter.py +0 -0
  212. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_token_expiry.py +0 -0
  213. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_tool_carrier_middleware_naming.py +0 -0
  214. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_tool_loop_trim.py +0 -0
  215. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_tool_observability_middleware.py +0 -0
  216. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_trace_payloads.py +0 -0
  217. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_tracing_kpi_content_confinement.py +0 -0
  218. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_url_helpers.py +0 -0
  219. {fred_runtime-3.5.2 → fred_runtime-4.0.0}/tests/test_user_token_refresher.py +0 -0
  220. {fred_runtime-3.5.2 → fred_runtime-4.0.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: 3.5.2
3
+ Version: 4.0.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>=3.8.3
16
- Requires-Dist: fred-sdk>=3.5.0
15
+ Requires-Dist: fred-core>=4.0.0
16
+ Requires-Dist: fred-sdk>=4.0.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
@@ -932,6 +932,7 @@ class _AgentExecuteRequest(BaseModel):
932
932
  context: dict[str, Any] | None = None
933
933
  checkpoint_id: str | None = Field(default=None, min_length=1)
934
934
  interrupt_id: str | None = Field(default=None, min_length=1)
935
+ occurrence_id: str | None = Field(default=None, min_length=1)
935
936
  resume_payload: Any | None = Field(
936
937
  default=None,
937
938
  description=(
@@ -956,6 +957,10 @@ class _AgentExecuteRequest(BaseModel):
956
957
  "TurnOptionsModel; the middleware receives only its own typed slice."
957
958
  ),
958
959
  )
960
+ # Server-derived, never caller-supplied: the occurrences already pending
961
+ # when this resume arrived, stamped by `_authorize_and_resolve` from the
962
+ # admission gate's own checkpoint read. See `_write_turn_history`.
963
+ previously_pending_occurrence_ids: tuple[str, ...] = ()
959
964
 
960
965
  @model_validator(mode="after")
961
966
  def _require_message_or_resume(self) -> "_AgentExecuteRequest":
@@ -1002,6 +1007,7 @@ def _to_internal_request(r: RuntimeExecuteRequest) -> "_AgentExecuteRequest":
1002
1007
  context=r.to_legacy_context() or None,
1003
1008
  checkpoint_id=r.checkpoint_id,
1004
1009
  interrupt_id=r.interrupt_id,
1010
+ occurrence_id=r.occurrence_id,
1005
1011
  resume_payload=r.resume_payload,
1006
1012
  invocation_turns=r.invocation_turns,
1007
1013
  inline_tuning=r.inline_tuning,
@@ -1842,11 +1848,14 @@ async def _authorize_and_resolve(
1842
1848
  "access_token_expires_at": None, # nosec B105
1843
1849
  }
1844
1850
  )
1845
- await _validate_session_checkpoint_access(request)
1851
+ previously_pending_occurrence_ids = await _validate_session_checkpoint_access(
1852
+ request
1853
+ )
1846
1854
  await _enforce_session_ownership(request, authenticated_user, container)
1847
1855
  async with runtime_stage_timer(container.get_kpi_writer(), "pod_authz"):
1848
1856
  await _authorize_execution_or_raise(request, authenticated_user, container)
1849
1857
  internal_req = _to_internal_request(request)
1858
+ internal_req.previously_pending_occurrence_ids = previously_pending_occurrence_ids
1850
1859
  # Stamp the trusted service-agent verdict (never the caller-supplied
1851
1860
  # context) so per-tool-call re-authorization can mirror the bypass
1852
1861
  # `_authorize_execution_or_raise` already granted above (RFC EVAL-AUTH,
@@ -1923,19 +1932,18 @@ def _validate_resolved_team(
1923
1932
  _REACT_V2_INTERRUPT_CHANNEL = "__interrupt__"
1924
1933
 
1925
1934
 
1926
- def _pending_react_v2_interrupt_ids(
1935
+ def _pending_react_v2_interrupt_occurrences(
1927
1936
  pending_writes: Sequence[tuple[str, str, Any]],
1928
- ) -> frozenset[str]:
1937
+ ) -> frozenset[tuple[str, str | None]]:
1929
1938
  """
1930
- Extract LangGraph's own `Interrupt.id` for every currently pending
1931
- `"__interrupt__"` write on a ReAct V2 checkpoint (#2216).
1939
+ Extract every pending ReAct V2 `(interrupt_id, occurrence_id)` pair.
1932
1940
 
1933
1941
  Collects ALL matching ids rather than the first one found: FRED's own
1934
1942
  `FredHitlMiddleware` never raises more than one interrupt per task
1935
1943
  (`test_hitl_resume_two_sequential_prompts_get_different_interrupt_ids`),
1936
1944
  but this function does not assume that — it proves the caller's
1937
- `interrupt_id` matches *some* currently pending occurrence, not merely
1938
- "a" pending write chosen arbitrarily.
1945
+ resume identity matches the exact occurrence, not merely a pending write
1946
+ chosen arbitrarily. Legacy payloads without an occurrence id keep `None`.
1939
1947
 
1940
1948
  Why `pending_writes` is read directly instead of going through
1941
1949
  LangGraph's supported `compiled_graph.aget_state(...).tasks[*].interrupts`:
@@ -1969,7 +1977,7 @@ def _pending_react_v2_interrupt_ids(
1969
1977
  that don't round-trip through LangGraph's own serde
1970
1978
  """
1971
1979
 
1972
- ids: set[str] = set()
1980
+ occurrences: set[tuple[str, str | None]] = set()
1973
1981
  for _task_id, channel, value in pending_writes:
1974
1982
  if channel != _REACT_V2_INTERRUPT_CHANNEL:
1975
1983
  continue
@@ -1982,8 +1990,21 @@ def _pending_react_v2_interrupt_ids(
1982
1990
  if interrupt_id is None and isinstance(candidate, dict):
1983
1991
  interrupt_id = candidate.get("id")
1984
1992
  if isinstance(interrupt_id, str) and interrupt_id:
1985
- ids.add(interrupt_id)
1986
- return frozenset(ids)
1993
+ payload = getattr(candidate, "value", None)
1994
+ if payload is None and isinstance(candidate, dict):
1995
+ payload = candidate.get("value")
1996
+ occurrence_id = getattr(payload, "occurrence_id", None)
1997
+ if occurrence_id is None and isinstance(payload, dict):
1998
+ occurrence_id = payload.get("occurrence_id")
1999
+ occurrences.add(
2000
+ (
2001
+ interrupt_id,
2002
+ occurrence_id
2003
+ if isinstance(occurrence_id, str) and occurrence_id
2004
+ else None,
2005
+ )
2006
+ )
2007
+ return frozenset(occurrences)
1987
2008
 
1988
2009
 
1989
2010
  def _resume_checkpoint_namespaces(request: RuntimeExecuteRequest) -> tuple[str, ...]:
@@ -2020,7 +2041,7 @@ def _resume_checkpoint_namespaces(request: RuntimeExecuteRequest) -> tuple[str,
2020
2041
 
2021
2042
  async def _validate_session_checkpoint_access(
2022
2043
  request: RuntimeExecuteRequest,
2023
- ) -> None:
2044
+ ) -> tuple[str, ...]:
2024
2045
  """
2025
2046
  Validate session/checkpoint consistency for resume-capable runtime requests.
2026
2047
 
@@ -2047,6 +2068,11 @@ async def _validate_session_checkpoint_access(
2047
2068
  - this helper is intentionally conservative: it only validates local
2048
2069
  session/checkpoint consistency that the runtime can prove itself
2049
2070
 
2071
+ Returns the occurrence ids already pending on the resumed checkpoint —
2072
+ the pauses an earlier run has therefore already surfaced and persisted.
2073
+ `_write_turn_history` skips their re-emission so one question keeps one
2074
+ history row. Every non-resume path returns an empty tuple.
2075
+
2050
2076
  Example:
2051
2077
  - `await _validate_session_checkpoint_access(request)`
2052
2078
 
@@ -2066,7 +2092,7 @@ async def _validate_session_checkpoint_access(
2066
2092
  `request.interrupt_id`, LangGraph's own `Interrupt.id` — this
2067
2093
  function requires it to exactly match one of the ids currently
2068
2094
  pending on the thread's latest checkpoint
2069
- (`_pending_react_v2_interrupt_ids`) — a stale response for an earlier
2095
+ (`_pending_react_v2_interrupt_occurrences`) — a stale response for an earlier
2070
2096
  interrupt must never be accepted for a later one (#2216 P1).
2071
2097
  `react_message_codec.py` threads the same id into LangGraph's own
2072
2098
  targeted `Command(resume={id: ...})` form as defense in depth, so the
@@ -2080,7 +2106,7 @@ async def _validate_session_checkpoint_access(
2080
2106
  or request.resume_payload is not None
2081
2107
  )
2082
2108
  if not needs_checkpoint_validation:
2083
- return
2109
+ return ()
2084
2110
 
2085
2111
  session_id = request.effective_session_id()
2086
2112
  if not session_id:
@@ -2092,7 +2118,7 @@ async def _validate_session_checkpoint_access(
2092
2118
 
2093
2119
  checkpointer = get_runtime_context().config.checkpointer
2094
2120
  if checkpointer is None:
2095
- return
2121
+ return ()
2096
2122
 
2097
2123
  loaded = None
2098
2124
  for checkpoint_ns in _resume_checkpoint_namespaces(request):
@@ -2142,7 +2168,7 @@ async def _validate_session_checkpoint_access(
2142
2168
  )
2143
2169
 
2144
2170
  if request.resume_payload is None:
2145
- return
2171
+ return ()
2146
2172
 
2147
2173
  if channel_values.get("runtime_kind") == "graph_v2":
2148
2174
  if channel_values.get("pending") is not True:
@@ -2161,7 +2187,7 @@ async def _validate_session_checkpoint_access(
2161
2187
  status_code=status.HTTP_409_CONFLICT,
2162
2188
  detail="checkpoint_id does not match the pending checkpoint for this session.",
2163
2189
  )
2164
- return
2190
+ return ()
2165
2191
 
2166
2192
  # Not the legacy Graph runtime (ReAct V2, or the fallback lookup landed on
2167
2193
  # some other non-graph_v2 checkpoint kind). #2216 P1: a checkpoint being
@@ -2169,20 +2195,33 @@ async def _validate_session_checkpoint_access(
2169
2195
  # an EARLIER interrupt on this same thread must never be allowed to
2170
2196
  # resume a LATER one. `request.interrupt_id` must exactly match one of
2171
2197
  # the ids currently pending on this checkpoint.
2172
- pending_interrupt_ids = _pending_react_v2_interrupt_ids(pending_writes)
2173
- if not pending_interrupt_ids:
2198
+ pending_occurrences = _pending_react_v2_interrupt_occurrences(pending_writes)
2199
+ if not pending_occurrences:
2174
2200
  raise HTTPException(
2175
2201
  status_code=status.HTTP_409_CONFLICT,
2176
2202
  detail="checkpoint is not waiting for resume.",
2177
2203
  )
2178
- if (
2179
- request.interrupt_id is None
2180
- or request.interrupt_id not in pending_interrupt_ids
2181
- ):
2204
+ matching_occurrence_ids = {
2205
+ occurrence_id
2206
+ for interrupt_id, occurrence_id in pending_occurrences
2207
+ if interrupt_id == request.interrupt_id
2208
+ }
2209
+ if not matching_occurrence_ids:
2182
2210
  raise HTTPException(
2183
2211
  status_code=status.HTTP_409_CONFLICT,
2184
2212
  detail="interrupt_id does not match the pending HITL request for this session.",
2185
2213
  )
2214
+ if request.occurrence_id not in matching_occurrence_ids:
2215
+ raise HTTPException(
2216
+ status_code=status.HTTP_409_CONFLICT,
2217
+ detail="occurrence_id does not match the pending HITL request for this session.",
2218
+ )
2219
+
2220
+ return tuple(
2221
+ occurrence_id
2222
+ for _, occurrence_id in pending_occurrences
2223
+ if occurrence_id is not None
2224
+ )
2186
2225
 
2187
2226
 
2188
2227
  @dataclass
@@ -2217,6 +2256,8 @@ async def _write_turn_history(
2217
2256
  agent_instance_id: str | None = None,
2218
2257
  exchange_id: str | None = None,
2219
2258
  resume_payload: Any | None = None,
2259
+ occurrence_id: str | None = None,
2260
+ previously_pending_occurrence_ids: tuple[str, ...] = (),
2220
2261
  ) -> None:
2221
2262
  """
2222
2263
  Persist one agent turn to the history store.
@@ -2236,6 +2277,9 @@ async def _write_turn_history(
2236
2277
  - ``payloads`` is the list of ``dict`` produced by ``_iterate_runtime_event_payloads``
2237
2278
  - ``resume_payload`` is set for HITL resume turns; the user's choice is stored
2238
2279
  as a ``Channel.hitl_response`` row instead of a plain user text row
2280
+ - ``previously_pending_occurrence_ids`` comes from the admission gate
2281
+ (`_validate_session_checkpoint_access`): a resumed run re-raises every
2282
+ sibling pause still waiting, and re-emitting one is not a new question
2239
2283
  - silently no-ops when ``session_id`` or ``history_store`` is absent
2240
2284
 
2241
2285
  Event-to-message mapping:
@@ -2279,16 +2323,33 @@ async def _write_turn_history(
2279
2323
 
2280
2324
  # 1. Opening row: user text on normal turns, HITL response on resume turns.
2281
2325
  if resume_payload is not None:
2282
- # Extract choice_id from whatever shape the resume payload takes.
2326
+ choice_id: str | None = None
2327
+ text: str | None = None
2283
2328
  if isinstance(resume_payload, dict):
2284
- choice_id = str(resume_payload.get("choice_id") or "")
2329
+ raw_choice_id = resume_payload.get("choice_id")
2330
+ if isinstance(raw_choice_id, str) and raw_choice_id:
2331
+ choice_id = raw_choice_id
2332
+ raw_text = resume_payload.get("text")
2333
+ if isinstance(raw_text, str) and raw_text:
2334
+ text = raw_text
2335
+ elif choice_id is None:
2336
+ raw_answer = resume_payload.get("answer")
2337
+ if isinstance(raw_answer, str) and raw_answer:
2338
+ text = raw_answer
2285
2339
  elif isinstance(resume_payload, str):
2286
2340
  choice_id = resume_payload
2287
2341
  else:
2288
2342
  choice_id = str(resume_payload)
2289
- if choice_id:
2343
+ if choice_id or text:
2290
2344
  messages.append(
2291
- make_hitl_response(session_id, exchange_id, rank, choice_id=choice_id)
2345
+ make_hitl_response(
2346
+ session_id,
2347
+ exchange_id,
2348
+ rank,
2349
+ choice_id=choice_id,
2350
+ text=text,
2351
+ occurrence_id=occurrence_id,
2352
+ )
2292
2353
  )
2293
2354
  rank += 1
2294
2355
  elif request_message:
@@ -2432,6 +2493,15 @@ async def _write_turn_history(
2432
2493
  # AND a reload while the gate is still open can reconstruct a
2433
2494
  # working (not just readable) prompt.
2434
2495
  req = payload.get("request", {})
2496
+ # A resumed run re-raises the siblings still waiting, so the same
2497
+ # pause is emitted again. One question keeps one row: the run that
2498
+ # first surfaced it already wrote it.
2499
+ surfaced_occurrence_id = req.get("occurrence_id")
2500
+ if (
2501
+ surfaced_occurrence_id
2502
+ and surfaced_occurrence_id in previously_pending_occurrence_ids
2503
+ ):
2504
+ continue
2435
2505
  question = req.get("question") or req.get("title") or "HITL pause"
2436
2506
  raw_choices = req.get("choices") or []
2437
2507
  raw_pending_calls = req.get("pending_calls") or []
@@ -2453,6 +2523,7 @@ async def _write_turn_history(
2453
2523
  title=req.get("title"),
2454
2524
  free_text=bool(req.get("free_text")),
2455
2525
  interrupt_id=req.get("interrupt_id"),
2526
+ occurrence_id=req.get("occurrence_id"),
2456
2527
  checkpoint_id=req.get("checkpoint_id"),
2457
2528
  pending_calls=[
2458
2529
  {
@@ -2917,6 +2988,8 @@ async def _stream(
2917
2988
  agent_instance_id=request.agent_instance_id,
2918
2989
  exchange_id=exchange_id,
2919
2990
  resume_payload=request.resume_payload,
2991
+ occurrence_id=request.occurrence_id,
2992
+ previously_pending_occurrence_ids=request.previously_pending_occurrence_ids,
2920
2993
  )
2921
2994
  )
2922
2995
 
@@ -3222,6 +3295,7 @@ class _HitlResumeClaim:
3222
3295
  _thread_id: str
3223
3296
  _checkpoint_ns: str
3224
3297
  _interrupt_id: str
3298
+ _occurrence_id: str | None
3225
3299
  _claim_token: str
3226
3300
 
3227
3301
  async def consume(self) -> None:
@@ -3229,6 +3303,7 @@ class _HitlResumeClaim:
3229
3303
  thread_id=self._thread_id,
3230
3304
  checkpoint_ns=self._checkpoint_ns,
3231
3305
  interrupt_id=self._interrupt_id,
3306
+ occurrence_id=self._occurrence_id,
3232
3307
  claim_token=self._claim_token,
3233
3308
  )
3234
3309
 
@@ -3238,6 +3313,7 @@ async def _claim_hitl_resume_before_invocation(
3238
3313
  session_id: str | None,
3239
3314
  checkpoint_ns: str,
3240
3315
  interrupt_id: str,
3316
+ occurrence_id: str | None = None,
3241
3317
  ) -> _HitlResumeClaim | None:
3242
3318
  """
3243
3319
  Acquire and confirm the durable single-use HITL resume claim,
@@ -3296,7 +3372,10 @@ async def _claim_hitl_resume_before_invocation(
3296
3372
  )
3297
3373
  return None
3298
3374
  claim_token = await checkpointer.aclaim_hitl_resume(
3299
- thread_id=session_id, checkpoint_ns=checkpoint_ns, interrupt_id=interrupt_id
3375
+ thread_id=session_id,
3376
+ checkpoint_ns=checkpoint_ns,
3377
+ interrupt_id=interrupt_id,
3378
+ occurrence_id=occurrence_id,
3300
3379
  )
3301
3380
  if claim_token is None:
3302
3381
  raise RuntimeError("This HITL request is already being resumed.")
@@ -3305,6 +3384,7 @@ async def _claim_hitl_resume_before_invocation(
3305
3384
  thread_id=session_id,
3306
3385
  checkpoint_ns=checkpoint_ns,
3307
3386
  interrupt_id=interrupt_id,
3387
+ occurrence_id=occurrence_id,
3308
3388
  claim_token=claim_token,
3309
3389
  )
3310
3390
  except Exception:
@@ -3317,6 +3397,7 @@ async def _claim_hitl_resume_before_invocation(
3317
3397
  thread_id=session_id,
3318
3398
  checkpoint_ns=checkpoint_ns,
3319
3399
  interrupt_id=interrupt_id,
3400
+ occurrence_id=occurrence_id,
3320
3401
  claim_token=claim_token,
3321
3402
  )
3322
3403
  raise
@@ -3330,6 +3411,7 @@ async def _claim_hitl_resume_before_invocation(
3330
3411
  thread_id=session_id,
3331
3412
  checkpoint_ns=checkpoint_ns,
3332
3413
  interrupt_id=interrupt_id,
3414
+ occurrence_id=occurrence_id,
3333
3415
  claim_token=claim_token,
3334
3416
  )
3335
3417
  raise RuntimeError(
@@ -3341,6 +3423,7 @@ async def _claim_hitl_resume_before_invocation(
3341
3423
  _thread_id=session_id,
3342
3424
  _checkpoint_ns=checkpoint_ns,
3343
3425
  _interrupt_id=interrupt_id,
3426
+ _occurrence_id=occurrence_id,
3344
3427
  _claim_token=claim_token,
3345
3428
  )
3346
3429
 
@@ -3405,6 +3488,7 @@ async def _iterate_runtime_event_payloads(
3405
3488
  )
3406
3489
  resolved_checkpoint_id = request.checkpoint_id or ctx.get("checkpoint_id")
3407
3490
  resolved_interrupt_id = request.interrupt_id or ctx.get("interrupt_id")
3491
+ resolved_occurrence_id = request.occurrence_id or ctx.get("occurrence_id")
3408
3492
 
3409
3493
  portable_context = PortableContext(
3410
3494
  request_id=request_id,
@@ -3425,6 +3509,7 @@ async def _iterate_runtime_event_payloads(
3425
3509
  "template_agent_id": definition.agent_id,
3426
3510
  "checkpoint_id": resolved_checkpoint_id,
3427
3511
  "interrupt_id": resolved_interrupt_id,
3512
+ "occurrence_id": resolved_occurrence_id,
3428
3513
  "execution_action": execution_action,
3429
3514
  "exchange_id": exchange_id,
3430
3515
  "is_service_agent": ctx.get("is_service_agent"),
@@ -3663,6 +3748,7 @@ async def _iterate_runtime_event_payloads(
3663
3748
  # regardless of runtime configuration.
3664
3749
  checkpoint_ns="",
3665
3750
  interrupt_id=request.interrupt_id,
3751
+ occurrence_id=request.occurrence_id,
3666
3752
  )
3667
3753
 
3668
3754
  async for event in executor.stream(react_input, execution_config):
@@ -4738,6 +4824,8 @@ def _build_agent_router(
4738
4824
  agent_instance_id=request.agent_instance_id,
4739
4825
  exchange_id=exchange_id,
4740
4826
  resume_payload=request.resume_payload,
4827
+ occurrence_id=request.occurrence_id,
4828
+ previously_pending_occurrence_ids=internal_req.previously_pending_occurrence_ids,
4741
4829
  )
4742
4830
  return _terminal_execute_payload(payloads)
4743
4831
 
@@ -4825,6 +4913,8 @@ def _build_agent_router(
4825
4913
  agent_instance_id=request.agent_instance_id,
4826
4914
  exchange_id=exchange_id,
4827
4915
  resume_payload=request.resume_payload,
4916
+ occurrence_id=request.occurrence_id,
4917
+ previously_pending_occurrence_ids=internal_req.previously_pending_occurrence_ids,
4828
4918
  )
4829
4919
  return _build_eval_trace(
4830
4920
  payloads=payloads,
@@ -5030,7 +5120,7 @@ def create_agent_app(
5030
5120
  # 10. start_kpi_tasks — asyncio tasks, after SQL engine is known
5031
5121
  # 11. set_runtime_context — wires all built parts into the global config
5032
5122
  log_setup(
5033
- service_name=config.app.name,
5123
+ service_name=config.app.runtime_id,
5034
5124
  log_level=config.app.log_level,
5035
5125
  store=build_log_store(
5036
5126
  log_store_config=config.storage.log_store,
@@ -5078,7 +5168,7 @@ def create_agent_app(
5078
5168
  FredRuntimeContext(
5079
5169
  RuntimeConfig(
5080
5170
  knowledge_flow_url=config.ai.knowledge_flow_url,
5081
- service_name=config.app.name,
5171
+ service_name=config.app.runtime_id,
5082
5172
  timeouts=config.ai.timeout,
5083
5173
  chat_model_factory=chat_factory,
5084
5174
  checkpointer=checkpointer,
@@ -89,23 +89,36 @@ from ..runtime_context import RuntimeTimeouts
89
89
 
90
90
  class PodAppConfig(BaseModel):
91
91
  """
92
- Basic HTTP server and local observability settings for an agent pod.
92
+ Identity and HTTP server settings for an agent pod.
93
93
 
94
94
  Why this exists:
95
- - every pod needs the same HTTP binding knobs plus the small Prometheus/KPI
96
- settings already used by the other Fred backends
95
+ - every pod needs the same identity and HTTP binding knobs, in the same
96
+ place, whoever built it
97
97
  - keeping these fields in `app` preserves the familiar startup contract for
98
98
  local benches and scrape-based debugging
99
99
 
100
100
  How to use it:
101
- - keep the defaults for simple local development
102
- - set `metrics_port` / `metrics_address` when `observability.metrics` is
103
- `prometheus`
101
+ - declare `runtime_id`; every other field defaults sanely for local work
102
+ - the metrics exporter is configured under `observability.kpi.prometheus`,
103
+ not here
104
104
 
105
105
  Example:
106
- - `PodAppConfig(base_url="/pod/v1", port=8000, metrics_port=9115)`
106
+ - `PodAppConfig(runtime_id="my-agents", base_url="/pod/v1", port=8000)`
107
107
  """
108
108
 
109
+ runtime_id: str = Field(
110
+ ...,
111
+ pattern=r"^[a-z][a-z0-9]*(-[a-z0-9]+)*$",
112
+ description=(
113
+ "Slug identifying this pod across every telemetry stream: the KPI "
114
+ "`service` dimension, the `runtime_id` KPI dimension, and the "
115
+ "`service` field of every log record. MUST equal the id the "
116
+ "control-plane registers this pod under in "
117
+ "`runtime_catalog_sources[].runtime_id` — that equality is what "
118
+ "lets a log line be joined to its own KPI. Lowercase slug only, so "
119
+ "no display prose can reach a Prometheus label."
120
+ ),
121
+ )
109
122
  name: str = "Fred Agent Pod"
110
123
  base_url: str = "/api/v1"
111
124
  host: str = "127.0.0.1"
@@ -369,7 +382,9 @@ class AgentPodConfig(BaseModel):
369
382
  _mcp_configuration: McpConfigurationLike | None = PrivateAttr(default=None)
370
383
  _platform_prompt_file: Any | None = PrivateAttr(default=None)
371
384
 
372
- app: PodAppConfig = Field(default_factory=PodAppConfig)
385
+ # Required, unlike every other section: `app.runtime_id` has no default, so
386
+ # an omitted `app:` block would leave the pod unable to name itself.
387
+ app: PodAppConfig
373
388
  security: SecurityConfiguration
374
389
  ai: PodAIConfig = Field(default_factory=PodAIConfig)
375
390
  observability: PodObservabilityConfig = Field(
@@ -142,7 +142,7 @@ class PodApplicationContext:
142
142
  self._kpi_writer = build_kpi_writer(
143
143
  kpi_config=config.observability.kpi,
144
144
  opensearch_config=config.storage.opensearch,
145
- service_name="fred-runtime",
145
+ service_name=config.app.runtime_id,
146
146
  log_level=config.app.log_level,
147
147
  )
148
148
 
@@ -196,7 +196,9 @@ def _pagination_footer(result: DocumentMarkdownResult, *, exhaustive: bool) -> s
196
196
  f"the end is reached.]"
197
197
  )
198
198
  return (
199
- f"\n\n[{seen}. More text remains — call again with "
199
+ f"\n\n[More text remains — {seen} read. You have NOT seen the "
200
+ f"whole document yet; do not present this page as the complete "
201
+ f"document. You may call this tool again with "
200
202
  f"offset={result.next_offset} to continue reading.]"
201
203
  )
202
204
  if result.offset == 0 and result.total_chars == 0:
@@ -283,6 +283,11 @@ def build_mcp_capability(server: MCPServerConfiguration) -> McpCapability:
283
283
  manifest = CapabilityManifest(
284
284
  id=server.id,
285
285
  version=MCP_CAPABILITY_SCHEMA_VERSION,
286
+ # An MCP server has no version of its own: the constant above is the
287
+ # stored-config schema version, and every other capability declares a
288
+ # real semver here. Surfacing the schema constant made every MCP row
289
+ # read "v1" in the admin catalog, which looked like information.
290
+ public_version=None,
286
291
  name=server.name,
287
292
  description=server.description or server.name,
288
293
  icon=_MCP_CAPABILITY_ICON,
@@ -327,6 +327,7 @@ def run_single_turn(
327
327
  color_enabled: bool,
328
328
  checkpoint_id: str | None = None,
329
329
  interrupt_id: str | None = None,
330
+ occurrence_id: str | None = None,
330
331
  resume_payload: Any = None,
331
332
  inline_tuning: dict[str, Any] | None = None,
332
333
  ) -> tuple[int, dict[str, Any] | None]:
@@ -336,12 +337,12 @@ def run_single_turn(
336
337
  Returns (exit_code, hitl_request) where hitl_request is set when the agent
337
338
  is paused at a HITL gate, or None when the turn completed normally.
338
339
 
339
- checkpoint_id / interrupt_id (#2216):
340
+ checkpoint_id / interrupt_id / occurrence_id:
340
341
  - forwarded verbatim to the pod, never aliased for each other — the
341
342
  legacy Graph V2 runtime resumes via checkpoint_id, ReAct V2 resumes
342
- via interrupt_id (LangGraph's own `Interrupt.id`). The caller extracts
343
- whichever one the pending `AwaitingHumanRuntimeEvent.request` actually
344
- carries; only one is ever set per resume.
343
+ via interrupt_id (LangGraph's own `Interrupt.id`). Tool-raised pauses
344
+ additionally carry occurrence_id. The caller echoes the identifiers
345
+ present on the pending `AwaitingHumanRuntimeEvent.request` verbatim.
345
346
  """
346
347
  if not stream:
347
348
  payload = client.execute(
@@ -352,6 +353,7 @@ def run_single_turn(
352
353
  team_id=team_id,
353
354
  checkpoint_id=checkpoint_id,
354
355
  interrupt_id=interrupt_id,
356
+ occurrence_id=occurrence_id,
355
357
  resume_payload=resume_payload,
356
358
  inline_tuning=inline_tuning,
357
359
  )
@@ -390,6 +392,7 @@ def run_single_turn(
390
392
  team_id=team_id,
391
393
  checkpoint_id=checkpoint_id,
392
394
  interrupt_id=interrupt_id,
395
+ occurrence_id=occurrence_id,
393
396
  resume_payload=resume_payload,
394
397
  inline_tuning=inline_tuning,
395
398
  ):
@@ -422,7 +425,8 @@ def run_single_turn(
422
425
  def build_hitl_resume_payload(
423
426
  *,
424
427
  raw_response: str,
425
- choices: list[dict[str, Any]] | tuple[dict[str, Any], ...],
428
+ choices: list[dict[str, Any]] | tuple[dict[str, Any], ...] = (),
429
+ free_text: bool = False,
426
430
  ) -> Any:
427
431
  """
428
432
  Convert one terminal HITL answer into the runtime resume payload shape.
@@ -431,7 +435,12 @@ def build_hitl_resume_payload(
431
435
  - the interactive shell accepts either a 1-based menu index or a raw
432
436
  choice id, but graph `choice_step(...)` expects a structured
433
437
  `{"choice_id": ...}` resume payload
438
+ - a typed answer must travel as `{"answer": ...}`, the same field the web
439
+ client sends: a bare string is stored as the chosen `choice_id`, which
440
+ is exactly the audit shape the response contract dropped
434
441
  """
442
+ if free_text:
443
+ return {"answer": raw_response}
435
444
  selected_choice_id = raw_response
436
445
  if raw_response.isdigit():
437
446
  idx = int(raw_response) - 1
@@ -93,6 +93,7 @@ class AgentPodClient:
93
93
  agent_instance_id: str | None = None,
94
94
  checkpoint_id: str | None = None,
95
95
  interrupt_id: str | None = None,
96
+ occurrence_id: str | None = None,
96
97
  resume_payload: Any = None,
97
98
  inline_tuning: dict[str, Any] | None = None,
98
99
  ) -> dict[str, Any]:
@@ -111,6 +112,8 @@ class AgentPodClient:
111
112
  payload["checkpoint_id"] = checkpoint_id
112
113
  if interrupt_id is not None:
113
114
  payload["interrupt_id"] = interrupt_id
115
+ if occurrence_id is not None:
116
+ payload["occurrence_id"] = occurrence_id
114
117
  if resume_payload is not None:
115
118
  payload["resume_payload"] = resume_payload
116
119
  if inline_tuning:
@@ -172,6 +175,7 @@ class AgentPodClient:
172
175
  agent_instance_id: str | None = None,
173
176
  checkpoint_id: str | None = None,
174
177
  interrupt_id: str | None = None,
178
+ occurrence_id: str | None = None,
175
179
  resume_payload: Any = None,
176
180
  inline_tuning: dict[str, Any] | None = None,
177
181
  ) -> list[dict[str, Any]]:
@@ -185,6 +189,7 @@ class AgentPodClient:
185
189
  agent_instance_id=agent_instance_id,
186
190
  checkpoint_id=checkpoint_id,
187
191
  interrupt_id=interrupt_id,
192
+ occurrence_id=occurrence_id,
188
193
  resume_payload=resume_payload,
189
194
  inline_tuning=inline_tuning,
190
195
  ):
@@ -202,6 +207,7 @@ class AgentPodClient:
202
207
  agent_instance_id: str | None = None,
203
208
  checkpoint_id: str | None = None,
204
209
  interrupt_id: str | None = None,
210
+ occurrence_id: str | None = None,
205
211
  resume_payload: Any = None,
206
212
  inline_tuning: dict[str, Any] | None = None,
207
213
  ) -> Iterator[dict[str, Any]]:
@@ -220,6 +226,8 @@ class AgentPodClient:
220
226
  payload["checkpoint_id"] = checkpoint_id
221
227
  if interrupt_id is not None:
222
228
  payload["interrupt_id"] = interrupt_id
229
+ if occurrence_id is not None:
230
+ payload["occurrence_id"] = occurrence_id
223
231
  if resume_payload is not None:
224
232
  payload["resume_payload"] = resume_payload
225
233
  if inline_tuning: