fred-runtime 3.3.7__tar.gz → 3.4.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/PKG-INFO +1 -1
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/app/agent_app.py +365 -28
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/app/context.py +86 -20
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/app/openai_compat_router.py +9 -2
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/capabilities/assembly.py +18 -2
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/capabilities/document_access/capability.py +22 -158
- fred_runtime-3.4.1/fred_runtime/capabilities/document_summarize/__init__.py +38 -0
- fred_runtime-3.4.1/fred_runtime/capabilities/document_summarize/capability.py +325 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/capabilities/errors.py +10 -6
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/deep/deep_runtime.py +1 -2
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/graph/graph_runtime.py +59 -9
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/model_routing/__init__.py +4 -0
- fred_runtime-3.4.1/fred_runtime/model_routing/authz.py +104 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/model_routing/contracts.py +107 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/model_routing/provider.py +100 -4
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/model_routing/resolver.py +54 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/react/middleware/checkpoint_hygiene.py +15 -11
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/react/middleware/tool_observability.py +5 -4
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/react/react_prompting.py +42 -5
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/react/react_runtime.py +1 -8
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/react/react_tool_binding.py +30 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/runtime_context.py +7 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/runtime_support/sql_checkpointer.py +22 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/support/thinking.py +72 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime.egg-info/PKG-INFO +1 -1
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime.egg-info/SOURCES.txt +8 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime.egg-info/entry_points.txt +1 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/pyproject.toml +4 -1
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_agent_app.py +668 -18
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_capability_agent_1973.py +43 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_capability_document_access_1906.py +9 -175
- fred_runtime-3.4.1/tests/test_capability_document_summarize.py +324 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_capability_endpoints_1974.py +17 -4
- fred_runtime-3.4.1/tests/test_context.py +370 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_deep_agent_middleware.py +0 -8
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_graph_capability_bridge.py +128 -1
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_history.py +219 -0
- fred_runtime-3.4.1/tests/test_model_enforcement.py +385 -0
- fred_runtime-3.4.1/tests/test_model_reasoning_enablement.py +328 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_model_routing.py +113 -1
- fred_runtime-3.4.1/tests/test_models_catalog_projection.py +199 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_react_middleware_frame.py +42 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_react_prompting.py +68 -3
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_react_thinking.py +124 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_sql_checkpointer_owner.py +34 -0
- fred_runtime-3.4.1/tests/test_tool_carrier_middleware_naming.py +139 -0
- fred_runtime-3.3.7/tests/test_context.py +0 -133
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/README.md +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/__init__.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/__main__.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/app/__init__.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/app/_catalogs.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/app/config.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/app/config_loader.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/app/container.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/app/dependencies.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/app/mcp_config.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/app/observability_factory.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/capabilities/__init__.py +1 -1
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/capabilities/assets.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/capabilities/demo.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/capabilities/demo_migrations/env.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/capabilities/demo_migrations/versions/d1e2m0a0b0c0_create_demo_echo_notes.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/capabilities/document_access/__init__.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/capabilities/mcp.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/capabilities/openapi_dump.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/capabilities/registry.py +1 -1
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/cli/__init__.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/cli/completion.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/cli/entrypoint.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/cli/history_display.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/cli/kpi_display.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/cli/pod_client.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/cli/repl.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/cli/repl_helpers.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/cli/url_helpers.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/client.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/common/__init__.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/common/context_aware_tool.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/common/kf_base_client.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/common/kf_document_client.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/common/kf_fast_text_client.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/common/kf_http_client.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/common/kf_markdown_media_client.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/common/kf_tag_client.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/common/kf_vectorsearch_client.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/common/kf_workspace_client.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/common/mcp_interceptors.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/common/mcp_runtime.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/common/mcp_toolkit.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/common/mcp_utils.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/common/structures.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/common/token_expiry.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/common/tool_node_utils.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/deep/__init__.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/eval/__init__.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/eval/collector.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/graph/__init__.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/integrations/__init__.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/integrations/inprocess_toolkit_registry.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/integrations/kf_vector_search/__init__.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/integrations/kf_vector_search/toolkit.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/integrations/v2_runtime/__init__.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/integrations/v2_runtime/adapters.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/migrations.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/model_routing/catalog.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/react/__init__.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/react/middleware/__init__.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/react/middleware/dynamic_prompt.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/react/middleware/frame.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/react/middleware/hitl.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/react/middleware/model_routing.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/react/middleware/shared.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/react/middleware/tracing_kpi.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/react/react_langchain_adapter.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/react/react_message_codec.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/react/react_model_adapter.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/react/react_stream_adapter.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/react/react_tool_loop.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/react/react_tool_rendering.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/react/react_tool_resolution.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/react/react_tool_utils.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/react/react_tracing.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/runtime_support/__init__.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/runtime_support/checkpoints.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/runtime_support/model_metadata.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/runtime_support/request_context_helpers.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/runtime_support/user_token_refresher.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/support/__init__.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/support/filesystem_context.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime/support/tool_loop.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime.egg-info/dependency_links.txt +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime.egg-info/requires.txt +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/fred_runtime.egg-info/top_level.txt +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/setup.cfg +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_capability_chat_controls_1976.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_capability_chat_parts_1977.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_capability_registry_1973.py +1 -1
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_capability_routes_tables_1979.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_capability_selection_1974.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_capability_tool_return_convention.py +1 -1
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_client.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_config_loader.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_context_aware_tool.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_conversational_memory.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_eval_collector.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_eval_trace.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_fred_workspace_fs.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_graph_runtime_invoke_agent.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_graph_runtime_observability.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_kf_vector_search_toolkit.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_kf_workspace_client.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_knowledge_search_tool_invoker_sources.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_kpi_display.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_mcp_config.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_openai_compat_router.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_pod_client.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_react_loop_regressions_1972.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_repl_helpers.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_runtime_context_prompt_injection.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_smoke.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_spike_capability_state_1971.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_token_expiry.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_tool_loop_trim.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_tool_observability_middleware.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_tracing_kpi_content_confinement.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_url_helpers.py +0 -0
- {fred_runtime-3.3.7 → fred_runtime-3.4.1}/tests/test_user_token_refresher.py +0 -0
|
@@ -56,8 +56,10 @@ from fastapi import APIRouter, Depends, FastAPI, HTTPException, Request, status
|
|
|
56
56
|
from fastapi.middleware.cors import CORSMiddleware
|
|
57
57
|
from fastapi.responses import StreamingResponse
|
|
58
58
|
from fred_core.common.config_loader import get_config
|
|
59
|
+
from fred_core.diagnostics import install_gc_diagnostics
|
|
59
60
|
from fred_core.history.history_schema import ChatMessage
|
|
60
61
|
from fred_core.kpi import KPIMiddleware
|
|
62
|
+
from fred_core.kpi.kpi_runtime_stage_metric import runtime_stage_timer
|
|
61
63
|
from fred_core.kpi.kpi_writer_structures import KPIActor
|
|
62
64
|
from fred_core.logs.audit_log import emit_audit_log
|
|
63
65
|
from fred_core.logs.log_setup import log_setup
|
|
@@ -88,6 +90,7 @@ from fred_sdk.contracts.context import (
|
|
|
88
90
|
PortableContext,
|
|
89
91
|
PortableEnvironment,
|
|
90
92
|
RuntimeContext,
|
|
93
|
+
TeamOperationRouteRule,
|
|
91
94
|
)
|
|
92
95
|
from fred_sdk.contracts.eval import EvalStep, EvalTrace
|
|
93
96
|
from fred_sdk.contracts.execution import (
|
|
@@ -791,6 +794,7 @@ def _build_runtime_services(
|
|
|
791
794
|
return RuntimeServices(
|
|
792
795
|
tracer=build_default_tracer(),
|
|
793
796
|
metrics=KPIWriterMetricsAdapter(runtime_config.kpi_writer),
|
|
797
|
+
kpi_writer=runtime_config.kpi_writer,
|
|
794
798
|
chat_model_factory=runtime_config.chat_model_factory,
|
|
795
799
|
tool_invoker=tool_invoker,
|
|
796
800
|
tool_provider=tool_provider,
|
|
@@ -967,6 +971,84 @@ class _McpCatalogResponse(BaseModel):
|
|
|
967
971
|
servers: list[_McpCatalogEntry]
|
|
968
972
|
|
|
969
973
|
|
|
974
|
+
class _ModelCatalogEntry(BaseModel):
|
|
975
|
+
id: str
|
|
976
|
+
provider: str
|
|
977
|
+
name: str
|
|
978
|
+
description: str | None = None
|
|
979
|
+
profile_ids: list[str] = Field(default_factory=list)
|
|
980
|
+
"""Every `models_catalog.yaml` profile_id sharing this entry's
|
|
981
|
+
(provider, name) — TEAM-ROUTING-POLICY-RFC.md §7.1's id-space
|
|
982
|
+
translation: a routing policy picks by profile_id (finer-grained than
|
|
983
|
+
this capability id), so control-plane needs a way back from profile_id
|
|
984
|
+
to capability id for write-time enablement validation, and the
|
|
985
|
+
team-settings picker needs the reverse (which profile_ids this enabled
|
|
986
|
+
capability actually offers). Declaration order in the source YAML,
|
|
987
|
+
first-seen first — same order `default_profile_by_capability` favors."""
|
|
988
|
+
thinking_profile_ids: list[str] = Field(default_factory=list)
|
|
989
|
+
"""The subset of `profile_ids` above declaring `supports_thinking`
|
|
990
|
+
(`MODEL-REASONING-ENABLEMENT-RFC.md` §5.3, REASON-01). DERIVED here,
|
|
991
|
+
never authored twice: aptitude is declared per profile (§4, where the
|
|
992
|
+
behaviour actually differs), while the admin toggle is keyed per model
|
|
993
|
+
because that is what the capability id space and the admin UI both are
|
|
994
|
+
(§5.2 — the id space is not negotiable). This field is the projection
|
|
995
|
+
between the two.
|
|
996
|
+
|
|
997
|
+
Empty means this model has NO reasoning-capable profile, and the admin row
|
|
998
|
+
shows no reasoning control at all — aptitude is not a choice, an
|
|
999
|
+
administrator cannot make a model reason. Non-empty means the row carries
|
|
1000
|
+
the toggle. Same established join as `profile_ids` above."""
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
class _ModelCatalogResponse(BaseModel):
|
|
1004
|
+
models: list[_ModelCatalogEntry]
|
|
1005
|
+
|
|
1006
|
+
|
|
1007
|
+
def _project_model_catalog_entries(catalog: Any) -> list[_ModelCatalogEntry]:
|
|
1008
|
+
"""One `_ModelCatalogEntry` per distinct (provider, name) pair in a
|
|
1009
|
+
loaded `ModelCatalog` (OBSERV-02 v3, `AGENT-CAPABILITY-RFC.md` §8.7).
|
|
1010
|
+
|
|
1011
|
+
Pulled out of `get_models_catalog` as a pure function so it is directly
|
|
1012
|
+
unit-testable without a running app (`agent_app.py` has no existing
|
|
1013
|
+
FastAPI TestClient harness — building one just for this route would be
|
|
1014
|
+
disproportionate; this function is where the actual logic worth testing
|
|
1015
|
+
lives). `catalog` is typed `Any` here to avoid a module-level
|
|
1016
|
+
`..model_routing` import (see the route's own lazy import for why);
|
|
1017
|
+
callers pass a real `ModelCatalog`.
|
|
1018
|
+
"""
|
|
1019
|
+
from fred_sdk.contracts.capability.manifest import model_capability_id
|
|
1020
|
+
|
|
1021
|
+
seen: dict[tuple[str, str], _ModelCatalogEntry] = {}
|
|
1022
|
+
for profile in catalog.profiles:
|
|
1023
|
+
provider = profile.model.provider
|
|
1024
|
+
name = profile.model.name
|
|
1025
|
+
# ModelProfile.validate_model (contracts.py) already guarantees both
|
|
1026
|
+
# are non-empty strings for every profile that exists — asserted
|
|
1027
|
+
# (not a skip/continue) so this narrows the type instead of hiding
|
|
1028
|
+
# an invariant violation the validator itself is supposed to prevent.
|
|
1029
|
+
assert provider and name, (
|
|
1030
|
+
f"ModelProfile {profile.profile_id!r} passed validation without provider/name"
|
|
1031
|
+
)
|
|
1032
|
+
key = (provider, name)
|
|
1033
|
+
existing = seen.get(key)
|
|
1034
|
+
if existing is None:
|
|
1035
|
+
existing = _ModelCatalogEntry(
|
|
1036
|
+
id=model_capability_id(provider, name),
|
|
1037
|
+
provider=provider,
|
|
1038
|
+
name=name,
|
|
1039
|
+
description=profile.description,
|
|
1040
|
+
profile_ids=[profile.profile_id],
|
|
1041
|
+
)
|
|
1042
|
+
seen[key] = existing
|
|
1043
|
+
else:
|
|
1044
|
+
existing.profile_ids.append(profile.profile_id)
|
|
1045
|
+
# REASON-01 §5.3: aptitude is per profile, the toggle is per model —
|
|
1046
|
+
# this one condition is the whole projection between them.
|
|
1047
|
+
if profile.supports_thinking:
|
|
1048
|
+
existing.thinking_profile_ids.append(profile.profile_id)
|
|
1049
|
+
return list(seen.values())
|
|
1050
|
+
|
|
1051
|
+
|
|
970
1052
|
class _ResolvedAgentInstance(BaseModel):
|
|
971
1053
|
agent_instance_id: str
|
|
972
1054
|
template_agent_id: str
|
|
@@ -1120,7 +1202,9 @@ async def _resolve_agent_instance(
|
|
|
1120
1202
|
registry: Mapping[str, ReActAgentDefinition | GraphAgentDefinition],
|
|
1121
1203
|
access_token: str | None,
|
|
1122
1204
|
control_plane_url: str | None,
|
|
1205
|
+
http_client: httpx.AsyncClient,
|
|
1123
1206
|
team_id: str | None = None,
|
|
1207
|
+
request_id: str | None = None,
|
|
1124
1208
|
) -> _ResolvedExecutionTarget:
|
|
1125
1209
|
"""
|
|
1126
1210
|
Resolve a direct or managed execution target into a concrete definition.
|
|
@@ -1195,9 +1279,17 @@ async def _resolve_agent_instance(
|
|
|
1195
1279
|
f"{control_plane_url.rstrip('/')}/teams/{team_id}/agent-instances/"
|
|
1196
1280
|
f"{request.agent_instance_id}/runtime"
|
|
1197
1281
|
)
|
|
1198
|
-
headers
|
|
1199
|
-
|
|
1200
|
-
|
|
1282
|
+
headers: dict[str, str] = {}
|
|
1283
|
+
if access_token:
|
|
1284
|
+
headers["Authorization"] = f"Bearer {access_token}"
|
|
1285
|
+
if request_id:
|
|
1286
|
+
# Correlates this pod-side call with the control-plane request it
|
|
1287
|
+
# triggers — both sides stamp the same id as `trace.trace_id` on their
|
|
1288
|
+
# `runtime.stage_latency_ms{runtime_stage=runtime_binding*}` event
|
|
1289
|
+
# (TURN-01 evidence gap: no way to join a turn to its binding call
|
|
1290
|
+
# besides timestamps).
|
|
1291
|
+
headers["X-Request-Id"] = request_id
|
|
1292
|
+
response = await http_client.get(url, headers=headers or None)
|
|
1201
1293
|
if response.status_code == status.HTTP_404_NOT_FOUND:
|
|
1202
1294
|
raise HTTPException(
|
|
1203
1295
|
status_code=404, detail=response.text or "Unknown agent instance."
|
|
@@ -1286,12 +1378,21 @@ async def _authorize_execution_or_raise(
|
|
|
1286
1378
|
the owner's `team_editor` tuple self-heals on first touch inside
|
|
1287
1379
|
`RebacEngine.check_user_team_permission_or_raise`, and `add_relation`'s
|
|
1288
1380
|
write-guard refuses any tuple naming a personal team except that one, so
|
|
1289
|
-
the
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
team
|
|
1381
|
+
the `CAN_USE_TEAM_AGENTS` check below already authorizes the owner
|
|
1382
|
+
(`team_editor` implies `team_member`) and denies everyone else (another
|
|
1383
|
+
user's space, or the bare `"personal"` alias, for which no tuple is
|
|
1384
|
+
ever provisioned) — no special-casing needed here.
|
|
1385
|
+
- otherwise require the caller to hold `CAN_USE_TEAM_AGENTS` on the
|
|
1386
|
+
requested team — `team_member`-only, unlike `CAN_READ` (which also
|
|
1387
|
+
admits the `public` marketplace-discovery relation, TEAM-09/TEAM-10).
|
|
1388
|
+
Execution is real content access, not discovery, so a public-team
|
|
1389
|
+
visitor must not pass here even though they can see the team profile.
|
|
1390
|
+
This used to check `CAN_READ` (the same relation the control-plane
|
|
1391
|
+
required before it would mint a grant) — that predates TEAM-09's
|
|
1392
|
+
`public` relation and was never revisited; `product/api.py`'s
|
|
1393
|
+
`post_prepare_execution` made the identical correction for the same
|
|
1394
|
+
reason (returns prompt content, not just config). The team is
|
|
1395
|
+
caller-supplied but safe: OpenFGA only authorizes teams the user
|
|
1295
1396
|
actually holds a relation to. Authorization and denial are both audited;
|
|
1296
1397
|
any OpenFGA denial fails closed (403).
|
|
1297
1398
|
|
|
@@ -1374,7 +1475,7 @@ async def _authorize_execution_or_raise(
|
|
|
1374
1475
|
# since no tuple is ever provisioned for that literal string).
|
|
1375
1476
|
try:
|
|
1376
1477
|
await rebac.check_user_team_permission_or_raise(
|
|
1377
|
-
authenticated_user, TeamPermission.
|
|
1478
|
+
authenticated_user, TeamPermission.CAN_USE_TEAM_AGENTS, team_id
|
|
1378
1479
|
)
|
|
1379
1480
|
except AuthorizationError as exc:
|
|
1380
1481
|
_emit_audit_event(
|
|
@@ -1507,7 +1608,8 @@ async def _authorize_and_resolve(
|
|
|
1507
1608
|
)
|
|
1508
1609
|
await _validate_session_checkpoint_access(request)
|
|
1509
1610
|
await _enforce_session_ownership(request, authenticated_user, container)
|
|
1510
|
-
|
|
1611
|
+
async with runtime_stage_timer(container.get_kpi_writer(), "pod_authz"):
|
|
1612
|
+
await _authorize_execution_or_raise(request, authenticated_user, container)
|
|
1511
1613
|
internal_req = _to_internal_request(request)
|
|
1512
1614
|
# Stamp the trusted service-agent verdict (never the caller-supplied
|
|
1513
1615
|
# context) so per-tool-call re-authorization can mirror the bypass
|
|
@@ -1521,13 +1623,19 @@ async def _authorize_and_resolve(
|
|
|
1521
1623
|
else:
|
|
1522
1624
|
ctx.pop("is_service_agent", None)
|
|
1523
1625
|
internal_req.context = ctx
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1626
|
+
binding_request_id = str(uuid4())
|
|
1627
|
+
async with runtime_stage_timer(
|
|
1628
|
+
container.get_kpi_writer(), "runtime_binding", trace_id=binding_request_id
|
|
1629
|
+
):
|
|
1630
|
+
target = await _resolve_agent_instance(
|
|
1631
|
+
request=internal_req,
|
|
1632
|
+
registry=registry,
|
|
1633
|
+
access_token=access_token,
|
|
1634
|
+
control_plane_url=get_runtime_context().config.control_plane_url,
|
|
1635
|
+
http_client=container.get_control_plane_http_client(),
|
|
1636
|
+
team_id=request.effective_team_id(),
|
|
1637
|
+
request_id=binding_request_id,
|
|
1638
|
+
)
|
|
1531
1639
|
_validate_resolved_team(request, target.team_id, container)
|
|
1532
1640
|
return internal_req, target
|
|
1533
1641
|
|
|
@@ -1651,6 +1759,27 @@ async def _validate_session_checkpoint_access(
|
|
|
1651
1759
|
)
|
|
1652
1760
|
|
|
1653
1761
|
|
|
1762
|
+
@dataclass
|
|
1763
|
+
class _OpenThought:
|
|
1764
|
+
"""
|
|
1765
|
+
One reasoning block being accumulated while mapping a turn to history rows.
|
|
1766
|
+
|
|
1767
|
+
Why the rank is reserved at THOUGHT_START rather than at THOUGHT_END:
|
|
1768
|
+
- a model-native block opens on the first reasoning token and closes only at
|
|
1769
|
+
the first answer delta (RFC AGENT-THINKING-API §7.3), so it brackets every
|
|
1770
|
+
tool call of the turn
|
|
1771
|
+
- the live UI assigns the row's rank at `thought_start` (`useChatSse.ts`);
|
|
1772
|
+
ranking it at close instead would file the reasoning *after* the tools it
|
|
1773
|
+
actually preceded, so a reloaded trace would not match what the user saw
|
|
1774
|
+
"""
|
|
1775
|
+
|
|
1776
|
+
rank: int
|
|
1777
|
+
phase: str
|
|
1778
|
+
title: str | None
|
|
1779
|
+
source: str | None
|
|
1780
|
+
text: list[str] = field(default_factory=list)
|
|
1781
|
+
|
|
1782
|
+
|
|
1654
1783
|
async def _write_turn_history(
|
|
1655
1784
|
*,
|
|
1656
1785
|
session_id: str,
|
|
@@ -1686,6 +1815,7 @@ async def _write_turn_history(
|
|
|
1686
1815
|
Event-to-message mapping:
|
|
1687
1816
|
- ``tool_call`` → ``Role.assistant / Channel.tool_call``
|
|
1688
1817
|
- ``tool_result`` → ``Role.tool / Channel.tool_result``
|
|
1818
|
+
- ``thought_*`` → ``Role.assistant / Channel.thought`` (one row per block)
|
|
1689
1819
|
- ``awaiting_human`` → ``Role.system / Channel.hitl_request`` (full choices)
|
|
1690
1820
|
- ``node_error`` → ``Role.system / Channel.error``
|
|
1691
1821
|
- ``final`` → ``Role.assistant / Channel.final`` (answer + sources)
|
|
@@ -1695,6 +1825,7 @@ async def _write_turn_history(
|
|
|
1695
1825
|
from fred_core.history.history_schema import (
|
|
1696
1826
|
Channel,
|
|
1697
1827
|
ChatMessage,
|
|
1828
|
+
ChatMetadata,
|
|
1698
1829
|
ChatTokenUsage,
|
|
1699
1830
|
Role,
|
|
1700
1831
|
TextPart,
|
|
@@ -1738,6 +1869,55 @@ async def _write_turn_history(
|
|
|
1738
1869
|
messages.append(make_user_text(session_id, exchange_id, rank, request_message))
|
|
1739
1870
|
rank += 1
|
|
1740
1871
|
|
|
1872
|
+
# Reasoning blocks still accumulating, keyed by thought_id. A block reserves
|
|
1873
|
+
# its rank when it opens but only becomes a row once its text is complete.
|
|
1874
|
+
open_thoughts: dict[str, _OpenThought] = {}
|
|
1875
|
+
|
|
1876
|
+
def _thought_row(
|
|
1877
|
+
block: _OpenThought,
|
|
1878
|
+
*,
|
|
1879
|
+
thought_id: str,
|
|
1880
|
+
conclusion: str | None = None,
|
|
1881
|
+
duration_ms: int | None = None,
|
|
1882
|
+
) -> ChatMessage:
|
|
1883
|
+
"""
|
|
1884
|
+
Build one ``Channel.thought`` row from an accumulated reasoning block.
|
|
1885
|
+
|
|
1886
|
+
The envelope must match what the live stream emits client-side
|
|
1887
|
+
(`useChatSse.ts`) — same role, same channel, same ``metadata.extras``
|
|
1888
|
+
keys — because the chat UI feeds streamed and reloaded messages through
|
|
1889
|
+
a single rendering path (`traceUtils.thoughtExtras`).
|
|
1890
|
+
|
|
1891
|
+
``streaming_delta`` is deliberately absent: a persisted block is
|
|
1892
|
+
complete, and carrying that flag would render it as still running.
|
|
1893
|
+
"""
|
|
1894
|
+
return ChatMessage(
|
|
1895
|
+
session_id=session_id,
|
|
1896
|
+
exchange_id=exchange_id,
|
|
1897
|
+
rank=block.rank,
|
|
1898
|
+
timestamp=datetime.now(timezone.utc),
|
|
1899
|
+
role=Role.assistant,
|
|
1900
|
+
channel=Channel.thought,
|
|
1901
|
+
parts=[TextPart(text="".join(block.text))],
|
|
1902
|
+
# `extras` is not a declared field: `ChatMetadata` sets
|
|
1903
|
+
# `extra="allow"` precisely so callers can attach structured payloads
|
|
1904
|
+
# without every consumer of the model growing a dependency on them.
|
|
1905
|
+
# `model_validate` is how that is reached type-safely — a keyword
|
|
1906
|
+
# argument would not type-check against the declared fields.
|
|
1907
|
+
metadata=ChatMetadata.model_validate(
|
|
1908
|
+
{
|
|
1909
|
+
"extras": {
|
|
1910
|
+
"thought_id": thought_id,
|
|
1911
|
+
"phase": block.phase,
|
|
1912
|
+
"title": block.title,
|
|
1913
|
+
"source": block.source,
|
|
1914
|
+
"conclusion": conclusion,
|
|
1915
|
+
"duration_ms": duration_ms,
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
),
|
|
1919
|
+
)
|
|
1920
|
+
|
|
1741
1921
|
# 2. Map runtime events to messages
|
|
1742
1922
|
final_content = ""
|
|
1743
1923
|
final_sources: list[VectorSearchHit] = []
|
|
@@ -1775,6 +1955,48 @@ async def _write_turn_history(
|
|
|
1775
1955
|
)
|
|
1776
1956
|
rank += 1
|
|
1777
1957
|
|
|
1958
|
+
elif kind == "thought_start":
|
|
1959
|
+
# `tool_use` blocks are synthetic bookkeeping the runtime opens around
|
|
1960
|
+
# every tool call (RFC Amendment A, Layer 1). The chat UI has hidden
|
|
1961
|
+
# them since Amendment B — the tool call/result combo row already
|
|
1962
|
+
# carries what ran, how it went and how long it took. Persisting them
|
|
1963
|
+
# would file up to `max_tool_calls_per_turn` content-free rows a turn.
|
|
1964
|
+
if payload.get("phase") == "tool_use":
|
|
1965
|
+
continue
|
|
1966
|
+
thought_id = str(payload.get("thought_id") or "")
|
|
1967
|
+
if not thought_id:
|
|
1968
|
+
continue
|
|
1969
|
+
open_thoughts[thought_id] = _OpenThought(
|
|
1970
|
+
rank=rank,
|
|
1971
|
+
phase=str(payload.get("phase") or "planning"),
|
|
1972
|
+
title=payload.get("title"),
|
|
1973
|
+
source=payload.get("source"),
|
|
1974
|
+
)
|
|
1975
|
+
rank += 1
|
|
1976
|
+
|
|
1977
|
+
elif kind == "thought_delta":
|
|
1978
|
+
block = open_thoughts.get(str(payload.get("thought_id") or ""))
|
|
1979
|
+
if block is not None:
|
|
1980
|
+
block.text.append(str(payload.get("delta") or ""))
|
|
1981
|
+
|
|
1982
|
+
elif kind == "thought_end":
|
|
1983
|
+
thought_id = str(payload.get("thought_id") or "")
|
|
1984
|
+
block = open_thoughts.pop(thought_id, None)
|
|
1985
|
+
# A block with neither text nor conclusion carries nothing to show;
|
|
1986
|
+
# persisting it would render an empty reasoning card on reload. Its
|
|
1987
|
+
# reserved rank is simply left unused — ranks must increase, not be
|
|
1988
|
+
# contiguous.
|
|
1989
|
+
conclusion = payload.get("conclusion")
|
|
1990
|
+
if block is not None and ("".join(block.text) or conclusion):
|
|
1991
|
+
messages.append(
|
|
1992
|
+
_thought_row(
|
|
1993
|
+
block,
|
|
1994
|
+
thought_id=thought_id,
|
|
1995
|
+
conclusion=conclusion,
|
|
1996
|
+
duration_ms=payload.get("duration_ms"),
|
|
1997
|
+
)
|
|
1998
|
+
)
|
|
1999
|
+
|
|
1778
2000
|
elif kind == "awaiting_human":
|
|
1779
2001
|
# Store the full HITL gate definition — question and all choices —
|
|
1780
2002
|
# so audit logs and UI replay have the complete structured record.
|
|
@@ -1833,6 +2055,14 @@ async def _write_turn_history(
|
|
|
1833
2055
|
final_model = payload.get("model_name")
|
|
1834
2056
|
final_finish_reason = payload.get("finish_reason")
|
|
1835
2057
|
|
|
2058
|
+
# 2b. Blocks the stream never closed (truncated turn, crashed node). The live
|
|
2059
|
+
# UI closes them itself on `final` so the accordion does not pulse forever
|
|
2060
|
+
# (`useChatSse.ts`); history keeps the same parity — reasoning the user was
|
|
2061
|
+
# shown must not disappear on reload just because the block was left open.
|
|
2062
|
+
for thought_id, block in open_thoughts.items():
|
|
2063
|
+
if "".join(block.text):
|
|
2064
|
+
messages.append(_thought_row(block, thought_id=thought_id))
|
|
2065
|
+
|
|
1836
2066
|
# 3. Terminal assistant message (from FinalRuntimeEvent)
|
|
1837
2067
|
if final_content or final_model:
|
|
1838
2068
|
messages.append(
|
|
@@ -2401,8 +2631,8 @@ def _build_capability_block(
|
|
|
2401
2631
|
# one. No production capability declares an active `HitlSpec` today,
|
|
2402
2632
|
# so refusing here costs nothing real yet; reconciling Graph's own
|
|
2403
2633
|
# node-level pause/resume with the per-tool HITL gate is real design
|
|
2404
|
-
# work, deferred (
|
|
2405
|
-
# RFC
|
|
2634
|
+
# work, deferred (see AGENT-CAPABILITY-RFC.md §3.9). Refusing loudly
|
|
2635
|
+
# keeps the RFC's "never silently degrade" guarantee intact in the meantime
|
|
2406
2636
|
# — a capability with `HitlSpec`s that silently ran ungated on Graph
|
|
2407
2637
|
# would be exactly the kind of governance gap this platform exists to
|
|
2408
2638
|
# prevent.
|
|
@@ -2472,6 +2702,19 @@ async def _iterate_runtime_event_payloads(
|
|
|
2472
2702
|
ctx = request.context or {}
|
|
2473
2703
|
correlation_id = ctx.get("correlation_id", request_id)
|
|
2474
2704
|
resolved_team_id = team_id or ctx.get("team_id")
|
|
2705
|
+
# kind="model" enforcement (OBSERV-02 v3, AGENT-CAPABILITY-RFC.md §8.7):
|
|
2706
|
+
# computed ONCE per turn, here — never inside model-routing resolution,
|
|
2707
|
+
# which runs multiple times per turn and must never itself make a ReBAC
|
|
2708
|
+
# call. No team context (agent-to-agent invocation) → no restriction,
|
|
2709
|
+
# the same posture _authorize_execution_or_raise already applies to a
|
|
2710
|
+
# teamless request.
|
|
2711
|
+
usable_model_ids: tuple[str, ...] | None = None
|
|
2712
|
+
if resolved_team_id is not None:
|
|
2713
|
+
from ..model_routing import usable_model_capability_ids
|
|
2714
|
+
|
|
2715
|
+
rebac = get_runtime_context().config.rebac_engine
|
|
2716
|
+
ids = await usable_model_capability_ids(rebac, resolved_team_id)
|
|
2717
|
+
usable_model_ids = tuple(ids) if ids is not None else None
|
|
2475
2718
|
execution_action = ctx.get("execution_action") or (
|
|
2476
2719
|
ExecutionGrantAction.RESUME.value
|
|
2477
2720
|
if request.resume_payload is not None
|
|
@@ -2540,11 +2783,55 @@ async def _iterate_runtime_event_payloads(
|
|
|
2540
2783
|
# When the final file is deleted this is absent, so the per-turn runtime
|
|
2541
2784
|
# notice disappears without leaving a checkpointed system message behind.
|
|
2542
2785
|
attachments_markdown=ctx.get("attachments_markdown"),
|
|
2786
|
+
# Team routing policy snapshot (TEAM-ROUTING-POLICY-RFC.md §3/§8):
|
|
2787
|
+
# control-plane resolves it once at prepare-execution and the frontend
|
|
2788
|
+
# forwards it unchanged, same channel as context_prompt_text above — but
|
|
2789
|
+
# it was also silently dropped here, so resolve_team_override in
|
|
2790
|
+
# fred_runtime.model_routing.provider always saw None and no team's
|
|
2791
|
+
# routing policy ever took effect on a real chat turn. ctx already holds
|
|
2792
|
+
# the flat dict from to_legacy_context()'s model_dump(exclude_none=True);
|
|
2793
|
+
# operation_route_rules needs re-validating back into typed rows.
|
|
2794
|
+
chat_default_profile_id=ctx.get("chat_default_profile_id"),
|
|
2795
|
+
operation_route_rules=[
|
|
2796
|
+
TeamOperationRouteRule.model_validate(rule)
|
|
2797
|
+
for rule in (ctx.get("operation_route_rules") or [])
|
|
2798
|
+
]
|
|
2799
|
+
or None,
|
|
2800
|
+
# Platform reasoning activation snapshot (REASON-01,
|
|
2801
|
+
# `MODEL-REASONING-ENABLEMENT-RFC.md` §5.5) — same channel and same
|
|
2802
|
+
# session-prep lifecycle as the two fields above, and forwarded here
|
|
2803
|
+
# for the same hard-won reason: this mapping is field-by-field, so a
|
|
2804
|
+
# field that is not named is silently dropped. Both comments above
|
|
2805
|
+
# record a production bug caused by exactly that. `RoutedChatModelFactory`
|
|
2806
|
+
# strips the reasoning settings for every model absent from this list
|
|
2807
|
+
# (§5.6.2), so dropping it here would pin reasoning permanently OFF and
|
|
2808
|
+
# make the admin toggle decorative — the failure §5.6.2 exists to
|
|
2809
|
+
# prevent, one layer up from where it is enforced.
|
|
2810
|
+
#
|
|
2811
|
+
# Intersected with level 3 (the agent's own switch) rather than taken
|
|
2812
|
+
# as-is, and that is the whole point of doing it HERE: this list rides
|
|
2813
|
+
# the request, `tuning` is resolved server-side from the managed
|
|
2814
|
+
# instance. An agent whose author did not enable reasoning must not
|
|
2815
|
+
# reason whatever the request claims the platform allows — all four
|
|
2816
|
+
# levels must hold (§3), so the ceiling is `level 2 AND level 3`.
|
|
2817
|
+
# Absent tuning (agent-to-agent invocation, no managed instance) means
|
|
2818
|
+
# no author ever enabled it: off, the default everywhere in REASON-01.
|
|
2819
|
+
reasoning_enabled_model_ids=(
|
|
2820
|
+
ctx.get("reasoning_enabled_model_ids")
|
|
2821
|
+
if tuning is not None and tuning.reasoning_enabled
|
|
2822
|
+
else []
|
|
2823
|
+
),
|
|
2824
|
+
# The user's per-question reasoning choice (REASON-01 level 4). Same
|
|
2825
|
+
# trap as every field above: unnamed here means silently dropped. Kept
|
|
2826
|
+
# tri-state on purpose — `ctx.get` yielding None means "the agent never
|
|
2827
|
+
# offered the choice", which is NOT the same as the user answering no.
|
|
2828
|
+
reasoning=ctx.get("reasoning"),
|
|
2543
2829
|
)
|
|
2544
2830
|
|
|
2545
2831
|
binding = BoundRuntimeContext(
|
|
2546
2832
|
runtime_context=runtime_context,
|
|
2547
2833
|
portable_context=portable_context,
|
|
2834
|
+
usable_model_ids=usable_model_ids,
|
|
2548
2835
|
)
|
|
2549
2836
|
|
|
2550
2837
|
services = _build_runtime_services(
|
|
@@ -2944,6 +3231,42 @@ def _build_agent_router(
|
|
|
2944
3231
|
]
|
|
2945
3232
|
)
|
|
2946
3233
|
|
|
3234
|
+
@router.get("/models-catalog")
|
|
3235
|
+
async def get_models_catalog() -> _ModelCatalogResponse:
|
|
3236
|
+
"""
|
|
3237
|
+
Return this pod's routable models, one entry per distinct
|
|
3238
|
+
(provider, name) pair (OBSERV-02 v3, `AGENT-CAPABILITY-RFC.md` §8.7).
|
|
3239
|
+
|
|
3240
|
+
Why this endpoint exists:
|
|
3241
|
+
- control-plane's capability catalog aggregation
|
|
3242
|
+
(`aggregate_capability_catalog`) has no other way to learn what
|
|
3243
|
+
models this pod can route to — `models_catalog.yaml` is loaded
|
|
3244
|
+
only here, for routing, with no prior control-plane consumer.
|
|
3245
|
+
- one entry per (provider, name), not per catalog profile: a model
|
|
3246
|
+
used for both the `chat` and `language` routing capability is one
|
|
3247
|
+
admin enablement decision, not two.
|
|
3248
|
+
|
|
3249
|
+
Re-reads `models_catalog.yaml` fresh on every call (same file
|
|
3250
|
+
`_build_chat_model_factory` loaded once at boot) rather than
|
|
3251
|
+
caching the parsed catalog — this route is polled rarely (admin
|
|
3252
|
+
catalog refresh), never per-turn, so the extra local file read is
|
|
3253
|
+
immaterial and keeps this always consistent with whatever the pod
|
|
3254
|
+
would actually resolve, with no separate cache to go stale.
|
|
3255
|
+
|
|
3256
|
+
How to use it:
|
|
3257
|
+
- call from control-plane's `_model_capabilities_for_source`
|
|
3258
|
+
|
|
3259
|
+
Example:
|
|
3260
|
+
- `GET /fred/agents/v2/agents/models-catalog`
|
|
3261
|
+
"""
|
|
3262
|
+
from ..model_routing import load_model_catalog
|
|
3263
|
+
|
|
3264
|
+
catalog_path = get_runtime_context().config.models_catalog_path
|
|
3265
|
+
if not catalog_path:
|
|
3266
|
+
return _ModelCatalogResponse(models=[])
|
|
3267
|
+
catalog = load_model_catalog(catalog_path)
|
|
3268
|
+
return _ModelCatalogResponse(models=_project_model_catalog_entries(catalog))
|
|
3269
|
+
|
|
2947
3270
|
@router.post("/capabilities/{capability_id}/validate-config")
|
|
2948
3271
|
async def validate_capability_config(
|
|
2949
3272
|
capability_id: str,
|
|
@@ -3884,13 +4207,19 @@ def create_agent_app(
|
|
|
3884
4207
|
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|
3885
4208
|
# Boot order (must be preserved — each step depends on the previous):
|
|
3886
4209
|
# 1. log_setup — formatter/handlers ready for all subsequent logs
|
|
3887
|
-
# 2.
|
|
3888
|
-
#
|
|
3889
|
-
#
|
|
3890
|
-
#
|
|
3891
|
-
#
|
|
3892
|
-
#
|
|
3893
|
-
#
|
|
4210
|
+
# 2. install_gc_diagnostics — independent of everything below; installed
|
|
4211
|
+
# early so SIGUSR1/SIGUSR2 (fred_core.diagnostics) are live for the
|
|
4212
|
+
# pod's whole lifetime, protecting every agent pod (ours and third
|
|
4213
|
+
# parties' alike) against the reference-cycle class of leak found in
|
|
4214
|
+
# ISSUE-010 without any per-pod code
|
|
4215
|
+
# 3. initialize_kpi_writer — needed by bootstrap_observability
|
|
4216
|
+
# 4. initialize_control_plane_client — sync, no network until first call
|
|
4217
|
+
# 5. bootstrap_observability — global tracer + metrics provider
|
|
4218
|
+
# 6. attach_pod_container — container in app.state before any request
|
|
4219
|
+
# 7. initialize_sql — async, may take time
|
|
4220
|
+
# 8. start_metrics_exporter — prometheus thread, after KPI writer exists
|
|
4221
|
+
# 9. start_kpi_tasks — asyncio tasks, after SQL engine is known
|
|
4222
|
+
# 10. set_runtime_context — wires all built parts into the global config
|
|
3894
4223
|
log_setup(
|
|
3895
4224
|
service_name=config.app.name,
|
|
3896
4225
|
log_level=config.app.log_level,
|
|
@@ -3899,8 +4228,10 @@ def create_agent_app(
|
|
|
3899
4228
|
opensearch_config=config.storage.opensearch,
|
|
3900
4229
|
),
|
|
3901
4230
|
)
|
|
4231
|
+
gc_diagnostics = install_gc_diagnostics()
|
|
3902
4232
|
container = build_pod_container(config)
|
|
3903
4233
|
container.initialize_kpi_writer()
|
|
4234
|
+
container.initialize_control_plane_client()
|
|
3904
4235
|
bootstrap_observability(
|
|
3905
4236
|
config.observability, kpi_writer=container.get_kpi_writer()
|
|
3906
4237
|
)
|
|
@@ -3918,7 +4249,11 @@ def create_agent_app(
|
|
|
3918
4249
|
# every execution against OpenFGA; a disabled/Noop engine (dev) means
|
|
3919
4250
|
# identity-only. Safe in all modes — the factory returns a Noop with a
|
|
3920
4251
|
# KeycloackDisabled admin client when user/m2m auth is off.
|
|
3921
|
-
rebac_engine =
|
|
4252
|
+
rebac_engine = (
|
|
4253
|
+
rebac_factory(security, kpi_writer=container.get_kpi_writer())
|
|
4254
|
+
if security is not None
|
|
4255
|
+
else None
|
|
4256
|
+
)
|
|
3922
4257
|
chat_factory = _build_chat_model_factory(config)
|
|
3923
4258
|
await container.initialize_sql()
|
|
3924
4259
|
container.start_metrics_exporter()
|
|
@@ -3939,6 +4274,7 @@ def create_agent_app(
|
|
|
3939
4274
|
checkpointer=checkpointer,
|
|
3940
4275
|
history_store=history_store,
|
|
3941
4276
|
mcp_configuration=config.get_mcp_configuration(),
|
|
4277
|
+
models_catalog_path=config.get_models_catalog_path(),
|
|
3942
4278
|
inprocess_toolkit_factory=build_inprocess_toolkit,
|
|
3943
4279
|
control_plane_url=config.platform.control_plane_url,
|
|
3944
4280
|
rebac_engine=rebac_engine,
|
|
@@ -3961,6 +4297,7 @@ def create_agent_app(
|
|
|
3961
4297
|
list(registry.keys()),
|
|
3962
4298
|
)
|
|
3963
4299
|
yield
|
|
4300
|
+
await gc_diagnostics.stop()
|
|
3964
4301
|
await container.shutdown()
|
|
3965
4302
|
|
|
3966
4303
|
app = FastAPI(
|