fred-runtime 2.0.10__tar.gz → 3.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/PKG-INFO +3 -3
  2. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/app/__init__.py +0 -2
  3. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/app/agent_app.py +46 -15
  4. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/app/config.py +9 -37
  5. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/app/context.py +15 -26
  6. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/app/observability_factory.py +5 -14
  7. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/common/context_aware_tool.py +50 -16
  8. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/common/kf_workspace_client.py +81 -177
  9. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/graph/graph_runtime.py +184 -117
  10. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/integrations/v2_runtime/adapters.py +107 -139
  11. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/react/react_langchain_adapter.py +4 -0
  12. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/react/react_message_codec.py +8 -11
  13. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/react/react_runtime.py +63 -10
  14. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/react/react_stream_adapter.py +73 -13
  15. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/react/react_tool_resolution.py +18 -58
  16. fred_runtime-3.1.0/fred_runtime/support/thinking.py +211 -0
  17. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/support/tool_loop.py +9 -0
  18. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime.egg-info/PKG-INFO +3 -3
  19. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime.egg-info/SOURCES.txt +4 -0
  20. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime.egg-info/requires.txt +2 -2
  21. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/pyproject.toml +3 -3
  22. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/tests/test_agent_app.py +100 -3
  23. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/tests/test_context_aware_tool.py +44 -1
  24. fred_runtime-3.1.0/tests/test_fred_workspace_fs.py +179 -0
  25. fred_runtime-3.1.0/tests/test_graph_runtime_invoke_agent.py +154 -0
  26. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/tests/test_history.py +7 -0
  27. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/tests/test_kf_workspace_client.py +79 -18
  28. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/tests/test_openai_compat_router.py +7 -0
  29. fred_runtime-3.1.0/tests/test_react_thinking.py +472 -0
  30. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/README.md +0 -0
  31. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/__init__.py +0 -0
  32. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/app/_catalogs.py +0 -0
  33. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/app/config_loader.py +0 -0
  34. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/app/container.py +0 -0
  35. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/app/dependencies.py +0 -0
  36. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/app/mcp_config.py +0 -0
  37. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/app/openai_compat_router.py +0 -0
  38. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/cli/__init__.py +0 -0
  39. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/cli/completion.py +0 -0
  40. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/cli/entrypoint.py +0 -0
  41. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/cli/history_display.py +0 -0
  42. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/cli/kpi_display.py +0 -0
  43. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/cli/pod_client.py +0 -0
  44. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/cli/repl.py +0 -0
  45. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/cli/repl_helpers.py +0 -0
  46. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/cli/url_helpers.py +0 -0
  47. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/client.py +0 -0
  48. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/common/__init__.py +0 -0
  49. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/common/kf_base_client.py +0 -0
  50. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/common/kf_fast_text_client.py +0 -0
  51. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/common/kf_http_client.py +0 -0
  52. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/common/kf_logs_client.py +0 -0
  53. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/common/kf_markdown_media_client.py +0 -0
  54. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/common/kf_vectorsearch_client.py +0 -0
  55. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/common/mcp_interceptors.py +0 -0
  56. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/common/mcp_runtime.py +0 -0
  57. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/common/mcp_toolkit.py +0 -0
  58. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/common/mcp_utils.py +0 -0
  59. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/common/structures.py +0 -0
  60. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/common/token_expiry.py +0 -0
  61. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/common/tool_node_utils.py +0 -0
  62. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/deep/__init__.py +0 -0
  63. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/deep/deep_runtime.py +0 -0
  64. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/eval/__init__.py +0 -0
  65. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/eval/collector.py +0 -0
  66. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/graph/__init__.py +0 -0
  67. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/integrations/__init__.py +0 -0
  68. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/integrations/v2_runtime/__init__.py +0 -0
  69. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/model_routing/__init__.py +0 -0
  70. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/model_routing/catalog.py +0 -0
  71. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/model_routing/contracts.py +0 -0
  72. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/model_routing/provider.py +0 -0
  73. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/model_routing/resolver.py +0 -0
  74. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/react/__init__.py +0 -0
  75. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/react/react_model_adapter.py +0 -0
  76. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/react/react_prompting.py +0 -0
  77. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/react/react_tool_binding.py +0 -0
  78. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/react/react_tool_loop.py +0 -0
  79. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/react/react_tool_rendering.py +0 -0
  80. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/react/react_tool_utils.py +0 -0
  81. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/react/react_tracing.py +0 -0
  82. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/runtime_context.py +0 -0
  83. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/runtime_support/__init__.py +0 -0
  84. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/runtime_support/checkpoints.py +0 -0
  85. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/runtime_support/model_metadata.py +0 -0
  86. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/runtime_support/request_context_helpers.py +0 -0
  87. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/runtime_support/sql_checkpointer.py +0 -0
  88. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/runtime_support/user_token_refresher.py +0 -0
  89. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/support/__init__.py +0 -0
  90. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/support/filesystem_context.py +0 -0
  91. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime/support/tool_approval.py +0 -0
  92. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime.egg-info/dependency_links.txt +0 -0
  93. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime.egg-info/entry_points.txt +0 -0
  94. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/fred_runtime.egg-info/top_level.txt +0 -0
  95. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/setup.cfg +0 -0
  96. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/tests/test_client.py +0 -0
  97. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/tests/test_config_loader.py +0 -0
  98. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/tests/test_context.py +0 -0
  99. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/tests/test_conversational_memory.py +0 -0
  100. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/tests/test_eval_collector.py +0 -0
  101. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/tests/test_eval_trace.py +0 -0
  102. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/tests/test_graph_runtime_observability.py +0 -0
  103. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/tests/test_kpi_display.py +0 -0
  104. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/tests/test_mcp_config.py +0 -0
  105. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/tests/test_model_routing.py +0 -0
  106. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/tests/test_pod_client.py +0 -0
  107. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/tests/test_repl_helpers.py +0 -0
  108. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/tests/test_smoke.py +0 -0
  109. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/tests/test_token_expiry.py +0 -0
  110. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/tests/test_url_helpers.py +0 -0
  111. {fred_runtime-2.0.10 → fred_runtime-3.1.0}/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: 2.0.10
