fred-runtime 3.3.6__tar.gz → 3.4.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.
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/PKG-INFO +1 -1
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/__main__.py +0 -1
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/app/agent_app.py +307 -78
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/app/context.py +34 -6
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/app/openai_compat_router.py +4 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/capabilities/__init__.py +2 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/capabilities/assembly.py +79 -13
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/capabilities/demo.py +28 -31
- fred_runtime-3.4.0/fred_runtime/capabilities/document_access/capability.py +886 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/capabilities/errors.py +16 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/capabilities/mcp.py +12 -2
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/capabilities/registry.py +61 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/common/context_aware_tool.py +6 -2
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/common/kf_base_client.py +20 -1
- fred_runtime-3.4.0/fred_runtime/common/kf_document_client.py +213 -0
- fred_runtime-3.4.0/fred_runtime/common/kf_tag_client.py +186 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/graph/graph_runtime.py +214 -7
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/integrations/v2_runtime/adapters.py +257 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/model_routing/__init__.py +4 -0
- fred_runtime-3.4.0/fred_runtime/model_routing/authz.py +104 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/model_routing/contracts.py +40 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/model_routing/provider.py +68 -3
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/model_routing/resolver.py +54 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/middleware/tool_observability.py +68 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/react_prompting.py +49 -6
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/react_runtime.py +11 -7
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/react_tool_loop.py +1 -1
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/runtime_context.py +15 -1
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/runtime_support/sql_checkpointer.py +22 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/support/tool_loop.py +39 -3
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime.egg-info/PKG-INFO +1 -1
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime.egg-info/SOURCES.txt +8 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/pyproject.toml +1 -1
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_agent_app.py +880 -18
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_capability_agent_1973.py +212 -18
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_capability_chat_parts_1977.py +5 -2
- fred_runtime-3.4.0/tests/test_capability_document_access_1906.py +1050 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_capability_endpoints_1974.py +122 -1
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_capability_registry_1973.py +108 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_capability_selection_1974.py +4 -0
- fred_runtime-3.4.0/tests/test_capability_tool_return_convention.py +213 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_context.py +75 -0
- fred_runtime-3.4.0/tests/test_graph_capability_bridge.py +651 -0
- fred_runtime-3.4.0/tests/test_model_enforcement.py +385 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_model_routing.py +113 -1
- fred_runtime-3.4.0/tests/test_models_catalog_projection.py +139 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_react_loop_regressions_1972.py +22 -13
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_react_prompting.py +23 -3
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_sql_checkpointer_owner.py +34 -0
- fred_runtime-3.4.0/tests/test_tool_loop_trim.py +105 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_tool_observability_middleware.py +179 -0
- fred_runtime-3.3.6/fred_runtime/capabilities/document_access/capability.py +0 -395
- fred_runtime-3.3.6/tests/test_capability_document_access_1906.py +0 -515
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/README.md +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/__init__.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/app/__init__.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/app/_catalogs.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/app/config.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/app/config_loader.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/app/container.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/app/dependencies.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/app/mcp_config.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/app/observability_factory.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/capabilities/assets.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/capabilities/demo_migrations/env.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/capabilities/demo_migrations/versions/d1e2m0a0b0c0_create_demo_echo_notes.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/capabilities/document_access/__init__.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/capabilities/openapi_dump.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/cli/__init__.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/cli/completion.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/cli/entrypoint.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/cli/history_display.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/cli/kpi_display.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/cli/pod_client.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/cli/repl.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/cli/repl_helpers.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/cli/url_helpers.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/client.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/common/__init__.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/common/kf_fast_text_client.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/common/kf_http_client.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/common/kf_markdown_media_client.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/common/kf_vectorsearch_client.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/common/kf_workspace_client.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/common/mcp_interceptors.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/common/mcp_runtime.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/common/mcp_toolkit.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/common/mcp_utils.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/common/structures.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/common/token_expiry.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/common/tool_node_utils.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/deep/__init__.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/deep/deep_runtime.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/eval/__init__.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/eval/collector.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/graph/__init__.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/integrations/__init__.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/integrations/inprocess_toolkit_registry.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/integrations/kf_vector_search/__init__.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/integrations/kf_vector_search/toolkit.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/integrations/v2_runtime/__init__.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/migrations.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/model_routing/catalog.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/__init__.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/middleware/__init__.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/middleware/checkpoint_hygiene.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/middleware/dynamic_prompt.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/middleware/frame.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/middleware/hitl.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/middleware/model_routing.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/middleware/shared.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/middleware/tracing_kpi.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/react_langchain_adapter.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/react_message_codec.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/react_model_adapter.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/react_stream_adapter.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/react_tool_binding.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/react_tool_rendering.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/react_tool_resolution.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/react_tool_utils.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/react_tracing.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/runtime_support/__init__.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/runtime_support/checkpoints.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/runtime_support/model_metadata.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/runtime_support/request_context_helpers.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/runtime_support/user_token_refresher.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/support/__init__.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/support/filesystem_context.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/support/thinking.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime.egg-info/dependency_links.txt +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime.egg-info/entry_points.txt +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime.egg-info/requires.txt +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime.egg-info/top_level.txt +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/setup.cfg +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_capability_chat_controls_1976.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_capability_routes_tables_1979.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_client.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_config_loader.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_context_aware_tool.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_conversational_memory.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_deep_agent_middleware.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_eval_collector.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_eval_trace.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_fred_workspace_fs.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_graph_runtime_invoke_agent.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_graph_runtime_observability.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_history.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_kf_vector_search_toolkit.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_kf_workspace_client.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_knowledge_search_tool_invoker_sources.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_kpi_display.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_mcp_config.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_openai_compat_router.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_pod_client.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_react_middleware_frame.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_react_thinking.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_repl_helpers.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_runtime_context_prompt_injection.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_smoke.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_spike_capability_state_1971.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_token_expiry.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_tracing_kpi_content_confinement.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_url_helpers.py +0 -0
- {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_user_token_refresher.py +0 -0
|
@@ -56,9 +56,9 @@ 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.common.team_id import is_personal_team_id, personal_team_id
|
|
60
59
|
from fred_core.history.history_schema import ChatMessage
|
|
61
60
|
from fred_core.kpi import KPIMiddleware
|
|
61
|
+
from fred_core.kpi.kpi_runtime_stage_metric import runtime_stage_timer
|
|
62
62
|
from fred_core.kpi.kpi_writer_structures import KPIActor
|
|
63
63
|
from fred_core.logs.audit_log import emit_audit_log
|
|
64
64
|
from fred_core.logs.log_setup import log_setup
|
|
@@ -89,6 +89,7 @@ from fred_sdk.contracts.context import (
|
|
|
89
89
|
PortableContext,
|
|
90
90
|
PortableEnvironment,
|
|
91
91
|
RuntimeContext,
|
|
92
|
+
TeamOperationRouteRule,
|
|
92
93
|
)
|
|
93
94
|
from fred_sdk.contracts.eval import EvalStep, EvalTrace
|
|
94
95
|
from fred_sdk.contracts.execution import (
|
|
@@ -146,8 +147,13 @@ from fred_runtime.runtime_support.checkpoints import load_checkpoint
|
|
|
146
147
|
from ..common.structures import AgentSettingsLike
|
|
147
148
|
from ..integrations.inprocess_toolkit_registry import build_inprocess_toolkit
|
|
148
149
|
from ..integrations.v2_runtime.adapters import (
|
|
150
|
+
AgentConfigAssetsAdapter,
|
|
149
151
|
CompositeToolInvoker,
|
|
152
|
+
DocumentContentAdapter,
|
|
153
|
+
DocumentFolderAdapter,
|
|
150
154
|
DocumentSearchAdapter,
|
|
155
|
+
DocumentSummarizeAdapter,
|
|
156
|
+
DocumentTreeAdapter,
|
|
151
157
|
FredKnowledgeSearchToolInvoker,
|
|
152
158
|
FredMcpToolProvider,
|
|
153
159
|
FredWorkspaceFs,
|
|
@@ -734,6 +740,16 @@ def _build_runtime_services(
|
|
|
734
740
|
binding=binding,
|
|
735
741
|
settings=settings,
|
|
736
742
|
)
|
|
743
|
+
# Companion document-access ports: tree listing + on-demand summarization,
|
|
744
|
+
# same private-binding doctrine as the search adapter.
|
|
745
|
+
document_tree = DocumentTreeAdapter(
|
|
746
|
+
binding=binding,
|
|
747
|
+
settings=settings,
|
|
748
|
+
)
|
|
749
|
+
document_summarize = DocumentSummarizeAdapter(
|
|
750
|
+
binding=binding,
|
|
751
|
+
settings=settings,
|
|
752
|
+
)
|
|
737
753
|
tool_provider = FredMcpToolProvider(
|
|
738
754
|
binding=binding,
|
|
739
755
|
settings=settings,
|
|
@@ -784,6 +800,14 @@ def _build_runtime_services(
|
|
|
784
800
|
checkpointer=runtime_config.checkpointer,
|
|
785
801
|
agent_invoker=agent_invoker,
|
|
786
802
|
document_search=document_search,
|
|
803
|
+
# #1903 capability ports: per-instance config assets (template fetch at
|
|
804
|
+
# tool time), image-document raw fetch, and folder listing. Same
|
|
805
|
+
# private-binding doctrine as document_search.
|
|
806
|
+
agent_assets=AgentConfigAssetsAdapter(binding=binding, settings=settings),
|
|
807
|
+
document_content=DocumentContentAdapter(binding=binding, settings=settings),
|
|
808
|
+
document_folders=DocumentFolderAdapter(binding=binding, settings=settings),
|
|
809
|
+
document_tree=document_tree,
|
|
810
|
+
document_summarize=document_summarize,
|
|
787
811
|
)
|
|
788
812
|
|
|
789
813
|
|
|
@@ -945,6 +969,66 @@ class _McpCatalogResponse(BaseModel):
|
|
|
945
969
|
servers: list[_McpCatalogEntry]
|
|
946
970
|
|
|
947
971
|
|
|
972
|
+
class _ModelCatalogEntry(BaseModel):
|
|
973
|
+
id: str
|
|
974
|
+
provider: str
|
|
975
|
+
name: str
|
|
976
|
+
description: str | None = None
|
|
977
|
+
profile_ids: list[str] = Field(default_factory=list)
|
|
978
|
+
"""Every `models_catalog.yaml` profile_id sharing this entry's
|
|
979
|
+
(provider, name) — TEAM-ROUTING-POLICY-RFC.md §7.1's id-space
|
|
980
|
+
translation: a routing policy picks by profile_id (finer-grained than
|
|
981
|
+
this capability id), so control-plane needs a way back from profile_id
|
|
982
|
+
to capability id for write-time enablement validation, and the
|
|
983
|
+
team-settings picker needs the reverse (which profile_ids this enabled
|
|
984
|
+
capability actually offers). Declaration order in the source YAML,
|
|
985
|
+
first-seen first — same order `default_profile_by_capability` favors."""
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
class _ModelCatalogResponse(BaseModel):
|
|
989
|
+
models: list[_ModelCatalogEntry]
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
def _project_model_catalog_entries(catalog: Any) -> list[_ModelCatalogEntry]:
|
|
993
|
+
"""One `_ModelCatalogEntry` per distinct (provider, name) pair in a
|
|
994
|
+
loaded `ModelCatalog` (OBSERV-02 v3, `AGENT-CAPABILITY-RFC.md` §8.7).
|
|
995
|
+
|
|
996
|
+
Pulled out of `get_models_catalog` as a pure function so it is directly
|
|
997
|
+
unit-testable without a running app (`agent_app.py` has no existing
|
|
998
|
+
FastAPI TestClient harness — building one just for this route would be
|
|
999
|
+
disproportionate; this function is where the actual logic worth testing
|
|
1000
|
+
lives). `catalog` is typed `Any` here to avoid a module-level
|
|
1001
|
+
`..model_routing` import (see the route's own lazy import for why);
|
|
1002
|
+
callers pass a real `ModelCatalog`.
|
|
1003
|
+
"""
|
|
1004
|
+
from fred_sdk.contracts.capability.manifest import model_capability_id
|
|
1005
|
+
|
|
1006
|
+
seen: dict[tuple[str, str], _ModelCatalogEntry] = {}
|
|
1007
|
+
for profile in catalog.profiles:
|
|
1008
|
+
provider = profile.model.provider
|
|
1009
|
+
name = profile.model.name
|
|
1010
|
+
# ModelProfile.validate_model (contracts.py) already guarantees both
|
|
1011
|
+
# are non-empty strings for every profile that exists — asserted
|
|
1012
|
+
# (not a skip/continue) so this narrows the type instead of hiding
|
|
1013
|
+
# an invariant violation the validator itself is supposed to prevent.
|
|
1014
|
+
assert provider and name, (
|
|
1015
|
+
f"ModelProfile {profile.profile_id!r} passed validation without provider/name"
|
|
1016
|
+
)
|
|
1017
|
+
key = (provider, name)
|
|
1018
|
+
existing = seen.get(key)
|
|
1019
|
+
if existing is None:
|
|
1020
|
+
seen[key] = _ModelCatalogEntry(
|
|
1021
|
+
id=model_capability_id(provider, name),
|
|
1022
|
+
provider=provider,
|
|
1023
|
+
name=name,
|
|
1024
|
+
description=profile.description,
|
|
1025
|
+
profile_ids=[profile.profile_id],
|
|
1026
|
+
)
|
|
1027
|
+
else:
|
|
1028
|
+
existing.profile_ids.append(profile.profile_id)
|
|
1029
|
+
return list(seen.values())
|
|
1030
|
+
|
|
1031
|
+
|
|
948
1032
|
class _ResolvedAgentInstance(BaseModel):
|
|
949
1033
|
agent_instance_id: str
|
|
950
1034
|
template_agent_id: str
|
|
@@ -1098,7 +1182,9 @@ async def _resolve_agent_instance(
|
|
|
1098
1182
|
registry: Mapping[str, ReActAgentDefinition | GraphAgentDefinition],
|
|
1099
1183
|
access_token: str | None,
|
|
1100
1184
|
control_plane_url: str | None,
|
|
1185
|
+
http_client: httpx.AsyncClient,
|
|
1101
1186
|
team_id: str | None = None,
|
|
1187
|
+
request_id: str | None = None,
|
|
1102
1188
|
) -> _ResolvedExecutionTarget:
|
|
1103
1189
|
"""
|
|
1104
1190
|
Resolve a direct or managed execution target into a concrete definition.
|
|
@@ -1173,9 +1259,17 @@ async def _resolve_agent_instance(
|
|
|
1173
1259
|
f"{control_plane_url.rstrip('/')}/teams/{team_id}/agent-instances/"
|
|
1174
1260
|
f"{request.agent_instance_id}/runtime"
|
|
1175
1261
|
)
|
|
1176
|
-
headers
|
|
1177
|
-
|
|
1178
|
-
|
|
1262
|
+
headers: dict[str, str] = {}
|
|
1263
|
+
if access_token:
|
|
1264
|
+
headers["Authorization"] = f"Bearer {access_token}"
|
|
1265
|
+
if request_id:
|
|
1266
|
+
# Correlates this pod-side call with the control-plane request it
|
|
1267
|
+
# triggers — both sides stamp the same id as `trace.trace_id` on their
|
|
1268
|
+
# `runtime.stage_latency_ms{runtime_stage=runtime_binding*}` event
|
|
1269
|
+
# (TURN-01 evidence gap: no way to join a turn to its binding call
|
|
1270
|
+
# besides timestamps).
|
|
1271
|
+
headers["X-Request-Id"] = request_id
|
|
1272
|
+
response = await http_client.get(url, headers=headers or None)
|
|
1179
1273
|
if response.status_code == status.HTTP_404_NOT_FOUND:
|
|
1180
1274
|
raise HTTPException(
|
|
1181
1275
|
status_code=404, detail=response.text or "Unknown agent instance."
|
|
@@ -1260,12 +1354,13 @@ async def _authorize_execution_or_raise(
|
|
|
1260
1354
|
- security disabled (no authenticated user) → skip (dev/local).
|
|
1261
1355
|
- ReBAC engine absent or disabled (Noop) → skip (identity-only dev posture);
|
|
1262
1356
|
the C3 profile guarantees an enabled engine in classified deployments.
|
|
1263
|
-
- a personal space (`personal-<uid>`) is
|
|
1264
|
-
|
|
1265
|
-
`
|
|
1266
|
-
|
|
1267
|
-
`
|
|
1268
|
-
|
|
1357
|
+
- a personal space (`personal-<uid>`) is a real ReBAC team object (AUTHZ-08):
|
|
1358
|
+
the owner's `team_editor` tuple self-heals on first touch inside
|
|
1359
|
+
`RebacEngine.check_user_team_permission_or_raise`, and `add_relation`'s
|
|
1360
|
+
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.
|
|
1269
1364
|
- otherwise require the caller to hold `CAN_READ` on the requested team — the
|
|
1270
1365
|
same relation the control-plane required before it would mint a grant. The
|
|
1271
1366
|
team is caller-supplied but safe: OpenFGA only authorizes teams the user
|
|
@@ -1341,36 +1436,14 @@ async def _authorize_execution_or_raise(
|
|
|
1341
1436
|
agent_instance_id=request.agent_instance_id,
|
|
1342
1437
|
)
|
|
1343
1438
|
return
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
if team_id == personal_team_id(authenticated_user.uid):
|
|
1353
|
-
_emit_audit_event(
|
|
1354
|
-
container,
|
|
1355
|
-
"info",
|
|
1356
|
-
"personal_space_owner_authorized",
|
|
1357
|
-
user_id=authenticated_user.uid,
|
|
1358
|
-
team_id=team_id,
|
|
1359
|
-
agent_instance_id=request.agent_instance_id,
|
|
1360
|
-
)
|
|
1361
|
-
return
|
|
1362
|
-
_emit_audit_event(
|
|
1363
|
-
container,
|
|
1364
|
-
"warning",
|
|
1365
|
-
"personal_space_denied",
|
|
1366
|
-
user_id=authenticated_user.uid,
|
|
1367
|
-
team_id=team_id,
|
|
1368
|
-
agent_instance_id=request.agent_instance_id,
|
|
1369
|
-
)
|
|
1370
|
-
raise HTTPException(
|
|
1371
|
-
status_code=status.HTTP_403_FORBIDDEN,
|
|
1372
|
-
detail=f"user {authenticated_user.uid!r} is not authorized for team {team_id!r}",
|
|
1373
|
-
)
|
|
1439
|
+
# Personal spaces (AUTHZ-08, supersedes AUTHZ-05 item 8b) are now real
|
|
1440
|
+
# ReBAC team objects: `RebacEngine.check_user_team_permission_or_raise`
|
|
1441
|
+
# self-heals the owner's own `team_editor` tuple on first touch and
|
|
1442
|
+
# `add_relation`'s write-guard refuses any other shape naming a personal
|
|
1443
|
+
# team, so the bare `check_user_team_permission_or_raise` call below
|
|
1444
|
+
# already authorizes the owner and denies everyone else — no special-
|
|
1445
|
+
# casing needed here (the bare "personal" alias also denies normally,
|
|
1446
|
+
# since no tuple is ever provisioned for that literal string).
|
|
1374
1447
|
try:
|
|
1375
1448
|
await rebac.check_user_team_permission_or_raise(
|
|
1376
1449
|
authenticated_user, TeamPermission.CAN_READ, team_id
|
|
@@ -1506,15 +1579,34 @@ async def _authorize_and_resolve(
|
|
|
1506
1579
|
)
|
|
1507
1580
|
await _validate_session_checkpoint_access(request)
|
|
1508
1581
|
await _enforce_session_ownership(request, authenticated_user, container)
|
|
1509
|
-
|
|
1582
|
+
async with runtime_stage_timer(container.get_kpi_writer(), "pod_authz"):
|
|
1583
|
+
await _authorize_execution_or_raise(request, authenticated_user, container)
|
|
1510
1584
|
internal_req = _to_internal_request(request)
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
)
|
|
1585
|
+
# Stamp the trusted service-agent verdict (never the caller-supplied
|
|
1586
|
+
# context) so per-tool-call re-authorization can mirror the bypass
|
|
1587
|
+
# `_authorize_execution_or_raise` already granted above (RFC EVAL-AUTH,
|
|
1588
|
+
# Solution A) instead of re-running a ReBAC check this identity was
|
|
1589
|
+
# never meant to satisfy. Overwritten unconditionally, both ways, so a
|
|
1590
|
+
# caller can't spoof it via a body-supplied `context.is_service_agent`.
|
|
1591
|
+
ctx = dict(internal_req.context or {})
|
|
1592
|
+
if authenticated_user is not None and is_service_agent(authenticated_user):
|
|
1593
|
+
ctx["is_service_agent"] = "true"
|
|
1594
|
+
else:
|
|
1595
|
+
ctx.pop("is_service_agent", None)
|
|
1596
|
+
internal_req.context = ctx
|
|
1597
|
+
binding_request_id = str(uuid4())
|
|
1598
|
+
async with runtime_stage_timer(
|
|
1599
|
+
container.get_kpi_writer(), "runtime_binding", trace_id=binding_request_id
|
|
1600
|
+
):
|
|
1601
|
+
target = await _resolve_agent_instance(
|
|
1602
|
+
request=internal_req,
|
|
1603
|
+
registry=registry,
|
|
1604
|
+
access_token=access_token,
|
|
1605
|
+
control_plane_url=get_runtime_context().config.control_plane_url,
|
|
1606
|
+
http_client=container.get_control_plane_http_client(),
|
|
1607
|
+
team_id=request.effective_team_id(),
|
|
1608
|
+
request_id=binding_request_id,
|
|
1609
|
+
)
|
|
1518
1610
|
_validate_resolved_team(request, target.team_id, container)
|
|
1519
1611
|
return internal_req, target
|
|
1520
1612
|
|
|
@@ -1757,6 +1849,7 @@ async def _write_turn_history(
|
|
|
1757
1849
|
call_id=payload["call_id"],
|
|
1758
1850
|
content=payload.get("content", ""),
|
|
1759
1851
|
ok=not payload.get("is_error", False),
|
|
1852
|
+
latency_ms=payload.get("latency_ms"),
|
|
1760
1853
|
)
|
|
1761
1854
|
)
|
|
1762
1855
|
rank += 1
|
|
@@ -2163,6 +2256,7 @@ def _build_capability_save_services(
|
|
|
2163
2256
|
user_id: str,
|
|
2164
2257
|
team_id: str | None,
|
|
2165
2258
|
access_token: str | None,
|
|
2259
|
+
agent_instance_id: str | None = None,
|
|
2166
2260
|
) -> RuntimeServices:
|
|
2167
2261
|
"""
|
|
2168
2262
|
Minimal `RuntimeServices` for one capability save-time validation (#1974).
|
|
@@ -2170,8 +2264,11 @@ def _build_capability_save_services(
|
|
|
2170
2264
|
Why this exists:
|
|
2171
2265
|
- `validate_config` may store uploaded asset binaries through the KF-backed
|
|
2172
2266
|
workspace port and keep only the storage keys in the stored config
|
|
2173
|
-
(RFC §3.4, §3.8) — so the save path needs `workspace_fs
|
|
2174
|
-
|
|
2267
|
+
(RFC §3.4, §3.8) — so the save path needs `workspace_fs` plus the
|
|
2268
|
+
per-instance `agent_assets` store (#1903), bound to the saving user's
|
|
2269
|
+
identity/team, but none of the execution-only services
|
|
2270
|
+
- `document_folders` lets an asset-parsing capability resolve author folder
|
|
2271
|
+
strings against the agent's space at save time (#1903 image support)
|
|
2175
2272
|
"""
|
|
2176
2273
|
|
|
2177
2274
|
request_id = str(uuid4())
|
|
@@ -2181,6 +2278,7 @@ def _build_capability_save_services(
|
|
|
2181
2278
|
user_id=user_id,
|
|
2182
2279
|
team_id=team_id,
|
|
2183
2280
|
access_token=access_token,
|
|
2281
|
+
agent_instance_id=agent_instance_id,
|
|
2184
2282
|
),
|
|
2185
2283
|
portable_context=PortableContext(
|
|
2186
2284
|
request_id=request_id,
|
|
@@ -2196,7 +2294,9 @@ def _build_capability_save_services(
|
|
|
2196
2294
|
)
|
|
2197
2295
|
settings = _PodAgentSettings(id=actor, name=actor, team_id=team_id, tuning=None)
|
|
2198
2296
|
return RuntimeServices(
|
|
2199
|
-
workspace_fs=FredWorkspaceFs(binding=binding, settings=settings)
|
|
2297
|
+
workspace_fs=FredWorkspaceFs(binding=binding, settings=settings),
|
|
2298
|
+
agent_assets=AgentConfigAssetsAdapter(binding=binding, settings=settings),
|
|
2299
|
+
document_folders=DocumentFolderAdapter(binding=binding, settings=settings),
|
|
2200
2300
|
)
|
|
2201
2301
|
|
|
2202
2302
|
|
|
@@ -2223,13 +2323,11 @@ def _effective_capability_ids(
|
|
|
2223
2323
|
`default_mcp_servers` ids; a selected id that is a known-but-DISABLED
|
|
2224
2324
|
catalog MCP server is dropped (the live tool provider skips it anyway —
|
|
2225
2325
|
#1988 keeps that tolerance), while an id the pod knows nothing about stays
|
|
2226
|
-
and fails loudly downstream.
|
|
2227
|
-
When the registry is absent the raw selection is returned
|
|
2228
|
-
caller decides whether that is an error.
|
|
2326
|
+
and fails loudly downstream. Resolved identically for ReAct and Graph
|
|
2327
|
+
agents. When the registry is absent the raw selection is returned
|
|
2328
|
+
unfiltered — the caller decides whether that is an error.
|
|
2229
2329
|
"""
|
|
2230
2330
|
|
|
2231
|
-
if not isinstance(definition, ReActAgentDefinition):
|
|
2232
|
-
return []
|
|
2233
2331
|
selected = tuning.selected_capability_ids if tuning is not None else None
|
|
2234
2332
|
if selected is None:
|
|
2235
2333
|
selected = [ref.id for ref in definition.default_mcp_servers]
|
|
@@ -2322,23 +2420,20 @@ def _build_capability_block(
|
|
|
2322
2420
|
Returns None when the agent selects no capabilities.
|
|
2323
2421
|
|
|
2324
2422
|
MCP handling (#1978, #1988): an MCP-server capability delivers its catalog
|
|
2325
|
-
`agent_instructions` as a prompt fragment
|
|
2423
|
+
`agent_instructions` as a prompt fragment via `middleware()`
|
|
2424
|
+
(`_McpInstructionsMiddleware`). The effective selection mirrors
|
|
2326
2425
|
`_active_mcp_server_refs`: a `None` capability selection (template default)
|
|
2327
2426
|
activates the template's `default_mcp_servers` as capabilities so their
|
|
2328
|
-
instructions are delivered — otherwise a default
|
|
2329
|
-
|
|
2427
|
+
instructions are delivered — otherwise a default ReAct agent would silently
|
|
2428
|
+
lose its non-negotiable grounding contract. This block is built identically
|
|
2429
|
+
for both agent kinds (CAPAB-02), but a Graph agent reads only `block.tools`
|
|
2430
|
+
— MCP tools reach it (a separate, already execution-model-agnostic path,
|
|
2431
|
+
`FredMcpToolProvider`), the `agent_instructions` prompt fragment does NOT
|
|
2432
|
+
(Graph never reads `block.middleware` at all). A Graph agent that needs an
|
|
2433
|
+
MCP server's grounding instructions must currently author them into its own
|
|
2434
|
+
system prompt — this is an explicit, known gap, not yet closed (CAPAB-02).
|
|
2330
2435
|
"""
|
|
2331
2436
|
|
|
2332
|
-
if not isinstance(definition, ReActAgentDefinition):
|
|
2333
|
-
# Capabilities (incl. MCP instruction fragments) are ReAct-only (RFC §5).
|
|
2334
|
-
# A non-ReAct template selecting real capabilities is still a loud error.
|
|
2335
|
-
if tuning is not None and tuning.selected_capability_ids:
|
|
2336
|
-
raise CapabilityError(
|
|
2337
|
-
"Capabilities are only supported on ReAct agents (RFC §5); "
|
|
2338
|
-
f"template '{definition.agent_id}' is not one."
|
|
2339
|
-
)
|
|
2340
|
-
return None
|
|
2341
|
-
|
|
2342
2437
|
selected = tuning.selected_capability_ids if tuning is not None else None
|
|
2343
2438
|
capability_config = tuning.capability_config if tuning is not None else {}
|
|
2344
2439
|
if capability_registry is None:
|
|
@@ -2359,6 +2454,49 @@ def _build_capability_block(
|
|
|
2359
2454
|
effective = _effective_capability_ids(tuning, definition, capability_registry)
|
|
2360
2455
|
if not effective:
|
|
2361
2456
|
return None
|
|
2457
|
+
if isinstance(definition, GraphAgentDefinition):
|
|
2458
|
+
# CAPAB-02: a capability can declare itself ReAct-only
|
|
2459
|
+
# (`CapabilityManifest.execution_models`) when its runtime need is a
|
|
2460
|
+
# `middleware()` hook `tools()` cannot express (e.g. PPT filler's
|
|
2461
|
+
# dynamic per-turn tool schema). Selecting one on a Graph agent must
|
|
2462
|
+
# fail loudly here — never silently contribute zero tools (RFC §3.9
|
|
2463
|
+
# "never silently degrade").
|
|
2464
|
+
react_only = [
|
|
2465
|
+
cap_id
|
|
2466
|
+
for cap_id in effective
|
|
2467
|
+
if cap_id in capability_registry
|
|
2468
|
+
and "graph"
|
|
2469
|
+
not in capability_registry.capability(cap_id).manifest.execution_models
|
|
2470
|
+
]
|
|
2471
|
+
if react_only:
|
|
2472
|
+
raise CapabilityError(
|
|
2473
|
+
f"Agent selects capabilities {react_only} which are ReAct-only "
|
|
2474
|
+
"(CapabilityManifest.execution_models) and cannot run on a "
|
|
2475
|
+
"Graph agent."
|
|
2476
|
+
)
|
|
2477
|
+
# CAPAB-02 stopgap: `CapabilityAgentBlock.hitl` is built (assembly.py)
|
|
2478
|
+
# but `GraphRuntime.invoke_runtime_tool` never consults it — a
|
|
2479
|
+
# capability's `HitlSpec` gates a ReAct tool call but not a Graph
|
|
2480
|
+
# one. No production capability declares an active `HitlSpec` today,
|
|
2481
|
+
# so refusing here costs nothing real yet; reconciling Graph's own
|
|
2482
|
+
# node-level pause/resume with the per-tool HITL gate is real design
|
|
2483
|
+
# work, deferred (see AGENT-CAPABILITY-RFC.md §3.9). Refusing loudly
|
|
2484
|
+
# keeps the RFC's "never silently degrade" guarantee intact in the meantime
|
|
2485
|
+
# — a capability with `HitlSpec`s that silently ran ungated on Graph
|
|
2486
|
+
# would be exactly the kind of governance gap this platform exists to
|
|
2487
|
+
# prevent.
|
|
2488
|
+
hitl_gated = [
|
|
2489
|
+
cap_id
|
|
2490
|
+
for cap_id in effective
|
|
2491
|
+
if cap_id in capability_registry
|
|
2492
|
+
and capability_registry.capability(cap_id).hitl_specs()
|
|
2493
|
+
]
|
|
2494
|
+
if hitl_gated:
|
|
2495
|
+
raise CapabilityError(
|
|
2496
|
+
f"Agent selects capabilities {hitl_gated} which declare "
|
|
2497
|
+
"HitlSpec approval gates; Graph agents do not yet enforce "
|
|
2498
|
+
"capability HITL (CAPAB-02) and cannot run them."
|
|
2499
|
+
)
|
|
2362
2500
|
contexts = build_capability_contexts(
|
|
2363
2501
|
capability_registry,
|
|
2364
2502
|
selected_capability_ids=effective,
|
|
@@ -2413,6 +2551,19 @@ async def _iterate_runtime_event_payloads(
|
|
|
2413
2551
|
ctx = request.context or {}
|
|
2414
2552
|
correlation_id = ctx.get("correlation_id", request_id)
|
|
2415
2553
|
resolved_team_id = team_id or ctx.get("team_id")
|
|
2554
|
+
# kind="model" enforcement (OBSERV-02 v3, AGENT-CAPABILITY-RFC.md §8.7):
|
|
2555
|
+
# computed ONCE per turn, here — never inside model-routing resolution,
|
|
2556
|
+
# which runs multiple times per turn and must never itself make a ReBAC
|
|
2557
|
+
# call. No team context (agent-to-agent invocation) → no restriction,
|
|
2558
|
+
# the same posture _authorize_execution_or_raise already applies to a
|
|
2559
|
+
# teamless request.
|
|
2560
|
+
usable_model_ids: tuple[str, ...] | None = None
|
|
2561
|
+
if resolved_team_id is not None:
|
|
2562
|
+
from ..model_routing import usable_model_capability_ids
|
|
2563
|
+
|
|
2564
|
+
rebac = get_runtime_context().config.rebac_engine
|
|
2565
|
+
ids = await usable_model_capability_ids(rebac, resolved_team_id)
|
|
2566
|
+
usable_model_ids = tuple(ids) if ids is not None else None
|
|
2416
2567
|
execution_action = ctx.get("execution_action") or (
|
|
2417
2568
|
ExecutionGrantAction.RESUME.value
|
|
2418
2569
|
if request.resume_payload is not None
|
|
@@ -2440,6 +2591,7 @@ async def _iterate_runtime_event_payloads(
|
|
|
2440
2591
|
"checkpoint_id": resolved_checkpoint_id,
|
|
2441
2592
|
"execution_action": execution_action,
|
|
2442
2593
|
"exchange_id": exchange_id,
|
|
2594
|
+
"is_service_agent": ctx.get("is_service_agent"),
|
|
2443
2595
|
}.items()
|
|
2444
2596
|
if isinstance(value, str) and value
|
|
2445
2597
|
},
|
|
@@ -2480,11 +2632,26 @@ async def _iterate_runtime_event_payloads(
|
|
|
2480
2632
|
# When the final file is deleted this is absent, so the per-turn runtime
|
|
2481
2633
|
# notice disappears without leaving a checkpointed system message behind.
|
|
2482
2634
|
attachments_markdown=ctx.get("attachments_markdown"),
|
|
2635
|
+
# Team routing policy snapshot (TEAM-ROUTING-POLICY-RFC.md §3/§8):
|
|
2636
|
+
# control-plane resolves it once at prepare-execution and the frontend
|
|
2637
|
+
# forwards it unchanged, same channel as context_prompt_text above — but
|
|
2638
|
+
# it was also silently dropped here, so resolve_team_override in
|
|
2639
|
+
# fred_runtime.model_routing.provider always saw None and no team's
|
|
2640
|
+
# routing policy ever took effect on a real chat turn. ctx already holds
|
|
2641
|
+
# the flat dict from to_legacy_context()'s model_dump(exclude_none=True);
|
|
2642
|
+
# operation_route_rules needs re-validating back into typed rows.
|
|
2643
|
+
chat_default_profile_id=ctx.get("chat_default_profile_id"),
|
|
2644
|
+
operation_route_rules=[
|
|
2645
|
+
TeamOperationRouteRule.model_validate(rule)
|
|
2646
|
+
for rule in (ctx.get("operation_route_rules") or [])
|
|
2647
|
+
]
|
|
2648
|
+
or None,
|
|
2483
2649
|
)
|
|
2484
2650
|
|
|
2485
2651
|
binding = BoundRuntimeContext(
|
|
2486
2652
|
runtime_context=runtime_context,
|
|
2487
2653
|
portable_context=portable_context,
|
|
2654
|
+
usable_model_ids=usable_model_ids,
|
|
2488
2655
|
)
|
|
2489
2656
|
|
|
2490
2657
|
services = _build_runtime_services(
|
|
@@ -2540,6 +2707,7 @@ async def _iterate_runtime_event_payloads(
|
|
|
2540
2707
|
runtime = GraphRuntime(
|
|
2541
2708
|
definition=definition,
|
|
2542
2709
|
services=services,
|
|
2710
|
+
capability_block=capability_block,
|
|
2543
2711
|
)
|
|
2544
2712
|
runtime.bind(binding)
|
|
2545
2713
|
await runtime.activate()
|
|
@@ -2806,7 +2974,7 @@ def _build_agent_router(
|
|
|
2806
2974
|
"""
|
|
2807
2975
|
|
|
2808
2976
|
capability_registry = _capability_registry_of(http_request)
|
|
2809
|
-
|
|
2977
|
+
all_capability_entries = (
|
|
2810
2978
|
[
|
|
2811
2979
|
CapabilityCatalogEntry.from_manifest(
|
|
2812
2980
|
capability_registry.capability(cap_id).manifest,
|
|
@@ -2826,7 +2994,21 @@ def _build_agent_router(
|
|
|
2826
2994
|
kind=definition.execution_category,
|
|
2827
2995
|
default_tuning=_definition_to_agent_tuning(definition),
|
|
2828
2996
|
available_mcp_servers=_available_mcp_servers_for_definition(definition),
|
|
2829
|
-
|
|
2997
|
+
# CAPAB-02: a capability declared `execution_models=("react",)`
|
|
2998
|
+
# must not even be offered for selection on a Graph template —
|
|
2999
|
+
# `_build_capability_block` would refuse it loudly at save/run
|
|
3000
|
+
# time regardless, but a picker that lists it first invites the
|
|
3001
|
+
# exact "select it, save it, discover the incompatibility at
|
|
3002
|
+
# first launch" flow the loud refusal exists to prevent.
|
|
3003
|
+
available_capabilities=(
|
|
3004
|
+
all_capability_entries
|
|
3005
|
+
if not isinstance(definition, GraphAgentDefinition)
|
|
3006
|
+
else [
|
|
3007
|
+
entry
|
|
3008
|
+
for entry in all_capability_entries
|
|
3009
|
+
if "graph" in entry.execution_models
|
|
3010
|
+
]
|
|
3011
|
+
),
|
|
2830
3012
|
default_capability_ids=[
|
|
2831
3013
|
ref.id for ref in definition.default_mcp_servers
|
|
2832
3014
|
],
|
|
@@ -2869,6 +3051,42 @@ def _build_agent_router(
|
|
|
2869
3051
|
]
|
|
2870
3052
|
)
|
|
2871
3053
|
|
|
3054
|
+
@router.get("/models-catalog")
|
|
3055
|
+
async def get_models_catalog() -> _ModelCatalogResponse:
|
|
3056
|
+
"""
|
|
3057
|
+
Return this pod's routable models, one entry per distinct
|
|
3058
|
+
(provider, name) pair (OBSERV-02 v3, `AGENT-CAPABILITY-RFC.md` §8.7).
|
|
3059
|
+
|
|
3060
|
+
Why this endpoint exists:
|
|
3061
|
+
- control-plane's capability catalog aggregation
|
|
3062
|
+
(`aggregate_capability_catalog`) has no other way to learn what
|
|
3063
|
+
models this pod can route to — `models_catalog.yaml` is loaded
|
|
3064
|
+
only here, for routing, with no prior control-plane consumer.
|
|
3065
|
+
- one entry per (provider, name), not per catalog profile: a model
|
|
3066
|
+
used for both the `chat` and `language` routing capability is one
|
|
3067
|
+
admin enablement decision, not two.
|
|
3068
|
+
|
|
3069
|
+
Re-reads `models_catalog.yaml` fresh on every call (same file
|
|
3070
|
+
`_build_chat_model_factory` loaded once at boot) rather than
|
|
3071
|
+
caching the parsed catalog — this route is polled rarely (admin
|
|
3072
|
+
catalog refresh), never per-turn, so the extra local file read is
|
|
3073
|
+
immaterial and keeps this always consistent with whatever the pod
|
|
3074
|
+
would actually resolve, with no separate cache to go stale.
|
|
3075
|
+
|
|
3076
|
+
How to use it:
|
|
3077
|
+
- call from control-plane's `_model_capabilities_for_source`
|
|
3078
|
+
|
|
3079
|
+
Example:
|
|
3080
|
+
- `GET /fred/agents/v2/agents/models-catalog`
|
|
3081
|
+
"""
|
|
3082
|
+
from ..model_routing import load_model_catalog
|
|
3083
|
+
|
|
3084
|
+
catalog_path = get_runtime_context().config.models_catalog_path
|
|
3085
|
+
if not catalog_path:
|
|
3086
|
+
return _ModelCatalogResponse(models=[])
|
|
3087
|
+
catalog = load_model_catalog(catalog_path)
|
|
3088
|
+
return _ModelCatalogResponse(models=_project_model_catalog_entries(catalog))
|
|
3089
|
+
|
|
2872
3090
|
@router.post("/capabilities/{capability_id}/validate-config")
|
|
2873
3091
|
async def validate_capability_config(
|
|
2874
3092
|
capability_id: str,
|
|
@@ -2979,6 +3197,10 @@ def _build_agent_router(
|
|
|
2979
3197
|
user_id=(caller.uid if caller is not None else None) or "anonymous",
|
|
2980
3198
|
team_id=team_id or None,
|
|
2981
3199
|
access_token=access_token,
|
|
3200
|
+
agent_instance_id=(
|
|
3201
|
+
form_instance_id if isinstance(form_instance_id, str) else None
|
|
3202
|
+
)
|
|
3203
|
+
or None,
|
|
2982
3204
|
),
|
|
2983
3205
|
)
|
|
2984
3206
|
try:
|
|
@@ -3806,12 +4028,13 @@ def create_agent_app(
|
|
|
3806
4028
|
# Boot order (must be preserved — each step depends on the previous):
|
|
3807
4029
|
# 1. log_setup — formatter/handlers ready for all subsequent logs
|
|
3808
4030
|
# 2. initialize_kpi_writer — needed by bootstrap_observability
|
|
3809
|
-
# 3.
|
|
3810
|
-
# 4.
|
|
3811
|
-
# 5.
|
|
3812
|
-
# 6.
|
|
3813
|
-
# 7.
|
|
3814
|
-
# 8.
|
|
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_exporter — prometheus thread, after KPI writer exists
|
|
4036
|
+
# 8. start_kpi_tasks — asyncio tasks, after SQL engine is known
|
|
4037
|
+
# 9. set_runtime_context — wires all built parts into the global config
|
|
3815
4038
|
log_setup(
|
|
3816
4039
|
service_name=config.app.name,
|
|
3817
4040
|
log_level=config.app.log_level,
|
|
@@ -3822,6 +4045,7 @@ def create_agent_app(
|
|
|
3822
4045
|
)
|
|
3823
4046
|
container = build_pod_container(config)
|
|
3824
4047
|
container.initialize_kpi_writer()
|
|
4048
|
+
container.initialize_control_plane_client()
|
|
3825
4049
|
bootstrap_observability(
|
|
3826
4050
|
config.observability, kpi_writer=container.get_kpi_writer()
|
|
3827
4051
|
)
|
|
@@ -3839,7 +4063,11 @@ def create_agent_app(
|
|
|
3839
4063
|
# every execution against OpenFGA; a disabled/Noop engine (dev) means
|
|
3840
4064
|
# identity-only. Safe in all modes — the factory returns a Noop with a
|
|
3841
4065
|
# KeycloackDisabled admin client when user/m2m auth is off.
|
|
3842
|
-
rebac_engine =
|
|
4066
|
+
rebac_engine = (
|
|
4067
|
+
rebac_factory(security, kpi_writer=container.get_kpi_writer())
|
|
4068
|
+
if security is not None
|
|
4069
|
+
else None
|
|
4070
|
+
)
|
|
3843
4071
|
chat_factory = _build_chat_model_factory(config)
|
|
3844
4072
|
await container.initialize_sql()
|
|
3845
4073
|
container.start_metrics_exporter()
|
|
@@ -3860,6 +4088,7 @@ def create_agent_app(
|
|
|
3860
4088
|
checkpointer=checkpointer,
|
|
3861
4089
|
history_store=history_store,
|
|
3862
4090
|
mcp_configuration=config.get_mcp_configuration(),
|
|
4091
|
+
models_catalog_path=config.get_models_catalog_path(),
|
|
3863
4092
|
inprocess_toolkit_factory=build_inprocess_toolkit,
|
|
3864
4093
|
control_plane_url=config.platform.control_plane_url,
|
|
3865
4094
|
rebac_engine=rebac_engine,
|