fred-runtime 3.3.5__tar.gz → 3.3.7__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.5 → fred_runtime-3.3.7}/PKG-INFO +1 -1
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/__main__.py +0 -1
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/app/agent_app.py +139 -60
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/capabilities/__init__.py +2 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/capabilities/assembly.py +65 -13
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/capabilities/demo.py +28 -31
- fred_runtime-3.3.7/fred_runtime/capabilities/document_access/capability.py +886 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/capabilities/errors.py +12 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/capabilities/mcp.py +12 -2
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/capabilities/registry.py +61 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/common/context_aware_tool.py +6 -2
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/common/kf_base_client.py +20 -1
- fred_runtime-3.3.7/fred_runtime/common/kf_document_client.py +213 -0
- fred_runtime-3.3.7/fred_runtime/common/kf_tag_client.py +186 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/deep/deep_runtime.py +46 -8
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/graph/graph_runtime.py +164 -7
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/integrations/v2_runtime/adapters.py +257 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/react/middleware/tool_observability.py +68 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/react/react_prompting.py +7 -1
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/react/react_runtime.py +11 -7
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/react/react_tool_loop.py +1 -1
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/runtime_context.py +8 -1
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/support/tool_loop.py +39 -3
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime.egg-info/PKG-INFO +1 -1
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime.egg-info/SOURCES.txt +6 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/pyproject.toml +1 -1
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_agent_app.py +482 -16
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_capability_agent_1973.py +169 -18
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_capability_chat_parts_1977.py +5 -2
- fred_runtime-3.3.7/tests/test_capability_document_access_1906.py +1050 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_capability_endpoints_1974.py +119 -1
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_capability_registry_1973.py +108 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_capability_selection_1974.py +4 -0
- fred_runtime-3.3.7/tests/test_capability_tool_return_convention.py +213 -0
- fred_runtime-3.3.7/tests/test_deep_agent_middleware.py +182 -0
- fred_runtime-3.3.7/tests/test_graph_capability_bridge.py +524 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_react_loop_regressions_1972.py +22 -13
- fred_runtime-3.3.7/tests/test_tool_loop_trim.py +105 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_tool_observability_middleware.py +179 -0
- fred_runtime-3.3.5/fred_runtime/capabilities/document_access/capability.py +0 -395
- fred_runtime-3.3.5/tests/test_capability_document_access_1906.py +0 -515
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/README.md +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/__init__.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/app/__init__.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/app/_catalogs.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/app/config.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/app/config_loader.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/app/container.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/app/context.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/app/dependencies.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/app/mcp_config.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/app/observability_factory.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/app/openai_compat_router.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/capabilities/assets.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/capabilities/demo_migrations/env.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/capabilities/demo_migrations/versions/d1e2m0a0b0c0_create_demo_echo_notes.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/capabilities/document_access/__init__.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/capabilities/openapi_dump.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/cli/__init__.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/cli/completion.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/cli/entrypoint.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/cli/history_display.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/cli/kpi_display.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/cli/pod_client.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/cli/repl.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/cli/repl_helpers.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/cli/url_helpers.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/client.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/common/__init__.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/common/kf_fast_text_client.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/common/kf_http_client.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/common/kf_markdown_media_client.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/common/kf_vectorsearch_client.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/common/kf_workspace_client.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/common/mcp_interceptors.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/common/mcp_runtime.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/common/mcp_toolkit.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/common/mcp_utils.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/common/structures.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/common/token_expiry.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/common/tool_node_utils.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/deep/__init__.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/eval/__init__.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/eval/collector.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/graph/__init__.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/integrations/__init__.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/integrations/inprocess_toolkit_registry.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/integrations/kf_vector_search/__init__.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/integrations/kf_vector_search/toolkit.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/integrations/v2_runtime/__init__.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/migrations.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/model_routing/__init__.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/model_routing/catalog.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/model_routing/contracts.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/model_routing/provider.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/model_routing/resolver.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/react/__init__.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/react/middleware/__init__.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/react/middleware/checkpoint_hygiene.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/react/middleware/dynamic_prompt.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/react/middleware/frame.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/react/middleware/hitl.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/react/middleware/model_routing.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/react/middleware/shared.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/react/middleware/tracing_kpi.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/react/react_langchain_adapter.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/react/react_message_codec.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/react/react_model_adapter.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/react/react_stream_adapter.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/react/react_tool_binding.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/react/react_tool_rendering.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/react/react_tool_resolution.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/react/react_tool_utils.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/react/react_tracing.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/runtime_support/__init__.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/runtime_support/checkpoints.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/runtime_support/model_metadata.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/runtime_support/request_context_helpers.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/runtime_support/sql_checkpointer.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/runtime_support/user_token_refresher.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/support/__init__.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/support/filesystem_context.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime/support/thinking.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime.egg-info/dependency_links.txt +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime.egg-info/entry_points.txt +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime.egg-info/requires.txt +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/fred_runtime.egg-info/top_level.txt +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/setup.cfg +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_capability_chat_controls_1976.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_capability_routes_tables_1979.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_client.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_config_loader.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_context.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_context_aware_tool.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_conversational_memory.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_eval_collector.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_eval_trace.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_fred_workspace_fs.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_graph_runtime_invoke_agent.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_graph_runtime_observability.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_history.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_kf_vector_search_toolkit.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_kf_workspace_client.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_knowledge_search_tool_invoker_sources.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_kpi_display.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_mcp_config.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_model_routing.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_openai_compat_router.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_pod_client.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_react_middleware_frame.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_react_prompting.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_react_thinking.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_repl_helpers.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_runtime_context_prompt_injection.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_smoke.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_spike_capability_state_1971.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_sql_checkpointer_owner.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_token_expiry.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_tracing_kpi_content_confinement.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_url_helpers.py +0 -0
- {fred_runtime-3.3.5 → fred_runtime-3.3.7}/tests/test_user_token_refresher.py +0 -0
|
@@ -56,7 +56,6 @@ 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
|
|
62
61
|
from fred_core.kpi.kpi_writer_structures import KPIActor
|
|
@@ -146,8 +145,13 @@ from fred_runtime.runtime_support.checkpoints import load_checkpoint
|
|
|
146
145
|
from ..common.structures import AgentSettingsLike
|
|
147
146
|
from ..integrations.inprocess_toolkit_registry import build_inprocess_toolkit
|
|
148
147
|
from ..integrations.v2_runtime.adapters import (
|
|
148
|
+
AgentConfigAssetsAdapter,
|
|
149
149
|
CompositeToolInvoker,
|
|
150
|
+
DocumentContentAdapter,
|
|
151
|
+
DocumentFolderAdapter,
|
|
150
152
|
DocumentSearchAdapter,
|
|
153
|
+
DocumentSummarizeAdapter,
|
|
154
|
+
DocumentTreeAdapter,
|
|
151
155
|
FredKnowledgeSearchToolInvoker,
|
|
152
156
|
FredMcpToolProvider,
|
|
153
157
|
FredWorkspaceFs,
|
|
@@ -734,6 +738,16 @@ def _build_runtime_services(
|
|
|
734
738
|
binding=binding,
|
|
735
739
|
settings=settings,
|
|
736
740
|
)
|
|
741
|
+
# Companion document-access ports: tree listing + on-demand summarization,
|
|
742
|
+
# same private-binding doctrine as the search adapter.
|
|
743
|
+
document_tree = DocumentTreeAdapter(
|
|
744
|
+
binding=binding,
|
|
745
|
+
settings=settings,
|
|
746
|
+
)
|
|
747
|
+
document_summarize = DocumentSummarizeAdapter(
|
|
748
|
+
binding=binding,
|
|
749
|
+
settings=settings,
|
|
750
|
+
)
|
|
737
751
|
tool_provider = FredMcpToolProvider(
|
|
738
752
|
binding=binding,
|
|
739
753
|
settings=settings,
|
|
@@ -784,6 +798,14 @@ def _build_runtime_services(
|
|
|
784
798
|
checkpointer=runtime_config.checkpointer,
|
|
785
799
|
agent_invoker=agent_invoker,
|
|
786
800
|
document_search=document_search,
|
|
801
|
+
# #1903 capability ports: per-instance config assets (template fetch at
|
|
802
|
+
# tool time), image-document raw fetch, and folder listing. Same
|
|
803
|
+
# private-binding doctrine as document_search.
|
|
804
|
+
agent_assets=AgentConfigAssetsAdapter(binding=binding, settings=settings),
|
|
805
|
+
document_content=DocumentContentAdapter(binding=binding, settings=settings),
|
|
806
|
+
document_folders=DocumentFolderAdapter(binding=binding, settings=settings),
|
|
807
|
+
document_tree=document_tree,
|
|
808
|
+
document_summarize=document_summarize,
|
|
787
809
|
)
|
|
788
810
|
|
|
789
811
|
|
|
@@ -1260,12 +1282,13 @@ async def _authorize_execution_or_raise(
|
|
|
1260
1282
|
- security disabled (no authenticated user) → skip (dev/local).
|
|
1261
1283
|
- ReBAC engine absent or disabled (Noop) → skip (identity-only dev posture);
|
|
1262
1284
|
the C3 profile guarantees an enabled engine in classified deployments.
|
|
1263
|
-
- a personal space (`personal-<uid>`) is
|
|
1264
|
-
|
|
1265
|
-
`
|
|
1266
|
-
|
|
1267
|
-
`
|
|
1268
|
-
|
|
1285
|
+
- a personal space (`personal-<uid>`) is a real ReBAC team object (AUTHZ-08):
|
|
1286
|
+
the owner's `team_editor` tuple self-heals on first touch inside
|
|
1287
|
+
`RebacEngine.check_user_team_permission_or_raise`, and `add_relation`'s
|
|
1288
|
+
write-guard refuses any tuple naming a personal team except that one, so
|
|
1289
|
+
the plain `CAN_READ` check below already authorizes the owner and denies
|
|
1290
|
+
everyone else (another user's space, or the bare `"personal"` alias, for
|
|
1291
|
+
which no tuple is ever provisioned) — no special-casing needed here.
|
|
1269
1292
|
- otherwise require the caller to hold `CAN_READ` on the requested team — the
|
|
1270
1293
|
same relation the control-plane required before it would mint a grant. The
|
|
1271
1294
|
team is caller-supplied but safe: OpenFGA only authorizes teams the user
|
|
@@ -1341,36 +1364,14 @@ async def _authorize_execution_or_raise(
|
|
|
1341
1364
|
agent_instance_id=request.agent_instance_id,
|
|
1342
1365
|
)
|
|
1343
1366
|
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
|
-
)
|
|
1367
|
+
# Personal spaces (AUTHZ-08, supersedes AUTHZ-05 item 8b) are now real
|
|
1368
|
+
# ReBAC team objects: `RebacEngine.check_user_team_permission_or_raise`
|
|
1369
|
+
# self-heals the owner's own `team_editor` tuple on first touch and
|
|
1370
|
+
# `add_relation`'s write-guard refuses any other shape naming a personal
|
|
1371
|
+
# team, so the bare `check_user_team_permission_or_raise` call below
|
|
1372
|
+
# already authorizes the owner and denies everyone else — no special-
|
|
1373
|
+
# casing needed here (the bare "personal" alias also denies normally,
|
|
1374
|
+
# since no tuple is ever provisioned for that literal string).
|
|
1374
1375
|
try:
|
|
1375
1376
|
await rebac.check_user_team_permission_or_raise(
|
|
1376
1377
|
authenticated_user, TeamPermission.CAN_READ, team_id
|
|
@@ -1508,6 +1509,18 @@ async def _authorize_and_resolve(
|
|
|
1508
1509
|
await _enforce_session_ownership(request, authenticated_user, container)
|
|
1509
1510
|
await _authorize_execution_or_raise(request, authenticated_user, container)
|
|
1510
1511
|
internal_req = _to_internal_request(request)
|
|
1512
|
+
# Stamp the trusted service-agent verdict (never the caller-supplied
|
|
1513
|
+
# context) so per-tool-call re-authorization can mirror the bypass
|
|
1514
|
+
# `_authorize_execution_or_raise` already granted above (RFC EVAL-AUTH,
|
|
1515
|
+
# Solution A) instead of re-running a ReBAC check this identity was
|
|
1516
|
+
# never meant to satisfy. Overwritten unconditionally, both ways, so a
|
|
1517
|
+
# caller can't spoof it via a body-supplied `context.is_service_agent`.
|
|
1518
|
+
ctx = dict(internal_req.context or {})
|
|
1519
|
+
if authenticated_user is not None and is_service_agent(authenticated_user):
|
|
1520
|
+
ctx["is_service_agent"] = "true"
|
|
1521
|
+
else:
|
|
1522
|
+
ctx.pop("is_service_agent", None)
|
|
1523
|
+
internal_req.context = ctx
|
|
1511
1524
|
target = await _resolve_agent_instance(
|
|
1512
1525
|
request=internal_req,
|
|
1513
1526
|
registry=registry,
|
|
@@ -1757,6 +1770,7 @@ async def _write_turn_history(
|
|
|
1757
1770
|
call_id=payload["call_id"],
|
|
1758
1771
|
content=payload.get("content", ""),
|
|
1759
1772
|
ok=not payload.get("is_error", False),
|
|
1773
|
+
latency_ms=payload.get("latency_ms"),
|
|
1760
1774
|
)
|
|
1761
1775
|
)
|
|
1762
1776
|
rank += 1
|
|
@@ -2163,6 +2177,7 @@ def _build_capability_save_services(
|
|
|
2163
2177
|
user_id: str,
|
|
2164
2178
|
team_id: str | None,
|
|
2165
2179
|
access_token: str | None,
|
|
2180
|
+
agent_instance_id: str | None = None,
|
|
2166
2181
|
) -> RuntimeServices:
|
|
2167
2182
|
"""
|
|
2168
2183
|
Minimal `RuntimeServices` for one capability save-time validation (#1974).
|
|
@@ -2170,8 +2185,11 @@ def _build_capability_save_services(
|
|
|
2170
2185
|
Why this exists:
|
|
2171
2186
|
- `validate_config` may store uploaded asset binaries through the KF-backed
|
|
2172
2187
|
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
|
-
|
|
2188
|
+
(RFC §3.4, §3.8) — so the save path needs `workspace_fs` plus the
|
|
2189
|
+
per-instance `agent_assets` store (#1903), bound to the saving user's
|
|
2190
|
+
identity/team, but none of the execution-only services
|
|
2191
|
+
- `document_folders` lets an asset-parsing capability resolve author folder
|
|
2192
|
+
strings against the agent's space at save time (#1903 image support)
|
|
2175
2193
|
"""
|
|
2176
2194
|
|
|
2177
2195
|
request_id = str(uuid4())
|
|
@@ -2181,6 +2199,7 @@ def _build_capability_save_services(
|
|
|
2181
2199
|
user_id=user_id,
|
|
2182
2200
|
team_id=team_id,
|
|
2183
2201
|
access_token=access_token,
|
|
2202
|
+
agent_instance_id=agent_instance_id,
|
|
2184
2203
|
),
|
|
2185
2204
|
portable_context=PortableContext(
|
|
2186
2205
|
request_id=request_id,
|
|
@@ -2196,7 +2215,9 @@ def _build_capability_save_services(
|
|
|
2196
2215
|
)
|
|
2197
2216
|
settings = _PodAgentSettings(id=actor, name=actor, team_id=team_id, tuning=None)
|
|
2198
2217
|
return RuntimeServices(
|
|
2199
|
-
workspace_fs=FredWorkspaceFs(binding=binding, settings=settings)
|
|
2218
|
+
workspace_fs=FredWorkspaceFs(binding=binding, settings=settings),
|
|
2219
|
+
agent_assets=AgentConfigAssetsAdapter(binding=binding, settings=settings),
|
|
2220
|
+
document_folders=DocumentFolderAdapter(binding=binding, settings=settings),
|
|
2200
2221
|
)
|
|
2201
2222
|
|
|
2202
2223
|
|
|
@@ -2223,13 +2244,11 @@ def _effective_capability_ids(
|
|
|
2223
2244
|
`default_mcp_servers` ids; a selected id that is a known-but-DISABLED
|
|
2224
2245
|
catalog MCP server is dropped (the live tool provider skips it anyway —
|
|
2225
2246
|
#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.
|
|
2247
|
+
and fails loudly downstream. Resolved identically for ReAct and Graph
|
|
2248
|
+
agents. When the registry is absent the raw selection is returned
|
|
2249
|
+
unfiltered — the caller decides whether that is an error.
|
|
2229
2250
|
"""
|
|
2230
2251
|
|
|
2231
|
-
if not isinstance(definition, ReActAgentDefinition):
|
|
2232
|
-
return []
|
|
2233
2252
|
selected = tuning.selected_capability_ids if tuning is not None else None
|
|
2234
2253
|
if selected is None:
|
|
2235
2254
|
selected = [ref.id for ref in definition.default_mcp_servers]
|
|
@@ -2322,23 +2341,20 @@ def _build_capability_block(
|
|
|
2322
2341
|
Returns None when the agent selects no capabilities.
|
|
2323
2342
|
|
|
2324
2343
|
MCP handling (#1978, #1988): an MCP-server capability delivers its catalog
|
|
2325
|
-
`agent_instructions` as a prompt fragment
|
|
2344
|
+
`agent_instructions` as a prompt fragment via `middleware()`
|
|
2345
|
+
(`_McpInstructionsMiddleware`). The effective selection mirrors
|
|
2326
2346
|
`_active_mcp_server_refs`: a `None` capability selection (template default)
|
|
2327
2347
|
activates the template's `default_mcp_servers` as capabilities so their
|
|
2328
|
-
instructions are delivered — otherwise a default
|
|
2329
|
-
|
|
2348
|
+
instructions are delivered — otherwise a default ReAct agent would silently
|
|
2349
|
+
lose its non-negotiable grounding contract. This block is built identically
|
|
2350
|
+
for both agent kinds (CAPAB-02), but a Graph agent reads only `block.tools`
|
|
2351
|
+
— MCP tools reach it (a separate, already execution-model-agnostic path,
|
|
2352
|
+
`FredMcpToolProvider`), the `agent_instructions` prompt fragment does NOT
|
|
2353
|
+
(Graph never reads `block.middleware` at all). A Graph agent that needs an
|
|
2354
|
+
MCP server's grounding instructions must currently author them into its own
|
|
2355
|
+
system prompt — this is an explicit, known gap, not yet closed (CAPAB-02).
|
|
2330
2356
|
"""
|
|
2331
2357
|
|
|
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
2358
|
selected = tuning.selected_capability_ids if tuning is not None else None
|
|
2343
2359
|
capability_config = tuning.capability_config if tuning is not None else {}
|
|
2344
2360
|
if capability_registry is None:
|
|
@@ -2359,6 +2375,49 @@ def _build_capability_block(
|
|
|
2359
2375
|
effective = _effective_capability_ids(tuning, definition, capability_registry)
|
|
2360
2376
|
if not effective:
|
|
2361
2377
|
return None
|
|
2378
|
+
if isinstance(definition, GraphAgentDefinition):
|
|
2379
|
+
# CAPAB-02: a capability can declare itself ReAct-only
|
|
2380
|
+
# (`CapabilityManifest.execution_models`) when its runtime need is a
|
|
2381
|
+
# `middleware()` hook `tools()` cannot express (e.g. PPT filler's
|
|
2382
|
+
# dynamic per-turn tool schema). Selecting one on a Graph agent must
|
|
2383
|
+
# fail loudly here — never silently contribute zero tools (RFC §3.9
|
|
2384
|
+
# "never silently degrade").
|
|
2385
|
+
react_only = [
|
|
2386
|
+
cap_id
|
|
2387
|
+
for cap_id in effective
|
|
2388
|
+
if cap_id in capability_registry
|
|
2389
|
+
and "graph"
|
|
2390
|
+
not in capability_registry.capability(cap_id).manifest.execution_models
|
|
2391
|
+
]
|
|
2392
|
+
if react_only:
|
|
2393
|
+
raise CapabilityError(
|
|
2394
|
+
f"Agent selects capabilities {react_only} which are ReAct-only "
|
|
2395
|
+
"(CapabilityManifest.execution_models) and cannot run on a "
|
|
2396
|
+
"Graph agent."
|
|
2397
|
+
)
|
|
2398
|
+
# CAPAB-02 stopgap: `CapabilityAgentBlock.hitl` is built (assembly.py)
|
|
2399
|
+
# but `GraphRuntime.invoke_runtime_tool` never consults it — a
|
|
2400
|
+
# capability's `HitlSpec` gates a ReAct tool call but not a Graph
|
|
2401
|
+
# one. No production capability declares an active `HitlSpec` today,
|
|
2402
|
+
# so refusing here costs nothing real yet; reconciling Graph's own
|
|
2403
|
+
# node-level pause/resume with the per-tool HITL gate is real design
|
|
2404
|
+
# work, deferred (id-legend.yaml CAPAB-02). Refusing loudly keeps the
|
|
2405
|
+
# RFC §3.9 "never silently degrade" guarantee intact in the meantime
|
|
2406
|
+
# — a capability with `HitlSpec`s that silently ran ungated on Graph
|
|
2407
|
+
# would be exactly the kind of governance gap this platform exists to
|
|
2408
|
+
# prevent.
|
|
2409
|
+
hitl_gated = [
|
|
2410
|
+
cap_id
|
|
2411
|
+
for cap_id in effective
|
|
2412
|
+
if cap_id in capability_registry
|
|
2413
|
+
and capability_registry.capability(cap_id).hitl_specs()
|
|
2414
|
+
]
|
|
2415
|
+
if hitl_gated:
|
|
2416
|
+
raise CapabilityError(
|
|
2417
|
+
f"Agent selects capabilities {hitl_gated} which declare "
|
|
2418
|
+
"HitlSpec approval gates; Graph agents do not yet enforce "
|
|
2419
|
+
"capability HITL (CAPAB-02) and cannot run them."
|
|
2420
|
+
)
|
|
2362
2421
|
contexts = build_capability_contexts(
|
|
2363
2422
|
capability_registry,
|
|
2364
2423
|
selected_capability_ids=effective,
|
|
@@ -2440,6 +2499,7 @@ async def _iterate_runtime_event_payloads(
|
|
|
2440
2499
|
"checkpoint_id": resolved_checkpoint_id,
|
|
2441
2500
|
"execution_action": execution_action,
|
|
2442
2501
|
"exchange_id": exchange_id,
|
|
2502
|
+
"is_service_agent": ctx.get("is_service_agent"),
|
|
2443
2503
|
}.items()
|
|
2444
2504
|
if isinstance(value, str) and value
|
|
2445
2505
|
},
|
|
@@ -2540,6 +2600,7 @@ async def _iterate_runtime_event_payloads(
|
|
|
2540
2600
|
runtime = GraphRuntime(
|
|
2541
2601
|
definition=definition,
|
|
2542
2602
|
services=services,
|
|
2603
|
+
capability_block=capability_block,
|
|
2543
2604
|
)
|
|
2544
2605
|
runtime.bind(binding)
|
|
2545
2606
|
await runtime.activate()
|
|
@@ -2806,7 +2867,7 @@ def _build_agent_router(
|
|
|
2806
2867
|
"""
|
|
2807
2868
|
|
|
2808
2869
|
capability_registry = _capability_registry_of(http_request)
|
|
2809
|
-
|
|
2870
|
+
all_capability_entries = (
|
|
2810
2871
|
[
|
|
2811
2872
|
CapabilityCatalogEntry.from_manifest(
|
|
2812
2873
|
capability_registry.capability(cap_id).manifest,
|
|
@@ -2826,7 +2887,21 @@ def _build_agent_router(
|
|
|
2826
2887
|
kind=definition.execution_category,
|
|
2827
2888
|
default_tuning=_definition_to_agent_tuning(definition),
|
|
2828
2889
|
available_mcp_servers=_available_mcp_servers_for_definition(definition),
|
|
2829
|
-
|
|
2890
|
+
# CAPAB-02: a capability declared `execution_models=("react",)`
|
|
2891
|
+
# must not even be offered for selection on a Graph template —
|
|
2892
|
+
# `_build_capability_block` would refuse it loudly at save/run
|
|
2893
|
+
# time regardless, but a picker that lists it first invites the
|
|
2894
|
+
# exact "select it, save it, discover the incompatibility at
|
|
2895
|
+
# first launch" flow the loud refusal exists to prevent.
|
|
2896
|
+
available_capabilities=(
|
|
2897
|
+
all_capability_entries
|
|
2898
|
+
if not isinstance(definition, GraphAgentDefinition)
|
|
2899
|
+
else [
|
|
2900
|
+
entry
|
|
2901
|
+
for entry in all_capability_entries
|
|
2902
|
+
if "graph" in entry.execution_models
|
|
2903
|
+
]
|
|
2904
|
+
),
|
|
2830
2905
|
default_capability_ids=[
|
|
2831
2906
|
ref.id for ref in definition.default_mcp_servers
|
|
2832
2907
|
],
|
|
@@ -2979,6 +3054,10 @@ def _build_agent_router(
|
|
|
2979
3054
|
user_id=(caller.uid if caller is not None else None) or "anonymous",
|
|
2980
3055
|
team_id=team_id or None,
|
|
2981
3056
|
access_token=access_token,
|
|
3057
|
+
agent_instance_id=(
|
|
3058
|
+
form_instance_id if isinstance(form_instance_id, str) else None
|
|
3059
|
+
)
|
|
3060
|
+
or None,
|
|
2982
3061
|
),
|
|
2983
3062
|
)
|
|
2984
3063
|
try:
|
|
@@ -55,6 +55,7 @@ from .errors import (
|
|
|
55
55
|
DuplicateChatPartKindError,
|
|
56
56
|
MissingRequiredEnvError,
|
|
57
57
|
TurnOptionsInvalidError,
|
|
58
|
+
InvalidExecutionModelError,
|
|
58
59
|
UnknownCapabilityError,
|
|
59
60
|
)
|
|
60
61
|
from .mcp import (
|
|
@@ -90,6 +91,7 @@ __all__ = [
|
|
|
90
91
|
"DuplicateChatPartKindError",
|
|
91
92
|
"MissingRequiredEnvError",
|
|
92
93
|
"TurnOptionsInvalidError",
|
|
94
|
+
"InvalidExecutionModelError",
|
|
93
95
|
"UnknownCapabilityError",
|
|
94
96
|
"boot_capability_registry",
|
|
95
97
|
"build_capability_agent_block",
|
|
@@ -24,6 +24,10 @@ Why this module exists:
|
|
|
24
24
|
change behavior), authored order preserved within one capability's stack
|
|
25
25
|
- capability `HitlSpec`s never become middleware; they are collected here
|
|
26
26
|
into `CapabilityHitlBinding`s for the single `FredHitlMiddleware` gate
|
|
27
|
+
- `capability.tools(ctx)` — the primary, execution-model-agnostic authoring
|
|
28
|
+
surface (fred-sdk `AgentCapability.tools`) — is read once per selected
|
|
29
|
+
capability into one deduped-by-name map: `block.tools` for Graph agents
|
|
30
|
+
(bridge, Phase 4) and HITL-tool resolution below both read that one map
|
|
27
31
|
|
|
28
32
|
How to use:
|
|
29
33
|
- per selected capability, build its typed context with
|
|
@@ -31,7 +35,8 @@ How to use:
|
|
|
31
35
|
are validated against the capability's own models — the RFC §3.5/§3.8
|
|
32
36
|
slice-validation pattern)
|
|
33
37
|
- then `build_capability_agent_block(registry, contexts)` and pass
|
|
34
|
-
`block.middleware` / `block.hitl` into the ReAct loop builder
|
|
38
|
+
`block.middleware` / `block.hitl` into the ReAct loop builder; `block.tools`
|
|
39
|
+
is the plain tool objects, execution-model-agnostic
|
|
35
40
|
"""
|
|
36
41
|
|
|
37
42
|
from __future__ import annotations
|
|
@@ -49,9 +54,11 @@ from fred_sdk.contracts.capability import (
|
|
|
49
54
|
ChatControlsResponse,
|
|
50
55
|
ChatControlsResult,
|
|
51
56
|
StoredCapabilityConfig,
|
|
57
|
+
ToolCarrierMiddleware,
|
|
52
58
|
)
|
|
53
59
|
from fred_sdk.contracts.runtime import RuntimeServices
|
|
54
60
|
from langchain.agents.middleware import AgentMiddleware
|
|
61
|
+
from langchain_core.tools import BaseTool
|
|
55
62
|
from pydantic import BaseModel, ValidationError
|
|
56
63
|
|
|
57
64
|
from fred_runtime.react.middleware.hitl import CapabilityHitlBinding
|
|
@@ -312,6 +319,7 @@ class CapabilityAgentBlock:
|
|
|
312
319
|
|
|
313
320
|
middleware: tuple[AgentMiddleware, ...]
|
|
314
321
|
hitl: Mapping[str, CapabilityHitlBinding]
|
|
322
|
+
tools: tuple[BaseTool, ...]
|
|
315
323
|
|
|
316
324
|
|
|
317
325
|
def build_capability_agent_block(
|
|
@@ -324,26 +332,68 @@ def build_capability_agent_block(
|
|
|
324
332
|
Determinism rule (RFC §5.3): iteration is `sorted(capability id)` — never
|
|
325
333
|
selection order, never registration order — and each capability's
|
|
326
334
|
authored middleware list order is preserved within its block.
|
|
335
|
+
|
|
336
|
+
`tools()` (not `middleware()`) is the single source of truth for a
|
|
337
|
+
capability's tool objects (RFC §3.2, §5): it is read directly, EXACTLY
|
|
338
|
+
once per selected capability (CAPAB-02), into one deduped-by-name map
|
|
339
|
+
shared by three consumers — the ReAct middleware binding (via
|
|
340
|
+
`ToolCarrierMiddleware`, built directly from these same instances), the
|
|
341
|
+
`HitlSpec.tool` resolution below, and the `tools` field Graph agents
|
|
342
|
+
consume. One call means one set of tool objects: no risk of a
|
|
343
|
+
stateful/non-deterministic `tools()` implementation producing a different
|
|
344
|
+
instance for the ReAct binding than the one `HitlGateRequest.tool`
|
|
345
|
+
exposes to a `when` predicate.
|
|
346
|
+
|
|
347
|
+
`tools()` and `middleware()` compose, they are not either/or: a
|
|
348
|
+
`ToolCarrierMiddleware` is added whenever `tools()` returns anything,
|
|
349
|
+
AND, separately, an overridden `middleware()` is always also called (for
|
|
350
|
+
the genuine ReAct-only hook `tools()` cannot express — RFC §3.2's
|
|
351
|
+
"implement `tools()` too; do not fold the tools into the middleware
|
|
352
|
+
override" case). Only the DEFAULT `middleware()` is skipped — calling it
|
|
353
|
+
would just rebuild the same `ToolCarrierMiddleware` from a second,
|
|
354
|
+
redundant `tools(ctx)` call, exactly what the single-call rule above
|
|
355
|
+
prevents. Two DIFFERENT capabilities exposing the same tool name is a
|
|
356
|
+
collision this module can see (both contexts are in hand right here) and
|
|
357
|
+
never silently shadows, matching the HITL-ownership collision below.
|
|
327
358
|
"""
|
|
328
359
|
|
|
329
360
|
middleware: list[AgentMiddleware] = []
|
|
330
361
|
hitl: dict[str, CapabilityHitlBinding] = {}
|
|
362
|
+
tools_by_name: dict[str, BaseTool] = {}
|
|
363
|
+
tool_owner: dict[str, str] = {}
|
|
331
364
|
for cap_id in sorted(contexts):
|
|
332
365
|
capability = registry.capability(cap_id)
|
|
333
366
|
ctx = contexts[cap_id]
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
367
|
+
capability_tools = tuple(capability.tools(ctx))
|
|
368
|
+
# These two are NOT mutually exclusive: a capability can implement
|
|
369
|
+
# `tools()` for its plain tools AND override `middleware()` for a
|
|
370
|
+
# genuine ReAct-only hook (RFC §3.2 — "implement tools() too; do not
|
|
371
|
+
# fold the tools into the middleware override"). An earlier version
|
|
372
|
+
# of this loop treated them as either/or, which silently dropped a
|
|
373
|
+
# combining capability's tools() output from the ReAct binding
|
|
374
|
+
# (block.tools/HITL still saw it, but create_agent() never did).
|
|
375
|
+
if capability_tools:
|
|
376
|
+
middleware.append(ToolCarrierMiddleware(capability_tools))
|
|
377
|
+
if type(capability).middleware is not AgentCapability.middleware:
|
|
378
|
+
# Overridden: call it too, for the hook tools() cannot express.
|
|
379
|
+
# No double-computation risk — the default middleware() (which
|
|
380
|
+
# would call tools(ctx) again internally) is never reached here.
|
|
381
|
+
middleware.extend(capability.middleware(ctx))
|
|
382
|
+
|
|
383
|
+
for candidate in capability_tools:
|
|
384
|
+
owner = tool_owner.get(candidate.name)
|
|
385
|
+
if owner is not None and owner != cap_id:
|
|
386
|
+
raise CapabilityAssemblyError(
|
|
387
|
+
f"Tool '{candidate.name}' is exposed by two capabilities "
|
|
388
|
+
f"('{owner}' and '{cap_id}'). Capability tools must be "
|
|
389
|
+
"uniquely named."
|
|
390
|
+
)
|
|
391
|
+
tools_by_name[candidate.name] = candidate
|
|
392
|
+
tool_owner[candidate.name] = cap_id
|
|
343
393
|
|
|
344
394
|
for spec in capability.hitl_specs():
|
|
345
|
-
|
|
346
|
-
if
|
|
395
|
+
owner_binding = hitl.get(spec.tool)
|
|
396
|
+
if owner_binding is not None:
|
|
347
397
|
raise CapabilityAssemblyError(
|
|
348
398
|
f"Tool '{spec.tool}' has HitlSpec declarations from two "
|
|
349
399
|
f"capabilities (capability '{cap_id}' collides with an "
|
|
@@ -354,4 +404,6 @@ def build_capability_agent_block(
|
|
|
354
404
|
context=ctx,
|
|
355
405
|
tool=tools_by_name.get(spec.tool),
|
|
356
406
|
)
|
|
357
|
-
return CapabilityAgentBlock(
|
|
407
|
+
return CapabilityAgentBlock(
|
|
408
|
+
middleware=tuple(middleware), hitl=hitl, tools=tuple(tools_by_name.values())
|
|
409
|
+
)
|
|
@@ -47,7 +47,6 @@ from fred_sdk.contracts.capability import (
|
|
|
47
47
|
)
|
|
48
48
|
from fred_sdk.contracts.context import ToolInvocationResult, UiPart
|
|
49
49
|
from fred_sdk.contracts.models import FieldSpec
|
|
50
|
-
from langchain.agents.middleware import AgentMiddleware
|
|
51
50
|
from langchain_core.tools import BaseTool, tool
|
|
52
51
|
from pydantic import BaseModel
|
|
53
52
|
from sqlalchemy import Boolean, DateTime, String
|
|
@@ -150,33 +149,6 @@ class DemoCardPart(BaseModel):
|
|
|
150
149
|
body: str = ""
|
|
151
150
|
|
|
152
151
|
|
|
153
|
-
class _DemoEchoMiddleware(AgentMiddleware):
|
|
154
|
-
"""Carries the `demo_echo` tool, bound to the instance's typed config."""
|
|
155
|
-
|
|
156
|
-
def __init__(self, ctx: CapabilityContext[DemoEchoConfig, EmptyModel]) -> None:
|
|
157
|
-
super().__init__()
|
|
158
|
-
config = ctx.config
|
|
159
|
-
|
|
160
|
-
@tool(response_format="content_and_artifact")
|
|
161
|
-
def demo_echo(text: str) -> tuple[str, ToolInvocationResult]:
|
|
162
|
-
"""Echo the given text back to the conversation."""
|
|
163
|
-
|
|
164
|
-
content = text.upper() if config.uppercase else text
|
|
165
|
-
# The artifact carries the capability's chat part; the runtime
|
|
166
|
-
# merges `ui_parts` onto the tool_result/final events (#1977).
|
|
167
|
-
# The cast is the reference pattern for capability parts: the
|
|
168
|
-
# static `UiPart` alias is the frozen base union, while the
|
|
169
|
-
# registry extends the RUNTIME union with this part at boot.
|
|
170
|
-
artifact = ToolInvocationResult(
|
|
171
|
-
tool_ref="demo_echo",
|
|
172
|
-
ui_parts=(cast(UiPart, DemoCardPart(title="Demo echo", body=content)),),
|
|
173
|
-
)
|
|
174
|
-
return content, artifact
|
|
175
|
-
|
|
176
|
-
tools: Sequence[BaseTool] = [demo_echo]
|
|
177
|
-
self.tools = tools
|
|
178
|
-
|
|
179
|
-
|
|
180
152
|
class DemoEchoCapability(AgentCapability[DemoEchoConfig, DemoEchoConfig, EmptyModel]):
|
|
181
153
|
"""One static tool + one scalar config field (RFC §3, tracer for #1973)."""
|
|
182
154
|
|
|
@@ -211,7 +183,32 @@ class DemoEchoCapability(AgentCapability[DemoEchoConfig, DemoEchoConfig, EmptyMo
|
|
|
211
183
|
|
|
212
184
|
return str(Path(__file__).resolve().parent / "demo_migrations")
|
|
213
185
|
|
|
214
|
-
def
|
|
186
|
+
def tools(
|
|
215
187
|
self, ctx: CapabilityContext[DemoEchoConfig, EmptyModel]
|
|
216
|
-
) ->
|
|
217
|
-
|
|
188
|
+
) -> Sequence[BaseTool]:
|
|
189
|
+
"""
|
|
190
|
+
The `demo_echo` tool, bound to the instance's typed config. This is
|
|
191
|
+
the capability's ONLY runtime contribution — `AgentCapability.middleware()`'s
|
|
192
|
+
default wraps this for `create_agent()`; no ReAct-loop-specific hook
|
|
193
|
+
is needed (mirrors `DocumentAccessCapability.tools`).
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
config = ctx.config
|
|
197
|
+
|
|
198
|
+
@tool(response_format="content_and_artifact")
|
|
199
|
+
async def demo_echo(text: str) -> tuple[str, ToolInvocationResult]:
|
|
200
|
+
"""Echo the given text back to the conversation."""
|
|
201
|
+
|
|
202
|
+
content = text.upper() if config.uppercase else text
|
|
203
|
+
# The artifact carries the capability's chat part; the runtime
|
|
204
|
+
# merges `ui_parts` onto the tool_result/final events (#1977).
|
|
205
|
+
# The cast is the reference pattern for capability parts: the
|
|
206
|
+
# static `UiPart` alias is the frozen base union, while the
|
|
207
|
+
# registry extends the RUNTIME union with this part at boot.
|
|
208
|
+
artifact = ToolInvocationResult(
|
|
209
|
+
tool_ref="demo_echo",
|
|
210
|
+
ui_parts=(cast(UiPart, DemoCardPart(title="Demo echo", body=content)),),
|
|
211
|
+
)
|
|
212
|
+
return content, artifact
|
|
213
|
+
|
|
214
|
+
return [demo_echo]
|