3
+ Version: 3.1.0
4
4
  Summary: Runtime adapters and infrastructure wiring for Fred v2 agents.
5
5
  Author-email: Thales <noreply@thalesgroup.com>
6
6
  License: Apache-2.0
@@ -12,8 +12,8 @@ Classifier: Programming Language :: Python :: 3 :: Only
12
12
  Classifier: Operating System :: OS Independent
13
13
  Requires-Python: <3.13,>=3.12
14
14
  Description-Content-Type: text/markdown
15
- Requires-Dist: fred-core>=2.0.5
16
- Requires-Dist: fred-sdk>=0.1.11
15
+ Requires-Dist: fred-core>=3.1.0
16
+ Requires-Dist: fred-sdk>=3.1.0
17
17
  Requires-Dist: alembic>=1.18.4
18
18
  Requires-Dist: deepagents>=0.4.11
19
19
  Requires-Dist: httpx>=0.28.1
@@ -22,7 +22,6 @@ from .agent_app import create_agent_app
22
22
  from .config import (
23
23
  AgentPodConfig,
24
24
  LangfuseObservabilityConfig,
25
- MetricsBackend,
26
25
  PodAIConfig,
27
26
  PodAppConfig,
28
27
  PodObservabilityConfig,
@@ -40,7 +39,6 @@ from .config_loader import (
40
39
  __all__ = [
41
40
  "AgentPodConfig",
42
41
  "LangfuseObservabilityConfig",
43
- "MetricsBackend",
44
42
  "PodAIConfig",
45
43
  "PodAppConfig",
46
44
  "PodObservabilityConfig",
@@ -57,6 +57,7 @@ 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
59
  from fred_core.history.history_schema import ChatMessage
60
+ from fred_core.kpi import KPIMiddleware
60
61
  from fred_core.kpi.kpi_writer_structures import KPIActor
61
62
  from fred_core.logs.log_setup import log_setup
62
63
  from fred_core.logs.memory_log_store import RamLogStore
@@ -110,10 +111,9 @@ from fred_runtime.runtime_support.checkpoints import load_checkpoint
110
111
  from ..common.structures import AgentSettingsLike
111
112
  from ..integrations.v2_runtime.adapters import (
112
113
  CompositeToolInvoker,
113
- FredArtifactPublisher,
114
114
  FredKnowledgeSearchToolInvoker,
115
115
  FredMcpToolProvider,
116
- FredResourceReader,
116
+ FredWorkspaceFs,
117
117
  KPIWriterMetricsAdapter,
118
118
  build_default_tracer,
119
119
  )
@@ -127,7 +127,11 @@ from ..runtime_support import refresh_user_access_token_from_keycloak
127
127
  from .config import AgentPodConfig
128
128
  from .container import build_pod_container
129
129
  from .context import AuditEventRecord, KpiTurnRecord, PodApplicationContext
130
- from .dependencies import attach_pod_container, get_pod_container
130
+ from .dependencies import (
131
+ attach_pod_container,
132
+ get_pod_container,
133
+ get_pod_container_from_app,
134
+ )
131
135
  from .observability_factory import bootstrap_observability
132
136
 
133
137
  logger = logging.getLogger(__name__)
@@ -563,6 +567,21 @@ class LocalRegistryAgentInvoker(AgentInvokerPort):
563
567
 
564
568
  context_dict = request.context.model_dump(mode="json")
565
569
  context_dict.setdefault("execution_action", ExecutionGrantAction.EXECUTE.value)
570
+ # RFC AGENT-INVOKE: apply the caller's per-call scope onto the callee's
571
+ # retrieval context. These keys are read back when the callee's
572
+ # RuntimeContext is built, so they narrow its document/library/search world.
573
+ # Scope narrows only; the callee still runs under the delegated identity.
574
+ if request.scope is not None:
575
+ if request.scope.document_uids is not None:
576
+ context_dict["selected_document_uids"] = list(
577
+ request.scope.document_uids
578
+ )
579
+ if request.scope.library_ids is not None:
580
+ context_dict["selected_document_libraries_ids"] = list(
581
+ request.scope.library_ids
582
+ )
583
+ if request.scope.search_policy is not None:
584
+ context_dict["search_policy"] = request.scope.search_policy
566
585
  execute_request = _AgentExecuteRequest.model_construct(
567
586
  agent_id=request.agent_id,
568
587
  agent_instance_id=None,
@@ -641,11 +660,7 @@ def _build_runtime_services(
641
660
  binding=binding,
642
661
  settings=settings,
643
662
  )
644
- artifact_publisher = FredArtifactPublisher(
645
- binding=binding,
646
- settings=settings,
647
- )
648
- resource_reader = FredResourceReader(
663
+ workspace_fs = FredWorkspaceFs(
649
664
  binding=binding,
650
665
  settings=settings,
651
666
  )
@@ -657,8 +672,7 @@ def _build_runtime_services(
657
672
  settings=settings,
658
673
  ports=AuthoredToolRuntimePorts(
659
674
  chat_model_factory=runtime_config.chat_model_factory,
660
- artifact_publisher=artifact_publisher,
661
- resource_reader=resource_reader,
675
+ workspace_fs=workspace_fs,
662
676
  fallback_tool_invoker=base_tool_invoker,
663
677
  media_fetcher=_build_media_fetcher(binding=binding, settings=settings),
664
678
  ),
@@ -688,8 +702,7 @@ def _build_runtime_services(
688
702
  chat_model_factory=runtime_config.chat_model_factory,
689
703
  tool_invoker=tool_invoker,
690
704
  tool_provider=tool_provider,
691
- artifact_publisher=artifact_publisher,
692
- resource_reader=resource_reader,
705
+ workspace_fs=workspace_fs,
693
706
  checkpointer=runtime_config.checkpointer,
694
707
  agent_invoker=agent_invoker,
695
708
  )
@@ -835,6 +848,7 @@ class _McpCatalogResponse(BaseModel):
835
848
  class _ResolvedAgentInstance(BaseModel):
836
849
  agent_instance_id: str
837
850
  template_agent_id: str
851
+ display_name: str = ""
838
852
  owner_scope: str
839
853
  owner_user_id: str | None = None
840
854
  owner_team_id: str | None = None
@@ -847,6 +861,7 @@ class _ResolvedExecutionTarget:
847
861
  definition: ReActAgentDefinition | GraphAgentDefinition
848
862
  effective_agent_id: str
849
863
  team_id: str | None = None
864
+ agent_instance_name: str | None = None
850
865
 
851
866
 
852
867
  def _apply_runtime_tuning(
@@ -1035,6 +1050,7 @@ async def _resolve_agent_instance(
1035
1050
  ),
1036
1051
  effective_agent_id=resolution.agent_instance_id,
1037
1052
  team_id=resolution.owner_team_id,
1053
+ agent_instance_name=resolution.display_name or None,
1038
1054
  )
1039
1055
 
1040
1056
 
@@ -1578,6 +1594,7 @@ def _emit_turn_completed(
1578
1594
  user_id: str,
1579
1595
  team_id: str | None,
1580
1596
  agent_instance_id: str | None,
1597
+ agent_instance_name: str | None,
1581
1598
  template_agent_id: str | None,
1582
1599
  payloads: list[dict[str, Any]],
1583
1600
  turn_start: float,
@@ -1611,6 +1628,8 @@ def _emit_turn_completed(
1611
1628
  prom_dims: dict[str, str | None] = {
1612
1629
  "team_id": team_id,
1613
1630
  "template_agent_id": template_agent_id,
1631
+ "agent_instance_id": agent_instance_id,
1632
+ "agent_instance_name": agent_instance_name,
1614
1633
  "runtime_id": runtime_id,
1615
1634
  "model_name": outcome.model_name,
1616
1635
  "finish_reason": outcome.finish_reason,
@@ -1627,7 +1646,7 @@ def _emit_turn_completed(
1627
1646
  "input_tokens": outcome.input_tokens,
1628
1647
  "output_tokens": outcome.output_tokens,
1629
1648
  },
1630
- actor=KPIActor(type="system"),
1649
+ actor=KPIActor(type="human", user_id=user_id),
1631
1650
  )
1632
1651
 
1633
1652
  if outcome.is_error:
@@ -1636,7 +1655,7 @@ def _emit_turn_completed(
1636
1655
  type="counter",
1637
1656
  value=1,
1638
1657
  dims=prom_dims,
1639
- actor=KPIActor(type="system"),
1658
+ actor=KPIActor(type="human", user_id=user_id),
1640
1659
  )
1641
1660
 
1642
1661
  # Append to container ring buffer (high-cardinality fields safe here).
@@ -1667,6 +1686,7 @@ async def _stream(
1667
1686
  access_token: str | None = None,
1668
1687
  *,
1669
1688
  team_id: str | None = None,
1689
+ agent_instance_name: str | None = None,
1670
1690
  registry: Mapping[str, ReActAgentDefinition | GraphAgentDefinition] | None = None,
1671
1691
  security_enabled: bool = False,
1672
1692
  container: PodApplicationContext,
@@ -1719,6 +1739,7 @@ async def _stream(
1719
1739
  user_id=user_id,
1720
1740
  team_id=resolved_team_id,
1721
1741
  agent_instance_id=request.agent_instance_id,
1742
+ agent_instance_name=agent_instance_name,
1722
1743
  template_agent_id=definition.agent_id,
1723
1744
  payloads=collected,
1724
1745
  turn_start=turn_start,
@@ -2612,6 +2633,7 @@ def _build_agent_router(
2612
2633
  user_id=user_id_str,
2613
2634
  team_id=target.team_id,
2614
2635
  agent_instance_id=request.agent_instance_id,
2636
+ agent_instance_name=target.agent_instance_name,
2615
2637
  template_agent_id=target.definition.agent_id,
2616
2638
  payloads=payloads,
2617
2639
  turn_start=turn_start,
@@ -2715,6 +2737,7 @@ def _build_agent_router(
2715
2737
  user_id=user_id_str,
2716
2738
  team_id=target.team_id,
2717
2739
  agent_instance_id=request.agent_instance_id,
2740
+ agent_instance_name=target.agent_instance_name,
2718
2741
  template_agent_id=target.definition.agent_id,
2719
2742
  payloads=payloads,
2720
2743
  turn_start=turn_start,
@@ -2825,6 +2848,7 @@ def _build_agent_router(
2825
2848
  internal_req,
2826
2849
  access_token=access_token,
2827
2850
  team_id=target.team_id,
2851
+ agent_instance_name=target.agent_instance_name,
2828
2852
  registry=registry,
2829
2853
  security_enabled=security_enabled,
2830
2854
  container=container,
@@ -2951,7 +2975,7 @@ def create_agent_app(
2951
2975
  "enabled" if security_enabled else "disabled",
2952
2976
  "sql" if container.get_checkpointer() is not None else "none",
2953
2977
  "sql" if container.get_history_store() is not None else "none",
2954
- config.observability.metrics.value,
2978
+ "prometheus" if config.observability.kpi.prometheus.enabled else "logging",
2955
2979
  list(registry.keys()),
2956
2980
  )
2957
2981
  yield
@@ -2977,6 +3001,13 @@ def create_agent_app(
2977
3001
  )
2978
3002
  logger.debug("[fred-runtime] CORS allow_origins=%s", authorized_origins)
2979
3003
 
3004
+ # KPI middleware — writer is lazily resolved from app.state because the
3005
+ # container (and its KPI writer) is only initialised during lifespan startup.
3006
+ app.add_middleware(
3007
+ KPIMiddleware,
3008
+ kpi=lambda: get_pod_container_from_app(app).get_kpi_writer(),
3009
+ )
3010
+
2980
3011
  api_router = APIRouter(prefix=base_url)
2981
3012
  api_router.include_router(
2982
3013
  _build_agent_router(registry, security_enabled=security_enabled)
@@ -70,6 +70,7 @@ if TYPE_CHECKING:
70
70
  from fred_runtime.runtime_context import McpConfigurationLike
71
71
 
72
72
  from fred_core.common import (
73
+ KpiObservabilityConfig,
73
74
  OpenSearchStoreConfig,
74
75
  PostgresStoreConfig,
75
76
  TemporalSchedulerConfig,
@@ -120,26 +121,6 @@ class PodAppConfig(BaseModel):
120
121
  ),
121
122
  )
122
123
  gcu_version: str | None = None
123
- metrics_address: str = "127.0.0.1"
124
- metrics_port: int = 9000
125
- kpi_process_metrics_interval_sec: int = Field(
126
- default=0,
127
- description=(
128
- "Emit process and SQL pool KPIs every N seconds. Set 0 to disable "
129
- "the background emitters."
130
- ),
131
- )
132
- kpi_log_summary_interval_sec: float = Field(
133
- default=0.0,
134
- description=(
135
- "Emit periodic KPI summary logs every N seconds for local benches. "
136
- "Set 0 to disable."
137
- ),
138
- )
139
- kpi_log_summary_top_n: int = Field(
140
- default=0,
141
- description="Top-N KPI summary rows to log. 0 means all / disabled.",
142
- )
143
124
  openai_compat: bool = True
144
125
  """
145
126
  Enable the OpenAI-compatible /v1/chat/completions and /v1/models endpoints.
@@ -209,21 +190,6 @@ class TracerBackend(str, Enum):
209
190
  langfuse = "langfuse"
210
191
 
211
192
 
212
- class MetricsBackend(str, Enum):
213
- """
214
- Metrics emission backend for the agent pod.
215
-
216
- - null — no metrics, all timer events are dropped
217
- - logging — each timer is emitted as a structured log entry (default)
218
- - prometheus — KPI/process metrics are exported in Prometheus format on the
219
- dedicated metrics port configured under `app`
220
- """
221
-
222
- null = "null"
223
- logging = "logging"
224
- prometheus = "prometheus"
225
-
226
-
227
193
  class LangfuseObservabilityConfig(BaseModel):
228
194
  """
229
195
  Langfuse connection settings.
@@ -245,14 +211,20 @@ class PodObservabilityConfig(BaseModel):
245
211
  Example:
246
212
  observability:
247
213
  tracer: logging # null | logging | langfuse
248
- metrics: logging # null | logging | prometheus
214
+ kpi:
215
+ log:
216
+ enabled: true
217
+ prometheus:
218
+ enabled: false
219
+ opensearch:
220
+ enabled: false
249
221
  langfuse:
250
222
  host: "http://localhost:3001"
251
223
  # LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY in .env
252
224
  """
253
225
 
254
226
  tracer: TracerBackend = TracerBackend.logging
255
- metrics: MetricsBackend = MetricsBackend.logging
227
+ kpi: KpiObservabilityConfig = Field(default_factory=KpiObservabilityConfig)
256
228
  langfuse: LangfuseObservabilityConfig = Field(
257
229
  default_factory=LangfuseObservabilityConfig
258
230
  )
@@ -24,16 +24,14 @@ from datetime import datetime, timezone
24
24
  from typing import TYPE_CHECKING, TypedDict
25
25
 
26
26
  from fred_core.kpi.base_kpi_writer import BaseKPIWriter
27
+ from fred_core.kpi.kpi_factory import build_kpi_writer
27
28
  from fred_core.kpi.kpi_process import emit_process_kpis, emit_sql_pool_kpis
28
- from fred_core.kpi.kpi_writer import KPIDefaults, KPIWriter
29
- from fred_core.kpi.log_kpi_store import KpiLogStore
30
29
  from fred_core.kpi.noop_kpi_writer import NoOpKPIWriter
31
- from fred_core.kpi.prometheus_kpi_store import PrometheusKPIStore
32
30
  from fred_sdk.contracts.runtime import HistoryStorePort
33
31
  from prometheus_client import start_http_server
34
32
  from sqlalchemy.ext.asyncio import AsyncEngine
35
33
 
36
- from fred_runtime.app.config import AgentPodConfig, MetricsBackend
34
+ from fred_runtime.app.config import AgentPodConfig
37
35
 
38
36
  if TYPE_CHECKING:
39
37
  pass
@@ -117,18 +115,11 @@ class PodApplicationContext:
117
115
  def initialize_kpi_writer(self) -> None:
118
116
  """Build the KPI writer from pod observability config."""
119
117
  config = self.configuration
120
- backend = config.observability.metrics
121
- if backend == MetricsBackend.null:
122
- self._kpi_writer = NoOpKPIWriter()
123
- return
124
- store = KpiLogStore(level=config.app.log_level)
125
- if backend == MetricsBackend.prometheus:
126
- store = PrometheusKPIStore(delegate=store) # type: ignore[arg-type]
127
- self._kpi_writer = KPIWriter(
128
- store=store,
129
- defaults=KPIDefaults(static_dims={"service": "fred-runtime"}),
130
- summary_interval_s=config.app.kpi_log_summary_interval_sec,
131
- summary_top_n=config.app.kpi_log_summary_top_n,
118
+ self._kpi_writer = build_kpi_writer(
119
+ kpi_config=config.observability.kpi,
120
+ opensearch_config=config.storage.opensearch,
121
+ service_name="fred-runtime",
122
+ log_level=config.app.log_level,
132
123
  )
133
124
 
134
125
  async def initialize_sql(self) -> None:
@@ -160,25 +151,23 @@ class PodApplicationContext:
160
151
 
161
152
  def start_metrics_exporter(self) -> None:
162
153
  """Start the Prometheus scrape endpoint when configured."""
163
- config = self.configuration
164
- if config.observability.metrics != MetricsBackend.prometheus:
154
+ prom_cfg = self.configuration.observability.kpi.prometheus
155
+ if not prom_cfg.enabled:
165
156
  return
166
- result = start_http_server(
167
- config.app.metrics_port,
168
- addr=config.app.metrics_address,
169
- )
157
+ result = start_http_server(prom_cfg.port, addr=prom_cfg.address)
170
158
  self._metrics_exporter = result if isinstance(result, tuple) else None
171
159
  logger.info(
172
160
  "[fred-runtime] Prometheus metrics exporter ready at %s:%s",
173
- config.app.metrics_address,
174
- config.app.metrics_port,
161
+ prom_cfg.address,
162
+ prom_cfg.port,
175
163
  )
176
164
 
177
165
  async def start_kpi_tasks(self) -> None:
178
166
  """Start background KPI flush tasks (process + SQL pool health)."""
179
167
  kpi_writer = self.get_kpi_writer()
180
- config = self.configuration
181
- interval_s = float(config.app.kpi_process_metrics_interval_sec)
168
+ interval_s = float(
169
+ self.configuration.observability.kpi.process_metrics_interval_sec
170
+ )
182
171
  if interval_s <= 0 or isinstance(kpi_writer, NoOpKPIWriter):
183
172
  return
184
173
  tasks: list[asyncio.Task[None]] = [
@@ -51,7 +51,7 @@ from fred_core.portable import (
51
51
  set_tracer,
52
52
  )
53
53
 
54
- from .config import MetricsBackend, PodObservabilityConfig, TracerBackend
54
+ from .config import PodObservabilityConfig, TracerBackend
55
55
 
56
56
  logger = logging.getLogger(__name__)
57
57
 
@@ -83,9 +83,9 @@ def bootstrap_observability(
83
83
  set_tracer(tracer)
84
84
  set_metrics_provider(metrics)
85
85
  logger.info(
86
- "[fred-runtime] observability ready — tracer=%s metrics=%s",
86
+ "[fred-runtime] observability ready — tracer=%s prometheus=%s",
87
87
  config.tracer.value,
88
- config.metrics.value,
88
+ config.kpi.prometheus.enabled,
89
89
  )
90
90
 
91
91
 
@@ -160,15 +160,10 @@ def _build_metrics(
160
160
  *,
161
161
  kpi_writer: BaseKPIWriter | None = None,
162
162
  ) -> MetricsProvider:
163
- if config.metrics == MetricsBackend.null:
164
- return MetricsProvider()
165
- if config.metrics == MetricsBackend.logging:
166
- return LoggingMetricsProvider()
167
- if config.metrics == MetricsBackend.prometheus:
163
+ if config.kpi.prometheus.enabled:
168
164
  if kpi_writer is None:
169
165
  logger.warning(
170
- "[fred-runtime] metrics=prometheus selected without a KPI writer"
171
- " — falling back to logging"
166
+ "[fred-runtime] prometheus enabled without a KPI writer — falling back to logging"
172
167
  )
173
168
  return LoggingMetricsProvider()
174
169
  from fred_runtime.integrations.v2_runtime.adapters import (
@@ -176,8 +171,4 @@ def _build_metrics(
176
171
  )
177
172
 
178
173
  return cast(MetricsProvider, KPIWriterMetricsAdapter(kpi_writer))
179
- logger.warning(
180
- "[fred-runtime] Unknown metrics backend '%s' — falling back to logging",
181
- config.metrics,
182
- )
183
174
  return LoggingMetricsProvider()
@@ -163,23 +163,57 @@ class ContextAwareTool(BaseTool):
163
163
 
164
164
  tool_properties = self._get_tool_properties()
165
165
 
166
- library_ids = get_document_library_tags_ids(context)
167
- if library_ids and "document_library_tags_ids" in tool_properties:
168
- kwargs["document_library_tags_ids"] = library_ids
169
- logger.info(
170
- "ContextAwareTool(%s) injecting library filter: %s",
171
- self.name,
172
- library_ids,
173
- )
166
+ # Snapshot what the agent (or LLM) explicitly requested, so every decision
167
+ # below can be logged as "requested -> applied" with no ambiguity. The
168
+ # picker selection (RuntimeContext) is a default scope an agent may NARROW;
169
+ # an explicit per-call scope is respected, never silently overwritten.
170
+ caller_document_uids = kwargs.get("document_uids")
171
+ caller_library_ids = kwargs.get("document_library_tags_ids")
172
+ picker_document_uids = get_document_uids(context)
173
+ picker_library_ids = get_document_library_tags_ids(context)
174
+
175
+ # Document scope is the most specific selector. If the agent passed it,
176
+ # keep it verbatim; otherwise fill from the picker selection.
177
+ if "document_uids" in tool_properties:
178
+ if caller_document_uids:
179
+ logger.info(
180
+ "ContextAwareTool(%s) document_uids: agent-scoped=%s (picker=%s NOT applied)",
181
+ self.name,
182
+ caller_document_uids,
183
+ picker_document_uids,
184
+ )
185
+ elif picker_document_uids:
186
+ kwargs["document_uids"] = picker_document_uids
187
+ logger.info(
188
+ "ContextAwareTool(%s) document_uids: applied picker selection=%s",
189
+ self.name,
190
+ picker_document_uids,
191
+ )
174
192
 
175
- document_uids = get_document_uids(context)
176
- if document_uids and "document_uids" in tool_properties:
177
- kwargs["document_uids"] = document_uids
178
- logger.info(
179
- "ContextAwareTool(%s) injecting document filter: %s",
180
- self.name,
181
- document_uids,
182
- )
193
+ # Library scope is broader than document scope. Inject the picker libraries
194
+ # only when the agent did not scope by library AND did not scope by document
195
+ # (a per-call document scope must not be widened back to whole libraries).
196
+ if "document_library_tags_ids" in tool_properties:
197
+ if caller_library_ids:
198
+ logger.info(
199
+ "ContextAwareTool(%s) library: agent-scoped=%s (picker=%s NOT applied)",
200
+ self.name,
201
+ caller_library_ids,
202
+ picker_library_ids,
203
+ )
204
+ elif caller_document_uids:
205
+ logger.info(
206
+ "ContextAwareTool(%s) library: NOT injected — agent scoped by document_uids=%s",
207
+ self.name,
208
+ caller_document_uids,
209
+ )
210
+ elif picker_library_ids:
211
+ kwargs["document_library_tags_ids"] = picker_library_ids
212
+ logger.info(
213
+ "ContextAwareTool(%s) library: applied picker selection=%s",
214
+ self.name,
215
+ picker_library_ids,
216
+ )
183
217
 
184
218
  session_id = context.session_id
185
219
  if (